feat: add tabbed main screen with Store/Git/ZIP tabs
New main.html consolidates all three install methods into a single tabbed interface using the same nav-tabs pattern as the plugins list. Includes updated card design with thumbnail overlay and larger icons. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TerminallyLazy committed
Mar 3, 2026 at 23:40 UTC
99aea498b92e14cc1fd59d9fa632c20436a99d56
1 file changed
+526
plugins/plugin_installer/webui/main.html
new
+526
@@ -0,0 +1,526 @@
1
+<html>
2
+<head>
3
+ <title>Install Plugin</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 x-init="$store.pluginInstallStore.setTab('store')">
12
+
13
+ <ul class="nav nav-tabs" role="tablist">
14
+ <li class="nav-item" role="presentation">
15
+ <button class="nav-link"
16
+ :class="{ active: $store.pluginInstallStore.activeTab === 'store' }"
17
+ type="button" role="tab"
18
+ @click="$store.pluginInstallStore.setTab('store')">
19
+ <span class="material-symbols-outlined pi-tab-icon">store</span> Browse
20
+ </button>
21
+ </li>
22
+ <li class="nav-item" role="presentation">
23
+ <button class="nav-link"
24
+ :class="{ active: $store.pluginInstallStore.activeTab === 'git' }"
25
+ type="button" role="tab"
26
+ @click="$store.pluginInstallStore.setTab('git')">
27
+ <span class="material-symbols-outlined pi-tab-icon">terminal</span> Git
28
+ </button>
29
+ </li>
30
+ <li class="nav-item" role="presentation">
31
+ <button class="nav-link"
32
+ :class="{ active: $store.pluginInstallStore.activeTab === 'zip' }"
33
+ type="button" role="tab"
34
+ @click="$store.pluginInstallStore.setTab('zip')">
35
+ <span class="material-symbols-outlined pi-tab-icon">upload_file</span> ZIP
36
+ </button>
37
+ </li>
38
+ </ul>
39
+
40
+ <!-- ═══ Store Tab ═══ -->
41
+ <div x-show="$store.pluginInstallStore.activeTab === 'store'"
42
+ x-init="$store.pluginInstallStore.resetIndex(); $store.pluginInstallStore.fetchIndex()">
43
+
44
+ <div class="pi-index-toolbar">
45
+ <input type="text" class="pi-search-input"
46
+ x-model="$store.pluginInstallStore.search"
47
+ @input="$store.pluginInstallStore.page = 1"
48
+ placeholder="Search plugins...">
49
+ <select class="pi-sort-select"
50
+ x-model="$store.pluginInstallStore.sortBy">
51
+ <option value="stars">Stars</option>
52
+ <option value="name">Name</option>
53
+ </select>
54
+ </div>
55
+
56
+ <div x-show="$store.pluginInstallStore.loading" class="pi-loading">
57
+ <span x-text="$store.pluginInstallStore.loadingMessage || 'Loading...'"></span>
58
+ </div>
59
+
60
+ <div x-show="$store.pluginInstallStore.error && !$store.pluginInstallStore.loading" class="pi-error">
61
+ <span x-text="$store.pluginInstallStore.error"></span>
62
+ </div>
63
+
64
+ <div x-show="$store.pluginInstallStore.index && !$store.pluginInstallStore.loading" class="pi-grid">
65
+ <template x-for="plugin in $store.pluginInstallStore.paginatedPlugins" :key="plugin.key">
66
+ <div class="pi-card" @click="$store.pluginInstallStore.openDetail(plugin)">
67
+ <div class="pi-card-thumb">
68
+ <template x-if="plugin.thumbnail">
69
+ <img :src="plugin.thumbnail" :alt="plugin.title" loading="lazy">
70
+ </template>
71
+ <template x-if="!plugin.thumbnail">
72
+ <span class="material-symbols-outlined pi-card-placeholder">extension</span>
73
+ </template>
74
+ <div class="pi-card-thumb-overlay">
75
+ <span class="pi-card-thumb-title" x-text="plugin.title || plugin.key"></span>
76
+ <template x-if="plugin.installed">
77
+ <span class="pi-badge-installed">Installed</span>
78
+ </template>
79
+ </div>
80
+ </div>
81
+ <div class="pi-card-body">
82
+ <div class="pi-card-desc" x-text="$store.pluginInstallStore.truncate(plugin.description, 100)"></div>
83
+ </div>
84
+ <div class="pi-card-footer">
85
+ <span class="pi-stars">
86
+ <span class="material-symbols-outlined" style="font-size:0.9rem">star</span>
87
+ <span x-text="plugin.stars || 0"></span>
88
+ </span>
89
+ </div>
90
+ </div>
91
+ </template>
92
+ </div>
93
+
94
+ <div x-show="$store.pluginInstallStore.index && !$store.pluginInstallStore.loading && $store.pluginInstallStore.filteredPlugins.length === 0"
95
+ class="pi-empty">
96
+ No plugins found matching your search.
97
+ </div>
98
+
99
+ <div x-show="$store.pluginInstallStore.totalPages > 1" class="pi-pagination">
100
+ <button class="button"
101
+ :disabled="$store.pluginInstallStore.page <= 1"
102
+ @click="$store.pluginInstallStore.setPage($store.pluginInstallStore.page - 1)">
103
+ <span class="material-symbols-outlined">chevron_left</span>
104
+ </button>
105
+ <span class="pi-page-info"
106
+ x-text="$store.pluginInstallStore.page + ' / ' + $store.pluginInstallStore.totalPages">
107
+ </span>
108
+ <button class="button"
109
+ :disabled="$store.pluginInstallStore.page >= $store.pluginInstallStore.totalPages"
110
+ @click="$store.pluginInstallStore.setPage($store.pluginInstallStore.page + 1)">
111
+ <span class="material-symbols-outlined">chevron_right</span>
112
+ </button>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- ═══ Git Tab ═══ -->
117
+ <div x-show="$store.pluginInstallStore.activeTab === 'git'"
118
+ x-init="$store.pluginInstallStore.resetGit()">
119
+
120
+ <div class="pi-form-group">
121
+ <label class="pi-label">Git Repository URL</label>
122
+ <input type="text" class="pi-input"
123
+ x-model="$store.pluginInstallStore.gitUrl"
124
+ :disabled="$store.pluginInstallStore.loading"
125
+ placeholder="https://github.com/user/my-plugin.git">
126
+ </div>
127
+
128
+ <div class="pi-form-group">
129
+ <label class="pi-label">Access Token <span class="pi-optional">(optional, for private repos)</span></label>
130
+ <input type="password" class="pi-input"
131
+ x-model="$store.pluginInstallStore.gitToken"
132
+ :disabled="$store.pluginInstallStore.loading"
133
+ placeholder="ghp_xxxx or glpat-xxxx">
134
+ <div class="pi-hint">Token is used only for cloning and is not stored.</div>
135
+ </div>
136
+
137
+ <div class="pi-actions">
138
+ <button class="button confirm"
139
+ @click="$store.pluginInstallStore.installGit()"
140
+ :disabled="$store.pluginInstallStore.loading || !$store.pluginInstallStore.gitUrl.trim()">
141
+ <template x-if="$store.pluginInstallStore.loading">
142
+ <span class="pi-button-loading">
143
+ <span class="spinner"></span>
144
+ <span x-text="$store.pluginInstallStore.loadingMessage || 'Cloning...'"></span>
145
+ </span>
146
+ </template>
147
+ <template x-if="!$store.pluginInstallStore.loading">
148
+ <span>
149
+ <span class="icon material-symbols-outlined">terminal</span> Clone & Install
150
+ </span>
151
+ </template>
152
+ </button>
153
+ </div>
154
+
155
+ <div x-show="$store.pluginInstallStore.error" class="pi-error">
156
+ <span x-text="$store.pluginInstallStore.error"></span>
157
+ </div>
158
+
159
+ <div x-show="$store.pluginInstallStore.result" class="pi-result">
160
+ <div class="pi-result-icon">
161
+ <span class="material-symbols-outlined">check_circle</span>
162
+ </div>
163
+ <div class="pi-result-text">
164
+ <strong x-text="'Plugin installed: ' + ($store.pluginInstallStore.result?.title || $store.pluginInstallStore.result?.plugin_name || '')"></strong>
165
+ <div class="pi-result-path" x-text="$store.pluginInstallStore.result?.path || ''"></div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+
170
+ <!-- ═══ ZIP Tab ═══ -->
171
+ <div x-show="$store.pluginInstallStore.activeTab === 'zip'"
172
+ x-init="$store.pluginInstallStore.resetZip()">
173
+
174
+ <div class="pi-upload-section">
175
+ <label for="plugin-zip-file" class="pi-upload-btn button confirm"
176
+ :class="{ 'pi-has-file': $store.pluginInstallStore.zipFile }">
177
+ <span class="icon material-symbols-outlined">upload_file</span>
178
+ <span x-text="$store.pluginInstallStore.zipFileName || 'Select Plugin ZIP File'"></span>
179
+ </label>
180
+ <input type="file" id="plugin-zip-file" accept=".zip" style="display:none"
181
+ @change="$store.pluginInstallStore.handleFileUpload($event)">
182
+ <div class="pi-hint">
183
+ The ZIP should contain a folder with a plugin.yaml file.
184
+ </div>
185
+ </div>
186
+
187
+ <div x-show="$store.pluginInstallStore.zipFile" class="pi-actions-center">
188
+ <button class="button confirm"
189
+ @click="$store.pluginInstallStore.installZip()"
190
+ :disabled="$store.pluginInstallStore.loading">
191
+ <span class="icon material-symbols-outlined">download</span> Install Plugin
192
+ </button>
193
+ </div>
194
+
195
+ <div x-show="$store.pluginInstallStore.loading" class="pi-loading">
196
+ <span x-text="$store.pluginInstallStore.loadingMessage || 'Processing...'"></span>
197
+ </div>
198
+
199
+ <div x-show="$store.pluginInstallStore.error" class="pi-error">
200
+ <span x-text="$store.pluginInstallStore.error"></span>
201
+ </div>
202
+
203
+ <div x-show="$store.pluginInstallStore.result" class="pi-result">
204
+ <div class="pi-result-icon">
205
+ <span class="material-symbols-outlined">check_circle</span>
206
+ </div>
207
+ <div class="pi-result-text">
208
+ <strong x-text="'Plugin installed: ' + ($store.pluginInstallStore.result?.title || $store.pluginInstallStore.result?.plugin_name || '')"></strong>
209
+ <div class="pi-result-path" x-text="$store.pluginInstallStore.result?.path || ''"></div>
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ </div>
215
+ </template>
216
+ </div>
217
+
218
+ <style>
219
+ /* ── Tabs ─────────────────────────────── */
220
+ .nav {
221
+ display: flex;
222
+ padding-left: 0;
223
+ margin: 0 0 1rem 0;
224
+ list-style: none;
225
+ border-bottom: 1px solid var(--color-border);
226
+ gap: 0.25rem;
227
+ }
228
+
229
+ .nav-link {
230
+ font-family: "Rubik", Arial, Helvetica, sans-serif;
231
+ border: 1px solid transparent;
232
+ border-top-left-radius: 4px;
233
+ border-top-right-radius: 4px;
234
+ padding: 0.4rem 0.7rem;
235
+ background: transparent;
236
+ color: var(--color-text-secondary);
237
+ cursor: pointer;
238
+ display: inline-flex;
239
+ align-items: center;
240
+ gap: 0.3rem;
241
+ }
242
+
243
+ .nav-link.active {
244
+ color: var(--color-text-primary);
245
+ border-color: var(--color-border);
246
+ border-bottom-color: transparent;
247
+ background: var(--color-background);
248
+ }
249
+
250
+ .pi-tab-icon {
251
+ font-size: 1.1rem;
252
+ }
253
+
254
+ /* ── Store Tab: Toolbar ──────────────── */
255
+ .pi-index-toolbar {
256
+ display: flex;
257
+ gap: 0.5rem;
258
+ margin-bottom: 1rem;
259
+ }
260
+
261
+ .pi-search-input {
262
+ flex: 1;
263
+ min-width: 0;
264
+ padding: 0.5rem 0.75rem;
265
+ border: 1px solid var(--color-border);
266
+ border-radius: 4px;
267
+ background: var(--color-bg-primary);
268
+ color: var(--color-text-primary);
269
+ font-size: 0.95rem;
270
+ }
271
+
272
+ .pi-sort-select {
273
+ padding: 0.5rem;
274
+ border: 1px solid var(--color-border);
275
+ border-radius: 4px;
276
+ background: var(--color-bg-primary);
277
+ color: var(--color-text-primary);
278
+ font-size: 0.9rem;
279
+ max-width: 140px;
280
+ flex-shrink: 0;
281
+ }
282
+
283
+ /* ── Store Tab: Grid ─────────────────── */
284
+ .pi-grid {
285
+ display: grid;
286
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
287
+ gap: 0.75rem;
288
+ }
289
+
290
+ .pi-card {
291
+ border: 1px solid var(--color-border);
292
+ border-radius: 6px;
293
+ background: var(--color-background);
294
+ cursor: pointer;
295
+ transition: border-color 0.15s, box-shadow 0.15s;
296
+ display: flex;
297
+ flex-direction: column;
298
+ overflow: hidden;
299
+ }
300
+
301
+ .pi-card:hover {
302
+ border-color: var(--color-highlight);
303
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
304
+ }
305
+
306
+ .pi-card-thumb {
307
+ width: 100%;
308
+ height: 140px;
309
+ overflow: hidden;
310
+ display: flex;
311
+ align-items: center;
312
+ justify-content: center;
313
+ background: var(--color-panel);
314
+ position: relative;
315
+ }
316
+
317
+ .pi-card-thumb img {
318
+ width: 100%;
319
+ height: 100%;
320
+ object-fit: contain;
321
+ padding: 0.5rem;
322
+ }
323
+
324
+ .pi-card-placeholder {
325
+ font-size: 4.5rem;
326
+ color: var(--color-text-muted);
327
+ }
328
+
329
+ .pi-card-thumb-overlay {
330
+ position: absolute;
331
+ bottom: 0;
332
+ left: 0;
333
+ right: 0;
334
+ padding: 0.4rem 0.6rem;
335
+ background: rgba(0, 0, 0, 0.55);
336
+ display: flex;
337
+ align-items: center;
338
+ gap: 0.4rem;
339
+ }
340
+
341
+ .pi-card-thumb-title {
342
+ font-weight: 600;
343
+ font-size: 0.85rem;
344
+ color: #fff;
345
+ white-space: nowrap;
346
+ overflow: hidden;
347
+ text-overflow: ellipsis;
348
+ flex: 1;
349
+ min-width: 0;
350
+ }
351
+
352
+ .pi-badge-installed {
353
+ font-size: 0.65rem;
354
+ font-weight: 600;
355
+ padding: 0.1rem 0.4rem;
356
+ border-radius: 3px;
357
+ background: rgba(34,197,94,0.3);
358
+ color: #4ade80;
359
+ flex-shrink: 0;
360
+ }
361
+
362
+ .pi-card-body {
363
+ padding: 0.5rem 0.75rem;
364
+ flex: 1;
365
+ }
366
+
367
+ .pi-card-desc {
368
+ font-size: 0.8rem;
369
+ color: var(--color-text-secondary);
370
+ line-height: 1.3;
371
+ }
372
+
373
+ .pi-card-footer {
374
+ padding: 0.4rem 0.75rem;
375
+ border-top: 1px solid var(--color-border);
376
+ font-size: 0.8rem;
377
+ color: var(--color-text-secondary);
378
+ }
379
+
380
+ .pi-stars {
381
+ display: inline-flex;
382
+ align-items: center;
383
+ gap: 0.2rem;
384
+ }
385
+
386
+ .pi-pagination {
387
+ display: flex;
388
+ align-items: center;
389
+ justify-content: center;
390
+ gap: 0.75rem;
391
+ margin-top: 1rem;
392
+ padding: 0.5rem 0;
393
+ }
394
+
395
+ .pi-page-info {
396
+ font-size: 0.9rem;
397
+ color: var(--color-text-secondary);
398
+ }
399
+
400
+ /* ── Git Tab ─────────────────────────── */
401
+ .pi-form-group {
402
+ margin-bottom: 1rem;
403
+ }
404
+
405
+ .pi-label {
406
+ display: block;
407
+ font-weight: 600;
408
+ margin-bottom: 0.35rem;
409
+ }
410
+
411
+ .pi-optional {
412
+ font-weight: 400;
413
+ color: var(--color-text-secondary);
414
+ font-size: 0.85rem;
415
+ }
416
+
417
+ .pi-input {
418
+ width: 100%;
419
+ padding: 0.6rem 0.75rem;
420
+ border: 1px solid var(--color-border);
421
+ border-radius: 4px;
422
+ background: var(--color-bg-primary);
423
+ color: var(--color-text-primary);
424
+ font-size: 0.95rem;
425
+ box-sizing: border-box;
426
+ }
427
+
428
+ .pi-hint {
429
+ margin-top: 0.35rem;
430
+ font-size: 0.8rem;
431
+ color: var(--color-text-secondary);
432
+ }
433
+
434
+ .pi-actions {
435
+ display: flex;
436
+ justify-content: flex-start;
437
+ margin: 1rem 0;
438
+ }
439
+
440
+ /* ── ZIP Tab ─────────────────────────── */
441
+ .pi-upload-section {
442
+ text-align: center;
443
+ padding: 2rem 1rem;
444
+ border: 2px dashed var(--color-border);
445
+ border-radius: 8px;
446
+ margin-bottom: 1rem;
447
+ }
448
+
449
+ .pi-upload-btn {
450
+ display: inline-flex;
451
+ align-items: center;
452
+ gap: 0.5rem;
453
+ padding: 0.75rem 1.5rem;
454
+ font-size: 1rem;
455
+ cursor: pointer;
456
+ }
457
+
458
+ .pi-upload-btn.pi-has-file {
459
+ background: var(--color-panel);
460
+ border-color: var(--color-highlight);
461
+ }
462
+
463
+ .pi-actions-center {
464
+ display: flex;
465
+ justify-content: center;
466
+ margin: 1rem 0;
467
+ }
468
+
469
+ /* ── Shared ──────────────────────────── */
470
+ .pi-loading {
471
+ text-align: center;
472
+ padding: 2rem;
473
+ color: var(--color-text-secondary);
474
+ }
475
+
476
+ .pi-error {
477
+ color: var(--color-error);
478
+ padding: 0.75rem;
479
+ background: var(--color-error-bg, rgba(239,68,68,0.1));
480
+ border-radius: 4px;
481
+ margin-top: 0.75rem;
482
+ }
483
+
484
+ .pi-empty {
485
+ text-align: center;
486
+ padding: 2rem;
487
+ color: var(--color-text-secondary);
488
+ }
489
+
490
+ .pi-button-loading {
491
+ display: inline-flex;
492
+ align-items: center;
493
+ gap: 0.5rem;
494
+ }
495
+
496
+ .pi-result {
497
+ display: flex;
498
+ align-items: center;
499
+ gap: 0.75rem;
500
+ padding: 0.75rem;
501
+ background: rgba(34,197,94,0.1);
502
+ border: 1px solid rgba(34,197,94,0.3);
503
+ border-radius: 4px;
504
+ margin-top: 0.75rem;
505
+ }
506
+
507
+ .pi-result-icon .material-symbols-outlined {
508
+ font-size: 2rem;
509
+ color: #22c55e;
510
+ }
511
+
512
+ .pi-result-path {
513
+ font-size: 0.85rem;
514
+ color: var(--color-text-secondary);
515
+ margin-top: 0.25rem;
516
+ font-family: var(--font-family-code);
517
+ }
518
+
519
+ @media (max-width: 500px) {
520
+ .pi-grid {
521
+ grid-template-columns: 1fr;
522
+ }
523
+ }
524
+ </style>
525
+</body>
526
+</html>