@devicai/ui 0.38.1 → 0.40.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 (49) hide show
  1. package/README.md +48 -4
  2. package/dist/cjs/components/ChatDrawer/ChatDrawer.js +29 -13
  3. package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
  4. package/dist/cjs/components/ChatDrawer/ChatInput.js +2 -2
  5. package/dist/cjs/components/ChatDrawer/ChatInput.js.map +1 -1
  6. package/dist/cjs/components/IntegrationsModal/IntegrationLogo.js +23 -0
  7. package/dist/cjs/components/IntegrationsModal/IntegrationLogo.js.map +1 -0
  8. package/dist/cjs/components/IntegrationsModal/IntegrationsHint.js +152 -0
  9. package/dist/cjs/components/IntegrationsModal/IntegrationsHint.js.map +1 -0
  10. package/dist/cjs/components/IntegrationsModal/IntegrationsLauncher.js +68 -0
  11. package/dist/cjs/components/IntegrationsModal/IntegrationsLauncher.js.map +1 -0
  12. package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js +78 -78
  13. package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
  14. package/dist/cjs/components/IntegrationsModal/useIntegrations.js +106 -0
  15. package/dist/cjs/components/IntegrationsModal/useIntegrations.js.map +1 -0
  16. package/dist/cjs/components/theme.js +40 -0
  17. package/dist/cjs/components/theme.js.map +1 -1
  18. package/dist/cjs/index.js +9 -0
  19. package/dist/cjs/index.js.map +1 -1
  20. package/dist/cjs/styles.css +1 -1
  21. package/dist/esm/components/ChatDrawer/ChatDrawer.js +29 -13
  22. package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
  23. package/dist/esm/components/ChatDrawer/ChatDrawer.types.d.ts +34 -7
  24. package/dist/esm/components/ChatDrawer/ChatInput.js +2 -2
  25. package/dist/esm/components/ChatDrawer/ChatInput.js.map +1 -1
  26. package/dist/esm/components/IntegrationsModal/IntegrationLogo.d.ts +16 -0
  27. package/dist/esm/components/IntegrationsModal/IntegrationLogo.js +21 -0
  28. package/dist/esm/components/IntegrationsModal/IntegrationLogo.js.map +1 -0
  29. package/dist/esm/components/IntegrationsModal/IntegrationsHint.d.ts +39 -0
  30. package/dist/esm/components/IntegrationsModal/IntegrationsHint.js +150 -0
  31. package/dist/esm/components/IntegrationsModal/IntegrationsHint.js.map +1 -0
  32. package/dist/esm/components/IntegrationsModal/IntegrationsLauncher.d.ts +31 -0
  33. package/dist/esm/components/IntegrationsModal/IntegrationsLauncher.js +65 -0
  34. package/dist/esm/components/IntegrationsModal/IntegrationsLauncher.js.map +1 -0
  35. package/dist/esm/components/IntegrationsModal/IntegrationsModal.d.ts +10 -1
  36. package/dist/esm/components/IntegrationsModal/IntegrationsModal.js +78 -78
  37. package/dist/esm/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
  38. package/dist/esm/components/IntegrationsModal/index.d.ts +8 -0
  39. package/dist/esm/components/IntegrationsModal/useIntegrations.d.ts +49 -0
  40. package/dist/esm/components/IntegrationsModal/useIntegrations.js +104 -0
  41. package/dist/esm/components/IntegrationsModal/useIntegrations.js.map +1 -0
  42. package/dist/esm/components/theme.d.ts +9 -0
  43. package/dist/esm/components/theme.js +40 -1
  44. package/dist/esm/components/theme.js.map +1 -1
  45. package/dist/esm/index.d.ts +3 -3
  46. package/dist/esm/index.js +5 -1
  47. package/dist/esm/index.js.map +1 -1
  48. package/dist/esm/styles.css +1 -1
  49. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IntegrationsHint.js","sources":["../../../../src/components/IntegrationsModal/IntegrationsHint.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type JSX,\n} from \"react\";\nimport type { Integration } from \"../../api/types\";\nimport { IntegrationLogo } from \"./IntegrationLogo\";\nimport type { IntegrationsState } from \"./useIntegrations\";\nimport \"./IntegrationsModal.css\";\n\nexport interface IntegrationsHintProps {\n /** Shared listing (see `useIntegrations`). */\n state: IntegrationsState;\n /** Opens the connected-apps modal. */\n onOpen: () => void;\n /**\n * Text on the left. Defaults to \"Connect your apps\", or \"Explore connected\n * apps\" once at least one is connected — an invitation the user has already\n * accepted stops being an invitation.\n */\n label?: string;\n /** Most logos to show before the `+N` box. @default 6 */\n maxLogos?: number;\n /**\n * Identity the dismissal is remembered against, so closing it for one end\n * user does not close it for the next one on the same browser.\n */\n storageKey?: string;\n /** Where the bar flies to when closed — the header control. */\n flyToSelector?: string;\n /** Light chips for a dark surface. */\n dark?: boolean;\n className?: string;\n}\n\nconst STORAGE_PREFIX = \"devic:apps-hint-dismissed:\";\n\n/** Reading storage throws in Safari's private mode and in sandboxed frames. */\nfunction wasDismissed(key?: string): boolean {\n if (!key) return false;\n try {\n return localStorage.getItem(STORAGE_PREFIX + key) === \"1\";\n } catch {\n return false;\n }\n}\n\nfunction rememberDismissal(key?: string): void {\n if (!key) return;\n try {\n localStorage.setItem(STORAGE_PREFIX + key, \"1\");\n } catch {\n // Then it comes back next session. Not worth a word to the user.\n }\n}\n\n/** Connected first, same order as the header control. */\nfunction order(integrations: Integration[]): Integration[] {\n return [...integrations].sort((a, b) => {\n if (a.connected !== b.connected) return a.connected ? -1 : 1;\n return 0;\n });\n}\n\n/**\n * How many 24px chips fit once the label and the close button are paid for.\n *\n * The label is the whole reason this row exists, so it gets its space first:\n * a strip reading \"Connect your …\" beside seven logos has lost the argument.\n * Never fewer than three chips, below which the row stops reading as a set of\n * apps at all.\n */\nfunction boxesThatFit(width: number, max: number): number {\n if (!width) return max + 1;\n const LABEL_AND_CLOSE = 195;\n return Math.max(3, Math.min(max + 1, Math.floor((width - LABEL_AND_CLOSE) / 30)));\n}\n\n/**\n * The strip above the composer that tells the end user their own apps can be\n * connected here.\n *\n * It exists because the header control is a row of small logos and nothing\n * else: discoverable once you know what it is, invisible until then. This says\n * it in words, once, and then gets out of the way for good — the dismissal is\n * remembered per end user, and closing it animates towards the header so that\n * what was dismissed is understood to still be there.\n */\nexport function IntegrationsHint({\n state,\n onOpen,\n label,\n maxLogos = 6,\n storageKey,\n flyToSelector = \".devic-int-launcher\",\n dark = false,\n className = \"\",\n}: IntegrationsHintProps): JSX.Element | null {\n const sorted = useMemo(() => order(state.integrations), [state.integrations]);\n const ref = useRef<HTMLDivElement>(null);\n const [boxes, setBoxes] = useState(maxLogos + 1);\n const [dismissed, setDismissed] = useState(() => wasDismissed(storageKey));\n const [leaving, setLeaving] = useState(false);\n\n // A different end user has their own answer to whether this was dismissed.\n useEffect(() => {\n setDismissed(wasDismissed(storageKey));\n setLeaving(false);\n }, [storageKey]);\n\n useEffect(() => {\n const el = ref.current;\n if (!el || typeof ResizeObserver === \"undefined\") return;\n const measure = () =>\n setBoxes(boxesThatFit(el.getBoundingClientRect().width, maxLogos));\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(el);\n return () => observer.disconnect();\n }, [maxLogos, state.offered, dismissed]);\n\n /**\n * Sends a copy of the logos from the bar to the header control.\n *\n * A dismissal that simply blanks the row reads as \"that is gone now\". The\n * flight is the sentence \"it moved up there\" said without words — so if it\n * cannot be drawn (reduced motion, no target, no Web Animations API), the bar\n * just closes and nothing is lost.\n */\n const flyToLauncher = useCallback(() => {\n const bar = ref.current;\n if (!bar || typeof document === \"undefined\") return;\n if (window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches) return;\n\n // Scoped to the drawer this bar lives in: a page with two drawers has two\n // launchers, and the copy must fly to its own.\n const root = bar.closest(\".devic-drawer\") ?? document;\n const target = flyToSelector\n ? (root.querySelector(flyToSelector) as HTMLElement | null)\n : null;\n if (!target) return;\n\n const logos = bar.querySelector(\".devic-int-hint-logos\");\n const from = (logos ?? bar).getBoundingClientRect();\n const to = target.getBoundingClientRect();\n\n const ghost = (logos ?? bar).cloneNode(true) as HTMLElement;\n ghost.classList.add(\"devic-int-hint-ghost\");\n ghost.style.left = `${from.left}px`;\n ghost.style.top = `${from.top}px`;\n ghost.style.width = `${from.width}px`;\n ghost.style.height = `${from.height}px`;\n document.body.appendChild(ghost);\n\n const dx = to.left + to.width / 2 - (from.left + from.width / 2);\n const dy = to.top + to.height / 2 - (from.top + from.height / 2);\n const scale = from.width ? Math.max(0.4, to.width / from.width) : 0.6;\n\n const animation = ghost.animate?.(\n [\n { transform: \"translate(0, 0) scale(1)\", opacity: 0.95 },\n {\n transform: `translate(${dx}px, ${dy}px) scale(${scale})`,\n opacity: 0,\n },\n ],\n { duration: 420, easing: \"cubic-bezier(0.4, 0, 0.2, 1)\" }\n );\n\n const remove = () => ghost.remove();\n if (animation) animation.onfinish = remove;\n else remove();\n // Belt and braces: a tab backgrounded mid-flight never fires onfinish.\n window.setTimeout(remove, 1200);\n }, [flyToSelector]);\n\n const dismiss = () => {\n flyToLauncher();\n rememberDismissal(storageKey);\n setLeaving(true);\n window.setTimeout(() => setDismissed(true), 220);\n };\n\n if (dismissed || !state.offered || sorted.length === 0) return null;\n\n const text =\n label ??\n (sorted.some((i) => i.connected)\n ? \"Explore connected apps\"\n : \"Connect your apps\");\n\n // When some apps will not fit, the `+N` box takes one of the slots itself.\n const capacity = Math.max(1, boxes);\n const shown = sorted.slice(\n 0,\n sorted.length > capacity ? capacity - 1 : capacity\n );\n const extra = sorted.length - shown.length;\n\n return (\n <div\n ref={ref}\n className={`devic-int-hint ${className}`.trim()}\n data-dark={dark}\n data-leaving={leaving}\n >\n <button\n type=\"button\"\n className=\"devic-int-hint-main\"\n onClick={onOpen}\n title={text}\n >\n <span className=\"devic-int-hint-label\">{text}</span>\n <span className=\"devic-int-hint-logos\" aria-hidden=\"true\">\n {shown.map((integration) => (\n <span\n key={integration.app}\n className=\"devic-int-hint-item\"\n data-connected={integration.connected}\n >\n <IntegrationLogo\n integration={integration}\n className=\"devic-int-hint-logo\"\n />\n </span>\n ))}\n {extra > 0 && (\n <span className=\"devic-int-hint-item devic-int-hint-more\">\n +{extra}\n </span>\n )}\n </span>\n </button>\n <button\n type=\"button\"\n className=\"devic-int-hint-close\"\n onClick={dismiss}\n aria-label=\"Hide this\"\n title=\"Hide this. Your apps stay in the header.\"\n >\n ×\n </button>\n </div>\n );\n}\n\nexport default IntegrationsHint;\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { type JSX } from "react";
