+
⚙️
Moderate Record
+
diff --git a/src/composables/useBulkSelection.ts b/src/composables/useBulkSelection.ts
new file mode 100644
index 0000000..14f9366
--- /dev/null
+++ b/src/composables/useBulkSelection.ts
@@ -0,0 +1,156 @@
+import { ref, computed } from 'vue'
+import type { Ref } from 'vue'
+
+// Heavy bulk: cross-page record selection that survives page navigation
+// (sessionStorage) and named "saved selections" that persist across sessions
+// (localStorage). One module-level store; every consumer of useBulkSelection
+// sees the same set, which is the point — selections accrue as the moderator
+// scrolls through different lists.
+
+export interface BulkRecordEntry {
+ id: string
+ // Human-readable label for the tray; should be self-contained without
+ // needing the original list context (e.g. "player on map · 12.345s").
+ label: string
+}
+
+export interface SavedSelection {
+ name: string
+ saved_at: number
+ entries: BulkRecordEntry[]
+}
+
+const SESSION_KEY = 'mod-bulk-selection-v1'
+const SAVED_KEY = 'mod-bulk-saved-v1'
+
+const loadSession = (): Map
=> {
+ try {
+ const raw = sessionStorage.getItem(SESSION_KEY)
+ if (!raw) return new Map()
+ const arr: BulkRecordEntry[] = JSON.parse(raw)
+ return new Map(arr.map(e => [e.id, e]))
+ } catch {
+ return new Map()
+ }
+}
+
+const loadSaved = (): SavedSelection[] => {
+ try {
+ const raw = localStorage.getItem(SAVED_KEY)
+ if (!raw) return []
+ return JSON.parse(raw)
+ } catch {
+ return []
+ }
+}
+
+const entries: Ref