@tangle-network/sandbox-ui 0.91.3 → 0.91.5

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/dist/globals.css CHANGED
@@ -1945,6 +1945,12 @@
1945
1945
  border-color: color-mix(in oklab, var(--color-destructive) 30%, transparent);
1946
1946
  }
1947
1947
  }
1948
+ .border-destructive\/40 {
1949
+ border-color: color-mix(in srgb, hsl(var(--destructive)) 40%, transparent);
1950
+ @supports (color: color-mix(in lab, red, red)) {
1951
+ border-color: color-mix(in oklab, var(--color-destructive) 40%, transparent);
1952
+ }
1953
+ }
1948
1954
  .border-destructive\/50 {
1949
1955
  border-color: color-mix(in srgb, hsl(var(--destructive)) 50%, transparent);
1950
1956
  @supports (color: color-mix(in lab, red, red)) {
@@ -93,9 +93,15 @@ interface IntegrationsPanelProps {
93
93
  featuredIds?: string[];
94
94
  /** Initial sort mode. Defaults to "featured". */
95
95
  defaultSort?: IntegrationSort;
96
+ /**
97
+ * Tiles the loading skeleton draws. A skeleton exists to reserve the space
98
+ * the real content will take, so pass the catalog size you expect when the
99
+ * consumer knows it; the default fills two rows at the widest breakpoint.
100
+ */
101
+ skeletonCount?: number;
96
102
  className?: string;
97
103
  }
98
- declare function IntegrationsPanel({ catalog, connections, healthByConnectionId, isLoading, error, onConnect, onDisconnect, getManageHref, onManage, emptyCatalogLabel, featuredIds, defaultSort, className, }: IntegrationsPanelProps): React.JSX.Element;
104
+ declare function IntegrationsPanel({ catalog, connections, healthByConnectionId, isLoading, error, onConnect, onDisconnect, getManageHref, onManage, emptyCatalogLabel, featuredIds, defaultSort, skeletonCount, className, }: IntegrationsPanelProps): React.JSX.Element;
99
105
 
100
106
  /**
101
107
  * Endpoint contract expected on the consumer app's server (which
@@ -64,6 +64,60 @@ var DEFAULT_FEATURED_IDS = [
64
64
  function defaultConnectorOf(provider) {
65
65
  return provider.connectors?.[0]?.connectorId ?? provider.providerId;
66
66
  }
67
+ var TILE_GRID_CLASS = "grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6";
68
+ var DEFAULT_SKELETON_COUNT = 12;
69
+ var TILE_BOX_CLASS = "flex min-w-0 aspect-square flex-col items-center justify-center gap-2 rounded-xl border p-3 text-center";
70
+ function IntegrationsToolbar({
71
+ query,
72
+ onQueryChange,
73
+ sort,
74
+ onSortChange
75
+ }) {
76
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between", children: [
77
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 items-center gap-2 rounded-lg border border-border bg-card px-3 py-2", children: [
78
+ /* @__PURE__ */ jsx(Search, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
79
+ /* @__PURE__ */ jsx(
80
+ "input",
81
+ {
82
+ type: "text",
83
+ value: query,
84
+ onChange: (e) => onQueryChange(e.target.value),
85
+ placeholder: "Search integrations...",
86
+ autoFocus: true,
87
+ "data-testid": "integration-search",
88
+ className: "flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
89
+ }
90
+ )
91
+ ] }),
92
+ /* @__PURE__ */ jsx(
93
+ "div",
94
+ {
95
+ role: "tablist",
96
+ "aria-label": "Sort integrations",
97
+ className: "flex shrink-0 items-center gap-1 rounded-lg border border-border bg-card p-1",
98
+ children: [
99
+ ["featured", "Featured"],
100
+ ["alpha", "A\u2013Z"]
101
+ ].map(([value, label]) => /* @__PURE__ */ jsx(
102
+ "button",
103
+ {
104
+ type: "button",
105
+ role: "tab",
106
+ "aria-selected": sort === value,
107
+ onClick: () => onSortChange(value),
108
+ "data-testid": `sort-${value}`,
109
+ className: cn(
110
+ "rounded-md px-2.5 py-1 text-xs font-medium transition-colors",
111
+ sort === value ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground"
112
+ ),
113
+ children: label
114
+ },
115
+ value
116
+ ))
117
+ }
118
+ )
119
+ ] });
120
+ }
67
121
  var LOGO_SIZE = 48;
