diff --git a/crates/ui-egui/src/blocks_palette.rs b/crates/ui-egui/src/blocks_palette.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/blocks_palette.rs
@@ -0,0 +1,420 @@
+//! Blocks palette: dockable panel for block management with AutoCAD-parity UI.
+
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use cadcraft_color::Rgb;
+use cadcraft_engine::Input;
+use cadcraft_geom::Vec2;
+use cadcraft_render::DisplayList;
+use egui::{Align2, Color32, FontId, Rect, RichText, Sense, Stroke, StrokeKind, pos2, vec2};
+
+use crate::CadApp;
+use crate::canvas::Xf;
+use crate::icons::{self, Icon};
+use crate::theme::Tokens;
+
+/// Presentation mode for the blocks palette.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum ViewMode {
+    /// Responsive multi-column thumbnail cards.
+    Grid,
+    /// Compact single-column detailed list rows.
+    List,
+}
+
+/// Per-drawing and per-revision cached vector display lists for block thumbnail previews.
+#[derive(Clone, Default)]
+struct ThumbnailCache {
+    /// Drawing identity counter.
+    uid: u64,
+    /// Drawing revision counter when these thumbnails were generated.
+    revision: u64,
+    /// Cached vector display lists keyed by block name.
+    thumbnails: HashMap<String, Option<Arc<DisplayList>>>,
+}
+
+/// Render the blocks palette panel with search, view switcher, and block catalog.
+pub fn blocks_panel(app: &mut CadApp, ui: &mut egui::Ui) {
+    let t = Tokens::get();
+
+    let Ok(st) = app.session.state() else {
+        ui.vertical_centered(|ui| {
+            ui.add_space(24.0);
+            ui.label(RichText::new(crate::tl!("No drawing open")).color(t.text_dim).size(13.0));
+        });
+        return;
+    };
+
+    let doc = st.doc.clone();
+    let uid = st.uid;
+    let revision = st.revision;
+
+    let mut block_infos: Vec<(String, usize, String)> = doc
+        .blocks
+        .iter()
+        .filter(|(k, _)| !k.starts_with('*'))
+        .map(|(name, block)| (name.clone(), block.entities.len(), block.description.clone()))
+        .collect();
+
+    block_infos.sort_by_key(|a| a.0.to_lowercase());
+    let total_count = block_infos.len();
+
+    let search_id = ui.scope_id().with("blocks_palette_search");
+    let mut search_query = ui.data_mut(|d| d.get_temp::<String>(search_id)).unwrap_or_default();
+
+    let view_mode_id = ui.scope_id().with("blocks_palette_view_mode");
+    let mut view_mode = ui.data_mut(|d| d.get_temp::<ViewMode>(view_mode_id)).unwrap_or(ViewMode::Grid);
+
+    let cache_id = ui.scope_id().with("blocks_palette_thumb_cache");
+    let mut cache = ui.data_mut(|d| d.get_temp::<ThumbnailCache>(cache_id)).unwrap_or_default();
+    if cache.uid != uid || cache.revision != revision {
+        cache.uid = uid;
+        cache.revision = revision;
+        cache.thumbnails.clear();
+    }
+
+    ui.add_space(4.0);
+    ui.horizontal(|ui| {
+        ui.spacing_mut().item_spacing = vec2(4.0, 0.0);
+        let clear_w = if search_query.is_empty() { 0.0 } else { 22.0 };
+        let search_w = (ui.available_width() - clear_w).max(40.0);
+
+        let search_resp = ui.add_sized(
+            vec2(search_w, 22.0),
+            egui::TextEdit::singleline(&mut search_query).hint_text(crate::tl!("Search blocks...")).margin(egui::Margin::symmetric(6, 3)),
+        );
+        if search_resp.changed() {
+            ui.data_mut(|d| d.insert_temp(search_id, search_query.clone()));
+        }
+
+        if !search_query.is_empty() && ui.add_sized(vec2(20.0, 22.0), egui::Button::new("✕")).on_hover_text(crate::tl!("Clear search")).clicked() {
+            search_query.clear();
+            ui.data_mut(|d| d.insert_temp(search_id, String::new()));
+        }
+    });
+
+    ui.add_space(4.0);
+
+    let filtered_infos: Vec<(String, usize, String)> = block_infos
+        .into_iter()
+        .filter(|(name, _, _)| search_query.is_empty() || name.to_lowercase().contains(&search_query.to_lowercase()))
+        .collect();
+
+    ui.horizontal(|ui| {
+        ui.spacing_mut().item_spacing = vec2(4.0, 0.0);
+
+        let count_str = if !search_query.is_empty() { format!("{}/{}", filtered_infos.len(), total_count) } else { format!("{total_count}") };
+        ui.label(RichText::new(crate::tf!("{n} blocks", n = count_str)).size(11.0).color(t.text_dim));
+
+        ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
+            if icons::button(ui, Icon::BlockCreate, 20.0, "Create Block (BLOCK)", false).clicked() {
+                app.start("block");
+            }
+
+            ui.add_space(2.0);
+
+            if icons::button(ui, Icon::List, 20.0, "List View", view_mode == ViewMode::List).clicked() {
+                view_mode = ViewMode::List;
+                ui.data_mut(|d| d.insert_temp(view_mode_id, view_mode));
+            }
+            if icons::button(ui, Icon::Grid, 20.0, "Grid View", view_mode == ViewMode::Grid).clicked() {
+                view_mode = ViewMode::Grid;
+                ui.data_mut(|d| d.insert_temp(view_mode_id, view_mode));
+            }
+        });
+    });
+
+    ui.add_space(4.0);
+    ui.separator();
+    ui.add_space(2.0);
+
+    egui::ScrollArea::vertical().id_salt("blocks_palette_scroll").auto_shrink([false, false]).show(ui, |ui| {
+        if filtered_infos.is_empty() {
+            ui.vertical_centered(|ui| {
+                ui.add_space(32.0);
+                let (icon_rect, _) = ui.allocate_exact_size(vec2(36.0, 36.0), Sense::hover());
+                icons::paint(ui.painter(), icon_rect, if search_query.is_empty() { Icon::Blocks } else { Icon::Search }, true);
+                ui.add_space(8.0);
+
+                if search_query.is_empty() {
+                    ui.label(RichText::new(crate::tl!("No blocks defined in this drawing")).color(t.text_dim).size(13.0));
+                    ui.add_space(10.0);
+                    if ui.button(crate::tl!("＋ Create Block")).clicked() {
+                        app.start("block");
+                    }
+                } else {
+                    ui.label(RichText::new(crate::tf!("No blocks match \"{query}\"", query = search_query)).color(t.text_dim).size(13.0));
+                    ui.add_space(8.0);
+                    if ui.button(crate::tl!("Clear filter")).clicked() {
+                        ui.data_mut(|d| d.insert_temp(search_id, String::new()));
+                    }
+                }
+                ui.add_space(32.0);
+            });
+            return;
+        }
+
+        for (name, _, _) in &filtered_infos {
+            if !cache.thumbnails.contains_key(name) {
+                let dl = doc.block(name).and_then(|block| {
+                    if block.entities.is_empty() {
+                        None
+                    } else {
+                        let rough =
+                            cadcraft_render::build_entities(&doc, block.entities.iter().map(|e| e.as_ref()), &cadcraft_render::Options::default());
+                        if rough.bounds.is_empty() || rough.prims.is_empty() { None } else { Some(Arc::new(rough)) }
+                    }
+                });
+                cache.thumbnails.insert(name.clone(), dl);
+            }
+        }
+
+        match view_mode {
+            ViewMode::Grid => {
+                render_grid_view(ui, &filtered_infos, &cache.thumbnails, t, app);
+            }
+            ViewMode::List => {
+                render_list_view(ui, &filtered_infos, &cache.thumbnails, t, app);
+            }
+        }
+    });
+
+    ui.data_mut(|d| d.insert_temp(cache_id, cache));
+}
+
+/// Render the block catalog as a responsive grid of cards filling available width.
+fn render_grid_view(
+    ui: &mut egui::Ui,
+    items: &[(String, usize, String)],
+    thumbnails: &HashMap<String, Option<Arc<DisplayList>>>,
+    t: Tokens,
+    app: &mut CadApp,
+) {
+    let avail_w = ui.available_width().max(100.0);
+    let spacing = 6.0;
+    let min_card_w = 96.0;
+    let cols = ((avail_w + spacing) / (min_card_w + spacing)).floor().max(1.0) as usize;
+    let card_w = ((avail_w - spacing * (cols as f32 - 1.0)) / cols as f32).floor();
+    let card_h = (card_w * 0.90).clamp(86.0, 102.0);
+
+    for chunk in items.chunks(cols) {
+        ui.horizontal(|ui| {
+            ui.spacing_mut().item_spacing = vec2(spacing, spacing);
+
+            for (name, count, desc) in chunk {
+                draw_grid_card(ui, name, *count, desc, card_w, card_h, thumbnails.get(name).and_then(|o| o.as_ref()), t, app);
+            }
+        });
+        ui.add_space(spacing);
+    }
+}
+
+/// Render an individual block card in grid view with vector thumbnail and interaction.
+#[allow(clippy::too_many_arguments)]
+fn draw_grid_card(
+    ui: &mut egui::Ui,
+    name: &str,
+    count: usize,
+    desc: &str,
+    w: f32,
+    h: f32,
+    preview: Option<&Arc<DisplayList>>,
+    t: Tokens,
+    app: &mut CadApp,
+) {
+    let (rect, resp) = ui.allocate_exact_size(vec2(w, h), Sense::click());
+
+    if resp.hovered() {
+        ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
+    }
+
+    let is_down = resp.is_pointer_button_down_on();
+    let is_hovered = resp.hovered();
+
+    let bg = if is_down {
+        t.tab_active
+    } else if is_hovered {
+        t.control_hover
+    } else {
+        t.control
+    };
+
+    let border_color = if is_hovered { t.accent } else { t.border };
+    let border_width = if is_hovered { 1.5 } else { 1.0 };
+
+    let p = ui.painter();
+    p.rect(rect, 6.0, bg, Stroke::new(border_width, border_color), StrokeKind::Inside);
+
+    let pad = 5.0;
+    let thumb_w = w - (pad * 2.0);
+    let thumb_h = (h * 0.52).floor();
+    let thumb_rect = Rect::from_min_size(pos2(rect.left() + pad, rect.top() + pad), vec2(thumb_w, thumb_h));
+
+    p.rect(thumb_rect, 4.0, t.canvas, Stroke::new(0.5, t.border), StrokeKind::Inside);
+
+    let mut drawn = false;
+    if let Some(list) = preview.filter(|l| !l.bounds.is_empty() && !l.prims.is_empty()) {
+        let b = list.bounds;
+        let inner = thumb_rect.shrink(3.0);
+        let bw = b.width().max(1e-9);
+        let bh = b.height().max(1e-9);
+        let scale = (f64::from(inner.width()) / bw).min(f64::from(inner.height()) / bh);
+        let xf = Xf { rect: thumb_rect, center: Vec2::new((b.min.x + b.max.x) / 2.0, (b.min.y + b.max.y) / 2.0), scale };
+        let clipped = p.with_clip_rect(thumb_rect);
+        crate::canvas::draw_list(&clipped, &xf, list, Rgb(t.canvas.r(), t.canvas.g(), t.canvas.b()), false);
+        drawn = true;
+    }
+
+    if !drawn {
+        icons::paint(p, Rect::from_center_size(thumb_rect.center(), vec2(22.0, 22.0)), Icon::Blocks, false);
+    }
+
+    let max_chars = ((w - 8.0) / 6.8).floor().max(3.0) as usize;
+    let short_name = truncate_text(name, max_chars);
+
+    let text_center_y = thumb_rect.bottom() + (h - thumb_rect.bottom() + rect.top()) * 0.32;
+    p.text(pos2(rect.center().x, text_center_y), Align2::CENTER_CENTER, short_name, FontId::proportional(11.0), t.text);
+
+    let count_center_y = thumb_rect.bottom() + (h - thumb_rect.bottom() + rect.top()) * 0.72;
+    p.text(pos2(rect.center().x, count_center_y), Align2::CENTER_CENTER, format!("{count} ent."), FontId::proportional(9.5), t.text_dim);
+
+    let tooltip_name = name.to_string();
+    let tooltip_desc = desc.to_string();
+    let resp = resp.on_hover_ui(|ui| {
+        ui.horizontal(|ui| {
+            let (ir, _) = ui.allocate_exact_size(vec2(16.0, 16.0), Sense::hover());
+            icons::paint(ui.painter(), ir, Icon::Blocks, false);
+            ui.label(RichText::new(&tooltip_name).strong().size(12.5));
+        });
+        if !tooltip_desc.is_empty() {
+            ui.label(RichText::new(&tooltip_desc).color(t.text_dim).size(11.0));
+        }
+        ui.label(RichText::new(crate::tf!("{n} entities", n = count)).color(t.text_dim).size(11.0));
+        ui.add_space(2.0);
+        ui.label(RichText::new(crate::tl!("💡 Click to insert • Right-click for options")).color(t.accent).size(10.0));
+    });
+
+    if resp.clicked() {
+        insert_block(app, name);
+    }
+
+    resp.context_menu(|ui| {
+        ui.style_mut().spacing.item_spacing = vec2(4.0, 4.0);
+        if ui.button(crate::tl!("Insert Block")).clicked() {
+            insert_block(app, name);
+            ui.close();
+        }
+        if ui.button(crate::tl!("Edit in Block Editor (BEDIT)")).clicked() {
+            let _ = app.run("bedit", serde_json::json!({ "name": name }));
+            ui.close();
+        }
+    });
+}
+
+/// Render the block catalog as a vertical list of detailed rows.
+fn render_list_view(
+    ui: &mut egui::Ui,
+    items: &[(String, usize, String)],
+    thumbnails: &HashMap<String, Option<Arc<DisplayList>>>,
+    t: Tokens,
+    app: &mut CadApp,
+) {
+    let row_h = 36.0;
+    let avail_w = ui.available_width();
+
+    for (name, count, desc) in items {
+        let (rect, resp) = ui.allocate_exact_size(vec2(avail_w, row_h), Sense::click());
+
+        if resp.hovered() {
+            ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
+        }
+
+        let bg = if resp.is_pointer_button_down_on() {
+            t.tab_active
+        } else if resp.hovered() {
+            t.control_hover
+        } else {
+            Color32::TRANSPARENT
+        };
+
+        let p = ui.painter();
+        if bg != Color32::TRANSPARENT {
+            p.rect_filled(rect, 4.0, bg);
+        }
+
+        let thumb_rect = Rect::from_min_size(pos2(rect.left() + 4.0, rect.top() + 4.0), vec2(28.0, 28.0));
+        p.rect(thumb_rect, 3.0, t.canvas, Stroke::new(0.5, t.border), StrokeKind::Inside);
+
+        let mut drawn = false;
+        if let Some(list) = thumbnails.get(name).and_then(|o| o.as_ref()).filter(|l| !l.bounds.is_empty() && !l.prims.is_empty()) {
+            let b = list.bounds;
+            let inner = thumb_rect.shrink(2.0);
+            let bw = b.width().max(1e-9);
+            let bh = b.height().max(1e-9);
+            let scale = (f64::from(inner.width()) / bw).min(f64::from(inner.height()) / bh);
+            let xf = Xf { rect: thumb_rect, center: Vec2::new((b.min.x + b.max.x) / 2.0, (b.min.y + b.max.y) / 2.0), scale };
+            let clipped = p.with_clip_rect(thumb_rect);
+            crate::canvas::draw_list(&clipped, &xf, list, Rgb(t.canvas.r(), t.canvas.g(), t.canvas.b()), false);
+            drawn = true;
+        }
+        if !drawn {
+            icons::paint(p, Rect::from_center_size(thumb_rect.center(), vec2(16.0, 16.0)), Icon::Blocks, false);
+        }
+
+        p.text(pos2(thumb_rect.right() + 8.0, rect.top() + 10.0), Align2::LEFT_CENTER, name, FontId::proportional(12.0), t.text);
+
+        let sub = if desc.is_empty() { format!("{count} ent.") } else { format!("{count} ent. — {desc}") };
+        p.text(pos2(thumb_rect.right() + 8.0, rect.bottom() - 9.0), Align2::LEFT_CENTER, sub, FontId::proportional(10.0), t.text_dim);
+
+        let insert_rect = Rect::from_center_size(pos2(rect.right() - 14.0, rect.center().y), vec2(14.0, 14.0));
+        icons::paint(p, insert_rect, Icon::Insert, !resp.hovered());
+
+        let tooltip_name = name.to_string();
+        let tooltip_count = *count;
+        let resp = resp.on_hover_ui(|ui| {
+            ui.label(RichText::new(&tooltip_name).strong().size(12.5));
+            ui.label(RichText::new(crate::tf!("{n} entities", n = tooltip_count)).color(t.text_dim).size(11.0));
+            ui.label(RichText::new(crate::tl!("💡 Click to insert • Right-click for options")).color(t.accent).size(10.0));
+        });
+
+        if resp.clicked() {
+            insert_block(app, name);
+        }
+
+        resp.context_menu(|ui| {
+            ui.style_mut().spacing.item_spacing = vec2(4.0, 4.0);
+            if ui.button(crate::tl!("Insert Block")).clicked() {
+                insert_block(app, name);
+                ui.close();
+            }
+            if ui.button(crate::tl!("Edit in Block Editor (BEDIT)")).clicked() {
+                let _ = app.run("bedit", serde_json::json!({ "name": name }));
+                ui.close();
+            }
+        });
+
+        ui.add_space(2.0);
+    }
+}
+
+/// Invoke the INSERT command with the specified block name.
+fn insert_block(app: &mut CadApp, name: &str) {
+    app.start("insert");
+    if let Err(e) = app.session.input(Input::Text(name.to_string())) {
+        app.session.echo(e.to_string());
+    }
+}
+
+/// Truncate `text` to `max_len` graphemes with an ellipsis suffix when exceeding bounds.
+fn truncate_text(text: &str, max_len: usize) -> String {
+    let char_count = text.chars().count();
+    if char_count <= max_len {
+        text.to_string()
+    } else if max_len <= 1 {
+        "…".to_string()
+    } else {
+        let truncated: String = text.chars().take(max_len - 1).collect();
+        format!("{truncated}…")
+    }
+}
diff --git a/crates/ui-egui/src/docking.rs b/crates/ui-egui/src/docking.rs
--- a/crates/ui-egui/src/docking.rs
+++ b/crates/ui-egui/src/docking.rs
@@ -17,12 +17,13 @@ pub enum Panel {
     QuickCalc,
     Layers,
     Properties,
+    Blocks,
     #[serde(rename = "externalReferences")]
     ExternalRefs,
 }
 
 impl Panel {
-    pub const ALL: &[Self] = &[Self::Canvas, Self::ToolSets, Self::Layers, Self::Properties, Self::ExternalRefs];
+    pub const ALL: &[Self] = &[Self::Canvas, Self::ToolSets, Self::Layers, Self::Properties, Self::Blocks, Self::ExternalRefs];
     pub fn id(self) -> &'static str {
         match self {
             Self::Canvas => "canvas",
@@ -30,6 +31,7 @@ impl Panel {
             Self::QuickCalc => "quickCalc",
             Self::Layers => "layers",
             Self::Properties => "properties",
+            Self::Blocks => "blocks",
             Self::ExternalRefs => "externalReferences",
         }
     }
@@ -40,6 +42,7 @@ impl Panel {
             Self::QuickCalc => "QuickCalc",
             Self::Layers => "Layers",
             Self::Properties => "Properties",
+            Self::Blocks => "Blocks",
             Self::ExternalRefs => "External References",
         }
     }
