@vtex/faststore-plugin-buyer-portal 2.0.8 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.github/workflows/release.yaml +4 -4
  2. package/AGENTS.md +6 -0
  3. package/CHANGELOG.md +23 -1
  4. package/docs/superpowers/plans/2026-06-26-contracts-listing-selection-search-pagination.md +1070 -0
  5. package/docs/superpowers/specs/2026-06-26-contracts-listing-selection-search-pagination-design.md +157 -0
  6. package/package.json +1 -1
  7. package/plugin.config.js +8 -0
  8. package/public/buyer-portal-icons.svg +37 -1
  9. package/specs/001-suma-phase-2/checklists/requirements.md +36 -0
  10. package/specs/001-suma-phase-2/contracts/contracts-api.md +187 -0
  11. package/specs/001-suma-phase-2/contracts/feature-flag.md +91 -0
  12. package/specs/001-suma-phase-2/data-model.md +119 -0
  13. package/specs/001-suma-phase-2/plan.md +123 -0
  14. package/specs/001-suma-phase-2/quickstart.md +156 -0
  15. package/specs/001-suma-phase-2/research.md +150 -0
  16. package/specs/001-suma-phase-2/spec.md +186 -0
  17. package/specs/001-suma-phase-2/tasks.md +253 -0
  18. package/specs/suma-add-contracts.md +224 -0
  19. package/specs/suma-contract-listing.md +295 -0
  20. package/specs/suma-contract-switching.md +201 -0
  21. package/specs/suma-remove-contracts.md +233 -0
  22. package/specs/suma-set-default-contract.md +208 -0
  23. package/src/features/budgets/layouts/BudgetsLayout/BudgetsLayout.tsx +113 -105
  24. package/src/features/buying-policies/layouts/BuyingPoliciesLayout/BuyingPoliciesLayout.tsx +60 -46
  25. package/src/features/contracts/clients/ContractsClient.ts +45 -0
  26. package/src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx +323 -0
  27. package/src/features/contracts/components/AddContractsDrawer/add-contracts-drawer.scss +282 -0
  28. package/src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx +58 -0
  29. package/src/features/contracts/components/ConfirmRemoveDrawer/confirm-remove-drawer.scss +18 -0
  30. package/src/features/contracts/components/ContractListItem/ContractListItem.tsx +120 -0
  31. package/src/features/contracts/components/ContractSelectAllRow/ContractSelectAllRow.tsx +37 -0
  32. package/src/features/contracts/components/ContractSelectAllRow/index.ts +2 -0
  33. package/src/features/contracts/components/ContractSelectionBar/ContractSelectionBar.tsx +47 -0
  34. package/src/features/contracts/components/ContractSelectionBar/index.ts +2 -0
  35. package/src/features/contracts/components/ContractSelectionList/ContractSelectionList.tsx +55 -0
  36. package/src/features/contracts/components/ContractSwitchOverlay/ContractSwitchOverlay.tsx +36 -0
  37. package/src/features/contracts/components/ContractSwitchOverlay/contract-switch-overlay.scss +7 -0
  38. package/src/features/contracts/components/ContractsListHeader/ContractsListHeader.tsx +58 -0
  39. package/src/features/contracts/components/ContractsListHeader/index.ts +2 -0
  40. package/src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx +69 -0
  41. package/src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx +52 -0
  42. package/src/features/contracts/components/UnableToRemoveContractsDrawer/unable-to-remove-contracts-drawer.scss +29 -0
  43. package/src/features/contracts/components/index.ts +14 -0
  44. package/src/features/contracts/hooks/__tests__/contractListingHelpers.test.ts +71 -0
  45. package/src/features/contracts/hooks/__tests__/contractSwitchChannelHelpers.test.ts +32 -0
  46. package/src/features/contracts/hooks/__tests__/useAddContracts.test.ts +63 -0
  47. package/src/features/contracts/hooks/__tests__/useContractSelection.test.ts +72 -0
  48. package/src/features/contracts/hooks/__tests__/useRemoveContracts.test.ts +130 -0
  49. package/src/features/contracts/hooks/__tests__/useSetDefaultContract.test.ts +82 -0
  50. package/src/features/contracts/hooks/contractListingHelpers.ts +45 -0
  51. package/src/features/contracts/hooks/contractSelectionHelpers.ts +28 -0
  52. package/src/features/contracts/hooks/contractSwitchChannelHelpers.ts +9 -0
  53. package/src/features/contracts/hooks/index.ts +19 -0
  54. package/src/features/contracts/hooks/useAddContracts.ts +27 -0
  55. package/src/features/contracts/hooks/useContractListing.ts +71 -0
  56. package/src/features/contracts/hooks/useContractSelection.ts +53 -0
  57. package/src/features/contracts/hooks/useContractSwitchChannel.ts +49 -0
  58. package/src/features/contracts/hooks/useListAvailableContracts.ts +27 -0
  59. package/src/features/contracts/hooks/useRemoveContracts.ts +21 -0
  60. package/src/features/contracts/hooks/useSetDefaultContract.ts +21 -0
  61. package/src/features/contracts/hooks/useSwitchContract.ts +76 -0
  62. package/src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx +316 -0
  63. package/src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx +415 -0
  64. package/src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx +83 -0
  65. package/src/features/contracts/layouts/ContractListingLayout/contract-listing-layout.scss +468 -0
  66. package/src/features/contracts/layouts/ContractListingLayout/utils.ts +4 -0
  67. package/src/features/contracts/layouts/index.ts +8 -0
  68. package/src/features/contracts/services/__tests__/contracts-client.test.ts +148 -0
  69. package/src/features/contracts/services/__tests__/list-attached-contracts.service.test.ts +95 -0
  70. package/src/features/contracts/services/add-contracts.service.ts +27 -0
  71. package/src/features/contracts/services/index.ts +24 -0
  72. package/src/features/contracts/services/list-attached-contracts.service.ts +40 -0
  73. package/src/features/contracts/services/list-available-contracts.service.ts +32 -0
  74. package/src/features/contracts/services/remove-contract.service.ts +27 -0
  75. package/src/features/contracts/services/remove-contracts.service.ts +36 -0
  76. package/src/features/contracts/services/set-default-contract.service.ts +27 -0
  77. package/src/features/contracts/types/ContractData.ts +1 -0
  78. package/src/features/contracts/utils/__tests__/changeContractToken.test.ts +141 -0
  79. package/src/features/contracts/utils/changeContractToken.ts +122 -0
  80. package/src/features/contracts/utils/clearPersistedSessionState.ts +74 -0
  81. package/src/features/org-units/components/OrgUnitDetailsNavbar/OrgUnitDetailsNavbar.tsx +45 -3
  82. package/src/features/org-units/components/OrgUnitDetailsNavbar/org-unit-details-navbar.scss +35 -0
  83. package/src/features/org-units/layouts/OrgUnitsLayout/OrgUnitsLayout.tsx +54 -48
  84. package/src/features/roles/layout/RolesLayout/RolesLayout.tsx +32 -23
  85. package/src/features/shared/components/BuyerPortalProvider/BuyerPortalProvider.tsx +1 -0
  86. package/src/features/shared/components/HeaderInside/HeaderInside.tsx +8 -1
  87. package/src/features/shared/components/HeaderInside/header-inside.scss +9 -0
  88. package/src/features/shared/layouts/BaseTabsLayout/SidebarMenu.tsx +6 -97
  89. package/src/features/shared/layouts/ContractTabsLayout/ContractTabsLayout.tsx +42 -5
  90. package/src/features/shared/layouts/LoadingTabsLayout/LoadingTabsLayout.tsx +1 -1
  91. package/src/features/shared/layouts/SumaPageLayout/FullSidebarNav.tsx +124 -0
  92. package/src/features/shared/layouts/SumaPageLayout/SumaPageLayout.tsx +56 -0
  93. package/src/features/shared/layouts/SumaPageLayout/SumaSidebar.tsx +80 -0
  94. package/src/features/shared/layouts/SumaPageLayout/SumaSidebarDrawer.tsx +48 -0
  95. package/src/features/shared/layouts/SumaPageLayout/index.ts +6 -0
  96. package/src/features/shared/layouts/SumaPageLayout/suma-sidebar-drawer.scss +37 -0
  97. package/src/features/shared/layouts/index.ts +6 -0
  98. package/src/features/shared/services/feature-flags/__tests__/get-feature-flags.service.test.ts +71 -0
  99. package/src/features/shared/services/feature-flags/get-feature-flags.service.ts +1 -0
  100. package/src/features/shared/utils/buyerPortalRoutes.ts +9 -0
  101. package/src/features/shared/utils/constants.ts +1 -1
  102. package/src/features/shared/utils/getSumaListingSidebarLinks.ts +18 -0
  103. package/src/features/shared/utils/index.ts +1 -0
  104. package/src/features/users/layouts/UsersLayout/UsersLayout.tsx +97 -103
  105. package/src/pages/contract-information.tsx +128 -0
  106. package/src/pages/contracts.tsx +104 -0
  107. package/src/pages/home.tsx +10 -6
  108. package/src/pages/org-unit-details.tsx +17 -13
  109. package/src/themes/layouts.scss +2 -0
  110. package/.agents/skills/create-page/SKILL.md +0 -154
  111. package/.agents/skills/create-page/metadata.json +0 -14
  112. package/.specify/memory/constitution.md +0 -83
