@weng-lab/genomebrowser-ui 0.2.2 → 0.2.3

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 (54) hide show
  1. package/.env.local +1 -1
  2. package/dist/TrackSelect/Folders/biosamples/data/human.json.d.ts +57141 -57141
  3. package/dist/TrackSelect/Folders/biosamples/data/mouse.json.d.ts +10394 -10394
  4. package/dist/TrackSelect/Folders/genes/data/human.json.d.ts +7 -7
  5. package/dist/TrackSelect/Folders/genes/data/mouse.json.d.ts +7 -7
  6. package/dist/genomebrowser-ui.es.js +9 -5
  7. package/dist/genomebrowser-ui.es.js.map +1 -1
  8. package/eslint.config.js +30 -30
  9. package/index.html +14 -14
  10. package/package.json +1 -1
  11. package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +137 -137
  12. package/src/TrackSelect/DataGrid/DefaultGroupingCell.tsx +64 -64
  13. package/src/TrackSelect/Dialogs/ClearDialog.tsx +63 -63
  14. package/src/TrackSelect/Dialogs/LimitDialog.tsx +33 -33
  15. package/src/TrackSelect/Dialogs/ResetDialog.tsx +43 -43
  16. package/src/TrackSelect/FolderList/Breadcrumb.tsx +38 -38
  17. package/src/TrackSelect/FolderList/FolderCard.tsx +51 -51
  18. package/src/TrackSelect/FolderList/FolderList.tsx +47 -47
  19. package/src/TrackSelect/Folders/NEW.md +929 -929
  20. package/src/TrackSelect/Folders/biosamples/data/formatBiosamples.go +254 -254
  21. package/src/TrackSelect/Folders/biosamples/data/human.json +57141 -57141
  22. package/src/TrackSelect/Folders/biosamples/data/mouse.json +10394 -10394
  23. package/src/TrackSelect/Folders/biosamples/human.ts +17 -17
  24. package/src/TrackSelect/Folders/biosamples/mouse.ts +17 -17
  25. package/src/TrackSelect/Folders/biosamples/shared/AssayToggle.tsx +78 -78
  26. package/src/TrackSelect/Folders/biosamples/shared/BiosampleGroupingCell.tsx +146 -146
  27. package/src/TrackSelect/Folders/biosamples/shared/BiosampleTreeItem.tsx +15 -15
  28. package/src/TrackSelect/Folders/biosamples/shared/columns.tsx +165 -165
  29. package/src/TrackSelect/Folders/biosamples/shared/constants.tsx +116 -116
  30. package/src/TrackSelect/Folders/biosamples/shared/createFolder.ts +116 -116
  31. package/src/TrackSelect/Folders/biosamples/shared/treeBuilder.ts +224 -224
  32. package/src/TrackSelect/Folders/biosamples/shared/types.ts +48 -48
  33. package/src/TrackSelect/Folders/genes/data/human.json +7 -7
  34. package/src/TrackSelect/Folders/genes/data/mouse.json +7 -7
  35. package/src/TrackSelect/Folders/genes/human.ts +16 -16
  36. package/src/TrackSelect/Folders/genes/mouse.ts +16 -16
  37. package/src/TrackSelect/Folders/genes/shared/columns.tsx +42 -42
  38. package/src/TrackSelect/Folders/genes/shared/createFolder.ts +68 -68
  39. package/src/TrackSelect/Folders/genes/shared/treeBuilder.ts +45 -45
  40. package/src/TrackSelect/Folders/genes/shared/types.ts +29 -29
  41. package/src/TrackSelect/Folders/index.ts +30 -30
  42. package/src/TrackSelect/Folders/types.ts +106 -106
  43. package/src/TrackSelect/TrackSelect.tsx +11 -7
  44. package/src/TrackSelect/TreeView/CustomTreeItem.tsx +214 -214
  45. package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +145 -145
  46. package/src/TrackSelect/store.ts +117 -117
  47. package/src/TrackSelect/types.ts +121 -121
  48. package/src/lib.ts +13 -13
  49. package/src/vite-env.d.ts +1 -1
  50. package/test/main.tsx +369 -369
  51. package/tsconfig.app.json +25 -25
  52. package/tsconfig.json +7 -7
  53. package/tsconfig.node.json +25 -25
  54. package/vite.config.ts +66 -66
