@stigmer/react 0.0.83 → 0.0.84

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 (58) hide show
  1. package/index.d.ts +3 -3
  2. package/index.d.ts.map +1 -1
  3. package/index.js +1 -1
  4. package/index.js.map +1 -1
  5. package/library/ResourceListView.d.ts +57 -7
  6. package/library/ResourceListView.d.ts.map +1 -1
  7. package/library/ResourceListView.js +147 -37
  8. package/library/ResourceListView.js.map +1 -1
  9. package/library/index.d.ts +1 -1
  10. package/library/index.d.ts.map +1 -1
  11. package/library/index.js.map +1 -1
  12. package/mcp-server/McpServerConfigPanel.d.ts +45 -0
  13. package/mcp-server/McpServerConfigPanel.d.ts.map +1 -1
  14. package/mcp-server/McpServerConfigPanel.js +90 -14
  15. package/mcp-server/McpServerConfigPanel.js.map +1 -1
  16. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  17. package/mcp-server/McpServerDetailView.js +168 -23
  18. package/mcp-server/McpServerDetailView.js.map +1 -1
  19. package/mcp-server/McpServerPicker.js +3 -3
  20. package/mcp-server/McpServerPicker.js.map +1 -1
  21. package/mcp-server/OAuthAppForm.d.ts +58 -0
  22. package/mcp-server/OAuthAppForm.d.ts.map +1 -0
  23. package/mcp-server/OAuthAppForm.js +67 -0
  24. package/mcp-server/OAuthAppForm.js.map +1 -0
  25. package/mcp-server/index.d.ts +6 -0
  26. package/mcp-server/index.d.ts.map +1 -1
  27. package/mcp-server/index.js +3 -0
  28. package/mcp-server/index.js.map +1 -1
  29. package/mcp-server/useDisconnectOAuth.d.ts +40 -0
  30. package/mcp-server/useDisconnectOAuth.d.ts.map +1 -0
  31. package/mcp-server/useDisconnectOAuth.js +46 -0
  32. package/mcp-server/useDisconnectOAuth.js.map +1 -0
  33. package/mcp-server/useMcpServerCredentials.d.ts +48 -0
  34. package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
  35. package/mcp-server/useMcpServerCredentials.js +18 -2
  36. package/mcp-server/useMcpServerCredentials.js.map +1 -1
  37. package/mcp-server/useOAuthGrantStatus.d.ts +9 -0
  38. package/mcp-server/useOAuthGrantStatus.d.ts.map +1 -1
  39. package/mcp-server/useOAuthGrantStatus.js +6 -1
  40. package/mcp-server/useOAuthGrantStatus.js.map +1 -1
  41. package/mcp-server/useOrgOAuthApp.d.ts +82 -0
  42. package/mcp-server/useOrgOAuthApp.d.ts.map +1 -0
  43. package/mcp-server/useOrgOAuthApp.js +160 -0
  44. package/mcp-server/useOrgOAuthApp.js.map +1 -0
  45. package/package.json +4 -4
  46. package/src/index.ts +3 -0
  47. package/src/library/ResourceListView.tsx +303 -46
  48. package/src/library/index.ts +4 -1
  49. package/src/mcp-server/McpServerConfigPanel.tsx +370 -45
  50. package/src/mcp-server/McpServerDetailView.tsx +447 -47
  51. package/src/mcp-server/McpServerPicker.tsx +3 -3
  52. package/src/mcp-server/OAuthAppForm.tsx +304 -0
  53. package/src/mcp-server/index.ts +9 -0
  54. package/src/mcp-server/useDisconnectOAuth.ts +76 -0
  55. package/src/mcp-server/useMcpServerCredentials.ts +70 -2
  56. package/src/mcp-server/useOAuthGrantStatus.ts +19 -1
  57. package/src/mcp-server/useOrgOAuthApp.ts +250 -0
  58. package/styles.css +1 -1
@@ -10,8 +10,12 @@ import type { ResourceListScope } from "../search";
10
10
 
