@shuffleio/shuffle-mcps 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ <div align="center">
2
+
3
+ <img src="./docs/agent-icon.png" alt="Shuffle MCPs" width="96" />
4
+
5
+ # Shuffle MCPs
6
+
7
+ Turn 3,000+ SaaS tools into MCP servers your agents can use. One search, one click, authenticated.
8
+
9
+ [![npm](https://img.shields.io/npm/v/shuffle-mcps.svg)](https://www.npmjs.com/package/shuffle-mcps)
10
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
11
+ [![upstream](https://img.shields.io/badge/upstream-Shuffle%2Fsingul.js-orange)](https://github.com/Shuffle/singul.js)
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ <table align="center">
18
+ <tr>
19
+ <td align="center" width="50%">
20
+ <img src="./docs/search-preview.png" alt="Search 3,000+ integrations" width="380" style="border-radius: 12px;" />
21
+ <br/><sub><b>1. Search any of 3,000+ tools</b></sub>
22
+ </td>
23
+ <td align="center" width="50%">
24
+ <img src="./docs/app-detail-preview.png" alt="Authenticate and use as MCP" width="380" style="border-radius: 12px;" />
25
+ <br/><sub><b>2. Authenticate — your agent uses it instantly</b></sub>
26
+ </td>
27
+ </tr>
28
+ </table>
29
+
30
+ &nbsp;
31
+
32
+ > **No SDK required.** Once a tool is authenticated in Shuffle, it is live as an MCP server immediately — call it from any agent over plain HTTP. This npm package is the optional UI layer for embedding the search-and-connect flow inside your own product.
33
+
34
+ ## Try it in 30 seconds
35
+
36
+ Authenticate any tool at [shuffler.io](https://shuffler.io), grab your API key, and call it:
37
+
38
+ ```bash
39
+ curl https://shuffler.io/api/v1/apps/YOUR_APP/run \
40
+ -H "Authorization: Bearer YOUR_API_KEY" \
41
+ -H "Content-Type: application/json" \
42
+ -d '{"action": "send_message", "fields": {"channel": "general", "text": "hi"}}'
43
+ ```
44
+
45
+ That is the MCP. Any agent, any language, no install. Full API reference: [shuffler.io/docs/API](https://shuffler.io/docs/API).
46
+
47
+ ## When to use this package
48
+
49
+ Install `shuffle-mcps` when you want to **embed** the search, authenticate, and manage flow inside your own React/Vue/Next.js app — onboarding wizards, integration drawers, app pickers. If your agent is just calling MCPs, you do not need this; use the HTTP API directly.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ npm install shuffle-mcps
55
+ ```
56
+
57
+ Peer deps: `react >= 18`, `react-dom >= 18`, `algoliasearch >= 5`.
58
+
59
+ ## Quick start
60
+
61
+ ```tsx
62
+ import { SingulJS } from 'shuffle-mcps';
63
+
64
+ export default function App() {
65
+ return (
66
+ <SingulJS
67
+ authToken="any-token"
68
+ inline
69
+ layout="grid"
70
+ gridColumns={3}
71
+ onAppSelected={(d) => console.log('picked', d.app.name)}
72
+ />
73
+ );
74
+ }
75
+ ```
76
+
77
+ ## Recipes
78
+
79
+ ### Search → detail drawer
80
+
81
+ The most common pattern. Set `preventDefault` and handle `onAppSelected` to chain into your own auth or detail UI:
82
+
83
+ ```tsx
84
+ <SingulJS
85
+ authToken={token}
86
+ inline
87
+ preventDefault
88
+ onAppSelected={(d) => openDetailDrawer(d.app)}
89
+ />
90
+ ```
91
+
92
+ Full two-drawer reference implementation: [`src/components/shared/AppSearchDrawer.tsx`](../components/shared/AppSearchDrawer.tsx).
93
+
94
+ ### Predefined search
95
+
96
+ ```tsx
97
+ <SingulJS
98
+ authToken="..."
99
+ inline
100
+ initialFilterQuery="siem"
101
+ placeholder="Search SIEM tools..."
102
+ />
103
+ ```
104
+
105
+ ### Multi-select
106
+
107
+ ```tsx
108
+ const [picked, setPicked] = useState<AlgoliaSearchApp[]>([]);
109
+
110
+ <SingulJS
111
+ authToken="..."
112
+ inline
113
+ multiSelect
114
+ showCheckbox
115
+ selectedApps={picked}
116
+ onSelectionChange={setPicked}
117
+ />
118
+ ```
119
+
120
+ ### Your own backend
121
+
122
+ ```tsx
123
+ <SingulJS
124
+ authToken={user.token}
125
+ apiKey={user.apiKey}
126
+ apiBaseUrl="https://your-backend.example.com"
127
+ algoliaAppId="YOUR_APP_ID"
128
+ algoliaApiKey="YOUR_SEARCH_KEY"
129
+ />
130
+ ```
131
+
132
+ When `apiKey` is set, the component also fetches the user's **private apps** from `/api/v1/apps` and merges them into the search results, with an **All / Public / Private** filter shown above the list. Status dots appear too: validated, configured, selected, inactive.
133
+
134
+ ## Common props
135
+
136
+ | Prop | Type | Description |
137
+ |---|---|---|
138
+ | `authToken` | `string` | Required. Forwarded into the auth URL. |
139
+ | `inline` | `boolean` | Inline results vs floating dropdown. |
140
+ | `layout` | `'list' \| 'grid'` | Result layout. Default `'list'`. |
141
+ | `initialFilterQuery` | `string` | Pre-filter without filling the input. |
142
+ | `preventDefault` | `boolean` | Skip default `window.open(authUrl)` so you can handle selection. |
143
+ | `onAppSelected` | `(detail) => void` | Fires on single-select pick. |
144
+ | `multiSelect` | `boolean` | Allow selecting multiple apps. |
145
+ | `apiKey` | `string` | Bearer token. Enables status dots and merges your private apps from `/api/v1/apps`. |
146
+ | `showSourceFilter` | `boolean` | Toggle the All / Public / Private filter. Default `true`. |
147
+ | `customStyles` | `CustomStyles` | Per-slot style overrides. |
148
+
149
+ Full prop reference, framework setup (Next.js, Vue), styling slots, custom rendering, and publishing: [**LIBRARY.md**](./LIBRARY.md).
150
+
151
+ ## Imperative handle
152
+
153
+ ```tsx
154
+ const ref = useRef<SingulJSHandle>(null);
155
+ <SingulJS ref={ref} authToken="..." inline />
156
+
157
+ ref.current?.search('slack');
158
+ ref.current?.clear();
159
+ ```
160
+
161
+ ## Next.js
162
+
163
+ The component touches `window`, so render it client-side with `'use client'` or `next/dynamic({ ssr: false })`. See [LIBRARY.md](./LIBRARY.md#nextjs) for details.
package/dist/index.cjs ADDED
@@ -0,0 +1,519 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // index.ts
30
+ var index_exports = {};
31
+ __export(index_exports, {
32
+ SingulJS: () => SingulJS,
33
+ default: () => SingulJS_default
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // SingulJS.tsx
38
+ var import_react = __toESM(require("react"), 1);
39
+ var import_algoliasearch = require("algoliasearch");
40
+ var import_singul = require("./singul-GZKBHJNI.css");
41
+ var import_jsx_runtime = require("react/jsx-runtime");
42
+ var DEFAULT_ALGOLIA_APP_ID = "JNSS5CFDZZ";
43
+ var DEFAULT_ALGOLIA_API_KEY = "33e4e3564f4f060e96e0531957bed552";
44
+ var DEFAULT_ALGOLIA_INDEX = "appsearch";
45
+ var SingulJS = import_react.default.forwardRef(({
46
+ authToken,
47
+ placeholder = "Search apps...",
48
+ layout = "list",
49
+ gridColumns = 3,
50
+ showDescription = false,
51
+ showCategories = false,
52
+ showCheckbox = false,
53
+ multiSelect = false,
54
+ selectedApps = [],
55
+ preventDefault = false,
56
+ inline = false,
57
+ initialQuery = "",
58
+ initialFilterQuery,
59
+ hitsPerPage = 15,
60
+ apiKey,
61
+ apiBaseUrl = "https://shuffler.io",
62
+ authPath = "/api/v1/apps/authentication",
63
+ appAuthPath = "/appauth",
64
+ privateAppsPath = "/api/v1/apps",
65
+ disablePrivateApps = false,
66
+ showSourceFilter = true,
67
+ singulBaseUrl = "https://singul.io",
68
+ hideAuthStatus = false,
69
+ authenticatedApps: externalAuthenticatedApps,
70
+ pinnedApps,
71
+ algoliaAppId = DEFAULT_ALGOLIA_APP_ID,
72
+ algoliaApiKey = DEFAULT_ALGOLIA_API_KEY,
73
+ algoliaIndexName = DEFAULT_ALGOLIA_INDEX,
74
+ customStyles = {},
75
+ className = "",
76
+ renderItem,
77
+ renderEmptyState,
78
+ renderLoadingState,
79
+ onAppSelected,
80
+ onSelectionChange,
81
+ onSearchChange
82
+ }, ref) => {
83
+ const [query, setQuery] = (0, import_react.useState)(initialQuery);
84
+ const [results, setResults] = (0, import_react.useState)([]);
85
+ const [privateApps, setPrivateApps] = (0, import_react.useState)([]);
86
+ const [sourceFilter, setSourceFilter] = (0, import_react.useState)("all");
87
+ const [isLoading, setIsLoading] = (0, import_react.useState)(false);
88
+ const [isOpen, setIsOpen] = (0, import_react.useState)(false);
89
+ const [selectedIndex, setSelectedIndex] = (0, import_react.useState)(-1);
90
+ const [internalSelectedApps, setInternalSelectedApps] = (0, import_react.useState)(selectedApps);
91
+ const [authenticatedApps, setAuthenticatedApps] = (0, import_react.useState)(externalAuthenticatedApps || []);
92
+ const hasInitialized = (0, import_react.useRef)(false);
93
+ const containerRef = (0, import_react.useRef)(null);
94
+ const inputRef = (0, import_react.useRef)(null);
95
+ const debounceTimer = (0, import_react.useRef)(null);
96
+ const searchClient = (0, import_react.useRef)(null);
97
+ (0, import_react.useEffect)(() => {
98
+ if (apiKey) {
99
+ const fetchAuthenticatedApps = async () => {
100
+ try {
101
+ const response = await fetch(`${apiBaseUrl}${authPath}`, {
102
+ headers: {
103
+ "Authorization": `Bearer ${apiKey}`
104
+ }
105
+ });
106
+ if (response.ok) {
107
+ const result = await response.json();
108
+ const authData = result.data || result;
109
+ if (Array.isArray(authData)) {
110
+ console.log("Loaded authenticated apps:", authData.length, authData.map((a) => a.app?.name));
111
+ setAuthenticatedApps(authData);
112
+ }
113
+ }
114
+ } catch (error) {
115
+ console.error("Failed to fetch authenticated apps:", error);
116
+ }
117
+ };
118
+ fetchAuthenticatedApps();
119
+ }
120
+ }, [apiKey, apiBaseUrl]);
121
+ (0, import_react.useEffect)(() => {
122
+ if (!apiKey || disablePrivateApps) {
123
+ setPrivateApps([]);
124
+ return;
125
+ }
126
+ const fetchPrivateApps = async () => {
127
+ try {
128
+ const response = await fetch(`${apiBaseUrl}${privateAppsPath}`, {
129
+ headers: { "Authorization": `Bearer ${apiKey}` },
130
+ credentials: "include"
131
+ });
132
+ if (!response.ok) return;
133
+ const data = await response.json();
134
+ const apps = Array.isArray(data) ? data : data?.data || [];
135
+ const normalized = apps.map((a) => ({
136
+ name: a.name || "",
137
+ description: a.description || "",
138
+ objectID: a.id || a.objectID || `private-${a.name}`,
139
+ creator: a.owner || a.creator || "",
140
+ app_version: a.app_version || "1.0.0",
141
+ image_url: a.large_image || a.image_url || "",
142
+ time_edited: a.edited || 0,
143
+ generated: !!a.generated,
144
+ invalid: !!a.invalid,
145
+ priority: a.priority || 0,
146
+ actions: Array.isArray(a.actions) ? a.actions.length : a.actions || 0,
147
+ tags: a.tags || [],
148
+ accessible_by: a.accessible_by || [],
149
+ categories: a.categories || [],
150
+ action_labels: a.action_labels || [],
151
+ triggers: a.triggers || [],
152
+ verified: !!a.verified,
153
+ source: "private"
154
+ }));
155
+ setPrivateApps(normalized);
156
+ } catch (error) {
157
+ console.error("Failed to fetch private apps:", error);
158
+ }
159
+ };
160
+ fetchPrivateApps();
161
+ }, [apiKey, apiBaseUrl, privateAppsPath, disablePrivateApps]);
162
+ (0, import_react.useEffect)(() => {
163
+ if (authenticatedApps.length > 0 && results.length > 0) {
164
+ const validatedApps = authenticatedApps.filter((auth) => auth.validation?.valid);
165
+ const appsToAutoSelect = results.filter(
166
+ (app) => validatedApps.some((auth) => auth.app.name.toLowerCase() === app.name.toLowerCase())
167
+ );
168
+ if (appsToAutoSelect.length > 0) {
169
+ const newSelection = [...internalSelectedApps];
170
+ appsToAutoSelect.forEach((app) => {
171
+ if (!newSelection.some((a) => a.objectID === app.objectID)) {
172
+ newSelection.push(app);
173
+ }
174
+ });
175
+ if (newSelection.length !== internalSelectedApps.length) {
176
+ setInternalSelectedApps(newSelection);
177
+ onSelectionChange?.(newSelection);
178
+ }
179
+ }
180
+ }
181
+ }, [authenticatedApps, results]);
182
+ (0, import_react.useEffect)(() => {
183
+ if (externalAuthenticatedApps) {
184
+ setAuthenticatedApps(externalAuthenticatedApps);
185
+ }
186
+ }, [externalAuthenticatedApps]);
187
+ (0, import_react.useEffect)(() => {
188
+ searchClient.current = (0, import_algoliasearch.algoliasearch)(algoliaAppId, algoliaApiKey);
189
+ if (!hasInitialized.current) {
190
+ hasInitialized.current = true;
191
+ const searchTerm = initialQuery || initialFilterQuery || "";
192
+ setTimeout(() => {
193
+ if (searchClient.current) {
194
+ performSearch(searchTerm);
195
+ onSearchChange?.(initialQuery);
196
+ }
197
+ }, 100);
198
+ }
199
+ }, []);
200
+ (0, import_react.useEffect)(() => {
201
+ setInternalSelectedApps(selectedApps);
202
+ }, [selectedApps]);
203
+ (0, import_react.useEffect)(() => {
204
+ const handleClickOutside = (event) => {
205
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
206
+ setIsOpen(false);
207
+ setSelectedIndex(-1);
208
+ }
209
+ };
210
+ document.addEventListener("click", handleClickOutside);
211
+ return () => document.removeEventListener("click", handleClickOutside);
212
+ }, []);
213
+ const isAppConfigured = (0, import_react.useCallback)((app) => {
214
+ return authenticatedApps.some(
215
+ (auth) => auth.app.name.toLowerCase() === app.name.toLowerCase() && auth.active === true
216
+ );
217
+ }, [authenticatedApps]);
218
+ const isAppValidated = (0, import_react.useCallback)((app) => {
219
+ return authenticatedApps.some(
220
+ (auth) => auth.app.name.toLowerCase() === app.name.toLowerCase() && auth.validation?.valid === true
221
+ );
222
+ }, [authenticatedApps]);
223
+ const getAppAuthState = (0, import_react.useCallback)((app) => {
224
+ return {
225
+ configured: isAppConfigured(app),
226
+ validated: isAppValidated(app)
227
+ };
228
+ }, [isAppConfigured, isAppValidated]);
229
+ const performSearch = (0, import_react.useCallback)(async (searchQuery) => {
230
+ if (!searchClient.current) {
231
+ return;
232
+ }
233
+ setIsLoading(true);
234
+ try {
235
+ const searchResult = await searchClient.current.searchSingleIndex({
236
+ indexName: algoliaIndexName,
237
+ searchParams: {
238
+ query: searchQuery || "",
239
+ // Empty string gets top results
240
+ hitsPerPage
241
+ }
242
+ });
243
+ const hits = searchResult.hits.map((h) => ({ ...h, source: "public" }));
244
+ setResults(hits);
245
+ setIsOpen(hits.length > 0);
246
+ setSelectedIndex(-1);
247
+ } catch (error) {
248
+ console.error("Search failed:", error);
249
+ setResults([]);
250
+ setIsOpen(false);
251
+ } finally {
252
+ setIsLoading(false);
253
+ }
254
+ }, [hitsPerPage]);
255
+ (0, import_react.useImperativeHandle)(ref, () => ({
256
+ search: (searchQuery) => {
257
+ setQuery(searchQuery);
258
+ onSearchChange?.(searchQuery);
259
+ performSearch(searchQuery);
260
+ },
261
+ clear: () => {
262
+ setQuery("");
263
+ performSearch("");
264
+ },
265
+ getQuery: () => query
266
+ }), [performSearch, onSearchChange, query]);
267
+ const handleInputChange = (0, import_react.useCallback)((event) => {
268
+ const value = event.target.value;
269
+ setQuery(value);
270
+ setSelectedIndex(-1);
271
+ onSearchChange?.(value);
272
+ if (debounceTimer.current) {
273
+ clearTimeout(debounceTimer.current);
274
+ }
275
+ debounceTimer.current = setTimeout(() => {
276
+ performSearch(value);
277
+ }, 300);
278
+ }, [performSearch, onSearchChange]);
279
+ const selectApp = (0, import_react.useCallback)((app) => {
280
+ const authUrl = `${apiBaseUrl}${appAuthPath}?app_id=${app.objectID}&auth=${authToken}&source=shuffle`;
281
+ if (multiSelect) {
282
+ const isAlreadySelected = internalSelectedApps.some((a) => a.objectID === app.objectID);
283
+ const newSelection = isAlreadySelected ? internalSelectedApps.filter((a) => a.objectID !== app.objectID) : [...internalSelectedApps, app];
284
+ setInternalSelectedApps(newSelection);
285
+ onSelectionChange?.(newSelection);
286
+ } else {
287
+ onAppSelected?.({ app, authUrl });
288
+ if (!preventDefault) {
289
+ window.open(authUrl, "_blank");
290
+ }
291
+ setIsOpen(false);
292
+ setQuery("");
293
+ setResults([]);
294
+ }
295
+ }, [authToken, apiBaseUrl, appAuthPath, multiSelect, internalSelectedApps, onAppSelected, onSelectionChange, preventDefault]);
296
+ const handleKeyDown = (0, import_react.useCallback)((event) => {
297
+ if (!isOpen) return;
298
+ switch (event.key) {
299
+ case "ArrowDown":
300
+ event.preventDefault();
301
+ setSelectedIndex((prev) => Math.min(prev + 1, displayResults.length - 1));
302
+ break;
303
+ case "ArrowUp":
304
+ event.preventDefault();
305
+ setSelectedIndex((prev) => Math.max(prev - 1, -1));
306
+ break;
307
+ case "Enter":
308
+ event.preventDefault();
309
+ if (selectedIndex >= 0 && displayResults[selectedIndex]) {
310
+ selectApp(displayResults[selectedIndex]);
311
+ }
312
+ break;
313
+ case "Escape":
314
+ event.preventDefault();
315
+ setIsOpen(false);
316
+ setSelectedIndex(-1);
317
+ inputRef.current?.blur();
318
+ break;
319
+ }
320
+ }, [isOpen, results, selectedIndex, selectApp]);
321
+ const isAppSelected = (0, import_react.useCallback)((app) => {
322
+ return internalSelectedApps.some((a) => a.objectID === app.objectID);
323
+ }, [internalSelectedApps]);
324
+ const filteredPrivateApps = (0, import_react.useMemo)(() => {
325
+ if (privateApps.length === 0) return [];
326
+ const q = query.trim().toLowerCase();
327
+ if (!q) return privateApps;
328
+ return privateApps.filter(
329
+ (a) => a.name.toLowerCase().includes(q) || (a.description || "").toLowerCase().includes(q) || (a.categories || []).some((c) => c.toLowerCase().includes(q))
330
+ );
331
+ }, [privateApps, query]);
332
+ const displayResults = (0, import_react.useMemo)(() => {
333
+ const norm = (n) => (n || "").toLowerCase().replace(/[\s_\-]+/g, "");
334
+ let merged;
335
+ if (sourceFilter === "public") {
336
+ merged = results;
337
+ } else if (sourceFilter === "private") {
338
+ merged = filteredPrivateApps;
339
+ } else {
340
+ const privateNames = new Set(filteredPrivateApps.map((a) => norm(a.name)));
341
+ const publicOnly = results.filter((a) => !privateNames.has(norm(a.name)));
342
+ merged = [...filteredPrivateApps, ...publicOnly];
343
+ }
344
+ if (!pinnedApps || pinnedApps.length === 0) return merged;
345
+ const pinnedNames = new Set(pinnedApps.map((a) => norm(a.name)));
346
+ return [...pinnedApps, ...merged.filter((a) => !pinnedNames.has(norm(a.name)))];
347
+ }, [pinnedApps, results, filteredPrivateApps, sourceFilter]);
348
+ const getGridColumnsStyle = (0, import_react.useMemo)(() => {
349
+ if (layout !== "grid") return {};
350
+ const cols = typeof gridColumns === "number" ? gridColumns : gridColumns.md || 3;
351
+ return { "--singul-grid-columns": cols };
352
+ }, [layout, gridColumns]);
353
+ const renderAppItem = (app, index) => {
354
+ const selected = isAppSelected(app);
355
+ const isHighlighted = index === selectedIndex;
356
+ const authState = getAppAuthState(app);
357
+ if (renderItem) {
358
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
359
+ "div",
360
+ {
361
+ onClick: () => selectApp(app),
362
+ onMouseEnter: () => setSelectedIndex(index),
363
+ children: renderItem(app, selected, () => selectApp(app), authState)
364
+ },
365
+ app.objectID
366
+ );
367
+ }
368
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
369
+ "div",
370
+ {
371
+ className: `singul-dropdown-item ${selected ? "singul-selected" : ""} ${authState.validated ? "singul-validated" : ""} ${authState.configured ? "singul-configured" : ""}`,
372
+ style: {
373
+ ...customStyles.dropdownItem,
374
+ ...selected ? customStyles.selectedItem : {}
375
+ },
376
+ onClick: () => selectApp(app),
377
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "singul-app-info", style: customStyles.appInfo, children: [
378
+ app.image_url && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { position: "relative", flexShrink: 0 }, children: [
379
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
380
+ "img",
381
+ {
382
+ src: app.image_url,
383
+ alt: app.name,
384
+ className: "singul-app-icon",
385
+ style: customStyles.appIcon
386
+ }
387
+ ),
388
+ !hideAuthStatus && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
389
+ "span",
390
+ {
391
+ className: `singul-status-dot ${authState.validated ? "singul-dot-validated" : authState.configured ? "singul-dot-configured" : selected ? "singul-dot-activated" : "singul-dot-inactive"}`
392
+ }
393
+ )
394
+ ] }),
395
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "singul-app-details", style: customStyles.appDetails, children: [
396
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "singul-app-name", style: customStyles.appName, children: [
397
+ app.name.replace(/_/g, " "),
398
+ app.source === "private" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "singul-private-badge", title: "From your private apps", children: "Private" })
399
+ ] }),
400
+ showDescription && app.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "singul-app-description", style: customStyles.appDescription, children: app.description }),
401
+ showCategories && app.categories?.[0] && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "singul-app-category", style: customStyles.appCategory, children: app.categories[0] })
402
+ ] }),
403
+ showCheckbox && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
404
+ "div",
405
+ {
406
+ className: `singul-checkbox ${selected ? "singul-checked" : ""}`,
407
+ style: selected ? customStyles.checkboxChecked : customStyles.checkbox,
408
+ children: selected && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "20 6 9 17 4 12" }) })
409
+ }
410
+ )
411
+ ] })
412
+ },
413
+ app.objectID
414
+ );
415
+ };
416
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
417
+ "div",
418
+ {
419
+ ref: containerRef,
420
+ className: `singul-container ${inline ? "singul-inline" : ""} ${className}`,
421
+ style: customStyles.container,
422
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "singul-search-bar-container", children: [
423
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "singul-search-input-wrapper", style: customStyles.inputWrapper, children: [
424
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
425
+ "input",
426
+ {
427
+ ref: inputRef,
428
+ type: "text",
429
+ className: "singul-search-input",
430
+ style: customStyles.input,
431
+ placeholder,
432
+ value: query,
433
+ onChange: handleInputChange,
434
+ onFocus: () => {
435
+ if (!inline && query.trim() && results.length > 0) {
436
+ setIsOpen(true);
437
+ }
438
+ },
439
+ onKeyDown: handleKeyDown,
440
+ autoComplete: "off"
441
+ }
442
+ ),
443
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "singul-search-icon", style: customStyles.searchIcon, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
444
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "11", cy: "11", r: "8" }),
445
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "m21 21-4.35-4.35" })
446
+ ] }) }),
447
+ isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "singul-loading-spinner", style: customStyles.loadingSpinner, children: renderLoadingState ? renderLoadingState() : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "singul-spinner", style: customStyles.spinner }) })
448
+ ] }),
449
+ showSourceFilter && privateApps.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "singul-source-filter", role: "tablist", "aria-label": "Filter by app source", children: [
450
+ { key: "all", label: "All", count: results.length + filteredPrivateApps.length },
451
+ { key: "public", label: "Public", count: results.length },
452
+ { key: "private", label: "Private", count: filteredPrivateApps.length }
453
+ ].map((opt) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
454
+ "button",
455
+ {
456
+ type: "button",
457
+ role: "tab",
458
+ "aria-selected": sourceFilter === opt.key,
459
+ className: `singul-source-pill ${sourceFilter === opt.key ? "singul-source-pill-active" : ""}`,
460
+ onClick: () => setSourceFilter(opt.key),
461
+ children: [
462
+ opt.label,
463
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "singul-source-count", children: opt.count })
464
+ ]
465
+ },
466
+ opt.key
467
+ )) }),
468
+ inline && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
469
+ "div",
470
+ {
471
+ className: `singul-results-container ${layout === "grid" ? "singul-results-grid" : ""}`,
472
+ style: {
473
+ ...customStyles.resultsContainer,
474
+ ...getGridColumnsStyle,
475
+ ...layout === "grid" ? { display: "grid" } : {}
476
+ },
477
+ children: displayResults.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
478
+ displayResults.map((app, index) => renderAppItem(app, index)),
479
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "singul-end-of-results", style: {
480
+ padding: "10px 16px",
481
+ color: "rgba(255, 255, 255, 0.3)",
482
+ textAlign: "center",
483
+ fontSize: "11px",
484
+ gridColumn: "1 / -1"
485
+ }, children: "Can't find what you're looking for? Try a different search term." })
486
+ ] }) : query.trim() ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "singul-empty-state", style: customStyles.emptyState, children: renderEmptyState ? renderEmptyState() : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
487
+ 'No integrations match "',
488
+ query,
489
+ '". Try searching for a different app or category.'
490
+ ] }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "singul-empty-state", style: customStyles.emptyState, children: "Start typing to search integrations..." })
491
+ }
492
+ ),
493
+ !inline && isOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
494
+ "div",
495
+ {
496
+ className: `singul-dropdown ${layout === "grid" ? "singul-dropdown-grid" : ""}`,
497
+ style: {
498
+ ...customStyles.dropdown,
499
+ ...getGridColumnsStyle,
500
+ ...layout === "grid" ? { display: "grid" } : {}
501
+ },
502
+ children: displayResults.length > 0 ? displayResults.map((app, index) => renderAppItem(app, index)) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "singul-empty-state", style: customStyles.emptyState, children: renderEmptyState ? renderEmptyState() : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
503
+ 'No apps found for "',
504
+ query,
505
+ '"'
506
+ ] }) })
507
+ }
508
+ )
509
+ ] })
510
+ }
511
+ );
512
+ });
513
+ SingulJS.displayName = "SingulJS";
514
+ var SingulJS_default = SingulJS;
515
+ // Annotate the CommonJS export names for ESM import in node:
516
+ 0 && (module.exports = {
517
+ SingulJS
518
+ });
519
+ //# sourceMappingURL=index.cjs.map