@@ -0,0 +1,1070 @@
1
+ # Contracts Listing — Selection, Search & Local Pagination Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make the SUMA contracts listing support checkbox multi-selection, local search, and local pagination — without the checkbox accidentally triggering a contract switch.
6
+
7
+ **Architecture:** All contracts are already loaded client-side. A new pure-helper module (`filterContracts`, `paginate`) plus a thin `useContractListing` hook drive search + pagination. Selection reuses the existing `useContractSelection`. `ContractListItem` splits its click target: the checkbox selects (stops propagation), the row body switches. New presentational components add the search/pagination header, a tri-state "Select all" row, and a floating selection bar.
8
+
9
+ **Tech Stack:** React + TypeScript, FastStore plugin, `@faststore/ui` (Icon, useUI), SCSS with `data-fs-bp-*` attributes, Vitest (node env).
10
+
11
+ ## Global Constraints
12
+
13
+ - Plugin boundary: this is a plugin, no `yarn dev`; verify via `make check`.
14
+ - Page size: **25** contracts per page.
15
+ - Search: **local**, case-insensitive substring over `name` and `email`.
16
+ - Select-all scope: **all filtered contracts** (across pages); selection persists across page/search changes.
17
+ - Remove button: **no-op placeholder** (bulk delete is future).
18
+ - Keep the contract-switch flow (`useSwitchContract`) and ⌘/Ctrl+A select-all keyboard shortcut.
19
+ - Tests run in **node env** (no jsdom/renderHook) — test pure functions, not React hooks.
20
+ - Typecheck has ~133 pre-existing env errors from linked `@faststore/core`; bar = **no new errors in touched files**.
21
+ - Icons: next=`ChevronRight` (shared Icon); prev=`ChevronRight` rotated 180° via CSS; Remove leading icon=`MinusCircle` (shared Icon); clear=`@faststore/ui` `Icon name="X"`; indeterminate checkbox = CSS bar.
22
+
23
+ ---
24
+
25
+ ### Task 1: Pure listing helpers (filter + paginate)
26
+
27
+ **Files:**
28
+ - Create: `src/features/contracts/hooks/contractListingHelpers.ts`
29
+ - Test: `src/features/contracts/hooks/__tests__/contractListingHelpers.test.ts`
30
+
31
+ **Interfaces:**
32
+ - Produces:
33
+ - `filterContracts(contracts: ContractData[], term: string): ContractData[]`
34
+ - `type PageResult = { pageItems: ContractData[]; from: number; to: number; total: number; hasPrev: boolean; hasNext: boolean }`
35
+ - `paginate(items: ContractData[], page: number, pageSize: number): PageResult`
36
+
37
+ - [ ] **Step 1: Write the failing test**
38
+
39
+ ```ts
40
+ import { describe, expect, it } from "vitest";
41
+
42
+ import { filterContracts, paginate } from "../contractListingHelpers";
43
+
44
+ import type { ContractData } from "../../types";
45
+
46
+ const make = (id: string, name: string, email = ""): ContractData =>
47
+ ({ id, name, email }) as ContractData;
48
+
49
+ const contracts = [
50
+ make("1", "Stellar Global", "ops@stellar.com"),
51
+ make("2", "Aether Energy"),
52
+ make("3", "Alpha Logistics", "hi@alpha.io"),
53
+ ];
54
+
55
+ describe("filterContracts", () => {
56
+ it("returns all contracts for an empty or whitespace term", () => {
57
+ expect(filterContracts(contracts, "")).toHaveLength(3);
58
+ expect(filterContracts(contracts, " ")).toHaveLength(3);
59
+ });
60
+
61
+ it("matches name case-insensitively", () => {
62
+ const result = filterContracts(contracts, "alpha");
63
+ expect(result.map((c) => c.id)).toEqual(["3"]);
64
+ });
65
+
66
+ it("matches email case-insensitively", () => {
67
+ const result = filterContracts(contracts, "STELLAR.COM");
68
+ expect(result.map((c) => c.id)).toEqual(["1"]);
69
+ });
70
+
71
+ it("returns empty when nothing matches", () => {
72
+ expect(filterContracts(contracts, "zzz")).toEqual([]);
73
+ });
74
+ });
75
+
76
+ describe("paginate", () => {
77
+ const items = Array.from({ length: 50 }, (_, i) => make(String(i + 1), `C${i + 1}`));
78
+
79
+ it("returns the first page with correct interval and flags", () => {
80
+ const r = paginate(items, 1, 25);
81
+ expect(r.pageItems).toHaveLength(25);
82
+ expect([r.from, r.to, r.total]).toEqual([1, 25, 50]);
83
+ expect(r.hasPrev).toBe(false);
84
+ expect(r.hasNext).toBe(true);
85
+ });
86
+
87
+ it("returns the last page with a partial slice", () => {
88
+ const r = paginate(items.slice(0, 30), 2, 25);
89
+ expect(r.pageItems).toHaveLength(5);
90
+ expect([r.from, r.to, r.total]).toEqual([26, 30, 30]);
91
+ expect(r.hasPrev).toBe(true);
92
+ expect(r.hasNext).toBe(false);
93
+ });
94
+
95
+ it("clamps a page past the end to the last valid page", () => {
96
+ const r = paginate(items.slice(0, 30), 99, 25);
97
+ expect(r.from).toBe(26);
98
+ expect(r.to).toBe(30);
99
+ });
100
+
101
+ it("handles an empty list", () => {
102
+ const r = paginate([], 1, 25);
103
+ expect(r.pageItems).toEqual([]);
104
+ expect([r.from, r.to, r.total]).toEqual([0, 0, 0]);
105
+ expect(r.hasPrev).toBe(false);
106
+ expect(r.hasNext).toBe(false);
107
+ });
108
+ });
109
+ ```
110
+
111
+ - [ ] **Step 2: Run test to verify it fails**
112
+
113
+ Run: `yarn vitest run src/features/contracts/hooks/__tests__/contractListingHelpers.test.ts`
114
+ Expected: FAIL (module/exports not found)
115
+
116
+ - [ ] **Step 3: Write minimal implementation**
117
+
118
+ ```ts
119
+ import type { ContractData } from "../types";
120
+
121
+ export type PageResult = {
122
+ pageItems: ContractData[];
123
+ from: number;
124
+ to: number;
125
+ total: number;
126
+ hasPrev: boolean;
127
+ hasNext: boolean;
128
+ };
129
+
130
+ export function filterContracts(
131
+ contracts: ContractData[],
132
+ term: string
133
+ ): ContractData[] {
134
+ const q = term.trim().toLowerCase();
135
+ if (!q) return contracts;
136
+ return contracts.filter((c) => {
137
+ const name = (c.name ?? "").toLowerCase();
138
+ const email = (c.email ?? "").toLowerCase();
139
+ return name.includes(q) || email.includes(q);
140
+ });
141
+ }
142
+
143
+ export function paginate(
144
+ items: ContractData[],
145
+ page: number,
146
+ pageSize: number
147
+ ): PageResult {
148
+ const total = items.length;
149
+ const lastPage = Math.max(1, Math.ceil(total / pageSize));
150
+ const safePage = Math.min(Math.max(1, page), lastPage);
151
+ const start = (safePage - 1) * pageSize;
152
+ const pageItems = items.slice(start, start + pageSize);
153
+ const from = total === 0 ? 0 : start + 1;
154
+ const to = total === 0 ? 0 : start + pageItems.length;
155
+ return {
156
+ pageItems,
157
+ from,
158
+ to,
159
+ total,
160
+ hasPrev: safePage > 1,
161
+ hasNext: safePage < lastPage,
162
+ };
163
+ }
164
+ ```
165
+
166
+ - [ ] **Step 4: Run test to verify it passes**
167
+
168
+ Run: `yarn vitest run src/features/contracts/hooks/__tests__/contractListingHelpers.test.ts`
169
+ Expected: PASS (all cases)
170
+
171
+ - [ ] **Step 5: Commit**
172
+
173
+ ```bash
174
+ git add src/features/contracts/hooks/contractListingHelpers.ts src/features/contracts/hooks/__tests__/contractListingHelpers.test.ts
175
+ git commit -m "feat(suma): add contract listing filter + paginate helpers"
176
+ ```
177
+
178
+ ---
179
+
180
+ ### Task 2: `useContractListing` hook
181
+
182
+ **Files:**
183
+ - Create: `src/features/contracts/hooks/useContractListing.ts`
184
+ - Modify: `src/features/contracts/hooks/index.ts`
185
+
186
+ **Interfaces:**
187
+ - Consumes: `filterContracts`, `paginate`, `PageResult` (Task 1).
188
+ - Produces:
189
+ ```ts
190
+ type UseContractListingReturn = {
191
+ searchTerm: string;
192
+ setSearch: (term: string) => void;
193
+ page: number;
194
+ nextPage: () => void;
195
+ prevPage: () => void;
196
+ pageItems: ContractData[];
197
+ filteredIds: string[];
198
+ from: number;
199
+ to: number;
200
+ total: number;
201
+ hasPrev: boolean;
202
+ hasNext: boolean;
203
+ };
204
+ function useContractListing(args: { contracts: ContractData[]; pageSize?: number }): UseContractListingReturn;
205
+ ```
206
+
207
+ - [ ] **Step 1: Write the hook**
208
+
209
+ ```ts
210
+ import { useMemo, useState } from "react";
211
+
212
+ import { filterContracts, paginate } from "./contractListingHelpers";
213
+
214
+ import type { ContractData } from "../types";
215
+
216
+ const DEFAULT_PAGE_SIZE = 25;
217
+
218
+ export type UseContractListingReturn = {
219
+ searchTerm: string;
220
+ setSearch: (term: string) => void;
221
+ page: number;
222
+ nextPage: () => void;
223
+ prevPage: () => void;
224
+ pageItems: ContractData[];
225
+ filteredIds: string[];
226
+ from: number;
227
+ to: number;
228
+ total: number;
229
+ hasPrev: boolean;
230
+ hasNext: boolean;
231
+ };
232
+
233
+ export function useContractListing({
234
+ contracts,
235
+ pageSize = DEFAULT_PAGE_SIZE,
236
+ }: {
237
+ contracts: ContractData[];
238
+ pageSize?: number;
239
+ }): UseContractListingReturn {
240
+ const [searchTerm, setSearchTerm] = useState("");
241
+ const [page, setPage] = useState(1);
242
+
243
+ const filtered = useMemo(
244
+ () => filterContracts(contracts, searchTerm),
245
+ [contracts, searchTerm]
246
+ );
247
+
248
+ const result = useMemo(
249
+ () => paginate(filtered, page, pageSize),
250
+ [filtered, page, pageSize]
251
+ );
252
+
253
+ const setSearch = (term: string) => {
254
+ setSearchTerm(term);
255
+ setPage(1);
256
+ };
257
+
258
+ const nextPage = () => result.hasNext && setPage((p) => p + 1);
259
+ const prevPage = () => result.hasPrev && setPage((p) => Math.max(1, p - 1));
260
+
261
+ return {
262
+ searchTerm,
263
+ setSearch,
264
+ page,
265
+ nextPage,
266
+ prevPage,
267
+ pageItems: result.pageItems,
268
+ filteredIds: filtered.map((c) => c.id),
269
+ from: result.from,
270
+ to: result.to,
271
+ total: result.total,
272
+ hasPrev: result.hasPrev,
273
+ hasNext: result.hasNext,
274
+ };
275
+ }
276
+ ```
277
+
278
+ - [ ] **Step 2: Export from the hooks barrel**
279
+
280
+ In `src/features/contracts/hooks/index.ts` add:
281
+
282
+ ```ts
283
+ export { useContractListing } from "./useContractListing";
284
+ export type { UseContractListingReturn } from "./useContractListing";
285
+ ```
286
+
287
+ - [ ] **Step 3: Typecheck the new files**
288
+
289
+ Run: `yarn typecheck 2>&1 | grep -E "useContractListing|contractListingHelpers" || echo "clean"`
290
+ Expected: `clean`
291
+
292
+ - [ ] **Step 4: Commit**
293
+
294
+ ```bash
295
+ git add src/features/contracts/hooks/useContractListing.ts src/features/contracts/hooks/index.ts
296
+ git commit -m "feat(suma): add useContractListing hook for local search + pagination"
297
+ ```
298
+
299
+ ---
300
+
301
+ ### Task 3: `ContractListItem` — split checkbox (select) from row (switch)
302
+
303
+ **Files:**
304
+ - Modify: `src/features/contracts/components/ContractListItem/ContractListItem.tsx`
305
+
306
+ **Interfaces:**
307
+ - Produces (new prop contract):
308
+ ```ts
309
+ type ContractListItemProps = {
310
+ contract: ContractData;
311
+ isSelected: boolean;
312
+ orgUnitId: string;
313
+ onSelect: (id: string, event: React.MouseEvent) => void; // replaces onModifierClick/onShiftClick
314
+ };
315
+ ```
316
+ - Consumes: `useSwitchContract` (existing), `useUI` (existing).
317
+
318
+ - [ ] **Step 1: Replace the props and handlers**
319
+
320
+ Full new file content:
321
+
322
+ ```tsx
323
+ import { useUI } from "@faststore/ui";
324
+
325
+ import { LetterHighlight } from "../../../shared/components";
326
+ import Icon from "../../../shared/components/Icon/Icon";
327
+ import { useSwitchContract } from "../../hooks";
328
+
329
+ import type { ContractData } from "../../types";
330
+
331
+ export type ContractListItemProps = {
332
+ contract: ContractData;
333
+ isSelected: boolean;
334
+ orgUnitId: string;
335
+ onSelect: (id: string, event: React.MouseEvent) => void;
336
+ };
337
+
338
+ export const ContractListItem = ({
339
+ contract,
340
+ isSelected,
341
+ orgUnitId,
342
+ onSelect,
343
+ }: ContractListItemProps) => {
344
+ const displayName = contract.name ?? contract.email;
345
+ const { pushToast } = useUI();
346
+ const { switchContract, loading } = useSwitchContract();
347
+
348
+ const handleCheckboxClick = (event: React.MouseEvent) => {
349
+ event.stopPropagation();
350
+ event.preventDefault();
351
+ onSelect(contract.id, event);
352
+ };
353
+
354
+ const handleRowClick = async () => {
355
+ if (loading) return;
356
+ const switched = await switchContract(contract.id, { orgUnitId });
357
+ if (!switched) {
358
+ pushToast({
359
+ message: "Failed to switch contract. Please try again.",
360
+ status: "ERROR",
361
+ });
362
+ }
363
+ };
364
+
365
+ return (
366
+ <div
367
+ data-fs-bp-contract-list-item
368
+ data-fs-bp-contract-list-item-selected={isSelected || undefined}
369
+ data-fs-bp-contract-list-item-loading={loading || undefined}
370
+ onClick={handleRowClick}
371
+ role="row"
372
+ aria-selected={isSelected}
373
+ aria-busy={loading || undefined}
374
+ >
375
+ <div
376
+ data-fs-bp-contract-list-item-checkbox
377
+ role="checkbox"
378
+ aria-checked={isSelected}
379
+ aria-label={`Select ${displayName ?? "contract"}`}
380
+ tabIndex={0}
381
+ onClick={handleCheckboxClick}
382
+ >
383
+ {isSelected && <Icon name="Check" width={12} height={12} />}
384
+ </div>
385
+
386
+ <LetterHighlight size="small" letter={displayName ?? ""} />
387
+
388
+ <span data-fs-bp-contract-list-item-name>{displayName}</span>
389
+
390
+ {contract.isDefault && (
391
+ <span
392
+ data-fs-bp-contract-list-item-default-badge
393
+ aria-label="Default contract"
394
+ >
395
+ <Icon name="Default" width={20} height={20} />
396
+ </span>
397
+ )}
398
+ </div>
399
+ );
400
+ };
401
+ ```
402
+
403
+ - [ ] **Step 2: Lint + typecheck this file**
404
+
405
+ Run: `yarn lint 2>&1 | tail -3 && yarn typecheck 2>&1 | grep ContractListItem || echo "clean"`
406
+ Expected: lint `Done`, typecheck `clean`
407
+
408
+ - [ ] **Step 3: Commit**
409
+
410
+ ```bash
411
+ git add src/features/contracts/components/ContractListItem/ContractListItem.tsx
412
+ git commit -m "feat(suma): split contract row checkbox-select from row-switch"
413
+ ```
414
+
415
+ ---
416
+
417
+ ### Task 4: `ContractsListHeader` — search + local pagination
418
+
419
+ **Files:**
420
+ - Create: `src/features/contracts/components/ContractsListHeader/ContractsListHeader.tsx`
421
+ - Create: `src/features/contracts/components/ContractsListHeader/index.ts`
422
+
423
+ **Interfaces:**
424
+ - Consumes: `InternalSearch` (`src/features/shared/components`), `Icon` (`@faststore/ui`).
425
+ - Produces:
426
+ ```ts
427
+ type ContractsListHeaderProps = {
428
+ searchTerm: string;
429
+ onSearch: (term: string) => void;
430
+ from: number;
431
+ to: number;
432
+ total: number;
433
+ hasPrev: boolean;
434
+ hasNext: boolean;
435
+ onPrev: () => void;
436
+ onNext: () => void;
437
+ };
438
+ ```
439
+
440
+ - [ ] **Step 1: Write the component**
441
+
442
+ `ContractsListHeader.tsx`:
443
+
444
+ ```tsx
445
+ import { Icon } from "@faststore/ui";
446
+
447
+ import { InternalSearch } from "../../../shared/components";
448
+
449
+ export type ContractsListHeaderProps = {
450
+ searchTerm: string;
451
+ onSearch: (term: string) => void;
452
+ from: number;
453
+ to: number;
454
+ total: number;
455
+ hasPrev: boolean;
456
+ hasNext: boolean;
457
+ onPrev: () => void;
458
+ onNext: () => void;
459
+ };
460
+
461
+ export const ContractsListHeader = ({
462
+ searchTerm,
463
+ onSearch,
464
+ from,
465
+ to,
466
+ total,
467
+ hasPrev,
468
+ hasNext,
469
+ onPrev,
470
+ onNext,
471
+ }: ContractsListHeaderProps) => {
472
+ return (
473
+ <div data-fs-bp-contracts-list-header>
474
+ <div data-fs-bp-contracts-list-header-search>
475
+ <InternalSearch defaultValue={searchTerm} textSearch={onSearch} />
476
+ </div>
477
+
478
+ <div data-fs-bp-contracts-list-header-pagination>
479
+ <span data-fs-bp-contracts-list-header-interval>
480
+ {from} — {to} of {total}
481
+ </span>
482
+ <button
483
+ type="button"
484
+ data-fs-bp-contracts-list-header-prev
485
+ onClick={onPrev}
486
+ disabled={!hasPrev}
487
+ aria-label="Previous page"
488
+ >
489
+ <Icon name="ChevronRight" width={20} height={20} />
490
+ </button>
491
+ <button
492
+ type="button"
493
+ data-fs-bp-contracts-list-header-next
494
+ onClick={onNext}
495
+ disabled={!hasNext}
496
+ aria-label="Next page"
497
+ >
498
+ <Icon name="ChevronRight" width={20} height={20} />
499
+ </button>
500
+ </div>
501
+ </div>
502
+ );
503
+ };
504
+ ```
505
+
506
+ `index.ts`:
507
+
508
+ ```ts
509
+ export { ContractsListHeader } from "./ContractsListHeader";
510
+ export type { ContractsListHeaderProps } from "./ContractsListHeader";
511
+ ```
512
+
513
+ - [ ] **Step 2: Lint + typecheck**
514
+
515
+ Run: `yarn lint 2>&1 | tail -3 && yarn typecheck 2>&1 | grep ContractsListHeader || echo "clean"`
516
+ Expected: lint `Done`, typecheck `clean`
517
+
518
+ - [ ] **Step 3: Commit**
519
+
520
+ ```bash
521
+ git add src/features/contracts/components/ContractsListHeader/
522
+ git commit -m "feat(suma): add contracts list header with search + pagination"
523
+ ```
524
+
525
+ ---
526
+
527
+ ### Task 5: `ContractSelectAllRow` — tri-state select all
528
+
529
+ **Files:**
530
+ - Create: `src/features/contracts/components/ContractSelectAllRow/ContractSelectAllRow.tsx`
531
+ - Create: `src/features/contracts/components/ContractSelectAllRow/index.ts`
532
+
533
+ **Interfaces:**
534
+ - Produces:
535
+ ```ts
536
+ type ContractSelectAllRowProps = {
537
+ total: number; // count of filtered contracts (label + all-selected check)
538
+ selectedCount: number; // how many of the filtered are selected
539
+ onToggle: () => void; // layout decides select-all vs clear
540
+ };
541
+ ```
542
+
543
+ - [ ] **Step 1: Write the component**
544
+
545
+ `ContractSelectAllRow.tsx`:
546
+
547
+ ```tsx
548
+ import Icon from "../../../shared/components/Icon/Icon";
549
+
550
+ export type ContractSelectAllRowProps = {
551
+ total: number;
552
+ selectedCount: number;
553
+ onToggle: () => void;
554
+ };
555
+
556
+ export const ContractSelectAllRow = ({
557
+ total,
558
+ selectedCount,
559
+ onToggle,
560
+ }: ContractSelectAllRowProps) => {
561
+ const allSelected = total > 0 && selectedCount >= total;
562
+ const indeterminate = selectedCount > 0 && !allSelected;
563
+
564
+ return (
565
+ <div data-fs-bp-contract-select-all-row onClick={onToggle}>
566
+ <div
567
+ data-fs-bp-contract-select-all-checkbox
568
+ data-fs-bp-contract-select-all-checkbox-checked={allSelected || undefined}
569
+ data-fs-bp-contract-select-all-checkbox-indeterminate={
570
+ indeterminate || undefined
571
+ }
572
+ role="checkbox"
573
+ aria-checked={indeterminate ? "mixed" : allSelected}
574
+ aria-label="Select all contracts"
575
+ tabIndex={0}
576
+ >
577
+ {allSelected && <Icon name="Check" width={12} height={12} />}
578
+ </div>
579
+ <span data-fs-bp-contract-select-all-label>Select all ({total})</span>
580
+ </div>
581
+ );
582
+ };
583
+ ```
584
+
585
+ (The indeterminate state renders a CSS minus bar via `[data-fs-bp-contract-select-all-checkbox-indeterminate]` — Task 7.)
586
+
587
+ `index.ts`:
588
+
589
+ ```ts
590
+ export { ContractSelectAllRow } from "./ContractSelectAllRow";
591
+ export type { ContractSelectAllRowProps } from "./ContractSelectAllRow";
592
+ ```
593
+
594
+ - [ ] **Step 2: Lint + typecheck**
595
+
596
+ Run: `yarn lint 2>&1 | tail -3 && yarn typecheck 2>&1 | grep ContractSelectAllRow || echo "clean"`
597
+ Expected: lint `Done`, typecheck `clean`
598
+
599
+ - [ ] **Step 3: Commit**
600
+
601
+ ```bash
602
+ git add src/features/contracts/components/ContractSelectAllRow/
603
+ git commit -m "feat(suma): add tri-state select-all row"
604
+ ```
605
+
606
+ ---
607
+
608
+ ### Task 6: `ContractSelectionBar` — floating selection pill
609
+
610
+ **Files:**
611
+ - Create: `src/features/contracts/components/ContractSelectionBar/ContractSelectionBar.tsx`
612
+ - Create: `src/features/contracts/components/ContractSelectionBar/index.ts`
613
+
614
+ **Interfaces:**
615
+ - Produces:
616
+ ```ts
617
+ type ContractSelectionBarProps = {
618
+ selectedCount: number;
619
+ onClear: () => void;
620
+ onRemove: () => void; // no-op placeholder from the layout for now
621
+ canRemove?: boolean; // hide Remove for single-contract orgs (default true)
622
+ };
623
+ ```
624
+
625
+ - [ ] **Step 1: Write the component**
626
+
627
+ `ContractSelectionBar.tsx`:
628
+
629
+ ```tsx
630
+ import { Icon as UIIcon } from "@faststore/ui";
631
+
632
+ import Icon from "../../../shared/components/Icon/Icon";
633
+
634
+ export type ContractSelectionBarProps = {
635
+ selectedCount: number;
636
+ onClear: () => void;
637
+ onRemove: () => void;
638
+ canRemove?: boolean;
639
+ };
640
+
641
+ export const ContractSelectionBar = ({
642
+ selectedCount,
643
+ onClear,
644
+ onRemove,
645
+ canRemove = true,
646
+ }: ContractSelectionBarProps) => {
647
+ if (selectedCount === 0) return null;
648
+
649
+ return (
650
+ <div data-fs-bp-contract-selection-bar role="toolbar">
651
+ <button
652
+ type="button"
653
+ data-fs-bp-contract-selection-bar-clear
654
+ onClick={onClear}
655
+ aria-label="Clear selection"
656
+ >
657
+ <UIIcon name="X" width={20} height={20} />
658
+ </button>
659
+
660
+ <span data-fs-bp-contract-selection-bar-count>
661
+ {selectedCount} selected
662
+ </span>
663
+
664
+ {canRemove && (
665
+ <button
666
+ type="button"
667
+ data-fs-bp-contract-selection-bar-remove
668
+ onClick={onRemove}
669
+ >
670
+ <Icon name="MinusCircle" width={20} height={20} />
671
+ Remove
672
+ </button>
673
+ )}
674
+ </div>
675
+ );
676
+ };
677
+ ```
678
+
679
+ `index.ts`:
680
+
681
+ ```ts
682
+ export { ContractSelectionBar } from "./ContractSelectionBar";
683
+ export type { ContractSelectionBarProps } from "./ContractSelectionBar";
684
+ ```
685
+
686
+ - [ ] **Step 2: Lint + typecheck**
687
+
688
+ Run: `yarn lint 2>&1 | tail -3 && yarn typecheck 2>&1 | grep ContractSelectionBar || echo "clean"`
689
+ Expected: lint `Done`, typecheck `clean`
690
+
691
+ - [ ] **Step 3: Commit**
692
+
693
+ ```bash
694
+ git add src/features/contracts/components/ContractSelectionBar/
695
+ git commit -m "feat(suma): add floating contract selection bar"
696
+ ```
697
+
698
+ ---
699
+
700
+ ### Task 7: Wire it together — `ContractSelectionList` + `ContractListingLayout` + SCSS
701
+
702
+ **Files:**
703
+ - Modify: `src/features/contracts/components/ContractSelectionList/ContractSelectionList.tsx`
704
+ - Modify: `src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx`
705
+ - Modify: `src/features/contracts/layouts/ContractListingLayout/contract-listing-layout.scss`
706
+
707
+ **Interfaces:**
708
+ - Consumes: `useContractListing` (Task 2), `useContractSelection` (existing),
709
+ `ContractsListHeader` (4), `ContractSelectAllRow` (5), `ContractSelectionBar` (6),
710
+ `ContractListItem` (3, `onSelect`).
711
+
712
+ - [ ] **Step 1: Slim `ContractSelectionList` to render page items + onSelect**
713
+
714
+ Full new content:
715
+
716
+ ```tsx
717
+ import { ContractListItem } from "../ContractListItem/ContractListItem";
718
+
719
+ import type { ContractData } from "../../types";
720
+
721
+ export type ContractSelectionListProps = {
722
+ contracts: ContractData[];
723
+ orgUnitId: string;
724
+ selectedIds: Set<string>;
725
+ onKeyDown: (event: React.KeyboardEvent) => void;
726
+ onSelect: (id: string, event: React.MouseEvent) => void;
727
+ };
728
+
729
+ export const ContractSelectionList = ({
730
+ contracts,
731
+ orgUnitId,
732
+ selectedIds,
733
+ onKeyDown,
734
+ onSelect,
735
+ }: ContractSelectionListProps) => {
736
+ if (contracts.length === 0) {
737
+ return (
738
+ <div data-fs-bp-contract-selection-list-empty>
739
+ <p>No contracts found for this organizational unit.</p>
740
+ </div>
741
+ );
742
+ }
743
+
744
+ return (
745
+ <div
746
+ data-fs-bp-contract-selection-list
747
+ onKeyDown={onKeyDown}
748
+ tabIndex={0}
749
+ role="grid"
750
+ aria-label="Contracts"
751
+ >
752
+ <div data-fs-bp-contract-selection-list-rows>
753
+ {contracts.map((contract) => (
754
+ <ContractListItem
755
+ key={contract.id}
756
+ contract={contract}
757
+ isSelected={selectedIds.has(contract.id)}
758
+ orgUnitId={orgUnitId}
759
+ onSelect={onSelect}
760
+ />
761
+ ))}
762
+ </div>
763
+ </div>
764
+ );
765
+ };
766
+ ```
767
+
768
+ - [ ] **Step 2: Update `ContractListingLayout` imports**
769
+
770
+ Add to the contracts component imports and hooks import:
771
+
772
+ ```tsx
773
+ import { ContractSelectionList } from "../../components/ContractSelectionList/ContractSelectionList";
774
+ import { ContractsListHeader } from "../../components/ContractsListHeader";
775
+ import { ContractSelectAllRow } from "../../components/ContractSelectAllRow";
776
+ import { ContractSelectionBar } from "../../components/ContractSelectionBar";
777
+ import {
778
+ useContractSelection,
779
+ useContractSwitchChannel,
780
+ useContractListing,
781
+ } from "../../hooks";
782
+ ```
783
+
784
+ - [ ] **Step 3: Replace selection wiring in `ContractListingLayout`**
785
+
786
+ Replace the `handleModifierClickWithContext` / `handleShiftClickWithContext` block and the destructure of `useContractSelection` with listing + selection wiring driven by filtered IDs:
787
+
788
+ ```tsx
789
+ const {
790
+ selectedIds,
791
+ handleModifierClick,
792
+ handleShiftClick,
793
+ handleSelectAll,
794
+ clearSelection,
795
+ } = useContractSelection();
796
+
797
+ const {
798
+ searchTerm,
799
+ setSearch,
800
+ pageItems,
801
+ filteredIds,
802
+ from,
803
+ to,
804
+ total,
805
+ hasPrev,
806
+ hasNext,
807
+ nextPage,
808
+ prevPage,
809
+ } = useContractListing({ contracts });
810
+
811
+ const selectedInFiltered = filteredIds.filter((id) =>
812
+ selectedIds.has(id)
813
+ ).length;
814
+
815
+ const handleSelect = (id: string, event: React.MouseEvent) => {
816
+ if (event.shiftKey) {
817
+ handleShiftClick(id, filteredIds);
818
+ } else {
819
+ handleModifierClick(id);
820
+ }
821
+ };
822
+
823
+ const handleToggleSelectAll = () => {
824
+ if (selectedInFiltered > 0) {
825
+ clearSelection();
826
+ } else {
827
+ handleSelectAll(filteredIds);
828
+ }
829
+ };
830
+
831
+ const handleKeyDown = (event: React.KeyboardEvent) => {
832
+ if ((event.metaKey || event.ctrlKey) && event.key === "a") {
833
+ event.preventDefault();
834
+ handleSelectAll(filteredIds);
835
+ }
836
+ };
837
+ ```
838
+
839
+ (Remove the now-unused `allIds` const and old handlers.)
840
+
841
+ - [ ] **Step 4: Replace the multi-contract JSX branch**
842
+
843
+ Replace the `<ContractSelectionList .../>` branch (the `: (` else of `isSingleContract`) with:
844
+
845
+ ```tsx
846
+ <>
847
+ <ContractsListHeader
848
+ searchTerm={searchTerm}
849
+ onSearch={setSearch}
850
+ from={from}
851
+ to={to}
852
+ total={total}
853
+ hasPrev={hasPrev}
854
+ hasNext={hasNext}
855
+ onPrev={prevPage}
856
+ onNext={nextPage}
857
+ />
858
+ <ContractSelectAllRow
859
+ total={total}
860
+ selectedCount={selectedInFiltered}
861
+ onToggle={handleToggleSelectAll}
862
+ />
863
+ <ContractSelectionList
864
+ contracts={pageItems}
865
+ orgUnitId={orgUnit.id}
866
+ selectedIds={selectedIds}
867
+ onKeyDown={handleKeyDown}
868
+ onSelect={handleSelect}
869
+ />
870
+ <ContractSelectionBar
871
+ selectedCount={selectedIds.size}
872
+ onClear={clearSelection}
873
+ onRemove={() => {
874
+ // TODO: bulk contract removal (future feature)
875
+ }}
876
+ />
877
+ </>
878
+ ```
879
+
880
+ - [ ] **Step 5: Add SCSS**
881
+
882
+ In `contract-listing-layout.scss`, inside `[data-fs-bp-contract-listing-section] { … }`, replace the old `[data-fs-bp-contract-selection-toolbar]` block with the following new blocks (header, select-all row, indeterminate checkbox, floating bar):
883
+
884
+ ```scss
885
+ [data-fs-bp-contracts-list-header] {
886
+ display: flex;
887
+ align-items: center;
888
+ justify-content: space-between;
889
+ gap: var(--fs-bp-gap-4);
890
+ padding: var(--fs-bp-padding-4) 0;
891
+
892
+ [data-fs-bp-contracts-list-header-search] {
893
+ flex: 1;
894
+ max-width: 22rem;
895
+ }
896
+
897
+ [data-fs-bp-contracts-list-header-pagination] {
898
+ display: flex;
899
+ align-items: center;
900
+ gap: var(--fs-bp-gap-2);
901
+
902
+ [data-fs-bp-contracts-list-header-interval] {
903
+ font-size: var(--fs-bp-text-body);
904
+ color: var(--fs-bp-color-neutral-7);
905
+ white-space: nowrap;
906
+ }
907
+
908
+ button {
909
+ display: inline-flex;
910
+ align-items: center;
911
+ justify-content: center;
912
+ background: none;
913
+ border: none;
914
+ padding: var(--fs-bp-padding-1);
915
+ cursor: pointer;
916
+ color: var(--fs-bp-color-neutral-8);
917
+ border-radius: var(--fs-bp-border-radius-full);
918
+
919
+ &:hover:not(:disabled) {
920
+ background-color: var(--fs-bp-color-neutral-1);
921
+ }
922
+
923
+ &:disabled {
924
+ color: var(--fs-bp-color-neutral-3);
925
+ cursor: default;
926
+ }
927
+ }
928
+
929
+ [data-fs-bp-contracts-list-header-prev] [data-fs-icon] {
930
+ transform: rotate(180deg);
931
+ }
932
+ }
933
+ }
934
+
935
+ [data-fs-bp-contract-select-all-row] {
936
+ display: flex;
937
+ align-items: center;
938
+ gap: var(--fs-bp-gap-3);
939
+ padding: var(--fs-bp-padding-3) var(--fs-bp-padding-2);
940
+ cursor: pointer;
941
+ user-select: none;
942
+
943
+ [data-fs-bp-contract-select-all-checkbox] {
944
+ flex-shrink: 0;
945
+ width: 1.125rem;
946
+ height: 1.125rem;
947
+ border: 2px solid #9e9e9e;
948
+ border-radius: 2px;
949
+ display: flex;
950
+ align-items: center;
951
+ justify-content: center;
952
+ color: var(--fs-bp-color-white);
953
+
954
+ &[data-fs-bp-contract-select-all-checkbox-checked],
955
+ &[data-fs-bp-contract-select-all-checkbox-indeterminate] {
956
+ background-color: var(--fs-bp-color-brand);
957
+ border-color: var(--fs-bp-color-brand);
958
+ }
959
+
960
+ &[data-fs-bp-contract-select-all-checkbox-indeterminate]::after {
961
+ content: "";
962
+ width: 0.625rem;
963
+ height: 2px;
964
+ background-color: var(--fs-bp-color-white);
965
+ border-radius: 1px;
966
+ }
967
+ }
968
+
969
+ [data-fs-bp-contract-select-all-label] {
970
+ font-size: var(--fs-bp-text-body);
971
+ font-weight: var(--fs-bp-weight-medium);
972
+ color: var(--fs-bp-color-neutral-7);
973
+ }
974
+ }
975
+
976
+ [data-fs-bp-contract-list-item-checkbox] {
977
+ cursor: pointer;
978
+
979
+ &:focus-visible {
980
+ outline: 2px solid var(--fs-bp-color-brand);
981
+ outline-offset: 2px;
982
+ }
983
+ }
984
+
985
+ [data-fs-bp-contract-selection-bar] {
986
+ position: fixed;
987
+ left: 50%;
988
+ bottom: var(--fs-bp-spacing-6);
989
+ transform: translateX(-50%);
990
+ z-index: 10;
991
+ display: flex;
992
+ align-items: center;
993
+ gap: var(--fs-bp-gap-6);
994
+ padding: var(--fs-bp-padding-3) var(--fs-bp-padding-6);
995
+ background-color: var(--fs-bp-color-white);
996
+ border-radius: var(--fs-bp-border-radius-full);
997
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.16);
998
+
999
+ [data-fs-bp-contract-selection-bar-clear] {
1000
+ display: inline-flex;
1001
+ background: none;
1002
+ border: none;
1003
+ padding: 0;
1004
+ cursor: pointer;
1005
+ color: var(--fs-bp-color-neutral-7);
1006
+ }
1007
+
1008
+ [data-fs-bp-contract-selection-bar-count] {
1009
+ font-size: var(--fs-bp-text-body);
1010
+ font-weight: var(--fs-bp-weight-medium);
1011
+ color: var(--fs-bp-color-black);
1012
+ }
1013
+
1014
+ [data-fs-bp-contract-selection-bar-remove] {
1015
+ display: inline-flex;
1016
+ align-items: center;
1017
+ gap: var(--fs-bp-gap-2);
1018
+ background: none;
1019
+ border: none;
1020
+ padding: 0;
1021
+ cursor: pointer;
1022
+ font-size: var(--fs-bp-text-body);
1023
+ font-weight: var(--fs-bp-weight-medium);
1024
+ color: #d32f2f;
1025
+
1026
+ &:hover {
1027
+ text-decoration: underline;
1028
+ }
1029
+ }
1030
+ }
1031
+ ```
1032
+
1033
+ - [ ] **Step 6: Full check**
1034
+
1035
+ Run: `yarn lint 2>&1 | tail -3 && yarn vitest run 2>&1 | tail -6 && yarn typecheck 2>&1 | grep -E "contracts/(components|layouts|hooks)" | grep -vE "pre-existing" || echo "no new contracts errors"`
1036
+ Expected: lint `Done`, all tests pass, no new typecheck errors in touched contracts files.
1037
+
1038
+ - [ ] **Step 7: Commit**
1039
+
1040
+ ```bash
1041
+ git add src/features/contracts/
1042
+ git commit -m "feat(suma): wire contracts listing search, pagination, selection UI"
1043
+ ```
1044
+
1045
+ ---
1046
+
1047
+ ### Task 8: Final verification + changelog
1048
+
1049
+ **Files:**
1050
+ - Possibly: changelog entry (per repo convention — check `docs`/CI `changelog.yml`).
1051
+
1052
+ - [ ] **Step 1: Run the full gate**
1053
+
1054
+ Run: `make check 2>&1 | tail -30`
1055
+ Expected: lint + tests pass; typecheck error count equals the pre-existing baseline (133) with **no** entries referencing the new/edited contracts files.
1056
+
1057
+ - [ ] **Step 2: Confirm no new typecheck errors in touched files**
1058
+
1059
+ Run: `yarn typecheck 2>&1 | grep -E "contractListingHelpers|useContractListing|ContractListItem|ContractsListHeader|ContractSelectAllRow|ContractSelectionBar|ContractSelectionList|ContractListingLayout" || echo "TOUCHED FILES CLEAN"`
1060
+ Expected: `TOUCHED FILES CLEAN`
1061
+
1062
+ - [ ] **Step 3: Manual verification (when linked to `b2bfaststoredev.store`)**
1063
+
1064
+ Walk the 9 checks in the design doc's "Manual verification" section: pagination, search-resets-page, checkbox-selects-without-switch, shift-range, tri-state select-all + clear, selection persistence across page/search, row-body switch still works, Remove inert.
1065
+
1066
+ ## Self-Review
1067
+
1068
+ - **Spec coverage:** click split (Task 3) · search (2,4) · pagination 25/prev-next (1,2,4) · select-all all-filtered + indeterminate (5,7) · floating bar + no-op Remove (6,7) · persistence (selection Set independent of paging, 7) · styling (7) · tests for helpers (1). All spec sections covered.
1069
+ - **Placeholder scan:** the only intentional placeholder is the Remove `onClick` no-op, explicitly required by the spec and marked with a TODO comment. No vague steps.
1070
+ - **Type consistency:** `onSelect(id, event)` is defined in Task 3 and consumed identically in Tasks 7. `filteredIds: string[]`, `from/to/total/hasPrev/hasNext` from Task 2 match the props passed in Task 7. `selectedCount`/`total`/`onToggle` (Task 5) and `selectedCount`/`onClear`/`onRemove` (Task 6) match the layout wiring in Task 7.