2
+ import type { IntegrationsState } from "./useIntegrations";
3
+ import "./IntegrationsModal.css";
4
+ /** How many logos fit before the rest are counted instead. */
5
+ export declare const DEFAULT_MAX_LOGOS = 6;
6
+ export interface IntegrationsLauncherProps {
7
+ /** Shared listing, so this and the modal load the catalogue once. */
8
+ state: IntegrationsState;
9
+ onClick: () => void;
10
+ /** Tooltip and accessible name. @default "Connected apps" */
11
+ label?: string;
12
+ /** Logos shown before the `+N` box. @default 6 */
13
+ maxLogos?: number;
14
+ /**
15
+ * Whether the surrounding surface is dark, so the logo chips go light. Many
16
+ * app logos are solid black on transparency and vanish otherwise.
17
+ */
18
+ dark?: boolean;
19
+ className?: string;
20
+ }
21
+ /**
22
+ * The header control that opens the connected-apps modal, drawn as the real
23
+ * logos of the apps on offer.
24
+ *
25
+ * It renders nothing until the server has confirmed the assistant offers apps
26
+ * to its tenants. A button that opens an empty dialog is worse than no button:
27
+ * it promises the end user something the assistant was never configured to
28
+ * give them.
29
+ */
30
+ export declare function IntegrationsLauncher({ state, onClick, label, maxLogos, dark, className, }: IntegrationsLauncherProps): JSX.Element | null;
31
+ export default IntegrationsLauncher;
@@ -0,0 +1,65 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useMemo, useRef, useState, useEffect } from 'react';
3
+ import { IntegrationLogo } from './IntegrationLogo.js';
4
+
5
+ /** How many logos fit before the rest are counted instead. */
6
+ const DEFAULT_MAX_LOGOS = 6;
7
+ /** Connected first: with more apps than fit, those are the ones worth showing. */
8
+ function order(integrations) {
9
+ return [...integrations].sort((a, b) => {
10
+ if (a.connected !== b.connected)
11
+ return a.connected ? -1 : 1;
12
+ return 0;
13
+ });
14
+ }
15
+ /**
16
+ * Logos a row this wide can afford.
17
+ *
18
+ * The stack shares the drawer header with a title, the conversation picker and
19
+ * two more buttons, and a 400px drawer is the common case. Six logos there push
20
+ * the picker down to a stub — so on a narrow header the count drops and the
21
+ * `+N` box absorbs the difference, which is what it is for.
22
+ */
23
+ function logosThatFit(hostWidth, max) {
24
+ if (!hostWidth)
25
+ return max;
26
+ if (hostWidth >= 520)
27
+ return max;
28
+ if (hostWidth >= 460)
29
+ return Math.min(max, 5);
30
+ return Math.min(max, 4);
31
+ }
32
+ /**
33
+ * The header control that opens the connected-apps modal, drawn as the real
34
+ * logos of the apps on offer.
35
+ *
36
+ * It renders nothing until the server has confirmed the assistant offers apps
37
+ * to its tenants. A button that opens an empty dialog is worse than no button:
38
+ * it promises the end user something the assistant was never configured to
39
+ * give them.
40
+ */
41
+ function IntegrationsLauncher({ state, onClick, label = "Connected apps", maxLogos = DEFAULT_MAX_LOGOS, dark = false, className = "", }) {
42
+ const sorted = useMemo(() => order(state.integrations), [state.integrations]);
43
+ const ref = useRef(null);
44
+ const [fit, setFit] = useState(maxLogos);
45
+ useEffect(() => {
46
+ const host = ref.current?.closest(".devic-drawer-header") ??
47
+ ref.current?.parentElement;
48
+ if (!host || typeof ResizeObserver === "undefined")
49
+ return;
50
+ const measure = () => setFit(logosThatFit(host.getBoundingClientRect().width, maxLogos));
51
+ measure();
52
+ const observer = new ResizeObserver(measure);
53
+ observer.observe(host);
54
+ return () => observer.disconnect();
55
+ }, [maxLogos, state.offered]);
56
+ if (!state.offered || sorted.length === 0)
57
+ return null;
58
+ const shown = sorted.slice(0, Math.max(1, fit));
59
+ const extra = sorted.length - shown.length;
60
+ const connected = sorted.filter((i) => i.connected).length;
61
+ return (jsxs("button", { type: "button", ref: ref, className: `devic-int-launcher ${className}`.trim(), "data-dark": dark, onClick: onClick, title: label, "aria-label": `${label} (${connected}/${sorted.length} connected)`, children: [shown.map((integration) => (jsx("span", { className: "devic-int-launcher-item", "data-connected": integration.connected, title: integration.name, children: jsx(IntegrationLogo, { integration: integration, className: "devic-int-launcher-logo" }) }, integration.app))), extra > 0 && (jsxs("span", { className: "devic-int-launcher-item devic-int-launcher-more", children: ["+", extra] }))] }));
62
+ }
63
+
64
+ export { DEFAULT_MAX_LOGOS, IntegrationsLauncher };
65
+ //# sourceMappingURL=IntegrationsLauncher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IntegrationsLauncher.js","sources":["../../../../src/components/IntegrationsModal/IntegrationsLauncher.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState, type JSX } from \"react\";\nimport type { Integration } from \"../../api/types\";\nimport { IntegrationLogo } from \"./IntegrationLogo\";\nimport type { IntegrationsState } from \"./useIntegrations\";\nimport \"./IntegrationsModal.css\";\n\n/** How many logos fit before the rest are counted instead. */\nexport const DEFAULT_MAX_LOGOS = 6;\n\nexport interface IntegrationsLauncherProps {\n /** Shared listing, so this and the modal load the catalogue once. */\n state: IntegrationsState;\n onClick: () => void;\n /** Tooltip and accessible name. @default \"Connected apps\" */\n label?: string;\n /** Logos shown before the `+N` box. @default 6 */\n maxLogos?: number;\n /**\n * Whether the surrounding surface is dark, so the logo chips go light. Many\n * app logos are solid black on transparency and vanish otherwise.\n */\n dark?: boolean;\n className?: string;\n}\n\n/** Connected first: with more apps than fit, those are the ones worth showing. */\nfunction order(integrations: Integration[]): Integration[] {\n return [...integrations].sort((a, b) => {\n if (a.connected !== b.connected) return a.connected ? -1 : 1;\n return 0;\n });\n}\n\n/**\n * Logos a row this wide can afford.\n *\n * The stack shares the drawer header with a title, the conversation picker and\n * two more buttons, and a 400px drawer is the common case. Six logos there push\n * the picker down to a stub — so on a narrow header the count drops and the\n * `+N` box absorbs the difference, which is what it is for.\n */\nfunction logosThatFit(hostWidth: number, max: number): number {\n if (!hostWidth) return max;\n if (hostWidth >= 520) return max;\n if (hostWidth >= 460) return Math.min(max, 5);\n return Math.min(max, 4);\n}\n\n/**\n * The header control that opens the connected-apps modal, drawn as the real\n * logos of the apps on offer.\n *\n * It renders nothing until the server has confirmed the assistant offers apps\n * to its tenants. A button that opens an empty dialog is worse than no button:\n * it promises the end user something the assistant was never configured to\n * give them.\n */\nexport function IntegrationsLauncher({\n state,\n onClick,\n label = \"Connected apps\",\n maxLogos = DEFAULT_MAX_LOGOS,\n dark = false,\n className = \"\",\n}: IntegrationsLauncherProps): JSX.Element | null {\n const sorted = useMemo(() => order(state.integrations), [state.integrations]);\n const ref = useRef<HTMLButtonElement>(null);\n const [fit, setFit] = useState(maxLogos);\n\n useEffect(() => {\n const host =\n ref.current?.closest(\".devic-drawer-header\") ??\n ref.current?.parentElement;\n if (!host || typeof ResizeObserver === \"undefined\") return;\n const measure = () =>\n setFit(logosThatFit(host.getBoundingClientRect().width, maxLogos));\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(host);\n return () => observer.disconnect();\n }, [maxLogos, state.offered]);\n\n if (!state.offered || sorted.length === 0) return null;\n\n const shown = sorted.slice(0, Math.max(1, fit));\n const extra = sorted.length - shown.length;\n const connected = sorted.filter((i) => i.connected).length;\n\n return (\n <button\n type=\"button\"\n ref={ref}\n className={`devic-int-launcher ${className}`.trim()}\n data-dark={dark}\n onClick={onClick}\n title={label}\n aria-label={`${label} (${connected}/${sorted.length} connected)`}\n >\n {shown.map((integration) => (\n <span\n key={integration.app}\n className=\"devic-int-launcher-item\"\n // Dimmed until connected, so the stack doubles as the status: the\n // end user can see at a glance which of their apps are set up.\n data-connected={integration.connected}\n title={integration.name}\n >\n <IntegrationLogo\n integration={integration}\n className=\"devic-int-launcher-logo\"\n />\n </span>\n ))}\n {extra > 0 && (\n <span className=\"devic-int-launcher-item devic-int-launcher-more\">\n +{extra}\n </span>\n )}\n </button>\n );\n}\n\nexport default IntegrationsLauncher;\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { type JSX } from "react";
2
2
  import type { Integration } from "../../api/types";