11
11
  const DEBOUNCE_MS = 300;
12
12
  const SKELETON_COUNT = 5;
13
+ const GRID_SKELETON_COUNT = 6;
13
14
  const MAX_VISIBLE_TAGS = 3;
14
15
 
16
+ /** Layout mode for {@link ResourceListView}. */
17
+ export type ResourceListLayout = "list" | "grid";
18
+
15
19
  /** Props for {@link ResourceListView}. */
16
20
  export interface ResourceListViewProps {
17
21
  /** Resource entries to display. */
@@ -53,12 +57,35 @@ export interface ResourceListViewProps {
53
57
  * Also called with `1` automatically when the search query or scope changes.
54
58
  */
55
59
  readonly onPageChange?: (page: number) => void;
60
+ /**
61
+ * Visual layout for items.
62
+ *
63
+ * - `"list"` (default) — vertical single-column rows
64
+ * - `"grid"` — responsive multi-column card grid
65
+ * (`grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`)
66
+ *
67
+ * When `layout` is `"grid"` and no `renderItem` is provided, the built-in
68
+ * {@link DefaultResourceCard} is used instead of {@link DefaultResourceRow}.
69
+ *
70
+ * @default "list"
71
+ */
72
+ readonly layout?: ResourceListLayout;
56
73
  /**
57
74
  * Custom renderer for list items. Receives the `SearchResult` and its
58
75
  * index. Falls back to a built-in row showing name, org, description,
59
76
  * visibility badge, and tags.
60
77
  */
61
78
  readonly renderItem?: (item: SearchResult, index: number) => React.ReactNode;
79
+ /**
80
+ * Renders an action element (e.g. a button) for each item.
81
+ *
82
+ * In grid mode the action is placed in the card's top-right corner.
83
+ * In list mode it is appended after the row content.
84
+ *
85
+ * The consumer is responsible for calling `e.stopPropagation()` if
86
+ * the action should not also trigger `onItemClick`.
87
+ */
88
+ readonly renderItemAction?: (item: SearchResult) => React.ReactNode;
62
89
  /**
63
90
  * Called when a list item is clicked or activated via keyboard (Enter/Space).
64
91
  * Providing this makes items interactive with hover/focus styles and
@@ -80,19 +107,28 @@ export interface ResourceListViewProps {
80
107
  }
81
108
 
82
109
  /**
83
- * Paginated, searchable list view for browsing Stigmer resources.
110
+ * Paginated, searchable view for browsing Stigmer resources.
111
+ *
112
+ * Supports two layout modes:
113
+ *
114
+ * - **`"list"`** (default) — vertical single-column rows, same as
115
+ * before. Each row shows a kind icon, name, org, description, and tags.
116
+ * - **`"grid"`** — responsive multi-column card grid
117
+ * (`grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`). Each card shows a
118
+ * large icon container, name, org, description, and an optional
119
+ * action slot in the top-right corner.
84
120
  *
85
- * Renders the standard Library list chrome: a toolbar with optional
86
- * search input and scope toggle, a list of resource rows with
87
- * loading/error/empty states, and optional pagination controls.
121
+ * Both modes share the same toolbar (search, scope toggle), pagination,
122
+ * loading/error/empty states, and keyboard navigation (grid mode adds
123
+ * ArrowLeft/Right and column-aware Up/Down).
88
124
  *
89
125
  * The component is controlled — the parent owns data and filter state.
90
126
  * Search debouncing (300ms) is managed internally so the parent only
91
127
  * receives debounced query values via {@link ResourceListViewProps.onSearchChange}.
92
128
  *
93
129
  * Only `items` and `isLoading` are required. Search, scope toggle,
94
- * pagination, and custom row rendering activate progressively when
95
- * their corresponding props are provided.
130
+ * pagination, layout mode, and custom row rendering activate
131
+ * progressively when their corresponding props are provided.
96
132
  *
97
133
  * When the debounced search query or scope changes, the component
98
134
  * automatically resets the page to 1 via `onPageChange` to prevent
@@ -106,6 +142,22 @@ export interface ResourceListViewProps {
106
142
  *
107
143
  * @example
108
144
  * ```tsx
145
+ * // Grid layout with action button
146
+ * <ResourceListView
147
+ * layout="grid"
148
+ * items={mcpServers}
149
+ * isLoading={isLoading}
150
+ * onItemClick={(item) => navigate(item.slug)}
151
+ * renderItemAction={(item) => (
152
+ * <button onClick={(e) => { e.stopPropagation(); connect(item); }}>
153
+ * <PlusIcon />
154
+ * </button>
155
+ * )}
156
+ * />
157
+ * ```
158
+ *
159
+ * @example
160
+ * ```tsx
109
161
  * // Full — search, scope toggle, pagination, and click handling
110
162
  * const [scope, setScope] = useState<ResourceListScope>("org");
111
163
  * const [query, setQuery] = useState("");
@@ -146,7 +198,9 @@ export function ResourceListView({
146
198
  scope,
147
199
  onScopeChange,
148
200
  onPageChange,
201
+ layout = "list",
149
202
  renderItem,
203
+ renderItemAction,
150
204
  onItemClick,
151
205
  emptyIcon,
152
206
  emptyTitle = "No resources found",
@@ -155,6 +209,7 @@ export function ResourceListView({
155
209
  className,
156
210
  "aria-label": ariaLabel = "Resource list",
157
211
  }: ResourceListViewProps) {
212
+ const isGrid = layout === "grid";
158
213
  const showToolbar =
159
214
  !!onSearchChange || (scope !== undefined && !!onScopeChange);
160
215
  const showPagination = !!onPageChange && totalPages > 1;
@@ -198,8 +253,46 @@ export function ResourceListView({
198
253
 
199
254
  // --- Keyboard navigation for interactive items ----------------------
200
255
  // Implements roving tabindex: only the focused item has tabIndex 0,
201
- // all others have -1. Arrow Up/Down moves focus between rows.
256
+ // all others have -1. Arrow keys move focus between items.
257
+ // In list mode: Up/Down. In grid mode: all four arrow keys with
258
+ // column-aware wrapping.
202
259
  const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
260
+ const gridColumnsRef = useRef(1);
261
+
262
+ const moveFocus = useCallback(
263
+ (from: HTMLDivElement, toIndex: number) => {
264
+ const clamped = Math.max(0, Math.min(toIndex, items.length - 1));
265
+ const el = itemRefs.current[clamped];
266
+ if (el && el !== from) {
267
+ from.tabIndex = -1;
268
+ el.tabIndex = 0;
269
+ el.focus();
270
+ }
271
+ },
272
+ [items.length],
273
+ );
274
+
275
+ const detectGridColumns = useCallback(() => {
276
+ if (!isGrid) return 1;
277
+ const first = itemRefs.current[0];
278
+ const second = itemRefs.current[1];
279
+ if (!first || !second) return 1;
280
+ const firstRect = first.getBoundingClientRect();
281
+ const secondRect = second.getBoundingClientRect();
282
+ if (Math.abs(firstRect.top - secondRect.top) < 4) {
283
+ let cols = 1;
284
+ for (let i = 1; i < itemRefs.current.length; i++) {
285
+ const r = itemRefs.current[i]?.getBoundingClientRect();
286
+ if (r && Math.abs(r.top - firstRect.top) < 4) {
287
+ cols++;
288
+ } else {
289
+ break;
290
+ }
291
+ }
292
+ return cols;
293
+ }
294
+ return 1;
295
+ }, [isGrid]);
203
296
 
204
297
  const handleItemKeyDown = useCallback(
205
298
  (
@@ -210,31 +303,41 @@ export function ResourceListView({
210
303
  if (e.key === "Enter" || e.key === " ") {
211
304
  e.preventDefault();
212
305
  onItemClick?.(item);
213
- } else if (e.key === "ArrowDown") {
214
- e.preventDefault();
215
- const next = Math.min(index + 1, items.length - 1);
216
- if (next !== index) {
217
- e.currentTarget.tabIndex = -1;
218
- const el = itemRefs.current[next];
219
- if (el) {
220
- el.tabIndex = 0;
221
- el.focus();
306
+ return;
307
+ }
308
+
309
+ const cols = isGrid ? detectGridColumns() : 1;
310
+ gridColumnsRef.current = cols;
311
+ let target = index;
312
+
313
+ switch (e.key) {
314
+ case "ArrowDown":
315
+ e.preventDefault();
316
+ target = isGrid ? Math.min(index + cols, items.length - 1) : Math.min(index + 1, items.length - 1);
317
+ break;
318
+ case "ArrowUp":
319
+ e.preventDefault();
320
+ target = isGrid ? Math.max(index - cols, 0) : Math.max(index - 1, 0);
321
+ break;
322
+ case "ArrowRight":
323
+ if (isGrid) {
324
+ e.preventDefault();
325
+ target = Math.min(index + 1, items.length - 1);
222
326
  }
223
- }
224
- } else if (e.key === "ArrowUp") {
225
- e.preventDefault();
226
- const prev = Math.max(index - 1, 0);
227
- if (prev !== index) {
228
- e.currentTarget.tabIndex = -1;
229
- const el = itemRefs.current[prev];
230
- if (el) {
231
- el.tabIndex = 0;
232
- el.focus();
327
+ break;
328
+ case "ArrowLeft":
329
+ if (isGrid) {
330
+ e.preventDefault();
331
+ target = Math.max(index - 1, 0);
233
332
  }
234
- }
333
+ break;
334
+ default:
335
+ return;
235
336
  }
337
+
338
+ if (target !== index) moveFocus(e.currentTarget, target);
236
339
  },
237
- [onItemClick, items.length],
340
+ [onItemClick, items.length, isGrid, detectGridColumns, moveFocus],
238
341
  );
239
342
 
240
343
  // --- Content resolution ---------------------------------------------
@@ -270,7 +373,7 @@ export function ResourceListView({
270
373
  </div>
271
374
  )}
272
375
 
273
- {showSkeletons && <SkeletonRows />}
376
+ {showSkeletons && (isGrid ? <SkeletonCards /> : <SkeletonRows />)}
274
377
 
275
378
  {showError && <ErrorState message={error!} onRetry={onRetry} />}
276
379
 
@@ -288,17 +391,19 @@ export function ResourceListView({
288
391
  aria-label={ariaLabel}
289
392
  aria-busy={isLoading || undefined}
290
393
  className={cn(
291
- "flex flex-col",
394
+ isGrid
395
+ ? "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"
396
+ : "flex flex-col",
292
397
  isLoading &&
293
398
  "pointer-events-none opacity-60 transition-opacity",
294
399
  )}
295
400
  >
296
401
  {items.map((item, index) => {
297
- const content = renderItem ? (
298
- renderItem(item, index)
299
- ) : (
300
- <DefaultResourceRow item={item} />
301
- );
402
+ const content = renderItem
403
+ ? renderItem(item, index)
404
+ : isGrid
405
+ ? <DefaultResourceCard item={item} action={renderItemAction?.(item)} />
406
+ : <DefaultResourceRow item={item} action={renderItemAction?.(item)} />;
302
407
 
303
408
  return (
304
409
  <div key={item.id || `resource-${index}`} role="listitem">
@@ -312,15 +417,32 @@ export function ResourceListView({
312
417
  onClick={() => onItemClick!(item)}
313
418
  onKeyDown={(e) => handleItemKeyDown(e, index, item)}
314
419
  className={cn(
315
- "group rounded-lg px-3 py-2.5 transition-colors",
316
- "cursor-pointer hover:bg-accent/50",
317
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
420
+ "group transition-colors",
421
+ isGrid
422
+ ? [
423
+ "flex h-full rounded-lg border border-border bg-card p-4",
424
+ "cursor-pointer hover:border-primary/40 hover:bg-accent/30",
425
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
426
+ ]
427
+ : [
428
+ "rounded-lg px-3 py-2.5",
429
+ "cursor-pointer hover:bg-accent/50",
430
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
431
+ ],
318
432
  )}
319
433
  >
320
434
  {content}
321
435
  </div>
322
436
  ) : (
323
- <div className="px-3 py-2.5">{content}</div>
437
+ <div
438
+ className={cn(
439
+ isGrid
440
+ ? "flex h-full rounded-lg border border-border bg-card p-4"
441
+ : "px-3 py-2.5",
442
+ )}
443
+ >
444
+ {content}
445
+ </div>
324
446
  )}
325
447
  </div>
326
448
  );
@@ -344,21 +466,25 @@ export function ResourceListView({
344
466
  // Internal components
345
467
  // ---------------------------------------------------------------------------
346
468
 
347
- function DefaultResourceRow({ item }: { readonly item: SearchResult }) {
469
+ function DefaultResourceRow({
470
+ item,
471
+ action,
472
+ }: {
473
+ readonly item: SearchResult;
474
+ readonly action?: React.ReactNode;
475
+ }) {
348
476
  const displayName = item.name || item.slug;
349
477
 
350
478
  return (
351
479
  <div className="flex items-start gap-3">
352
- <KindIcon kind={item.kind} />
480
+ <RowIcon kind={item.kind} iconUrl={item.iconUrl} />
353
481
  <div className="min-w-0 flex-1">
354
482
  <div className="flex items-center gap-2">
355
483
  <span className="truncate text-sm font-medium text-foreground">
356
484
  {displayName}
357
485
  </span>
358
486
  {item.visibility === ApiResourceVisibility.visibility_public && (
359
- <span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
360
- Public
361
- </span>
487
+ <VisibilityBadge />
362
488
  )}
363
489
  </div>
364
490
  <div className="mt-0.5 flex items-start gap-1.5 text-xs text-muted-foreground">
@@ -392,12 +518,114 @@ function DefaultResourceRow({ item }: { readonly item: SearchResult }) {
392
518
  </div>
393
519
  )}
394
520
  </div>
521
+ {action && <div className="ml-auto shrink-0">{action}</div>}
522
+ </div>
523
+ );
524
+ }
525
+
526
+ function DefaultResourceCard({
527
+ item,
528
+ action,
529
+ }: {
530
+ readonly item: SearchResult;
531
+ readonly action?: React.ReactNode;
532
+ }) {
533
+ const displayName = item.name || item.slug;
534
+
535
+ return (
536
+ <div className="flex min-w-0 flex-1 flex-col gap-3">
537
+ <div className="flex items-start gap-3">
538
+ <ResourceIcon kind={item.kind} iconUrl={item.iconUrl} />
539
+ <div className="min-w-0 flex-1">
540
+ <div className="flex items-center gap-2">
541
+ <span className="truncate text-sm font-semibold text-foreground">
542
+ {displayName}
543
+ </span>
544
+ {item.visibility === ApiResourceVisibility.visibility_public && (
545
+ <VisibilityBadge />
546
+ )}
547
+ </div>
548
+ <span className="mt-0.5 block text-xs text-muted-foreground">
549
+ {item.org}
550
+ </span>
551
+ </div>
552
+ {action && <div className="shrink-0">{action}</div>}
553
+ </div>
554
+ {item.description && (
555
+ <p className="line-clamp-2 text-xs leading-relaxed text-muted-foreground">
556
+ {item.description}
557
+ </p>
558
+ )}
395
559
  </div>
396
560
  );
397
561
  }
398
562
 
399
- function KindIcon({ kind }: { readonly kind: ApiResourceKind }) {
400
- const cls = "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground";
563
+ function ResourceIcon({
564
+ kind,
565
+ iconUrl,
566
+ }: {
567
+ readonly kind: ApiResourceKind;
568
+ readonly iconUrl?: string;
569
+ }) {
570
+ const [imgError, setImgError] = useState(false);
571
+
572
+ return (
573
+ <span className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted">
574
+ {iconUrl && !imgError ? (
575
+ <img
576
+ src={iconUrl}
577
+ alt=""
578
+ className="size-6 rounded object-contain"
579
+ onError={() => setImgError(true)}
580
+ />
581
+ ) : (
582
+ <KindIcon kind={kind} size="lg" />
583
+ )}
584
+ </span>
585
+ );
586
+ }
587
+
588
+ function RowIcon({
589
+ kind,
590
+ iconUrl,
591
+ }: {
592
+ readonly kind: ApiResourceKind;
593
+ readonly iconUrl?: string;
594
+ }) {
595
+ const [imgError, setImgError] = useState(false);
596
+
597
+ if (iconUrl && !imgError) {
598
+ return (
599
+ <img
600
+ src={iconUrl}
601
+ alt=""
602
+ className="mt-0.5 h-4 w-4 shrink-0 rounded-sm object-contain"
603
+ onError={() => setImgError(true)}
604
+ />
605
+ );
606
+ }
607
+
608
+ return <KindIcon kind={kind} />;
609
+ }
610
+
611
+ function VisibilityBadge() {
612
+ return (
613
+ <span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
614
+ Public
615
+ </span>
616
+ );
617
+ }
618
+
619
+ function KindIcon({
620
+ kind,
621
+ size = "sm",
622
+ }: {
623
+ readonly kind: ApiResourceKind;
624
+ readonly size?: "sm" | "lg";
625
+ }) {
626
+ const cls = size === "lg"
627
+ ? "h-5 w-5 shrink-0 text-muted-foreground"
628
+ : "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground";
401
629
 
402
630
  switch (kind) {
403
631
  case ApiResourceKind.agent:
@@ -441,6 +669,35 @@ function SkeletonRows() {
441
669
  );
442
670
  }
443
671
 
672
+ function SkeletonCards() {
673
+ return (
674
+ <div
675
+ className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"
676
+ aria-busy="true"
677
+ >
678
+ {Array.from({ length: GRID_SKELETON_COUNT }, (_, i) => (
679
+ <div
680
+ key={i}
681
+ className="flex flex-col gap-3 rounded-lg border border-border bg-card p-4"
682
+ aria-hidden="true"
683
+ >
684
+ <div className="flex items-start gap-3">
685
+ <div className="size-10 shrink-0 animate-pulse rounded-lg bg-muted" />
686
+ <div className="flex-1 space-y-2">
687
+ <div className="h-4 w-3/5 animate-pulse rounded bg-muted" />
688
+ <div className="h-3 w-2/5 animate-pulse rounded bg-muted" />
689
+ </div>
690
+ </div>
691
+ <div className="space-y-1.5">
692
+ <div className="h-3 w-full animate-pulse rounded bg-muted" />
693
+ <div className="h-3 w-4/5 animate-pulse rounded bg-muted" />
694
+ </div>
695
+ </div>
696
+ ))}
697
+ </div>
698
+ );
699
+ }
700
+
444
701
  function EmptyState({
445
702
  icon,
446
703
  title,
@@ -2,7 +2,10 @@ export { ScopeToggle } from "./ScopeToggle";
2
2
  export type { ScopeToggleProps } from "./ScopeToggle";
3
3
 
4
4
  export { ResourceListView } from "./ResourceListView";
5
- export type { ResourceListViewProps } from "./ResourceListView";
5
+ export type {
6
+ ResourceListViewProps,
7
+ ResourceListLayout,
8
+ } from "./ResourceListView";
6
9
 
7
10
  export { ResourceCountCard } from "./ResourceCountCard";
8
11
  export type { ResourceCountCardProps } from "./ResourceCountCard";