@@ -52,6 +55,7 @@ impl Panel {
             Self::QuickCalc => Some(Icon::Table),
             Self::Layers => Some(Icon::Layers),
             Self::Properties => Some(Icon::Properties),
+            Self::Blocks => Some(Icon::Blocks),
             Self::ExternalRefs => Some(Icon::Attach),
         }
     }
@@ -145,10 +149,11 @@ fn docked(layout: &Layout<Panel>, panel: Panel) -> bool {
 fn home_anchor(layout: &Layout<Panel>, panel: Panel) -> Option<(Panel, bool)> {
     let neighbours: &[(Panel, bool)] = match panel {
         Panel::Canvas | Panel::ToolSets => &[],
-        Panel::Layers => &[(Panel::Properties, false), (Panel::ExternalRefs, false)],
-        Panel::Properties => &[(Panel::Layers, true), (Panel::ExternalRefs, false)],
-        Panel::ExternalRefs => &[(Panel::Properties, true), (Panel::Layers, true)],
-        Panel::QuickCalc => &[(Panel::ExternalRefs, true), (Panel::Properties, true), (Panel::Layers, true)],
+        Panel::Layers => &[(Panel::Blocks, false), (Panel::Properties, false), (Panel::ExternalRefs, false)],
+        Panel::Blocks => &[(Panel::Layers, true), (Panel::Properties, false), (Panel::ExternalRefs, false)],
+        Panel::Properties => &[(Panel::Blocks, true), (Panel::Layers, true), (Panel::ExternalRefs, false)],
+        Panel::ExternalRefs => &[(Panel::Properties, true), (Panel::Blocks, true), (Panel::Layers, true)],
+        Panel::QuickCalc => &[(Panel::ExternalRefs, true), (Panel::Properties, true), (Panel::Blocks, true), (Panel::Layers, true)],
     };
     if let Some(found) = neighbours.iter().copied().find(|(neighbour, _)| docked(layout, *neighbour)) {
         return Some(found);
@@ -411,6 +416,9 @@ impl DockContent<Panel> for Panels<'_> {
                     crate::shortcut_menus::palette(app, ui, crate::palettes::properties_section, crate::shortcut_menus::properties_entries)
                 });
             }
