Replace click routing with direct button handlers

This commit is contained in:
dwrz
2026-11-01 11:42:15 +00:00
parent 456d9ee6f3
commit a70634a1e9
4 changed files with 211 additions and 245 deletions
+196 -197
View File
@@ -95,6 +95,7 @@ export class UI {
// Toolbar.
this.$context = document.getElementById('context');
this.$reset = document.getElementById('reset');
this.$scrollUp = document.getElementById('scroll-up');
this.$scrollDown = document.getElementById('scroll-down');
this.$mute = document.getElementById('mute');
this.$settings = document.getElementById('settings');
@@ -214,6 +215,11 @@ export class UI {
// stay clickable whatever the last enabled state was.
this.$send.disabled = false;
} else {
// A pending stop confirmation must not stay highlighted on the
// button after the run ends and the button returns to send.
if (this.$pendingConfirm?.$target === this.$send) {
this._clearConfirm();
}
// Restore the enabled state recorded before the run. A caller
// cannot leave the button disabled by forgetting to call
// updateSendEnabled after this.
@@ -277,9 +283,8 @@ export class UI {
const $name = $chip.querySelector('.compose__attachment-name');
$name.textContent = file.name;
$name.title = file.name;
$chip
.querySelector('.compose__attachment-remove')
.addEventListener('click', () => onRemove(index));
$chip.querySelector('.compose__attachment-remove').onclick = () =>
onRemove(index);
this.$attachments.appendChild($chip);
}
}
@@ -402,6 +407,12 @@ export class UI {
` [data-parent-id="${CSS.escape(id)}"]`,
)
.forEach(($bubble) => {
if (
this.$pendingConfirm &&
$bubble.contains(this.$pendingConfirm.$target)
) {
this._clearConfirm();
}
$bubble.remove();
});
}
@@ -543,6 +554,7 @@ export class UI {
$content.innerHTML =
'<em class="message__placeholder">System Default</em>';
}
this._bindMessageActions($bubble, { id: 'system' });
this.$chat.insertBefore($bubble, this.$chatLoading);
}
@@ -557,6 +569,7 @@ export class UI {
this.$tplErrorMessage.content.cloneNode(true).firstElementChild;
$bubble.querySelector('.message__content').textContent = message;
$bubble.dataset.copyText = message;
this._bindMessageActions($bubble, { isError: true });
this.$chat.insertBefore($bubble, this.$chatLoading);
this.scrollToBottom();
}
@@ -567,7 +580,15 @@ export class UI {
*/
clearChat() {
for (const $bubble of [...this.$chat.children]) {
if ($bubble !== this.$chatLoading) $bubble.remove();
if ($bubble !== this.$chatLoading) {
if (
this.$pendingConfirm &&
$bubble.contains(this.$pendingConfirm.$target)
) {
this._clearConfirm();
}
$bubble.remove();
}
}
}
@@ -634,7 +655,7 @@ export class UI {
const $bubble = this.$chat.querySelector(
`[data-id="${CSS.escape(messageId)}"]`,
);
const $btn = $bubble?.querySelector('[data-action="play"]');
const $btn = $bubble?.querySelector('.message__action-btn--play');
if (!$btn) return;
$btn.replaceChildren(this._icon(playing ? 'stop' : 'volume'));
$btn.classList.toggle('playing', playing);
@@ -877,6 +898,12 @@ export class UI {
/** closeConversations hides the picker. */
closeConversations() {
if (
this.$pendingConfirm &&
this.$conversationsOverlay.contains(this.$pendingConfirm.$target)
) {
this._clearConfirm();
}
this.$conversationsOverlay.classList.remove('open');
this.$conversations.focus();
}
@@ -890,6 +917,12 @@ export class UI {
* @param {{currentId?: string | null}} [options]
*/
renderConversationList(rows, { currentId = null } = {}) {
if (
this.$pendingConfirm &&
this.$conversationsList.contains(this.$pendingConfirm.$target)
) {
this._clearConfirm();
}
this.$conversationsList.replaceChildren();
if (rows.length === 0) {
const $empty = this.document.createElement('div');
@@ -903,7 +936,8 @@ export class UI {
for (const conversation of rows) {
const $item =
this.$tplConversationItem.content.cloneNode(true).firstElementChild;
$item.dataset.conversationId = conversation.id;
const id = conversation.id;
$item.dataset.conversationId = id;
$item.querySelector('.conversations__item-title').textContent =
conversationLabel(conversation);
$item.querySelector('.conversations__item-created').textContent =
@@ -914,10 +948,19 @@ export class UI {
`Updated ${CONVERSATION_DATE.format(
new Date(conversation.updated_at),
)}`;
if (conversation.id === currentId) {
if (id === currentId) {
$item.classList.add('conversations__item--current');
$item.setAttribute('aria-current', 'true');
}
$item.querySelector('.conversations__item-main').onclick = () =>
this.handlers.onSelectConversation(id);
$item.querySelector('.conversations__item-rename').onclick = () =>
this.handlers.onRenameConversation(id);
const $delete = $item.querySelector('.conversations__item-delete');
$delete.onclick = () =>
this._confirm(`conversation:${id}`, $delete, 'pending', () =>
this.handlers.onDeleteConversation(id),
);
this.$conversationsList.appendChild($item);
}
}
@@ -1057,9 +1100,21 @@ export class UI {
// ====================
_bindEvents() {
// One delegated click listener routes every data-action.
this.document.addEventListener('click', (event) =>
this._handleClick(event),
// Clear a pending two-press confirmation when a click lands
// outside its button. Running in the capture phase clears an
// earlier confirmation before the clicked button's own handler
// arms or confirms a new one.
this.document.addEventListener(
'click',
(event) => {
if (
this.$pendingConfirm &&
!this.$pendingConfirm.$target.contains(event.target)
) {
this._clearConfirm();
}
},
true,
);
// Press-and-hold for push-to-talk and transcription.
@@ -1103,8 +1158,18 @@ export class UI {
this.handlers.onComposeInput();
});
// Attachments.
this.$attach.addEventListener('click', () => this.$fileInput.click());
// Compose buttons and attachments.
this.$attach.onclick = () => this.$fileInput.click();
this.$location.onclick = () => this.handlers.onLocationToggle();
this.$send.onclick = () => {
if (this.$send.classList.contains('loading')) {
this._confirm('stop', this.$send, 'pending', () =>
this.handlers.onStop(),
);
} else {
this.handlers.onComposeSubmit();
}
};
this.$fileInput.addEventListener('change', (event) => {
this.handlers.onAttachFiles(event.target.files);
this.$fileInput.value = '';
@@ -1115,9 +1180,27 @@ export class UI {
passive: true,
});
// Toolbar buttons.
this.$settings.onclick = () => this._openSettings();
this.$conversations.onclick = () => this.openConversations();
this.$reset.onclick = () =>
this._confirm('reset', this.$reset, 'footer__toolbar-btn--pending', () =>
this.handlers.onReset(),
);
this.$scrollUp.onclick = () => {
this.setAutoScroll(false);
this.$chat.scrollTop = 0;
};
this.$scrollDown.onclick = () => {
// Set scrollTop first, so the scroll listener sees the
// bottom position and does not turn following off again.
this.$chat.scrollTop = this.$chat.scrollHeight;
this.setAutoScroll(true);
};
this.$mute.onclick = () => this.handlers.onToggleMute();
// Settings modal.
this.$settings.addEventListener('click', () => this._openSettings());
this.$settingsClose.addEventListener('click', () => this._closeSettings());
this.$settingsClose.onclick = () => this._closeSettings();
this.$settingsOverlay.addEventListener('click', (event) => {
if (event.target === this.$settingsOverlay) this._closeSettings();
});
@@ -1131,11 +1214,9 @@ export class UI {
}
});
this.$settingsTabs.forEach(($tab) => {
$tab.addEventListener('click', () => this._switchTab($tab.dataset.tab));
$tab.onclick = () => this._switchTab($tab.dataset.tab);
});
this.$saveSystemMessage.addEventListener('click', () =>
this._saveSystemMessage(),
);
this.$saveSystemMessage.onclick = () => this._saveSystemMessage();
this.$model.addEventListener('change', () => {
const { provider, model } = this.getSelectedModel();
this.handlers.onModelChange({ provider, model });
@@ -1144,12 +1225,8 @@ export class UI {
this.handlers.onVoiceChange(this.$voice.value),
);
// Conversations modal. The toolbar button opens it through the
// data-action route; these listeners close it and report the
// search text and the selected row.
this.$conversationsClose.addEventListener('click', () =>
this.closeConversations(),
);
// Conversations modal.
this.$conversationsClose.onclick = () => this.closeConversations();
this.$conversationsOverlay.addEventListener('click', (event) => {
if (event.target === this.$conversationsOverlay) {
this.closeConversations();
@@ -1167,39 +1244,11 @@ export class UI {
this.$conversationsSearch.addEventListener('input', () =>
this.handlers.onConversationSearch(this.$conversationsSearch.value),
);
this.$conversationNew.addEventListener('click', () =>
this.handlers.onNewConversation(),
);
this.$conversationsList.addEventListener('click', (event) => {
const $item = event.target.closest('[data-conversation-id]');
if (!$item) return;
const id = $item.dataset.conversationId;
const $rename = event.target.closest('.conversations__item-rename');
if ($rename) {
this._clearConfirm();
this.handlers.onRenameConversation(id);
return;
}
const $delete = event.target.closest('.conversations__item-delete');
if ($delete) {
// Deleting is irreversible: the second press on the same
// button deletes, any other click cancels the request.
this._confirm(`conversation:${id}`, $delete, 'pending', () =>
this.handlers.onDeleteConversation(id),
);
return;
}
this._clearConfirm();
this.handlers.onSelectConversation(id);
});
this.$conversationNew.onclick = () => this.handlers.onNewConversation();
// Compose settings modal.
this.$composeSettings.addEventListener('click', () =>
this.handlers.onComposeSettings(),
);
this.$composeSettingsClose.addEventListener('click', () =>
this._closeComposeSettings(),
);
this.$composeSettings.onclick = () => this.handlers.onComposeSettings();
this.$composeSettingsClose.onclick = () => this._closeComposeSettings();
this.$composeSettingsOverlay.addEventListener('click', (event) => {
if (event.target === this.$composeSettingsOverlay) {
this._closeComposeSettings();
@@ -1207,143 +1256,6 @@ export class UI {
});
}
_handleClick(event) {
const $target = event.target.closest('[data-action]');
const action = $target?.dataset.action;
// A click outside any action button cancels every pending
// confirmation.
if (!action) {
this._clearConfirm();
return;
}
const $message = $target.closest('.message');
const messageId = $message?.dataset.messageId || $message?.dataset.id;
switch (action) {
case 'send':
if (this.$send.classList.contains('loading')) {
this._confirm('stop', $target, 'pending', () =>
this.handlers.onStop(),
);
} else {
this._clearConfirm();
this.handlers.onComposeSubmit();
}
break;
case 'reset':
this._confirm('reset', $target, 'footer__toolbar-btn--pending', () =>
this.handlers.onReset(),
);
break;
case 'delete':
// Error bubbles are display-only: remove without a request.
if (
!$message ||
(!$message.dataset.id && !$message.dataset.messageId)
) {
this._clearConfirm();
$message?.remove();
break;
}
this._confirm(
`delete:${messageId}:${$message.dataset.field ?? ''}` +
`:${$message.dataset.toolCallId ?? ''}`,
$target,
'pending',
() =>
this.handlers.onDelete({
id: messageId,
field: $message.dataset.field || null,
toolCallId: $message.dataset.toolCallId || null,
}),
);
break;
case 'approve':
case 'deny': {
this._clearConfirm();
const execution = this.pendingApprovals.find(
({ messageId: mid, toolCallId }) =>
mid === $message?.dataset.parentId &&
toolCallId === $message?.dataset.toolCallId,
);
if (!execution) break;
if (action === 'approve') {
this.handlers.onApprove(execution);
} else {
this.handlers.onDeny(execution);
}
break;
}
case 'edit':
this._clearConfirm();
this.handlers.onEdit({
id: messageId,
field: $message?.dataset.field || null,
toolCallId: $message?.dataset.toolCallId || null,
});
break;
case 'play':
this._clearConfirm();
if (messageId) this.handlers.onPlay(messageId);
break;
case 'copy': {
this._clearConfirm();
const text = $message?.dataset.copyText || '';
this._copyToClipboard($target, text);
break;
}
case 'inspect': {
this._clearConfirm();
const isOpen = $message.classList.toggle('message--debug-open');
$target.setAttribute('aria-expanded', String(isOpen));
break;
}
case 'scroll-up':
this._clearConfirm();
this.setAutoScroll(false);
this.$chat.scrollTop = 0;
break;
case 'scroll-down':
// Set scrollTop first, so the scroll listener sees the
// bottom position and does not turn following off again.
this._clearConfirm();
this.$chat.scrollTop = this.$chat.scrollHeight;
this.setAutoScroll(true);
break;
case 'mute':
this._clearConfirm();
this.handlers.onToggleMute();
break;
case 'location':
this._clearConfirm();
this.handlers.onLocationToggle();
break;
case 'conversations':
this._clearConfirm();
this.openConversations();
break;
case 'settings':
this._clearConfirm();
this._openSettings();
break;
case 'compose-settings':
this._clearConfirm();
this.handlers.onComposeSettings();
break;
case 'attach':
// Handled by the listener on the button itself.
break;
case 'ptt':
case 'transcribe':
// Handled by press-and-hold.
break;
default:
this._clearConfirm();
}
}
/**
* _confirm runs action on the second press of the same control.
* The first press marks the control; any other click clears the
@@ -1507,15 +1419,13 @@ export class UI {
if (event.key === 'Escape') close(null);
};
$modal
.querySelector('.modal__close')
.addEventListener('click', () => close(null));
$modal.querySelector('.modal__close').onclick = () => close(null);
$overlay.addEventListener('click', (event) => {
if (event.target === $overlay) close(null);
});
this.document.addEventListener('keydown', onKey);
$save.addEventListener('click', () => {
$save.onclick = () => {
let value;
try {
value = readValue($modal);
@@ -1538,7 +1448,7 @@ export class UI {
return;
}
close(value);
});
};
$modal.querySelector('textarea, input')?.focus();
});
@@ -1548,6 +1458,86 @@ export class UI {
// PRIVATE: RENDERING
// ====================
/**
* _bindMessageActions attaches click handlers to the action
* buttons present in a cloned message bubble.
*
* Copy reads $bubble.dataset.copyText when clicked so tool bubbles
* copy their updated output after _fillToolOutput runs. Approve
* and deny look up the current execution record in
* this.pendingApprovals when clicked.
*
* @param {HTMLElement} $bubble
* @param {{id?: string | null, field?: string | null, toolCallId?: string | null, isError?: boolean}} [options]
*/
_bindMessageActions(
$bubble,
{ id = null, field = null, toolCallId = null, isError = false } = {},
) {
const $play = $bubble.querySelector('.message__action-btn--play');
if ($play) {
$play.onclick = () => {
if (id) this.handlers.onPlay(id);
};
}
const $delete = $bubble.querySelector('.message__action-btn--delete');
if ($delete) {
$delete.onclick = () => {
// Error bubbles are display-only: remove without a request.
if (isError || !id) {
$bubble.remove();
return;
}
this._confirm(
`delete:${id}:${field ?? ''}:${toolCallId ?? ''}`,
$delete,
'pending',
() => this.handlers.onDelete({ id, field, toolCallId }),
);
};
}
const $inspect = $bubble.querySelector('.message__action-btn--inspect');
if ($inspect) {
$inspect.onclick = () => {
const isOpen = $bubble.classList.toggle('message--debug-open');
$inspect.setAttribute('aria-expanded', String(isOpen));
};
}
const $edit = $bubble.querySelector('.message__action-btn--edit');
if ($edit) {
$edit.onclick = () => this.handlers.onEdit({ id, field, toolCallId });
}
const $copy = $bubble.querySelector('.message__action-btn--copy');
if ($copy) {
$copy.onclick = () =>
this._copyToClipboard($copy, $bubble.dataset.copyText || '');
}
const findExecution = () =>
this.pendingApprovals.find(
(execution) =>
execution.messageId === id && execution.toolCallId === toolCallId,
);
const $approve = $bubble.querySelector('.message__approval-btn--approve');
if ($approve) {
$approve.onclick = () => {
const execution = findExecution();
if (execution) this.handlers.onApprove(execution);
};
}
const $deny = $bubble.querySelector('.message__approval-btn--deny');
if ($deny) {
$deny.onclick = () => {
const execution = findExecution();
if (execution) this.handlers.onDeny(execution);
};
}
}
_renderUserMessage(row) {
const $bubble =
this.$tplUserMessage.content.cloneNode(true).firstElementChild;
@@ -1557,6 +1547,7 @@ export class UI {
$bubble.dataset.copyText = text;
this._renderImagePills($bubble, row);
this._populateDebug($bubble.querySelector('.message__debug-list'), row);
this._bindMessageActions($bubble, { id: row.id });
this.$chat.insertBefore($bubble, this.$chatLoading);
this.scrollToBottom();
}
@@ -1588,6 +1579,7 @@ export class UI {
$bubble.dataset.voice = row.voice || '';
$bubble.dataset.copyText = text;
this._populateDebug($bubble.querySelector('.message__debug-list'), row);
this._bindMessageActions($bubble, { id: row.id });
this.$chat.insertBefore($bubble, this.$chatLoading);
this.scrollToBottom();
}
@@ -1603,6 +1595,7 @@ export class UI {
$bubble.querySelector('.message__content').textContent = reasoning;
$bubble.dataset.copyText = reasoning;
this._populateDebug($bubble.querySelector('.message__debug-list'), row);
this._bindMessageActions($bubble, { id: row.id, field: 'reasoning' });
this.$chat.insertBefore($bubble, this.$chatLoading);
this.scrollToBottom();
}
@@ -1651,6 +1644,11 @@ export class UI {
...result,
voice: parentRow?.voice,
});
this._bindMessageActions($bubble, {
id: parentRow?.id ?? null,
field: parentRow ? 'tool' : null,
toolCallId: callId || null,
});
this.$chat.insertBefore($bubble, this.$chatLoading);
this._applyApproval($bubble);
this.scrollToBottom();
@@ -1689,6 +1687,7 @@ export class UI {
const text = extractText(row.content?.content);
$bubble.querySelector('.message__content').textContent = text;
$bubble.dataset.copyText = text;
this._bindMessageActions($bubble, { id: row.id });
this.$chat.insertBefore($bubble, this.$chatLoading);
}
@@ -14,7 +14,6 @@
type="button"
class="compose__action-btn"
id="compose-settings"
data-action="compose-settings"
aria-label="Compose settings"
>
<svg class="icon"><use href="/static/icons.svg#settings-2"></use></svg>
@@ -23,7 +22,6 @@
type="button"
class="compose__action-btn"
id="attach"
data-action="attach"
aria-label="Attach files"
>
<svg class="icon"><use href="/static/icons.svg#attach"></use></svg>
@@ -32,7 +30,6 @@
type="button"
class="compose__action-btn"
id="location"
data-action="location"
aria-label="Include location"
title="Include GPS location"
>
@@ -64,7 +61,6 @@
type="button"
class="compose__action-btn compose__action-btn--send"
id="send"
data-action="send"
aria-label="Send message"
>
<svg class="icon compose__icon--send"><use href="/static/icons.svg#send"></use></svg>
@@ -4,7 +4,6 @@
type="button"
class="footer__toolbar-btn"
id="settings"
data-action="settings"
aria-label="Settings"
>
<svg class="icon"><use href="/static/icons.svg#settings"></use></svg>
@@ -13,7 +12,6 @@
type="button"
class="footer__toolbar-btn"
id="conversations"
data-action="conversations"
aria-label="Conversations"
>
<svg class="icon"><use href="/static/icons.svg#menu"></use></svg>
@@ -24,7 +22,6 @@
type="button"
class="footer__toolbar-btn"
id="reset"
data-action="reset"
aria-label="Reset conversation"
>
<svg class="icon"><use href="/static/icons.svg#trash"></use></svg>
@@ -33,7 +30,6 @@
type="button"
class="footer__toolbar-btn"
id="scroll-up"
data-action="scroll-up"
aria-label="Scroll to top"
>
<svg class="icon"><use href="/static/icons.svg#scroll-up"></use></svg>
@@ -42,7 +38,6 @@
type="button"
class="footer__toolbar-btn"
id="scroll-down"
data-action="scroll-down"
aria-label="Scroll to bottom and follow"
>
<svg class="icon"><use href="/static/icons.svg#scroll-down"></use></svg>
@@ -51,7 +46,6 @@
type="button"
class="footer__toolbar-btn"
id="mute"
data-action="mute"
aria-label="Mute"
>
<svg class="icon"><use href="/static/icons.svg#volume"></use></svg>
@@ -9,15 +9,13 @@
<button
type="button"
class="message__action-btn message__action-btn--delete"
data-action="delete"
aria-label="Delete message"
>
<svg class="icon"><use href="/static/icons.svg#trash"></use></svg>
</button>
<button
type="button"
class="message__action-btn"
data-action="inspect"
class="message__action-btn message__action-btn--inspect"
aria-label="Show details"
aria-expanded="false"
>
@@ -25,16 +23,14 @@
</button>
<button
type="button"
class="message__action-btn"
data-action="edit"
class="message__action-btn message__action-btn--edit"
aria-label="Edit message"
>
<svg class="icon"><use href="/static/icons.svg#edit"></use></svg>
</button>
<button
type="button"
class="message__action-btn"
data-action="copy"
class="message__action-btn message__action-btn--copy"
aria-label="Copy to clipboard"
>
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
@@ -64,7 +60,6 @@
<button
type="button"
class="message__action-btn message__action-btn--play"
data-action="play"
aria-label="Play"
>
<svg class="icon"><use href="/static/icons.svg#volume"></use></svg>
@@ -72,15 +67,13 @@
<button
type="button"
class="message__action-btn message__action-btn--delete"
data-action="delete"
aria-label="Delete message"
>
<svg class="icon"><use href="/static/icons.svg#trash"></use></svg>
</button>
<button
type="button"
class="message__action-btn"
data-action="inspect"
class="message__action-btn message__action-btn--inspect"
aria-label="Show details"
aria-expanded="false"
>
@@ -88,16 +81,14 @@
</button>
<button
type="button"
class="message__action-btn"
data-action="edit"
class="message__action-btn message__action-btn--edit"
aria-label="Edit message"
>
<svg class="icon"><use href="/static/icons.svg#edit"></use></svg>
</button>
<button
type="button"
class="message__action-btn"
data-action="copy"
class="message__action-btn message__action-btn--copy"
aria-label="Copy to clipboard"
>
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
@@ -120,8 +111,7 @@
<div class="message__actions-buttons">
<button
type="button"
class="message__action-btn"
data-action="copy"
class="message__action-btn message__action-btn--copy"
aria-label="Copy to clipboard"
>
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
@@ -129,7 +119,6 @@
<button
type="button"
class="message__action-btn message__action-btn--delete"
data-action="delete"
aria-label="Delete message"
>
<svg class="icon"><use href="/static/icons.svg#trash"></use></svg>
@@ -179,15 +168,13 @@
<button
type="button"
class="message__action-btn message__action-btn--delete"
data-action="delete"
aria-label="Delete message"
>
<svg class="icon"><use href="/static/icons.svg#trash"></use></svg>
</button>
<button
type="button"
class="message__action-btn"
data-action="inspect"
class="message__action-btn message__action-btn--inspect"
aria-label="Show details"
aria-expanded="false"
>
@@ -195,16 +182,14 @@
</button>
<button
type="button"
class="message__action-btn"
data-action="edit"
class="message__action-btn message__action-btn--edit"
aria-label="Edit message"
>
<svg class="icon"><use href="/static/icons.svg#edit"></use></svg>
</button>
<button
type="button"
class="message__action-btn"
data-action="copy"
class="message__action-btn message__action-btn--copy"
aria-label="Copy to clipboard"
>
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
@@ -241,13 +226,11 @@
<button
type="button"
class="message__approval-btn message__approval-btn--approve"
data-action="approve"
aria-label="Approve tool call"
>Approve</button>
<button
type="button"
class="message__approval-btn message__approval-btn--deny"
data-action="deny"
aria-label="Deny tool call"
>Deny</button>
</div>
@@ -257,15 +240,13 @@
<button
type="button"
class="message__action-btn message__action-btn--delete"
data-action="delete"
aria-label="Delete message"
>
<svg class="icon"><use href="/static/icons.svg#trash"></use></svg>
</button>
<button
type="button"
class="message__action-btn"
data-action="inspect"
class="message__action-btn message__action-btn--inspect"
aria-label="Show details"
aria-expanded="false"
>
@@ -273,16 +254,14 @@
</button>
<button
type="button"
class="message__action-btn"
data-action="edit"
class="message__action-btn message__action-btn--edit"
aria-label="Edit message"
>
<svg class="icon"><use href="/static/icons.svg#edit"></use></svg>
</button>
<button
type="button"
class="message__action-btn"
data-action="copy"
class="message__action-btn message__action-btn--copy"
aria-label="Copy to clipboard"
>
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
@@ -310,16 +289,14 @@
<div class="message__actions-buttons">
<button
type="button"
class="message__action-btn"
data-action="edit"
class="message__action-btn message__action-btn--edit"
aria-label="Edit system message"
>
<svg class="icon"><use href="/static/icons.svg#edit"></use></svg>
</button>
<button
type="button"
class="message__action-btn"
data-action="copy"
class="message__action-btn message__action-btn--copy"
aria-label="Copy to clipboard"
>
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>