| 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> |