68
122
  function buildConnectionIndex(connections) {
69
123
  const index = /* @__PURE__ */ new Map();
@@ -109,6 +163,7 @@ function IntegrationsPanel({
109
163
  emptyCatalogLabel = "No integrations available yet.",
110
164
  featuredIds = DEFAULT_FEATURED_IDS,
111
165
  defaultSort = "featured",
166
+ skeletonCount = DEFAULT_SKELETON_COUNT,
112
167
  className
113
168
  }) {
114
169
  const [query, setQuery] = React.useState("");
@@ -147,6 +202,39 @@ function IntegrationsPanel({
147
202
  setDisconnectTarget(null);
148
203
  setDisconnectError(null);
149
204
  }, []);
205
+ const [connectingId, setConnectingId] = React.useState(null);
206
+ const [connectError, setConnectError] = React.useState(null);
207
+ const activeConnect = React.useRef(0);
208
+ const startConnect = React.useCallback(
209
+ async (input) => {
210
+ const reqId = activeConnect.current += 1;
211
+ setConnectError(null);
212
+ let pending;
213
+ try {
214
+ pending = onConnect(input);
215
+ } catch (e) {
216
+ setConnectError(
217
+ e instanceof Error ? e.message : "Failed to start the connection."
218
+ );
219
+ return;
220
+ }
221
+ if (typeof pending?.then !== "function") {
222
+ return;
223
+ }
224
+ setConnectingId(input.providerId);
225
+ try {
226
+ await pending;
227
+ } catch (e) {
228
+ if (activeConnect.current !== reqId) return;
229
+ setConnectError(
230
+ e instanceof Error ? e.message : "Failed to start the connection."
231
+ );
232
+ } finally {
233
+ if (activeConnect.current === reqId) setConnectingId(null);
234
+ }
235
+ },
236
+ [onConnect]
237
+ );
150
238
  const confirmDisconnect = React.useCallback(async () => {
151
239
  if (!disconnectTarget) return;
152
240
  const reqId = activeDisconnect.current += 1;
@@ -172,22 +260,38 @@ function IntegrationsPanel({
172
260
  ] }) }) });
173
261
  }
174
262
  if (isLoading && catalog.length === 0) {
175
- return /* @__PURE__ */ jsx(
176
- "div",
177
- {
178
- className: cn(
179
- "grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6",
180
- className
181
- ),
182
- children: Array.from({ length: 12 }).map((_, i) => /* @__PURE__ */ jsx(
183
- "div",
184
- {
185
- className: "aspect-square animate-pulse rounded-xl border border-border bg-muted/40"
186
- },
187
- i
188
- ))
189
- }
190
- );
263
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), "data-testid": "integrations-skeleton", children: [
264
+ /* @__PURE__ */ jsx(
265
+ IntegrationsToolbar,
266
+ {
267
+ query,
268
+ onQueryChange: setQuery,
269
+ sort,
270
+ onSortChange: setSort
271
+ }
272
+ ),
273
+ /* @__PURE__ */ jsx("div", { className: TILE_GRID_CLASS, children: Array.from({ length: skeletonCount }).map((_, i) => /* @__PURE__ */ jsxs(
274
+ "div",
275
+ {
276
+ "aria-hidden": true,
277
+ className: cn(
278
+ TILE_BOX_CLASS,
279
+ "animate-pulse border-border bg-muted/40"
280
+ ),
281
+ children: [
282
+ /* @__PURE__ */ jsx(
283
+ "div",
284
+ {
285
+ className: "shrink-0 rounded-2xl bg-muted",
286
+ style: { width: LOGO_SIZE, height: LOGO_SIZE }
287
+ }
288
+ ),
289
+ /* @__PURE__ */ jsx("div", { className: "flex h-8 w-full items-center justify-center", children: /* @__PURE__ */ jsx("div", { className: "h-2.5 w-3/4 rounded bg-muted" }) })
290
+ ]
291
+ },
292
+ i
293
+ )) })
294
+ ] });
191
295
  }
