main
html 372 lines 14 KB
Raw
1 <html>
2 <head>
3 <title>Plugin Settings</title>
4 <script type="module">
5 import { store } from "/components/plugins/plugin-settings-store.js";
6 </script>
7 </head>
8 <body>
9 <div x-data="(() => {
10 const context = $instantiate($store.pluginSettingsPrototype);
11 return {
12 context,
13 get config() {
14 return this.context.settings;
15 },
16 };
17 })()">
18 <template x-if="context">
19 <div>
20 <div x-create="(() => {
21 const modal = $el.closest('.modal');
22 if (modal) {
23 modal.__pluginSettingsContext = context;
24 const modalTitle = modal.querySelector('.modal-title');
25 if (modalTitle) modalTitle.textContent = context.modalTitle || 'Plugin Settings';
26 }
27 })()"
28 x-destroy="context.cleanup()">
29
30 <!-- Context toolbar: Project + Agent profile (only when at least one scope is configurable) -->
31 <div class="plugin-settings-scope-section"
32 x-show="(context.perProjectConfig || context.perAgentConfig) && !context.hideSettingsActions">
33 <div class="plugin-settings-scope-header">
34 <div class="plugin-settings-scope-header-copy">
35 <div class="plugin-settings-scope-title">Settings scope</div>
36 <div class="plugin-settings-scope-desc"
37 x-text="
38 context.perProjectConfig && context.perAgentConfig
39 ? 'This plugin supports settings per project or agent profile.'
40 : context.perProjectConfig
41 ? 'This plugin supports settings per project.'
42 : 'This plugin supports settings per agent profile.'
43 ">
44 </div>
45 </div>
46 <div class="plugin-settings-scope-header-toggle"
47 x-show="$store.pluginToggle && !$store.pluginToggle.alwaysEnabled">
48 <span class="plugin-settings-toolbar-label">Enabled</span>
49 <label class="toggle">
50 <input type="checkbox"
51 :checked="$store.pluginToggle?.status === 'enabled'"
52 :disabled="$store.pluginToggle?.isSaving || context.isLoading"
53 @change="context.setPluginEnabled($event.target.checked)">
54 <span class="toggler"></span>
55 </label>
56 <span class="plugin-toggle-status-text" x-text="$store.pluginToggle?.statusLabel"></span>
57 </div>
58 </div>
59 <div class="plugin-settings-toolbar">
60 <div class="plugin-settings-toolbar-row">
61
62 <label class="plugin-settings-toolbar-item"
63 x-show="context.perProjectConfig">
64 <span class="plugin-settings-toolbar-label">Project</span>
65 <select x-model="context.projectName"
66 x-init="$nextTick(() => $el.value = context.projectName)"
67 @change="context.onScopeChanged()"
68 :disabled="!context.perProjectConfig">
69 <option value="">Global</option>
70 <template x-for="project in context.projects" :key="project.key">
71 <option :value="project.key" x-text="project.label"></option>
72 </template>
73 </select>
74 </label>
75
76 <label class="plugin-settings-toolbar-item"
77 x-show="context.perAgentConfig">
78 <span class="plugin-settings-toolbar-label">Agent profile</span>
79 <select x-model="context.agentProfileKey"
80 x-init="$nextTick(() => $el.value = context.agentProfileKey)"
81 @change="context.onScopeChanged()"
82 :disabled="!context.perAgentConfig">
83 <option value="">All profiles</option>
84 <template x-for="profile in context.agentProfiles" :key="profile.key">
85 <option :value="profile.key" x-text="profile.label"></option>
86 </template>
87 </select>
88 </label>
89
90 <button type="button" class="button plugin-settings-toolbar-button" title="Show existing configurations" @click="context.openConfigListModal()">
91 <x-icon class="icon" name="list"></x-icon>
92 </button>
93
94 </div>
95 </div>
96
97 <div x-show="context.scopeMismatchMessage" class="plugin-settings-scope-info">
98 <x-icon name="info"></x-icon>
99 <span x-text="context.scopeMismatchMessage"></span>
100 </div>
101 </div>
102
103 <!-- Error -->
104 <div x-show="context.error" class="plugin-settings-error">
105 <x-icon name="error"></x-icon>
106 <span x-text="context.error"></span>
107 </div>
108
109 <!-- Loading -->
110 <div x-show="context.isLoading" class="plugin-settings-loading">
111 <x-icon class="spinning" name="progress_activity"></x-icon>
112 <span>Loading settings...</span>
113 </div>
114
115 <!-- Plugin settings body: plugin provides /plugins/<name>/webui/config.html -->
116 <div x-show="!context.isLoading"
117 class="plugin-settings-body"
118 x-html="context.settingsComponentHtml">
119 </div>
120 <div x-show="!context.isLoading && !context.pluginMeta?.has_config_screen"
121 class="plugin-settings-body">
122 <div class="section-title">Scoped configurations</div>
123 <div class="section-description">
124 Use the scope controls and list button above to review or remove existing configurations.
125 </div>
126 </div>
127
128 </div>
129
130 <!-- Footer (pinned outside scroll area) -->
131 <div class="modal-footer plugin-settings-footer" data-modal-footer>
132 <div class="plugin-settings-footer-nav"
133 x-show="context.wizardFooter?.visible?.()">
134 <div class="plugin-settings-footer-nav-row">
135 <button class="btn"
136 @click="context.wizardFooter?.onBack?.()"
137 :disabled="!context.wizardFooter?.canGoBack?.()">
138 <span x-text="context.wizardFooter?.backLabel?.() || 'Back'"></span>
139 </button>
140 <div class="plugin-settings-footer-note" x-text="context.wizardFooter?.note?.() || ''"></div>
141 </div>
142 </div>
143
144 <div class="plugin-settings-footer-actions">
145 <button class="btn"
146 x-show="!context.hideSettingsActions && context.pluginMeta?.has_config_screen"
147 @click="context.resetToDefault()"
148 :disabled="context?.isSaving || context?.isLoading">
149 Reset to default
150 </button>
151 <button class="btn btn-field"
152 x-show="context.wizardFooter?.showNext?.()"
153 @click="context.wizardFooter?.onNext?.()"
154 :disabled="context.wizardFooter?.nextDisabled?.()">
155 <span x-text="context.wizardFooter?.nextLabel?.() || 'Next'"></span>
156 </button>
157 <button class="btn btn-ok"
158 x-show="!context.hideSettingsActions && context.pluginMeta?.has_config_screen && (!context.wizardFooter || context.wizardFooter?.showSave?.())"
159 @click="context.save()"
160 :disabled="context?.isSaving || context?.isLoading">
161 Save
162 </button>
163 <button class="btn btn-cancel"
164 @click="window.closeModal?.()">
165 Cancel
166 </button>
167 </div>
168 </div>
169 </div>
170 </template>
171 </div>
172
173 <style>
174 .plugin-settings-toolbar, .plugin-settings-body {
175 padding: 1rem;
176 }
177
178 .plugin-settings-toolbar-row {
179 display: flex;
180 align-items: center;
181 gap: clamp(0.75rem, 2vw, 2rem);
182 flex-wrap: wrap;
183 }
184
185 .plugin-settings-toolbar-item {
186 display: flex;
187 align-items: center;
188 gap: 0.5rem;
189 flex: 1 1 0;
190 min-width: 12rem;
191 margin: 0;
192 }
193
194 .plugin-settings-toolbar-button {
195 flex: 0 0 auto;
196 padding: 0.5rem 0.75rem;
197 height: 2.5rem;
198 }
199
200 .plugin-settings-toolbar-label {
201 font-weight: 600;
202 color: var(--color-text-secondary);
203 white-space: nowrap;
204 }
205
206 .plugin-settings-toolbar-item select {
207 flex: 1 1 auto;
208 min-width: 0;
209 }
210
211 .plugin-settings-scope-info {
212 display: flex;
213 align-items: flex-start;
214 gap: 0.5rem;
215 padding: 0 1rem 1rem 1rem;
216 color: var(--color-text-secondary);
217 font-size: var(--font-size-small);
218 }
219
220 @media (max-width: 640px) {
221 .plugin-settings-toolbar-item {
222 flex-basis: 100%;
223 min-width: 0;
224 }
225 }
226
227 .plugin-settings-error {
228 display: flex;
229 align-items: center;
230 gap: 0.5rem;
231 color: var(--color-error, #e74c3c);
232 background: var(--color-error-bg, #fdecea);
233 border-radius: 4px;
234 padding: 0.5rem 0.75rem;
235 margin-bottom: 0.75rem;
236 font-size: var(--font-size-small);
237 }
238
239 .plugin-settings-loading {
240 display: flex;
241 align-items: center;
242 justify-content: center;
243 gap: 0.5rem;
244 padding: 2rem;
245 color: var(--color-text-secondary);
246 }
247
248 .plugin-settings-body {
249 min-height: 4rem;
250 }
251
252 .plugin-settings-footer {
253 justify-content: space-between;
254 gap: 1rem;
255 flex-wrap: wrap;
256 }
257
258 .plugin-settings-footer-nav {
259 flex: 1 1 22rem;
260 min-width: 18rem;
261 }
262
263 .plugin-settings-footer-nav-row {
264 display: flex;
265 align-items: center;
266 gap: 0.75rem;
267 width: 100%;
268 }
269
270 .plugin-settings-footer-note {
271 color: var(--color-text-secondary);
272 font-size: var(--font-size-small);
273 line-height: 1.4;
274 flex: 1 1 auto;
275 min-width: 0;
276 }
277
278 .plugin-settings-footer-actions {
279 display: flex;
280 align-items: center;
281 gap: 1rem;
282 margin-left: auto;
283 }
284
285 .spinning {
286 animation: spin 1s linear infinite;
287 }
288
289 @keyframes spin {
290 from { transform: rotate(0deg); }
291 to { transform: rotate(360deg); }
292 }
293
294 .plugin-settings-scope-section {
295 border: 1px solid var(--color-border);
296 border-radius: 4px;
297 margin: 1rem;
298 padding: 0;
299 overflow: hidden;
300 }
301
302 .plugin-settings-scope-header {
303 display: flex;
304 align-items: flex-start;
305 justify-content: space-between;
306 gap: 1rem;
307 padding: 0.75rem 1rem;
308 border-bottom: 1px solid var(--color-border);
309 background: var(--color-bg-secondary);
310 }
311
312 .plugin-settings-scope-header-copy {
313 min-width: 0;
314 }
315
316 .plugin-settings-scope-header-toggle {
317 display: flex;
318 align-items: center;
319 gap: 0.75rem;
320 margin-left: auto;
321 flex: 0 0 auto;
322 white-space: nowrap;
323 }
324
325 .plugin-settings-scope-title {
326 font-weight: 600;
327 font-size: var(--font-size-normal);
328 color: var(--color-text-primary);
329 }
330
331 .plugin-settings-scope-desc {
332 font-size: var(--font-size-small);
333 color: var(--color-text-secondary);
334 margin-top: 0.25rem;
335 }
336
337 .plugin-toggle-status-text {
338 font-weight: 600;
339 color: var(--color-text-primary);
340 min-width: 2rem;
341 }
342
343 @media (max-width: 640px) {
344 .plugin-settings-scope-header {
345 flex-direction: column;
346 align-items: stretch;
347 }
348
349 .plugin-settings-scope-header-toggle {
350 margin-left: 0;
351 justify-content: flex-start;
352 white-space: normal;
353 }
354
355 .plugin-settings-footer-nav {
356 width: 100%;
357 min-width: 0;
358 }
359
360 .plugin-settings-footer-nav-row {
361 flex-wrap: wrap;
362 }
363
364 .plugin-settings-footer-actions {
365 width: 100%;
366 justify-content: flex-end;
367 margin-left: 0;
368 }
369 }
370 </style>
371 </body>
372 </html>