@vitessce/launcher 3.8.6

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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +34 -0
  3. package/dist/OrbitControls-BF52POQp.js +18694 -0
  4. package/dist/ReactNeuroglancer-BzSHY22y.js +65780 -0
  5. package/dist/blosc-DvQQ1ST0.js +887 -0
  6. package/dist/chunk-INHXZS53-DHVJiuU3.js +19 -0
  7. package/dist/deflate-CQwOaSLw.js +10 -0
  8. package/dist/higlass-C7qBSekP.js +133163 -0
  9. package/dist/index-B00rYWyD.js +3686 -0
  10. package/dist/index-BIrF4FKE.js +9854 -0
  11. package/dist/index-CYL9MBhI.js +384168 -0
  12. package/dist/index.js +5 -0
  13. package/dist/jpeg-BFml4vEx.js +856 -0
  14. package/dist/lerc-BOiupD76.js +2057 -0
  15. package/dist/lz4-BIbM36RN.js +801 -0
  16. package/dist/lzw-DjHwit8p.js +125 -0
  17. package/dist/packbits-io9qfQZa.js +27 -0
  18. package/dist/pako.esm-SxljTded.js +2216 -0
  19. package/dist/raw-BzEs_FKC.js +9 -0
  20. package/dist/troika-three-text.esm-26C0zM6u.js +5685 -0
  21. package/dist/webimage-CwJqBfRI.js +29 -0
  22. package/dist/zstd-CO575QiM.js +769 -0
  23. package/dist-tsc/ControlledLauncher.d.ts +2 -0
  24. package/dist-tsc/ControlledLauncher.d.ts.map +1 -0
  25. package/dist-tsc/ControlledLauncher.js +129 -0
  26. package/dist-tsc/LauncherStart.d.ts +2 -0
  27. package/dist-tsc/LauncherStart.d.ts.map +1 -0
  28. package/dist-tsc/LauncherStart.js +142 -0
  29. package/dist-tsc/UncontrolledLauncher.d.ts +14 -0
  30. package/dist-tsc/UncontrolledLauncher.d.ts.map +1 -0
  31. package/dist-tsc/UncontrolledLauncher.js +52 -0
  32. package/dist-tsc/index.d.ts +3 -0
  33. package/dist-tsc/index.d.ts.map +1 -0
  34. package/dist-tsc/index.js +2 -0
  35. package/dist-tsc/launcher-styles.d.ts +10 -0
  36. package/dist-tsc/launcher-styles.d.ts.map +1 -0
  37. package/dist-tsc/launcher-styles.js +69 -0
  38. package/dist-tsc/use-hash-param.d.ts +3 -0
  39. package/dist-tsc/use-hash-param.d.ts.map +1 -0
  40. package/dist-tsc/use-hash-param.js +110 -0
  41. package/dist-tsc/use-query-or-hash-param.d.ts +2 -0
  42. package/dist-tsc/use-query-or-hash-param.d.ts.map +1 -0
  43. package/dist-tsc/use-query-or-hash-param.js +51 -0
  44. package/package.json +51 -0
  45. package/src/ControlledLauncher.js +208 -0
  46. package/src/LauncherStart.js +351 -0
  47. package/src/UncontrolledLauncher.js +110 -0
  48. package/src/index.js +2 -0
  49. package/src/launcher-styles.js +71 -0
  50. package/src/use-hash-param.js +124 -0
  51. package/src/use-query-or-hash-param.js +63 -0
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@vitessce/launcher",
3
+ "version": "3.8.6",
4
+ "author": "HIDIVE Lab at HMS",
5
+ "homepage": "http://vitessce.io",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/vitessce/vitessce.git"
9
+ },
10
+ "license": "MIT",
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "files": [
14
+ "src",
15
+ "dist",
16
+ "dist-tsc"
17
+ ],
18
+ "dependencies": {
19
+ "clsx": "^1.1.1",
20
+ "use-query-params": "^1.1.9",
21
+ "react-dropzone": "^14.3.8",
22
+ "@tanstack/react-query": "^5.80.2",
23
+ "@vitessce/all": "3.8.6",
24
+ "@vitessce/constants-internal": "3.8.6",
25
+ "@vitessce/example-configs": "3.8.6",
26
+ "@vitessce/example-plugins": "3.8.6",
27
+ "@vitessce/globals": "3.8.6",
28
+ "@vitessce/styles": "3.8.6",
29
+ "@vitessce/vit-s": "3.8.6",
30
+ "@vitessce/config": "3.8.6"
31
+ },
32
+ "devDependencies": {
33
+ "react": "18.3.1",
34
+ "vite": "^7.0.0",
35
+ "vitest": "^3.1.4"
36
+ },
37
+ "peerDependencies": {
38
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
39
+ },
40
+ "scripts": {
41
+ "bundle": "pnpm exec vite build -c ../../scripts/vite.config.mjs",
42
+ "test": "pnpm exec vitest --run"
43
+ },
44
+ "module": "dist/index.js",
45
+ "exports": {
46
+ ".": {
47
+ "types": "./dist-tsc/index.d.ts",
48
+ "import": "./dist/index.js"
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,208 @@
1
+ /* eslint-disable no-unused-vars */
2
+ /* eslint-disable no-nested-ternary */
3
+ /* eslint-disable max-len */
4
+ /* eslint-disable no-console */
5
+ /* eslint-disable prefer-destructuring */
6
+ import React, { useMemo, useState, useEffect } from 'react';
7
+ import { Vitessce } from '@vitessce/all';
8
+ import { generateConfigAlt as generateConfig, parseUrls } from '@vitessce/config';
9
+ import { Alert } from '@vitessce/styles';
10
+ import clsx from 'clsx';
11
+ import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
12
+ import { useStyles } from './launcher-styles.js';
13
+ import { LauncherStart } from './LauncherStart.js';
14
+
15
+
16
+ function logConfigUpgrade(prevConfig, nextConfig) {
17
+ // eslint-disable-next-line no-console
18
+ console.log(`Upgrade view config schema from ${prevConfig.version} to ${nextConfig.version}`);
19
+ // eslint-disable-next-line no-console
20
+ console.log(prevConfig);
21
+ // eslint-disable-next-line no-console
22
+ console.log(nextConfig);
23
+ }
24
+
25
+ function ControlledLauncherInner(props) {
26
+ const {
27
+ isEditing,
28
+ setIsEditing,
29
+ setIsUsingLocalFiles,
30
+ configUrl,
31
+ setConfigUrl,
32
+ sourceUrlArr,
33
+ setSourceUrlArr,
34
+ marginTop = 0,
35
+ launcherError,
36
+ setLauncherError,
37
+ } = props;
38
+
39
+ const { classes } = useStyles();
40
+
41
+ const [validConfigFromDroppedData, setValidConfigFromDroppedData] = useState(null);
42
+ const [storesFromDroppedData, setStoresFromDroppedData] = useState(null);
43
+
44
+ const configUrlQueryEnabled = !isEditing && !!configUrl;
45
+ const configUrlResult = useQuery({
46
+ enabled: configUrlQueryEnabled,
47
+ queryKey: ['config-from-url', configUrl],
48
+ queryFn: async () => {
49
+ const resp = await fetch(configUrl);
50
+ if (!resp.ok) {
51
+ throw new Error(`Failed to fetch config from URL: ${resp.status} ${resp.statusText}`);
52
+ }
53
+ const configJson = await resp.json();
54
+ return configJson;
55
+ },
56
+ });
57
+
58
+ const sourceUrlQueryEnabled = !isEditing && Array.isArray(sourceUrlArr) && sourceUrlArr.length > 0;
59
+ const sourceUrlResult = useQuery({
60
+ enabled: sourceUrlQueryEnabled,
61
+ queryKey: ['data-metadata-from-urls', sourceUrlArr],
62
+ queryFn: async () => {
63
+ const { config, stores } = await generateConfig(parseUrls(sourceUrlArr));
64
+ return { config, stores };
65
+ },
66
+ });
67
+
68
+ let isLoading = false;
69
+ let validConfig = null;
70
+ let stores = null;
71
+ if (configUrlQueryEnabled) {
72
+ isLoading = configUrlResult.isLoading;
73
+ if (configUrlResult.isSuccess) {
74
+ validConfig = configUrlResult.data;
75
+ setLauncherError(null);
76
+ } else if (configUrlResult.isError) {
77
+ setIsEditing(true);
78
+ setLauncherError(`Error loading config from URL: ${configUrlResult.error.message}`);
79
+ }
80
+ } else if (sourceUrlQueryEnabled) {
81
+ isLoading = sourceUrlResult.isLoading;
82
+ if (sourceUrlResult.isSuccess) {
83
+ validConfig = sourceUrlResult.data.config.toJSON();
84
+ stores = sourceUrlResult.data.stores;
85
+ setLauncherError(null);
86
+ } else if (sourceUrlResult.isError) {
87
+ setIsEditing(true);
88
+ setLauncherError(`Error loading data/metadata from source URL(s): ${sourceUrlResult.error.message}`);
89
+ }
90
+ } else if (validConfigFromDroppedData) {
91
+ validConfig = validConfigFromDroppedData;
92
+ stores = storesFromDroppedData;
93
+ setLauncherError(null);
94
+ }
95
+
96
+
97
+ // Possible states:
98
+ // - needsStart: No URL parameters; Show the launcher UI.
99
+ // - Awaiting URL parameter parsing.
100
+ // - Invalid/unrecognized example ID
101
+ // - Loading config or data/metadata from URL.
102
+ // - Loaded config but was invalid.
103
+ // - Could not identify a valid/supported data format from data file extension(s).
104
+ // - Suggest to append `$supportedFileType` to source URL(s) to specify the data format if file extension is non-standard.
105
+ // - Failed to load config or data/metadata from URL.
106
+ // - Error if BOTH config and source parameters are provided.
107
+ // - Error if BOTH hash and query parameters are provided.
108
+
109
+ const pageMode = false; // TODO
110
+ const PageComponent = null; // TODO
111
+ const isExpanded = true; // TODO
112
+
113
+ const onDropHandler = null; // TODO
114
+ const debug = false; // TODO
115
+ const theme = 'light2'; // TODO
116
+
117
+
118
+ return (isEditing ? (
119
+ <>
120
+ {launcherError ? (
121
+ <Alert severity="error" onClose={() => setLauncherError(null)}>
122
+ {launcherError}
123
+ </Alert>
124
+ ) : null}
125
+ <LauncherStart
126
+ setIsEditing={setIsEditing}
127
+ setIsUsingLocalFiles={setIsUsingLocalFiles}
128
+ configUrl={configUrl}
129
+ setConfigUrl={setConfigUrl}
130
+ sourceUrlArr={sourceUrlArr}
131
+ setSourceUrlArr={setSourceUrlArr}
132
+ setValidConfigFromDroppedData={setValidConfigFromDroppedData}
133
+ setStoresFromDroppedData={setStoresFromDroppedData}
134
+ launcherError={launcherError}
135
+ setLauncherError={setLauncherError}
136
+ />
137
+ </>
138
+ ) : (validConfig ? (
139
+ <main
140
+ className={clsx(classes.vitessceApp, { 'vitessce-expanded': isExpanded, 'vitessce-page': pageMode })}
141
+ style={{
142
+ height: `calc(100vh - ${marginTop}px)`,
143
+ }}
144
+ >
145
+ {pageMode ? (
146
+ <style>{`
147
+ .vitessce-container {
148
+ height: max(100%,100vh);
149
+ width: 100%;
150
+ overflow: hidden;
151
+ position: relative;
152
+ left: 0;
153
+ }
154
+ `}
155
+ </style>
156
+ ) : (
157
+ <style>{`
158
+ .vitessce-container {
159
+ min-height: calc(100vh - ${marginTop}px);
160
+ width: 100%;
161
+ overflow: hidden;
162
+ position: relative;
163
+ left: 0;
164
+ }
165
+ `}
166
+ </style>
167
+ )}
168
+ <Vitessce
169
+ theme={theme}
170
+ validateOnConfigChange={debug}
171
+ onConfigChange={debug ? console.log : undefined}
172
+ onConfigUpgrade={debug ? logConfigUpgrade : undefined}
173
+ config={validConfig}
174
+ height={isExpanded ? undefined : 800}
175
+ pageMode={pageMode}
176
+ stores={stores}
177
+ onDrop={onDropHandler}
178
+ >
179
+ {pageMode && PageComponent ? (<PageComponent />) : null}
180
+ </Vitessce>
181
+ </main>
182
+ ) : (
183
+ isLoading ? (
184
+ <h2>Loading...</h2>
185
+ ) : (
186
+ <p>Error: undefined state</p>
187
+ )
188
+ )));
189
+ }
190
+
191
+
192
+ export function ControlledLauncher(props) {
193
+ const queryClient = useMemo(() => new QueryClient({
194
+ // Reference: https://tanstack.com/query/latest/docs/react/guides/window-focus-refetching
195
+ defaultOptions: {
196
+ queries: {
197
+ refetchOnWindowFocus: false,
198
+ retry: 2,
199
+ },
200
+ },
201
+ }), []);
202
+
203
+ return (
204
+ <QueryClientProvider client={queryClient}>
205
+ <ControlledLauncherInner {...props} />
206
+ </QueryClientProvider>
207
+ );
208
+ }
@@ -0,0 +1,351 @@
1
+ /* eslint-disable no-unused-vars */
2
+ /* eslint-disable react/no-unknown-property */
3
+ /* eslint-disable max-len */
4
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
5
+ import { createOnDrop } from '@vitessce/vit-s';
6
+ import {
7
+ TextField,
8
+ Button,
9
+ Typography,
10
+ Card,
11
+ CardContent,
12
+ Accordion,
13
+ AccordionSummary,
14
+ AccordionDetails,
15
+ ExpandMore,
16
+ } from '@vitessce/styles';
17
+ import clsx from 'clsx';
18
+ import { useStyles } from './launcher-styles.js';
19
+
20
+
21
+ export function LauncherStart(props) {
22
+ const {
23
+ setIsEditing,
24
+ setIsUsingLocalFiles,
25
+ configUrl,
26
+ setConfigUrl,
27
+ sourceUrlArr,
28
+ setSourceUrlArr,
29
+ setValidConfigFromDroppedData,
30
+ setStoresFromDroppedData,
31
+ setLauncherError,
32
+ } = props;
33
+ const { classes } = useStyles();
34
+
35
+ const [spotlightCard, setSpotlightCard] = useState(null);
36
+
37
+ // Single dropzone for both data files/folders and config files.
38
+ const dropzoneRef = useRef(null);
39
+ const localDataInputFoldersRef = useRef(null);
40
+ const localDataInputFilesRef = useRef(null);
41
+ const localConfigInputRef = useRef(null);
42
+
43
+ // Handlers for drag-and-drop and file input changes.
44
+ const onDropHandler = useMemo(() => createOnDrop(
45
+ { setViewConfig: setValidConfigFromDroppedData, setStores: setStoresFromDroppedData },
46
+ false, // isFileInput
47
+ true, // isConfigInput
48
+ ), [setValidConfigFromDroppedData, setStoresFromDroppedData]);
49
+ const onFileInputHandler = useMemo(() => createOnDrop(
50
+ { setViewConfig: setValidConfigFromDroppedData, setStores: setStoresFromDroppedData },
51
+ true, // isFileInput
52
+ true, // isConfigInput
53
+ ), [setValidConfigFromDroppedData, setStoresFromDroppedData]);
54
+
55
+ // Effect for setting up drag-and-drop event listeners for data file/folder input.
56
+ useEffect(() => {
57
+ // Make the entire Launcher a dropzone,
58
+ // and then determine whether the dropped file(s) are config or data files
59
+ // (e.g., if a single .json file, assume config; else data).
60
+ const zone = dropzoneRef.current;
61
+ const zoneDataInput1 = localDataInputFoldersRef.current;
62
+ const zoneDataInput2 = localDataInputFilesRef.current;
63
+ const zoneConfigInput = localConfigInputRef.current;
64
+
65
+ const onDragEnter = (e) => {
66
+ e.preventDefault();
67
+ setSpotlightCard('data-local');
68
+ };
69
+ const onDragLeave = () => {
70
+ setSpotlightCard(null);
71
+ };
72
+ const onDragOver = (e) => {
73
+ e.preventDefault();
74
+ };
75
+ const onDrop = async (e) => {
76
+ e.preventDefault();
77
+
78
+ setIsEditing(false);
79
+ // Clear the other field values.
80
+ setConfigUrl(undefined);
81
+ setSourceUrlArr(undefined);
82
+
83
+ // Call onDrop handler passed in from parent of <VitS/Vitessce/> via prop.
84
+ try {
85
+ await onDropHandler(e);
86
+ setIsUsingLocalFiles(true);
87
+ } catch (err) {
88
+ setLauncherError(`Error processing dropped files: ${err.message}`);
89
+ setIsEditing(true);
90
+ }
91
+ setSpotlightCard(null);
92
+ };
93
+
94
+ const onDataInputChange = async (e) => {
95
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop
96
+ e.preventDefault();
97
+
98
+ setIsEditing(false);
99
+ setSpotlightCard('data-local');
100
+ // Clear the other field values.
101
+ setConfigUrl(undefined);
102
+ setSourceUrlArr(undefined);
103
+
104
+ try {
105
+ await onFileInputHandler(e);
106
+ setIsUsingLocalFiles(true);
107
+ } catch (err) {
108
+ setLauncherError(`Error processing selected files: ${err.message}`);
109
+ setIsEditing(true);
110
+ }
111
+ setSpotlightCard(null);
112
+ };
113
+
114
+ const onConfigInputChange = async (e) => {
115
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop
116
+ e.preventDefault();
117
+
118
+ setIsEditing(false);
119
+ setSpotlightCard('config-local');
120
+ // Clear the other field values.
121
+ setConfigUrl(undefined);
122
+ setSourceUrlArr(undefined);
123
+
124
+ try {
125
+ await onFileInputHandler(e);
126
+ setIsUsingLocalFiles(true);
127
+ } catch (err) {
128
+ setLauncherError(`Error processing selected file: ${err.message}`);
129
+ setIsEditing(true);
130
+ }
131
+ setSpotlightCard(null);
132
+ };
133
+
134
+ // The dragenter event happens at the moment you drag something in to the target element,
135
+ // and then it stops.
136
+ // The dragover event happens during the time you are dragging something until you drop it.
137
+ zone.addEventListener('dragenter', onDragEnter);
138
+ zone.addEventListener('dragleave', onDragLeave);
139
+ zone.addEventListener('dragover', onDragOver);
140
+ zone.addEventListener('drop', onDrop);
141
+ zoneDataInput1.addEventListener('change', onDataInputChange);
142
+ zoneDataInput2.addEventListener('change', onDataInputChange);
143
+ zoneConfigInput.addEventListener('change', onConfigInputChange);
144
+
145
+ return () => {
146
+ zone.removeEventListener('dragenter', onDragEnter);
147
+ zone.removeEventListener('dragleave', onDragLeave);
148
+ zone.removeEventListener('dragover', onDragOver);
149
+ zone.removeEventListener('drop', onDrop);
150
+ zoneDataInput1.removeEventListener('change', onDataInputChange);
151
+ zoneDataInput2.removeEventListener('change', onDataInputChange);
152
+ zoneConfigInput.removeEventListener('change', onConfigInputChange);
153
+ };
154
+ }, [dropzoneRef, localDataInputFoldersRef, localDataInputFilesRef,
155
+ onDropHandler, setIsUsingLocalFiles, setIsEditing, setSpotlightCard,
156
+ onFileInputHandler, setConfigUrl, setSourceUrlArr, setLauncherError,
157
+ ]);
158
+
159
+ return (
160
+ <div className={classes.launcher} ref={dropzoneRef}>
161
+ <div className={classes.launcherRow}>
162
+ <div className={clsx({ [classes.cardDim]: spotlightCard && (spotlightCard === 'config-local' || spotlightCard === 'config-remote') })}>
163
+ <Typography variant="h5">Begin with data</Typography>
164
+ {/* TODO: generalize to support both "begin with data" and "add data" flows? */}
165
+ <p>Let Vitessce <a href="https://vitessce.io/docs/default-config-json/">infer an initial configuration</a> based on the contents of your <a href="https://vitessce.io/docs/data-types-file-types/">data files.</a></p>
166
+ {/* TODO: eventually, link to tutorials/videos showing how to use each of the Begin with... options */}
167
+ </div>
168
+ <div className={classes.launcherCardRow}>
169
+ <Card className={clsx(classes.launcherCard, { [classes.cardDim]: spotlightCard && spotlightCard !== 'data-local' })}>
170
+ <span className={classes.cardDashed}>
171
+ <CardContent className={classes.cardContent}>
172
+ <Typography variant="h6">Local data <br />(Drag and drop)</Typography>
173
+ <p>Select or drop local files (or folders) to view them in Vitessce. Vitessce launches with a default configuration (based on file extensions and contents). Files remain local; no upload occurs.</p>
174
+ <div className={classes.buttonSpacer} />
175
+ <div className={classes.dataButtonGroup}>
176
+ <Button component="label" htmlFor="data-local-input-directory" variant="outlined" fullWidth>
177
+ <span>Select folder(s)</span>
178
+ <input
179
+ id="data-local-input-directory"
180
+ type="file"
181
+ ref={localDataInputFoldersRef}
182
+ className={classes.hiddenFileInput}
183
+ multiple
184
+ directory="true"
185
+ webkitdirectory="true"
186
+ mozdirectory="true"
187
+ />
188
+ </Button>
189
+ &nbsp;
190
+ <Button component="label" htmlFor="data-local-input-files" variant="outlined" fullWidth>
191
+ <span>Select file(s)</span>
192
+ <input
193
+ id="data-local-input-files"
194
+ type="file"
195
+ ref={localDataInputFilesRef}
196
+ className={classes.hiddenFileInput}
197
+ multiple
198
+ />
199
+ </Button>
200
+ </div>
201
+ <div className={classes.buttonSpacer} />
202
+ </CardContent>
203
+ </span>
204
+ </Card>
205
+ <Card className={clsx(classes.launcherCard, { [classes.cardDim]: spotlightCard && spotlightCard !== 'data-remote' })}>
206
+ <CardContent>
207
+ <Typography variant="h6">Remote data <br /> (Load from URL)</Typography>
208
+ <p>
209
+ Enter file (or folder) URLs to view them in Vitessce. Vitessce launches with a default configuration (based on file types and contents). See our <a href="https://vitessce.io/docs/data-hosting/">data hosting</a> documentation for assistance in making your data accessible to Vitessce.&nbsp;
210
+ {/* <span className="select-examples">
211
+ <label>Try an example:&nbsp;</label>
212
+ <select>
213
+ <option>TODO</option>
214
+ </select>
215
+ </span> */}
216
+ </p>
217
+ <div className={classes.textareaAndButton}>
218
+ <TextField
219
+ multiline
220
+ label="Data URL(s)"
221
+ placeholder="One or more file URLs (semicolon-separated)"
222
+ minRows={2}
223
+ className={classes.dataUrlTextarea}
224
+ value={sourceUrlArr ? sourceUrlArr.join(';') : ''}
225
+ onChange={(e) => {
226
+ // Clear existing state.
227
+ setConfigUrl(undefined);
228
+ setSourceUrlArr(e.target.value);
229
+ }}
230
+ onBlur={() => setSpotlightCard(null)}
231
+ onFocus={() => {
232
+ setSpotlightCard('data-remote');
233
+ // We need to set isEditing, otherwise the launcher UI will disappear
234
+ // as soon as there is a non-empty sourceUrlArr value.
235
+ setIsEditing(true);
236
+ }}
237
+ />
238
+ <Button
239
+ variant="outlined"
240
+ onClick={() => {
241
+ setIsEditing(false);
242
+ }}
243
+ >Visualize
244
+ </Button>
245
+ </div>
246
+ <Accordion disableGutters style={{ marginTop: '5px' }}>
247
+ <AccordionSummary
248
+ expandIcon={<ExpandMore />}
249
+ aria-controls="panel1-content"
250
+ id="panel1-header"
251
+ >Specifying file extensions
252
+ </AccordionSummary>
253
+ <AccordionDetails>
254
+ <Typography variant="body2">
255
+ Vitessce automatically tries to infer the file type based on the file extension.
256
+ For URLs with non-standard extensions, the file type can be specified by appending <code><b>$</b>FILE_TYPE</code> to the URL.
257
+ For example, to specify that a folder at <code>https://example.com/my_data.zarr</code> is in SpatialData format, use the URL <code>https://example.com/my_data.zarr<b>$</b>spatialdata.zarr</code>.
258
+ See our <a href="https://vitessce.io/docs/data-types-file-types/">data types and file types documentation</a> for a list of supported file types.
259
+ </Typography>
260
+ </AccordionDetails>
261
+ </Accordion>
262
+ </CardContent>
263
+ </Card>
264
+ </div>
265
+ </div>
266
+ <div className={classes.launcherRow}>
267
+ <div className={clsx({ [classes.cardDim]: spotlightCard && (spotlightCard === 'data-local' || spotlightCard === 'data-remote') })}>
268
+ <Typography variant="h5">Begin with configuration</Typography>
269
+ <p>Use the options below to supply a Vitessce <a href="https://vitessce.io/docs/view-config-json/">configuration in JSON format.</a></p>
270
+ </div>
271
+ <div className={classes.launcherCardRow}>
272
+ {/* <div className={classes.launcherCard}>
273
+ <h3>Config Editor</h3>
274
+ <p>Use the online configuration editor to paste, create, or edit a Vitessce configuration using JSON or JS API syntax.</p>
275
+ <button>Launch JSON editor</button>&nbsp;
276
+ <button>Launch JS editor</button>&nbsp;
277
+ <button>Launch Python editor</button>
278
+ </div> */}
279
+ <Card className={clsx(classes.launcherCard, { [classes.cardDim]: spotlightCard && spotlightCard !== 'config-local' })}>
280
+ <span className={classes.cardDashed}>
281
+ <CardContent className={classes.cardContent}>
282
+ <Typography variant="h6">Local config file <br /> (Drag and drop)</Typography>
283
+ <p>View a configured Vitessce visualization by selecting or dropping a local JSON file.</p>
284
+ <div className={classes.buttonSpacer} />
285
+ <Button component="label" variant="outlined" fullWidth>
286
+ <span>Select file</span>
287
+ <input
288
+ type="file"
289
+ ref={localConfigInputRef}
290
+ className={classes.hiddenFileInput}
291
+ accept=".json,application/json"
292
+ />
293
+ </Button>
294
+ <div className={classes.buttonSpacer} />
295
+ </CardContent>
296
+ </span>
297
+ </Card>
298
+ <Card className={clsx(classes.launcherCard, { [classes.cardDim]: spotlightCard && spotlightCard !== 'config-remote' })}>
299
+ <CardContent>
300
+ <Typography variant="h6">Remote config file <br /> (Load from URL)</Typography>
301
+ <p>
302
+ View a configured Vitessce visualization by specifying a URL to a JSON config file. See our <a href="https://vitessce.io/docs/data-hosting/">data hosting</a> documentation for assistance in making your config file accessible to Vitessce.&nbsp;
303
+ {/* <span className="select-examples">
304
+ <label>Try an example:&nbsp;</label>
305
+ <select>
306
+ <option>TODO</option>
307
+ </select>
308
+ </span> */}
309
+ </p>
310
+ <div className={classes.textareaAndButton}>
311
+ {/* TODO: add another textField in case the user wants to paste JSON and view it via a data URI? */}
312
+ <TextField
313
+ multiline
314
+ label="Config URL"
315
+ placeholder="Enter a URL"
316
+ minRows={2}
317
+ className={classes.dataUrlTextarea}
318
+ value={configUrl || ''}
319
+ onChange={(e) => {
320
+ // Clear existing state.
321
+ setSourceUrlArr(undefined);
322
+ setConfigUrl(e.target.value);
323
+ }}
324
+ onBlur={() => setSpotlightCard(null)}
325
+ onFocus={() => {
326
+ setSpotlightCard('config-remote');
327
+ // We need to set isEditing, otherwise the launcher UI will disappear
328
+ // as soon as there is a non-empty configUrl value.
329
+ setIsEditing(true);
330
+ }}
331
+ />
332
+ <Button
333
+ variant="outlined"
334
+ onClick={() => {
335
+ setIsEditing(false);
336
+ }}
337
+ >Visualize
338
+ </Button>
339
+ </div>
340
+ </CardContent>
341
+ </Card>
342
+ </div>
343
+ {/* TODO: finish implementing the below row once a sidebar is available that allows for adding data post-launching */}
344
+ {/* <div className={classes.launcherRow}>
345
+ <Typography variant="h5">Begin with a view type</Typography>
346
+ <p>Select a view type to get started with configuring a new visualization.</p>
347
+ </div> */}
348
+ </div>
349
+ </div>
350
+ );
351
+ }