3
3
  import { type DevicTheme } from "../theme";
4
+ import { type IntegrationsState } from "./useIntegrations";
4
5
  import "./IntegrationsModal.css";
5
6
  export interface IntegrationsModalProps {
6
7
  /** Whether the modal is visible. */
@@ -18,6 +19,8 @@ export interface IntegrationsModalProps {
18
19
  baseUrl?: string;
19
20
  /** Modal title. @default "Connected apps" */
20
21
  title?: string;
22
+ /** Search field placeholder. @default "Search connected apps" */
23
+ searchPlaceholder?: string;
21
24
  /** Called after an account is connected or disconnected. */
22
25
  onChange?: (integrations: Integration[]) => void;
23
26
  /**
@@ -26,6 +29,12 @@ export interface IntegrationsModalProps {
26
29
  * themed application is the one thing this must not do.
27
30
  */
28
31
  theme?: DevicTheme;
32
+ /**
33
+ * Listing loaded elsewhere (see `useIntegrations`). The drawer already has to
34
+ * load it to decide whether its button exists, and passing it down is what
35
+ * keeps opening the modal from asking for the very same thing again.
36
+ */
37
+ state?: IntegrationsState;
29
38
  }
30
39
  /**
31
40
  * Modal where the END USER of an application manages their *own* third-party
@@ -43,5 +52,5 @@ export interface IntegrationsModalProps {
43
52
  * after the round trip is what gets it blocked. When it is blocked anyway, the
44
53
  * URL is offered as a link instead.
45
54
  */
46
- export declare function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId, apiKey, baseUrl, title, onChange, theme, }: IntegrationsModalProps): JSX.Element | null;
55
+ export declare function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId, apiKey, baseUrl, title, searchPlaceholder, onChange, theme, state, }: IntegrationsModalProps): JSX.Element | null;
47
56
  export default IntegrationsModal;
