main
html 793 lines 26.4 KB
Raw
1 <html>
2 <head>
3 <title>Browse Plugins</title>
4 <script type="module">
5 import { store } from "/plugins/_plugin_installer/webui/pluginInstallStore.js";
6 </script>
7 </head>
8 <body>
9 <div x-data>
10 <template x-if="$store.pluginInstallStore">
11 <div class="pi-browse-shell" x-create="$store.pluginInstallStore.openIndexView()">
12
13 <section class="pi-browse-hero">
14 <div class="pi-browse-copy">
15 <div class="pi-browse-eyebrow">Community Plugin Hub</div>
16 <h2 class="pi-browse-title">Discover community plugins for Agent Zero</h2>
17 <p class="pi-browse-description">
18 These plugins are provided by the community of Agent Zero. Contribute by publishing in the <a href="https://github.com/agent0ai/a0-plugins" target="_blank">index repository</a>.
19 </p>
20 </div>
21 <div class="pi-browse-summary" x-text="$store.pluginInstallStore.browseResultsSummary"></div>
22 </section>
23
24 <div class="pi-index-toolbar">
25 <label class="pi-search-field">
26 <x-icon name="search"></x-icon>
27 <input type="text"
28 class="pi-search-input"
29 x-model="$store.pluginInstallStore.search"
30 @input="$store.pluginInstallStore.page = 1"
31 placeholder="Search plugins, authors, or tags...">
32 </label>
33
34 <label class="pi-sort-field">
35 <span class="pi-field-label">Sort</span>
36 <select class="pi-sort-select"
37 x-model="$store.pluginInstallStore.sortBy">
38 <option value="stars">Stars</option>
39 <option value="updated">Newest</option>
40 <option value="name">Name</option>
41 </select>
42 </label>
43
44 <button type="button"
45 class="button icon-button pi-reload-button"
46 aria-label="Reload Plugin Hub"
47 title="Reload Plugin Hub"
48 @click="$store.pluginInstallStore.reloadIndex()"
49 :disabled="$store.pluginInstallStore.loading">
50 <x-icon name="refresh"></x-icon>
51 </button>
52 </div>
53
54 <div class="pi-filter-row" x-show="$store.pluginInstallStore.browseFilters.length > 1">
55 <template x-for="filter in $store.pluginInstallStore.browseFilters" :key="filter.key">
56 <button type="button"
57 class="pi-filter-chip"
58 :class="{
59 active: $store.pluginInstallStore.browseFilter === filter.key,
60 'pi-filter-chip-update': filter.key === 'update' && filter.count > 0,
61 'pi-filter-chip-new': filter.key === 'new' && filter.count > 0
62 }"
63 @click="$store.pluginInstallStore.setBrowseFilter(filter.key)">
64 <span x-text="filter.label"></span>
65 <span class="pi-filter-count" x-text="filter.count"></span>
66 </button>
67 </template>
68 </div>
69
70 <div x-show="$store.pluginInstallStore.loading" class="pi-state-card pi-state-loading">
71 <x-icon name="progress_activity"></x-icon>
72 <div>
73 <div class="pi-state-title">Loading Plugin Hub</div>
74 <div class="pi-state-text" x-text="$store.pluginInstallStore.loadingMessage || 'Loading plugins...'"></div>
75 </div>
76 </div>
77
78 <div x-show="$store.pluginInstallStore.index && !$store.pluginInstallStore.loading && $store.pluginInstallStore.filteredPlugins.length > 0"
79 class="pi-grid">
80 <template x-for="plugin in $store.pluginInstallStore.paginatedPlugins" :key="plugin.key">
81 <button type="button" class="pi-card" @click="$store.pluginInstallStore.openDetail(plugin)">
82 <div class="pi-card-header">
83 <div class="pi-card-thumb">
84 <template x-if="plugin.thumbnail">
85 <img :src="plugin.thumbnail" :alt="plugin.title" loading="lazy">
86 </template>
87 <template x-if="!plugin.thumbnail">
88 <x-icon class="pi-card-placeholder" name="extension"></x-icon>
89 </template>
90 </div>
91
92 <div class="pi-card-heading">
93 <h3 class="pi-card-title" x-text="plugin.title || plugin.key"></h3>
94
95 <div class="pi-card-subtitle"
96 x-text="$store.pluginInstallStore.getBrowseSubtitle(plugin)">
97 </div>
98
99 <span class="pi-card-stars">
100 <x-icon name="star"></x-icon>
101 <span x-text="plugin.stars || 0"></span>
102 </span>
103
104 </div>
105
106 </div>
107
108 <div class="pi-card-body">
109
110 <div class="pi-card-desc"
111 x-text="$store.pluginInstallStore.truncate(plugin.description, 110)">
112 </div>
113
114 </div>
115
116 <div class="pi-card-footer">
117
118 <div class="pi-card-bubbles pi-card-bubbles-tags">
119 <template x-if="$store.pluginInstallStore.getBrowsePrimaryTag(plugin)">
120 <span class="pi-card-bubble pi-card-bubble-tag">
121 <x-icon name="sell"></x-icon>
122 <span class="pi-card-bubble-text"
123 x-text="$store.pluginInstallStore.getBrowsePrimaryTag(plugin)"></span>
124 </span>
125 </template>
126 </div>
127
128 <div class="pi-card-bubbles pi-card-bubbles-status">
129 <template x-if="plugin.suspended">
130 <span class="pi-card-bubble pi-card-bubble-suspended">
131 <x-icon name="priority_high"></x-icon>
132 <span class="pi-card-bubble-text">Suspended</span>
133 </span>
134 </template>
135 <template x-if="plugin.installed">
136 <span class="pi-card-bubble pi-card-bubble-installed">
137 <x-icon name="check_circle"></x-icon>
138 <span class="pi-card-bubble-text">Installed</span>
139 </span>
140 </template>
141 <template x-if="plugin.has_update">
142 <span class="pi-card-bubble pi-card-bubble-update">
143 <x-icon name="update"></x-icon>
144 <span class="pi-card-bubble-text">New version</span>
145 </span>
146 </template>
147 <template x-if="$store.pluginInstallStore.isNewPlugin(plugin)">
148 <span class="pi-card-bubble pi-card-bubble-new">
149 <x-icon name="new_releases"></x-icon>
150 <span class="pi-card-bubble-text">New</span>
151 </span>
152 </template>
153 </div>
154
155 </div>
156 </button>
157 </template>
158 </div>
159
160 <div x-show="$store.pluginInstallStore.index && !$store.pluginInstallStore.loading && $store.pluginInstallStore.filteredPlugins.length === 0"
161 class="pi-state-card pi-state-empty">
162 <x-icon name="search_off"></x-icon>
163 <div>
164 <div class="pi-state-title">No plugins match this view</div>
165 <div class="pi-state-text">
166 Try a different search or switch to another Plugin Hub filter.
167 </div>
168 </div>
169 </div>
170
171 <div x-show="$store.pluginInstallStore.totalPages > 1" class="pi-pagination">
172 <button class="button"
173 :disabled="$store.pluginInstallStore.page <= 1"
174 @click="$store.pluginInstallStore.setPage($store.pluginInstallStore.page - 1)">
175 <x-icon name="chevron_left"></x-icon>
176 </button>
177 <span class="pi-page-info"
178 x-text="'Page ' + $store.pluginInstallStore.page + ' of ' + $store.pluginInstallStore.totalPages">
179 </span>
180 <button class="button"
181 :disabled="$store.pluginInstallStore.page >= $store.pluginInstallStore.totalPages"
182 @click="$store.pluginInstallStore.setPage($store.pluginInstallStore.page + 1)">
183 <x-icon name="chevron_right"></x-icon>
184 </button>
185 </div>
186 </div>
187 </template>
188 </div>
189
190 <style>
191 .pi-browse-shell {
192 display: flex;
193 flex-direction: column;
194 gap: 1rem;
195 font-family: "Rubik", Arial, Helvetica, sans-serif;
196 }
197
198 .pi-browse-shell button,
199 .pi-browse-shell input,
200 .pi-browse-shell select {
201 font-family: inherit;
202 }
203
204 .pi-browse-hero {
205 display: flex;
206 align-items: flex-start;
207 justify-content: space-between;
208 gap: 1rem;
209 padding: 0.25rem 0 0.35rem;
210 }
211
212 .pi-browse-copy {
213 max-width: 44rem;
214 }
215
216 .pi-browse-eyebrow {
217 margin-bottom: 0.4rem;
218 font-size: 0.78rem;
219 font-weight: 700;
220 letter-spacing: 0.08em;
221 text-transform: uppercase;
222 color: var(--color-highlight);
223 }
224
225 .pi-browse-title {
226 margin: 0;
227 font-size: 1.6rem;
228 line-height: 1.15;
229 color: var(--color-text-primary);
230 }
231
232 .pi-browse-description {
233 margin: 0.5rem 0 0;
234 max-width: 38rem;
235 font-size: 0.95rem;
236 line-height: 1.55;
237 color: var(--color-text-secondary);
238 }
239
240 .pi-browse-summary {
241 flex-shrink: 0;
242 padding: 0.6rem 0.85rem;
243 border: 1px solid var(--color-border);
244 border-radius: 0.5rem;
245 background: var(--color-panel);
246 font-size: 0.85rem;
247 font-weight: 600;
248 color: var(--color-text-primary);
249 }
250
251 .pi-index-toolbar {
252 display: flex;
253 gap: 0.75rem;
254 align-items: stretch;
255 }
256
257 .pi-search-field,
258 .pi-sort-field {
259 display: flex;
260 align-items: center;
261 gap: 0.55rem;
262 padding: 0.55rem 0;
263 border: 0;
264 border-bottom: 1px solid var(--color-border);
265 border-radius: 0;
266 }
267
268 .pi-search-field {
269 flex: 1;
270 min-width: 0;
271 }
272
273 .pi-search-field .material-symbols-outlined {
274 font-size: 1.15rem;
275 color: var(--color-text-muted);
276 }
277
278 .pi-search-input {
279 width: 100%;
280 min-width: 0;
281 padding: 0;
282 border: 0;
283 outline: none;
284 color: var(--color-text-primary);
285 font-size: 0.95rem;
286 }
287
288 .pi-sort-field {
289 min-width: 160px;
290 flex-shrink: 0;
291 justify-content: space-between;
292 }
293
294 .pi-reload-button {
295 flex-shrink: 0;
296 display: inline-flex;
297 align-items: center;
298 justify-content: center;
299 min-width: 0;
300 min-height: 0;
301 padding: 0.45rem 0.6rem;
302 }
303
304 .pi-reload-button .material-symbols-outlined {
305 font-size: 1.1rem;
306 }
307
308 .pi-field-label {
309 font-size: 0.8rem;
310 font-weight: 600;
311 color: var(--color-text-muted);
312 text-transform: uppercase;
313 letter-spacing: 0.05em;
314 }
315
316 .pi-sort-select {
317 width: 100%;
318 padding: 0;
319 border: 0;
320 outline: none;
321 color: var(--color-text-primary);
322 font-size: 0.95rem;
323 }
324
325 .pi-filter-row {
326 display: flex;
327 flex-wrap: wrap;
328 gap: 0.6rem;
329 }
330
331 .pi-filter-chip {
332 display: inline-flex;
333 align-items: center;
334 gap: 0.45rem;
335 padding: 0.45rem 0.75rem;
336 border: 1px solid var(--color-border);
337 border-radius: 0.5rem;
338 background: var(--color-panel);
339 color: var(--color-text-secondary);
340 font-size: 0.85rem;
341 font-weight: 600;
342 cursor: pointer;
343 transition: border-color 0.15s, background 0.15s, color 0.15s;
344 }
345
346 .pi-filter-chip:hover {
347 color: var(--color-text-primary);
348 background: rgba(255, 255, 255, 0.04);
349 }
350
351 .pi-filter-chip.active {
352 border-color: var(--color-border);
353 background: rgba(255, 255, 255, 0.08);
354 color: var(--color-text-primary);
355 }
356
357 .pi-filter-chip-update {
358 border-color: rgba(59, 130, 246, 0.22);
359 color: #93c5fd;
360 }
361
362 .pi-filter-chip-update:hover,
363 .pi-filter-chip-update.active {
364 border-color: rgba(59, 130, 246, 0.38);
365 background: rgba(59, 130, 246, 0.12);
366 color: #bfdbfe;
367 }
368
369 .pi-filter-chip-update .pi-filter-count {
370 background: rgba(59, 130, 246, 0.16);
371 color: #bfdbfe;
372 }
373
374 .pi-filter-chip-new {
375 border-color: rgba(34, 197, 94, 0.22);
376 color: #86efac;
377 }
378
379 .pi-filter-chip-new:hover,
380 .pi-filter-chip-new.active {
381 border-color: rgba(34, 197, 94, 0.4);
382 background: rgba(34, 197, 94, 0.14);
383 color: #bbf7d0;
384 }
385
386 .pi-filter-chip-new .pi-filter-count {
387 background: rgba(34, 197, 94, 0.16);
388 color: #bbf7d0;
389 }
390
391 body.light-mode .pi-filter-chip-update {
392 border-color: rgba(37, 99, 235, 0.35);
393 background: rgba(37, 99, 235, 0.08);
394 color: #1d4ed8;
395 }
396
397 body.light-mode .pi-filter-chip-update:hover,
398 body.light-mode .pi-filter-chip-update.active {
399 border-color: rgba(37, 99, 235, 0.5);
400 background: rgba(37, 99, 235, 0.14);
401 color: #1e40af;
402 }
403
404 body.light-mode .pi-filter-chip-update .pi-filter-count {
405 background: rgba(37, 99, 235, 0.18);
406 color: #1e3a8a;
407 }
408
409 body.light-mode .pi-filter-chip-new {
410 border-color: rgba(22, 163, 74, 0.35);
411 background: rgba(22, 163, 74, 0.08);
412 color: #166534;
413 }
414
415 body.light-mode .pi-filter-chip-new:hover,
416 body.light-mode .pi-filter-chip-new.active {
417 border-color: rgba(21, 128, 61, 0.5);
418 background: rgba(22, 163, 74, 0.14);
419 color: #14532d;
420 }
421
422 body.light-mode .pi-filter-chip-new .pi-filter-count {
423 background: rgba(22, 163, 74, 0.16);
424 color: #14532d;
425 }
426
427 .pi-filter-count {
428 padding: 0.12rem 0.4rem;
429 border-radius: 0.5rem;
430 background: rgba(255, 255, 255, 0.06);
431 color: var(--color-text-muted);
432 font-size: 0.75rem;
433 }
434
435 .pi-state-card {
436 display: flex;
437 align-items: center;
438 gap: 0.85rem;
439 padding: 1rem 1.05rem;
440 border: 1px solid var(--color-border);
441 border-radius: 14px;
442 background: var(--color-panel);
443 }
444
445 .pi-state-card .material-symbols-outlined {
446 font-size: 1.5rem;
447 }
448
449 .pi-state-title {
450 font-size: 0.95rem;
451 font-weight: 600;
452 color: var(--color-text-primary);
453 }
454
455 .pi-state-text {
456 margin-top: 0.15rem;
457 font-size: 0.88rem;
458 color: var(--color-text-secondary);
459 }
460
461 .pi-state-loading .material-symbols-outlined {
462 color: var(--color-highlight);
463 }
464
465 .pi-state-empty .material-symbols-outlined {
466 color: var(--color-text-muted);
467 }
468
469 .pi-grid {
470 display: grid;
471 grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
472 gap: 0.95rem;
473 }
474
475 .pi-card {
476 width: 100%;
477 padding: 0;
478 border: 1px solid var(--color-border);
479 border-radius: 10px;
480 background: var(--color-background);
481 color: inherit;
482 text-align: left;
483 cursor: pointer;
484 display: flex;
485 flex-direction: column;
486 overflow: hidden;
487 transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
488 }
489
490 .pi-card:hover {
491 box-shadow: 0 12px 30px rgba(0, 0, 0, 0.18);
492 }
493
494 .pi-card-header {
495 display: flex;
496 align-items: center;
497 gap: 0.75rem;
498 padding: 0.85rem;
499 }
500
501 .pi-card-thumb {
502 width: 56px;
503 height: 56px;
504 flex-shrink: 0;
505 display: flex;
506 align-items: center;
507 justify-content: center;
508 border-radius: 0.5rem;
509 overflow: hidden;
510 background: radial-gradient(circle at top, #6b728089, var(--color-panel)),
511 var(--color-panel);
512 border: 1px solid var(--color-border);
513 }
514
515 .pi-card-thumb img {
516 width: 100%;
517 height: 100%;
518 object-fit: contain;
519 }
520
521 .pi-card-placeholder {
522 font-size: 2rem;
523 color: var(--color-text-muted);
524 }
525
526 .pi-card-heading {
527 min-width: 0;
528 flex: 1;
529 }
530
531 .pi-card-title {
532 margin: 0;
533 font-size: 1rem;
534 line-height: 1.25;
535 color: var(--color-text-primary);
536 }
537
538 .pi-card-subtitle {
539 margin-top: 0.2rem;
540 font-size: 0.82rem;
541 color: var(--color-text-muted);
542 white-space: nowrap;
543 overflow: hidden;
544 text-overflow: ellipsis;
545 }
546
547 .pi-card-arrow {
548 font-size: 1rem;
549 color: var(--color-text-muted);
550 flex-shrink: 0;
551 }
552
553 .pi-card-body {
554 flex: 1;
555 display: flex;
556 flex-direction: column;
557 gap: 0.6rem;
558 padding: 0 0.85rem 0.85rem;
559 }
560
561 .pi-card-desc {
562 font-size: 0.88rem;
563 line-height: 1.5;
564 color: var(--color-text-secondary);
565 }
566
567 .pi-card-footer {
568 display: flex;
569 align-items: center;
570 justify-content: space-between;
571 gap: 0.75rem;
572 padding: 0.65rem 0.85rem 0.75rem;
573 border-top: 1px solid var(--color-border);
574 }
575
576 .pi-card-stars {
577 display: inline-flex;
578 align-items: center;
579 gap: 0.3rem;
580 padding: var(--spacing-xxs) 0;
581 font-size: 0.85rem;
582 color: var(--color-text-secondary);
583 }
584
585 .pi-card-stars .material-symbols-outlined {
586 font-size: 1rem;
587 color: #fbbf24;
588 }
589
590 .pi-card-bubbles {
591 display: flex;
592 align-items: center;
593 gap: 0.35rem;
594 flex-shrink: 0;
595 }
596
597 .pi-card-bubbles-tags {
598 min-width: 0;
599 flex: 1 1 auto;
600 justify-content: flex-start;
601 }
602
603 .pi-card-bubbles-status {
604 justify-content: flex-end;
605 }
606
607 .pi-card-bubble {
608 display: inline-flex;
609 align-items: center;
610 height: 26px;
611 padding: 0 0.45rem;
612 border-radius: 6px;
613 border: 1px solid var(--color-border);
614 background: var(--color-panel);
615 overflow: hidden;
616 cursor: default;
617 transition: background 0.2s ease, border-color 0.2s ease;
618 }
619
620 .pi-card-bubble .material-symbols-outlined {
621 font-size: 0.95rem;
622 flex-shrink: 0;
623 line-height: 1;
624 }
625
626 .pi-card-bubble-text {
627 max-width: 0;
628 opacity: 0;
629 overflow: hidden;
630 white-space: nowrap;
631 font-size: 0.72rem;
632 font-weight: 600;
633 transition: max-width 0.25s ease, opacity 0.2s ease, margin-left 0.2s ease;
634 margin-left: 0;
635 }
636
637 /* Category tag (left): label always visible */
638 .pi-card-bubbles-tags .pi-card-bubble {
639 min-width: 0;
640 max-width: 100%;
641 }
642
643 .pi-card-bubbles-tags .pi-card-bubble-text {
644 flex: 1 1 auto;
645 min-width: 0;
646 max-width: 10rem;
647 opacity: 1;
648 margin-left: 0.3rem;
649 text-overflow: ellipsis;
650 }
651
652 /* Status pills (right): icon until hover */
653 .pi-card-bubbles-status .pi-card-bubble:hover .pi-card-bubble-text {
654 max-width: 8rem;
655 opacity: 1;
656 margin-left: 0.3rem;
657 }
658
659 .pi-card-bubble-tag .material-symbols-outlined {
660 color: var(--color-text-muted);
661 }
662
663 .pi-card-bubble-tag .pi-card-bubble-text {
664 color: var(--color-text-secondary);
665 }
666
667 .pi-card-bubble-installed .material-symbols-outlined {
668 color: #4ade80;
669 }
670
671 .pi-card-bubble-installed .pi-card-bubble-text {
672 color: #4ade80;
673 }
674
675 .pi-card-bubble-installed:hover {
676 background: rgba(34, 197, 94, 0.1);
677 border-color: rgba(34, 197, 94, 0.3);
678 }
679
680 .pi-card-bubble-update .material-symbols-outlined {
681 color: #60a5fa;
682 }
683
684 .pi-card-bubble-update .pi-card-bubble-text {
685 color: #60a5fa;
686 }
687
688 .pi-card-bubble-update:hover {
689 background: rgba(59, 130, 246, 0.1);
690 border-color: rgba(59, 130, 246, 0.3);
691 }
692
693 .pi-card-bubble-new .material-symbols-outlined {
694 color: #4ade80;
695 }
696
697 .pi-card-bubble-new .pi-card-bubble-text {
698 color: #4ade80;
699 }
700
701 .pi-card-bubble-new:hover {
702 background: rgba(34, 197, 94, 0.1);
703 border-color: rgba(34, 197, 94, 0.3);
704 }
705
706 .pi-card-bubble-suspended .material-symbols-outlined {
707 color: #f59e0b;
708 }
709
710 .pi-card-bubble-suspended .pi-card-bubble-text {
711 color: #f59e0b;
712 }
713
714 .pi-card-bubble-suspended:hover {
715 background: rgba(245, 158, 11, 0.12);
716 border-color: rgba(245, 158, 11, 0.35);
717 }
718
719 body.light-mode .pi-card-bubble-installed .material-symbols-outlined,
720 body.light-mode .pi-card-bubble-installed .pi-card-bubble-text {
721 color: #166534;
722 }
723
724 body.light-mode .pi-card-bubble-installed:hover {
725 background: rgba(34, 197, 94, 0.12);
726 border-color: rgba(34, 197, 94, 0.35);
727 }
728
729 body.light-mode .pi-card-bubble-update .material-symbols-outlined,
730 body.light-mode .pi-card-bubble-update .pi-card-bubble-text {
731 color: #1d4ed8;
732 }
733
734 body.light-mode .pi-card-bubble-update:hover {
735 background: rgba(59, 130, 246, 0.1);
736 border-color: rgba(59, 130, 246, 0.35);
737 }
738
739 body.light-mode .pi-card-bubble-new .material-symbols-outlined,
740 body.light-mode .pi-card-bubble-new .pi-card-bubble-text {
741 color: #166534;
742 }
743
744 body.light-mode .pi-card-bubble-new:hover {
745 background: rgba(34, 197, 94, 0.12);
746 border-color: rgba(34, 197, 94, 0.35);
747 }
748
749 body.light-mode .pi-card-bubble-suspended .material-symbols-outlined,
750 body.light-mode .pi-card-bubble-suspended .pi-card-bubble-text {
751 color: #b45309;
752 }
753
754 body.light-mode .pi-card-bubble-suspended:hover {
755 background: rgba(245, 158, 11, 0.16);
756 border-color: rgba(217, 119, 6, 0.35);
757 }
758
759 .pi-pagination {
760 display: flex;
761 align-items: center;
762 justify-content: center;
763 gap: 0.75rem;
764 padding: 0.4rem 0 0.1rem;
765 }
766
767 .pi-page-info {
768 font-size: 0.9rem;
769 color: var(--color-text-secondary);
770 }
771
772 @media (max-width: 720px) {
773 .pi-browse-hero {
774 flex-direction: column;
775 }
776
777 .pi-index-toolbar {
778 flex-direction: column;
779 }
780
781 .pi-sort-field {
782 min-width: 0;
783 }
784 }
785
786 @media (max-width: 500px) {
787 .pi-grid {
788 grid-template-columns: 1fr;
789 }
790 }
791 </style>
792 </body>
793 </html>