+            Panel::Blocks => {
+                crate::blocks_palette::blocks_panel(app, ui);
+            }
             Panel::ExternalRefs => {
                 egui::ScrollArea::both().id_salt("dock-xrefs").auto_shrink([false, false]).show(ui, |ui| crate::xrefs::section(app, ui));
             }
@@ -441,7 +449,7 @@ fn panel_limits(panel: &Panel) -> PanelLimits {
         Panel::Canvas => (egui::vec2(300.0, 180.0), 1_000_000.0),
         Panel::QuickCalc => (egui::vec2(240.0, 160.0), 600.0),
         Panel::ToolSets => (egui::vec2(220.0, 150.0), 480.0),
-        Panel::Layers | Panel::Properties | Panel::ExternalRefs => (egui::vec2(220.0, 100.0), 720.0),
+        Panel::Layers | Panel::Properties | Panel::Blocks | Panel::ExternalRefs => (egui::vec2(220.0, 100.0), 720.0),
     };
     PanelLimits { min, max: egui::vec2(max_width, 1_000_000.0) }
 }
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -12,6 +12,7 @@ pub mod aliasdlg;
 pub mod animation;
 pub mod autosave;
 pub mod bedit;
+pub mod blocks_palette;
 pub mod canvas;
 pub mod capture;
 pub mod chrome;
diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs
--- a/crates/ui-egui/src/menus.rs
+++ b/crates/ui-egui/src/menus.rs
@@ -27,6 +27,7 @@ pub const UI_COMMANDS: &[(&str, &str, &[&str], Option<&str>)] = &[
     ("ui.toggle.toolsets", "Tool Sets", &["Window", "Tool Sets"], Some("Cmd+3")),
     ("ui.panel.properties", "Properties Inspector", &["Window", "Properties Inspector"], Some("Cmd+1")),
     ("ui.panel.layers", "Layers Palette", &["Window", "Layers Palette"], None),
+    ("ui.panel.blocks", "Blocks Palette", &["Window", "Blocks Palette"], None),
     ("ui.toggle.quickcalc", "QuickCalc", &["Window", "QuickCalc"], Some("Cmd+8")),
     ("ui.panel.quickcalc", "QuickCalc", &[], None),
     ("ui.toggle.systemcursor", "Show System Cursor", &["View", "Accessibility", "Show System Cursor"], None),
@@ -101,8 +102,12 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
     if matches!(id, "ui.dock" | "ui.dock.reset") {
         return Some(crate::docking::command(app, id == "ui.dock.reset", params));
     }
-    if matches!(id, "ui.panel.properties" | "ui.panel.layers") {
-        let panel = if id == "ui.panel.properties" { "properties" } else { "layers" };
+    if matches!(id, "ui.panel.properties" | "ui.panel.layers" | "ui.panel.blocks") {
+        let panel = match id {
+            "ui.panel.properties" => "properties",
+            "ui.panel.layers" => "layers",
+            _ => "blocks",
+        };
         return Some(crate::docking::command(app, false, &json!({"operation":"open", "panel":panel})));
     }
     if id == "ui.toggle.quickcalc" {
