@weng-lab/genomebrowser-ui 0.1.6 → 0.1.8

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.
@@ -1,32 +1,30 @@
1
1
  import {
2
2
  Box,
3
- Stack,
4
- TextField,
3
+ Button,
4
+ Dialog,
5
+ DialogActions,
6
+ DialogContent,
7
+ DialogContentText,
8
+ DialogTitle,
5
9
  FormControlLabel,
10
+ Stack,
6
11
  Switch,
7
- Button,
12
+ TextField,
8
13
  } from "@mui/material";
14
+ import { GridRowSelectionModel } from "@mui/x-data-grid";
15
+ import { TreeViewBaseItem } from "@mui/x-tree-view";
16
+ import React, { useEffect, useMemo, useRef, useState } from "react";
9
17
  import { DataGridWrapper } from "./DataGrid/DataGridWrapper";
10
- import { searchTracks, flattenIntoRow } from "./DataGrid/dataGridHelpers";
18
+ import { flattenIntoRow, searchTracks } from "./DataGrid/dataGridHelpers";
11
19
  import { TreeViewWrapper } from "./TreeView/TreeViewWrapper";
12
20
  import {
13
21
  buildSortedAssayTreeView,
14
22
  buildTreeView,
15
23
  searchTreeItems,
16
24
  } from "./TreeView/treeViewHelpers";
17
- import { SearchTracksProps, ExtendedTreeItemProps, RowInfo } from "./types";
18
- import { rows, rowById } from "./consts";
19
- import React, { useState, useMemo, useEffect } from "react";
20
- import { TreeViewBaseItem } from "@mui/x-tree-view";
21
- import { GridRowSelectionModel } from "@mui/x-data-grid";
22
- import {
23
- Dialog,
24
- DialogTitle,
25
- DialogContent,
26
- DialogContentText,
27
- DialogActions,
28
- } from "@mui/material";
25
+ import { rowById, rows } from "./consts";
29
26
  import { SelectionStoreInstance } from "./store";
27
+ import { ExtendedTreeItemProps, SearchTracksProps } from "./types";
30
28
 
31
29
  export interface TrackSelectProps {
32
30
  store: SelectionStoreInstance;
@@ -37,21 +35,19 @@ export default function TrackSelect({ store }: TrackSelectProps) {
37
35
  const [sortedAssay, setSortedAssay] = useState(false);
38
36
  const [searchQuery, setSearchQuery] = useState("");
39
37
  const [isSearchResult, setIsSearchResult] = useState(false);
40
- const selectedTracks = store((s) => s.selectedTracks);
38
+ const selectedIds = store((s) => s.selectedIds);
39
+ const getTrackIds = store((s) => s.getTrackIds);
41
40
  const setSelected = store((s) => s.setSelected);
42
41
  const clear = store((s) => s.clear);
43
42
  const MAX_ACTIVE = store((s) => s.maxTracks);
44
43
 
45
- // Derive active tracks from selectedTracks (all keys are real track IDs)
46
- const activeTracks = useMemo(
47
- () => new Set(selectedTracks.keys()),
48
- [selectedTracks],
49
- );
44
+ // Get only real track IDs (no auto-generated group IDs)
45
+ const trackIds = useMemo(() => getTrackIds(), [selectedIds, getTrackIds]);
50
46
 
51
47
  const treeItems = useMemo(() => {
52
48
  return sortedAssay
53
49
  ? buildSortedAssayTreeView(
54
- Array.from(selectedTracks.keys()),
50
+ Array.from(trackIds),
55
51
  {
56
52
  id: "1",
57
53
  isAssayItem: false,
@@ -63,7 +59,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
63
59
  rowById,
64
60
  )
65
61
  : buildTreeView(
66
- Array.from(selectedTracks.keys()),
62
+ Array.from(trackIds),
67
63
  {
68
64
  id: "1",
69
65
  isAssayItem: false,
@@ -74,7 +70,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
74
70
  },
75
71
  rowById,
76
72
  );
77
- }, [selectedTracks, sortedAssay]);
73
+ }, [trackIds, sortedAssay]);
78
74
 
79
75
  const [filteredRows, setFilteredRows] = useState(rows);
80
76
  const [filteredTreeItems, setFilteredTreeItems] = useState([
@@ -87,116 +83,168 @@ export default function TrackSelect({ store }: TrackSelectProps) {
87
83
  allRowInfo: [],
88
84
  },
89
85
  ] as TreeViewBaseItem<ExtendedTreeItemProps>[]);
86
+ const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
87
+ const searchResultIdsRef = useRef<Set<string>>(new Set());
90
88
 
91
89
  useEffect(() => {
92
90
  if (searchQuery === "") {
93
91
  setFilteredTreeItems(treeItems);
94
92
  setFilteredRows(rows);
95
93
  setIsSearchResult(false);
94
+ searchResultIdsRef.current = new Set();
95
+ } else if (searchResultIdsRef.current.size > 0) {
96
+ // When selection changes during search, rebuild tree from selected items that match search
97
+ const matchingTrackIds = Array.from(trackIds).filter((id) =>
98
+ searchResultIdsRef.current.has(id),
99
+ );
100
+
101
+ const newTreeItems = sortedAssay
102
+ ? buildSortedAssayTreeView(
103
+ matchingTrackIds,
104
+ {
105
+ id: "1",
106
+ isAssayItem: false,
107
+ label: "Biosamples",
108
+ icon: "folder",
109
+ children: [],
110
+ allRowInfo: [],
111
+ },
112
+ rowById,
113
+ )
114
+ : buildTreeView(
115
+ matchingTrackIds,
116
+ {
117
+ id: "1",
118
+ isAssayItem: false,
119
+ label: "Biosamples",
120
+ icon: "folder",
121
+ children: [],
122
+ allRowInfo: [],
123
+ },
124
+ rowById,
125
+ );
126
+
127
+ setFilteredTreeItems(newTreeItems);
96
128
  }
97
- }, [treeItems, searchQuery]);
129
+ }, [treeItems, searchQuery, trackIds, sortedAssay]);
98
130
 
