@weng-lab/genomebrowser-ui 0.3.6 → 0.4.0-beta.1

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 (91) hide show
  1. package/.env.local +1 -1
  2. package/dist/TrackSelect/Folders/biosamples/shared/BiosampleViewSelector.d.ts +7 -0
  3. package/dist/TrackSelect/Folders/biosamples/shared/createFolder.d.ts +1 -13
  4. package/dist/TrackSelect/Folders/biosamples/shared/toTrack.d.ts +20 -0
  5. package/dist/TrackSelect/Folders/biosamples/shared/types.d.ts +4 -13
  6. package/dist/TrackSelect/Folders/genes/shared/columns.d.ts +2 -2
  7. package/dist/TrackSelect/Folders/genes/shared/createFolder.d.ts +1 -3
  8. package/dist/TrackSelect/Folders/genes/shared/toTrack.d.ts +18 -0
  9. package/dist/TrackSelect/Folders/genes/shared/types.d.ts +2 -0
  10. package/dist/TrackSelect/Folders/index.d.ts +6 -12
  11. package/dist/TrackSelect/Folders/mohd/data/human.json.d.ts +2948 -0
  12. package/dist/TrackSelect/Folders/mohd/human.d.ts +1 -0
  13. package/dist/TrackSelect/Folders/mohd/shared/MohdGroupingCell.d.ts +2 -0
  14. package/dist/TrackSelect/Folders/mohd/shared/MohdTreeItem.d.ts +3 -0
  15. package/dist/TrackSelect/Folders/mohd/shared/MohdViewSelector.d.ts +7 -0
  16. package/dist/TrackSelect/Folders/mohd/shared/columns.d.ts +5 -0
  17. package/dist/TrackSelect/Folders/mohd/shared/config.d.ts +42 -0
  18. package/dist/TrackSelect/Folders/mohd/shared/createFolder.d.ts +9 -0
  19. package/dist/TrackSelect/Folders/mohd/shared/toTrack.d.ts +9 -0
  20. package/dist/TrackSelect/Folders/mohd/shared/types.d.ts +40 -0
  21. package/dist/TrackSelect/Folders/other-tracks/shared/toTrack.d.ts +5 -0
  22. package/dist/TrackSelect/Folders/other-tracks/shared/types.d.ts +1 -0
  23. package/dist/TrackSelect/Folders/types.d.ts +23 -55
  24. package/dist/TrackSelect/TrackSelect.d.ts +10 -7
  25. package/dist/TrackSelect/TreeView/TreeViewWrapper.d.ts +1 -1
  26. package/dist/TrackSelect/buildSelectedTree.d.ts +15 -0
  27. package/dist/TrackSelect/managedTracks.d.ts +13 -0
  28. package/dist/TrackSelect/resolveFolderView.d.ts +2 -0
  29. package/dist/TrackSelect/trackContext.d.ts +5 -0
  30. package/dist/TrackSelect/types.d.ts +12 -33
  31. package/dist/genomebrowser-ui.es.js +2231 -1732
  32. package/dist/genomebrowser-ui.es.js.map +1 -1
  33. package/dist/lib.d.ts +4 -4
  34. package/dist/muiLicense.d.ts +1 -0
  35. package/package.json +6 -3
  36. package/src/TrackSelect/Dialogs/ClearDialog.tsx +3 -8
  37. package/src/TrackSelect/Dialogs/ResetDialog.tsx +5 -4
  38. package/src/TrackSelect/FolderList/FolderCard.tsx +1 -1
  39. package/src/TrackSelect/Folders/biosamples/shared/BiosampleViewSelector.tsx +33 -0
  40. package/src/TrackSelect/Folders/biosamples/shared/createFolder.ts +39 -58
  41. package/src/TrackSelect/Folders/biosamples/shared/toTrack.ts +138 -0
  42. package/src/TrackSelect/Folders/biosamples/shared/types.ts +4 -16
  43. package/src/TrackSelect/Folders/genes/shared/columns.tsx +2 -2
  44. package/src/TrackSelect/Folders/genes/shared/createFolder.ts +11 -31
  45. package/src/TrackSelect/Folders/genes/shared/toTrack.ts +59 -0
  46. package/src/TrackSelect/Folders/genes/shared/types.ts +2 -0
  47. package/src/TrackSelect/Folders/index.ts +14 -17
  48. package/src/TrackSelect/Folders/mohd/data/human.json +2945 -0
  49. package/src/TrackSelect/Folders/mohd/human.ts +10 -0
  50. package/src/TrackSelect/Folders/mohd/shared/MohdGroupingCell.tsx +68 -0
  51. package/src/TrackSelect/Folders/mohd/shared/MohdTreeItem.tsx +17 -0
  52. package/src/TrackSelect/Folders/mohd/shared/MohdViewSelector.tsx +33 -0
  53. package/src/TrackSelect/Folders/mohd/shared/columns.tsx +79 -0
  54. package/src/TrackSelect/Folders/mohd/shared/config.tsx +71 -0
  55. package/src/TrackSelect/Folders/mohd/shared/createFolder.ts +144 -0
  56. package/src/TrackSelect/Folders/mohd/shared/toTrack.ts +164 -0
  57. package/src/TrackSelect/Folders/mohd/shared/types.ts +46 -0
  58. package/src/TrackSelect/Folders/other-tracks/shared/createFolder.ts +13 -14
  59. package/src/TrackSelect/Folders/other-tracks/shared/toTrack.ts +17 -0
  60. package/src/TrackSelect/Folders/other-tracks/shared/types.ts +1 -0
  61. package/src/TrackSelect/Folders/types.ts +26 -69
  62. package/src/TrackSelect/TrackSelect.tsx +301 -257
  63. package/src/TrackSelect/TreeView/CustomTreeItem.tsx +9 -9
  64. package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +84 -6
  65. package/src/TrackSelect/buildSelectedTree.ts +145 -0
  66. package/src/TrackSelect/managedTracks.ts +92 -0
  67. package/src/TrackSelect/resolveFolderView.ts +20 -0
  68. package/src/TrackSelect/trackContext.ts +9 -0
  69. package/src/TrackSelect/types.ts +14 -39
  70. package/src/lib.ts +13 -7
  71. package/src/muiLicense.ts +9 -0
  72. package/src/vite-env.d.ts +9 -0
  73. package/test/TrackSelect.test.tsx +435 -0
  74. package/test/main.tsx +36 -352
  75. package/test/mocks/logo-test.tsx +11 -0
  76. package/test/mohdDisplay.test.tsx +45 -0
  77. package/test/startup.test.ts +206 -0
  78. package/test/trackSelectState.test.ts +176 -0
  79. package/vite.config.ts +1 -0
  80. package/vitest.config.ts +20 -0
  81. package/dist/TrackSelect/Folders/biosamples/shared/AssayToggle.d.ts +0 -18
  82. package/dist/TrackSelect/Folders/biosamples/shared/treeBuilder.d.ts +0 -28
  83. package/dist/TrackSelect/Folders/genes/shared/treeBuilder.d.ts +0 -13
  84. package/dist/TrackSelect/Folders/other-tracks/shared/treeBuilder.d.ts +0 -4
  85. package/dist/TrackSelect/store.d.ts +0 -4
  86. package/src/TrackSelect/Folders/NEW.md +0 -929
  87. package/src/TrackSelect/Folders/biosamples/shared/AssayToggle.tsx +0 -78
  88. package/src/TrackSelect/Folders/biosamples/shared/treeBuilder.ts +0 -224
  89. package/src/TrackSelect/Folders/genes/shared/treeBuilder.ts +0 -45
  90. package/src/TrackSelect/Folders/other-tracks/shared/treeBuilder.ts +0 -34
  91. package/src/TrackSelect/store.ts +0 -117
