diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/OffsetPager.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/OffsetPager.razor
new file mode 100644
index 00000000..90b309e5
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/OffsetPager.razor
@@ -0,0 +1,53 @@
+@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
+
+@* T35a — reusable offset-pagination bar. Purely presentational: the host page
+ owns page-number state, query execution, and the HasNextPage decision.
+ Parameters: Page, PageChanged, HasNextPage, TotalCount, PageSize, Disabled. *@
+
+
+
+
+ @SummaryText
+
+
+
+
+@code {
+ [Parameter] public int Page { get; set; } = 1;
+ [Parameter] public EventCallback PageChanged { get; set; }
+ [Parameter] public bool HasNextPage { get; set; }
+ [Parameter] public int? TotalCount { get; set; }
+ [Parameter] public int PageSize { get; set; }
+ [Parameter] public bool Disabled { get; set; }
+
+ private bool HasPrev => Page > 1;
+
+ private int? PageCount =>
+ TotalCount is { } t && PageSize > 0
+ ? (int)Math.Ceiling(t / (double)PageSize)
+ : (int?)null;
+
+ private string SummaryText
+ {
+ get
+ {
+ var text = $"Page {Page}";
+ if (PageCount is { } pc) text += $" of {pc}";
+ if (TotalCount is { } total) text += $" · {total} total";
+ return text;
+ }
+ }
+
+ private Task OnPrevAsync() => PageChanged.InvokeAsync(Page - 1);
+ private Task OnNextAsync() => PageChanged.InvokeAsync(Page + 1);
+}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/OffsetPagerTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/OffsetPagerTests.cs
new file mode 100644
index 00000000..4fe7d9cc
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/OffsetPagerTests.cs
@@ -0,0 +1,109 @@
+using Bunit;
+using Microsoft.AspNetCore.Components;
+using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
+
+///
+/// bUnit tests for the reusable pagination bar (T35a).
+/// The component is purely presentational — it emits PageChanged callbacks
+/// and renders disabled states; the host page owns page-number and query state.
+///
+public class OffsetPagerTests : BunitContext
+{
+ // ── PrevDisabled_OnFirstPage ──────────────────────────────────────────────
+
+ [Fact]
+ public void PrevDisabled_OnFirstPage()
+ {
+ var cut = Render(ps => ps
+ .Add(p => p.Page, 1)
+ .Add(p => p.HasNextPage, true));
+
+ var prev = cut.Find("[data-test='pager-prev']");
+
+ Assert.NotNull(prev.GetAttribute("disabled"));
+ }
+
+ // ── NextDisabled_WhenNoNextPage ───────────────────────────────────────────
+
+ [Fact]
+ public void NextDisabled_WhenNoNextPage()
+ {
+ var cut = Render(ps => ps
+ .Add(p => p.Page, 3)
+ .Add(p => p.HasNextPage, false));
+
+ var next = cut.Find("[data-test='pager-next']");
+
+ Assert.NotNull(next.GetAttribute("disabled"));
+ }
+
+ // ── Next_Click_EmitsPagePlusOne ───────────────────────────────────────────
+
+ [Fact]
+ public void Next_Click_EmitsPagePlusOne()
+ {
+ int? emitted = null;
+ var cut = Render(ps => ps
+ .Add(p => p.Page, 2)
+ .Add(p => p.HasNextPage, true)
+ .Add(p => p.PageChanged,
+ EventCallback.Factory.Create(this, v => emitted = v)));
+
+ cut.Find("[data-test='pager-next']").Click();
+
+ Assert.Equal(3, emitted);
+ }
+
+ // ── Prev_Click_EmitsPageMinusOne ─────────────────────────────────────────
+
+ [Fact]
+ public void Prev_Click_EmitsPageMinusOne()
+ {
+ int? emitted = null;
+ var cut = Render(ps => ps
+ .Add(p => p.Page, 2)
+ .Add(p => p.HasNextPage, false)
+ .Add(p => p.PageChanged,
+ EventCallback.Factory.Create(this, v => emitted = v)));
+
+ cut.Find("[data-test='pager-prev']").Click();
+
+ Assert.Equal(1, emitted);
+ }
+
+ // ── Summary_RendersPageOfTotal ────────────────────────────────────────────
+
+ [Fact]
+ public void Summary_RendersPageOfTotal()
+ {
+ var cut = Render(ps => ps
+ .Add(p => p.Page, 2)
+ .Add(p => p.PageSize, 50)
+ .Add(p => p.TotalCount, 230)
+ .Add(p => p.HasNextPage, true));
+
+ var summary = cut.Find("[data-test='pager-summary']");
+
+ Assert.Contains("Page 2 of 5", summary.TextContent);
+ Assert.Contains("230 total", summary.TextContent);
+ }
+
+ // ── BothDisabled_WhenDisabled ─────────────────────────────────────────────
+
+ [Fact]
+ public void BothDisabled_WhenDisabled()
+ {
+ var cut = Render(ps => ps
+ .Add(p => p.Page, 3)
+ .Add(p => p.HasNextPage, true)
+ .Add(p => p.Disabled, true));
+
+ var prev = cut.Find("[data-test='pager-prev']");
+ var next = cut.Find("[data-test='pager-next']");
+
+ Assert.NotNull(prev.GetAttribute("disabled"));
+ Assert.NotNull(next.GetAttribute("disabled"));
+ }
+}