From 8fd3c8843081191fbf246c7f80c264c2eebc1644 Mon Sep 17 00:00:00 2001
From: Laura Sach <5183697+lawsie@users.noreply.github.com>
Date: Tue, 11 Aug 2026 10:19:06 +0100
Subject: [PATCH 1/4] Switch enable/disable icon
---
ui/contextmenu.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/ui/contextmenu.js b/ui/contextmenu.js
index d6b4353e..7ad82039 100644
--- a/ui/contextmenu.js
+++ b/ui/contextmenu.js
@@ -739,12 +739,12 @@ export function initContextMenus(workspace) {
const enableBtn = document.createElement('button');
enableBtn.type = 'button';
enableBtn.className = 'fc-block-toolbar-btn';
- // fa-ban — shown when the block is currently disabled.
+ // fa-ban — shown when the block is currently enabled (click to disable).
const blockDisableSvg = mkFaSvg(
'',
'0 0 512 512'
);
- // fa-circle-check — shown when the block is currently enabled.
+ // fa-circle-check — shown when the block is currently disabled (click to enable).
const blockEnableSvg = mkFaSvg(
'',
'0 0 512 512'
@@ -1058,7 +1058,7 @@ export function initContextMenus(workspace) {
'aria-label',
disabled ? getToolbarLabel('context_enable_option', 'Enable') : getToolbarLabel('context_disable_option', 'Disable')
);
- enableBtn.innerHTML = disabled ? blockDisableSvg : blockEnableSvg;
+ enableBtn.innerHTML = disabled ? blockEnableSvg : blockDisableSvg;
}
function showBlockToolbar(block, { keyboard = false } = {}) {
From a9385ba92c1d7eff592009668d440ce9c57a9b10 Mon Sep 17 00:00:00 2001
From: Laura Sach <5183697+lawsie@users.noreply.github.com>
Date: Tue, 11 Aug 2026 10:23:10 +0100
Subject: [PATCH 2/4] Add duplicate / comment to detached block
---
ui/contextmenu.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/ui/contextmenu.js b/ui/contextmenu.js
index 7ad82039..517cbe3c 100644
--- a/ui/contextmenu.js
+++ b/ui/contextmenu.js
@@ -1021,8 +1021,9 @@ export function initContextMenus(workspace) {
const block = toolbarBlock;
if (!block) return;
const simplified = isLooseAndMovable(block);
- duplicateBtn.style.display = simplified ? 'none' : '';
- commentBtn.style.display = isBlockLocked(block) || simplified ? 'none' : '';
+ duplicateBtn.style.display = '';
+ commentBtn.style.display = isBlockLocked(block) ? 'none' : '';
+ enableBtn.style.display = isBlockLocked(block) || simplified ? 'none' : '';
moveHint.style.display = toolbarKeyboardMode && simplified ? '' : 'none';
// Loose blocks never show View, regardless of mesh state — no point
// waiting on a mesh check for a block that isn't placed anywhere yet.
@@ -1070,7 +1071,6 @@ export function initContextMenus(workspace) {
const locked = isBlockLocked(block);
detachBtn.style.display = !locked && isDetachable(block) ? '' : 'none';
deleteBtn.style.display = locked ? 'none' : '';
- enableBtn.style.display = locked ? 'none' : '';
updateSimplifiedToolbar();
updateCommentButton(block);
updateEnableButton(block);
From 341e79222471eebd07939655e4f267b7b2ca042b Mon Sep 17 00:00:00 2001
From: Laura Sach <5183697+lawsie@users.noreply.github.com>
Date: Tue, 11 Aug 2026 10:42:20 +0100
Subject: [PATCH 3/4] Add hover text to buttons
---
locale/de.js | 8 ++++++++
locale/en.js | 8 ++++++++
locale/es.js | 8 ++++++++
locale/fr.js | 8 ++++++++
locale/it.js | 8 ++++++++
locale/pl.js | 8 ++++++++
locale/pt.js | 8 ++++++++
locale/sv.js | 8 ++++++++
ui/contextmenu.js | 38 ++++++++++++++++++++++++++------------
9 files changed, 90 insertions(+), 12 deletions(-)
diff --git a/locale/de.js b/locale/de.js
index 8f56395d..2ac11d65 100644
--- a/locale/de.js
+++ b/locale/de.js
@@ -1082,6 +1082,8 @@ export default {
select_button_ui: 'Objekt auswählen',
duplicate_button_ui: 'Duplizieren',
delete_button_ui: 'Löschen',
+ delete_block_button_ui: 'Block löschen',
+ duplicate_block_button_ui: 'Block duplizieren',
camera_button_ui: 'Kamera-Steuerung',
eye_button_ui: 'Um ausgewähltes Objekt kreisen',
info_panel_link_ui: 'Flock XR Webseite (neuer Tab)',
@@ -1109,6 +1111,12 @@ export default {
context_expand_all_option: 'Alle ausklappen',
context_disable_option: 'Deaktivieren',
context_enable_option: 'Aktivieren',
+ add_comment: 'Kommentar hinzufügen',
+ delete_comment: 'Kommentar löschen',
+ view_in_canvas: 'Im Canvas anzeigen',
+ exit_canvas_view: 'Umkreisen stoppen',
+ detach_block_option: 'Trennen',
+ view_in_canvas_option: 'Im Canvas anzeigen',
context_copy_option: 'Kopieren',
context_paste_option: 'Einfügen',
context_cut_option: 'Ausschneiden',
diff --git a/locale/en.js b/locale/en.js
index 63480c98..ecfa03c3 100644
--- a/locale/en.js
+++ b/locale/en.js
@@ -1140,6 +1140,8 @@ export default {
select_button_ui: 'Select object',
duplicate_button_ui: 'Duplicate selected object',
delete_button_ui: 'Delete selected object',
+ delete_block_button_ui: 'Delete block',
+ duplicate_block_button_ui: 'Duplicate block',
camera_button_ui: 'Camera controls',
eye_button_ui: 'Orbit around selected object',
@@ -1185,6 +1187,12 @@ export default {
context_expand_all_option: 'Expand all',
context_disable_option: 'Disable',
context_enable_option: 'Enable',
+ add_comment: 'Add comment',
+ delete_comment: 'Delete comment',
+ view_in_canvas: 'View in canvas',
+ exit_canvas_view: 'Stop orbiting object',
+ detach_block_option: 'Detach',
+ view_in_canvas_option: 'View in canvas',
context_copy_option: 'Copy',
context_paste_option: 'Paste',
context_cut_option: 'Cut',
diff --git a/locale/es.js b/locale/es.js
index aa9a06de..46827731 100644
--- a/locale/es.js
+++ b/locale/es.js
@@ -1132,6 +1132,8 @@ export default {
select_button_ui: 'Seleccionar objeto', // human
duplicate_button_ui: 'Duplicar objeto seleccionado', // human
delete_button_ui: 'Eliminar objeto seleccionado', // human
+ delete_block_button_ui: 'Eliminar bloque',
+ duplicate_block_button_ui: 'Duplicar bloque',
camera_button_ui: 'Controles de cámara', // human
eye_button_ui: 'Orbitar alrededor del objeto seleccionado',
@@ -1178,6 +1180,12 @@ export default {
context_expand_all_option: 'Expandir todo',
context_disable_option: 'Deshabilitar',
context_enable_option: 'Habilitar',
+ add_comment: 'Añadir comentario',
+ delete_comment: 'Eliminar comentario',
+ view_in_canvas: 'Ver en el lienzo',
+ exit_canvas_view: 'Dejar de orbitar el objeto',
+ detach_block_option: 'Desconectar',
+ view_in_canvas_option: 'Ver en el lienzo',
context_copy_option: 'Copiar', // human
context_paste_option: 'Pegar', // human
context_cut_option: 'Cortar', // human
diff --git a/locale/fr.js b/locale/fr.js
index 75f7ade4..3a5f3172 100644
--- a/locale/fr.js
+++ b/locale/fr.js
@@ -1075,6 +1075,8 @@ export default {
select_button_ui: "Sélectionner l'objet",
duplicate_button_ui: "Dupliquer l'objet sélectionné",
delete_button_ui: "Supprimer l'objet sélectionné",
+ delete_block_button_ui: 'Supprimer le bloc',
+ duplicate_block_button_ui: 'Dupliquer le bloc',
camera_button_ui: 'Contrôles de la caméra',
eye_button_ui: "Orbiter autour de l'objet sélectionné",
@@ -1121,6 +1123,12 @@ export default {
context_expand_all_option: 'Tout développer',
context_disable_option: 'Désactiver',
context_enable_option: 'Activer',
+ add_comment: 'Ajouter un commentaire',
+ delete_comment: 'Supprimer le commentaire',
+ view_in_canvas: 'Afficher dans le canvas',
+ exit_canvas_view: "Arrêter l'orbite",
+ detach_block_option: 'Détacher',
+ view_in_canvas_option: 'Afficher dans le canvas',
context_copy_option: 'Copier',
context_paste_option: 'Coller',
context_cut_option: 'Couper',
diff --git a/locale/it.js b/locale/it.js
index e10c2e54..af38d9e6 100644
--- a/locale/it.js
+++ b/locale/it.js
@@ -1084,6 +1084,8 @@ export default {
select_button_ui: 'Seleziona oggetto',
duplicate_button_ui: 'Duplica oggetto selezionato',
delete_button_ui: 'Elimina oggetto selezionato',
+ delete_block_button_ui: 'Elimina blocco',
+ duplicate_block_button_ui: 'Duplica blocco',
camera_button_ui: 'Controlli camera',
eye_button_ui: "Orbita attorno all'oggetto selezionato",
@@ -1130,6 +1132,12 @@ export default {
context_expand_all_option: 'Espandi tutto',
context_disable_option: 'Disabilita',
context_enable_option: 'Abilita',
+ add_comment: 'Aggiungi commento',
+ delete_comment: 'Elimina commento',
+ view_in_canvas: 'Visualizza nel canvas',
+ exit_canvas_view: "Interrompi l'orbita",
+ detach_block_option: 'Stacca',
+ view_in_canvas_option: 'Visualizza nel canvas',
context_copy_option: 'Copia',
context_paste_option: 'Incolla',
context_cut_option: 'Taglia',
diff --git a/locale/pl.js b/locale/pl.js
index 6db2d64c..8b72a050 100644
--- a/locale/pl.js
+++ b/locale/pl.js
@@ -1073,6 +1073,8 @@ export default {
select_button_ui: 'Wybierz obiekt',
duplicate_button_ui: 'Duplikuj wybrany obiekt',
delete_button_ui: 'Usuń wybrany obiekt',
+ delete_block_button_ui: 'Usuń blok',
+ duplicate_block_button_ui: 'Duplikuj blok',
camera_button_ui: 'Kontrola kamery',
eye_button_ui: 'Obracaj wokół wybranego obiektu',
@@ -1118,6 +1120,12 @@ export default {
context_expand_all_option: 'Rozwiń wszystko',
context_disable_option: 'Wyłącz',
context_enable_option: 'Włącz',
+ add_comment: 'Dodaj komentarz',
+ delete_comment: 'Usuń komentarz',
+ view_in_canvas: 'Wyświetl na płótnie',
+ exit_canvas_view: 'Zatrzymaj krążenie wokół obiektu',
+ detach_block_option: 'Odłącz',
+ view_in_canvas_option: 'Wyświetl na płótnie',
context_copy_option: 'Kopiuj',
context_paste_option: 'Wklej',
context_cut_option: 'Wytnij',
diff --git a/locale/pt.js b/locale/pt.js
index 229979cf..0feff7cc 100644
--- a/locale/pt.js
+++ b/locale/pt.js
@@ -1083,6 +1083,8 @@ export default {
select_button_ui: 'Selecionar objeto',
duplicate_button_ui: 'Duplicar o objeto selecionado',
delete_button_ui: 'Eliminar o objeto selecionado',
+ delete_block_button_ui: 'Excluir bloco',
+ duplicate_block_button_ui: 'Duplicar bloco',
camera_button_ui: 'Controlos da câmara',
eye_button_ui: 'Orbitar ao redor do objeto selecionado',
@@ -1129,6 +1131,12 @@ export default {
context_expand_all_option: 'Expandir tudo',
context_disable_option: 'Desativar',
context_enable_option: 'Ativar',
+ add_comment: 'Adicionar comentário',
+ delete_comment: 'Excluir comentário',
+ view_in_canvas: 'Ver na tela',
+ exit_canvas_view: 'Parar de orbitar o objeto',
+ detach_block_option: 'Desconectar',
+ view_in_canvas_option: 'Ver na tela',
context_copy_option: 'Copiar',
context_paste_option: 'Colar',
context_cut_option: 'Cortar',
diff --git a/locale/sv.js b/locale/sv.js
index 45a43551..ad06b928 100644
--- a/locale/sv.js
+++ b/locale/sv.js
@@ -1061,6 +1061,8 @@ export default {
select_button_ui: 'Välj objekt',
duplicate_button_ui: 'Duplicera valt objekt',
delete_button_ui: 'Radera valt objekt',
+ delete_block_button_ui: 'Radera block',
+ duplicate_block_button_ui: 'Duplicera block',
camera_button_ui: 'Kamerakontroller',
eye_button_ui: 'Kretsa runt valt objekt',
@@ -1106,6 +1108,12 @@ export default {
context_expand_all_option: 'Expandera alla',
context_disable_option: 'Inaktivera',
context_enable_option: 'Aktivera',
+ add_comment: 'Lägg till kommentar',
+ delete_comment: 'Ta bort kommentar',
+ view_in_canvas: 'Visa i canvas',
+ exit_canvas_view: 'Sluta kretsa runt objektet',
+ detach_block_option: 'Koppla loss',
+ view_in_canvas_option: 'Visa i canvas',
context_copy_option: 'Kopiera',
context_paste_option: 'Klistra in',
context_cut_option: 'Klipp ut',
diff --git a/ui/contextmenu.js b/ui/contextmenu.js
index 517cbe3c..5650bb66 100644
--- a/ui/contextmenu.js
+++ b/ui/contextmenu.js
@@ -680,10 +680,16 @@ export function initContextMenus(workspace) {
return result === key ? fallback : result;
};
+ // Helper: set both the a11y label and the native hover tooltip together,
+ // so toolbar buttons always show hover text like the rest of the app.
+ const setToolbarLabel = (el, label) => {
+ el.setAttribute('aria-label', label);
+ el.title = label;
+ };
+
const duplicateBtn = document.createElement('button');
duplicateBtn.type = 'button';
duplicateBtn.className = 'fc-block-toolbar-btn';
- duplicateBtn.setAttribute('aria-label', getToolbarLabel('duplicate_button_ui', 'Duplicate'));
// fa-copy
duplicateBtn.innerHTML = mkFaSvg(
''
@@ -692,7 +698,6 @@ export function initContextMenus(workspace) {
const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.className = 'fc-block-toolbar-btn fc-block-toolbar-btn--delete';
- deleteBtn.setAttribute('aria-label', getToolbarLabel('delete_button_ui', 'Delete'));
// fa-trash
deleteBtn.innerHTML = mkFaSvg(
''
@@ -701,13 +706,21 @@ export function initContextMenus(workspace) {
const detachBtn = document.createElement('button');
detachBtn.type = 'button';
detachBtn.className = 'fc-block-toolbar-btn';
- detachBtn.setAttribute('aria-label', getToolbarLabel('shortcut_detach_block', 'Detach'));
// fa-link-slash
detachBtn.innerHTML = mkFaSvg(
'',
'0 0 640 512'
);
+ // duplicate/delete/detach labels don't change per-block, so nothing else
+ // re-applies them after creation — without this, switching language after
+ // the toolbar first renders would leave them stuck in the old language.
+ function refreshStaticToolbarLabels() {
+ setToolbarLabel(duplicateBtn, getToolbarLabel('duplicate_block_button_ui', 'Duplicate block'));
+ setToolbarLabel(deleteBtn, getToolbarLabel('delete_block_button_ui', 'Delete block'));
+ setToolbarLabel(detachBtn, getToolbarLabel('shortcut_detach_block', 'Detach'));
+ }
+
// Passive "press M to move" hint. Looks like a toolbar button but is inert
// (pointer-events: none): it exists purely so keyboard users see the move
// icon with an M badge when they land on a loose block.
@@ -723,7 +736,7 @@ export function initContextMenus(workspace) {
const commentBtn = document.createElement('button');
commentBtn.type = 'button';
commentBtn.className = 'fc-block-toolbar-btn';
- commentBtn.setAttribute('aria-label', getToolbarLabel('add_comment', 'Add comment'));
+ setToolbarLabel(commentBtn, getToolbarLabel('add_comment', 'Add comment'));
// fa-comment (pre-6.7 path revision; same rendered glyph as current fa-comment)
const commentAddSvg = mkFaSvg(
'',
@@ -764,7 +777,7 @@ export function initContextMenus(workspace) {
const viewBtn = document.createElement('button');
viewBtn.type = 'button';
viewBtn.className = 'fc-block-toolbar-btn';
- viewBtn.setAttribute('aria-label', getToolbarLabel('view_in_canvas', 'View in canvas'));
+ setToolbarLabel(viewBtn, getToolbarLabel('view_in_canvas', 'View in canvas'));
viewBtn.innerHTML = viewEnterSvg;
blockToolbar.append(duplicateBtn, detachBtn, moveHint, commentBtn, enableBtn, viewBtn, deleteBtn);
@@ -1044,8 +1057,8 @@ export function initContextMenus(workspace) {
// crossed-out "delete" icon when it does, plain "add" icon when it doesn't.
function updateCommentButton(block) {
const hasComment = block.getCommentText() !== null;
- commentBtn.setAttribute(
- 'aria-label',
+ setToolbarLabel(
+ commentBtn,
hasComment
? getToolbarLabel('delete_comment', 'Delete comment')
: getToolbarLabel('add_comment', 'Add comment')
@@ -1055,8 +1068,8 @@ export function initContextMenus(workspace) {
function updateEnableButton(block) {
const disabled = block.hasDisabledReason('MANUALLY_DISABLED');
- enableBtn.setAttribute(
- 'aria-label',
+ setToolbarLabel(
+ enableBtn,
disabled ? getToolbarLabel('context_enable_option', 'Enable') : getToolbarLabel('context_disable_option', 'Disable')
);
enableBtn.innerHTML = disabled ? blockEnableSvg : blockDisableSvg;
@@ -1071,6 +1084,7 @@ export function initContextMenus(workspace) {
const locked = isBlockLocked(block);
detachBtn.style.display = !locked && isDetachable(block) ? '' : 'none';
deleteBtn.style.display = locked ? 'none' : '';
+ refreshStaticToolbarLabels();
updateSimplifiedToolbar();
updateCommentButton(block);
updateEnableButton(block);
@@ -1086,10 +1100,10 @@ export function initContextMenus(workspace) {
!!window.orbitViewActive &&
(window.orbitBlock === block || (meshRoot && window.orbitMesh === meshRoot));
viewBtn.innerHTML = exitMode ? viewExitSvg : viewEnterSvg;
- viewBtn.setAttribute(
- 'aria-label',
+ setToolbarLabel(
+ viewBtn,
exitMode
- ? getToolbarLabel('exit_canvas_view', 'Exit canvas view')
+ ? getToolbarLabel('exit_canvas_view', 'Stop orbiting object')
: getToolbarLabel('view_in_canvas', 'View in canvas')
);
blockToolbar.classList.add('visible');
From 6586eb3895011c0fbcbde033b7a7c73989a04d0d Mon Sep 17 00:00:00 2001
From: Laura Sach <5183697+lawsie@users.noreply.github.com>
Date: Tue, 11 Aug 2026 10:56:56 +0100
Subject: [PATCH 4/4] Rabbit complaints
---
ui/contextmenu.js | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/ui/contextmenu.js b/ui/contextmenu.js
index 5650bb66..35624f21 100644
--- a/ui/contextmenu.js
+++ b/ui/contextmenu.js
@@ -1034,9 +1034,11 @@ export function initContextMenus(workspace) {
const block = toolbarBlock;
if (!block) return;
const simplified = isLooseAndMovable(block);
+ const locked = isBlockLocked(block);
duplicateBtn.style.display = '';
- commentBtn.style.display = isBlockLocked(block) ? 'none' : '';
- enableBtn.style.display = isBlockLocked(block) || simplified ? 'none' : '';
+ commentBtn.style.display = locked ? 'none' : '';
+ detachBtn.style.display = locked || !isDetachable(block) ? 'none' : '';
+ enableBtn.style.display = locked || simplified ? 'none' : '';
moveHint.style.display = toolbarKeyboardMode && simplified ? '' : 'none';
// Loose blocks never show View, regardless of mesh state — no point
// waiting on a mesh check for a block that isn't placed anywhere yet.
@@ -1082,7 +1084,6 @@ export function initContextMenus(workspace) {
// Locked blocks can't be edited: hide the mutating buttons (detach,
// comment, enable/disable, delete), leaving duplicate and view-in-canvas available.
const locked = isBlockLocked(block);
- detachBtn.style.display = !locked && isDetachable(block) ? '' : 'none';
deleteBtn.style.display = locked ? 'none' : '';
refreshStaticToolbarLabels();
updateSimplifiedToolbar();