99
131
  const handleToggle = () => {
100
132
  setSortedAssay(!sortedAssay);
101
133
  };
102
134
 
103
135
  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
104
- setSearchQuery(e.target.value);
136
+ const query = e.target.value;
137
+ setSearchQuery(query);
138
+
139
+ // Clear previous timeout
140
+ if (searchTimeoutRef.current) {
141
+ clearTimeout(searchTimeoutRef.current);
142
+ }
105
143
 
106
- const dataGridSearchProps: SearchTracksProps = {
107
- jsonStructure: "tracks",
108
- query: e.target.value,
109
- keyWeightMap: [
110
- "displayname",
111
- "ontology",
112
- "lifeStage",
113
- "sampleType",
114
- "type",
115
- "experimentAccession",
116
- "fileAccession",
117
- ],
118
- };
144
+ // Debounce the search
145
+ searchTimeoutRef.current = setTimeout(() => {
146
+ if (query === "") {
147
+ return; // useEffect handles empty query
148
+ }
119
149
 
120
- const treeSearchProps: SearchTracksProps = {
121
- treeItems: treeItems,
122
- query: e.target.value,
123
- keyWeightMap: [
124
- "displayname",
125
- "ontology",
126
- "lifeStage",
127
- "sampleType",
128
- "type",
129
- "experimentAccession",
130
- "fileAccession",
131
- ],
132
- };
133
- const newDataGridRows = searchTracks(dataGridSearchProps)
134
- .map((t) => t.item)
135
- .map(flattenIntoRow);
150
+ const dataGridSearchProps: SearchTracksProps = {
151
+ jsonStructure: "tracks",
152
+ query: query,
153
+ keyWeightMap: [
154
+ "displayname",
155
+ "ontology",
156
+ "lifeStage",
157
+ "sampleType",
158
+ "type",
159
+ "experimentAccession",
160
+ "fileAccession",
161
+ ],
162
+ };
136
163
 
137
- // we only want the intersection of filtered tracks displayed on the DataGrid and user-selected tracks to be displayed on the tree
138
- const newDataGridIds = newDataGridRows.map((r) => r.experimentAccession);
139
- const retIds = searchTreeItems(treeSearchProps).map(
140
- (r) => r.item.experimentAccession,
141
- );
142
- const newTreeIds = retIds.filter((i) => newDataGridIds.includes(i));
164
+ const treeSearchProps: SearchTracksProps = {
165
+ treeItems: treeItems,
166
+ query: query,
167
+ keyWeightMap: [
168
+ "displayname",
169
+ "ontology",
170
+ "lifeStage",
171
+ "sampleType",
172
+ "type",
173
+ "experimentAccession",
174
+ "fileAccession",
175
+ ],
176
+ };
177
+ const newDataGridRows = searchTracks(dataGridSearchProps)
178
+ .map((t) => t.item)
179
+ .map(flattenIntoRow);
143
180
 
144
- // build new tree from the newTreeIds...maybe it would be faster to prune the current tree instead of rebuilding it?
145
- const newTreeItems = sortedAssay
146
- ? buildSortedAssayTreeView(
147
- newTreeIds,
148
- {
149
- id: "1",
150
- isAssayItem: false,
151
- label: "Biosamples",
152
- icon: "folder",
153
- children: [],
154
- allRowInfo: [],
155
- },
156
- rowById,
157
- )
158
- : buildTreeView(
159
- newTreeIds,
160
- {
161
- id: "1",
162
- isAssayItem: false,
163
- label: "Biosamples",
164
- icon: "folder",
165
- children: [],
166
- allRowInfo: [],
167
- },
168
- rowById,
169
- );
181
+ // we only want the intersection of filtered tracks displayed on the DataGrid and user-selected tracks to be displayed on the tree
182
+ const newDataGridIds = newDataGridRows.map((r) => r.experimentAccession);
183
+ const retIds = searchTreeItems(treeSearchProps).map(
184
+ (r) => r.item.experimentAccession,
185
+ );
186
+ const newTreeIds = retIds.filter((i) => newDataGridIds.includes(i));
187
+
188
+ // build new tree from the newTreeIds
189
+ const newTreeItems = sortedAssay
190
+ ? buildSortedAssayTreeView(
191
+ newTreeIds,
192
+ {
193
+ id: "1",
194
+ isAssayItem: false,
195
+ label: "Biosamples",
196
+ icon: "folder",
197
+ children: [],
198
+ allRowInfo: [],
199
+ },
200
+ rowById,
201
+ )
202
+ : buildTreeView(
203
+ newTreeIds,
204
+ {
205
+ id: "1",
206
+ isAssayItem: false,
207
+ label: "Biosamples",
208
+ icon: "folder",
209
+ children: [],
210
+ allRowInfo: [],
211
+ },
212
+ rowById,
213
+ );
214
+
215
+ // Store search result IDs in ref for use in useEffect
216
+ searchResultIdsRef.current = new Set(newDataGridIds);
170
217
 
171
- setFilteredRows(newDataGridRows);
172
- setIsSearchResult(true);
173
- setFilteredTreeItems(newTreeItems);
218
+ setFilteredRows(newDataGridRows);
219
+ setIsSearchResult(true);
220
+ setFilteredTreeItems(newTreeItems);
221
+ }, 300);
174
222
  };
