From b788ce76a513182fd454e65d26d904cb14b2ac9a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 09:54:50 +0000 Subject: [PATCH 1/2] Add categories to toolbox search results When searching, matching categories and subcategories now appear at the top of the flyout as clickable buttons above the matching blocks. Clicking a category button navigates to that category in the toolbox sidebar and opens its flyout (including expanding the parent first for subcategories). - buildCategoryIndex() traverses the toolbox definition to collect all categories and subcategories with their display names and paths - navigateToCategory() resolves the raw-name path to live toolbox items, expanding parents and selecting the target via ensurePointerFocusedSelection_ - matchBlocks now also filters the category index and passes matches to showMatchingBlocks - showMatchingBlocks renders a "Categories" label + per-category buttons (with categorystyle-based CSS classes) before the block list - CSS added for the category header label and per-categorystyle button colours https://claude.ai/code/session_012QbU2xUemokbaybUrz5VG3 --- main/blocklyinit.js | 174 +++++++++++++++++++++++++++++++++++++++++++- style/blockly.css | 57 +++++++++++++++ 2 files changed, 227 insertions(+), 4 deletions(-) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index 612c065c..153e041c 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -1869,6 +1869,119 @@ export function overrideSearchPlugin(workspace) { return indexedBlocks; } + function buildCategoryIndex() { + const categories = []; + let idCounter = 0; + + function resolveDisplayName(name) { + if (!name) return ""; + return ( + Blockly.utils.replaceMessageReferences(name) || + name + ); + } + + function collectCategories(schema, parentPath, parentDisplayName) { + if ( + !schema || + schema.kind?.toLowerCase() !== "category" + ) + return; + const rawName = schema.name; + if (!rawName) return; + + const displayName = resolveDisplayName(rawName); + const categoryPath = [...parentPath, rawName]; + const id = `cat${idCounter++}`; + + const searchText = parentDisplayName + ? `${displayName} ${parentDisplayName}` + : displayName; + + categories.push({ + kind: "category", + id, + name: rawName, + displayName, + parentDisplayName, + categoryPath, + icon: schema.icon, + categorystyle: schema.categorystyle, + text: searchText, + }); + + schema.contents?.forEach((item) => { + if (item.kind?.toLowerCase() === "category") { + collectCategories( + item, + categoryPath, + displayName, + ); + } + }); + } + + workspace.options.languageTree?.contents?.forEach((item) => { + if (item.kind?.toLowerCase() === "category") { + collectCategories(item, [], null); + } + }); + + return categories; + } + + function navigateToCategory(categoryPath) { + const toolbox = workspace.getToolbox?.(); + if (!toolbox) return; + + let currentItems = toolbox.getToolboxItems?.() || []; + let targetItem = null; + + for (let i = 0; i < categoryPath.length; i++) { + const rawName = categoryPath[i]; + const found = currentItems.find( + (item) => + item.getToolboxItemDef?.()?.name === + rawName || + item.toolboxItemDef_?.name === rawName, + ); + + if (!found) return; + + if (i < categoryPath.length - 1) { + if (typeof found.setExpanded === "function") { + found.setExpanded(true); + } + currentItems = + found.getChildToolboxItems?.() || []; + } else { + targetItem = found; + } + } + + if (!targetItem) return; + + if ( + typeof targetItem.ensurePointerFocusedSelection_ === + "function" + ) { + targetItem.ensurePointerFocusedSelection_(); + } else { + toolbox.setSelectedItem?.(targetItem); + if (typeof targetItem.setSelected === "function") { + targetItem.setSelected(true); + } + if (typeof targetItem.setExpanded === "function") { + targetItem.setExpanded(true); + } + const flyout = toolbox.getFlyout?.(); + if (flyout) { + const contents = targetItem.getContents?.(); + if (contents) flyout.show?.(contents); + } + } + } + const searchToolboxItem = workspace .getToolbox() ?.getToolboxItems?.() @@ -2012,7 +2125,14 @@ export function overrideSearchPlugin(workspace) { return false; }); - this.showMatchingBlocks(matches); + if (!Array.isArray(workspace.flockCategoryIndex)) { + workspace.flockCategoryIndex = buildCategoryIndex(); + } + const categoryMatches = workspace.flockCategoryIndex.filter( + (cat) => cat.text.toLowerCase().includes(query), + ); + + this.showMatchingBlocks(matches, categoryMatches); }; function createXmlFromJson( @@ -2093,7 +2213,10 @@ export function overrideSearchPlugin(workspace) { return blockXml; } - SearchCategory.prototype.showMatchingBlocks = function (matches) { + SearchCategory.prototype.showMatchingBlocks = function ( + matches, + categoryMatches = [], + ) { if (!isSearchCategorySelected(this)) { return; } @@ -2106,10 +2229,53 @@ export function overrideSearchPlugin(workspace) { flyout.hide(); flyout.show([]); - const xmlList = matches.map((match) => + const xmlItems = []; + + if (categoryMatches.length > 0) { + const label = document.createElement("label"); + label.setAttribute("text", "Categories"); + label.setAttribute( + "web-class", + "flockSearchCategoryHeader", + ); + xmlItems.push(label); + + categoryMatches.forEach((cat) => { + const key = `flockCatNav_${cat.id}`; + this.workspace_.registerButtonCallback( + key, + () => { + navigateToCategory( + cat.categoryPath, + ); + }, + ); + const btn = document.createElement("button"); + const btnText = cat.parentDisplayName + ? `${cat.parentDisplayName} \u203a ${cat.displayName}` + : cat.displayName; + btn.setAttribute("text", btnText); + btn.setAttribute("callbackKey", key); + btn.setAttribute( + "web-class", + `flockSearchCategoryButton${cat.categorystyle ? ` flock-cat-${cat.categorystyle}` : ""}`, + ); + xmlItems.push(btn); + }); + + if (matches.length > 0) { + const sep = document.createElement("sep"); + sep.setAttribute("gap", "24"); + xmlItems.push(sep); + } + } + + const blockXmlList = matches.map((match) => createXmlFromJson(match.full), ); - flyout.show(xmlList); + xmlItems.push(...blockXmlList); + + flyout.show(xmlItems); }; const toolboxDef = workspace.options.languageTree; diff --git a/style/blockly.css b/style/blockly.css index 26cf1651..58841e6a 100644 --- a/style/blockly.css +++ b/style/blockly.css @@ -741,6 +741,63 @@ textarea.blocklyCommentText.blocklyTextarea.blocklyText { box-sizing: border-box !important; } +/* ── Toolbox search: category results ─────────────────────────────────── */ + +/* "Categories" header label */ +.blocklyFlyoutLabelText.flockSearchCategoryHeader { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + fill: #888; +} + +/* Category button background */ +.blocklyFlyoutButton.flockSearchCategoryButton rect.blocklyFlyoutButtonBackground { + rx: 6; + ry: 6; + fill: #5b67a5; + opacity: 0.92; +} +.blocklyFlyoutButton.flockSearchCategoryButton rect.blocklyFlyoutButtonShadow { + rx: 6; + ry: 6; + fill: rgba(0, 0, 0, 0.15); +} +.blocklyFlyoutButton.flockSearchCategoryButton .blocklyFlyoutButtonLabel { + fill: #fff; + font-weight: 600; + font-size: 13px; +} +.blocklyFlyoutButton.flockSearchCategoryButton:hover rect.blocklyFlyoutButtonBackground { + opacity: 1; + fill: #4a57a0; +} + +/* Per-categorystyle colours for the button background */ +.blocklyFlyoutButton.flock-cat-scene_category rect.blocklyFlyoutButtonBackground { fill: #5c8a40; } +.blocklyFlyoutButton.flock-cat-events_category rect.blocklyFlyoutButtonBackground { fill: #a04040; } +.blocklyFlyoutButton.flock-cat-transform_category rect.blocklyFlyoutButtonBackground { fill: #8a7e40; } +.blocklyFlyoutButton.flock-cat-animate_category rect.blocklyFlyoutButtonBackground { fill: #7a7040; } +.blocklyFlyoutButton.flock-cat-materials_category rect.blocklyFlyoutButtonBackground { fill: #6b3b8a; } +.blocklyFlyoutButton.flock-cat-sound_category rect.blocklyFlyoutButtonBackground { fill: #a06040; } +.blocklyFlyoutButton.flock-cat-sensing_category rect.blocklyFlyoutButtonBackground { fill: #3b7a8a; } +.blocklyFlyoutButton.flock-cat-control_category rect.blocklyFlyoutButtonBackground { fill: #508a50; } +.blocklyFlyoutButton.flock-cat-logic_category rect.blocklyFlyoutButtonBackground { fill: #4a5ab0; } +.blocklyFlyoutButton.flock-cat-variables_category rect.blocklyFlyoutButtonBackground { fill: #963065; } +.blocklyFlyoutButton.flock-cat-text_category rect.blocklyFlyoutButtonBackground { fill: #5545be; } +.blocklyFlyoutButton.flock-cat-math_category rect.blocklyFlyoutButtonBackground { fill: #3a4ab0; } +.blocklyFlyoutButton.flock-cat-procedures_category rect.blocklyFlyoutButtonBackground { fill: #7a3a8a; } +.blocklyFlyoutButton.flock-cat-snippets_category rect.blocklyFlyoutButtonBackground { fill: #387091; } + +/* Dark theme adjustments */ +[data-theme="dark"] .blocklyFlyoutLabelText.flockSearchCategoryHeader { + fill: #aaa; +} +[data-theme="dark"] .blocklyFlyoutButton.flockSearchCategoryButton rect.blocklyFlyoutButtonBackground { + opacity: 0.85; +} + From 9b7c90a4dd5d437d6ea06949d69a0297bf87d794 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 10:16:41 +0000 Subject: [PATCH 2/2] Fix Blockly.utils.parsing.replaceMessageReferences API path Blockly v12 places replaceMessageReferences under utils.parsing, not directly on utils. Fix both the new buildCategoryIndex resolveDisplayName helper and the pre-existing getBlockMessage helper which had the same bug (it rarely fired because most blocks have a toString() result). https://claude.ai/code/session_012QbU2xUemokbaybUrz5VG3 --- main/blocklyinit.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/main/blocklyinit.js b/main/blocklyinit.js index 153e041c..bd25096f 100644 --- a/main/blocklyinit.js +++ b/main/blocklyinit.js @@ -1630,7 +1630,7 @@ export function overrideSearchPlugin(workspace) { } const resolvedMessage = - Blockly.utils.replaceMessageReferences(message0); + Blockly.utils.parsing.replaceMessageReferences(message0); return translate(resolvedMessage); } @@ -1876,8 +1876,9 @@ export function overrideSearchPlugin(workspace) { function resolveDisplayName(name) { if (!name) return ""; return ( - Blockly.utils.replaceMessageReferences(name) || - name + Blockly.utils.parsing.replaceMessageReferences( + name, + ) || name ); }