From 69845616f0958552d54fb3742209a6b1bb4b4cce Mon Sep 17 00:00:00 2001 From: Wyatt Brege Date: Tue, 10 Mar 2026 16:24:22 -0400 Subject: [PATCH 1/7] refactor: unify editor affordance controls Editor controls - add shared editor affordance buttons and action icon entries - move hover-expand color shifts into the shared theme utility - support icon-name overrides so widgets can swap icons w/o inlining svg Wiki - remove old legacy verbiage --- monitorat/static/editor/editor.css | 85 +++++++++++++++++++++++++++++ monitorat/static/editor/editor.js | 57 ++++++++++++++++++- monitorat/static/themes/default.css | 10 +++- monitorat/static/ui/icons.js | 15 ++++- monitorat/widgets/wiki/app.js | 42 +++++++------- monitorat/widgets/wiki/style.css | 28 ++++++---- 6 files changed, 200 insertions(+), 37 deletions(-) diff --git a/monitorat/static/editor/editor.css b/monitorat/static/editor/editor.css index f789516..8027e0c 100644 --- a/monitorat/static/editor/editor.css +++ b/monitorat/static/editor/editor.css @@ -188,6 +188,91 @@ color: var(--text-primary); } +.editor-affordance-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + margin: 0; + border: none; + border-radius: 999px; + background: transparent; + color: var(--text-muted); + cursor: pointer; + opacity: 0.42; + transition: + opacity 0.15s ease, + color 0.15s ease; +} + +.editor-affordance-btn svg { + width: 16px; + height: 16px; + display: block; + transition: transform 0.15s ease; +} + +.editor-affordance-btn:hover, +.editor-affordance-btn:focus-visible { + background: transparent; + opacity: 1; + color: var(--text-primary); +} + +.editor-affordance-btn:hover svg, +.editor-affordance-btn:focus-visible svg { + transform: scale(1.14); +} + +.editor-affordance-btn:focus-visible { + outline: 2px solid var(--border-muted); + outline-offset: 2px; +} + +.editor-affordance-visible { + opacity: 0.34; +} + +.editor-affordance-reveal-parent { + position: relative; +} + +.editor-affordance-corner { + position: absolute; + top: 6px; + right: 6px; +} + +.editor-affordance-reveal { + opacity: 0; + pointer-events: none; + transform: scale(0.94); +} + +.editor-affordance-reveal-parent:hover .editor-affordance-reveal, +.editor-affordance-reveal-parent:focus-within .editor-affordance-reveal, +.editor-affordance-reveal:hover, +.editor-affordance-reveal:focus-visible { + opacity: 0.58; + pointer-events: auto; + transform: scale(1); +} + +.editor-affordance-reveal:hover, +.editor-affordance-reveal:focus-visible { + opacity: 1; +} + +@media (hover: none), (pointer: coarse) { + .editor-affordance-reveal { + opacity: 0.42; + pointer-events: auto; + transform: scale(1); + } +} + @media (max-height: 600px) { .editor-panes { min-height: 120px; diff --git a/monitorat/static/editor/editor.js b/monitorat/static/editor/editor.js index 9980175..ce36bc8 100644 --- a/monitorat/static/editor/editor.js +++ b/monitorat/static/editor/editor.js @@ -7,8 +7,6 @@ window.Editor = (() => { const CHEVRON_UP = ''; const ICON_SAVE = ''; - const ICON_RESTORE = - ''; const ICON_DELETE = ''; const ICON_CANCEL = @@ -348,3 +346,58 @@ window.Editor = (() => { clearDraft, }; })(); + +window.monitorShared = window.monitorShared || {}; +window.monitorShared.EditorControls = (() => { + function createActionButton(options = {}) { + const { + className = '', + icon = '', + iconName = '', + title = '', + label = '', + visible = false, + } = options; + + const button = document.createElement('button'); + button.type = 'button'; + button.className = [ + 'editor-affordance-btn', + visible ? 'editor-affordance-visible' : 'editor-affordance-reveal', + className, + ] + .filter(Boolean) + .join(' '); + if (title) { + button.title = title; + } + if (label) { + button.setAttribute('aria-label', label); + } + button.innerHTML = + icon || window.monitorShared.IconHandler.getActionIcon(iconName); + return button; + } + + function createOverflowButton(options = {}) { + return createActionButton({ + ...options, + icon: + options.icon || + window.monitorShared.IconHandler.getActionIcon('overflow'), + }); + } + + function createEditButton(options = {}) { + return createActionButton({ + ...options, + iconName: options.iconName || 'edit', + }); + } + + return { + createActionButton, + createEditButton, + createOverflowButton, + }; +})(); diff --git a/monitorat/static/themes/default.css b/monitorat/static/themes/default.css index 982852b..2a07993 100644 --- a/monitorat/static/themes/default.css +++ b/monitorat/static/themes/default.css @@ -945,12 +945,18 @@ h6:hover .header-anchor { /* Utilities */ .hover-expand { + color: var(--text-muted); transform: scale(1); transform-origin: center; - transition: transform 0.15s ease; + transition: + color 0.15s ease, + transform 0.15s ease; } .hover-expand:hover, -.hover-expand-parent:hover .hover-expand { +.hover-expand:focus-visible, +.hover-expand-parent:hover .hover-expand, +.hover-expand-parent:focus-within .hover-expand { + color: var(--text-primary); transform: scale(1.12); } diff --git a/monitorat/static/ui/icons.js b/monitorat/static/ui/icons.js index 315f376..9689ccd 100644 --- a/monitorat/static/ui/icons.js +++ b/monitorat/static/ui/icons.js @@ -6,6 +6,14 @@ */ const IconHandler = (() => { + const ACTION_ICONS = { + edit: '', + 'edit-doc': + '', + overflow: + '', + }; + function getFileExtension(path) { return path?.split('.')?.pop()?.toLowerCase() || ''; } @@ -93,13 +101,18 @@ const IconHandler = (() => { const parser = new DOMParser(); const parsed = parser.parseFromString(svgText, 'image/svg+xml'); const root = parsed.documentElement; - if (root && root.tagName.toLowerCase().endsWith('svg')) { + if (root?.tagName.toLowerCase().endsWith('svg')) { return root; } return null; } + function getActionIcon(name) { + return ACTION_ICONS[name] || ''; + } + return { + getActionIcon, renderIcon, }; })(); diff --git a/monitorat/widgets/wiki/app.js b/monitorat/widgets/wiki/app.js index 81ff9d9..8bd829d 100644 --- a/monitorat/widgets/wiki/app.js +++ b/monitorat/widgets/wiki/app.js @@ -313,23 +313,19 @@ class WikiWidget { } addEditButton() { + const headerElement = this.container.querySelector( + '[data-wiki-section-header="content"]', + ); const notesContainer = this.container.querySelector('.notes'); - const markdownBody = this.container.querySelector('.markdown-body'); - const targetElement = notesContainer || markdownBody; - - if (!targetElement) return; - - const editBtn = document.createElement('button'); - editBtn.className = 'editor-edit-btn hover-expand'; - editBtn.type = 'button'; - editBtn.title = 'Edit'; - editBtn.setAttribute('aria-label', 'Edit document'); - editBtn.innerHTML = ` - - - - - `; + if (!notesContainer) return; + + const controls = window.monitorShared?.EditorControls; + const editBtn = + controls?.createEditButton({ + title: 'Edit', + label: 'Edit document', + iconName: 'edit-doc', + }) || document.createElement('button'); editBtn.addEventListener('click', (event) => { event.stopPropagation(); @@ -338,13 +334,15 @@ class WikiWidget { } }); - if (notesContainer) { - notesContainer.insertBefore(editBtn, notesContainer.firstChild); - } else if (markdownBody) { - markdownBody.insertBefore(editBtn, markdownBody.firstChild); - } else { - targetElement.appendChild(editBtn); + if (headerElement?.textContent?.trim()) { + headerElement.classList.add('editor-affordance-reveal-parent'); + headerElement.appendChild(editBtn); + return; } + + notesContainer.classList.add('editor-affordance-reveal-parent'); + editBtn.classList.add('editor-affordance-corner', 'wiki-edit-floating-btn'); + notesContainer.appendChild(editBtn); } } diff --git a/monitorat/widgets/wiki/style.css b/monitorat/widgets/wiki/style.css index c27cfe2..8d9980b 100644 --- a/monitorat/widgets/wiki/style.css +++ b/monitorat/widgets/wiki/style.css @@ -9,11 +9,20 @@ background: transparent; padding: 16px 18px; } -.notes .editor-edit-btn { - float: inline-end; - margin-block-start: 10px; - margin-inline-end: 10px; - margin-inline-start: 0; +.wiki .feature-header[data-wiki-section-header="content"] { + display: flex; + align-items: center; + gap: 8px; +} +.wiki + .feature-header[data-wiki-section-header="content"] + > .editor-affordance-btn { + margin-inline-start: auto; +} +.notes .wiki-edit-floating-btn { + top: 10px; + right: 10px; + z-index: 2; } .notes[data-mode="seamless"], .notes[data-mode="rail"] { @@ -30,11 +39,10 @@ border-inline-start: 2px solid var(--border-muted); padding-inline-start: 16px; } -.notes[data-mode="seamless"] .editor-edit-btn, -.notes[data-mode="rail"] .editor-edit-btn { - margin-block: 0 8px; - margin-inline-start: 8px; - margin-inline-end: 0; +.notes[data-mode="seamless"] .wiki-edit-floating-btn, +.notes[data-mode="rail"] .wiki-edit-floating-btn { + top: 0; + right: 0; } .notes .markdown-body details { margin-bottom: 1em; From 4c2ea871059be5a532ef9f3d819873e0cae2fe0c Mon Sep 17 00:00:00 2001 From: Wyatt Brege Date: Tue, 10 Mar 2026 17:01:47 -0400 Subject: [PATCH 2/7] refactor: move reminders edit actions to shared overflow affordances --- monitorat/static/controls/listing.js | 21 ++++++++++++++- monitorat/static/editor/editor.css | 11 +++++++- .../widgets/reminders/features/alerts.js | 26 ++++++++----------- .../widgets/reminders/features/controls.js | 6 +++++ monitorat/widgets/reminders/style.css | 9 +------ 5 files changed, 48 insertions(+), 25 deletions(-) diff --git a/monitorat/static/controls/listing.js b/monitorat/static/controls/listing.js index de627f5..3b5a355 100644 --- a/monitorat/static/controls/listing.js +++ b/monitorat/static/controls/listing.js @@ -64,7 +64,7 @@ class ListingControls { initializeAddButton() { if (!this.addConfig) return; - const addButton = this.container.querySelector(this.selectors.add); + let addButton = this.container.querySelector(this.selectors.add); if (!addButton) return; if (this.addConfig.enabled === false) { @@ -72,6 +72,25 @@ class ListingControls { return; } + const affordance = this.addConfig.affordance; + if (affordance) { + const controls = window.monitorShared?.EditorControls; + const createButton = + affordance.type === 'edit' + ? controls?.createEditButton + : controls?.createOverflowButton; + const nextButton = createButton?.({ + className: affordance.className || '', + title: affordance.title || '', + label: affordance.label || '', + visible: affordance.visible !== false, + }); + if (nextButton) { + addButton.replaceWith(nextButton); + addButton = nextButton; + } + } + if (typeof this.addConfig.onClick === 'function') { addButton.addEventListener('click', () => this.addConfig.onClick()); } diff --git a/monitorat/static/editor/editor.css b/monitorat/static/editor/editor.css index 8027e0c..69ed6ca 100644 --- a/monitorat/static/editor/editor.css +++ b/monitorat/static/editor/editor.css @@ -203,6 +203,7 @@ cursor: pointer; opacity: 0.42; transition: + background-color 0.15s ease, opacity 0.15s ease, color 0.15s ease; } @@ -216,7 +217,7 @@ .editor-affordance-btn:hover, .editor-affordance-btn:focus-visible { - background: transparent; + background: color-mix(in srgb, var(--text-primary) 12%, transparent); opacity: 1; color: var(--text-primary); } @@ -245,6 +246,14 @@ right: 6px; } +.editor-affordance-corner-surface { + top: 0; + right: 0; + width: 34px; + height: 34px; + border-radius: 0 8px 0 10px; +} + .editor-affordance-reveal { opacity: 0; pointer-events: none; diff --git a/monitorat/widgets/reminders/features/alerts.js b/monitorat/widgets/reminders/features/alerts.js index d0aee4b..1fd6f02 100644 --- a/monitorat/widgets/reminders/features/alerts.js +++ b/monitorat/widgets/reminders/features/alerts.js @@ -50,11 +50,14 @@ class RemindersAlerts { const hasBadge = showBadge && reminder._source; const isDisabled = reminder.disabled === true; const statusClass = isDisabled ? 'disabled' : reminder.status; + const canEdit = + !disableActions && this.widget.canEditReminders() && !reminder._source; const classes = [ 'reminder-alert', 'status-card', `status-${statusClass}`, 'hover-expand-parent', + canEdit ? 'editor-affordance-reveal-parent' : '', isDisabled ? 'is-disabled' : '', ]; @@ -122,23 +125,16 @@ class RemindersAlerts { content.appendChild(textDiv); content.appendChild(statsDiv); - const canEdit = - !disableActions && this.widget.canEditReminders() && !reminder._source; - const actions = []; if (canEdit) { - const editButton = document.createElement('button'); - editButton.type = 'button'; - editButton.className = - 'reminder-edit-button editor-edit-btn hover-expand'; - editButton.title = 'Edit reminder'; - editButton.setAttribute('aria-label', 'Edit reminder'); - editButton.innerHTML = ` - - - - - `; + const controls = window.monitorShared?.EditorControls; + const editButton = + controls?.createOverflowButton({ + className: + 'editor-affordance-corner editor-affordance-corner-surface', + title: 'Edit reminder', + label: 'Edit reminder', + }) || document.createElement('button'); editButton.addEventListener('click', (event) => { event.stopPropagation(); this.widget.openReminderEditor(reminder); diff --git a/monitorat/widgets/reminders/features/controls.js b/monitorat/widgets/reminders/features/controls.js index 90cae56..cdb822e 100644 --- a/monitorat/widgets/reminders/features/controls.js +++ b/monitorat/widgets/reminders/features/controls.js @@ -40,6 +40,12 @@ class RemindersControls { }, add: { enabled: this.widget.canEditReminders(), + affordance: { + type: 'overflow', + visible: true, + title: 'Add reminder', + label: 'Add reminder', + }, onClick: () => { this.widget.openReminderEditor(); }, diff --git a/monitorat/widgets/reminders/style.css b/monitorat/widgets/reminders/style.css index 4a545f8..664dfe2 100644 --- a/monitorat/widgets/reminders/style.css +++ b/monitorat/widgets/reminders/style.css @@ -61,14 +61,7 @@ position: relative; } .reminder-alert-content { - padding-right: 28px; -} -.reminder-edit-button { - position: absolute; - top: 0; - bottom: 0; - right: 10px; - margin: auto; + padding-right: 30px; } .reminder-editor-preview { padding: 12px 0; From a30786422d56a685ecd1b3acf09ccf25747d7eb7 Mon Sep 17 00:00:00 2001 From: Wyatt Brege Date: Tue, 10 Mar 2026 17:47:22 -0400 Subject: [PATCH 3/7] refactor: move service editing to shared overflow controls Shared editor controls - add a shared card-action wrapper for on-card overflow buttons - move the clipped corner geometry into the editor component layer - keep default affordance buttons on rounded-square hover surfaces Services - replace the desktop card edit pencil with the shared overflow action - preserve service card classes during status refreshes instead of rebuilding the card class string - add a modal edit action for compact and mobile service flows - reserve the card corner so service text does not run under the action zone Reminders - switch reminder card edit actions to the shared card-action helper --- monitorat/static/editor/editor.css | 25 +++++++++++- monitorat/static/editor/editor.js | 31 +++++++++++++- .../widgets/reminders/features/alerts.js | 14 +++---- .../widgets/services/features/controls.js | 6 +++ monitorat/widgets/services/features/info.js | 20 ++++++++++ .../widgets/services/features/snapshot.js | 40 +++++++++---------- monitorat/widgets/services/style.css | 16 ++++++-- 7 files changed, 115 insertions(+), 37 deletions(-) diff --git a/monitorat/static/editor/editor.css b/monitorat/static/editor/editor.css index 69ed6ca..346995a 100644 --- a/monitorat/static/editor/editor.css +++ b/monitorat/static/editor/editor.css @@ -197,7 +197,7 @@ padding: 0; margin: 0; border: none; - border-radius: 999px; + border-radius: 8px; background: transparent; color: var(--text-muted); cursor: pointer; @@ -246,12 +246,33 @@ right: 6px; } -.editor-affordance-corner-surface { +.editor-affordance-card-action { top: 0; right: 0; width: 34px; height: 34px; border-radius: 0 8px 0 10px; + color: var(--text-muted); + transition: background-color 0.15s ease; +} + +.editor-affordance-card-action .editor-affordance-btn { + width: 100%; + height: 100%; + border-radius: inherit; + opacity: 1; + color: inherit; +} + +.editor-affordance-card-action .editor-affordance-btn:hover, +.editor-affordance-card-action .editor-affordance-btn:focus-visible { + background: transparent; +} + +.editor-affordance-card-action:hover, +.editor-affordance-card-action:focus-within { + color: var(--text-primary); + background: color-mix(in srgb, var(--text-primary) 12%, transparent); } .editor-affordance-reveal { diff --git a/monitorat/static/editor/editor.js b/monitorat/static/editor/editor.js index ce36bc8..fc65527 100644 --- a/monitorat/static/editor/editor.js +++ b/monitorat/static/editor/editor.js @@ -357,13 +357,18 @@ window.monitorShared.EditorControls = (() => { title = '', label = '', visible = false, + state = visible ? 'visible' : 'reveal', } = options; const button = document.createElement('button'); button.type = 'button'; button.className = [ 'editor-affordance-btn', - visible ? 'editor-affordance-visible' : 'editor-affordance-reveal', + state === 'visible' + ? 'editor-affordance-visible' + : state === 'reveal' + ? 'editor-affordance-reveal' + : '', className, ] .filter(Boolean) @@ -395,8 +400,32 @@ window.monitorShared.EditorControls = (() => { }); } + function wrapCardAction(button, className = '') { + const container = document.createElement('div'); + container.className = [ + 'editor-affordance-card-action', + 'editor-affordance-corner', + 'editor-affordance-reveal', + className, + ] + .filter(Boolean) + .join(' '); + container.appendChild(button); + return { button, container }; + } + + function createCardOverflowButton(options = {}) { + const { className = '', ...buttonOptions } = options; + const button = createOverflowButton({ + ...buttonOptions, + state: 'none', + }); + return wrapCardAction(button, className); + } + return { createActionButton, + createCardOverflowButton, createEditButton, createOverflowButton, }; diff --git a/monitorat/widgets/reminders/features/alerts.js b/monitorat/widgets/reminders/features/alerts.js index 1fd6f02..d3e23f9 100644 --- a/monitorat/widgets/reminders/features/alerts.js +++ b/monitorat/widgets/reminders/features/alerts.js @@ -128,18 +128,16 @@ class RemindersAlerts { const actions = []; if (canEdit) { const controls = window.monitorShared?.EditorControls; - const editButton = - controls?.createOverflowButton({ - className: - 'editor-affordance-corner editor-affordance-corner-surface', - title: 'Edit reminder', - label: 'Edit reminder', - }) || document.createElement('button'); + const editAction = controls?.createCardOverflowButton({ + title: 'Edit reminder', + label: 'Edit reminder', + }); + const editButton = editAction?.button || document.createElement('button'); editButton.addEventListener('click', (event) => { event.stopPropagation(); this.widget.openReminderEditor(reminder); }); - actions.push(editButton); + actions.push(editAction?.container || editButton); } const Alerts = window.monitorShared.Alerts; diff --git a/monitorat/widgets/services/features/controls.js b/monitorat/widgets/services/features/controls.js index 96ebc13..64f2ce5 100644 --- a/monitorat/widgets/services/features/controls.js +++ b/monitorat/widgets/services/features/controls.js @@ -36,6 +36,12 @@ class ServicesControls { }, add: { enabled: this.widget.canEditServices(), + affordance: { + type: 'overflow', + visible: true, + title: 'Add service', + label: 'Add service', + }, onClick: () => { this.widget.openServiceEditor(null); }, diff --git a/monitorat/widgets/services/features/info.js b/monitorat/widgets/services/features/info.js index 7fd9790..4e4bf10 100644 --- a/monitorat/widgets/services/features/info.js +++ b/monitorat/widgets/services/features/info.js @@ -98,6 +98,7 @@ class ServicesInfo { service.local && service.local !== service.url && service.local.trim() !== ''; + const canEdit = this.widget.canEditServices() && !service._source; const imgBase = service._source ? `api/proxy/${service._source}/img` : this.widget.getImgBase(); @@ -168,6 +169,18 @@ class ServicesInfo { : '' } + ${ + canEdit + ? ` +
+ +
+ ` + : '' + } `; window.Modal.show({ @@ -203,6 +216,13 @@ class ServicesInfo { } }); }); + + document.querySelectorAll('.service-modal-edit').forEach((button) => { + button.addEventListener('click', () => { + window.Modal.hide(); + this.widget.openServiceEditor(service); + }); + }); } } diff --git a/monitorat/widgets/services/features/snapshot.js b/monitorat/widgets/services/features/snapshot.js index 0089f26..cbf2503 100644 --- a/monitorat/widgets/services/features/snapshot.js +++ b/monitorat/widgets/services/features/snapshot.js @@ -4,19 +4,12 @@ // Single-source is the trivial case: one source, no badges. // Multi-source merges all services with source badges. -const EDIT_ICON = - ''; - class ServicesSnapshot { constructor(widget) { this.widget = widget; this.info = widget.features.info; } - getEditIcon() { - return EDIT_ICON; - } - render() { const cardsContainer = this.widget.container.querySelector('.service-grid'); if (!cardsContainer || !this.widget.servicesData) return; @@ -107,11 +100,15 @@ class ServicesSnapshot { const card = document.createElement('div'); const mode = this.widget.getDisplayMode(); const hasBadge = showBadge && service._source; + const canEdit = mode !== 'compact' && this.widget.canEditServices(); const baseClass = mode === 'compact' ? 'service-card compact' : 'service-card card status-card'; card.className = `${baseClass}${hasBadge ? ' has-badge' : ''}`; + if (canEdit) { + card.classList.add('editor-affordance-reveal-parent'); + } card.setAttribute('data-service-key', service._key); card.setAttribute('data-service-source', service._source || ''); @@ -184,18 +181,18 @@ class ServicesSnapshot { }); card.appendChild(infoBtn); - if (this.widget.canEditServices()) { - const editBtn = document.createElement('button'); - editBtn.type = 'button'; - editBtn.className = - 'service-edit-btn service-action-btn editor-edit-btn'; - editBtn.innerHTML = this.getEditIcon(); - editBtn.title = 'Edit service'; + if (canEdit) { + const controls = window.monitorShared?.EditorControls; + const editAction = controls?.createCardOverflowButton({ + title: 'Edit service', + label: 'Edit service', + }); + const editBtn = editAction?.button || document.createElement('button'); editBtn.addEventListener('click', (event) => { event.stopPropagation(); this.widget.openServiceEditor(service); }); - card.appendChild(editBtn); + card.appendChild(editAction?.container || editBtn); } } @@ -209,7 +206,7 @@ class ServicesSnapshot { if ( event.target.closest('.service-info-btn') || event.target.closest('.service-status-dot') || - event.target.closest('.service-edit-btn') + event.target.closest('.editor-affordance-card-action') ) { return; } @@ -317,13 +314,12 @@ class ServicesSnapshot { }); } - const hasBadge = card.classList.contains('has-badge'); - const isCompact = card.classList.contains('compact'); - const baseClass = isCompact - ? 'service-card compact' - : 'service-card card status-card'; const statusClass = this.widget.getStatusClass(overallStatus); - card.className = `${baseClass}${hasBadge ? ' has-badge' : ''} ${statusClass}`; + const statusClasses = this.widget + .getStatusSeverity() + .map((status) => this.widget.getStatusClass(status)); + card.classList.remove(...statusClasses); + card.classList.add(statusClass); const statusTextElement = card.querySelector('.service-status'); if (statusTextElement) { diff --git a/monitorat/widgets/services/style.css b/monitorat/widgets/services/style.css index fc41b70..30e76c6 100644 --- a/monitorat/widgets/services/style.css +++ b/monitorat/widgets/services/style.css @@ -125,6 +125,7 @@ } .service-info { flex: 1; + padding-right: 28px; } .service-card.compact .service-info { display: none; @@ -184,10 +185,11 @@ bottom: 6px; pointer-events: auto; } -.service-edit-btn { - position: absolute; - right: 38px; - bottom: 6px; +.service-card .editor-affordance-card-action { + pointer-events: auto; + z-index: 2; +} +.service-card .editor-affordance-card-action .editor-affordance-btn { pointer-events: auto; } .service-action-btn { @@ -436,3 +438,9 @@ width: 18px; height: 18px; } + +.service-modal-actions { + display: flex; + justify-content: flex-end; + margin-top: 16px; +} From 824c85beebfc0327b6d76853749c8df995fbf1bc Mon Sep 17 00:00:00 2001 From: Wyatt Brege Date: Tue, 10 Mar 2026 18:00:57 -0400 Subject: [PATCH 4/7] refactor: route service card actions through the info modal Services cards - remove the desktop info icon from service cards - make the shared overflow action open the service info modal - keep status refreshes updating card state without rebuilding card classes Services modal - replace the bottom edit button with a right-aligned wrench in the modal header row - style the wrench with the shared affordance hover treatment - keep compact and desktop service editing reachable from the info modal Shared icons - add a wrench action icon for modal edit entrypoints --- monitorat/static/ui/icons.js | 2 + monitorat/widgets/services/features/info.js | 36 ++++++------- .../widgets/services/features/snapshot.js | 17 ++---- monitorat/widgets/services/style.css | 53 +++++++------------ 4 files changed, 38 insertions(+), 70 deletions(-) diff --git a/monitorat/static/ui/icons.js b/monitorat/static/ui/icons.js index 9689ccd..97f1182 100644 --- a/monitorat/static/ui/icons.js +++ b/monitorat/static/ui/icons.js @@ -12,6 +12,8 @@ const IconHandler = (() => { '', overflow: '', + wrench: + '', }; function getFileExtension(path) { diff --git a/monitorat/widgets/services/features/info.js b/monitorat/widgets/services/features/info.js index 4e4bf10..5b516e1 100644 --- a/monitorat/widgets/services/features/info.js +++ b/monitorat/widgets/services/features/info.js @@ -8,7 +8,6 @@ const SERVICE_TYPE_ICONS = { }; const SERVICE_ACTION_ICONS = { - info: '', external: '', local: @@ -21,10 +20,6 @@ class ServicesInfo { this.widget = widget; } - getInfoIcon() { - return SERVICE_ACTION_ICONS.info; - } - getServiceStatusInfo(service) { const statusData = service._source ? this.widget.statusBySource[service._source] || {} @@ -126,11 +121,22 @@ class ServicesInfo { const content = `
-
- ${service.name} - +
+
+ ${service.name} + +
+ ${service.name}
- ${service.name} + ${ + canEdit + ? ` + + ` + : '' + }
${statusHtml} - ${ - canEdit - ? ` -
- -
- ` - : '' - } `; window.Modal.show({ diff --git a/monitorat/widgets/services/features/snapshot.js b/monitorat/widgets/services/features/snapshot.js index cbf2503..96f7c4f 100644 --- a/monitorat/widgets/services/features/snapshot.js +++ b/monitorat/widgets/services/features/snapshot.js @@ -170,27 +170,17 @@ class ServicesSnapshot { card.appendChild(iconContainer); card.appendChild(info); - const infoBtn = document.createElement('button'); - infoBtn.type = 'button'; - infoBtn.className = 'service-info-btn service-action-btn info-button'; - infoBtn.innerHTML = this.info.getInfoIcon(); - infoBtn.title = 'Service details'; - infoBtn.addEventListener('click', (event) => { - event.stopPropagation(); - this.info.open(service); - }); - card.appendChild(infoBtn); if (canEdit) { const controls = window.monitorShared?.EditorControls; const editAction = controls?.createCardOverflowButton({ - title: 'Edit service', - label: 'Edit service', + title: 'Service details', + label: 'Service details', }); const editBtn = editAction?.button || document.createElement('button'); editBtn.addEventListener('click', (event) => { event.stopPropagation(); - this.widget.openServiceEditor(service); + this.info.open(service); }); card.appendChild(editAction?.container || editBtn); } @@ -204,7 +194,6 @@ class ServicesSnapshot { return; } if ( - event.target.closest('.service-info-btn') || event.target.closest('.service-status-dot') || event.target.closest('.editor-affordance-card-action') ) { diff --git a/monitorat/widgets/services/style.css b/monitorat/widgets/services/style.css index 30e76c6..946c7e3 100644 --- a/monitorat/widgets/services/style.css +++ b/monitorat/widgets/services/style.css @@ -179,12 +179,6 @@ .service-card.status-never .service-status-dot { background: rgb(var(--status-unknown-rgb)); } -.service-info-btn { - position: absolute; - right: 6px; - bottom: 6px; - pointer-events: auto; -} .service-card .editor-affordance-card-action { pointer-events: auto; z-index: 2; @@ -192,30 +186,6 @@ .service-card .editor-affordance-card-action .editor-affordance-btn { pointer-events: auto; } -.service-action-btn { - display: inline-flex; - align-items: center; - justify-content: center; - width: 34px; - height: 34px; - padding: 7px; - margin: 0; - opacity: 0.58; -} -.service-action-btn:hover, -.service-action-btn:focus-visible { - opacity: 1; - transform: none; -} -.service-action-btn svg { - width: 100%; - height: 100%; - transition: transform 0.15s ease; -} -.service-action-btn:hover svg, -.service-action-btn:focus-visible svg { - transform: scale(1.14); -} .service-card.has-badge { position: relative; } @@ -243,10 +213,18 @@ .url-picker-service { display: flex; align-items: center; - gap: 12px; + justify-content: space-between; + gap: 16px; margin-bottom: 16px; } +.url-picker-service-main { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + .url-picker-title { display: inline-flex; align-items: center; @@ -308,6 +286,7 @@ font-size: 1.1rem; font-weight: 500; color: var(--text-primary); + min-width: 0; } .url-picker-links { @@ -439,8 +418,12 @@ height: 18px; } -.service-modal-actions { - display: flex; - justify-content: flex-end; - margin-top: 16px; +.service-modal-edit { + flex-shrink: 0; + color: var(--text-muted); +} + +.service-modal-edit svg { + width: 18px; + height: 18px; } From 82e9fcfefa60c891260dc7acfb6cbf57c41b0ee4 Mon Sep 17 00:00:00 2001 From: Wyatt Brege Date: Tue, 10 Mar 2026 18:13:36 -0400 Subject: [PATCH 5/7] refactor: move metrics configure to the snapshot header Metrics snapshot - remove the hidden widget-wide configure controls row - add the shared visible overflow affordance to the snapshot feature-header - scope metrics editing to the snapshot subwidget only Metrics styling - align the snapshot header affordance with the shared widget-level action treatment - leave history and events controls unchanged --- monitorat/widgets/metrics/app.js | 21 +++++++++------- monitorat/widgets/metrics/features/editor.js | 2 +- monitorat/widgets/metrics/index.html | 5 ---- monitorat/widgets/metrics/style.css | 26 ++++++++++++++------ 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/monitorat/widgets/metrics/app.js b/monitorat/widgets/metrics/app.js index 76f7b06..5e68a67 100644 --- a/monitorat/widgets/metrics/app.js +++ b/monitorat/widgets/metrics/app.js @@ -294,19 +294,22 @@ class MetricsWidget { } addEditButton() { - const controlsRow = this.container.querySelector( - '[data-metrics="widget-controls"]', + const snapshotHeader = this.container.querySelector( + '[data-metrics-section-header="snapshot"]', ); - if (!controlsRow) return; - - controlsRow.style.display = ''; - const configureBtn = controlsRow.querySelector('.metrics-configure'); - if (!configureBtn) return; - - configureBtn.style.display = ''; + if (!snapshotHeader) return; + + const controls = window.monitorShared?.EditorControls; + const configureBtn = + controls?.createOverflowButton({ + visible: true, + title: 'Configure metrics', + label: 'Configure metrics', + }) || document.createElement('button'); configureBtn.addEventListener('click', () => { this.openMetricsEditor(); }); + snapshotHeader.appendChild(configureBtn); } setupEventListeners() { diff --git a/monitorat/widgets/metrics/features/editor.js b/monitorat/widgets/metrics/features/editor.js index 60ea242..a2f075b 100644 --- a/monitorat/widgets/metrics/features/editor.js +++ b/monitorat/widgets/metrics/features/editor.js @@ -35,7 +35,7 @@ class MetricsEditor { file: sourcePath, content: '', useForm: true, - title: 'Snapshot Tiles', + title: 'Metrics Snapshot Tiles', labels: { edit: 'Configure', preview: 'Preview' }, onSave: async () => { const payload = this.collectQuantities(formContainer); diff --git a/monitorat/widgets/metrics/index.html b/monitorat/widgets/metrics/index.html index 0575c26..a853dfe 100644 --- a/monitorat/widgets/metrics/index.html +++ b/monitorat/widgets/metrics/index.html @@ -1,11 +1,6 @@
- -
diff --git a/monitorat/widgets/metrics/style.css b/monitorat/widgets/metrics/style.css index 8329349..bc5bb72 100644 --- a/monitorat/widgets/metrics/style.css +++ b/monitorat/widgets/metrics/style.css @@ -12,6 +12,19 @@ gap: 6px; } +.metrics .alerts-header { + display: flex; + align-items: center; + gap: 12px; +} + +.metrics .alerts-actions { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + .metrics .metrics-events .feature-header[data-metrics-section-header="events"] { margin-bottom: 4px; } @@ -28,17 +41,16 @@ margin-bottom: 0; } -.metrics .alerts-header { +.metrics .feature-header[data-metrics-section-header="snapshot"] { display: flex; align-items: center; - gap: 12px; + gap: 8px; } -.metrics .alerts-actions { - display: flex; - align-items: center; - gap: 12px; - flex-wrap: wrap; +.metrics + .feature-header[data-metrics-section-header="snapshot"] + > .editor-affordance-btn { + margin-inline-start: auto; } .metrics .alert { From e970eaf8014f3a786f1c8d7ded3dadbc3d015cdb Mon Sep 17 00:00:00 2001 From: Wyatt Brege Date: Tue, 10 Mar 2026 18:17:12 -0400 Subject: [PATCH 6/7] chore: remove orphaned editor-edit-btn CSS --- monitorat/static/editor/editor.css | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/monitorat/static/editor/editor.css b/monitorat/static/editor/editor.css index 346995a..f4ec01e 100644 --- a/monitorat/static/editor/editor.css +++ b/monitorat/static/editor/editor.css @@ -159,35 +159,6 @@ font-weight: 500; } -.editor-edit-btn { - display: inline-flex; - align-items: center; - justify-content: center; - border: none; - padding: 4px; - border-radius: 999px; - background: transparent; - color: var(--text-muted); - cursor: pointer; - transition: - background 0.15s ease, - color 0.15s ease; - width: 28px; - height: 28px; - margin-left: 8px; -} - -.editor-edit-btn svg { - width: 16px; - height: 16px; - display: block; -} - -.editor-edit-btn:hover { - background: transparent; - color: var(--text-primary); -} - .editor-affordance-btn { display: inline-flex; align-items: center; From da386c5ca82ef41f941aa1d325932762dd8c24cd Mon Sep 17 00:00:00 2001 From: Wyatt Brege Date: Tue, 10 Mar 2026 18:23:04 -0400 Subject: [PATCH 7/7] docs: update editor demo for new affordances --- demo/editor/README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/demo/editor/README.md b/demo/editor/README.md index 77240b5..dfe31fe 100644 --- a/demo/editor/README.md +++ b/demo/editor/README.md @@ -4,7 +4,14 @@ This is the **editor demo** for monitorat. Any widget that can be edited or conf ### How to Edit -Click the pencil icon next to or on any wiki or card, or create a new entry with "Add Widget item" button, to open the editor. +Use the widget affordances: + +- Wiki: hover the wiki header and click the pencil-paper icon +- Reminders: click the header `...` to add a reminder, or hover a card and + click its corner `...` to edit it +- Services: click the header `...` to add a service, or hover a card and click + its corner `...` to open the info modal, then use the wrench to edit +- Metrics: click the `...` in the snapshot header to configure snapshot tiles ### Features @@ -21,15 +28,14 @@ Click the pencil icon next to or on any wiki or card, or create a new entry with ### Card Editors -- **Add**: Click the "Add Widget Item" button to add a new display card for +- **Add**: Click the header `...` action to add a new display card for - System Metrics - Services - Reminders -- **Edit**: Click the pencil icon to edit a display card +- **Edit**: Use the card `...` action or modal wrench for widget items - **Save**: Click Save to persist changes ### Caveats - **Histories**: Charts and Tables, in addition to their display quantities and dropdowns, are only editable through YAML - **Notifications**: Apprise URLs for the notification harness are also YAML-only (used by multiple widgets) -