@stigmer/react 0.0.82 → 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.
- package/index.d.ts +3 -3
- package/index.d.ts.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/library/ResourceListView.d.ts +57 -7
- package/library/ResourceListView.d.ts.map +1 -1
- package/library/ResourceListView.js +147 -37
- package/library/ResourceListView.js.map +1 -1
- package/library/index.d.ts +1 -1
- package/library/index.d.ts.map +1 -1
- package/library/index.js.map +1 -1
- package/mcp-server/McpServerConfigPanel.d.ts +45 -0
- package/mcp-server/McpServerConfigPanel.d.ts.map +1 -1
- package/mcp-server/McpServerConfigPanel.js +90 -14
- package/mcp-server/McpServerConfigPanel.js.map +1 -1
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +168 -23
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/McpServerPicker.js +3 -3
- package/mcp-server/McpServerPicker.js.map +1 -1
- package/mcp-server/OAuthAppForm.d.ts +58 -0
- package/mcp-server/OAuthAppForm.d.ts.map +1 -0
- package/mcp-server/OAuthAppForm.js +67 -0
- package/mcp-server/OAuthAppForm.js.map +1 -0
- package/mcp-server/index.d.ts +6 -0
- package/mcp-server/index.d.ts.map +1 -1
- package/mcp-server/index.js +3 -0
- package/mcp-server/index.js.map +1 -1
- package/mcp-server/useDisconnectOAuth.d.ts +40 -0
- package/mcp-server/useDisconnectOAuth.d.ts.map +1 -0
- package/mcp-server/useDisconnectOAuth.js +46 -0
- package/mcp-server/useDisconnectOAuth.js.map +1 -0
- package/mcp-server/useMcpServerCredentials.d.ts +48 -0
- package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
- package/mcp-server/useMcpServerCredentials.js +18 -2
- package/mcp-server/useMcpServerCredentials.js.map +1 -1
- package/mcp-server/useOAuthGrantStatus.d.ts +9 -0
- package/mcp-server/useOAuthGrantStatus.d.ts.map +1 -1
- package/mcp-server/useOAuthGrantStatus.js +6 -1
- package/mcp-server/useOAuthGrantStatus.js.map +1 -1
- package/mcp-server/useOrgOAuthApp.d.ts +82 -0
- package/mcp-server/useOrgOAuthApp.d.ts.map +1 -0
- package/mcp-server/useOrgOAuthApp.js +160 -0
- package/mcp-server/useOrgOAuthApp.js.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +3 -0
- package/src/library/ResourceListView.tsx +303 -46
- package/src/library/index.ts +4 -1
- package/src/mcp-server/McpServerConfigPanel.tsx +370 -45
- package/src/mcp-server/McpServerDetailView.tsx +447 -47
- package/src/mcp-server/McpServerPicker.tsx +3 -3
- package/src/mcp-server/OAuthAppForm.tsx +304 -0
- package/src/mcp-server/index.ts +9 -0
- package/src/mcp-server/useDisconnectOAuth.ts +76 -0
- package/src/mcp-server/useMcpServerCredentials.ts +70 -2
- package/src/mcp-server/useOAuthGrantStatus.ts +19 -1
- package/src/mcp-server/useOrgOAuthApp.ts +250 -0
- 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
|
|
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
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
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({
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
|
400
|
-
|
|
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,
|
package/src/library/index.ts
CHANGED
|
@@ -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 {
|
|
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";
|