@@ -1,13 +1,16 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
- import { useMemo, useState, useCallback, useRef, useEffect } from 'react';
2
+ import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
- import { DevicApiClient } from '../../api/client.js';
5
- import { useOptionalDevicContext } from '../../provider/DevicContext.js';
6
- import { themeVars } from '../theme.js';
4
+ import { isDarkTheme, themeVars } from '../theme.js';
5
+ import { IntegrationLogo } from './IntegrationLogo.js';
6
+ import { useIntegrations } from './useIntegrations.js';
7
7
 
8
8
  function PlugIcon() {
9
9
  return (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: [jsx("path", { d: "M12 22v-5" }), jsx("path", { d: "M9 8V2" }), jsx("path", { d: "M15 8V2" }), jsx("path", { d: "M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z" })] }));
10
10
  }
11
+ function SearchIcon() {
12
+ return (jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [jsx("circle", { cx: "11", cy: "11", r: "7" }), jsx("path", { d: "m20 20-3.5-3.5" })] }));
13
+ }
11
14
  /** A random value tying an OAuth round trip to the window that started it. */
12
15
  function newNonce() {
13
16
  const c = typeof crypto !== "undefined" ? crypto : undefined;
@@ -32,6 +35,14 @@ function accountLabel(account) {
32
35
  return account.status.toLowerCase();
33
36
  return `connected ${when.toLocaleDateString()}`;
34
37
  }
38
+ function matches(integration, query) {
39
+ const q = query.trim().toLowerCase();
40
+ if (!q)
41
+ return true;
42
+ return (integration.name.toLowerCase().includes(q) ||
43
+ integration.app.toLowerCase().includes(q) ||
44
+ (integration.description ?? "").toLowerCase().includes(q));
45
+ }
35
46
  /**
36
47
  * Modal where the END USER of an application manages their *own* third-party
37
48
  * accounts: the apps the developer offered to tenants of this assistant, each
@@ -48,70 +59,55 @@ function accountLabel(account) {
48
59
  * after the round trip is what gets it blocked. When it is blocked anyway, the
49
60
  * URL is offered as a link instead.
50
61
  */
51
- function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId, apiKey, baseUrl, title = "Connected apps", onChange, theme, }) {
52
- const context = useOptionalDevicContext();
53
- const resolvedApiKey = apiKey || context?.apiKey;
54
- const resolvedBaseUrl = baseUrl || context?.baseUrl || "https://api.devic.ai";
55
- const resolvedTenantId = tenantId || context?.tenantId;
56
- const resolvedSubtenantId = subtenantId || context?.subtenantId;
57
- const client = useMemo(() => resolvedApiKey
58
- ? new DevicApiClient({
59
- apiKey: resolvedApiKey,
60
- baseUrl: resolvedBaseUrl,
61
- })
62
- : null, [resolvedApiKey, resolvedBaseUrl]);
63
- const scopeOptions = useMemo(() => ({
62
+ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId, apiKey, baseUrl, title = "Connected apps", searchPlaceholder = "Search connected apps", onChange, theme, state, }) {
63
+ // Hooks cannot be skipped, so the fallback is always built and only fetches
64
+ // when nobody handed a listing down.
65
+ const own = useIntegrations({
64
66
  assistantId,
65
- tenantId: resolvedTenantId || undefined,
66
- subtenantId: resolvedSubtenantId || undefined,
67
- }), [assistantId, resolvedTenantId, resolvedSubtenantId]);
68
- const [loading, setLoading] = useState(false);
69
- const [integrations, setIntegrations] = useState([]);
70
- const [error, setError] = useState(null);
67
+ tenantId,
68
+ subtenantId,
69
+ apiKey,
70
+ baseUrl,
71
+ enabled: isOpen && !state,
72
+ });
73
+ const { integrations, loading, error: loadError, refresh, client, scope } = state ?? own;
74
+ /** Errors from connecting or disconnecting, kept apart from load failures. */
75
+ const [actionError, setActionError] = useState(null);
76
+ const error = actionError ?? loadError;
71
77
  /** App slug with a connect/disconnect in flight, so only its card is busy. */