@@ -1,145 +1,145 @@
1
- import { Avatar, Box, Paper, Typography } from "@mui/material";
2
- import { RichTreeView, TreeViewBaseItem } from "@mui/x-tree-view";
3
- import { useEffect, useMemo, useState } from "react";
4
- import {
5
- CustomTreeItemProps,
6
- ExtendedTreeItemProps,
7
- FolderTreeConfig,
8
- TreeViewWrapperProps,
9
- } from "../types";
10
- import { CustomTreeItem } from "./CustomTreeItem";
11
-
12
- /**
13
- * Recursively collects all item IDs that have children (expandable items)
14
- */
15
- function getAllExpandableItemIds(
16
- items: TreeViewBaseItem<ExtendedTreeItemProps>[],
17
- ): string[] {
18
- const ids: string[] = [];
19
- for (const item of items) {
20
- if (item.children && item.children.length > 0) {
21
- ids.push(item.id);
22
- ids.push(...getAllExpandableItemIds(item.children));
23
- }
24
- }
25
- return ids;
26
- }
27
-
28
- /**
29
- * Internal component that renders a single folder's tree with its own expanded state.
30
- */
31
- function FolderTree({
32
- items,
33
- TreeItemComponent,
34
- onRemove,
35
- }: {
36
- items: FolderTreeConfig["items"];
37
- TreeItemComponent: FolderTreeConfig["TreeItemComponent"];
38
- onRemove: (item: TreeViewBaseItem<ExtendedTreeItemProps>) => void;
39
- }) {
40
- const allExpandableIds = useMemo(
41
- () => getAllExpandableItemIds(items),
42
- [items],
43
- );
44
- const [expandedItems, setExpandedItems] =
45
- useState<string[]>(allExpandableIds);
46
-
47
- // Auto-expand new items when they're added
48
- useEffect(() => {
49
- setExpandedItems((prev) => {
50
- const newIds = allExpandableIds.filter((id) => !prev.includes(id));
51
- if (newIds.length > 0) {
52
- return [...prev, ...newIds];
53
- }
54
- return prev;
55
- });
56
- }, [allExpandableIds]);
57
-
58
- const handleRemoveTreeItem = (
59
- item: TreeViewBaseItem<ExtendedTreeItemProps>,
60
- ) => {
61
- onRemove(item);
62
- };
63
-
64
- const TreeItem = TreeItemComponent ?? CustomTreeItem;
65
-
66
- return (
67
- <RichTreeView
68
- items={items}
69
- expandedItems={expandedItems}
70
- onExpandedItemsChange={(_event, ids) => setExpandedItems(ids)}
71
- slots={{ item: TreeItem }}
72
- slotProps={{
73
- item: {
74
- onRemove: handleRemoveTreeItem,
75
- } as Partial<CustomTreeItemProps>,
76
- }}
77
- sx={{
78
- ml: 1,
79
- mr: 1,
80
- }}
81
- itemChildrenIndentation={0}
82
- />
83
- );
84
- }
85
-
86
- export function TreeViewWrapper({
87
- folderTrees,
88
- selectedCount,
89
- onRemove,
90
- }: TreeViewWrapperProps) {
91
- return (
92
- <Paper
93
- sx={{
94
- height: 500,
95
- width: "100%",
96
- border: "10px solid",
97
- borderColor: "grey.200",
98
- boxSizing: "border-box",
99
- borderRadius: 2,
100
- display: "flex",
101
- flexDirection: "column",
102
- }}
103
- >
104
- <Box
105
- sx={{
106
- display: "flex",
107
- alignItems: "center",
108
- gap: 1,
109
- py: 1,
110
- backgroundColor: "grey.200",
111
- flexShrink: 0,
112
- }}
113
- >
114
- <Avatar
115
- sx={{
116
- width: 30,
117
- height: 30,
118
- fontSize: 14,
119
- fontWeight: "bold",
120
- bgcolor: "white",
121
- color: "text.primary",
122
- }}
123
- >
124
- {selectedCount}
125
- </Avatar>
126
- <Typography fontWeight="bold">Active Tracks</Typography>
127
- </Box>
128
- <Box
129
- sx={{
130
- flex: 1,
131
- overflow: "auto",
132
- }}
133
- >
134
- {folderTrees.map((folderTree) => (
135
- <FolderTree
136
- key={folderTree.folderId}
137
- items={folderTree.items}
138
- TreeItemComponent={folderTree.TreeItemComponent}
139
- onRemove={onRemove}
140
- />
141
- ))}
142
- </Box>
143
- </Paper>
144
- );
145
- }
1
+ import { Avatar, Box, Paper, Typography } from "@mui/material";
2
+ import { RichTreeView, TreeViewBaseItem } from "@mui/x-tree-view";
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import {
5
+ CustomTreeItemProps,
6
+ ExtendedTreeItemProps,
7
+ FolderTreeConfig,
8
+ TreeViewWrapperProps,
9
+ } from "../types";
10
+ import { CustomTreeItem } from "./CustomTreeItem";
11
+
12
+ /**
13
+ * Recursively collects all item IDs that have children (expandable items)
14
+ */
15
+ function getAllExpandableItemIds(
16
+ items: TreeViewBaseItem<ExtendedTreeItemProps>[],
17
+ ): string[] {
18
+ const ids: string[] = [];
19
+ for (const item of items) {
20
+ if (item.children && item.children.length > 0) {
21
+ ids.push(item.id);
22
+ ids.push(...getAllExpandableItemIds(item.children));
23
+ }
24
+ }
25
+ return ids;
26
+ }
27
+
28
+ /**
29
+ * Internal component that renders a single folder's tree with its own expanded state.
30
+ */
31
+ function FolderTree({
32
+ items,
33
+ TreeItemComponent,
34
+ onRemove,
35
+ }: {
36
+ items: FolderTreeConfig["items"];
37
+ TreeItemComponent: FolderTreeConfig["TreeItemComponent"];
38
+ onRemove: (item: TreeViewBaseItem<ExtendedTreeItemProps>) => void;
39
+ }) {
40
+ const allExpandableIds = useMemo(
41
+ () => getAllExpandableItemIds(items),
42
+ [items],
43
+ );
44
+ const [expandedItems, setExpandedItems] =
45
+ useState<string[]>(allExpandableIds);
46
+
47
+ // Auto-expand new items when they're added
48
+ useEffect(() => {
49
+ setExpandedItems((prev) => {
50
+ const newIds = allExpandableIds.filter((id) => !prev.includes(id));
51
+ if (newIds.length > 0) {
52
+ return [...prev, ...newIds];
53
+ }
54
+ return prev;
55
+ });
56
+ }, [allExpandableIds]);
57
+
58
+ const handleRemoveTreeItem = (
59
+ item: TreeViewBaseItem<ExtendedTreeItemProps>,
60
+ ) => {
61
+ onRemove(item);
62
+ };
63
+
64
+ const TreeItem = TreeItemComponent ?? CustomTreeItem;
65
+
66
+ return (
67
+ <RichTreeView
68
+ items={items}
69
+ expandedItems={expandedItems}
70
+ onExpandedItemsChange={(_event, ids) => setExpandedItems(ids)}
71
+ slots={{ item: TreeItem }}
72
+ slotProps={{
73
+ item: {
74
+ onRemove: handleRemoveTreeItem,
75
+ } as Partial<CustomTreeItemProps>,
76
+ }}
77
+ sx={{
78
+ ml: 1,
79
+ mr: 1,
80
+ }}
81
+ itemChildrenIndentation={0}
82
+ />
83
+ );
84
+ }
85
+
86
+ export function TreeViewWrapper({
87
+ folderTrees,
88
+ selectedCount,
89
+ onRemove,
90
+ }: TreeViewWrapperProps) {
91
+ return (
92
+ <Paper
93
+ sx={{
94
+ height: 500,
95
+ width: "100%",
96
+ border: "10px solid",
97
+ borderColor: "grey.200",
98
+ boxSizing: "border-box",
99
+ borderRadius: 2,
100
+ display: "flex",
101
+ flexDirection: "column",
102
+ }}
103
+ >
104
+ <Box
105
+ sx={{
106
+ display: "flex",
107
+ alignItems: "center",
108
+ gap: 1,
109
+ py: 1,
110
+ backgroundColor: "grey.200",
111
+ flexShrink: 0,
112
+ }}
113
+ >
114
+ <Avatar
115
+ sx={{
116
+ width: 30,
117
+ height: 30,
118
+ fontSize: 14,
119
+ fontWeight: "bold",
120
+ bgcolor: "white",
121
+ color: "text.primary",
122
+ }}
123
+ >
124
+ {selectedCount}
125
+ </Avatar>
126
+ <Typography fontWeight="bold">Active Tracks</Typography>
127
+ </Box>
128
+ <Box
129
+ sx={{
130
+ flex: 1,
131
+ overflow: "auto",
132
+ }}
133
+ >
134
+ {folderTrees.map((folderTree) => (
135
+ <FolderTree
136
+ key={folderTree.folderId}
137
+ items={folderTree.items}
138
+ TreeItemComponent={folderTree.TreeItemComponent}
139
+ onRemove={onRemove}
140
+ />
141
+ ))}
142
+ </Box>
143
+ </Paper>
144
+ );
145
+ }
@@ -1,117 +1,117 @@
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
- }
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
+ }