diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TemplatePickerDialog.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TemplatePickerDialog.razor
new file mode 100644
index 00000000..079bd570
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TemplatePickerDialog.razor
@@ -0,0 +1,138 @@
+@*
+ Body component for the Create Instance template picker, hosted by
+ IDialogService.ShowAsync. Wraps the shared TemplateFolderTree (the same tree
+ the Templates page renders) with its own search box. Templates are the only
+ selectable nodes — folders just toggle — and a double-click selects. Closes
+ with TemplatePickResult, a record so Cancel (null) is distinguishable from a
+ pick, the same contract MoveTemplateDialog uses.
+
+ Selection is tracked here rather than through TreeView's SelectedKey: in
+ Single mode TemplateFolderTree forwards only the Checkbox-mode SelectedKeys
+ pair, so the row highlight has to come from this component's own NodeContent.
+*@
+@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates
+@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
+
+
+
+
+ @if (!string.IsNullOrEmpty(_search))
+ {
+ _search = string.Empty">✕
+ }
+
+
_tree?.ExpandAll()">
+
_tree?.CollapseAll()">
+
+
+
+
+
+
+
+
+ OnNodeClick(node)"
+ @ondblclick="() => OnNodeDoubleClick(node)">@node.Name
+ @if (node.Kind == TemplateTreeNodeKind.Folder && node.Children.Count > 0)
+ {
+
+ @node.Children.Count
+
+ }
+
+ No templates match.
+
+
+
+
+
+@code {
+ /// Supplied by IDialogService.ShowAsync ; closes/cancels the modal.
+ [Parameter] public DialogContext Context { get; set; } = default!;
+
+ /// Folder catalogue backing the tree.
+ [Parameter] public IReadOnlyList Folders { get; set; } = Array.Empty();
+
+ /// Template catalogue backing the tree.
+ [Parameter] public IReadOnlyList Templates { get; set; } = Array.Empty();
+
+ /// Template to open on: preselected, highlighted, and revealed in the tree.
+ [Parameter] public int? InitialTemplateId { get; set; }
+
+ private TemplateFolderTree? _tree;
+ private string _search = string.Empty;
+ private int _selectedId;
+ private string[]? _revealKeys;
+ private Dictionary _templatesById = new();
+ private Dictionary _foldersById = new();
+
+ protected override void OnParametersSet()
+ {
+ _templatesById = Templates.ToDictionary(t => t.Id);
+ _foldersById = Folders.ToDictionary(f => f.Id);
+ if (_selectedId == 0 && InitialTemplateId is int id && _templatesById.ContainsKey(id))
+ {
+ _selectedId = id;
+ _revealKeys = new[] { $"t:{id}" };
+ }
+ }
+
+ private void OnSearchInput(ChangeEventArgs e) => _search = e.Value?.ToString() ?? string.Empty;
+
+ private string LabelClass(TemplateTreeNode node)
+ {
+ if (node.Kind == TemplateTreeNodeKind.Folder) return "fw-semibold";
+ return node.Kind == TemplateTreeNodeKind.Template && node.Id == _selectedId
+ ? "text-primary fw-semibold"
+ : string.Empty;
+ }
+
+ private void OnNodeClick(TemplateTreeNode node)
+ {
+ // Folders and composition slots are structure, not pickable targets.
+ if (node.Kind != TemplateTreeNodeKind.Template) return;
+ _selectedId = node.Id;
+ }
+
+ private void OnNodeDoubleClick(TemplateTreeNode node)
+ {
+ if (node.Kind != TemplateTreeNodeKind.Template) return;
+ _selectedId = node.Id;
+ Submit();
+ }
+
+ private void Submit()
+ {
+ if (_selectedId > 0) Context.Close(new TemplatePickResult(_selectedId));
+ }
+
+ /// Resolved result of the picker; null from ShowAsync means Cancel.
+ /// Id of the template the operator picked.
+ public sealed record TemplatePickResult(int TemplateId);
+}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/TemplatePickerDialogTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/TemplatePickerDialogTests.cs
new file mode 100644
index 00000000..317d8a91
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/TemplatePickerDialogTests.cs
@@ -0,0 +1,136 @@
+using Bunit;
+using Microsoft.Extensions.DependencyInjection;
+using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
+using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
+
+///
+/// bUnit tests for the Create Instance template picker: templates are the only
+/// selectable nodes, Select stays disabled until one is picked, double-click
+/// selects, Cancel resolves null, and an InitialTemplateId opens the tree on
+/// that template.
+///
+public class TemplatePickerDialogTests : BunitContext
+{
+ private readonly DialogService _service = new();
+
+ public TemplatePickerDialogTests()
+ {
+ Services.AddSingleton(_service);
+ JSInterop.Mode = JSRuntimeMode.Loose;
+ }
+
+ private static (List, List) Catalogue()
+ {
+ var a = new TemplateFolder("A") { Id = 1 };
+ var b = new TemplateFolder("B") { Id = 2, ParentFolderId = 1 };
+ var other = new TemplateFolder("Other") { Id = 3 };
+ var t7 = new Template("Deep") { Id = 7, FolderId = 2 };
+ var t8 = new Template("Elsewhere") { Id = 8, FolderId = 3 };
+ return (new() { a, b, other }, new() { t7, t8 });
+ }
+
+ private async Task<(IRenderedComponent cut, Task pending)> Open(int? initial = null)
+ {
+ var (f, t) = Catalogue();
+ var cut = Render();
+ Task pending = null!;
+ // Block body, not an expression body: an expression-bodied lambda returning the
+ // pending Task binds to InvokeAsync(Func) and would await the dialog itself.
+ await cut.InvokeAsync(() =>
+ {
+ pending = _service.ShowAsync(
+ "Choose template",
+ ctx => b =>
+ {
+ b.OpenComponent(0);
+ b.AddAttribute(1, nameof(TemplatePickerDialog.Context), ctx);
+ b.AddAttribute(2, nameof(TemplatePickerDialog.Folders), f);
+ b.AddAttribute(3, nameof(TemplatePickerDialog.Templates), t);
+ b.AddAttribute(4, nameof(TemplatePickerDialog.InitialTemplateId), initial);
+ b.CloseComponent();
+ },
+ size: "modal-lg");
+ });
+ cut.Render();
+ return (cut, pending);
+ }
+
+ private static void ExpandFolder(IRenderedComponent cut, string label) =>
+ cut.FindAll("li[role='treeitem']")
+ .First(li => li.QuerySelector(".tv-label")!.TextContent.Trim() == label)
+ .QuerySelector(".tv-toggle")!.Click();
+
+ private static AngleSharp.Dom.IElement Label(IRenderedComponent cut, string text) =>
+ cut.FindAll(".tv-label").First(l => l.TextContent.Trim() == text);
+
+ [Fact]
+ public async Task Select_IsDisabled_UntilATemplateIsClicked()
+ {
+ var (cut, _) = await Open();
+ Assert.True(cut.Find("button[data-test='picker-select']").HasAttribute("disabled"));
+ Assert.Contains("Select a template", cut.Find("[data-test='picker-status']").TextContent);
+ }
+
+ [Fact]
+ public async Task ClickingTemplate_ThenSelect_ResolvesItsId()
+ {
+ var (cut, pending) = await Open();
+ ExpandFolder(cut, "A");
+ ExpandFolder(cut, "B");
+ Label(cut, "Deep").Click();
+
+ Assert.False(cut.Find("button[data-test='picker-select']").HasAttribute("disabled"));
+ Assert.Contains("A / B / Deep", cut.Find("[data-test='picker-status']").TextContent);
+
+ cut.Find("button[data-test='picker-select']").Click();
+ Assert.Equal(7, (await pending)!.TemplateId);
+ }
+
+ [Fact]
+ public async Task DoubleClickingTemplate_Resolves()
+ {
+ var (cut, pending) = await Open();
+ ExpandFolder(cut, "A");
+ ExpandFolder(cut, "B");
+ Label(cut, "Deep").DoubleClick();
+ Assert.Equal(7, (await pending)!.TemplateId);
+ }
+
+ [Fact]
+ public async Task ClickingFolder_SelectsNothing()
+ {
+ var (cut, _) = await Open();
+ Label(cut, "A").Click();
+ Assert.True(cut.Find("button[data-test='picker-select']").HasAttribute("disabled"));
+ }
+
+ [Fact]
+ public async Task Cancel_ResolvesNull()
+ {
+ var (cut, pending) = await Open();
+ cut.Find("button[data-test='picker-cancel']").Click();
+ Assert.Null(await pending);
+ }
+
+ [Fact]
+ public async Task InitialTemplateId_IsRevealedAndHighlighted()
+ {
+ var (cut, _) = await Open(initial: 7);
+ Assert.Contains("Deep", cut.Markup);
+ Assert.DoesNotContain("Elsewhere", cut.Markup);
+ Assert.False(cut.Find("button[data-test='picker-select']").HasAttribute("disabled"));
+ Assert.Contains("A / B / Deep", cut.Find("[data-test='picker-status']").TextContent);
+ Assert.Contains("text-primary", Label(cut, "Deep").ClassName);
+ }
+
+ [Fact]
+ public async Task Search_FiltersTree()
+ {
+ var (cut, _) = await Open();
+ cut.Find("input[data-test='picker-search']").Input("Else");
+ Assert.Contains("Elsewhere", cut.Markup);
+ Assert.DoesNotContain("Deep", cut.Markup);
+ }
+}