@@ -1,78 +0,0 @@
1
- import { FormControlLabel, Switch } from "@mui/material";
2
- import { FolderRuntimeConfig } from "../../types";
3
- import {
4
- defaultColumns,
5
- defaultGroupingModel,
6
- defaultLeafField,
7
- sortedByAssayColumns,
8
- sortedByAssayGroupingModel,
9
- sortedByAssayLeafField,
10
- } from "./columns";
11
- import { buildTreeView, buildSortedAssayTreeView } from "./treeBuilder";
12
-
13
- export interface AssayToggleProps {
14
- updateConfig: (partial: Partial<FolderRuntimeConfig>) => void;
15
- folderId: string;
16
- label: string;
17
- config: FolderRuntimeConfig;
18
- }
19
-
20
- /**
21
- * Biosample-specific toolbar component that toggles between
22
- * sample-grouped and assay-grouped views.
23
- *
24
- * When toggled, it updates the folder's runtime config to switch:
25
- * - columns: Different column definitions for each view
26
- * - groupingModel: ["ontology", "displayName"] vs ["assay", "ontology"]
27
- * - leafField: "assay" vs "displayName"
28
- * - buildTree: Different tree builder function
29
- */
30
- export function AssayToggle({
31
- updateConfig,
32
- folderId,
33
- label,
34
- config,
35
- }: AssayToggleProps) {
36
- // Derive toggle state from current config's leafField
37
- const sortedByAssay = config.leafField === sortedByAssayLeafField;
38
-
39
- const handleToggle = () => {
40
- const newValue = !sortedByAssay;
41
-
42
- if (newValue) {
43
- // Switch to assay-grouped view
44
- updateConfig({
45
- columns: sortedByAssayColumns,
46
- groupingModel: sortedByAssayGroupingModel,
47
- leafField: sortedByAssayLeafField,
48
- buildTree: (selectedIds, rowById) =>
49
- buildSortedAssayTreeView(selectedIds, rowById, label, folderId),
50
- });
51
- } else {
52
- // Switch back to default (sample-grouped) view
53
- updateConfig({
54
- columns: defaultColumns,
55
- groupingModel: defaultGroupingModel,
56
- leafField: defaultLeafField,
57
- buildTree: (selectedIds, rowById) =>
58
- buildTreeView(selectedIds, rowById, label, folderId),
59
- });
60
- }
61
- };
62
-
63
- return (
64
- <FormControlLabel
65
- sx={{ display: "flex", justifyContent: "flex-end" }}
66
- value="Sort by assay"
67
- control={
68
- <Switch
69
- color="primary"
70
- checked={sortedByAssay}
71
- onChange={handleToggle}
72
- />
73
- }
74
- label="Sort by assay"
75
- labelPlacement="end"
76
- />
77
- );
78
- }
@@ -1,224 +0,0 @@
1
- import { TreeViewBaseItem } from "@mui/x-tree-view";
2
- import { ExtendedTreeItemProps } from "../../../types";
3
- import { BiosampleRowInfo } from "./types";
4
- import { assayTypes, ontologyTypes, formatAssayType } from "./constants";
5
-
6
- /** Format an ID like "h3k27ac-ENCFF922YMQ" or "folder::h3k27ac-ENCFF922YMQ" to "H3K27ac - ENCFF922YMQ" */
7
- function formatIdLabel(id: string): string {
8
- // Remove folder prefix if present (e.g., "FolderName::h3k27ac-ENCFF922YMQ" -> "h3k27ac-ENCFF922YMQ")
9
- const rawId = id.includes("::") ? id.split("::").pop()! : id;
10
-
11
- const hyphenIndex = rawId.indexOf("-");
12
- if (hyphenIndex === -1) return rawId;
13
-
14
- const assayPart = rawId.substring(0, hyphenIndex);
15
- const accessionPart = rawId.substring(hyphenIndex + 1);
16
-
17
- return `${formatAssayType(assayPart)} - ${accessionPart}`;
18
- }
19
-
20
- /**
21
- * Creates the root node for the tree view
22
- */
23
- function createRootNode(
24
- label: string,
25
- folderId: string,
26
- ): TreeViewBaseItem<ExtendedTreeItemProps> {
27
- return {
28
- id: `${folderId}::root`,
29
- label,
30
- icon: "folder",
31
- children: [],
32
- allExpAccessions: [],
33
- };
34
- }
35
-
36
- /**
37
- * Builds tree in the sorted by assay view
38
- * Hierarchy: Assay -> Ontology -> DisplayName (leaf)
39
- *
40
- * This is the reverse of the default view - instead of grouping by sample first,
41
- * we group by assay first, making displayName the leaf node.
42
- *
43
- * @param selectedIds - list of selected row IDs
44
- * @param rowById - Mapping between an id and its BiosampleRowInfo object
45
- * @param rootLabel - Label for the root node
46
- * @param folderId - Folder ID to prefix tree item IDs with
47
- * @returns tree items for the RichTreeView
48
- */
49
- export function buildSortedAssayTreeView(
50
- selectedIds: string[],
51
- rowById: Map<string, BiosampleRowInfo>,
52
- rootLabel: string = "Biosamples",
53
- folderId: string = "",
54
- ): TreeViewBaseItem<ExtendedTreeItemProps>[] {
55
- const root = createRootNode(rootLabel, folderId);
56
- const assayMap = new Map<string, TreeViewBaseItem<ExtendedTreeItemProps>>();
57
- const ontologyMap = new Map<
58
- string,
59
- TreeViewBaseItem<ExtendedTreeItemProps>
60
- >();
61
- // Track which displayName nodes exist per assay+ontology combination
62
- // and which experiment IDs they contain
63
- const displayNameMap = new Map<
64
- string,
65
- TreeViewBaseItem<ExtendedTreeItemProps>
66
- >();
67
-
68
- const selectedRows = selectedIds.reduce<BiosampleRowInfo[]>((acc, id) => {
69
- const row = rowById.get(id);
70
- if (row) acc.push(row);
71
- return acc;
72
- }, []);
73
-
74
- selectedRows.forEach((row) => {
75
- const assayKey = `${folderId}::${row.assay}`;
76
- let assayNode = assayMap.get(assayKey);
77
- if (!assayNode) {
78
- assayNode = {
79
- id: assayKey,
80
- isAssayItem: true,
81
- label: row.assay,
82
- icon: "removeable",
83
- assayName: row.assay, // Add assayName so the icon renders correctly
84
- children: [],
85
- allExpAccessions: [],
86
- };
87
- assayMap.set(assayKey, assayNode);
88
- root.children!.push(assayNode);
89
- }
90
-
91
- const ontologyKey = `${folderId}::${row.assay}-${row.ontology}`;
92
- let ontologyNode = ontologyMap.get(ontologyKey);
93
- if (!ontologyNode) {
94
- ontologyNode = {
95
- id: ontologyKey,
96
- isAssayItem: false,
97
- label: row.ontology,
98
- icon: "removeable",
99
- children: [],
100
- allExpAccessions: [],
101
- };
102
- assayNode.children!.push(ontologyNode);
103
- ontologyMap.set(ontologyKey, ontologyNode);
104
- }
105
-
106
- // DisplayName is now the leaf node (no children, no assay icon)
107
- const displayNameKey = `${folderId}::${row.assay}-${row.ontology}-${row.displayName}`;
108
- let displayNameNode = displayNameMap.get(displayNameKey);
109
- if (!displayNameNode) {
110
- displayNameNode = {
111
- id: displayNameKey,
112
- isAssayItem: false,
113
- label: row.displayName,
114
- icon: "removeable",
115
- children: [],
116
- allExpAccessions: [],
117
- };
118
- ontologyNode.children!.push(displayNameNode);
119
- displayNameMap.set(displayNameKey, displayNameNode);
120
- }
121
-
122
- // Add this experiment ID to all parent nodes' allExpAccessions
123
- assayNode.allExpAccessions!.push(row.id);
124
- ontologyNode.allExpAccessions!.push(row.id);
125
- displayNameNode.allExpAccessions!.push(row.id);
126
- });
127
-
128
- // standardize the order of the assay folders
129
- root.children!.sort((a, b): number => {
130
- const aAssay = a.id.split("::")[1] ?? a.id;
131
- const bAssay = b.id.split("::")[1] ?? b.id;
132
- return assayTypes.indexOf(aAssay) - assayTypes.indexOf(bAssay);
133
- });
134
-
135
- return [root];
136
- }
137
-
138
- /**
139
- * Builds tree in the default view (sorted by ontology)
140
- * Hierarchy: Ontology -> DisplayName -> Experiment
141
- *
142
- * @param selectedIds - list of selected row IDs
143
- * @param rowById - Mapping between an id and its BiosampleRowInfo object
144
- * @param rootLabel - Label for the root node
145
- * @param folderId - Folder ID to prefix tree item IDs with
146
- * @returns tree items for the RichTreeView
147
- */
148
- export function buildTreeView(
149
- selectedIds: string[],
150
- rowById: Map<string, BiosampleRowInfo>,
151
- rootLabel: string = "Biosamples",
152
- folderId: string = "",
153
- ): TreeViewBaseItem<ExtendedTreeItemProps>[] {
154
- const root = createRootNode(rootLabel, folderId);
155
- const ontologyMap = new Map<
156
- string,
157
- TreeViewBaseItem<ExtendedTreeItemProps>
158
- >();
159
- const displayNameMap = new Map<
160
- string,
161
- TreeViewBaseItem<ExtendedTreeItemProps>
162
- >();
163
-
164
- const selectedRows = selectedIds.reduce<BiosampleRowInfo[]>((acc, id) => {
165
- const row = rowById.get(id);
166
- if (row) acc.push(row);
167
- return acc;
168
- }, []);
169
-
170
- selectedRows.forEach((row) => {
171
- if (!row) {
172
- return;
173
- }
174
- const ontologyKey = `${folderId}::${row.ontology}`;
175
- let ontologyNode = ontologyMap.get(ontologyKey);
176
- if (!ontologyNode) {
177
- ontologyNode = {
178
- id: ontologyKey,
179
- label: row.ontology,
180
- icon: "removeable",
181
- children: [],
182
- allExpAccessions: [],
183
- };
184
- ontologyMap.set(ontologyKey, ontologyNode);
185
- root.children!.push(ontologyNode);
186
- }
187
-
188
- const displayNameKey = `${folderId}::${row.ontology}-${row.displayName}`;
189
- let displayNameNode = displayNameMap.get(displayNameKey);
190
- if (!displayNameNode) {
191
- displayNameNode = {
192
- id: displayNameKey,
193
- label: row.displayName,
194
- icon: "removeable",
195
- children: [],
196
- allExpAccessions: [],
197
- };
198
- ontologyNode.children!.push(displayNameNode);
199
- displayNameMap.set(displayNameKey, displayNameNode);
200
- }
201
-
202
- const expNode: TreeViewBaseItem<ExtendedTreeItemProps> = {
203
- id: row.id,
204
- label: formatIdLabel(row.id),
205
- icon: "removeable",
206
- assayName: row.assay,
207
- children: [],
208
- allExpAccessions: [row.id],
209
- };
210
- displayNameNode.children!.push(expNode);
211
-
212
- ontologyNode.allExpAccessions!.push(row.id);
213
- displayNameNode.allExpAccessions!.push(row.id);
214
- });
215
-
216
- // standardize the order of the ontology folders
217
- root.children!.sort((a, b): number => {
218
- const aOntology = a.id.split("::")[1] ?? a.id;
219
- const bOntology = b.id.split("::")[1] ?? b.id;
220
- return ontologyTypes.indexOf(aOntology) - ontologyTypes.indexOf(bOntology);
221
- });
222
-
223
- return [root];
224
- }
@@ -1,45 +0,0 @@
1
- import { TreeViewBaseItem } from "@mui/x-tree-view";
2
- import { ExtendedTreeItemProps } from "../../../types";
3
- import { GeneRowInfo } from "./types";
4
-
5
- /**
6
- * Builds a flat tree structure for the TreeView panel (selected items)
7
- * Since genes have no grouping, this is a simple root -> leaf structure
8
- *
9
- * @param selectedIds - Array of selected row IDs
10
- * @param rowById - Map of row ID to row data
11
- * @param rootLabel - Label for the root node
12
- * @returns Tree structure for RichTreeView
13
- */
14
- export function buildTreeView(
15
- selectedIds: string[],
16
- rowById: Map<string, GeneRowInfo>,
17
- rootLabel: string = "Genes",
18
- ): TreeViewBaseItem<ExtendedTreeItemProps>[] {
19
- // Root node
20
- const root: TreeViewBaseItem<ExtendedTreeItemProps> = {
21
- id: "root",
22
- label: rootLabel,
23
- icon: "folder",
24
- children: [],
25
- allExpAccessions: [],
26
- };
27
-
28
- // Get selected rows and add as direct children of root
29
- selectedIds.forEach((id) => {
30
- const row = rowById.get(id);
31
- if (row) {
32
- const leafNode: TreeViewBaseItem<ExtendedTreeItemProps> = {
33
- id: row.id,
34
- label: row.displayName,
35
- icon: "removeable",
36
- children: [],
37
- allExpAccessions: [row.id],
38
- };
39
- root.children!.push(leafNode);
40
- root.allExpAccessions!.push(row.id);
41
- }
42
- });
43
-
44
- return [root];
45
- }
@@ -1,34 +0,0 @@
1
- import { TreeViewBaseItem } from "@mui/x-tree-view";
2
- import { ExtendedTreeItemProps } from "../../../types";
3
- import { OtherTrackInfo } from "./types";
4
-
5
- export function buildTreeView(
6
- selectedIds: string[],
7
- rowById: Map<string, OtherTrackInfo>,
8
- rootLabel: string = "Other Tracks",
9
- ): TreeViewBaseItem<ExtendedTreeItemProps>[] {
10
- const root: TreeViewBaseItem<ExtendedTreeItemProps> = {
11
- id: "root",
12
- label: rootLabel,
13
- icon: "folder",
14
- children: [],
15
- allExpAccessions: [],
16
- };
17
-
18
- selectedIds.forEach((id) => {
19
- const row = rowById.get(id);
20
- if (row) {
21
- const leafNode: TreeViewBaseItem<ExtendedTreeItemProps> = {
22
- id: row.id,
23
- label: row.name,
24
- icon: "removeable",
25
- children: [],
26
- allExpAccessions: [row.id],
27
- };
28
- root.children!.push(leafNode);
29
- root.allExpAccessions!.push(row.id);
30
- }
31
- });
32
-
33
- return [root];
34
- }
@@ -1,117 +0,0 @@
1
- import { create, StoreApi, UseBoundStore } from "zustand";
2
- import { SelectionAction, SelectionState } from "./types";
3
-
4
- export type SelectionStoreInstance = UseBoundStore<
5
- StoreApi<SelectionState & SelectionAction>
6
- >;
7
-
8
- const DEFAULT_STORAGE_KEY = "trackSelect_selection";
9
-
10
- type SerializedSelection = Record<string, string[]>;
11
-
12
- const serializeSelection = (
13
- selection: Map<string, Set<string>>,
14
- ): SerializedSelection => {
15
- const obj: SerializedSelection = {};
16
- selection.forEach((ids, folderId) => {
17
- obj[folderId] = Array.from(ids);
18
- });
19
- return obj;
20
- };
21
-
22
- const deserializeSelection = (
23
- data: SerializedSelection,
24
- ): Map<string, Set<string>> => {
25
- const map = new Map<string, Set<string>>();
26
- Object.entries(data).forEach(([folderId, ids]) => {
27
- map.set(folderId, new Set(ids));
28
- });
29
- return map;
30
- };
31
-
32
- const loadFromStorage = (
33
- storageKey: string,
34
- ): Map<string, Set<string>> | undefined => {
35
- try {
36
- const stored = sessionStorage.getItem(storageKey);
37
- if (stored) {
38
- const parsed = JSON.parse(stored) as SerializedSelection;
39
- return deserializeSelection(parsed);
40
- }
41
- } catch {
42
- // Ignore storage errors
43
- }
44
- return undefined;
45
- };
46
-
47
- const saveToStorage = (
48
- selection: Map<string, Set<string>>,
49
- storageKey: string,
50
- ) => {
51
- try {
52
- const serialized = serializeSelection(selection);
53
- sessionStorage.setItem(storageKey, JSON.stringify(serialized));
54
- } catch {
55
- // Ignore storage errors
56
- }
57
- };
58
-
59
- const buildSelectionMap = (
60
- folderIds: string[],
61
- initialSelection?: Map<string, Set<string>>,
62
- ) => {
63
- const map = new Map<string, Set<string>>();
64
- folderIds.forEach((folderId) => {
65
- const initial = initialSelection?.get(folderId);
66
- map.set(folderId, initial ? new Set(initial) : new Set<string>());
67
- });
68
- return map;
69
- };
70
-
71
- export function createSelectionStore(
72
- folderIds: string[],
73
- storageKey: string = DEFAULT_STORAGE_KEY,
74
- initialSelection?: Map<string, Set<string>>,
75
- ) {
76
- const storedSelection = loadFromStorage(storageKey);
77
- // Storage wins: use stored if exists, else fall back to initialSelection
78
- const selectedByFolder = buildSelectionMap(
79
- folderIds,
80
- storedSelection ?? initialSelection,
81
- );
82
- const activeFolderId = folderIds[0] ?? "";
83
-
84
- const store = create<SelectionState & SelectionAction>((set) => ({
85
- selectedByFolder,
86
- activeFolderId,
87
- clear: (folderId?: string) =>
88
- set((state) => {
89
- if (folderId) {
90
- const next = new Map(state.selectedByFolder);
91
- next.set(folderId, new Set<string>());
92
- return { selectedByFolder: next };
93
- }
94
-
95
- const next = new Map<string, Set<string>>();
96
- state.selectedByFolder.forEach((_value, id) => {
97
- next.set(id, new Set<string>());
98
- });
99
- return { selectedByFolder: next };
100
- }),
101
- setActiveFolder: (folderId: string) =>
102
- set(() => ({ activeFolderId: folderId })),
103
- setSelection: (folderId: string, ids: Set<string>) =>
104
- set((state) => {
105
- const next = new Map(state.selectedByFolder);
106
- next.set(folderId, new Set(ids));
107
- return { selectedByFolder: next };
108
- }),
109
- }));
110
-
111
- // Subscribe to changes and persist to storage
112
- store.subscribe((state) => {
113
- saveToStorage(state.selectedByFolder, storageKey);
114
- });
115
-
116
- return store;
117
- }