diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/KeysetPager.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/KeysetPager.razor
new file mode 100644
index 00000000..586f2814
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/KeysetPager.razor
@@ -0,0 +1,24 @@
+@* Reusable keyset (cursor) pagination button bar — purely presentational.
+ All cursor/navigation logic lives in the consuming page via callbacks. *@
+
+
+
+ Page @PageNumber · @RowCount rows
+
+
+
+@code {
+ [Parameter] public int PageNumber { get; set; } = 1;
+ [Parameter] public int RowCount { get; set; }
+ [Parameter] public bool CanGoBack { get; set; }
+ [Parameter] public bool HasNextPage { get; set; }
+ [Parameter] public bool Disabled { get; set; }
+ [Parameter] public EventCallback OnPrevious { get; set; }
+ [Parameter] public EventCallback OnNext { get; set; }
+}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/KeysetPagerTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/KeysetPagerTests.cs
new file mode 100644
index 00000000..926523a2
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/KeysetPagerTests.cs
@@ -0,0 +1,94 @@
+using Bunit;
+using Microsoft.AspNetCore.Components;
+using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
+
+///
+/// Component tests for — the purely-presentational
+/// cursor-pagination button bar shared by SiteCallsReport and AuditResultsGrid.
+///
+public class KeysetPagerTests : BunitContext
+{
+ [Fact]
+ public void PrevDisabled_WhenCannotGoBack()
+ {
+ var cut = Render(ps => ps
+ .Add(p => p.CanGoBack, false)
+ .Add(p => p.HasNextPage, true));
+
+ var prev = cut.Find("[data-test='keyset-prev']");
+
+ Assert.NotNull(prev.GetAttribute("disabled"));
+ }
+
+ [Fact]
+ public void NextDisabled_WhenNoNextPage()
+ {
+ var cut = Render(ps => ps
+ .Add(p => p.CanGoBack, true)
+ .Add(p => p.HasNextPage, false));
+
+ var next = cut.Find("[data-test='keyset-next']");
+
+ Assert.NotNull(next.GetAttribute("disabled"));
+ }
+
+ [Fact]
+ public void Prev_Click_InvokesOnPrevious()
+ {
+ var fired = false;
+ var cut = Render(ps => ps
+ .Add(p => p.CanGoBack, true)
+ .Add(p => p.HasNextPage, false)
+ .Add(p => p.OnPrevious,
+ EventCallback.Factory.Create(this, () => { fired = true; })));
+
+ cut.Find("[data-test='keyset-prev']").Click();
+
+ Assert.True(fired);
+ }
+
+ [Fact]
+ public void Next_Click_InvokesOnNext()
+ {
+ var fired = false;
+ var cut = Render(ps => ps
+ .Add(p => p.CanGoBack, false)
+ .Add(p => p.HasNextPage, true)
+ .Add(p => p.OnNext,
+ EventCallback.Factory.Create(this, () => { fired = true; })));
+
+ cut.Find("[data-test='keyset-next']").Click();
+
+ Assert.True(fired);
+ }
+
+ [Fact]
+ public void Summary_RendersPageAndRows()
+ {
+ var cut = Render(ps => ps
+ .Add(p => p.PageNumber, 3)
+ .Add(p => p.RowCount, 42));
+
+ var summary = cut.Find("[data-test='keyset-summary']");
+
+ Assert.Contains("Page 3", summary.TextContent);
+ Assert.Contains("42 rows", summary.TextContent);
+ }
+
+ [Fact]
+ public void BothDisabled_WhenDisabled()
+ {
+ var cut = Render(ps => ps
+ .Add(p => p.CanGoBack, true)
+ .Add(p => p.HasNextPage, true)
+ .Add(p => p.Disabled, true));
+
+ var prev = cut.Find("[data-test='keyset-prev']");
+ var next = cut.Find("[data-test='keyset-next']");
+
+ Assert.NotNull(prev.GetAttribute("disabled"));
+ Assert.NotNull(next.GetAttribute("disabled"));
+ }
+}