72
78
  const [busyApp, setBusyApp] = useState(null);
73
79
  /** Authorization URL surfaced as a link when the popup was blocked. */
74
80
  const [blockedUrl, setBlockedUrl] = useState(null);
75
- /**
76
- * Apps whose logo failed to load. The URL comes from the provider and is
77
- * fetched from whatever host it names, so a broken one is a matter of when,
78
- * not if and a broken-image icon in someone else's product looks like our
79
- * bug.
80
- */
81
- const [brokenLogos, setBrokenLogos] = useState([]);
82
- const refresh = useCallback(async (dropCache = false) => {
83
- if (!client) {
84
- setError("API key not configured");
85
- return;
86
- }
87
- setLoading(true);
88
- setError(null);
89
- try {
90
- // The server caches a scope's connections briefly; after an OAuth round
91
- // trip that cache is exactly one step behind, so drop it first.
92
- if (dropCache) {
93
- await client.refreshIntegrations(scopeOptions).catch(() => undefined);
94
- }
95
- const list = await client.getIntegrations(scopeOptions);
96
- setIntegrations(list);
97
- onChange?.(list);
98
- }
99
- catch (err) {
100
- setError(err instanceof Error ? err.message : String(err));
101
- }
102
- finally {
103
- setLoading(false);
104
- }
105
- }, [client, scopeOptions, onChange]);
106
- // Fetch on every open: accounts may have been connected or revoked elsewhere.
81
+ const [query, setQuery] = useState("");
82
+ const visible = useMemo(() => integrations.filter((i) => matches(i, query)), [integrations, query]);
83
+ // Report the listing without making the caller's identity part of the
84
+ // dependency: an inline arrow would fire this on every render.
85
+ const onChangeRef = useRef(onChange);
86
+ onChangeRef.current = onChange;
87
+ useEffect(() => {
88
+ if (integrations.length)
89
+ onChangeRef.current?.(integrations);
90
+ }, [integrations]);
91
+ // Reopening starts clean, and re-reads: accounts may have been connected or
92
+ // revoked elsewhere since the last look.
107
93
  const wasOpenRef = useRef(false);