192
296
  if (catalog.length === 0) {
193
297
  return /* @__PURE__ */ jsx(
@@ -200,58 +304,32 @@ function IntegrationsPanel({
200
304
  );
201
305
  }
202
306
  const disconnectAccountLabel = disconnectTarget?.accountDisplay;
203
- return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), children: [
204
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between", children: [
205
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 items-center gap-2 rounded-lg border border-border bg-card px-3 py-2", children: [
206
- /* @__PURE__ */ jsx(Search, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
207
- /* @__PURE__ */ jsx(
208
- "input",
209
- {
210
- type: "text",
211
- value: query,
212
- onChange: (e) => setQuery(e.target.value),
213
- placeholder: "Search integrations...",
214
- autoFocus: true,
215
- "data-testid": "integration-search",
216
- className: "flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
217
- }
218
- )
219
- ] }),
220
- /* @__PURE__ */ jsx(
221
- "div",
222
- {
223
- role: "tablist",
224
- "aria-label": "Sort integrations",
225
- className: "flex shrink-0 items-center gap-1 rounded-lg border border-border bg-card p-1",
226
- children: [
227
- ["featured", "Featured"],
228
- ["alpha", "A\u2013Z"]
229
- ].map(([value, label]) => /* @__PURE__ */ jsx(
230
- "button",
231
- {
232
- type: "button",
233
- role: "tab",
234
- "aria-selected": sort === value,
235
- onClick: () => setSort(value),
236
- "data-testid": `sort-${value}`,
237
- className: cn(
238
- "rounded-md px-2.5 py-1 text-xs font-medium transition-colors",
239
- sort === value ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground"
240
- ),
241
- children: label
242
- },
243
- value
244
- ))
245
- }
246
- )
247
- ] }),
307
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), "data-testid": "integrations-panel", children: [
308
+ /* @__PURE__ */ jsx(
309
+ IntegrationsToolbar,
310
+ {
311
+ query,
312
+ onQueryChange: setQuery,
313
+ sort,
314
+ onSortChange: setSort
315
+ }
316
+ ),
317
+ connectError ? /* @__PURE__ */ jsx(
318
+ "div",
319
+ {
320
+ role: "alert",
321
+ "data-testid": "integration-connect-error",
322
+ className: "mb-3 rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive",
323
+ children: connectError
324
+ }
325
+ ) : null,
248
326
  visible.length === 0 ? /* @__PURE__ */ jsx(
249
327
  EmptyState,
250
328
  {
251
329
  title: "No matches",
252
330
  description: `No integrations match "${query.trim()}".`
253
331
  }
254
- ) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6", children: visible.map((provider) => {
332
+ ) : /* @__PURE__ */ jsx("div", { className: TILE_GRID_CLASS, children: visible.map((provider) => {
255
333
  const connectorId = defaultConnectorOf(provider);
256
334
  const live = connectionIndex.get(
257
335
  `${provider.providerId}:${connectorId}`
@@ -268,7 +346,8 @@ function IntegrationsPanel({
268
346
  "data-connected": "true",
269
347
  title: live.accountDisplay ? `${name} \u2014 ${live.accountDisplay}` : name,
270
348
  className: cn(
271
- "group relative flex min-w-0 aspect-square flex-col items-center justify-center gap-2 rounded-xl border p-3 text-center",
349
+ TILE_BOX_CLASS,
350
+ "group relative",
272
351
  "border-[var(--surface-success-border)] bg-[var(--surface-success-bg)]"
273
352
  ),
274
353
  children: [
@@ -393,10 +472,19 @@ function IntegrationsPanel({
393
472
  type: "button",
394
473
  "data-testid": `integration-${provider.providerId}`,
395
474
  "data-connected": "false",
396
- onClick: () => onConnect({ providerId: provider.providerId, connectorId }),
475
+ "data-connecting": connectingId === provider.providerId ? "true" : void 0,
476
+ disabled: connectingId === provider.providerId,
477
+ "aria-busy": connectingId === provider.providerId,
478
+ onClick: () => {
479
+ void startConnect({
480
+ providerId: provider.providerId,
481
+ connectorId
482
+ });
483
+ },
397
484
  title: provider.description ? provider.description : `Connect ${name}`,
398
485
  className: cn(
399
- "group flex min-w-0 aspect-square flex-col items-center justify-center gap-2 rounded-xl border border-border bg-card p-3 text-center transition-all",
486
+ TILE_BOX_CLASS,
487
+ "group border-border bg-card transition-all",
400
488
  "hover:border-primary/40 hover:bg-accent/40 hover:shadow-sm focus:outline-none focus-visible:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary/20"
401
489
  ),
402
490
  children: [
package/dist/styles.css CHANGED
@@ -1945,6 +1945,12 @@
1945
1945
  border-color: color-mix(in oklab, var(--color-destructive) 30%, transparent);
1946
1946
  }
1947
1947
  }
1948
+ .border-destructive\/40 {
1949
+ border-color: color-mix(in srgb, hsl(var(--destructive)) 40%, transparent);
1950
+ @supports (color: color-mix(in lab, red, red)) {
1951
+ border-color: color-mix(in oklab, var(--color-destructive) 40%, transparent);
1952
+ }
1953
+ }
1948
1954
  .border-destructive\/50 {
1949
1955
  border-color: color-mix(in srgb, hsl(var(--destructive)) 50%, transparent);
1950
1956
  @supports (color: color-mix(in lab, red, red)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox-ui",
3
- "version": "0.91.3",
3
+ "version": "0.91.5",
4
4
  "description": "Unified UI component library for Tangle Sandbox — primitives, chat, dashboard, terminal, editor, and workspace components",
5
5
  "repository": {
6
6
  "type": "git",