175
223
 
176
224
  const handleSelection = (newSelection: GridRowSelectionModel) => {
177
- const idsSet =
225
+ const allIds: Set<string> =
178
226
  (newSelection && (newSelection as any).ids) ?? new Set<string>();
179
227
 
180
- // Build a Map of only real track IDs (filter out auto-generated group IDs)
181
- const newTracks = new Map<string, RowInfo>();
182
- idsSet.forEach((id: string) => {
183
- const row = rowById.get(id);
184
- if (row) {
185
- newTracks.set(id, row);
228
+ // Count only real track IDs for the limit check
229
+ let realTrackCount = 0;
230
+ allIds.forEach((id: string) => {
231
+ if (rowById.has(id)) {
232
+ realTrackCount++;
186
233
  }
187
234
  });
188
235
 
189
236
  // Block only if the new selection would exceed the limit
190
- if (newTracks.size > MAX_ACTIVE) {
237
+ if (realTrackCount > MAX_ACTIVE) {
191
238
  setLimitDialogOpen(true);
192
239
  return;
193
240
  }
194
241
 
195
- setSelected(newTracks);
242
+ // Store ALL IDs (including auto-generated group IDs)
243
+ setSelected(allIds);
196
244
  };
197
245
 
198
246
  return (
199
- <Box sx={{ flex: 1 }}>
247
+ <Box sx={{ flex: 1, pt: 1 }}>
200
248
  <Box display="flex" justifyContent="space-between" sx={{ mb: 3 }}>
201
249
  <TextField
202
250
  id="outlined-suffix-shrink"
@@ -222,7 +270,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
222
270
  ? `${filteredRows.length} Search Results`
223
271
  : `${rows.length} Available Tracks`
224
272
  }
225
- selectedTracks={selectedTracks}
273
+ selectedIds={selectedIds}
226
274
  handleSelection={handleSelection}
227
275
  sortedAssay={sortedAssay}
228
276
  />
@@ -231,8 +279,7 @@ export default function TrackSelect({ store }: TrackSelectProps) {
231
279
  <TreeViewWrapper
232
280
  store={store}
233
281
  items={filteredTreeItems}
234
- selectedTracks={selectedTracks}
235
- activeTracks={activeTracks}
282
+ trackIds={trackIds}
236
283
  isSearchResult={isSearchResult}
237
284
  />
238
285
  </Box>
@@ -12,7 +12,7 @@ import { Avatar } from "@mui/material";
12
12
  export function TreeViewWrapper({
13
13
  store,
14
14
  items,
15
- activeTracks,
15
+ trackIds,
16
16
  isSearchResult,
17
17
  }: TreeViewWrapperProps) {
18
18
  const removeIds = store((s) => s.removeIds);
@@ -28,11 +28,13 @@ export function TreeViewWrapper({
28
28
  removedIds.forEach((id) => {
29
29
  const row = rowById.get(id);
30
30
  if (row) {
31
- // Add the auto-generated group IDs for this track's ontology and assay
31
+ // Add the auto-generated group IDs for this track's grouping hierarchy
32
+ // Default view: ontology -> displayname
32
33
  idsToRemove.add(`auto-generated-row-ontology/${row.ontology}`);
33
34
  idsToRemove.add(
34
- `auto-generated-row-ontology/${row.ontology}-assay/${row.assay}`,
35
+ `auto-generated-row-ontology/${row.ontology}-displayname/${row.displayname}`,
35
36
  );
37
+ // Sorted by assay view: assay -> ontology -> displayname
36
38
  idsToRemove.add(`auto-generated-row-assay/${row.assay}`);
37
39
  idsToRemove.add(
38
40
  `auto-generated-row-assay/${row.assay}-ontology/${row.ontology}`,
@@ -76,7 +78,7 @@ export function TreeViewWrapper({
76
78
  color: "text.primary",
77
79
  }}
78
80
  >
79
- {activeTracks.size}
81
+ {trackIds.size}
80
82
  </Avatar>
81
83
  <Typography fontWeight="bold">
82
84
  Active Tracks
@@ -99,8 +99,10 @@ export function buildSortedAssayTreeView(
99
99
  id: row.experimentAccession,
100
100
  isAssayItem: false,
101
101
  label: row.experimentAccession,
102
- icon: row.assay,
102
+ icon: "removeable",
103
+ assayName: row.assay,
103
104
  children: [],
105
+ allExpAccessions: [row.experimentAccession],
104
106
  };
105
107
  sampleAssayMap.set(row.displayname + row.assay, expNode);
106
108
  displayNameNode.children!.push(expNode);
@@ -183,8 +185,10 @@ export function buildTreeView(
183
185
  expNode = {
184
186
  id: row.experimentAccession,
185
187
  label: row.experimentAccession,
186
- icon: row.assay,
188
+ icon: "removeable",
189
+ assayName: row.assay,
187
190
  children: [],
191
+ allExpAccessions: [row.experimentAccession],
188
192
  };
189
193
  sampleAssayMap.set(row.displayname + row.assay, expNode);
190
194
  displayNameNode.children!.push(expNode);
@@ -282,7 +286,7 @@ const TreeItemLabelText = styled(Typography)({
282
286
  fontFamily: "inherit",
283
287
  });
284
288
 
285
- function CustomLabel({ icon: Icon, children, isAssayItem, ...other }: CustomLabelProps) {
289
+ function CustomLabel({ icon: Icon, children, isAssayItem, assayName, ...other }: CustomLabelProps) {
286
290
  const variant = isAssayItem ? "subtitle2" : "body2";
287
291
  const fontWeight = isAssayItem ? "bold" : 500;
288
292
  return (
@@ -305,8 +309,9 @@ function CustomLabel({ icon: Icon, children, isAssayItem, ...other }: CustomLabe
305
309
  sx={{ mr: 1, fontSize: "1.2rem" }}
306
310
  />
307
311
  )}
308
- <Stack direction="row" spacing={2} alignItems="center">
309
- { isAssayItem && AssayIcon(other.id) }
312
+ <Stack direction="row" spacing={1} alignItems="center">
313
+ {isAssayItem && AssayIcon(other.id)}
314
+ {assayName && AssayIcon(assayName)}
310
315
  <TreeItemLabelText fontWeight={fontWeight} variant={variant}>{children}</TreeItemLabelText>
311
316
  </Stack>
312
317
  </TreeItemLabel>
@@ -330,14 +335,7 @@ const TreeItemContent = styled("div")(({ theme }) => ({
330
335
  marginBottom: theme.spacing(0.5),
331
336
  marginTop: theme.spacing(0.5),
332
337
  fontWeight: 500,
333
- [`&[data-focused], &[data-selected]`]: {
334
- backgroundColor: theme.palette.primary.dark,
335
- color: theme.palette.primary.contrastText,
336
- ...theme.applyStyles("light", {
337
- backgroundColor: theme.palette.primary.main,
338
- }),
339
- },
340
- "&:not([data-focused], [data-selected]):hover": {
338
+ "&:hover": {
341
339
  backgroundColor: alpha(theme.palette.primary.main, 0.1),
342
340
  color: "white",
343
341
  ...theme.applyStyles("light", {
@@ -417,6 +415,7 @@ export const CustomTreeItem = React.forwardRef(function CustomTreeItem(
417
415
  ),
418
416
  expandable: (status.expandable && status.expanded).toString(),
419
417
  isAssayItem: item.isAssayItem,
418
+ assayName: item.assayName,
420
419
  id: item.id
421
420
  })}
422
421
  />
@@ -1,27 +1,50 @@
1
1
  import { create, StoreApi, UseBoundStore } from "zustand";
2
- import { SelectionState, SelectionAction, RowInfo } from "./types";
2
+ import { rowById } from "./consts";
3
+ import { RowInfo, SelectionAction, SelectionState } from "./types";
3
4
 
4
5
  export type SelectionStoreInstance = UseBoundStore<
5
6
  StoreApi<SelectionState & SelectionAction>
6
7
  >;
7
8
 
8
- export function createSelectionStore(initialTracks?: Map<string, RowInfo>) {
9
+ // Helper to check if an ID is auto-generated by DataGrid grouping
10
+ const isAutoGeneratedId = (id: string) => id.startsWith("auto-generated-row-");
11
+
12
+ export function createSelectionStore(initialIds?: Set<string>) {
9
13
  return create<SelectionState & SelectionAction>((set, get) => ({
10
14
  maxTracks: 30,
11
- selectedTracks: initialTracks ? new Map(initialTracks) : new Map<string, RowInfo>(),
12
- selectedIds: () => new Set(get().selectedTracks.keys()),
13
- setSelected: (tracks: Map<string, RowInfo>) =>
15
+ // Stores ALL selected IDs, including auto-generated group IDs
16
+ selectedIds: initialIds ? new Set(initialIds) : new Set<string>(),
17
+ // Returns only real track IDs (filters out auto-generated group IDs)
18
+ getTrackIds: () => {
19
+ const all = get().selectedIds;
20
+ return new Set([...all].filter((id) => !isAutoGeneratedId(id)));
21
+ },
22
+ // Returns a Map of track IDs to RowInfo (no auto-generated IDs)
23
+ getTrackMap: () => {
24
+ const all = get().selectedIds;
25
+ const map = new Map<string, RowInfo>();
26
+ all.forEach((id) => {
27
+ if (!isAutoGeneratedId(id)) {
28
+ const row = rowById.get(id);
29
+ if (row) {
30
+ map.set(id, row);
31
+ }
32
+ }
33
+ });
34
+ return map;
35
+ },
36
+ setSelected: (ids: Set<string>) =>
14
37
  set(() => ({
15
- selectedTracks: new Map(tracks),
38
+ selectedIds: new Set(ids),
16
39
  })),
17
40
  removeIds: (removedIds: Set<string>) =>
18
41
  set((state) => {
19
- const next = new Map(state.selectedTracks);
42
+ const next = new Set(state.selectedIds);
20
43
  removedIds.forEach((id) => {
21
44
  next.delete(id);
22
45
  });
23
- return { selectedTracks: next };
46
+ return { selectedIds: next };
24
47
  }),
25
- clear: () => set(() => ({ selectedTracks: new Map<string, RowInfo>() })),
48
+ clear: () => set(() => ({ selectedIds: new Set<string>() })),
26
49
  }));
27
50
  }
File without changes
@@ -59,6 +59,10 @@ export type ExtendedTreeItemProps = {
59
59
  label: string;
60
60
  icon: string;
61
61
  isAssayItem?: boolean;
62
+ /**
63
+ * The assay name for leaf nodes (experiment accession items)
64
+ */
65
+ assayName?: string;
62
66
  /**
63
67
  * list of all the experimentAccession values in the children/grandchildren of the item, or the accession of the item itself
64
68
  * this is used in updating the rowSelectionModel when removing items from the Tree View panel
@@ -71,8 +75,7 @@ export type ExtendedTreeItemProps = {
71
75
  export type TreeViewWrapperProps = {
72
76
  store: SelectionStoreInstance;
73
77
  items: TreeViewBaseItem<ExtendedTreeItemProps>[];
74
- selectedTracks: Map<string, RowInfo>;
75
- activeTracks: Set<string>; // doesn't have the autogenerated row groupings to provide accurate number of tracks
78
+ trackIds: Set<string>; // real track IDs only (no auto-generated)
76
79
  isSearchResult: boolean;
77
80
  };
78
81
 
@@ -80,6 +83,7 @@ export interface CustomLabelProps {
80
83
  id: string;
81
84
  children: React.ReactNode;
82
85
  isAssayItem?: boolean;
86
+ assayName?: string;
83
87
  icon: React.ElementType | React.ReactElement;
84
88
  }
85
89
 
@@ -94,12 +98,16 @@ export interface CustomTreeItemProps
94
98
  */
95
99
  export type SelectionState = {
96
100
  maxTracks: number;
97
- selectedTracks: Map<string, RowInfo>;
101
+ // All selected IDs including auto-generated group IDs from DataGrid
102
+ selectedIds: Set<string>;
98
103
  };
99
104
 
100
105
  export type SelectionAction = {
101
- selectedIds: () => Set<string>;
102
- setSelected: (tracks: Map<string, RowInfo>) => void;
106
+ // Returns only real track IDs (filters out auto-generated group IDs)
107
+ getTrackIds: () => Set<string>;
108
+ // Returns a Map of track IDs to RowInfo (no auto-generated IDs)
109
+ getTrackMap: () => Map<string, RowInfo>;
110
+ setSelected: (ids: Set<string>) => void;
103
111
  removeIds: (removedIds: Set<string>) => void;
104
112
  clear: () => void;
105
113
  };
@@ -126,7 +134,7 @@ interface BaseTableProps extends Omit<DataGridPremiumProps, "columns"> {
126
134
 
127
135
  type DataGridWrapperProps = {
128
136
  rows: RowInfo[];
129
- selectedTracks: Map<string, RowInfo>;
137
+ selectedIds: Set<string>; // all IDs including auto-generated group IDs
130
138
  handleSelection: (newSelection: GridRowSelectionModel) => void;
131
139
  sortedAssay: boolean;
132
140
  };
package/test/main.tsx CHANGED
@@ -1,13 +1,10 @@
1
- import TrackSelect from "../src/TrackSelect/TrackSelect"
1
+ import { createSelectionStore } from "../src/TrackSelect/store";
2
+ import TrackSelect from "../src/TrackSelect/TrackSelect";
2
3
  import { createRoot } from "react-dom/client";
3
4
 
4
5
  function Main() {
5
- return (
6
- <TrackSelect>
7
- </TrackSelect>
8
- )
6
+ const store = createSelectionStore();
7
+ return <TrackSelect store={store} />;
9
8
  }
10
9
 
11
- createRoot(document.getElementById("root")!).render(
12
- <Main/>
13
- );
10
+ createRoot(document.getElementById("root")!).render(<Main />);