94
+ const openedBeforeRef = useRef(false);
108
95
  useEffect(() => {
109
96
  if (isOpen && !wasOpenRef.current) {
110
97
  setBlockedUrl(null);
111
- void refresh();
98
+ setActionError(null);
99
+ setQuery("");
100
+ // The very first open of an uncontrolled modal is already covered by the
101
+ // hook switching on; asking again here would double every first open.
102
+ if (state || openedBeforeRef.current)
103
+ void refresh();
104
+ openedBeforeRef.current = true;
112
105
  }
113
106
  wasOpenRef.current = isOpen;
114
- }, [isOpen, refresh]);
107
+ // `state.refresh` is stable per scope; re-running on every render of the
108
+ // owner is exactly what this must not do.
109
+ // eslint-disable-next-line react-hooks/exhaustive-deps
110
+ }, [isOpen]);
115
111
  // Escape closes
116
112
  useEffect(() => {
117
113
  if (!isOpen)
@@ -166,7 +162,7 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
166
162
  const handleConnect = async (integration) => {
167
163
  if (!client || busyApp)
168
164
  return;
169
- setError(null);
165
+ setActionError(null);
170
166
  setBlockedUrl(null);
171
167
  setBusyApp(integration.app);
172
168
  const nonce = newNonce();
@@ -174,7 +170,7 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
174
170
  // Opened empty inside the click, navigated once the URL is known.
175
171
  const popup = window.open("", "devic-oauth", "width=520,height=680,menubar=no,toolbar=no");
176
172
  try {
177
- const { authorizationUrl } = await client.connectIntegration(integration.app, { ...scopeOptions, returnTo });
173
+ const { authorizationUrl } = await client.connectIntegration(integration.app, { ...scope, returnTo });
178
174
  pendingRef.current = { app: integration.app, returnTo };
179
175
  if (popup && !popup.closed) {
180
176
  popupRef.current = popup;
@@ -197,7 +193,7 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
197
193
  catch (err) {
198
194
  popup?.close();
199
195
  pendingRef.current = null;
200
- setError(err instanceof Error ? err.message : String(err));
196
+ setActionError(err instanceof Error ? err.message : String(err));
201
197
  setBusyApp(null);
202
198
  }
203
199
  };
@@ -205,13 +201,13 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
205
201
  if (!client || busyApp)
206
202
  return;
207
203
  setBusyApp(app);
208
- setError(null);
204
+ setActionError(null);
209
205
  try {
210
- await client.disconnectIntegration(account.id, scopeOptions);
206
+ await client.disconnectIntegration(account.id, scope);
211
207
  await refresh(true);
212
208
  }
213
209
  catch (err) {
214
- setError(err instanceof Error ? err.message : String(err));
210
+ setActionError(err instanceof Error ? err.message : String(err));
215
211
  }
216
212
  finally {
217
213
  setBusyApp(null);
@@ -223,23 +219,27 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
223
219
  // The variables go on the overlay, not on the modal: the backdrop is part
224
220
  // of the dialog, and a portal inherits nothing from the drawer that opened
225
221
  // it.
226
- jsx("div", { className: "devic-int-overlay", style: themeVars(theme), onClick: onClose, children: jsxs("div", { className: "devic-int-modal", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: (e) => e.stopPropagation(), children: [jsxs("div", { className: "devic-int-header", children: [jsxs("h3", { className: "devic-int-title", children: [jsx(PlugIcon, {}), title] }), jsx("button", { className: "devic-int-close", onClick: onClose, type: "button", "aria-label": "Close", children: "\u00D7" })] }), jsxs("div", { className: "devic-int-body", children: [error && jsx("div", { className: "devic-int-error", children: error }), blockedUrl && (jsxs("div", { className: "devic-int-notice", children: ["Your browser blocked the pop-up.", " ", jsx("a", { href: blockedUrl.url, target: "_blank", rel: "noopener noreferrer", onClick: () => {
222
+ jsx("div", { className: "devic-int-overlay", style: themeVars(theme), "data-dark": isDarkTheme(theme), onClick: onClose, children: jsxs("div", { className: "devic-int-modal", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: (e) => e.stopPropagation(), children: [jsxs("div", { className: "devic-int-header", children: [jsxs("h3", { className: "devic-int-title", children: [jsx(PlugIcon, {}), title] }), jsx("button", { className: "devic-int-close", onClick: onClose, type: "button", "aria-label": "Close", children: "\u00D7" })] }), jsxs("div", { className: "devic-int-search", children: [jsx(SearchIcon, {}), jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: searchPlaceholder, "aria-label": searchPlaceholder, autoComplete: "off" })] }), jsxs("div", { className: "devic-int-body", children: [error && jsx("div", { className: "devic-int-error", children: error }), blockedUrl && (jsxs("div", { className: "devic-int-notice", children: ["Your browser blocked the pop-up.", " ", jsx("a", { href: blockedUrl.url, target: "_blank", rel: "noopener noreferrer", onClick: () => {
227
223
  pendingRef.current = null;
228
224
  setBlockedUrl(null);
229
- }, children: "Open the authorisation page" }), " ", "and come back \u2014 then use Refresh."] })), loading && integrations.length === 0 ? (jsx("div", { className: "devic-int-loading", children: "Loading apps\u2026" })) : integrations.length === 0 ? (jsx("div", { className: "devic-int-empty", children: "No apps available here yet." })) : (integrations.map((integration) => {
230
- const state = stateOf(integration);
231
- const busy = busyApp === integration.app;
232
- return (jsxs("div", { className: "devic-int-card", "data-state": state.key, children: [jsxs("div", { className: "devic-int-card-head", children: [integration.logo &&
233
- !brokenLogos.includes(integration.app) ? (jsx("img", { className: "devic-int-logo", src: integration.logo, alt: "", "aria-hidden": "true", onError: () => setBrokenLogos((prev) => prev.includes(integration.app)
234
- ? prev
235
- : [...prev, integration.app]) })) : (jsx("span", { className: "devic-int-logo devic-int-logo-fallback", children: integration.name.charAt(0).toUpperCase() })), jsxs("div", { className: "devic-int-card-text", children: [jsx("div", { className: "devic-int-name", children: integration.name }), jsx("div", { className: "devic-int-state", children: state.label })] }), jsx("button", { type: "button", className: `devic-int-btn${state.key === "connected" ? "" : " devic-int-btn-primary"}`, onClick: () => handleConnect(integration), disabled: busy || !!busyApp, children: busy
236
- ? "Waiting…"
237
- : state.key === "disconnected"
238
- ? "Connect"
239
- : state.key === "reconnect"
240
- ? "Reconnect"
241
- : "Add account" })] }), integration.description && (jsx("div", { className: "devic-int-description", children: integration.description })), integration.accounts.length > 0 && (jsx("ul", { className: "devic-int-accounts", children: integration.accounts.map((account) => (jsxs("li", { className: "devic-int-account", children: [jsx("span", { className: "devic-int-dot", "data-ok": !account.needsReconnect, "aria-hidden": "true" }), jsxs("span", { className: "devic-int-account-label", children: [accountLabel(account), account.needsReconnect && (jsxs("span", { className: "devic-int-account-warn", children: [" ", "\u00B7 reconnect required"] }))] }), 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));
242
- }))] }), jsxs("div", { className: "devic-int-footer", children: [jsx("span", { children: "Only you can see and use the accounts you connect here." }), jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-small", onClick: () => void refresh(true), disabled: loading || !!busyApp, children: "Refresh" })] })] }) }), document.body);
225
+ }, children: "Open the authorisation page" }), " ", "and come back \u2014 then use Refresh."] })), loading && integrations.length === 0 ? (jsx("div", { className: "devic-int-loading", children: "Loading apps\u2026" })) : integrations.length === 0 ? (jsx("div", { className: "devic-int-empty", children: "No apps available here yet." })) : visible.length === 0 ? (jsxs("div", { className: "devic-int-empty", children: ["No apps match \u201C", query.trim(), "\u201D."] })) : (jsx("div", { className: "devic-int-grid", children: visible.map((integration) => {
226
+ const cardState = stateOf(integration);
227
+ const busy = busyApp === integration.app;
228
+ return (jsxs("div", { className: "devic-int-card", "data-state": cardState.key, children: [jsxs("div", { className: "devic-int-card-head", children: [jsx(IntegrationLogo, { integration: integration }), jsxs("span", { className: "devic-int-state", children: [jsx("span", { className: "devic-int-dot", "data-ok": cardState.key === "connected", "data-off": cardState.key === "disconnected", "aria-hidden": "true" }), cardState.label] })] }), jsx("div", { className: "devic-int-name", title: integration.name, children: integration.name }), integration.description && (jsx("div", { className: "devic-int-description", title: integration.description, children: integration.description })), jsx("button", { type: "button", className: `devic-int-btn devic-int-btn-block${cardState.key === "connected"
229
+ ? ""
230
+ : " devic-int-btn-primary"}`, onClick: () => handleConnect(integration), disabled: busy || !!busyApp, title: cardState.key === "connected"
231
+ ? "Sign in with a different account. The one connected now is replaced."
232
+ : undefined, children: busy
233
+ ? "Waiting…"
234
+ : cardState.key === "disconnected"
235
+ ? "Connect"
236
+ : cardState.key === "reconnect"
237
+ ? "Reconnect"
238
+ : // Not "Add account": one account per app is all
239
+ // the assistant can use, and connecting again
240
+ // retires the previous one.
241
+ "Switch account" }), integration.accounts.length > 0 && (jsx("ul", { className: "devic-int-accounts", children: integration.accounts.map((account) => (jsxs("li", { className: "devic-int-account", children: [jsx("span", { className: "devic-int-dot", "data-ok": !account.needsReconnect, "aria-hidden": "true" }), jsxs("span", { className: "devic-int-account-label", children: [accountLabel(account), account.needsReconnect && (jsxs("span", { className: "devic-int-account-warn", children: [" ", "\u00B7 reconnect required"] }))] }), jsx("button", { type: "button", className: "devic-int-unlink", onClick: () => handleDisconnect(integration.app, account), disabled: !!busyApp, title: "Disconnect this account", "aria-label": `Disconnect ${integration.name}`, children: "\u00D7" })] }, account.id))) }))] }, integration.app));
242
+ }) }))] }), jsxs("div", { className: "devic-int-footer", children: [jsx("span", { children: "Only you can see and use the accounts you connect here." }), jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-small", onClick: () => void refresh(true), disabled: loading || !!busyApp, children: "Refresh" })] })] }) }), document.body);
243
243
  }
244
244
 
245
245
  export { IntegrationsModal };
@@ -1 +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"],"mappings":"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"}
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 type { Integration, IntegrationAccount } from \"../../api/types\";\nimport { isDarkTheme, themeVars, type DevicTheme } from \"../theme\";\nimport { IntegrationLogo } from \"./IntegrationLogo\";\nimport { useIntegrations, type IntegrationsState } from \"./useIntegrations\";\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 /** Search field placeholder. @default \"Search connected apps\" */\n searchPlaceholder?: 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 * Listing loaded elsewhere (see `useIntegrations`). The drawer already has to\n * load it to decide whether its button exists, and passing it down is what\n * keeps opening the modal from asking for the very same thing again.\n */\n state?: IntegrationsState;\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\nfunction SearchIcon(): JSX.Element {\n return (\n <svg\n width=\"14\"\n height=\"14\"\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 <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.5-3.5\" />\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\nfunction matches(integration: Integration, query: string): boolean {\n const q = query.trim().toLowerCase();\n if (!q) return true;\n return (\n integration.name.toLowerCase().includes(q) ||\n integration.app.toLowerCase().includes(q) ||\n (integration.description ?? \"\").toLowerCase().includes(q)\n );\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 searchPlaceholder = \"Search connected apps\",\n onChange,\n theme,\n state,\n}: IntegrationsModalProps): JSX.Element | null {\n // Hooks cannot be skipped, so the fallback is always built and only fetches\n // when nobody handed a listing down.\n const own = useIntegrations({\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n enabled: isOpen && !state,\n });\n const { integrations, loading, error: loadError, refresh, client, scope } =\n state ?? own;\n\n /** Errors from connecting or disconnecting, kept apart from load failures. */\n const [actionError, setActionError] = useState<string | null>(null);\n const error = actionError ?? loadError;\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 const [query, setQuery] = useState(\"\");\n\n const visible = useMemo(\n () => integrations.filter((i) => matches(i, query)),\n [integrations, query]\n );\n\n // Report the listing without making the caller's identity part of the\n // dependency: an inline arrow would fire this on every render.\n const onChangeRef = useRef(onChange);\n onChangeRef.current = onChange;\n useEffect(() => {\n if (integrations.length) onChangeRef.current?.(integrations);\n }, [integrations]);\n\n // Reopening starts clean, and re-reads: accounts may have been connected or\n // revoked elsewhere since the last look.\n const wasOpenRef = useRef(false);\n const openedBeforeRef = useRef(false);\n useEffect(() => {\n if (isOpen && !wasOpenRef.current) {\n setBlockedUrl(null);\n setActionError(null);\n setQuery(\"\");\n // The very first open of an uncontrolled modal is already covered by the\n // hook switching on; asking again here would double every first open.\n if (state || openedBeforeRef.current) void refresh();\n openedBeforeRef.current = true;\n }\n wasOpenRef.current = isOpen;\n // `state.refresh` is stable per scope; re-running on every render of the\n // owner is exactly what this must not do.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen]);\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 setActionError(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 { ...scope, 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 setActionError(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 setActionError(null);\n try {\n await client.disconnectIntegration(account.id, scope);\n await refresh(true);\n } catch (err) {\n setActionError(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 data-dark={isDarkTheme(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-search\">\n <SearchIcon />\n <input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={searchPlaceholder}\n aria-label={searchPlaceholder}\n autoComplete=\"off\"\n />\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\">No apps available here yet.</div>\n ) : visible.length === 0 ? (\n <div className=\"devic-int-empty\">\n No apps match “{query.trim()}”.\n </div>\n ) : (\n <div className=\"devic-int-grid\">\n {visible.map((integration) => {\n const cardState = stateOf(integration);\n const busy = busyApp === integration.app;\n return (\n <div\n key={integration.app}\n className=\"devic-int-card\"\n data-state={cardState.key}\n >\n <div className=\"devic-int-card-head\">\n <IntegrationLogo integration={integration} />\n <span className=\"devic-int-state\">\n <span\n className=\"devic-int-dot\"\n data-ok={cardState.key === \"connected\"}\n data-off={cardState.key === \"disconnected\"}\n aria-hidden=\"true\"\n />\n {cardState.label}\n </span>\n </div>\n\n <div className=\"devic-int-name\" title={integration.name}>\n {integration.name}\n </div>\n\n {integration.description && (\n <div\n className=\"devic-int-description\"\n title={integration.description}\n >\n {integration.description}\n </div>\n )}\n\n <button\n type=\"button\"\n className={`devic-int-btn devic-int-btn-block${\n cardState.key === \"connected\"\n ? \"\"\n : \" devic-int-btn-primary\"\n }`}\n onClick={() => handleConnect(integration)}\n disabled={busy || !!busyApp}\n title={\n cardState.key === \"connected\"\n ? \"Sign in with a different account. The one connected now is replaced.\"\n : undefined\n }\n >\n {busy\n ? \"Waiting…\"\n : cardState.key === \"disconnected\"\n ? \"Connect\"\n : cardState.key === \"reconnect\"\n ? \"Reconnect\"\n : // Not \"Add account\": one account per app is all\n // the assistant can use, and connecting again\n // retires the previous one.\n \"Switch account\"}\n </button>\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-unlink\"\n onClick={() =>\n handleDisconnect(integration.app, account)\n }\n disabled={!!busyApp}\n title=\"Disconnect this account\"\n aria-label={`Disconnect ${integration.name}`}\n >\n ×\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n })}\n </div>\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"],"mappings":"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"}
@@ -1,2 +1,10 @@
1
1
  export { IntegrationsModal, default } from "./IntegrationsModal";
2
2
  export type { IntegrationsModalProps } from "./IntegrationsModal";
3
+ export { IntegrationsLauncher, DEFAULT_MAX_LOGOS } from "./IntegrationsLauncher";
4
+ export type { IntegrationsLauncherProps } from "./IntegrationsLauncher";
5
+ export { IntegrationsHint } from "./IntegrationsHint";
6
+ export type { IntegrationsHintProps } from "./IntegrationsHint";
7
+ export { IntegrationLogo } from "./IntegrationLogo";
8
+ export type { IntegrationLogoProps } from "./IntegrationLogo";
9
+ export { useIntegrations } from "./useIntegrations";
10
+ export type { IntegrationsState, IntegrationsScope, UseIntegrationsOptions, } from "./useIntegrations";