+
@@ -143,7 +177,7 @@
/>
-
⚙️
Moderate Record
+
-
-
-
-
\ No newline at end of file
+
diff --git a/src/components/TimeLists/TimesListItemColumn.vue b/src/components/TimeLists/TimesListItemColumn.vue
new file mode 100644
index 0000000..d6145fc
--- /dev/null
+++ b/src/components/TimeLists/TimesListItemColumn.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
{{ props.time.rank }}.
+
+
+ ({{ props.time.time - wrTime > 0 ? '+' : '-' }}{{ dateTimeFormats.time(Math.abs(props.time.time - wrTime)) }})
+
+
+
+
+
diff --git a/src/components/TimeLists/TimesListItemMoreDetails.vue b/src/components/TimeLists/TimesListItemMoreDetails.vue
new file mode 100644
index 0000000..8a11f00
--- /dev/null
+++ b/src/components/TimeLists/TimesListItemMoreDetails.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+ {{ col.label }}:
+
+
+
+ {{ col.data === 'sync' ? props.time[col.data].toFixed(2) + '%' : props.time[col.data] }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/TimesListPagination.vue b/src/components/TimeLists/TimesListPagination.vue
similarity index 76%
rename from src/components/TimesListPagination.vue
rename to src/components/TimeLists/TimesListPagination.vue
index e5a1f55..ffa5036 100644
--- a/src/components/TimesListPagination.vue
+++ b/src/components/TimeLists/TimesListPagination.vue
@@ -1,21 +1,27 @@
+
+
+
+
+
+
+
+
+
+ {{ props.sort }}
+
+
+
+
+
+
+
+
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ invalidatedLabel }}
+
+
+
+
+
+
+
+
+ {{ option.label }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/TimesList.vue b/src/components/TimesList.vue
deleted file mode 100644
index 6e8da3c..0000000
--- a/src/components/TimesList.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/components/TimesListHeading.vue b/src/components/TimesListHeading.vue
deleted file mode 100644
index fbd4d98..0000000
--- a/src/components/TimesListHeading.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/components/TimesListItemContent.vue b/src/components/TimesListItemContent.vue
deleted file mode 100644
index 09898fa..0000000
--- a/src/components/TimesListItemContent.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
-
-
- {{ props.time.rank }}.
-
-
-
-
-
- {{ data }}
-
-
-
-
-
- ({{ props.time.time - wrTime > 0 ? '+' : '-' }}{{ dateTimeFormats.time(Math.abs(props.time.time - wrTime)) }})
-
-
-
-
\ No newline at end of file
diff --git a/src/components/TimesListItemMoreDetails.vue b/src/components/TimesListItemMoreDetails.vue
deleted file mode 100644
index 34b5bc8..0000000
--- a/src/components/TimesListItemMoreDetails.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
- {{ col.label }}:
-
-
-
- {{ col.data === 'sync' ? props.time[col.data].toFixed(2) + '%' : props.time[col.data] }}
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/components/ValidityIndicator.vue b/src/components/ValidityIndicator.vue
deleted file mode 100644
index 185ad52..0000000
--- a/src/components/ValidityIndicator.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
- {{ indicatorSymbol }}
-
-
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