aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <marussy@mit.bme.hu>2024-02-24 20:41:53 +0100
committerLibravatar GitHub <noreply@github.com>2024-02-24 20:41:53 +0100
commit833e2f58afaafbea3e6fbf2cb3d9aa53a641fc84 (patch)
tree3b3e495b5718ae9a8833e1b5f9fa34377a11de8f
parentMerge pull request #53 from kris7t/imports (diff)
parentchore(deps); bump dependencies (diff)
downloadrefinery-833e2f58afaafbea3e6fbf2cb3d9aa53a641fc84.tar.gz
refinery-833e2f58afaafbea3e6fbf2cb3d9aa53a641fc84.tar.zst
refinery-833e2f58afaafbea3e6fbf2cb3d9aa53a641fc84.zip
Merge pull request #55 from kris7t/svg-export
Frontend: file management and svg export
-rw-r--r--LICENSES/OFL-1.1.txt43
-rw-r--r--gradle/libs.versions.toml6
-rw-r--r--package.json2
-rw-r--r--subprojects/frontend/config/graphvizUMDVitePlugin.ts1
-rw-r--r--subprojects/frontend/package.json33
-rw-r--r--subprojects/frontend/src/PaneButtons.tsx5
-rw-r--r--subprojects/frontend/src/RootStore.ts33
-rw-r--r--subprojects/frontend/src/TopBar.tsx39
-rw-r--r--subprojects/frontend/src/editor/EditorButtons.tsx30
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts124
-rw-r--r--subprojects/frontend/src/editor/GeneratedModelStore.ts5
-rw-r--r--subprojects/frontend/src/editor/createEditorState.ts15
-rw-r--r--subprojects/frontend/src/graph/DotGraphVisualizer.tsx5
-rw-r--r--subprojects/frontend/src/graph/GraphArea.tsx14
-rw-r--r--subprojects/frontend/src/graph/GraphStore.ts9
-rw-r--r--subprojects/frontend/src/graph/GraphTheme.tsx122
-rw-r--r--subprojects/frontend/src/graph/SlideInDialog.tsx109
-rw-r--r--subprojects/frontend/src/graph/SlideInPanel.tsx97
-rw-r--r--subprojects/frontend/src/graph/VisibilityDialog.tsx318
-rw-r--r--subprojects/frontend/src/graph/VisibilityPanel.tsx303
-rw-r--r--subprojects/frontend/src/graph/export/ExportPanel.tsx227
-rw-r--r--subprojects/frontend/src/graph/export/ExportSettingsStore.ts67
-rw-r--r--subprojects/frontend/src/graph/export/exportDiagram.tsx389
-rw-r--r--subprojects/frontend/src/graph/export/open-sans-latin-bold.ttfbin0 -> 33444 bytes
-rw-r--r--subprojects/frontend/src/graph/export/open-sans-latin-bold.ttf.license8
-rw-r--r--subprojects/frontend/src/graph/export/open-sans-latin-italic.ttfbin0 -> 36192 bytes
-rw-r--r--subprojects/frontend/src/graph/export/open-sans-latin-italic.ttf.license8
-rw-r--r--subprojects/frontend/src/graph/export/open-sans-latin-regular.ttfbin0 -> 33508 bytes
-rw-r--r--subprojects/frontend/src/graph/export/open-sans-latin-regular.ttf.license8
-rw-r--r--subprojects/frontend/src/graph/export/serializePDF.ts37
-rw-r--r--subprojects/frontend/src/graph/postProcessSVG.ts33
-rw-r--r--subprojects/frontend/src/theme/ThemeProvider.tsx4
-rw-r--r--subprojects/frontend/src/utils/fileIO.ts105
-rw-r--r--subprojects/frontend/types/filesystemAccess.d.ts40
-rw-r--r--subprojects/frontend/vite.config.ts5
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java4
-rw-r--r--yarn.lock502
-rw-r--r--yarn.lock.license2
38 files changed, 2155 insertions, 597 deletions
diff --git a/LICENSES/OFL-1.1.txt b/LICENSES/OFL-1.1.txt
new file mode 100644
index 00000000..6fe84ee2
--- /dev/null
+++ b/LICENSES/OFL-1.1.txt
@@ -0,0 +1,43 @@
1SIL OPEN FONT LICENSE
2
3Version 1.1 - 26 February 2007
4
5PREAMBLE
6
7The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
8
9The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
10
11DEFINITIONS
12
13"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
14
15"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
16
17"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
18
19"Modified Version" refers to any derivative made by adding to, deleting, or substituting — in part or in whole — any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
20
21"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
22
23PERMISSION & CONDITIONS
24
25Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
26
271) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
28
292) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
30
313) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
32
334) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
34
355) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
36
37TERMINATION
38
39This license becomes null and void if any of the above conditions are not met.
40
41DISCLAIMER
42
43THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index df12d2f1..0ed6470f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -8,9 +8,9 @@ jetty = "12.0.6"
8jmh = "1.37" 8jmh = "1.37"
9junit = "5.10.2" 9junit = "5.10.2"
10mockito = "5.10.0" 10mockito = "5.10.0"
11mwe2 = "2.17.0.M3" 11mwe2 = "2.17.0"
12slf4j = "2.0.12" 12slf4j = "2.0.12"
13xtext = "2.34.0.M1" 13xtext = "2.34.0.M2"
14 14
15[libraries] 15[libraries]
16eclipseCollections = { group = "org.eclipse.collections", name = "eclipse-collections", version.ref = "eclipseCollections" } 16eclipseCollections = { group = "org.eclipse.collections", name = "eclipse-collections", version.ref = "eclipseCollections" }
@@ -37,7 +37,7 @@ junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", ver
37junit-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" } 37junit-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" }
38mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } 38mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" }
39mockito-junit = { group = "org.mockito", name = "mockito-junit-jupiter", version.ref = "mockito" } 39mockito-junit = { group = "org.mockito", name = "mockito-junit-jupiter", version.ref = "mockito" }
40mwe-utils = { group = "org.eclipse.emf", name = "org.eclipse.emf.mwe.utils", version = "1.11.0.M3" } 40mwe-utils = { group = "org.eclipse.emf", name = "org.eclipse.emf.mwe.utils", version = "1.11.0" }
41mwe2-launch = { group = "org.eclipse.emf", name = "org.eclipse.emf.mwe2.launch", version.ref = "mwe2" } 41mwe2-launch = { group = "org.eclipse.emf", name = "org.eclipse.emf.mwe2.launch", version.ref = "mwe2" }
42mwe2-lib = { group = "org.eclipse.emf", name = "org.eclipse.emf.mwe2.lib", version.ref = "mwe2" } 42mwe2-lib = { group = "org.eclipse.emf", name = "org.eclipse.emf.mwe2.lib", version.ref = "mwe2" }
43ortools = { group = "com.google.ortools", name = "ortools-java", version = "9.8.3296" } 43ortools = { group = "com.google.ortools", name = "ortools-java", version = "9.8.3296" }
diff --git a/package.json b/package.json
index 7822bcc1..f7b8bd83 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
25 }, 25 },
26 "packageManager": "yarn@4.1.0", 26 "packageManager": "yarn@4.1.0",
27 "devDependencies": { 27 "devDependencies": {
28 "eslint": "^8.56.0", 28 "eslint": "^8.57.0",
29 "typescript": "5.3.3" 29 "typescript": "5.3.3"
30 }, 30 },
31 "resolutions": { 31 "resolutions": {
diff --git a/subprojects/frontend/config/graphvizUMDVitePlugin.ts b/subprojects/frontend/config/graphvizUMDVitePlugin.ts
index 0c3c9aa0..8f3511bc 100644
--- a/subprojects/frontend/config/graphvizUMDVitePlugin.ts
+++ b/subprojects/frontend/config/graphvizUMDVitePlugin.ts
@@ -35,7 +35,6 @@ export default function graphvizUMDVitePlugin(): PluginOption {
35 if (resolvedPath === undefined) { 35 if (resolvedPath === undefined) {
36 return; 36 return;
37 } 37 }
38 console.log(resolvedPath);
39 if (command === 'serve') { 38 if (command === 'serve') {
40 url = `/@fs/${resolvedPath}`; 39 url = `/@fs/${resolvedPath}`;
41 } else { 40 } else {
diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json
index 7d5e19d1..73bb463d 100644
--- a/subprojects/frontend/package.json
+++ b/subprojects/frontend/package.json
@@ -1,6 +1,6 @@
1{ 1{
2 "//": [ 2 "//": [
3 "SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>", 3 "SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/>",
4 "", 4 "",
5 "SPDX-License-Identifier: EPL-2.0" 5 "SPDX-License-Identifier: EPL-2.0"
6 ], 6 ],
@@ -33,21 +33,25 @@
33 "@codemirror/language": "^6.10.1", 33 "@codemirror/language": "^6.10.1",
34 "@codemirror/lint": "^6.5.0", 34 "@codemirror/lint": "^6.5.0",
35 "@codemirror/search": "^6.5.6", 35 "@codemirror/search": "^6.5.6",
36 "@codemirror/state": "^6.4.0", 36 "@codemirror/state": "^6.4.1",
37 "@codemirror/view": "^6.24.0", 37 "@codemirror/view": "^6.24.1",
38 "@emotion/cache": "^11.11.0",
38 "@emotion/react": "^11.11.3", 39 "@emotion/react": "^11.11.3",
40 "@emotion/serialize": "^1.1.3",
39 "@emotion/styled": "^11.11.0", 41 "@emotion/styled": "^11.11.0",
42 "@emotion/utils": "^1.2.1",
40 "@fontsource-variable/jetbrains-mono": "^5.0.19", 43 "@fontsource-variable/jetbrains-mono": "^5.0.19",
41 "@fontsource-variable/open-sans": "^5.0.25", 44 "@fontsource-variable/open-sans": "^5.0.25",
45 "@fontsource/open-sans": "^5.0.24",
42 "@hpcc-js/wasm": "^2.16.0", 46 "@hpcc-js/wasm": "^2.16.0",
43 "@lezer/common": "^1.2.1", 47 "@lezer/common": "^1.2.1",
44 "@lezer/highlight": "^1.2.0", 48 "@lezer/highlight": "^1.2.0",
45 "@lezer/lr": "^1.4.0", 49 "@lezer/lr": "^1.4.0",
46 "@material-icons/svg": "^1.0.33", 50 "@material-icons/svg": "^1.0.33",
47 "@mui/icons-material": "^5.15.10", 51 "@mui/icons-material": "^5.15.11",
48 "@mui/material": "^5.15.10", 52 "@mui/material": "^5.15.11",
49 "@mui/system": "^5.15.9", 53 "@mui/system": "^5.15.11",
50 "@mui/x-data-grid": "^6.19.4", 54 "@mui/x-data-grid": "^6.19.5",
51 "ansi-styles": "^6.2.1", 55 "ansi-styles": "^6.2.1",
52 "csstype": "^3.1.3", 56 "csstype": "^3.1.3",
53 "d3": "^7.8.5", 57 "d3": "^7.8.5",
@@ -55,17 +59,19 @@
55 "d3-selection": "^3.0.0", 59 "d3-selection": "^3.0.0",
56 "d3-zoom": "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch", 60 "d3-zoom": "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch",
57 "escape-string-regexp": "^5.0.0", 61 "escape-string-regexp": "^5.0.0",
62 "jspdf": "^2.5.1",
58 "lodash-es": "^4.17.21", 63 "lodash-es": "^4.17.21",
59 "loglevel": "^1.9.1", 64 "loglevel": "^1.9.1",
60 "loglevel-plugin-prefix": "^0.8.4", 65 "loglevel-plugin-prefix": "^0.8.4",
61 "mobx": "^6.12.0", 66 "mobx": "^6.12.0",
62 "mobx-react-lite": "^4.0.5", 67 "mobx-react-lite": "^4.0.5",
63 "ms": "^2.1.3", 68 "ms": "^2.1.3",
64 "nanoid": "^5.0.5", 69 "nanoid": "^5.0.6",
65 "notistack": "^3.0.1", 70 "notistack": "^3.0.1",
66 "react": "^18.2.0", 71 "react": "^18.2.0",
67 "react-dom": "^18.2.0", 72 "react-dom": "^18.2.0",
68 "react-resize-detector": "^10.0.1", 73 "react-resize-detector": "^10.0.1",
74 "svg2pdf.js": "^2.2.3",
69 "xstate": "^4.38.3", 75 "xstate": "^4.38.3",
70 "zod": "^3.22.4" 76 "zod": "^3.22.4"
71 }, 77 },
@@ -75,21 +81,22 @@
75 "@types/d3-graphviz": "^2.6.10", 81 "@types/d3-graphviz": "^2.6.10",
76 "@types/d3-selection": "^3.0.10", 82 "@types/d3-selection": "^3.0.10",
77 "@types/d3-zoom": "^3.0.8", 83 "@types/d3-zoom": "^3.0.8",
78 "@types/eslint": "^8.56.2", 84 "@types/eslint": "^8.56.3",
79 "@types/html-minifier-terser": "^7.0.2", 85 "@types/html-minifier-terser": "^7.0.2",
86 "@types/jspdf": "^2.0.0",
80 "@types/lodash-es": "^4.17.12", 87 "@types/lodash-es": "^4.17.12",
81 "@types/micromatch": "^4.0.6", 88 "@types/micromatch": "^4.0.6",
82 "@types/ms": "^0.7.34", 89 "@types/ms": "^0.7.34",
83 "@types/node": "^20.11.19", 90 "@types/node": "^20.11.20",
84 "@types/pnpapi": "^0.0.5", 91 "@types/pnpapi": "^0.0.5",
85 "@types/react": "^18.2.56", 92 "@types/react": "^18.2.58",
86 "@types/react-dom": "^18.2.19", 93 "@types/react-dom": "^18.2.19",
87 "@typescript-eslint/eslint-plugin": "^6.21.0", 94 "@typescript-eslint/eslint-plugin": "^6.21.0",
88 "@typescript-eslint/parser": "^6.21.0", 95 "@typescript-eslint/parser": "^6.21.0",
89 "@vitejs/plugin-react-swc": "^3.6.0", 96 "@vitejs/plugin-react-swc": "^3.6.0",
90 "@xstate/cli": "^0.5.17", 97 "@xstate/cli": "^0.5.17",
91 "cross-env": "^7.0.3", 98 "cross-env": "^7.0.3",
92 "eslint": "^8.56.0", 99 "eslint": "^8.57.0",
93 "eslint-config-airbnb": "^19.0.4", 100 "eslint-config-airbnb": "^19.0.4",
94 "eslint-config-airbnb-typescript": "^17.1.0", 101 "eslint-config-airbnb-typescript": "^17.1.0",
95 "eslint-config-prettier": "^9.1.0", 102 "eslint-config-prettier": "^9.1.0",
@@ -105,7 +112,7 @@
105 "pnpapi": "^0.0.0", 112 "pnpapi": "^0.0.0",
106 "prettier": "^3.2.5", 113 "prettier": "^3.2.5",
107 "typescript": "5.3.3", 114 "typescript": "5.3.3",
108 "vite": "^5.1.3", 115 "vite": "^5.1.4",
109 "vite-plugin-pwa": "^0.19.0", 116 "vite-plugin-pwa": "^0.19.0",
110 "workbox-window": "^7.0.0" 117 "workbox-window": "^7.0.0"
111 } 118 }
diff --git a/subprojects/frontend/src/PaneButtons.tsx b/subprojects/frontend/src/PaneButtons.tsx
index 7e884ab0..7c759c36 100644
--- a/subprojects/frontend/src/PaneButtons.tsx
+++ b/subprojects/frontend/src/PaneButtons.tsx
@@ -1,5 +1,5 @@
1/* 1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> 2 * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/>
3 * 3 *
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
@@ -27,6 +27,9 @@ const PaneButtonGroup = styled(ToggleButtonGroup, {
27 '.MuiToggleButton-root': { 27 '.MuiToggleButton-root': {
28 fontSize: '1rem', 28 fontSize: '1rem',
29 lineHeight: '1.5', 29 lineHeight: '1.5',
30 // Must remove margin along with the border to avoid the button
31 // moving around (into the space of the missing border) when selected.
32 margin: '0',
30 border: 'none', 33 border: 'none',
31 ...(hideLabel ? {} : { paddingBlock: 6 }), 34 ...(hideLabel ? {} : { paddingBlock: 6 }),
32 '&::before': { 35 '&::before': {
diff --git a/subprojects/frontend/src/RootStore.ts b/subprojects/frontend/src/RootStore.ts
index e277c808..c029f746 100644
--- a/subprojects/frontend/src/RootStore.ts
+++ b/subprojects/frontend/src/RootStore.ts
@@ -5,10 +5,16 @@
5 */ 5 */
6 6
7import { getLogger } from 'loglevel'; 7import { getLogger } from 'loglevel';
8import { makeAutoObservable, runInAction } from 'mobx'; 8import {
9 IReactionDisposer,
10 autorun,
11 makeAutoObservable,
12 runInAction,
13} from 'mobx';
9 14
10import PWAStore from './PWAStore'; 15import PWAStore from './PWAStore';
11import type EditorStore from './editor/EditorStore'; 16import type EditorStore from './editor/EditorStore';
17import ExportSettingsScotre from './graph/export/ExportSettingsStore';
12import Compressor from './persistence/Compressor'; 18import Compressor from './persistence/Compressor';
13import ThemeStore from './theme/ThemeStore'; 19import ThemeStore from './theme/ThemeStore';
14 20
@@ -29,16 +35,26 @@ export default class RootStore {
29 35
30 readonly themeStore: ThemeStore; 36 readonly themeStore: ThemeStore;
31 37
38 readonly exportSettingsStore: ExportSettingsScotre;
39
32 disposed = false; 40 disposed = false;
33 41
42 private titleReaction: IReactionDisposer | undefined;
43
34 constructor() { 44 constructor() {
35 this.pwaStore = new PWAStore(); 45 this.pwaStore = new PWAStore();
36 this.themeStore = new ThemeStore(); 46 this.themeStore = new ThemeStore();
37 makeAutoObservable<RootStore, 'compressor' | 'editorStoreClass'>(this, { 47 this.exportSettingsStore = new ExportSettingsScotre();
48 makeAutoObservable<
49 RootStore,
50 'compressor' | 'editorStoreClass' | 'titleReaction'
51 >(this, {
38 compressor: false, 52 compressor: false,
39 editorStoreClass: false, 53 editorStoreClass: false,
40 pwaStore: false, 54 pwaStore: false,
41 themeStore: false, 55 themeStore: false,
56 exportSettingsStore: false,
57 titleReaction: false,
42 }); 58 });
43 (async () => { 59 (async () => {
44 const { default: EditorStore } = await import('./editor/EditorStore'); 60 const { default: EditorStore } = await import('./editor/EditorStore');
@@ -61,11 +77,21 @@ export default class RootStore {
61 this.initialValue = initialValue; 77 this.initialValue = initialValue;
62 if (this.editorStoreClass !== undefined) { 78 if (this.editorStoreClass !== undefined) {
63 const EditorStore = this.editorStoreClass; 79 const EditorStore = this.editorStoreClass;
64 this.editorStore = new EditorStore( 80 const editorStore = new EditorStore(
65 this.initialValue, 81 this.initialValue,
66 this.pwaStore, 82 this.pwaStore,
67 (text) => this.compressor.compress(text), 83 (text) => this.compressor.compress(text),
68 ); 84 );
85 this.editorStore = editorStore;
86 this.titleReaction?.();
87 this.titleReaction = autorun(() => {
88 const { simpleName, unsavedChanges } = editorStore;
89 if (simpleName === undefined) {
90 document.title = 'Refinery';
91 } else {
92 document.title = `${unsavedChanges ? '\u25cf ' : ''}${simpleName} - Refinery`;
93 }
94 });
69 } 95 }
70 } 96 }
71 97
@@ -73,6 +99,7 @@ export default class RootStore {
73 if (this.disposed) { 99 if (this.disposed) {
74 return; 100 return;
75 } 101 }
102 this.titleReaction?.();
76 this.editorStore?.dispose(); 103 this.editorStore?.dispose();
77 this.compressor.dispose(); 104 this.compressor.dispose();
78 this.disposed = true; 105 this.disposed = true;
diff --git a/subprojects/frontend/src/TopBar.tsx b/subprojects/frontend/src/TopBar.tsx
index 867a24a0..738052c7 100644
--- a/subprojects/frontend/src/TopBar.tsx
+++ b/subprojects/frontend/src/TopBar.tsx
@@ -55,6 +55,27 @@ function useWindowControlsOverlayVisible(): boolean {
55 return windowControlsOverlayVisible; 55 return windowControlsOverlayVisible;
56} 56}
57 57
58function RefineryIcon({ size }: { size: number }): JSX.Element {
59 const theme = useTheme();
60 return (
61 <svg
62 xmlns="http://www.w3.org/2000/svg"
63 width={size}
64 height={size}
65 viewBox="0 0 512 515"
66 >
67 <path
68 d="M447.98 179.335c-139.95-9.583-301.272-50.91-384-147.336v46.117C98.45 129.623 209.442 178.137 294.243 199.1c-84.796 20.963-195.791 69.476-230.265 120.985v46.117c82.73-96.422 244.053-137.752 384.002-147.334z"
69 fill={theme.palette.text.primary}
70 />
71 <path
72 d="M447.98 296.729c-113.755 4.192-287.485 40.727-384 136.557v46.716c95.14-103.612 279.898-137.754 384-143.745z"
73 fill={theme.palette.primary.main}
74 />
75 </svg>
76 );
77}
78
58const DevModeBadge = styled('div')(({ theme }) => ({ 79const DevModeBadge = styled('div')(({ theme }) => ({
59 ...theme.typography.button, 80 ...theme.typography.button,
60 display: 'inline-block', 81 display: 'inline-block',
@@ -64,6 +85,16 @@ const DevModeBadge = styled('div')(({ theme }) => ({
64 borderRadius: theme.shape.borderRadius, 85 borderRadius: theme.shape.borderRadius,
65})); 86}));
66 87
88const FileName = styled('span', {
89 shouldForwardProp: (prop) => prop !== 'unsavedChanges',
90})<{ unsavedChanges: boolean }>(({ theme, unsavedChanges }) => ({
91 marginLeft: theme.spacing(1),
92 fontWeight: theme.typography.fontWeightLight,
93 fontSize: '1.25rem',
94 lineHeight: '1.6rem',
95 fontStyle: unsavedChanges ? 'italic' : 'normal',
96}));
97
67export default observer(function TopBar(): JSX.Element { 98export default observer(function TopBar(): JSX.Element {
68 const { editorStore, themeStore } = useRootStore(); 99 const { editorStore, themeStore } = useRootStore();
69 const overlayVisible = useWindowControlsOverlayVisible(); 100 const overlayVisible = useWindowControlsOverlayVisible();
@@ -101,9 +132,15 @@ export default observer(function TopBar(): JSX.Element {
101 py: 0.5, 132 py: 0.5,
102 }} 133 }}
103 > 134 >
104 <Typography variant="h6" component="h1"> 135 <RefineryIcon size={24} />
136 <Typography variant="h6" component="h1" pl={1}>
105 Refinery {import.meta.env.DEV && <DevModeBadge>Dev</DevModeBadge>} 137 Refinery {import.meta.env.DEV && <DevModeBadge>Dev</DevModeBadge>}
106 </Typography> 138 </Typography>
139 {large && editorStore?.simpleName !== undefined && (
140 <FileName unsavedChanges={editorStore.unsavedChanges}>
141 {editorStore.simpleName}
142 </FileName>
143 )}
107 <Stack direction="row" alignItems="center" flexGrow={1} marginLeft={1}> 144 <Stack direction="row" alignItems="center" flexGrow={1} marginLeft={1}>
108 {medium && !large && ( 145 {medium && !large && (
109 <PaneButtons themeStore={themeStore} hideLabel /> 146 <PaneButtons themeStore={themeStore} hideLabel />
diff --git a/subprojects/frontend/src/editor/EditorButtons.tsx b/subprojects/frontend/src/editor/EditorButtons.tsx
index f4513909..4afba607 100644
--- a/subprojects/frontend/src/editor/EditorButtons.tsx
+++ b/subprojects/frontend/src/editor/EditorButtons.tsx
@@ -7,11 +7,14 @@
7import type { Diagnostic } from '@codemirror/lint'; 7import type { Diagnostic } from '@codemirror/lint';
8import CancelIcon from '@mui/icons-material/Cancel'; 8import CancelIcon from '@mui/icons-material/Cancel';
9import CheckIcon from '@mui/icons-material/Check'; 9import CheckIcon from '@mui/icons-material/Check';
10import FileOpenIcon from '@mui/icons-material/FileOpen';
10import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; 11import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
11import FormatPaintIcon from '@mui/icons-material/FormatPaint'; 12import FormatPaintIcon from '@mui/icons-material/FormatPaint';
12import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; 13import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
13import LooksIcon from '@mui/icons-material/Looks'; 14import LooksIcon from '@mui/icons-material/Looks';
14import RedoIcon from '@mui/icons-material/Redo'; 15import RedoIcon from '@mui/icons-material/Redo';
16import SaveIcon from '@mui/icons-material/Save';
17import SaveAsIcon from '@mui/icons-material/SaveAs';
15import SearchIcon from '@mui/icons-material/Search'; 18import SearchIcon from '@mui/icons-material/Search';
16import UndoIcon from '@mui/icons-material/Undo'; 19import UndoIcon from '@mui/icons-material/Undo';
17import WarningIcon from '@mui/icons-material/Warning'; 20import WarningIcon from '@mui/icons-material/Warning';
@@ -47,10 +50,37 @@ export default observer(function EditorButtons({
47 return ( 50 return (
48 <Stack direction="row" flexGrow={1}> 51 <Stack direction="row" flexGrow={1}>
49 <IconButton 52 <IconButton
53 disabled={editorStore === undefined}
54 onClick={() => editorStore?.openFile()}
55 aria-label="Open"
56 color="inherit"
57 >
58 <FileOpenIcon fontSize="small" />
59 </IconButton>
60 <IconButton
61 disabled={editorStore === undefined || !editorStore.unsavedChanges}
62 onClick={() => editorStore?.saveFile()}
63 aria-label="Save"
64 color="inherit"
65 >
66 <SaveIcon fontSize="small" />
67 </IconButton>
68 {'showSaveFilePicker' in window && (
69 <IconButton
70 disabled={editorStore === undefined}
71 onClick={() => editorStore?.saveFileAs()}
72 aria-label="Save as"
73 color="inherit"
74 >
75 <SaveAsIcon fontSize="small" />
76 </IconButton>
77 )}
78 <IconButton
50 disabled={editorStore === undefined || !editorStore.canUndo} 79 disabled={editorStore === undefined || !editorStore.canUndo}
51 onClick={() => editorStore?.undo()} 80 onClick={() => editorStore?.undo()}
52 aria-label="Undo" 81 aria-label="Undo"
53 color="inherit" 82 color="inherit"
83 sx={{ ml: 1 }}
54 > 84 >
55 <UndoIcon fontSize="small" /> 85 <UndoIcon fontSize="small" />
56 </IconButton> 86 </IconButton>
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts
index 5e7d05e1..33bca382 100644
--- a/subprojects/frontend/src/editor/EditorStore.ts
+++ b/subprojects/frontend/src/editor/EditorStore.ts
@@ -27,6 +27,13 @@ import { nanoid } from 'nanoid';
27 27
28import type PWAStore from '../PWAStore'; 28import type PWAStore from '../PWAStore';
29import GraphStore from '../graph/GraphStore'; 29import GraphStore from '../graph/GraphStore';
30import {
31 type OpenResult,
32 type OpenTextFileResult,
33 openTextFile,
34 saveTextFile,
35 saveBlob,
36} from '../utils/fileIO';
30import getLogger from '../utils/getLogger'; 37import getLogger from '../utils/getLogger';
31import type XtextClient from '../xtext/XtextClient'; 38import type XtextClient from '../xtext/XtextClient';
32import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; 39import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults';
@@ -35,7 +42,10 @@ import EditorErrors from './EditorErrors';
35import GeneratedModelStore from './GeneratedModelStore'; 42import GeneratedModelStore from './GeneratedModelStore';
36import LintPanelStore from './LintPanelStore'; 43import LintPanelStore from './LintPanelStore';
37import SearchPanelStore from './SearchPanelStore'; 44import SearchPanelStore from './SearchPanelStore';
38import createEditorState from './createEditorState'; 45import createEditorState, {
46 createHistoryExtension,
47 historyCompartment,
48} from './createEditorState';
39import { countDiagnostics } from './exposeDiagnostics'; 49import { countDiagnostics } from './exposeDiagnostics';
40import { type IOccurrence, setOccurrences } from './findOccurrences'; 50import { type IOccurrence, setOccurrences } from './findOccurrences';
41import { 51import {
@@ -45,6 +55,25 @@ import {
45 55
46const log = getLogger('editor.EditorStore'); 56const log = getLogger('editor.EditorStore');
47 57
58const REFINERY_CONTENT_TYPE = 'text/x-refinery';
59
60const FILE_PICKER_OPTIONS: FilePickerOptions = {
61 id: 'problem',
62 types: [
63 {
64 description: 'Refinery files',
65 accept: {
66 [REFINERY_CONTENT_TYPE]: [
67 '.problem',
68 '.PROBLEM',
69 '.refinery',
70 '.REFINERY',
71 ],
72 },
73 },
74 ],
75};
76
48export default class EditorStore { 77export default class EditorStore {
49 readonly id: string; 78 readonly id: string;
50 79
@@ -76,6 +105,12 @@ export default class EditorStore {
76 105
77 selectedGeneratedModel: string | undefined; 106 selectedGeneratedModel: string | undefined;
78 107
108 fileName: string | undefined;
109
110 private fileHandle: FileSystemFileHandle | undefined;
111
112 unsavedChanges = false;
113
79 constructor( 114 constructor(
80 initialValue: string, 115 initialValue: string,
81 pwaStore: PWAStore, 116 pwaStore: PWAStore,
@@ -201,6 +236,9 @@ export default class EditorStore {
201 log.trace('Editor transaction', tr); 236 log.trace('Editor transaction', tr);
202 this.state = tr.state; 237 this.state = tr.state;
203 this.client?.onTransaction(tr); 238 this.client?.onTransaction(tr);
239 if (tr.docChanged) {
240 this.unsavedChanges = true;
241 }
204 } 242 }
205 243
206 doCommand(command: Command): boolean { 244 doCommand(command: Command): boolean {
@@ -403,4 +441,88 @@ export default class EditorStore {
403 }); 441 });
404 return generating; 442 return generating;
405 } 443 }
444
445 openFile(): boolean {
446 openTextFile(FILE_PICKER_OPTIONS)
447 .then((result) => this.fileOpened(result))
448 .catch((error) => log.error('Failed to open file', error));
449 return true;
450 }
451
452 private clearUnsavedChanges(): void {
453 this.unsavedChanges = false;
454 }
455
456 private setFile({ name, handle }: OpenResult): void {
457 log.info('Opened file', name);
458 this.fileName = name;
459 this.fileHandle = handle;
460 }
461
462 private fileOpened(result: OpenTextFileResult): void {
463 this.dispatch({
464 changes: [
465 {
466 from: 0,
467 to: this.state.doc.length,
468 insert: result.text,
469 },
470 ],
471 effects: [historyCompartment.reconfigure([])],
472 });
473 // Clear history by removing and re-adding the history extension. See
474 // https://stackoverflow.com/a/77943295 and
475 // https://discuss.codemirror.net/t/codemirror-6-cm-clearhistory-equivalent/2851/10
476 this.dispatch({
477 effects: [historyCompartment.reconfigure([createHistoryExtension()])],
478 });
479 this.setFile(result);
480 this.clearUnsavedChanges();
481 }
482
483 saveFile(): boolean {
484 if (!this.unsavedChanges) {
485 return false;
486 }
487 if (this.fileHandle === undefined) {
488 return this.saveFileAs();
489 }
490 saveTextFile(this.fileHandle, this.state.sliceDoc())
491 .then(() => this.clearUnsavedChanges())
492 .catch((error) => log.error('Failed to save file', error));
493 return true;
494 }
495
496 saveFileAs(): boolean {
497 const blob = new Blob([this.state.sliceDoc()], {
498 type: REFINERY_CONTENT_TYPE,
499 });
500 saveBlob(blob, this.fileName ?? 'graph.problem', FILE_PICKER_OPTIONS)
501 .then((result) => this.fileSavedAs(result))
502 .catch((error) => log.error('Failed to save file', error));
503 return true;
504 }
505
506 private fileSavedAs(result: OpenResult | undefined) {
507 if (result !== undefined) {
508 this.setFile(result);
509 }
510 this.clearUnsavedChanges();
511 }
512
513 get simpleName(): string | undefined {
514 const { fileName } = this;
515 if (fileName === undefined) {
516 return undefined;
517 }
518 const index = fileName.lastIndexOf('.');
519 if (index < 0) {
520 return fileName;
521 }
522 return fileName.substring(0, index);
523 }
524
525 get simpleNameOrFallback(): string {
526 return this.simpleName ?? 'graph';
527 }
406} 528}
diff --git a/subprojects/frontend/src/editor/GeneratedModelStore.ts b/subprojects/frontend/src/editor/GeneratedModelStore.ts
index f2695d9a..4af49e2c 100644
--- a/subprojects/frontend/src/editor/GeneratedModelStore.ts
+++ b/subprojects/frontend/src/editor/GeneratedModelStore.ts
@@ -21,7 +21,7 @@ export default class GeneratedModelStore {
21 graph: GraphStore | undefined; 21 graph: GraphStore | undefined;
22 22
23 constructor( 23 constructor(
24 randomSeed: number, 24 private readonly randomSeed: number,
25 private readonly editorStore: EditorStore, 25 private readonly editorStore: EditorStore,
26 ) { 26 ) {
27 const time = new Date().toLocaleTimeString(undefined, { hour12: false }); 27 const time = new Date().toLocaleTimeString(undefined, { hour12: false });
@@ -50,7 +50,8 @@ export default class GeneratedModelStore {
50 50
51 setSemantics(semantics: SemanticsSuccessResult): void { 51 setSemantics(semantics: SemanticsSuccessResult): void {
52 if (this.running) { 52 if (this.running) {
53 this.graph = new GraphStore(this.editorStore); 53 const name = `${this.editorStore.simpleNameOrFallback}_solution_${this.randomSeed}`;
54 this.graph = new GraphStore(this.editorStore, name);
54 this.graph.setSemantics(semantics); 55 this.graph.setSemantics(semantics);
55 } 56 }
56 } 57 }
diff --git a/subprojects/frontend/src/editor/createEditorState.ts b/subprojects/frontend/src/editor/createEditorState.ts
index 67b8fb9e..9b29228f 100644
--- a/subprojects/frontend/src/editor/createEditorState.ts
+++ b/subprojects/frontend/src/editor/createEditorState.ts
@@ -1,5 +1,5 @@
1/* 1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> 2 * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/>
3 * 3 *
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
@@ -26,7 +26,7 @@ import {
26} from '@codemirror/language'; 26} from '@codemirror/language';
27import { lintKeymap, lintGutter } from '@codemirror/lint'; 27import { lintKeymap, lintGutter } from '@codemirror/lint';
28import { search, searchKeymap } from '@codemirror/search'; 28import { search, searchKeymap } from '@codemirror/search';
29import { EditorState } from '@codemirror/state'; 29import { Compartment, EditorState, type Extension } from '@codemirror/state';
30import { 30import {
31 drawSelection, 31 drawSelection,
32 highlightActiveLine, 32 highlightActiveLine,
@@ -46,6 +46,12 @@ import exposeDiagnostics from './exposeDiagnostics';
46import findOccurrences from './findOccurrences'; 46import findOccurrences from './findOccurrences';
47import semanticHighlighting from './semanticHighlighting'; 47import semanticHighlighting from './semanticHighlighting';
48 48
49export const historyCompartment = new Compartment();
50
51export function createHistoryExtension(): Extension {
52 return history();
53}
54
49export default function createEditorState( 55export default function createEditorState(
50 initialValue: string, 56 initialValue: string,
51 store: EditorStore, 57 store: EditorStore,
@@ -66,7 +72,7 @@ export default function createEditorState(
66 highlightActiveLine(), 72 highlightActiveLine(),
67 highlightActiveLineGutter(), 73 highlightActiveLineGutter(),
68 highlightSpecialChars(), 74 highlightSpecialChars(),
69 history(), 75 historyCompartment.of([createHistoryExtension()]),
70 indentOnInput(), 76 indentOnInput(),
71 rectangularSelection(), 77 rectangularSelection(),
72 search({ 78 search({
@@ -103,6 +109,9 @@ export default function createEditorState(
103 }), 109 }),
104 keymap.of([ 110 keymap.of([
105 { key: 'Mod-Shift-f', run: () => store.formatText() }, 111 { key: 'Mod-Shift-f', run: () => store.formatText() },
112 { key: 'Ctrl-o', run: () => store.openFile() },
113 { key: 'Ctrl-s', run: () => store.saveFile() },
114 { key: 'Ctrl-Shift-s', run: () => store.saveFileAs() },
106 ...closeBracketsKeymap, 115 ...closeBracketsKeymap,
107 ...completionKeymap, 116 ...completionKeymap,
108 ...foldKeymap, 117 ...foldKeymap,
diff --git a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
index 72ac58fa..cc8b5116 100644
--- a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
+++ b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
@@ -30,11 +30,13 @@ function DotGraphVisualizer({
30 fitZoom, 30 fitZoom,
31 transitionTime, 31 transitionTime,
32 animateThreshold, 32 animateThreshold,
33 setSvgContainer,
33}: { 34}: {
34 graph: GraphStore; 35 graph: GraphStore;
35 fitZoom?: FitZoomCallback; 36 fitZoom?: FitZoomCallback;
36 transitionTime?: number; 37 transitionTime?: number;
37 animateThreshold?: number; 38 animateThreshold?: number;
39 setSvgContainer?: (container: HTMLElement | undefined) => void;
38}): JSX.Element { 40}): JSX.Element {
39 const transitionTimeOrDefault = 41 const transitionTimeOrDefault =
40 transitionTime ?? DotGraphVisualizer.defaultProps.transitionTime; 42 transitionTime ?? DotGraphVisualizer.defaultProps.transitionTime;
@@ -48,6 +50,7 @@ function DotGraphVisualizer({
48 50
49 const setElement = useCallback( 51 const setElement = useCallback(
50 (element: HTMLDivElement | null) => { 52 (element: HTMLDivElement | null) => {
53 setSvgContainer?.(element ?? undefined);
51 if (disposerRef.current !== undefined) { 54 if (disposerRef.current !== undefined) {
52 disposerRef.current(); 55 disposerRef.current();
53 disposerRef.current = undefined; 56 disposerRef.current = undefined;
@@ -147,6 +150,7 @@ function DotGraphVisualizer({
147 transitionTimeOrDefault, 150 transitionTimeOrDefault,
148 animateThresholdOrDefault, 151 animateThresholdOrDefault,
149 animate, 152 animate,
153 setSvgContainer,
150 ], 154 ],
151 ); 155 );
152 156
@@ -157,6 +161,7 @@ DotGraphVisualizer.defaultProps = {
157 fitZoom: undefined, 161 fitZoom: undefined,
158 transitionTime: 250, 162 transitionTime: 250,
159 animateThreshold: 100, 163 animateThreshold: 100,
164 setSvgContainer: undefined,
160}; 165};
161 166
162export default observer(DotGraphVisualizer); 167export default observer(DotGraphVisualizer);
diff --git a/subprojects/frontend/src/graph/GraphArea.tsx b/subprojects/frontend/src/graph/GraphArea.tsx
index d5801b9a..b5d93aef 100644
--- a/subprojects/frontend/src/graph/GraphArea.tsx
+++ b/subprojects/frontend/src/graph/GraphArea.tsx
@@ -1,5 +1,5 @@
1/* 1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> 2 * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors <https://refinery.tools/>
3 * 3 *
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
@@ -7,18 +7,21 @@
7import Box from '@mui/material/Box'; 7import Box from '@mui/material/Box';
8import { useTheme } from '@mui/material/styles'; 8import { useTheme } from '@mui/material/styles';
9import { observer } from 'mobx-react-lite'; 9import { observer } from 'mobx-react-lite';
10import { useState } from 'react';
10import { useResizeDetector } from 'react-resize-detector'; 11import { useResizeDetector } from 'react-resize-detector';
11 12
12import DotGraphVisualizer from './DotGraphVisualizer'; 13import DotGraphVisualizer from './DotGraphVisualizer';
13import type GraphStore from './GraphStore'; 14import type GraphStore from './GraphStore';
14import VisibilityPanel from './VisibilityPanel'; 15import VisibilityPanel from './VisibilityPanel';
15import ZoomCanvas from './ZoomCanvas'; 16import ZoomCanvas from './ZoomCanvas';
17import ExportPanel from './export/ExportPanel';
16 18
17function GraphArea({ graph }: { graph: GraphStore }): JSX.Element { 19function GraphArea({ graph }: { graph: GraphStore }): JSX.Element {
18 const { breakpoints } = useTheme(); 20 const { breakpoints } = useTheme();
19 const { ref, width, height } = useResizeDetector({ 21 const { ref, width, height } = useResizeDetector({
20 refreshMode: 'debounce', 22 refreshMode: 'debounce',
21 }); 23 });
24 const [svgContainer, setSvgContainer] = useState<HTMLElement | undefined>();
22 25
23 const breakpoint = breakpoints.values.sm; 26 const breakpoint = breakpoints.values.sm;
24 const dialog = 27 const dialog =
@@ -36,9 +39,16 @@ function GraphArea({ graph }: { graph: GraphStore }): JSX.Element {
36 ref={ref} 39 ref={ref}
37 > 40 >
38 <ZoomCanvas> 41 <ZoomCanvas>
39 {(fitZoom) => <DotGraphVisualizer graph={graph} fitZoom={fitZoom} />} 42 {(fitZoom) => (
43 <DotGraphVisualizer
44 graph={graph}
45 fitZoom={fitZoom}
46 setSvgContainer={setSvgContainer}
47 />
48 )}
40 </ZoomCanvas> 49 </ZoomCanvas>
41 <VisibilityPanel graph={graph} dialog={dialog} /> 50 <VisibilityPanel graph={graph} dialog={dialog} />
51 <ExportPanel graph={graph} svgContainer={svgContainer} dialog={dialog} />
42 </Box> 52 </Box>
43 ); 53 );
44} 54}
diff --git a/subprojects/frontend/src/graph/GraphStore.ts b/subprojects/frontend/src/graph/GraphStore.ts
index 58c4422d..d9282326 100644
--- a/subprojects/frontend/src/graph/GraphStore.ts
+++ b/subprojects/frontend/src/graph/GraphStore.ts
@@ -66,7 +66,10 @@ export default class GraphStore {
66 66
67 selectedSymbol: RelationMetadata | undefined; 67 selectedSymbol: RelationMetadata | undefined;
68 68
69 constructor(private readonly editorStore: EditorStore) { 69 constructor(
70 private readonly editorStore: EditorStore,
71 private readonly nameOverride?: string,
72 ) {
70 makeAutoObservable<GraphStore, 'editorStore'>(this, { 73 makeAutoObservable<GraphStore, 'editorStore'>(this, {
71 editorStore: false, 74 editorStore: false,
72 semantics: observable.ref, 75 semantics: observable.ref,
@@ -190,4 +193,8 @@ export default class GraphStore {
190 get colorNodes(): boolean { 193 get colorNodes(): boolean {
191 return this.editorStore.colorIdentifiers; 194 return this.editorStore.colorIdentifiers;
192 } 195 }
196
197 get name(): string {
198 return this.nameOverride ?? this.editorStore.simpleNameOrFallback;
199 }
193} 200}
diff --git a/subprojects/frontend/src/graph/GraphTheme.tsx b/subprojects/frontend/src/graph/GraphTheme.tsx
index 7334f559..34954345 100644
--- a/subprojects/frontend/src/graph/GraphTheme.tsx
+++ b/subprojects/frontend/src/graph/GraphTheme.tsx
@@ -27,10 +27,10 @@ function createEdgeColor(
27 '& text': { 27 '& text': {
28 fill: stroke, 28 fill: stroke,
29 }, 29 },
30 '& [stroke="black"]': { 30 '.edge-line': {
31 stroke, 31 stroke,
32 }, 32 },
33 '& [fill="black"]': { 33 '.edge-arrow': {
34 fill: fill ?? stroke, 34 fill: fill ?? stroke,
35 }, 35 },
36 }, 36 },
@@ -43,50 +43,72 @@ function createTypeHashStyles(theme: Theme, colorNodes: boolean): CSSObject {
43 } 43 }
44 const result: CSSObject = {}; 44 const result: CSSObject = {};
45 range(theme.palette.highlight.typeHash.length).forEach((i) => { 45 range(theme.palette.highlight.typeHash.length).forEach((i) => {
46 result[`.node-typeHash-${i}`] = { 46 result[`.node-typeHash-${i} .node-header`] = {
47 '& [fill="green"]': { 47 fill: theme.palette.highlight.typeHash[i]?.box,
48 fill: theme.palette.highlight.typeHash[i]?.box,
49 },
50 }; 48 };
51 }); 49 });
52 return result; 50 return result;
53} 51}
54 52
55export default styled('div', { 53function iconStyle(
56 name: 'GraphTheme', 54 svg: string,
57})<{ colorNodes: boolean }>(({ theme, colorNodes }) => ({ 55 color: string,
58 '& svg': { 56 noEmbedIcons?: boolean,
59 userSelect: 'none', 57): CSSObject {
58 if (noEmbedIcons) {
59 return {
60 fill: color,
61 };
62 }
63 return {
64 maskImage: svgURL(svg),
65 background: color,
66 };
67}
68
69export function createGraphTheme({
70 theme,
71 colorNodes,
72 noEmbedIcons,
73}: {
74 theme: Theme;
75 colorNodes: boolean;
76 noEmbedIcons?: boolean;
77}): CSSObject {
78 const shadowAlapha = theme.palette.mode === 'dark' ? 0.32 : 0.24;
79
80 return {
60 '.node': { 81 '.node': {
61 '& text': { 82 '& text': {
62 fontFamily: theme.typography.fontFamily, 83 fontFamily: theme.typography.fontFamily,
63 fill: theme.palette.text.primary, 84 fill: theme.palette.text.primary,
64 }, 85 },
65 '& [stroke="black"]': { 86 '.node-outline': {
66 stroke: theme.palette.text.primary, 87 stroke: theme.palette.text.primary,
67 }, 88 },
68 '& [fill="green"]': { 89 '.node-header': {
69 fill: 90 fill:
70 theme.palette.mode === 'dark' 91 theme.palette.mode === 'dark'
71 ? theme.palette.primary.dark 92 ? theme.palette.primary.dark
72 : theme.palette.primary.light, 93 : theme.palette.primary.light,
73 }, 94 },
74 '& [fill="white"]': { 95 '.node-bg': {
75 fill: theme.palette.background.default, 96 fill: theme.palette.background.default,
76 }, 97 },
77 }, 98 },
78 '.node-INDIVIDUAL': { 99 '.node-INDIVIDUAL .node-outline': {
79 '& [stroke="black"]': { 100 strokeWidth: 2,
80 strokeWidth: 2,
81 },
82 },
83 '.node-shadow[fill="white"]': {
84 fill: alpha(
85 theme.palette.text.primary,
86 theme.palette.mode === 'dark' ? 0.32 : 0.24,
87 ),
88 }, 101 },
89 '.node-exists-UNKNOWN [stroke="black"]': { 102 '.node-shadow.node-bg': noEmbedIcons
103 ? {
104 // Inkscape can't handle opacity in exported SVG.
105 fill: theme.palette.text.primary,
106 opacity: shadowAlapha,
107 }
108 : {
109 fill: alpha(theme.palette.text.primary, shadowAlapha),
110 },
111 '.node-exists-UNKNOWN .node-outline': {
90 strokeDasharray: '5 2', 112 strokeDasharray: '5 2',
91 }, 113 },
92 ...createTypeHashStyles(theme, colorNodes), 114 ...createTypeHashStyles(theme, colorNodes),
@@ -95,39 +117,47 @@ export default styled('div', {
95 fontFamily: theme.typography.fontFamily, 117 fontFamily: theme.typography.fontFamily,
96 fill: theme.palette.text.primary, 118 fill: theme.palette.text.primary,
97 }, 119 },
98 '& [stroke="black"]': { 120 '.edge-line': {
99 stroke: theme.palette.text.primary, 121 stroke: theme.palette.text.primary,
100 }, 122 },
101 '& [fill="black"]': { 123 '.edge-arrow': {
102 fill: theme.palette.text.primary, 124 fill: theme.palette.text.primary,
103 }, 125 },
104 }, 126 },
105 ...createEdgeColor('UNKNOWN', theme.palette.text.secondary, 'none'), 127 ...createEdgeColor('UNKNOWN', theme.palette.text.secondary, 'none'),
106 ...createEdgeColor('ERROR', theme.palette.error.main), 128 ...createEdgeColor('ERROR', theme.palette.error.main),
107 '.icon': { 129 ...(noEmbedIcons
108 maskSize: '12px 12px', 130 ? {}
109 maskPosition: '50% 50%', 131 : {
110 maskRepeat: 'no-repeat', 132 '.icon': {
111 width: '100%', 133 maskSize: '12px 12px',
112 height: '100%', 134 maskPosition: '50% 50%',
113 }, 135 maskRepeat: 'no-repeat',
114 '.icon-TRUE': { 136 width: '100%',
115 maskImage: svgURL(labelSVG), 137 height: '100%',
116 background: theme.palette.text.primary, 138 },
117 }, 139 }),
118 '.icon-UNKNOWN': { 140 '.icon-TRUE': iconStyle(labelSVG, theme.palette.text.primary, noEmbedIcons),
119 maskImage: svgURL(labelOutlinedSVG), 141 '.icon-UNKNOWN': iconStyle(
120 background: theme.palette.text.secondary, 142 labelOutlinedSVG,
121 }, 143 theme.palette.text.secondary,
122 '.icon-ERROR': { 144 noEmbedIcons,
123 maskImage: svgURL(cancelSVG), 145 ),
124 background: theme.palette.error.main, 146 '.icon-ERROR': iconStyle(cancelSVG, theme.palette.error.main, noEmbedIcons),
125 },
126 'text.label-UNKNOWN': { 147 'text.label-UNKNOWN': {
127 fill: theme.palette.text.secondary, 148 fill: theme.palette.text.secondary,
128 }, 149 },
129 'text.label-ERROR': { 150 'text.label-ERROR': {
130 fill: theme.palette.error.main, 151 fill: theme.palette.error.main,
131 }, 152 },
153 };
154}
155
156export default styled('div', {
157 name: 'GraphTheme',
158})<{ colorNodes: boolean }>((args) => ({
159 '& svg': {
160 userSelect: 'none',
161 ...createGraphTheme(args),
132 }, 162 },
133})); 163}));
diff --git a/subprojects/frontend/src/graph/SlideInDialog.tsx b/subprojects/frontend/src/graph/SlideInDialog.tsx
new file mode 100644
index 00000000..d9060fb0
--- /dev/null
+++ b/subprojects/frontend/src/graph/SlideInDialog.tsx
@@ -0,0 +1,109 @@
1/*
2 * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import CloseIcon from '@mui/icons-material/Close';
8import Button from '@mui/material/Button';
9import IconButton from '@mui/material/IconButton';
10import Typography from '@mui/material/Typography';
11import { styled } from '@mui/material/styles';
12import React, { useId } from 'react';
13
14const SlideInDialogRoot = styled('div', {
15 name: 'SlideInDialog-Root',
16 shouldForwardProp: (propName) => propName !== 'dialog',
17})<{ dialog: boolean }>(({ theme, dialog }) => {
18 return {
19 maxHeight: '100%',
20 maxWidth: '100%',
21 overflow: 'hidden',
22 display: 'flex',
23 flexDirection: 'column',
24 '.SlideInDialog-title': {
25 display: 'flex',
26 flexDirection: 'row',
27 alignItems: 'center',
28 padding: theme.spacing(1),
29 paddingLeft: theme.spacing(2),
30 borderBottom: `1px solid ${theme.palette.divider}`,
31 '& h2': {
32 flexGrow: 1,
33 },
34 '.MuiIconButton-root': {
35 flexGrow: 0,
36 flexShrink: 0,
37 marginLeft: theme.spacing(2),
38 },
39 },
40 '.MuiFormControlLabel-root': {
41 marginLeft: 0,
42 paddingTop: theme.spacing(1),
43 paddingLeft: theme.spacing(1),
44 '& + .MuiFormControlLabel-root': {
45 paddingTop: 0,
46 },
47 },
48 '.SlideInDialog-buttons': {
49 padding: theme.spacing(1),
50 display: 'flex',
51 flexDirection: 'row',
52 justifyContent: 'flex-end',
53 ...(dialog
54 ? {
55 marginTop: theme.spacing(1),
56 borderTop: `1px solid ${theme.palette.divider}`,
57 }
58 : {}),
59 },
60 };
61});
62
63export default function SlideInDialog({
64 close,
65 dialog,
66 title,
67 buttons,
68 children,
69}: {
70 close: () => void;
71 dialog?: boolean;
72 title: string;
73 buttons: React.ReactNode | ((close: () => void) => React.ReactNode);
74 children?: React.ReactNode;
75}): JSX.Element {
76 const titleId = useId();
77
78 return (
79 <SlideInDialogRoot
80 dialog={dialog ?? SlideInDialog.defaultProps.dialog}
81 aria-labelledby={dialog ? titleId : undefined}
82 >
83 {dialog && (
84 <div className="SlideInDialog-title">
85 <Typography variant="h6" component="h2" id={titleId}>
86 {title}
87 </Typography>
88 <IconButton aria-label="Close" onClick={close}>
89 <CloseIcon />
90 </IconButton>
91 </div>
92 )}
93 {children}
94 <div className="SlideInDialog-buttons">
95 {typeof buttons === 'function' ? buttons(close) : buttons}
96 {!dialog && (
97 <Button color="inherit" onClick={close}>
98 Close
99 </Button>
100 )}
101 </div>
102 </SlideInDialogRoot>
103 );
104}
105
106SlideInDialog.defaultProps = {
107 dialog: false,
108 children: undefined,
109};
diff --git a/subprojects/frontend/src/graph/SlideInPanel.tsx b/subprojects/frontend/src/graph/SlideInPanel.tsx
new file mode 100644
index 00000000..2c189b5b
--- /dev/null
+++ b/subprojects/frontend/src/graph/SlideInPanel.tsx
@@ -0,0 +1,97 @@
1/*
2 * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import Dialog from '@mui/material/Dialog';
8import IconButton from '@mui/material/IconButton';
9import Paper from '@mui/material/Paper';
10import Slide from '@mui/material/Slide';
11import { styled } from '@mui/material/styles';
12import React, { useCallback, useId, useState } from 'react';
13
14import SlideInDialog from './SlideInDialog';
15
16const SlideInPanelRoot = styled('div', {
17 name: 'SlideInPanel-Root',
18 shouldForwardProp: (propName) => propName !== 'anchor',
19})<{ anchor: 'left' | 'right' }>(({ theme, anchor }) => ({
20 position: 'absolute',
21 padding: theme.spacing(1),
22 top: 0,
23 [anchor]: 0,
24 maxHeight: '100%',
25 maxWidth: '100%',
26 overflow: 'hidden',
27 display: 'flex',
28 flexDirection: 'column',
29 alignItems: anchor === 'left' ? 'start' : 'end',
30 '.SlideInPanel-drawer': {
31 overflow: 'hidden',
32 display: 'flex',
33 maxWidth: '100%',
34 margin: theme.spacing(1),
35 },
36}));
37
38export default function SlideInPanel({
39 anchor,
40 dialog,
41 title,
42 icon,
43 iconLabel,
44 buttons,
45 children,
46}: {
47 anchor: 'left' | 'right';
48 dialog: boolean;
49 title: string;
50 icon: (show: boolean) => React.ReactNode;
51 iconLabel: string;
52 buttons: React.ReactNode | ((close: () => void) => React.ReactNode);
53 children?: React.ReactNode;
54}): JSX.Element {
55 const id = useId();
56 const [show, setShow] = useState(false);
57 const close = useCallback(() => setShow(false), []);
58
59 return (
60 <SlideInPanelRoot anchor={anchor}>
61 <IconButton
62 role="switch"
63 aria-checked={show}
64 aria-controls={dialog ? undefined : id}
65 aria-label={iconLabel}
66 onClick={() => setShow(!show)}
67 >
68 {icon(show)}
69 </IconButton>
70 {dialog ? (
71 <Dialog open={show} onClose={close} maxWidth="xl">
72 <SlideInDialog close={close} dialog title={title} buttons={buttons}>
73 {children}
74 </SlideInDialog>
75 </Dialog>
76 ) : (
77 <Slide
78 direction={anchor === 'left' ? 'right' : 'left'}
79 in={show}
80 id={id}
81 mountOnEnter
82 unmountOnExit
83 >
84 <Paper className="SlideInPanel-drawer" elevation={4}>
85 <SlideInDialog close={close} title={title} buttons={buttons}>
86 {children}
87 </SlideInDialog>
88 </Paper>
89 </Slide>
90 )}
91 </SlideInPanelRoot>
92 );
93}
94
95SlideInPanel.defaultProps = {
96 children: undefined,
97};
diff --git a/subprojects/frontend/src/graph/VisibilityDialog.tsx b/subprojects/frontend/src/graph/VisibilityDialog.tsx
deleted file mode 100644
index bfdcd59f..00000000
--- a/subprojects/frontend/src/graph/VisibilityDialog.tsx
+++ /dev/null
@@ -1,318 +0,0 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import CloseIcon from '@mui/icons-material/Close';
8import FilterListIcon from '@mui/icons-material/FilterList';
9import LabelIcon from '@mui/icons-material/Label';
10import LabelOutlinedIcon from '@mui/icons-material/LabelOutlined';
11import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied';
12import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
13import Button from '@mui/material/Button';
14import Checkbox from '@mui/material/Checkbox';
15import FormControlLabel from '@mui/material/FormControlLabel';
16import IconButton from '@mui/material/IconButton';
17import Switch from '@mui/material/Switch';
18import Typography from '@mui/material/Typography';
19import { styled } from '@mui/material/styles';
20import { observer } from 'mobx-react-lite';
21import { useId } from 'react';
22
23import type GraphStore from './GraphStore';
24import { isVisibilityAllowed } from './GraphStore';
25import RelationName from './RelationName';
26
27const VisibilityDialogRoot = styled('div', {
28 name: 'VisibilityDialog-Root',
29 shouldForwardProp: (propName) => propName !== 'dialog',
30})<{ dialog: boolean }>(({ theme, dialog }) => {
31 const overlayOpacity = dialog ? 0.16 : 0.09;
32 return {
33 maxHeight: '100%',
34 maxWidth: '100%',
35 overflow: 'hidden',
36 display: 'flex',
37 flexDirection: 'column',
38 '.VisibilityDialog-title': {
39 display: 'flex',
40 flexDirection: 'row',
41 alignItems: 'center',
42 padding: theme.spacing(1),
43 paddingLeft: theme.spacing(2),
44 borderBottom: `1px solid ${theme.palette.divider}`,
45 '& h2': {
46 flexGrow: 1,
47 },
48 '.MuiIconButton-root': {
49 flexGrow: 0,
50 flexShrink: 0,
51 marginLeft: theme.spacing(2),
52 },
53 },
54 '.MuiFormControlLabel-root': {
55 marginLeft: 0,
56 paddingTop: theme.spacing(1),
57 paddingLeft: theme.spacing(1),
58 '& + .MuiFormControlLabel-root': {
59 paddingTop: 0,
60 },
61 },
62 '.VisibilityDialog-scroll': {
63 display: 'flex',
64 flexDirection: 'column',
65 height: 'auto',
66 overflowX: 'hidden',
67 overflowY: 'auto',
68 margin: `0 ${theme.spacing(2)}`,
69 '& table': {
70 // We use flexbox instead of `display: table` to get proper text-overflow
71 // behavior for overly long relation names.
72 display: 'flex',
73 flexDirection: 'column',
74 },
75 '& thead, & tbody': {
76 display: 'flex',
77 flexDirection: 'column',
78 },
79 '& thead': {
80 position: 'sticky',
81 top: 0,
82 zIndex: 999,
83 backgroundColor: theme.palette.background.paper,
84 ...(theme.palette.mode === 'dark'
85 ? {
86 // In dark mode, MUI Paper gets a lighter overlay.
87 backgroundImage: `linear-gradient(
88 rgba(255, 255, 255, ${overlayOpacity}),
89 rgba(255, 255, 255, ${overlayOpacity})
90 )`,
91 }
92 : {}),
93 '& tr': {
94 height: '44px',
95 },
96 },
97 '& tr': {
98 display: 'flex',
99 flexDirection: 'row',
100 maxWidth: '100%',
101 },
102 '& tbody tr': {
103 transition: theme.transitions.create('background', {
104 duration: theme.transitions.duration.shortest,
105 }),
106 '&:hover': {
107 background: theme.palette.action.hover,
108 '@media (hover: none)': {
109 background: 'transparent',
110 },
111 },
112 },
113 '& th, & td': {
114 display: 'flex',
115 flexDirection: 'row',
116 alignItems: 'center',
117 justifyContent: 'center',
118 // Set width in advance, since we can't rely on `display: table-cell`.
119 width: '44px',
120 },
121 '& th:nth-of-type(3), & td:nth-of-type(3)': {
122 justifyContent: 'start',
123 paddingLeft: theme.spacing(1),
124 paddingRight: theme.spacing(2),
125 // Only let the last column grow or shrink.
126 flexGrow: 1,
127 flexShrink: 1,
128 // Compute the maximum available space in advance to let the text overflow.
129 maxWidth: 'calc(100% - 88px)',
130 width: 'min-content',
131 },
132 '& td:nth-of-type(3)': {
133 cursor: 'pointer',
134 userSelect: 'none',
135 WebkitTapHighlightColor: 'transparent',
136 },
137
138 '& thead th, .VisibilityDialog-custom tr:last-child td': {
139 borderBottom: `1px solid ${theme.palette.divider}`,
140 },
141 },
142 // Hack to apply `text-overflow`.
143 '.VisibilityDialog-nowrap': {
144 maxWidth: '100%',
145 overflow: 'hidden',
146 wordWrap: 'nowrap',
147 textOverflow: 'ellipsis',
148 },
149 '.VisibilityDialog-buttons': {
150 padding: theme.spacing(1),
151 display: 'flex',
152 flexDirection: 'row',
153 justifyContent: 'flex-end',
154 ...(dialog
155 ? {
156 marginTop: theme.spacing(1),
157 borderTop: `1px solid ${theme.palette.divider}`,
158 }
159 : {}),
160 },
161 '.VisibilityDialog-empty': {
162 display: 'flex',
163 flexDirection: 'column',
164 alignItems: 'center',
165 color: theme.palette.text.secondary,
166 },
167 '.VisibilityDialog-emptyIcon': {
168 fontSize: '6rem',
169 marginBottom: theme.spacing(1),
170 },
171 };
172});
173
174function VisibilityDialog({
175 graph,
176 close,
177 dialog,
178}: {
179 graph: GraphStore;
180 close: () => void;
181 dialog?: boolean;
182}): JSX.Element {
183 const titleId = useId();
184
185 const builtinRows: JSX.Element[] = [];
186 const rows: JSX.Element[] = [];
187 graph.relationMetadata.forEach((metadata, name) => {
188 if (!isVisibilityAllowed(metadata, 'must')) {
189 return;
190 }
191 const visibility = graph.getVisibility(name);
192 const row = (
193 <tr key={metadata.name}>
194 <td>
195 <Checkbox
196 checked={visibility !== 'none'}
197 aria-label={`Show true and error values of ${metadata.simpleName}`}
198 onClick={() =>
199 graph.setVisibility(name, visibility === 'none' ? 'must' : 'none')
200 }
201 />
202 </td>
203 <td>
204 <Checkbox
205 checked={visibility === 'all'}
206 disabled={!isVisibilityAllowed(metadata, 'all')}
207 aria-label={`Show all values of ${metadata.simpleName}`}
208 onClick={() =>
209 graph.setVisibility(name, visibility === 'all' ? 'must' : 'all')
210 }
211 />
212 </td>
213 <td
214 onClick={() => graph.cycleVisibility(name)}
215 aria-label="Toggle visiblity"
216 >
217 <div className="VisibilityDialog-nowrap">
218 <RelationName metadata={metadata} abbreviate={graph.abbreviate} />
219 </div>
220 </td>
221 </tr>
222 );
223 if (name.startsWith('builtin::')) {
224 builtinRows.push(row);
225 } else {
226 rows.push(row);
227 }
228 });
229
230 const hasRows = rows.length > 0 || builtinRows.length > 0;
231
232 return (
233 <VisibilityDialogRoot
234 dialog={dialog ?? VisibilityDialog.defaultProps.dialog}
235 aria-labelledby={dialog ? titleId : undefined}
236 >
237 {dialog && (
238 <div className="VisibilityDialog-title">
239 <Typography variant="h6" component="h2" id={titleId}>
240 Customize view
241 </Typography>
242 <IconButton aria-label="Close" onClick={close}>
243 <CloseIcon />
244 </IconButton>
245 </div>
246 )}
247 <FormControlLabel
248 control={
249 <Switch
250 checked={!graph.abbreviate}
251 onClick={() => graph.toggleAbbrevaite()}
252 />
253 }
254 label="Fully qualified names"
255 />
256 <FormControlLabel
257 control={
258 <Switch checked={graph.scopes} onClick={() => graph.toggleScopes()} />
259 }
260 label="Object scopes"
261 />
262 <div className="VisibilityDialog-scroll">
263 {hasRows ? (
264 <table cellSpacing={0}>
265 <thead>
266 <tr>
267 <th aria-label="Show true and error values">
268 <LabelIcon />
269 </th>
270 <th aria-label="Show unknown values">
271 <LabelOutlinedIcon />
272 </th>
273 <th>Symbol</th>
274 </tr>
275 </thead>
276 <tbody className="VisibilityDialog-custom">{...rows}</tbody>
277 <tbody className="VisibilityDialog-builtin">{...builtinRows}</tbody>
278 </table>
279 ) : (
280 <div className="VisibilityDialog-empty">
281 <SentimentVeryDissatisfiedIcon
282 className="VisibilityDialog-emptyIcon"
283 fontSize="inherit"
284 />
285 <div>Partial model is empty</div>
286 </div>
287 )}
288 </div>
289 <div className="VisibilityDialog-buttons">
290 <Button
291 color="inherit"
292 onClick={() => graph.hideAll()}
293 startIcon={<VisibilityOffIcon />}
294 >
295 Hide all
296 </Button>
297 <Button
298 color="inherit"
299 onClick={() => graph.resetFilter()}
300 startIcon={<FilterListIcon />}
301 >
302 Reset filter
303 </Button>
304 {!dialog && (
305 <Button color="inherit" onClick={close}>
306 Close
307 </Button>
308 )}
309 </div>
310 </VisibilityDialogRoot>
311 );
312}
313
314VisibilityDialog.defaultProps = {
315 dialog: false,
316};
317
318export default observer(VisibilityDialog);
diff --git a/subprojects/frontend/src/graph/VisibilityPanel.tsx b/subprojects/frontend/src/graph/VisibilityPanel.tsx
index 20c4ffca..210ff5d5 100644
--- a/subprojects/frontend/src/graph/VisibilityPanel.tsx
+++ b/subprojects/frontend/src/graph/VisibilityPanel.tsx
@@ -1,43 +1,133 @@
1/* 1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> 2 * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors <https://refinery.tools/>
3 * 3 *
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
6 6
7import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; 7import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
8import FilterListIcon from '@mui/icons-material/FilterList';
9import LabelIcon from '@mui/icons-material/Label';
10import LabelOutlinedIcon from '@mui/icons-material/LabelOutlined';
11import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied';
8import TuneIcon from '@mui/icons-material/Tune'; 12import TuneIcon from '@mui/icons-material/Tune';
13import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
9import Badge from '@mui/material/Badge'; 14import Badge from '@mui/material/Badge';
10import Dialog from '@mui/material/Dialog'; 15import Button from '@mui/material/Button';
11import IconButton from '@mui/material/IconButton'; 16import Checkbox from '@mui/material/Checkbox';
12import Paper from '@mui/material/Paper'; 17import FormControlLabel from '@mui/material/FormControlLabel';
13import Slide from '@mui/material/Slide'; 18import Switch from '@mui/material/Switch';
14import { styled } from '@mui/material/styles'; 19import { styled } from '@mui/material/styles';
15import { observer } from 'mobx-react-lite'; 20import { observer } from 'mobx-react-lite';
16import { useCallback, useId, useState } from 'react'; 21import { useCallback } from 'react';
17 22
18import type GraphStore from './GraphStore'; 23import type GraphStore from './GraphStore';
19import VisibilityDialog from './VisibilityDialog'; 24import { isVisibilityAllowed } from './GraphStore';
25import RelationName from './RelationName';
26import SlideInPanel from './SlideInPanel';
20 27
21const VisibilityPanelRoot = styled('div', { 28const VisibilityDialogScroll = styled('div', {
22 name: 'VisibilityPanel-Root', 29 name: 'VisibilityDialog-Scroll',
23})(({ theme }) => ({ 30 shouldForwardProp: (propName) => propName !== 'dialog',
24 position: 'absolute', 31})<{ dialog: boolean }>(({ theme, dialog }) => {
25 padding: theme.spacing(1), 32 const overlayOpacity = dialog ? 0.16 : 0.09;
26 top: 0, 33 return {
27 left: 0,
28 maxHeight: '100%',
29 maxWidth: '100%',
30 overflow: 'hidden',
31 display: 'flex',
32 flexDirection: 'column',
33 alignItems: 'start',
34 '.VisibilityPanel-drawer': {
35 overflow: 'hidden',
36 display: 'flex', 34 display: 'flex',
37 maxWidth: '100%', 35 flexDirection: 'column',
38 margin: theme.spacing(1), 36 height: 'auto',
39 }, 37 overflowX: 'hidden',
40})); 38 overflowY: 'auto',
39 margin: `0 ${theme.spacing(2)}`,
40 '& table': {
41 // We use flexbox instead of `display: table` to get proper text-overflow
42 // behavior for overly long relation names.
43 display: 'flex',
44 flexDirection: 'column',
45 },
46 '& thead, & tbody': {
47 display: 'flex',
48 flexDirection: 'column',
49 },
50 '& thead': {
51 position: 'sticky',
52 top: 0,
53 zIndex: 999,
54 backgroundColor: theme.palette.background.paper,
55 ...(theme.palette.mode === 'dark'
56 ? {
57 // In dark mode, MUI Paper gets a lighter overlay.
58 backgroundImage: `linear-gradient(
59 rgba(255, 255, 255, ${overlayOpacity}),
60 rgba(255, 255, 255, ${overlayOpacity})
61 )`,
62 }
63 : {}),
64 '& tr': {
65 height: '44px',
66 },
67 },
68 '& tr': {
69 display: 'flex',
70 flexDirection: 'row',
71 maxWidth: '100%',
72 },
73 '& tbody tr': {
74 transition: theme.transitions.create('background', {
75 duration: theme.transitions.duration.shortest,
76 }),
77 '&:hover': {
78 background: theme.palette.action.hover,
79 '@media (hover: none)': {
80 background: 'transparent',
81 },
82 },
83 },
84 '& th, & td': {
85 display: 'flex',
86 flexDirection: 'row',
87 alignItems: 'center',
88 justifyContent: 'center',
89 // Set width in advance, since we can't rely on `display: table-cell`.
90 width: '44px',
91 },
92 '& th:nth-of-type(3), & td:nth-of-type(3)': {
93 justifyContent: 'start',
94 paddingLeft: theme.spacing(1),
95 paddingRight: theme.spacing(2),
96 // Only let the last column grow or shrink.
97 flexGrow: 1,
98 flexShrink: 1,
99 // Compute the maximum available space in advance to let the text overflow.
100 maxWidth: 'calc(100% - 88px)',
101 width: 'min-content',
102 },
103 '& td:nth-of-type(3)': {
104 cursor: 'pointer',
105 userSelect: 'none',
106 WebkitTapHighlightColor: 'transparent',
107 },
108
109 '& thead th, .VisibilityDialog-custom tr:last-child td': {
110 borderBottom: `1px solid ${theme.palette.divider}`,
111 },
112 // Hack to apply `text-overflow`.
113 '.VisibilityDialog-nowrap': {
114 maxWidth: '100%',
115 overflow: 'hidden',
116 wordWrap: 'nowrap',
117 textOverflow: 'ellipsis',
118 },
119 '.VisibilityDialog-empty': {
120 display: 'flex',
121 flexDirection: 'column',
122 alignItems: 'center',
123 color: theme.palette.text.secondary,
124 },
125 '.VisibilityDialog-emptyIcon': {
126 fontSize: '6rem',
127 marginBottom: theme.spacing(1),
128 },
129 };
130});
41 131
42function VisibilityPanel({ 132function VisibilityPanel({
43 graph, 133 graph,
@@ -46,45 +136,132 @@ function VisibilityPanel({
46 graph: GraphStore; 136 graph: GraphStore;
47 dialog: boolean; 137 dialog: boolean;
48}): JSX.Element { 138}): JSX.Element {
49 const id = useId(); 139 const builtinRows: JSX.Element[] = [];
50 const [showFilter, setShowFilter] = useState(false); 140 const rows: JSX.Element[] = [];
51 const close = useCallback(() => setShowFilter(false), []); 141 graph.relationMetadata.forEach((metadata, name) => {
142 if (!isVisibilityAllowed(metadata, 'must')) {
143 return;
144 }
145 const visibility = graph.getVisibility(name);
146 const row = (
147 <tr key={metadata.name}>
148 <td>
149 <Checkbox
150 checked={visibility !== 'none'}
151 aria-label={`Show true and error values of ${metadata.simpleName}`}
152 onClick={() =>
153 graph.setVisibility(name, visibility === 'none' ? 'must' : 'none')
154 }
155 />
156 </td>
157 <td>
158 <Checkbox
159 checked={visibility === 'all'}
160 disabled={!isVisibilityAllowed(metadata, 'all')}
161 aria-label={`Show all values of ${metadata.simpleName}`}
162 onClick={() =>
163 graph.setVisibility(name, visibility === 'all' ? 'must' : 'all')
164 }
165 />
166 </td>
167 <td
168 onClick={() => graph.cycleVisibility(name)}
169 aria-label="Toggle visiblity"
170 >
171 <div className="VisibilityDialog-nowrap">
172 <RelationName metadata={metadata} abbreviate={graph.abbreviate} />
173 </div>
174 </td>
175 </tr>
176 );
177 if (name.startsWith('builtin::')) {
178 builtinRows.push(row);
179 } else {
180 rows.push(row);
181 }
182 });
183
184 const hasRows = rows.length > 0 || builtinRows.length > 0;
185
186 const hideBadge = graph.visibility.size === 0;
187 const icon = useCallback(
188 (show: boolean) => (
189 <Badge color="primary" variant="dot" invisible={hideBadge}>
190 {show && !dialog ? <ChevronLeftIcon /> : <TuneIcon />}
191 </Badge>
192 ),
193 [dialog, hideBadge],
194 );
52 195
53 return ( 196 return (
54 <VisibilityPanelRoot> 197 <SlideInPanel
55 <IconButton 198 anchor="left"
56 role="switch" 199 dialog={dialog}
57 aria-checked={showFilter} 200 title="Customize view"
58 aria-controls={dialog ? undefined : id} 201 icon={icon}
59 aria-label="Show filter panel" 202 iconLabel="Show filter panel"
60 onClick={() => setShowFilter(!showFilter)} 203 buttons={
61 > 204 <>
62 <Badge 205 <Button
63 color="primary" 206 color="inherit"
64 variant="dot" 207 onClick={() => graph.hideAll()}
65 invisible={graph.visibility.size === 0} 208 startIcon={<VisibilityOffIcon />}
66 > 209 >
67 {showFilter && !dialog ? <ChevronLeftIcon /> : <TuneIcon />} 210 Hide all
68 </Badge> 211 </Button>
69 </IconButton> 212 <Button
70 {dialog ? ( 213 color="inherit"
71 <Dialog open={showFilter} onClose={close} maxWidth="xl"> 214 onClick={() => graph.resetFilter()}
72 <VisibilityDialog graph={graph} close={close} dialog /> 215 startIcon={<FilterListIcon />}
73 </Dialog> 216 >
74 ) : ( 217 Reset filter
75 <Slide 218 </Button>
76 direction="right" 219 </>
77 in={showFilter} 220 }
78 id={id} 221 >
79 mountOnEnter 222 <FormControlLabel
80 unmountOnExit 223 control={
81 > 224 <Switch
82 <Paper className="VisibilityPanel-drawer" elevation={4}> 225 checked={!graph.abbreviate}
83 <VisibilityDialog graph={graph} close={close} /> 226 onClick={() => graph.toggleAbbrevaite()}
84 </Paper> 227 />
85 </Slide> 228 }
86 )} 229 label="Fully qualified names"
87 </VisibilityPanelRoot> 230 />
231 <FormControlLabel
232 control={
233 <Switch checked={graph.scopes} onClick={() => graph.toggleScopes()} />
234 }
235 label="Object scopes"
236 />
237 <VisibilityDialogScroll dialog={dialog}>
238 {hasRows ? (
239 <table cellSpacing={0}>
240 <thead>
241 <tr>
242 <th aria-label="Show true and error values">
243 <LabelIcon />
244 </th>
245 <th aria-label="Show unknown values">
246 <LabelOutlinedIcon />
247 </th>
248 <th>Symbol</th>
249 </tr>
250 </thead>
251 <tbody className="VisibilityDialog-custom">{...rows}</tbody>
252 <tbody className="VisibilityDialog-builtin">{...builtinRows}</tbody>
253 </table>
254 ) : (
255 <div className="VisibilityDialog-empty">
256 <SentimentVeryDissatisfiedIcon
257 className="VisibilityDialog-emptyIcon"
258 fontSize="inherit"
259 />
260 <div>Partial model is empty</div>
261 </div>
262 )}
263 </VisibilityDialogScroll>
264 </SlideInPanel>
88 ); 265 );
89} 266}
90 267
diff --git a/subprojects/frontend/src/graph/export/ExportPanel.tsx b/subprojects/frontend/src/graph/export/ExportPanel.tsx
new file mode 100644
index 00000000..c93fa837
--- /dev/null
+++ b/subprojects/frontend/src/graph/export/ExportPanel.tsx
@@ -0,0 +1,227 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import ChevronRightIcon from '@mui/icons-material/ChevronRight';
8import ContentCopyIcon from '@mui/icons-material/ContentCopy';
9import DarkModeIcon from '@mui/icons-material/DarkMode';
10import ImageIcon from '@mui/icons-material/Image';
11import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
12import LightModeIcon from '@mui/icons-material/LightMode';
13import SaveAltIcon from '@mui/icons-material/SaveAlt';
14import ShapeLineIcon from '@mui/icons-material/ShapeLine';
15import Box from '@mui/material/Box';
16import Button from '@mui/material/Button';
17import FormControlLabel from '@mui/material/FormControlLabel';
18import Slider from '@mui/material/Slider';
19import Stack from '@mui/material/Stack';
20import Switch from '@mui/material/Switch';
21import ToggleButton from '@mui/material/ToggleButton';
22import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
23import Typography from '@mui/material/Typography';
24import { styled } from '@mui/material/styles';
25import { observer } from 'mobx-react-lite';
26import { useCallback } from 'react';
27
28import { useRootStore } from '../../RootStoreProvider';
29import getLogger from '../../utils/getLogger';
30import type GraphStore from '../GraphStore';
31import SlideInPanel from '../SlideInPanel';
32
33import exportDiagram from './exportDiagram';
34
35const log = getLogger('graph.ExportPanel');
36
37const SwitchButtonGroup = styled(ToggleButtonGroup, {
38 name: 'ExportPanel-SwitchButtonGroup',
39})(({ theme }) => ({
40 marginTop: theme.spacing(2),
41 marginInline: theme.spacing(2),
42 minWidth: '260px',
43 '.MuiToggleButton-root': {
44 width: '100%',
45 fontSize: '1rem',
46 lineHeight: '1.5',
47 },
48 '& svg': {
49 margin: '0 6px 0 0',
50 },
51}));
52
53function getLabel(value: number): string {
54 return `${value}%`;
55}
56
57const marks = [100, 200, 300, 400].map((value) => ({
58 value,
59 label: (
60 <Stack direction="column" alignItems="center">
61 <ImageIcon sx={{ width: `${11 + (value / 100) * 3}px` }} />
62 <Typography variant="caption">{getLabel(value)}</Typography>
63 </Stack>
64 ),
65}));
66
67function ExportPanel({
68 graph,
69 svgContainer,
70 dialog,
71}: {
72 graph: GraphStore;
73 svgContainer: HTMLElement | undefined;
74 dialog: boolean;
75}): JSX.Element {
76 const { exportSettingsStore } = useRootStore();
77
78 const icon = useCallback(
79 (show: boolean) =>
80 show && !dialog ? <ChevronRightIcon /> : <SaveAltIcon />,
81 [dialog],
82 );
83
84 const { format } = exportSettingsStore;
85 const emptyGraph = graph.semantics.nodes.length === 0;
86 const buttons = useCallback(
87 (close: () => void) => (
88 <>
89 <Button
90 color="inherit"
91 startIcon={<SaveAltIcon />}
92 disabled={emptyGraph}
93 onClick={() => {
94 exportDiagram(svgContainer, graph, exportSettingsStore, 'download')
95 .then(close)
96 .catch((error) => {
97 log.error('Failed to download diagram', error);
98 });
99 }}
100 >
101 Download
102 </Button>
103 {'write' in navigator.clipboard && format === 'png' && (
104 <Button
105 color="inherit"
106 startIcon={<ContentCopyIcon />}
107 disabled={emptyGraph}
108 onClick={() => {
109 exportDiagram(svgContainer, graph, exportSettingsStore, 'copy')
110 .then(close)
111 .catch((error) => {
112 log.error('Failed to copy diagram', error);
113 });
114 }}
115 >
116 Copy
117 </Button>
118 )}
119 </>
120 ),
121 [svgContainer, graph, exportSettingsStore, format, emptyGraph],
122 );
123
124 return (
125 <SlideInPanel
126 anchor="right"
127 dialog={dialog}
128 title="Export diagram"
129 icon={icon}
130 iconLabel="Show export panel"
131 buttons={buttons}
132 >
133 <SwitchButtonGroup size="small" className="rounded">
134 <ToggleButton
135 value="svg"
136 selected={exportSettingsStore.format === 'svg'}
137 onClick={() => exportSettingsStore.setFormat('svg')}
138 >
139 <ShapeLineIcon fontSize="small" /> SVG
140 </ToggleButton>
141 <ToggleButton
142 value="pdf"
143 selected={exportSettingsStore.format === 'pdf'}
144 onClick={() => exportSettingsStore.setFormat('pdf')}
145 >
146 <InsertDriveFileOutlinedIcon fontSize="small" /> PDF
147 </ToggleButton>
148 <ToggleButton
149 value="png"
150 selected={exportSettingsStore.format === 'png'}
151 onClick={() => exportSettingsStore.setFormat('png')}
152 >
153 <ImageIcon fontSize="small" /> PNG
154 </ToggleButton>
155 </SwitchButtonGroup>
156 <SwitchButtonGroup size="small" className="rounded">
157 <ToggleButton
158 value="svg"
159 selected={exportSettingsStore.theme === 'light'}
160 onClick={() => exportSettingsStore.setTheme('light')}
161 >
162 <LightModeIcon fontSize="small" /> Light
163 </ToggleButton>
164 <ToggleButton
165 value="png"
166 selected={exportSettingsStore.theme === 'dark'}
167 onClick={() => exportSettingsStore.setTheme('dark')}
168 >
169 <DarkModeIcon fontSize="small" /> Dark
170 </ToggleButton>
171 </SwitchButtonGroup>
172 <FormControlLabel
173 control={
174 <Switch
175 checked={exportSettingsStore.transparent}
176 onClick={() => exportSettingsStore.toggleTransparent()}
177 />
178 }
179 label="Transparent background"
180 />
181 {exportSettingsStore.canEmbedFonts && (
182 <FormControlLabel
183 control={
184 <Switch
185 checked={exportSettingsStore.embedFonts}
186 onClick={() => exportSettingsStore.toggleEmbedFonts()}
187 />
188 }
189 label={
190 <Stack direction="column">
191 <Typography>Embed fonts</Typography>
192 <Typography variant="caption">
193 {exportSettingsStore.format === 'pdf' ? (
194 <>+20&thinsp;kB fully embedded</>
195 ) : (
196 <>+75&thinsp;kB, only supported in browsers</>
197 )}
198 </Typography>
199 </Stack>
200 }
201 />
202 )}
203 {exportSettingsStore.canScale && (
204 <Box mx={4} mt={1} mb={2}>
205 <Slider
206 aria-label="Image scale"
207 value={exportSettingsStore.scale}
208 min={100}
209 max={400}
210 valueLabelFormat={getLabel}
211 getAriaValueText={getLabel}
212 step={50}
213 valueLabelDisplay="auto"
214 marks={marks}
215 onChange={(_, value) => {
216 if (typeof value === 'number') {
217 exportSettingsStore.setScale(value);
218 }
219 }}
220 />
221 </Box>
222 )}
223 </SlideInPanel>
224 );
225}
226
227export default observer(ExportPanel);
diff --git a/subprojects/frontend/src/graph/export/ExportSettingsStore.ts b/subprojects/frontend/src/graph/export/ExportSettingsStore.ts
new file mode 100644
index 00000000..53a161ab
--- /dev/null
+++ b/subprojects/frontend/src/graph/export/ExportSettingsStore.ts
@@ -0,0 +1,67 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import { makeAutoObservable } from 'mobx';
8
9export type ExportFormat = 'svg' | 'pdf' | 'png';
10export type ExportTheme = 'light' | 'dark';
11
12export default class ExportSettingsStore {
13 format: ExportFormat = 'svg';
14
15 theme: ExportTheme = 'light';
16
17 transparent = true;
18
19 embedSVGFonts = false;
20
21 embedPDFFonts = true;
22
23 scale = 100;
24
25 constructor() {
26 makeAutoObservable(this);
27 }
28
29 setFormat(format: ExportFormat): void {
30 this.format = format;
31 }
32
33 setTheme(theme: ExportTheme): void {
34 this.theme = theme;
35 }
36
37 toggleTransparent(): void {
38 this.transparent = !this.transparent;
39 }
40
41 toggleEmbedFonts(): void {
42 this.embedFonts = !this.embedFonts;
43 }
44
45 setScale(scale: number): void {
46 this.scale = scale;
47 }
48
49 get embedFonts(): boolean {
50 return this.format === 'pdf' ? this.embedPDFFonts : this.embedSVGFonts;
51 }
52
53 private set embedFonts(embedFonts: boolean) {
54 if (this.format === 'pdf') {
55 this.embedPDFFonts = embedFonts;
56 }
57 this.embedSVGFonts = embedFonts;
58 }
59
60 get canEmbedFonts(): boolean {
61 return this.format === 'svg' || this.format === 'pdf';
62 }
63
64 get canScale(): boolean {
65 return this.format === 'png';
66 }
67}
diff --git a/subprojects/frontend/src/graph/export/exportDiagram.tsx b/subprojects/frontend/src/graph/export/exportDiagram.tsx
new file mode 100644
index 00000000..44489d28
--- /dev/null
+++ b/subprojects/frontend/src/graph/export/exportDiagram.tsx
@@ -0,0 +1,389 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import createCache from '@emotion/cache';
8import { serializeStyles } from '@emotion/serialize';
9import type { StyleSheet } from '@emotion/utils';
10import italicFontURL from '@fontsource/open-sans/files/open-sans-latin-400-italic.woff2?url';
11import normalFontURL from '@fontsource/open-sans/files/open-sans-latin-400-normal.woff2?url';
12import boldFontURL from '@fontsource/open-sans/files/open-sans-latin-700-normal.woff2?url';
13import variableItalicFontURL from '@fontsource-variable/open-sans/files/open-sans-latin-wght-italic.woff2?url';
14import variableFontURL from '@fontsource-variable/open-sans/files/open-sans-latin-wght-normal.woff2?url';
15import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw';
16import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw';
17import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw';
18import type { Theme } from '@mui/material/styles';
19
20import { darkTheme, lightTheme } from '../../theme/ThemeProvider';
21import { copyBlob, saveBlob } from '../../utils/fileIO';
22import type GraphStore from '../GraphStore';
23import { createGraphTheme } from '../GraphTheme';
24import { SVG_NS } from '../postProcessSVG';
25
26import type ExportSettingsStore from './ExportSettingsStore';
27
28const PROLOG = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>';
29const PNG_CONTENT_TYPE = 'image/png';
30const SVG_CONTENT_TYPE = 'image/svg+xml';
31const EXPORT_ID = 'export-image';
32
33const ICONS: Map<string, Element> = new Map();
34
35function importSVG(svgSource: string, className: string): void {
36 const parser = new DOMParser();
37 const svgDocument = parser.parseFromString(svgSource, SVG_CONTENT_TYPE);
38 const root = svgDocument.children[0];
39 if (root === undefined) {
40 return;
41 }
42 root.id = className;
43 root.classList.add(className);
44 ICONS.set(className, root);
45}
46
47importSVG(labelSVG, 'icon-TRUE');
48importSVG(labelOutlinedSVG, 'icon-UNKNOWN');
49importSVG(cancelSVG, 'icon-ERROR');
50
51function addBackground(
52 svgDocument: XMLDocument,
53 svg: SVGSVGElement,
54 theme: Theme,
55): void {
56 const viewBox = svg.getAttribute('viewBox')?.split(' ');
57 const rect = svgDocument.createElementNS(SVG_NS, 'rect');
58 rect.setAttribute('x', viewBox?.[0] ?? '0');
59 rect.setAttribute('y', viewBox?.[1] ?? '0');
60 rect.setAttribute('width', viewBox?.[2] ?? '0');
61 rect.setAttribute('height', viewBox?.[3] ?? '0');
62 rect.setAttribute('fill', theme.palette.background.default);
63 svg.prepend(rect);
64}
65
66async function fetchAsFontURL(url: string): Promise<string> {
67 const fetchResult = await fetch(url);
68 const buffer = await fetchResult.arrayBuffer();
69 const blob = new Blob([buffer], { type: 'font/woff2' });
70 return new Promise((resolve, reject) => {
71 const fileReader = new FileReader();
72 fileReader.addEventListener('load', () => {
73 resolve(fileReader.result as string);
74 });
75 fileReader.addEventListener('error', () => {
76 reject(fileReader.error);
77 });
78 fileReader.readAsDataURL(blob);
79 });
80}
81
82let fontCSS: string | undefined;
83let variableFontCSS: string | undefined;
84
85async function fetchFontCSS(): Promise<string> {
86 if (fontCSS !== undefined) {
87 return fontCSS;
88 }
89 const [normalDataURL, boldDataURL, italicDataURL] = await Promise.all([
90 fetchAsFontURL(normalFontURL),
91 fetchAsFontURL(boldFontURL),
92 fetchAsFontURL(italicFontURL),
93 ]);
94 fontCSS = `
95@font-face {
96 font-family: 'Open Sans';
97 font-style: normal;
98 font-display: swap;
99 font-weight: 400;
100 src: url(${normalDataURL}) format('woff2');
101}
102@font-face {
103 font-family: 'Open Sans';
104 font-style: normal;
105 font-display: swap;
106 font-weight: 700;
107 src: url(${boldDataURL}) format('woff2');
108}
109@font-face {
110 font-family: 'Open Sans';
111 font-style: italic;
112 font-display: swap;
113 font-weight: 400;
114 src: url(${italicDataURL}) format('woff2');
115}`;
116 return fontCSS;
117}
118
119async function fetchVariableFontCSS(): Promise<string> {
120 if (variableFontCSS !== undefined) {
121 return variableFontCSS;
122 }
123 const [variableDataURL, variableItalicDataURL] = await Promise.all([
124 fetchAsFontURL(variableFontURL),
125 fetchAsFontURL(variableItalicFontURL),
126 ]);
127 variableFontCSS = `
128@font-face {
129 font-family: 'Open Sans Variable';
130 font-style: normal;
131 font-display: swap;
132 font-weight: 300 800;
133 src: url(${variableDataURL}) format('woff2-variations');
134}
135@font-face {
136 font-family: 'Open Sans Variable';
137 font-style: italic;
138 font-display: swap;
139 font-weight: 300 800;
140 src: url(${variableItalicDataURL}) format('woff2-variations');
141}`;
142 return variableFontCSS;
143}
144
145function appendStyles(
146 svgDocument: XMLDocument,
147 svg: SVGSVGElement,
148 theme: Theme,
149 colorNodes: boolean,
150 fontsCSS: string,
151): void {
152 const cache = createCache({
153 key: 'refinery',
154 container: svg,
155 prepend: true,
156 });
157 // @ts-expect-error `CSSObject` types don't match up between `@mui/material` and
158 // `@emotion/serialize`, but they are compatible in practice.
159 const styles = serializeStyles([createGraphTheme], cache.registered, {
160 theme,
161 colorNodes,
162 noEmbedIcons: true,
163 });
164 const rules: string[] = [fontsCSS];
165 const sheet = {
166 insert(rule) {
167 rules.push(rule);
168 },
169 } as StyleSheet;
170 cache.insert('', styles, sheet, false);
171 const styleElement = svgDocument.createElementNS(SVG_NS, 'style');
172 svg.prepend(styleElement);
173 styleElement.innerHTML = rules.join('');
174}
175
176function fixForeignObjects(svgDocument: XMLDocument, svg: SVGSVGElement): void {
177 const foreignObjects: SVGForeignObjectElement[] = [];
178 svg
179 .querySelectorAll('foreignObject')
180 .forEach((object) => foreignObjects.push(object));
181 foreignObjects.forEach((object) => {
182 const useElement = svgDocument.createElementNS(SVG_NS, 'use');
183 let x = Number(object.getAttribute('x') ?? '0');
184 let y = Number(object.getAttribute('y') ?? '0');
185 const width = Number(object.getAttribute('width') ?? '0');
186 const height = Number(object.getAttribute('height') ?? '0');
187 const size = Math.min(width, height);
188 x += (width - size) / 2;
189 y += (height - size) / 2;
190 useElement.setAttribute('x', String(x));
191 useElement.setAttribute('y', String(y));
192 useElement.setAttribute('width', String(size));
193 useElement.setAttribute('height', String(size));
194 useElement.id = object.id;
195 object.children[0]?.classList?.forEach((className) => {
196 useElement.classList.add(className);
197 if (ICONS.has(className)) {
198 useElement.setAttribute('href', `#${className}`);
199 }
200 });
201 object.replaceWith(useElement);
202 });
203 const defs = svgDocument.createElementNS(SVG_NS, 'defs');
204 svg.prepend(defs);
205 ICONS.forEach((value) => {
206 const importedValue = svgDocument.importNode(value, true);
207 defs.appendChild(importedValue);
208 });
209}
210
211function serializeSVG(svgDocument: XMLDocument): Blob {
212 const serializer = new XMLSerializer();
213 const svgText = `${PROLOG}\n${serializer.serializeToString(svgDocument)}`;
214 return new Blob([svgText], {
215 type: SVG_CONTENT_TYPE,
216 });
217}
218
219async function serializePNG(
220 serializedSVG: Blob,
221 svg: SVGSVGElement,
222 settings: ExportSettingsStore,
223 theme: Theme,
224): Promise<Blob> {
225 const scale = settings.scale / 100;
226 const baseWidth = svg.width.baseVal.value;
227 const baseHeight = svg.height.baseVal.value;
228 const exactWidth = baseWidth * scale;
229 const exactHeight = baseHeight * scale;
230 const width = Math.round(exactWidth);
231 const height = Math.round(exactHeight);
232
233 const canvas = document.createElement('canvas');
234 canvas.width = width;
235 canvas.height = height;
236
237 const image = document.createElement('img');
238 const url = window.URL.createObjectURL(serializedSVG);
239 try {
240 await new Promise((resolve, reject) => {
241 image.addEventListener('load', () => resolve(undefined));
242 image.addEventListener('error', ({ error }) =>
243 reject(
244 error instanceof Error
245 ? error
246 : new Error(`Failed to load image: ${error}`),
247 ),
248 );
249 image.src = url;
250 });
251 } finally {
252 window.URL.revokeObjectURL(url);
253 }
254
255 const context = canvas.getContext('2d');
256 if (context === null) {
257 throw new Error('Failed to get canvas 2D context');
258 }
259 if (!settings.transparent) {
260 context.fillStyle = theme.palette.background.default;
261 context.fillRect(0, 0, width, height);
262 }
263 context.drawImage(
264 image,
265 0,
266 0,
267 baseWidth,
268 baseHeight,
269 0,
270 0,
271 exactWidth,
272 exactHeight,
273 );
274
275 return new Promise<Blob>((resolve, reject) => {
276 canvas.toBlob((exportedBlob) => {
277 if (exportedBlob === null) {
278 reject(new Error('Failed to export PNG blob'));
279 } else {
280 resolve(exportedBlob);
281 }
282 }, PNG_CONTENT_TYPE);
283 });
284}
285
286let serializePDFCached:
287 | ((svg: SVGSVGElement, embedFonts: boolean) => Promise<Blob>)
288 | undefined;
289
290async function serializePDF(
291 svg: SVGSVGElement,
292 settings: ExportSettingsStore,
293): Promise<Blob> {
294 if (serializePDFCached === undefined) {
295 serializePDFCached = (await import('./serializePDF')).default;
296 }
297 return serializePDFCached(svg, settings.embedFonts);
298}
299
300export default async function exportDiagram(
301 svgContainer: HTMLElement | undefined,
302 graph: GraphStore,
303 settings: ExportSettingsStore,
304 mode: 'download' | 'copy',
305): Promise<void> {
306 const svg = svgContainer?.querySelector('svg');
307 if (!svg) {
308 return;
309 }
310 const svgDocument = document.implementation.createDocument(
311 SVG_NS,
312 'svg',
313 null,
314 );
315 const copyOfSVG = svgDocument.importNode(svg, true);
316 const originalRoot = svgDocument.childNodes[0];
317 if (originalRoot === undefined) {
318 svgDocument.appendChild(copyOfSVG);
319 } else {
320 svgDocument.replaceChild(copyOfSVG, originalRoot);
321 }
322
323 const theme = settings.theme === 'light' ? lightTheme : darkTheme;
324 if (!settings.transparent) {
325 addBackground(svgDocument, copyOfSVG, theme);
326 }
327
328 fixForeignObjects(svgDocument, copyOfSVG);
329
330 const { colorNodes } = graph;
331 let fontsCSS = '';
332 if (settings.format === 'png') {
333 // If we are creating a PNG, font file size doesn't matter,
334 // and we can reuse fonts the browser has already downloaded.
335 fontsCSS = await fetchVariableFontCSS();
336 } else if (settings.format === 'svg' && settings.embedFonts) {
337 fontsCSS = await fetchFontCSS();
338 }
339 appendStyles(svgDocument, copyOfSVG, theme, colorNodes, fontsCSS);
340
341 if (settings.format === 'pdf') {
342 const pdf = await serializePDF(copyOfSVG, settings);
343 await saveBlob(pdf, `${graph.name}.pdf`, {
344 id: EXPORT_ID,
345 types: [
346 {
347 description: 'PDF files',
348 accept: {
349 'application/pdf': ['.pdf', '.PDF'],
350 },
351 },
352 ],
353 });
354 return;
355 }
356 const serializedSVG = serializeSVG(svgDocument);
357 if (settings.format === 'png') {
358 const png = await serializePNG(serializedSVG, svg, settings, theme);
359 if (mode === 'copy') {
360 await copyBlob(png);
361 } else {
362 await saveBlob(png, `${graph.name}.png`, {
363 id: EXPORT_ID,
364 types: [
365 {
366 description: 'PNG graphics',
367 accept: {
368 [PNG_CONTENT_TYPE]: ['.png', '.PNG'],
369 },
370 },
371 ],
372 });
373 }
374 } else if (mode === 'copy') {
375 await copyBlob(serializedSVG);
376 } else {
377 await saveBlob(serializedSVG, `${graph.name}.svg`, {
378 id: EXPORT_ID,
379 types: [
380 {
381 description: 'SVG graphics',
382 accept: {
383 [SVG_CONTENT_TYPE]: ['.svg', '.SVG'],
384 },
385 },
386 ],
387 });
388 }
389}
diff --git a/subprojects/frontend/src/graph/export/open-sans-latin-bold.ttf b/subprojects/frontend/src/graph/export/open-sans-latin-bold.ttf
new file mode 100644
index 00000000..7472d192
--- /dev/null
+++ b/subprojects/frontend/src/graph/export/open-sans-latin-bold.ttf
Binary files differ
diff --git a/subprojects/frontend/src/graph/export/open-sans-latin-bold.ttf.license b/subprojects/frontend/src/graph/export/open-sans-latin-bold.ttf.license
new file mode 100644
index 00000000..442f1821
--- /dev/null
+++ b/subprojects/frontend/src/graph/export/open-sans-latin-bold.ttf.license
@@ -0,0 +1,8 @@
1Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
2
3SPDX-License-Identifier: OFL-1.1
4
5This file was derived from Open Sans v3.3
6(https://github.com/googlefonts/opensans/blob/bd7e37632246368c60fdcbd374dbf9bad11969b6/fonts/ttf/OpenSans-Bold.ttf)
7using the Font Squirrel Web Font Generator (https://www.fontsquirrel.com/tools/webfont-generator)
8with the Basic Subsetting setting to reduce the file size by retaining latin characters only.
diff --git a/subprojects/frontend/src/graph/export/open-sans-latin-italic.ttf b/subprojects/frontend/src/graph/export/open-sans-latin-italic.ttf
new file mode 100644
index 00000000..cecb0ce1
--- /dev/null
+++ b/subprojects/frontend/src/graph/export/open-sans-latin-italic.ttf
Binary files differ
diff --git a/subprojects/frontend/src/graph/export/open-sans-latin-italic.ttf.license b/subprojects/frontend/src/graph/export/open-sans-latin-italic.ttf.license
new file mode 100644
index 00000000..0657164a
--- /dev/null
+++ b/subprojects/frontend/src/graph/export/open-sans-latin-italic.ttf.license
@@ -0,0 +1,8 @@
1Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
2
3SPDX-License-Identifier: OFL-1.1
4
5This file was derived from Open Sans v3.3
6(https://github.com/googlefonts/opensans/blob/bd7e37632246368c60fdcbd374dbf9bad11969b6/fonts/ttf/OpenSans-Italic.ttf)
7using the Font Squirrel Web Font Generator (https://www.fontsquirrel.com/tools/webfont-generator)
8with the Basic Subsetting setting to reduce the file size by retaining latin characters only.
diff --git a/subprojects/frontend/src/graph/export/open-sans-latin-regular.ttf b/subprojects/frontend/src/graph/export/open-sans-latin-regular.ttf
new file mode 100644
index 00000000..46c0f716
--- /dev/null
+++ b/subprojects/frontend/src/graph/export/open-sans-latin-regular.ttf
Binary files differ
diff --git a/subprojects/frontend/src/graph/export/open-sans-latin-regular.ttf.license b/subprojects/frontend/src/graph/export/open-sans-latin-regular.ttf.license
new file mode 100644
index 00000000..8bc20e51
--- /dev/null
+++ b/subprojects/frontend/src/graph/export/open-sans-latin-regular.ttf.license
@@ -0,0 +1,8 @@
1Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
2
3SPDX-License-Identifier: OFL-1.1
4
5This file was derived from Open Sans v3.3
6(https://github.com/googlefonts/opensans/blob/bd7e37632246368c60fdcbd374dbf9bad11969b6/fonts/ttf/OpenSans-Regular.ttf)
7using the Font Squirrel Web Font Generator (https://www.fontsquirrel.com/tools/webfont-generator)
8with the Basic Subsetting setting to reduce the file size by retaining latin characters only.
diff --git a/subprojects/frontend/src/graph/export/serializePDF.ts b/subprojects/frontend/src/graph/export/serializePDF.ts
new file mode 100644
index 00000000..75d1a4f4
--- /dev/null
+++ b/subprojects/frontend/src/graph/export/serializePDF.ts
@@ -0,0 +1,37 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import { jsPDF } from 'jspdf';
8import { svg2pdf } from 'svg2pdf.js';
9
10import boldFontURL from './open-sans-latin-bold.ttf?url';
11import italicFontURL from './open-sans-latin-italic.ttf?url';
12import normalFontURL from './open-sans-latin-regular.ttf?url';
13
14export default async function serializePDF(
15 svg: SVGSVGElement,
16 embedFonts: boolean,
17): Promise<Blob> {
18 const width = svg.width.baseVal.value;
19 const height = svg.height.baseVal.value;
20 // eslint-disable-next-line new-cap -- jsPDF uses a lowercase constructor.
21 const document = new jsPDF({
22 orientation: width > height ? 'l' : 'p',
23 unit: 'px',
24 format: [width, height],
25 compress: true,
26 });
27 if (embedFonts) {
28 document.addFont(normalFontURL, 'Open Sans', 'normal', 400);
29 document.addFont(italicFontURL, 'Open Sans', 'italic', 400);
30 document.addFont(boldFontURL, 'Open Sans', 'normal', 700);
31 }
32 const result = await svg2pdf(svg, document, {
33 width,
34 height,
35 });
36 return result.output('blob');
37}
diff --git a/subprojects/frontend/src/graph/postProcessSVG.ts b/subprojects/frontend/src/graph/postProcessSVG.ts
index a580f5c6..bf990f3a 100644
--- a/subprojects/frontend/src/graph/postProcessSVG.ts
+++ b/subprojects/frontend/src/graph/postProcessSVG.ts
@@ -1,13 +1,13 @@
1/* 1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> 2 * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors <https://refinery.tools/>
3 * 3 *
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
6 6
7import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox'; 7import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox';
8 8
9const SVG_NS = 'http://www.w3.org/2000/svg'; 9export const SVG_NS = 'http://www.w3.org/2000/svg';
10const XLINK_NS = 'http://www.w3.org/1999/xlink'; 10export const XLINK_NS = 'http://www.w3.org/1999/xlink';
11 11
12function modifyAttribute(element: Element, attribute: string, change: number) { 12function modifyAttribute(element: Element, attribute: string, change: number) {
13 const valueString = element.getAttribute(attribute); 13 const valueString = element.getAttribute(attribute);
@@ -166,6 +166,32 @@ function replaceImages(node: SVGGElement) {
166 }); 166 });
167} 167}
168 168
169function markerColorToClass(svg: SVGSVGElement) {
170 svg.querySelectorAll('.node [stroke="black"]').forEach((node) => {
171 node.removeAttribute('stroke');
172 node.classList.add('node-outline');
173 });
174 svg.querySelectorAll('.node [fill="green"]').forEach((node) => {
175 node.removeAttribute('fill');
176 node.classList.add('node-header');
177 });
178 svg.querySelectorAll('.node [fill="white"]').forEach((node) => {
179 node.removeAttribute('fill');
180 node.classList.add('node-bg');
181 });
182 svg.querySelectorAll('.edge [stroke="black"]').forEach((node) => {
183 node.removeAttribute('stroke');
184 node.classList.add('edge-line');
185 });
186 svg.querySelectorAll('.edge [fill="black"]').forEach((node) => {
187 node.removeAttribute('fill');
188 node.classList.add('edge-arrow');
189 });
190 svg.querySelectorAll('[font-family]').forEach((node) => {
191 node.removeAttribute('font-family');
192 });
193}
194
169export default function postProcessSvg(svg: SVGSVGElement) { 195export default function postProcessSvg(svg: SVGSVGElement) {
170 // svg 196 // svg
171 // .querySelectorAll<SVGTitleElement>('title') 197 // .querySelectorAll<SVGTitleElement>('title')
@@ -183,4 +209,5 @@ export default function postProcessSvg(svg: SVGSVGElement) {
183 svg.viewBox.baseVal.height + 12, 209 svg.viewBox.baseVal.height + 12,
184 ]; 210 ];
185 svg.setAttribute('viewBox', viewBox.join(' ')); 211 svg.setAttribute('viewBox', viewBox.join(' '));
212 markerColorToClass(svg);
186} 213}
diff --git a/subprojects/frontend/src/theme/ThemeProvider.tsx b/subprojects/frontend/src/theme/ThemeProvider.tsx
index a996cde8..6905fb4b 100644
--- a/subprojects/frontend/src/theme/ThemeProvider.tsx
+++ b/subprojects/frontend/src/theme/ThemeProvider.tsx
@@ -220,7 +220,7 @@ function createResponsiveTheme(
220 return responsiveFontSizes(themeWithOverrides); 220 return responsiveFontSizes(themeWithOverrides);
221} 221}
222 222
223const lightTheme = (() => { 223export const lightTheme = (() => {
224 const primaryText = '#19202b'; 224 const primaryText = '#19202b';
225 const disabledText = '#a0a1a7'; 225 const disabledText = '#a0a1a7';
226 const darkBackground = '#f5f5f5'; 226 const darkBackground = '#f5f5f5';
@@ -282,7 +282,7 @@ const lightTheme = (() => {
282 }); 282 });
283})(); 283})();
284 284
285const darkTheme = (() => { 285export const darkTheme = (() => {
286 const primaryText = '#ebebff'; 286 const primaryText = '#ebebff';
287 const secondaryText = '#abb2bf'; 287 const secondaryText = '#abb2bf';
288 const darkBackground = '#21252b'; 288 const darkBackground = '#21252b';
diff --git a/subprojects/frontend/src/utils/fileIO.ts b/subprojects/frontend/src/utils/fileIO.ts
new file mode 100644
index 00000000..fe0b1fbb
--- /dev/null
+++ b/subprojects/frontend/src/utils/fileIO.ts
@@ -0,0 +1,105 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7export interface OpenResult {
8 name: string;
9 handle: FileSystemFileHandle | undefined;
10}
11
12export interface OpenTextFileResult extends OpenResult {
13 text: string;
14}
15
16export async function openTextFile(
17 options: FilePickerOptions,
18): Promise<OpenTextFileResult> {
19 let file: File;
20 let handle: FileSystemFileHandle | undefined;
21 if ('showOpenFilePicker' in window) {
22 [handle] = await window.showOpenFilePicker(options);
23 if (handle === undefined) {
24 throw new Error('No file was selected');
25 }
26 file = await handle.getFile();
27 } else {
28 const input = document.createElement('input');
29 input.type = 'file';
30 file = await new Promise((resolve, reject) => {
31 input.addEventListener('change', () => {
32 const { files } = input;
33 const result = files?.item(0);
34 if (result) {
35 resolve(result);
36 } else {
37 reject(new Error('No file was selected'));
38 }
39 });
40 input.click();
41 });
42 }
43 const text = await file.text();
44 return {
45 name: file.name,
46 text,
47 handle,
48 };
49}
50
51export async function saveTextFile(
52 handle: FileSystemFileHandle,
53 text: string,
54): Promise<void> {
55 const writable = await handle.createWritable();
56 try {
57 await writable.write(text);
58 } finally {
59 await writable.close();
60 }
61}
62
63export async function saveBlob(
64 blob: Blob,
65 name: string,
66 options: FilePickerOptions,
67): Promise<OpenResult | undefined> {
68 if ('showSaveFilePicker' in window) {
69 const handle = await window.showSaveFilePicker({
70 ...options,
71 suggestedName: name,
72 });
73 const writable = await handle.createWritable();
74 try {
75 await writable.write(blob);
76 } finally {
77 await writable.close();
78 }
79 return {
80 name: handle.name,
81 handle,
82 };
83 }
84 const link = document.createElement('a');
85 const url = window.URL.createObjectURL(blob);
86 try {
87 link.href = url;
88 link.download = name;
89 link.click();
90 } finally {
91 window.URL.revokeObjectURL(url);
92 }
93 return undefined;
94}
95
96export async function copyBlob(blob: Blob): Promise<void> {
97 const { clipboard } = navigator;
98 if ('write' in clipboard) {
99 await clipboard.write([
100 new ClipboardItem({
101 [blob.type]: blob,
102 }),
103 ]);
104 }
105}
diff --git a/subprojects/frontend/types/filesystemAccess.d.ts b/subprojects/frontend/types/filesystemAccess.d.ts
new file mode 100644
index 00000000..e9accc77
--- /dev/null
+++ b/subprojects/frontend/types/filesystemAccess.d.ts
@@ -0,0 +1,40 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7interface FilePickerOptions {
8 id?: string;
9 types?: {
10 description?: string;
11 accept: Record<string, string[]>;
12 }[];
13}
14
15interface FilePickerSaveOptions extends FilePickerOptions {
16 suggestedName?: string;
17}
18
19interface Window {
20 showOpenFilePicker?: (
21 options?: FilePickerOpenOptions,
22 ) => Promise<FileSystemFileHandle[]>;
23 showSaveFilePicker?: (
24 options?: FilePickerSaveOptions,
25 ) => Promise<FileSystemFileHandle>;
26}
27
28interface FileSystemHandlePermissionDescriptor {
29 mode?: 'read' | 'readwrite';
30}
31
32interface FileSystemHandle {
33 queryPermission?: (
34 options?: FileSystemHandlePermissionDescriptor,
35 ) => Promise<PermissionStatus>;
36
37 requestPermission?: (
38 options?: FileSystemHandlePermissionDescriptor,
39 ) => Promise<PermissionStatus>;
40}
diff --git a/subprojects/frontend/vite.config.ts b/subprojects/frontend/vite.config.ts
index fffaae20..2d066e38 100644
--- a/subprojects/frontend/vite.config.ts
+++ b/subprojects/frontend/vite.config.ts
@@ -1,5 +1,5 @@
1/* 1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> 2 * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/>
3 * 3 *
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
@@ -30,6 +30,9 @@ const { mode, isDevelopment, devModePlugins, serverOptions } =
30process.env['NODE_ENV'] ??= mode; 30process.env['NODE_ENV'] ??= mode;
31 31
32const fontsGlob = [ 32const fontsGlob = [
33 'open-sans-latin-*.ttf',
34 'open-sans-latin-400-{normal,italic}-*.woff2',
35 'open-sans-latin-700-*.woff2',
33 'open-sans-latin-wdth-{normal,italic}-*.woff2', 36 'open-sans-latin-wdth-{normal,italic}-*.woff2',
34 'jetbrains-mono-latin-wght-{normal,italic}-*.woff2', 37 'jetbrains-mono-latin-wght-{normal,italic}-*.woff2',
35]; 38];
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java
index cc87917f..19eeeff3 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java
@@ -20,8 +20,8 @@ public class SecurityHeadersFilter implements Filter {
20 // CodeMirror needs inline styles, see e.g., 20 // CodeMirror needs inline styles, see e.g.,
21 // https://discuss.codemirror.net/t/inline-styles-and-content-security-policy/1311/2 21 // https://discuss.codemirror.net/t/inline-styles-and-content-security-policy/1311/2
22 "style-src 'self' 'unsafe-inline'; " + 22 "style-src 'self' 'unsafe-inline'; " +
23 // Use 'data:' for displaying inline SVG backgrounds. 23 // Use 'data:' for displaying inline SVG backgrounds and blob for rendering SVG.
24 "img-src 'self' data:; " + 24 "img-src 'self' data: blob:; " +
25 "font-src 'self'; " + 25 "font-src 'self'; " +
26 // Fetch data:application/octet-stream;base64 URIs to unpack compressed URL fragments. 26 // Fetch data:application/octet-stream;base64 URIs to unpack compressed URL fragments.
27 "connect-src 'self' data:; " + 27 "connect-src 'self' data:; " +
diff --git a/yarn.lock b/yarn.lock
index c55f95d4..13125135 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -36,12 +36,12 @@ __metadata:
36 linkType: hard 36 linkType: hard
37 37
38"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.22.5": 38"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.22.5":
39 version: 7.22.13 39 version: 7.23.5
40 resolution: "@babel/code-frame@npm:7.22.13" 40 resolution: "@babel/code-frame@npm:7.23.5"
41 dependencies: 41 dependencies:
42 "@babel/highlight": "npm:^7.22.13" 42 "@babel/highlight": "npm:^7.23.4"
43 chalk: "npm:^2.4.2" 43 chalk: "npm:^2.4.2"
44 checksum: 10c0/f4cc8ae1000265677daf4845083b72f88d00d311adb1a93c94eb4b07bf0ed6828a81ae4ac43ee7d476775000b93a28a9cddec18fbdc5796212d8dcccd5de72bd 44 checksum: 10c0/a10e843595ddd9f97faa99917414813c06214f4d9205294013e20c70fbdf4f943760da37dec1d998bf3e6fc20fa2918a47c0e987a7e458663feb7698063ad7c6
45 languageName: node 45 languageName: node
46 linkType: hard 46 linkType: hard
47 47
@@ -211,11 +211,11 @@ __metadata:
211 linkType: hard 211 linkType: hard
212 212
213"@babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.18.6, @babel/helper-module-imports@npm:^7.22.5": 213"@babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.18.6, @babel/helper-module-imports@npm:^7.22.5":
214 version: 7.22.5 214 version: 7.22.15
215 resolution: "@babel/helper-module-imports@npm:7.22.5" 215 resolution: "@babel/helper-module-imports@npm:7.22.15"
216 dependencies: 216 dependencies:
217 "@babel/types": "npm:^7.22.5" 217 "@babel/types": "npm:^7.22.15"
218 checksum: 10c0/04f8c0586c485c33017c63e0fc5fc16bd33b883cef3c88e4b3a8bf7bc807b3f9a7bcb9372fbcc01c0a539a5d1cdb477e7bdec77e250669edab00f796683b6b07 218 checksum: 10c0/4e0d7fc36d02c1b8c8b3006dfbfeedf7a367d3334a04934255de5128115ea0bafdeb3e5736a2559917f0653e4e437400d54542da0468e08d3cbc86d3bbfa8f30
219 languageName: node 219 languageName: node
220 linkType: hard 220 linkType: hard
221 221
@@ -349,14 +349,14 @@ __metadata:
349 languageName: node 349 languageName: node
350 linkType: hard 350 linkType: hard
351 351
352"@babel/highlight@npm:^7.22.13": 352"@babel/highlight@npm:^7.23.4":
353 version: 7.22.20 353 version: 7.23.4
354 resolution: "@babel/highlight@npm:7.22.20" 354 resolution: "@babel/highlight@npm:7.23.4"
355 dependencies: 355 dependencies:
356 "@babel/helper-validator-identifier": "npm:^7.22.20" 356 "@babel/helper-validator-identifier": "npm:^7.22.20"
357 chalk: "npm:^2.4.2" 357 chalk: "npm:^2.4.2"
358 js-tokens: "npm:^4.0.0" 358 js-tokens: "npm:^4.0.0"
359 checksum: 10c0/f3c3a193afad23434297d88e81d1d6c0c2cf02423de2139ada7ce0a7fc62d8559abf4cc996533c1a9beca7fc990010eb8d544097f75e818ac113bf39ed810aa2 359 checksum: 10c0/fbff9fcb2f5539289c3c097d130e852afd10d89a3a08ac0b5ebebbc055cc84a4bcc3dcfed463d488cde12dd0902ef1858279e31d7349b2e8cee43913744bda33
360 languageName: node 360 languageName: node
361 linkType: hard 361 linkType: hard
362 362
@@ -1234,7 +1234,7 @@ __metadata:
1234 languageName: node 1234 languageName: node
1235 linkType: hard 1235 linkType: hard
1236 1236
1237"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": 1237"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7":
1238 version: 7.23.9 1238 version: 7.23.9
1239 resolution: "@babel/runtime@npm:7.23.9" 1239 resolution: "@babel/runtime@npm:7.23.9"
1240 dependencies: 1240 dependencies:
@@ -1348,21 +1348,21 @@ __metadata:
1348 languageName: node 1348 languageName: node
1349 linkType: hard 1349 linkType: hard
1350 1350
1351"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.4.0": 1351"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.4.1":
1352 version: 6.4.0 1352 version: 6.4.1
1353 resolution: "@codemirror/state@npm:6.4.0" 1353 resolution: "@codemirror/state@npm:6.4.1"
1354 checksum: 10c0/f1a94ab45dd5ae067d7f273bbe1a02fb118fa3defae012ff8a18711083bc7fb29436e3a7642c2621bb19a0d51546b398595b63dfdd48e5e15dcf6c629ec81ca1 1354 checksum: 10c0/cdab74d0ca4e262531a257ac419c9c44124f3ace8b0ca1262598a9218fbb6fd8f0afeb4b5ed2f64552a9573a0fc5d55481d4b9b05e9505ef729f9bd0f9469423
1355 languageName: node 1355 languageName: node
1356 linkType: hard 1356 linkType: hard
1357 1357
1358"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.24.0": 1358"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.24.1":
1359 version: 6.24.0 1359 version: 6.24.1
1360 resolution: "@codemirror/view@npm:6.24.0" 1360 resolution: "@codemirror/view@npm:6.24.1"
1361 dependencies: 1361 dependencies:
1362 "@codemirror/state": "npm:^6.4.0" 1362 "@codemirror/state": "npm:^6.4.0"
1363 style-mod: "npm:^4.1.0" 1363 style-mod: "npm:^4.1.0"
1364 w3c-keyname: "npm:^2.2.4" 1364 w3c-keyname: "npm:^2.2.4"
1365 checksum: 10c0/27ab6fb82093a533b0a1a2f032b4a0f5707ae85513d21350fb05e1e03c3284a8bb278054c1a77317a6ae512f491a466337f7e35a32eadfb4da33f7efa1b6dcde 1365 checksum: 10c0/56ec45fa5b064310dd9561cec324633b992c1b918b09c304e8856a4bc22eab0d686c5ab37ed8ccb6e4b19bd0dade5c8a3dd256f056305e7d91b303e847949d23
1366 languageName: node 1366 languageName: node
1367 linkType: hard 1367 linkType: hard
1368 1368
@@ -1701,10 +1701,10 @@ __metadata:
1701 languageName: node 1701 languageName: node
1702 linkType: hard 1702 linkType: hard
1703 1703
1704"@eslint/js@npm:8.56.0": 1704"@eslint/js@npm:8.57.0":
1705 version: 8.56.0 1705 version: 8.57.0
1706 resolution: "@eslint/js@npm:8.56.0" 1706 resolution: "@eslint/js@npm:8.57.0"
1707 checksum: 10c0/60b3a1cf240e2479cec9742424224465dc50e46d781da1b7f5ef240501b2d1202c225bd456207faac4b34a64f4765833345bc4ddffd00395e1db40fa8c426f5a 1707 checksum: 10c0/9a518bb8625ba3350613903a6d8c622352ab0c6557a59fe6ff6178bf882bf57123f9d92aa826ee8ac3ee74b9c6203fe630e9ee00efb03d753962dcf65ee4bd94
1708 languageName: node 1708 languageName: node
1709 linkType: hard 1709 linkType: hard
1710 1710
@@ -1760,6 +1760,13 @@ __metadata:
1760 languageName: node 1760 languageName: node
1761 linkType: hard 1761 linkType: hard
1762 1762
1763"@fontsource/open-sans@npm:^5.0.24":
1764 version: 5.0.24
1765 resolution: "@fontsource/open-sans@npm:5.0.24"
1766 checksum: 10c0/e3306f3ff0b9d85703f5b3114f33e31747dcbdf35a2eff425d0a9afb61bfd69c7eb29a691a5acd4582aa808ec123e51f223b49dad60d2caae9890d72e03feb7d
1767 languageName: node
1768 linkType: hard
1769
1763"@hpcc-js/wasm@npm:^2.16.0": 1770"@hpcc-js/wasm@npm:^2.16.0":
1764 version: 2.16.0 1771 version: 2.16.0
1765 resolution: "@hpcc-js/wasm@npm:2.16.0" 1772 resolution: "@hpcc-js/wasm@npm:2.16.0"
@@ -1771,14 +1778,14 @@ __metadata:
1771 languageName: node 1778 languageName: node
1772 linkType: hard 1779 linkType: hard
1773 1780
1774"@humanwhocodes/config-array@npm:^0.11.13": 1781"@humanwhocodes/config-array@npm:^0.11.14":
1775 version: 0.11.13 1782 version: 0.11.14
1776 resolution: "@humanwhocodes/config-array@npm:0.11.13" 1783 resolution: "@humanwhocodes/config-array@npm:0.11.14"
1777 dependencies: 1784 dependencies:
1778 "@humanwhocodes/object-schema": "npm:^2.0.1" 1785 "@humanwhocodes/object-schema": "npm:^2.0.2"
1779 debug: "npm:^4.1.1" 1786 debug: "npm:^4.3.1"
1780 minimatch: "npm:^3.0.5" 1787 minimatch: "npm:^3.0.5"
1781 checksum: 10c0/d76ca802d853366094d0e98ff0d0994117fc8eff96649cd357b15e469e428228f597cd2e929d54ab089051684949955f16ee905bb19f7b2f0446fb377157be7a 1788 checksum: 10c0/66f725b4ee5fdd8322c737cb5013e19fac72d4d69c8bf4b7feb192fcb83442b035b92186f8e9497c220e58b2d51a080f28a73f7899bc1ab288c3be172c467541
1782 languageName: node 1789 languageName: node
1783 linkType: hard 1790 linkType: hard
1784 1791
@@ -1789,10 +1796,10 @@ __metadata:
1789 languageName: node 1796 languageName: node
1790 linkType: hard 1797 linkType: hard
1791 1798
1792"@humanwhocodes/object-schema@npm:^2.0.1": 1799"@humanwhocodes/object-schema@npm:^2.0.2":
1793 version: 2.0.1 1800 version: 2.0.2
1794 resolution: "@humanwhocodes/object-schema@npm:2.0.1" 1801 resolution: "@humanwhocodes/object-schema@npm:2.0.2"
1795 checksum: 10c0/9dba24e59fdb4041829d92b693aacb778add3b6f612aaa9c0774f3b650c11a378cc64f042a59da85c11dae33df456580a3c36837b953541aed6ff94294f97fac 1802 checksum: 10c0/6fd83dc320231d71c4541d0244051df61f301817e9f9da9fd4cb7e44ec8aacbde5958c1665b0c419401ab935114fdf532a6ad5d4e7294b1af2f347dd91a6983f
1796 languageName: node 1803 languageName: node
1797 linkType: hard 1804 linkType: hard
1798 1805
@@ -1906,14 +1913,14 @@ __metadata:
1906 languageName: node 1913 languageName: node
1907 linkType: hard 1914 linkType: hard
1908 1915
1909"@mui/base@npm:5.0.0-beta.36": 1916"@mui/base@npm:5.0.0-beta.37":
1910 version: 5.0.0-beta.36 1917 version: 5.0.0-beta.37
1911 resolution: "@mui/base@npm:5.0.0-beta.36" 1918 resolution: "@mui/base@npm:5.0.0-beta.37"
1912 dependencies: 1919 dependencies:
1913 "@babel/runtime": "npm:^7.23.9" 1920 "@babel/runtime": "npm:^7.23.9"
1914 "@floating-ui/react-dom": "npm:^2.0.8" 1921 "@floating-ui/react-dom": "npm:^2.0.8"
1915 "@mui/types": "npm:^7.2.13" 1922 "@mui/types": "npm:^7.2.13"
1916 "@mui/utils": "npm:^5.15.9" 1923 "@mui/utils": "npm:^5.15.11"
1917 "@popperjs/core": "npm:^2.11.8" 1924 "@popperjs/core": "npm:^2.11.8"
1918 clsx: "npm:^2.1.0" 1925 clsx: "npm:^2.1.0"
1919 prop-types: "npm:^15.8.1" 1926 prop-types: "npm:^15.8.1"
@@ -1924,20 +1931,20 @@ __metadata:
1924 peerDependenciesMeta: 1931 peerDependenciesMeta:
1925 "@types/react": 1932 "@types/react":
1926 optional: true 1933 optional: true
1927 checksum: 10c0/b789cd9e6906d56698bb5536704aefa475283081f1287b8340bb74dea4465565f7958c5e61bbc000f0d2a8e97aaf95a203f35cc737115e3edc7ea389da3d9c9e 1934 checksum: 10c0/bcee89198381b1058aa2a7e3122597a34a03fb3b351b1e0776674a996fb633bdeeae0656f14f905df7d4fced2aa6ae0a2b4fb3b462de387668481d8427448ca3
1928 languageName: node 1935 languageName: node
1929 linkType: hard 1936 linkType: hard
1930 1937
1931"@mui/core-downloads-tracker@npm:^5.15.10": 1938"@mui/core-downloads-tracker@npm:^5.15.11":
1932 version: 5.15.10 1939 version: 5.15.11
1933 resolution: "@mui/core-downloads-tracker@npm:5.15.10" 1940 resolution: "@mui/core-downloads-tracker@npm:5.15.11"
1934 checksum: 10c0/d05da33eb3532994aa0b71e8131c8c21b4c6256976bfa704b2268c2bd88ae2c6853b0832c2af904507820e56563d373020827310abd626e146e026200b76eacf 1941 checksum: 10c0/e7d9fd674c1d71eb428ceffb9bbb36edaa421caa8c438e6977014f283bcae8530d6daa7c4289015576976505c6321ddaec2970b0af4c2d8619da206a025cc033
1935 languageName: node 1942 languageName: node
1936 linkType: hard 1943 linkType: hard
1937 1944
1938"@mui/icons-material@npm:^5.15.10": 1945"@mui/icons-material@npm:^5.15.11":
1939 version: 5.15.10 1946 version: 5.15.11
1940 resolution: "@mui/icons-material@npm:5.15.10" 1947 resolution: "@mui/icons-material@npm:5.15.11"
1941 dependencies: 1948 dependencies:
1942 "@babel/runtime": "npm:^7.23.9" 1949 "@babel/runtime": "npm:^7.23.9"
1943 peerDependencies: 1950 peerDependencies:
@@ -1947,20 +1954,20 @@ __metadata:
1947 peerDependenciesMeta: 1954 peerDependenciesMeta:
1948 "@types/react": 1955 "@types/react":
1949 optional: true 1956 optional: true
1950 checksum: 10c0/d49747d962ad9dec172d498173ee60fe29f3c818d15458fdc21b5d7a1cfa3a5acf8c6f3e6e35c665a3b0f4a088ac6806601c0d408403117f6711740448db4346 1957 checksum: 10c0/59cc9e62a82985cd357815df021ab2d4ef3b8528574ca8d67245ef02eb4df290110ba146bf014ece463992906fdb6e08b495800f94644b9c43ef1b3a4cbf6783
1951 languageName: node 1958 languageName: node
1952 linkType: hard 1959 linkType: hard
1953 1960
1954"@mui/material@npm:^5.15.10": 1961"@mui/material@npm:^5.15.11":
1955 version: 5.15.10 1962 version: 5.15.11
1956 resolution: "@mui/material@npm:5.15.10" 1963 resolution: "@mui/material@npm:5.15.11"
1957 dependencies: 1964 dependencies:
1958 "@babel/runtime": "npm:^7.23.9" 1965 "@babel/runtime": "npm:^7.23.9"
1959 "@mui/base": "npm:5.0.0-beta.36" 1966 "@mui/base": "npm:5.0.0-beta.37"
1960 "@mui/core-downloads-tracker": "npm:^5.15.10" 1967 "@mui/core-downloads-tracker": "npm:^5.15.11"
1961 "@mui/system": "npm:^5.15.9" 1968 "@mui/system": "npm:^5.15.11"
1962 "@mui/types": "npm:^7.2.13" 1969 "@mui/types": "npm:^7.2.13"
1963 "@mui/utils": "npm:^5.15.9" 1970 "@mui/utils": "npm:^5.15.11"
1964 "@types/react-transition-group": "npm:^4.4.10" 1971 "@types/react-transition-group": "npm:^4.4.10"
1965 clsx: "npm:^2.1.0" 1972 clsx: "npm:^2.1.0"
1966 csstype: "npm:^3.1.3" 1973 csstype: "npm:^3.1.3"
@@ -1980,16 +1987,16 @@ __metadata:
1980 optional: true 1987 optional: true
1981 "@types/react": 1988 "@types/react":
1982 optional: true 1989 optional: true
1983 checksum: 10c0/057147a7f68269641954dd456852a801c71fe10cc1dee7c4d669e8327a792ac2dee4083c4562b918a3696276a9035ba2a5b1b5261dc7f1a385711b938ae508fe 1990 checksum: 10c0/21f885a07d3cf1407d21a3f1d1dcd86076585508f912e85c00d8093e181b4dfb2fc8ebf954e06f699f691220eb4a3f1601f7a51b39e90486ae7fcd222f114015
1984 languageName: node 1991 languageName: node
1985 linkType: hard 1992 linkType: hard
1986 1993
1987"@mui/private-theming@npm:^5.15.9": 1994"@mui/private-theming@npm:^5.15.11":
1988 version: 5.15.9 1995 version: 5.15.11
1989 resolution: "@mui/private-theming@npm:5.15.9" 1996 resolution: "@mui/private-theming@npm:5.15.11"
1990 dependencies: 1997 dependencies:
1991 "@babel/runtime": "npm:^7.23.9" 1998 "@babel/runtime": "npm:^7.23.9"
1992 "@mui/utils": "npm:^5.15.9" 1999 "@mui/utils": "npm:^5.15.11"
1993 prop-types: "npm:^15.8.1" 2000 prop-types: "npm:^15.8.1"
1994 peerDependencies: 2001 peerDependencies:
1995 "@types/react": ^17.0.0 || ^18.0.0 2002 "@types/react": ^17.0.0 || ^18.0.0
@@ -1997,13 +2004,13 @@ __metadata:
1997 peerDependenciesMeta: 2004 peerDependenciesMeta:
1998 "@types/react": 2005 "@types/react":
1999 optional: true 2006 optional: true
2000 checksum: 10c0/a5c995b7f2faa9aa39ccf258bc2cafde9a8c265dd15bfbaec974650690d81d662eaae0524f0f8e17d16fe7893a6907cc0c388ac5648cce7b502a77d722daa116 2007 checksum: 10c0/29763c2df3a18bd5a4772302dc1d734c9101c02f42b7ae1c2c21b048c4fc5b7a12f83c30ac2297087c1499222ebcfcd3e23d72717cc163732f962dfde5260dd3
2001 languageName: node 2008 languageName: node
2002 linkType: hard 2009 linkType: hard
2003 2010
2004"@mui/styled-engine@npm:^5.15.9": 2011"@mui/styled-engine@npm:^5.15.11":
2005 version: 5.15.9 2012 version: 5.15.11
2006 resolution: "@mui/styled-engine@npm:5.15.9" 2013 resolution: "@mui/styled-engine@npm:5.15.11"
2007 dependencies: 2014 dependencies:
2008 "@babel/runtime": "npm:^7.23.9" 2015 "@babel/runtime": "npm:^7.23.9"
2009 "@emotion/cache": "npm:^11.11.0" 2016 "@emotion/cache": "npm:^11.11.0"
@@ -2018,19 +2025,19 @@ __metadata:
2018 optional: true 2025 optional: true
2019 "@emotion/styled": 2026 "@emotion/styled":
2020 optional: true 2027 optional: true
2021 checksum: 10c0/47968ec55a5a36936dc8c245c1d62fe9be82bdaba800aeea08d111702b3aea8388bc3c2a46ee4ee315efe77b1c7ecf63308fc518afa69114b2fc90a203de48e0 2028 checksum: 10c0/b168565256c82ed34946f954e9088c339917c980451b5dd3d6dca9d36e6d39b5900ea85ffc5736b66d25f841dc761a8c2c70c111c42bec794309f2cafb3975ce
2022 languageName: node 2029 languageName: node
2023 linkType: hard 2030 linkType: hard
2024 2031
2025"@mui/system@npm:^5.15.9": 2032"@mui/system@npm:^5.15.11":
2026 version: 5.15.9 2033 version: 5.15.11
2027 resolution: "@mui/system@npm:5.15.9" 2034 resolution: "@mui/system@npm:5.15.11"
2028 dependencies: 2035 dependencies:
2029 "@babel/runtime": "npm:^7.23.9" 2036 "@babel/runtime": "npm:^7.23.9"
2030 "@mui/private-theming": "npm:^5.15.9" 2037 "@mui/private-theming": "npm:^5.15.11"
2031 "@mui/styled-engine": "npm:^5.15.9" 2038 "@mui/styled-engine": "npm:^5.15.11"
2032 "@mui/types": "npm:^7.2.13" 2039 "@mui/types": "npm:^7.2.13"
2033 "@mui/utils": "npm:^5.15.9" 2040 "@mui/utils": "npm:^5.15.11"
2034 clsx: "npm:^2.1.0" 2041 clsx: "npm:^2.1.0"
2035 csstype: "npm:^3.1.3" 2042 csstype: "npm:^3.1.3"
2036 prop-types: "npm:^15.8.1" 2043 prop-types: "npm:^15.8.1"
@@ -2046,7 +2053,7 @@ __metadata:
2046 optional: true 2053 optional: true
2047 "@types/react": 2054 "@types/react":
2048 optional: true 2055 optional: true
2049 checksum: 10c0/187a14e689be1434c7db646f2f0f19c5fe60eb565dd0fa2d0a7b903240fd36ccdd3a5d2318630a9e0186036ef61fbebd58a9ef752f7b9b329660614ddbdf37f1 2056 checksum: 10c0/27094ac751ad024bf9e67b26feb94dd09ea3550facb6d3dc1a6e15f1901a1cc03751221826908fa881df1eab5d5c03c8b5e737bfae627e1d5a0fa3033c5decaa
2050 languageName: node 2057 languageName: node
2051 linkType: hard 2058 linkType: hard
2052 2059
@@ -2062,9 +2069,9 @@ __metadata:
2062 languageName: node 2069 languageName: node
2063 linkType: hard 2070 linkType: hard
2064 2071
2065"@mui/utils@npm:^5.14.16, @mui/utils@npm:^5.15.9": 2072"@mui/utils@npm:^5.14.16, @mui/utils@npm:^5.15.11":
2066 version: 5.15.9 2073 version: 5.15.11
2067 resolution: "@mui/utils@npm:5.15.9" 2074 resolution: "@mui/utils@npm:5.15.11"
2068 dependencies: 2075 dependencies:
2069 "@babel/runtime": "npm:^7.23.9" 2076 "@babel/runtime": "npm:^7.23.9"
2070 "@types/prop-types": "npm:^15.7.11" 2077 "@types/prop-types": "npm:^15.7.11"
@@ -2076,13 +2083,13 @@ __metadata:
2076 peerDependenciesMeta: 2083 peerDependenciesMeta:
2077 "@types/react": 2084 "@types/react":
2078 optional: true 2085 optional: true
2079 checksum: 10c0/42d3db7f73f47ad719b8a4769eac2aeaec683166c953e67518f6edb7d14464576daf6566619a384f5f7e48dcb1478053ff3e4ce1be244fd332f7e988da9e1401 2086 checksum: 10c0/bbdd0a8cd5bd04021b6e0b7836bd5a5852e54d95392ad786816dd0e5c1942f82f5d645721ac8c716394f74b0628a8c909139fc86103a3aa40d147ab9eb234fb9
2080 languageName: node 2087 languageName: node
2081 linkType: hard 2088 linkType: hard
2082 2089
2083"@mui/x-data-grid@npm:^6.19.4": 2090"@mui/x-data-grid@npm:^6.19.5":
2084 version: 6.19.4 2091 version: 6.19.5
2085 resolution: "@mui/x-data-grid@npm:6.19.4" 2092 resolution: "@mui/x-data-grid@npm:6.19.5"
2086 dependencies: 2093 dependencies:
2087 "@babel/runtime": "npm:^7.23.2" 2094 "@babel/runtime": "npm:^7.23.2"
2088 "@mui/utils": "npm:^5.14.16" 2095 "@mui/utils": "npm:^5.14.16"
@@ -2094,7 +2101,7 @@ __metadata:
2094 "@mui/system": ^5.4.1 2101 "@mui/system": ^5.4.1
2095 react: ^17.0.0 || ^18.0.0 2102 react: ^17.0.0 || ^18.0.0
2096 react-dom: ^17.0.0 || ^18.0.0 2103 react-dom: ^17.0.0 || ^18.0.0
2097 checksum: 10c0/2de403c7cadb9c34baed609afc500288c927518abf70f362eb8a45ac6161497aebb9f3b13e72e0192a6deb508c0db2ffb13d8ae09cece2e52490bd2b956179ed 2104 checksum: 10c0/64fadfab30fc210c5f7f4fff16bc08a4357498c67b8c341106d42189bcda8e17e59115b9b7cdae7af1f0c181ce4afeae9b41d56d455dd2bfad51bb8c5ef3786d
2098 languageName: node 2105 languageName: node
2099 linkType: hard 2106 linkType: hard
2100 2107
@@ -2164,34 +2171,39 @@ __metadata:
2164 "@codemirror/language": "npm:^6.10.1" 2171 "@codemirror/language": "npm:^6.10.1"
2165 "@codemirror/lint": "npm:^6.5.0" 2172 "@codemirror/lint": "npm:^6.5.0"
2166 "@codemirror/search": "npm:^6.5.6" 2173 "@codemirror/search": "npm:^6.5.6"
2167 "@codemirror/state": "npm:^6.4.0" 2174 "@codemirror/state": "npm:^6.4.1"
2168 "@codemirror/view": "npm:^6.24.0" 2175 "@codemirror/view": "npm:^6.24.1"
2176 "@emotion/cache": "npm:^11.11.0"
2169 "@emotion/react": "npm:^11.11.3" 2177 "@emotion/react": "npm:^11.11.3"
2178 "@emotion/serialize": "npm:^1.1.3"
2170 "@emotion/styled": "npm:^11.11.0" 2179 "@emotion/styled": "npm:^11.11.0"
2180 "@emotion/utils": "npm:^1.2.1"
2171 "@fontsource-variable/jetbrains-mono": "npm:^5.0.19" 2181 "@fontsource-variable/jetbrains-mono": "npm:^5.0.19"
2172 "@fontsource-variable/open-sans": "npm:^5.0.25" 2182 "@fontsource-variable/open-sans": "npm:^5.0.25"
2183 "@fontsource/open-sans": "npm:^5.0.24"
2173 "@hpcc-js/wasm": "npm:^2.16.0" 2184 "@hpcc-js/wasm": "npm:^2.16.0"
2174 "@lezer/common": "npm:^1.2.1" 2185 "@lezer/common": "npm:^1.2.1"
2175 "@lezer/generator": "npm:^1.6.0" 2186 "@lezer/generator": "npm:^1.6.0"
2176 "@lezer/highlight": "npm:^1.2.0" 2187 "@lezer/highlight": "npm:^1.2.0"
2177 "@lezer/lr": "npm:^1.4.0" 2188 "@lezer/lr": "npm:^1.4.0"
2178 "@material-icons/svg": "npm:^1.0.33" 2189 "@material-icons/svg": "npm:^1.0.33"
2179 "@mui/icons-material": "npm:^5.15.10" 2190 "@mui/icons-material": "npm:^5.15.11"
2180 "@mui/material": "npm:^5.15.10" 2191 "@mui/material": "npm:^5.15.11"
2181 "@mui/system": "npm:^5.15.9" 2192 "@mui/system": "npm:^5.15.11"
2182 "@mui/x-data-grid": "npm:^6.19.4" 2193 "@mui/x-data-grid": "npm:^6.19.5"
2183 "@types/d3": "npm:^7.4.3" 2194 "@types/d3": "npm:^7.4.3"
2184 "@types/d3-graphviz": "npm:^2.6.10" 2195 "@types/d3-graphviz": "npm:^2.6.10"
2185 "@types/d3-selection": "npm:^3.0.10" 2196 "@types/d3-selection": "npm:^3.0.10"
2186 "@types/d3-zoom": "npm:^3.0.8" 2197 "@types/d3-zoom": "npm:^3.0.8"
2187 "@types/eslint": "npm:^8.56.2" 2198 "@types/eslint": "npm:^8.56.3"
2188 "@types/html-minifier-terser": "npm:^7.0.2" 2199 "@types/html-minifier-terser": "npm:^7.0.2"
2200 "@types/jspdf": "npm:^2.0.0"
2189 "@types/lodash-es": "npm:^4.17.12" 2201 "@types/lodash-es": "npm:^4.17.12"
2190 "@types/micromatch": "npm:^4.0.6" 2202 "@types/micromatch": "npm:^4.0.6"
2191 "@types/ms": "npm:^0.7.34" 2203 "@types/ms": "npm:^0.7.34"
2192 "@types/node": "npm:^20.11.19" 2204 "@types/node": "npm:^20.11.20"
2193 "@types/pnpapi": "npm:^0.0.5" 2205 "@types/pnpapi": "npm:^0.0.5"
2194 "@types/react": "npm:^18.2.56" 2206 "@types/react": "npm:^18.2.58"
2195 "@types/react-dom": "npm:^18.2.19" 2207 "@types/react-dom": "npm:^18.2.19"
2196 "@typescript-eslint/eslint-plugin": "npm:^6.21.0" 2208 "@typescript-eslint/eslint-plugin": "npm:^6.21.0"
2197 "@typescript-eslint/parser": "npm:^6.21.0" 2209 "@typescript-eslint/parser": "npm:^6.21.0"
@@ -2205,7 +2217,7 @@ __metadata:
2205 d3-selection: "npm:^3.0.0" 2217 d3-selection: "npm:^3.0.0"
2206 d3-zoom: "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch" 2218 d3-zoom: "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch"
2207 escape-string-regexp: "npm:^5.0.0" 2219 escape-string-regexp: "npm:^5.0.0"
2208 eslint: "npm:^8.56.0" 2220 eslint: "npm:^8.57.0"
2209 eslint-config-airbnb: "npm:^19.0.4" 2221 eslint-config-airbnb: "npm:^19.0.4"
2210 eslint-config-airbnb-typescript: "npm:^17.1.0" 2222 eslint-config-airbnb-typescript: "npm:^17.1.0"
2211 eslint-config-prettier: "npm:^9.1.0" 2223 eslint-config-prettier: "npm:^9.1.0"
@@ -2217,6 +2229,7 @@ __metadata:
2217 eslint-plugin-react: "npm:^7.33.2" 2229 eslint-plugin-react: "npm:^7.33.2"
2218 eslint-plugin-react-hooks: "npm:^4.6.0" 2230 eslint-plugin-react-hooks: "npm:^4.6.0"
2219 html-minifier-terser: "npm:^7.2.0" 2231 html-minifier-terser: "npm:^7.2.0"
2232 jspdf: "npm:^2.5.1"
2220 lodash-es: "npm:^4.17.21" 2233 lodash-es: "npm:^4.17.21"
2221 loglevel: "npm:^1.9.1" 2234 loglevel: "npm:^1.9.1"
2222 loglevel-plugin-prefix: "npm:^0.8.4" 2235 loglevel-plugin-prefix: "npm:^0.8.4"
@@ -2224,15 +2237,16 @@ __metadata:
2224 mobx: "npm:^6.12.0" 2237 mobx: "npm:^6.12.0"
2225 mobx-react-lite: "npm:^4.0.5" 2238 mobx-react-lite: "npm:^4.0.5"
2226 ms: "npm:^2.1.3" 2239 ms: "npm:^2.1.3"
2227 nanoid: "npm:^5.0.5" 2240 nanoid: "npm:^5.0.6"
2228 notistack: "npm:^3.0.1" 2241 notistack: "npm:^3.0.1"
2229 pnpapi: "npm:^0.0.0" 2242 pnpapi: "npm:^0.0.0"
2230 prettier: "npm:^3.2.5" 2243 prettier: "npm:^3.2.5"
2231 react: "npm:^18.2.0" 2244 react: "npm:^18.2.0"
2232 react-dom: "npm:^18.2.0" 2245 react-dom: "npm:^18.2.0"
2233 react-resize-detector: "npm:^10.0.1" 2246 react-resize-detector: "npm:^10.0.1"
2247 svg2pdf.js: "npm:^2.2.3"
2234 typescript: "npm:5.3.3" 2248 typescript: "npm:5.3.3"
2235 vite: "npm:^5.1.3" 2249 vite: "npm:^5.1.4"
2236 vite-plugin-pwa: "npm:^0.19.0" 2250 vite-plugin-pwa: "npm:^0.19.0"
2237 workbox-window: "npm:^7.0.0" 2251 workbox-window: "npm:^7.0.0"
2238 xstate: "npm:^4.38.3" 2252 xstate: "npm:^4.38.3"
@@ -2244,7 +2258,7 @@ __metadata:
2244 version: 0.0.0-use.local 2258 version: 0.0.0-use.local
2245 resolution: "@refinery/root@workspace:." 2259 resolution: "@refinery/root@workspace:."
2246 dependencies: 2260 dependencies:
2247 eslint: "npm:^8.56.0" 2261 eslint: "npm:^8.57.0"
2248 typescript: "npm:5.3.3" 2262 typescript: "npm:5.3.3"
2249 languageName: unknown 2263 languageName: unknown
2250 linkType: soft 2264 linkType: soft
@@ -2872,13 +2886,13 @@ __metadata:
2872 languageName: node 2886 languageName: node
2873 linkType: hard 2887 linkType: hard
2874 2888
2875"@types/eslint@npm:^8.56.2": 2889"@types/eslint@npm:^8.56.3":
2876 version: 8.56.2 2890 version: 8.56.3
2877 resolution: "@types/eslint@npm:8.56.2" 2891 resolution: "@types/eslint@npm:8.56.3"
2878 dependencies: 2892 dependencies:
2879 "@types/estree": "npm:*" 2893 "@types/estree": "npm:*"
2880 "@types/json-schema": "npm:*" 2894 "@types/json-schema": "npm:*"
2881 checksum: 10c0/e33ca87a30a9454ba9943e1270ac759996f5fe598a1c1afbaec1d1e7346a339e20bf2a9d81f177067116bbaa6cfa4f748993cb338f57978ae862ad38ffae56fe 2895 checksum: 10c0/c5d81d0001fae211451b39d82b2bc8d7224b00d52a514954a33840a3665f36f3bde3be602eec6ad08d1fff59108052cd7746ced4237116bc3d8ac01a7cf5b5fe
2882 languageName: node 2896 languageName: node
2883 linkType: hard 2897 linkType: hard
2884 2898
@@ -2924,6 +2938,15 @@ __metadata:
2924 languageName: node 2938 languageName: node
2925 linkType: hard 2939 linkType: hard
2926 2940
2941"@types/jspdf@npm:^2.0.0":
2942 version: 2.0.0
2943 resolution: "@types/jspdf@npm:2.0.0"
2944 dependencies:
2945 jspdf: "npm:*"
2946 checksum: 10c0/69bed9c099c9a0d369c2734fa97862cb67c4d1151ce45b7f9f3c6dfedf7b1bb612cf0561aa5768b7adf5cec207adf2fa82ffbcb7907c716b33b1f5a8f75fe5fc
2947 languageName: node
2948 linkType: hard
2949
2927"@types/lodash-es@npm:^4.17.12": 2950"@types/lodash-es@npm:^4.17.12":
2928 version: 4.17.12 2951 version: 4.17.12
2929 resolution: "@types/lodash-es@npm:4.17.12" 2952 resolution: "@types/lodash-es@npm:4.17.12"
@@ -2956,19 +2979,19 @@ __metadata:
2956 languageName: node 2979 languageName: node
2957 linkType: hard 2980 linkType: hard
2958 2981
2959"@types/node@npm:*, @types/node@npm:^20.11.19": 2982"@types/node@npm:*, @types/node@npm:^20.11.20":
2960 version: 20.11.19 2983 version: 20.11.20
2961 resolution: "@types/node@npm:20.11.19" 2984 resolution: "@types/node@npm:20.11.20"
2962 dependencies: 2985 dependencies:
2963 undici-types: "npm:~5.26.4" 2986 undici-types: "npm:~5.26.4"
2964 checksum: 10c0/f451ef0a1d78f29c57bad7b77e49ebec945f2a6d0d7a89851d7e185ee9fe7ad94d651c0dfbcb7858c9fa791310c8b40a881e2260f56bd3c1b7e7ae92723373ae 2987 checksum: 10c0/8e8de211e6d54425c603388a9b5cc9c434101985d0a1c88aabbf65d10df2b1fccd71855c20e61ae8a75c7aea56cb0f64e722cf7914cff1247d0b62ce21996ac4
2965 languageName: node 2988 languageName: node
2966 linkType: hard 2989 linkType: hard
2967 2990
2968"@types/parse-json@npm:^4.0.0": 2991"@types/parse-json@npm:^4.0.0":
2969 version: 4.0.0 2992 version: 4.0.2
2970 resolution: "@types/parse-json@npm:4.0.0" 2993 resolution: "@types/parse-json@npm:4.0.2"
2971 checksum: 10c0/1d3012ab2fcdad1ba313e1d065b737578f6506c8958e2a7a5bdbdef517c7e930796cb1599ee067d5dee942fb3a764df64b5eef7e9ae98548d776e86dcffba985 2994 checksum: 10c0/b1b863ac34a2c2172fbe0807a1ec4d5cb684e48d422d15ec95980b81475fac4fdb3768a8b13eef39130203a7c04340fc167bae057c7ebcafd7dec9fe6c36aeb1
2972 languageName: node 2995 languageName: node
2973 linkType: hard 2996 linkType: hard
2974 2997
@@ -2986,6 +3009,13 @@ __metadata:
2986 languageName: node 3009 languageName: node
2987 linkType: hard 3010 linkType: hard
2988 3011
3012"@types/raf@npm:^3.4.0":
3013 version: 3.4.3
3014 resolution: "@types/raf@npm:3.4.3"
3015 checksum: 10c0/dea835f0daa399c51db9137f5337dc08a2b4a5f61f645658966ecabaebbbd0fd59551f384a1141e14e22a1cc5a591da7d4d88c60a525ad1399108b6dd2641d75
3016 languageName: node
3017 linkType: hard
3018
2989"@types/react-dom@npm:^18.2.19": 3019"@types/react-dom@npm:^18.2.19":
2990 version: 18.2.19 3020 version: 18.2.19
2991 resolution: "@types/react-dom@npm:18.2.19" 3021 resolution: "@types/react-dom@npm:18.2.19"
@@ -3004,14 +3034,14 @@ __metadata:
3004 languageName: node 3034 languageName: node
3005 linkType: hard 3035 linkType: hard
3006 3036
3007"@types/react@npm:*, @types/react@npm:^18.2.56": 3037"@types/react@npm:*, @types/react@npm:^18.2.58":
3008 version: 18.2.56 3038 version: 18.2.58
3009 resolution: "@types/react@npm:18.2.56" 3039 resolution: "@types/react@npm:18.2.58"
3010 dependencies: 3040 dependencies:
3011 "@types/prop-types": "npm:*" 3041 "@types/prop-types": "npm:*"
3012 "@types/scheduler": "npm:*" 3042 "@types/scheduler": "npm:*"
3013 csstype: "npm:^3.0.2" 3043 csstype: "npm:^3.0.2"
3014 checksum: 10c0/a6dab9569799538a9e01d340a721ef1a6f5532bd11cae8d8ab9af00dab2edcafaa00950f7bf2f9ae5bbb1839d890e9ac6eb1ea1186200894b7178dde7b503269 3044 checksum: 10c0/80145b707b780d682092b51d520f58a0171c4067ff36cf488d3346d92b715b27fd334acd0fabb8eb21a4eb6c4061f1535e8bfa6642a7f4025e63ebec868fb6d1
3015 languageName: node 3045 languageName: node
3016 linkType: hard 3046 linkType: hard
3017 3047
@@ -3537,6 +3567,15 @@ __metadata:
3537 languageName: node 3567 languageName: node
3538 linkType: hard 3568 linkType: hard
3539 3569
3570"atob@npm:^2.1.2":
3571 version: 2.1.2
3572 resolution: "atob@npm:2.1.2"
3573 bin:
3574 atob: bin/atob.js
3575 checksum: 10c0/ada635b519dc0c576bb0b3ca63a73b50eefacf390abb3f062558342a8d68f2db91d0c8db54ce81b0d89de3b0f000de71f3ae7d761fd7d8cc624278fe443d6c7e
3576 languageName: node
3577 linkType: hard
3578
3540"available-typed-arrays@npm:^1.0.5": 3579"available-typed-arrays@npm:^1.0.5":
3541 version: 1.0.5 3580 version: 1.0.5
3542 resolution: "available-typed-arrays@npm:1.0.5" 3581 resolution: "available-typed-arrays@npm:1.0.5"
@@ -3623,6 +3662,13 @@ __metadata:
3623 languageName: node 3662 languageName: node
3624 linkType: hard 3663 linkType: hard
3625 3664
3665"base64-arraybuffer@npm:^1.0.2":
3666 version: 1.0.2
3667 resolution: "base64-arraybuffer@npm:1.0.2"
3668 checksum: 10c0/3acac95c70f9406e87a41073558ba85b6be9dbffb013a3d2a710e3f2d534d506c911847d5d9be4de458af6362c676de0a5c4c2d7bdf4def502d00b313368e72f
3669 languageName: node
3670 linkType: hard
3671
3626"binary-extensions@npm:^2.0.0": 3672"binary-extensions@npm:^2.0.0":
3627 version: 2.2.0 3673 version: 2.2.0
3628 resolution: "binary-extensions@npm:2.2.0" 3674 resolution: "binary-extensions@npm:2.2.0"
@@ -3672,6 +3718,15 @@ __metadata:
3672 languageName: node 3718 languageName: node
3673 linkType: hard 3719 linkType: hard
3674 3720
3721"btoa@npm:^1.2.1":
3722 version: 1.2.1
3723 resolution: "btoa@npm:1.2.1"
3724 bin:
3725 btoa: bin/btoa.js
3726 checksum: 10c0/557b9682e40a68ae057af1b377e28884e6ff756ba0f499fe0f8c7b725a5bfb5c0d891604ac09944dbe330c9d43fb3976fef734f9372608d0d8e78a30eda292ae
3727 languageName: node
3728 linkType: hard
3729
3675"buffer-from@npm:^1.0.0": 3730"buffer-from@npm:^1.0.0":
3676 version: 1.1.2 3731 version: 1.1.2
3677 resolution: "buffer-from@npm:1.1.2" 3732 resolution: "buffer-from@npm:1.1.2"
@@ -3735,9 +3790,25 @@ __metadata:
3735 linkType: hard 3790 linkType: hard
3736 3791
3737"caniuse-lite@npm:^1.0.30001517": 3792"caniuse-lite@npm:^1.0.30001517":
3738 version: 1.0.30001588 3793 version: 1.0.30001589
3739 resolution: "caniuse-lite@npm:1.0.30001588" 3794 resolution: "caniuse-lite@npm:1.0.30001589"
3740 checksum: 10c0/f8333cb52e7ebc169d462763cecc33807530f1e04d22ba1084e05a583907aa801fb3c013d60b38d54cb792440f48efcd2a1a68f22d5fce896b5bd0277392347c 3795 checksum: 10c0/20debfb949413f603011bc7dacaf050010778bc4f8632c86fafd1bd0c43180c95ae7c31f6c82348f6309e5e221934e327c3607a216e3f09640284acf78cd6d4d
3796 languageName: node
3797 linkType: hard
3798
3799"canvg@npm:^3.0.6":
3800 version: 3.0.10
3801 resolution: "canvg@npm:3.0.10"
3802 dependencies:
3803 "@babel/runtime": "npm:^7.12.5"
3804 "@types/raf": "npm:^3.4.0"
3805 core-js: "npm:^3.8.3"
3806 raf: "npm:^3.4.1"
3807 regenerator-runtime: "npm:^0.13.7"
3808 rgbcolor: "npm:^1.0.1"
3809 stackblur-canvas: "npm:^2.0.0"
3810 svg-pathdata: "npm:^6.0.3"
3811 checksum: 10c0/b6bcd95d60c923c6a4e2be49e1fc1d395790577913a5a68439a2bb5a784ee75533ed7720bef69f2d9d0404203b4d61e89fdf1346f829e5da71e54cc57614153f
3741 languageName: node 3812 languageName: node
3742 linkType: hard 3813 linkType: hard
3743 3814
@@ -3942,6 +4013,13 @@ __metadata:
3942 languageName: node 4013 languageName: node
3943 linkType: hard 4014 linkType: hard
3944 4015
4016"core-js@npm:^3.6.0, core-js@npm:^3.8.3":
4017 version: 3.36.0
4018 resolution: "core-js@npm:3.36.0"
4019 checksum: 10c0/62dcb41ba79ead581e4c5b2740ae18bfe6ee230e853893736d16edb01b580574d8645ff6c5513d1c75d59620f8451aee45c119d3c4f5ebc66cff5f003a816864
4020 languageName: node
4021 linkType: hard
4022
3945"cosmiconfig@npm:^7.0.0": 4023"cosmiconfig@npm:^7.0.0":
3946 version: 7.1.0 4024 version: 7.1.0
3947 resolution: "cosmiconfig@npm:7.1.0" 4025 resolution: "cosmiconfig@npm:7.1.0"
@@ -3992,6 +4070,24 @@ __metadata:
3992 languageName: node 4070 languageName: node
3993 linkType: hard 4071 linkType: hard
3994 4072
4073"css-line-break@npm:^2.1.0":
4074 version: 2.1.0
4075 resolution: "css-line-break@npm:2.1.0"
4076 dependencies:
4077 utrie: "npm:^1.0.2"
4078 checksum: 10c0/b2222d99d5daf7861ecddc050244fdce296fad74b000dcff6bdfb1eb16dc2ef0b9ffe2c1c965e3239bd05ebe9eadb6d5438a91592fa8648d27a338e827cf9048
4079 languageName: node
4080 linkType: hard
4081
4082"cssesc@npm:^3.0.0":
4083 version: 3.0.0
4084 resolution: "cssesc@npm:3.0.0"
4085 bin:
4086 cssesc: bin/cssesc
4087 checksum: 10c0/6bcfd898662671be15ae7827120472c5667afb3d7429f1f917737f3bf84c4176003228131b643ae74543f17a394446247df090c597bb9a728cce298606ed0aa7
4088 languageName: node
4089 linkType: hard
4090
3995"csstype@npm:^3.0.2, csstype@npm:^3.1.3": 4091"csstype@npm:^3.0.2, csstype@npm:^3.1.3":
3996 version: 3.1.3 4092 version: 3.1.3
3997 resolution: "csstype@npm:3.1.3" 4093 resolution: "csstype@npm:3.1.3"
@@ -4373,7 +4469,7 @@ __metadata:
4373 languageName: node 4469 languageName: node
4374 linkType: hard 4470 linkType: hard
4375 4471
4376"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": 4472"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4":
4377 version: 4.3.4 4473 version: 4.3.4
4378 resolution: "debug@npm:4.3.4" 4474 resolution: "debug@npm:4.3.4"
4379 dependencies: 4475 dependencies:
@@ -4490,6 +4586,13 @@ __metadata:
4490 languageName: node 4586 languageName: node
4491 linkType: hard 4587 linkType: hard
4492 4588
4589"dompurify@npm:^2.2.0":
4590 version: 2.4.7
4591 resolution: "dompurify@npm:2.4.7"
4592 checksum: 10c0/c04fa6a7c7276d0bc80e6330f697cfecd96ec1d3964b17de916f26cb0b5b2c9a98ba343d84e759f2b8339e577e619ef3749f3d128ef18ddb8230b09bd2ff3f29
4593 languageName: node
4594 linkType: hard
4595
4493"dot-case@npm:^3.0.4": 4596"dot-case@npm:^3.0.4":
4494 version: 3.0.4 4597 version: 3.0.4
4495 resolution: "dot-case@npm:3.0.4" 4598 resolution: "dot-case@npm:3.0.4"
@@ -5039,15 +5142,15 @@ __metadata:
5039 languageName: node 5142 languageName: node
5040 linkType: hard 5143 linkType: hard
5041 5144
5042"eslint@npm:^8.56.0": 5145"eslint@npm:^8.57.0":
5043 version: 8.56.0 5146 version: 8.57.0
5044 resolution: "eslint@npm:8.56.0" 5147 resolution: "eslint@npm:8.57.0"
5045 dependencies: 5148 dependencies:
5046 "@eslint-community/eslint-utils": "npm:^4.2.0" 5149 "@eslint-community/eslint-utils": "npm:^4.2.0"
5047 "@eslint-community/regexpp": "npm:^4.6.1" 5150 "@eslint-community/regexpp": "npm:^4.6.1"
5048 "@eslint/eslintrc": "npm:^2.1.4" 5151 "@eslint/eslintrc": "npm:^2.1.4"
5049 "@eslint/js": "npm:8.56.0" 5152 "@eslint/js": "npm:8.57.0"
5050 "@humanwhocodes/config-array": "npm:^0.11.13" 5153 "@humanwhocodes/config-array": "npm:^0.11.14"
5051 "@humanwhocodes/module-importer": "npm:^1.0.1" 5154 "@humanwhocodes/module-importer": "npm:^1.0.1"
5052 "@nodelib/fs.walk": "npm:^1.2.8" 5155 "@nodelib/fs.walk": "npm:^1.2.8"
5053 "@ungap/structured-clone": "npm:^1.2.0" 5156 "@ungap/structured-clone": "npm:^1.2.0"
@@ -5083,7 +5186,7 @@ __metadata:
5083 text-table: "npm:^0.2.0" 5186 text-table: "npm:^0.2.0"
5084 bin: 5187 bin:
5085 eslint: bin/eslint.js 5188 eslint: bin/eslint.js
5086 checksum: 10c0/2be598f7da1339d045ad933ffd3d4742bee610515cd2b0d9a2b8b729395a01d4e913552fff555b559fccaefd89d7b37632825789d1b06470608737ae69ab43fb 5189 checksum: 10c0/00bb96fd2471039a312435a6776fe1fd557c056755eaa2b96093ef3a8508c92c8775d5f754768be6b1dddd09fdd3379ddb231eeb9b6c579ee17ea7d68000a529
5087 languageName: node 5190 languageName: node
5088 linkType: hard 5191 linkType: hard
5089 5192
@@ -5204,6 +5307,13 @@ __metadata:
5204 languageName: node 5307 languageName: node
5205 linkType: hard 5308 linkType: hard
5206 5309
5310"fflate@npm:^0.4.8":
5311 version: 0.4.8
5312 resolution: "fflate@npm:0.4.8"
5313 checksum: 10c0/29d1eddaaa5deab61b1c6b0d21282adacadbc4d2c01e94d8b1ee784398151673b9c563e53f97a801bc410a1ae55e8de5378114a743430e643e7a0644ba8e5a42
5314 languageName: node
5315 linkType: hard
5316
5207"file-entry-cache@npm:^6.0.1": 5317"file-entry-cache@npm:^6.0.1":
5208 version: 6.0.1 5318 version: 6.0.1
5209 resolution: "file-entry-cache@npm:6.0.1" 5319 resolution: "file-entry-cache@npm:6.0.1"
@@ -5265,6 +5375,13 @@ __metadata:
5265 languageName: node 5375 languageName: node
5266 linkType: hard 5376 linkType: hard
5267 5377
5378"font-family-papandreou@npm:^0.2.0-patch1":
5379 version: 0.2.0-patch2
5380 resolution: "font-family-papandreou@npm:0.2.0-patch2"
5381 checksum: 10c0/f2028070906b71a648b3aba63e22b4077fa3adf5949749f27fc7b44a41ffd1f017221e0d9bd550b8a1d8fcef7cfc3fc22cf6e3d99c5cc8a00ebfee29a3f26841
5382 languageName: node
5383 linkType: hard
5384
5268"for-each@npm:^0.3.3": 5385"for-each@npm:^0.3.3":
5269 version: 0.3.3 5386 version: 0.3.3
5270 resolution: "for-each@npm:0.3.3" 5387 resolution: "for-each@npm:0.3.3"
@@ -5656,6 +5773,16 @@ __metadata:
5656 languageName: node 5773 languageName: node
5657 linkType: hard 5774 linkType: hard
5658 5775
5776"html2canvas@npm:^1.0.0-rc.5":
5777 version: 1.4.1
5778 resolution: "html2canvas@npm:1.4.1"
5779 dependencies:
5780 css-line-break: "npm:^2.1.0"
5781 text-segmentation: "npm:^1.0.3"
5782 checksum: 10c0/6de86f75762b00948edf2ea559f16da0a1ec3facc4a8a7d3f35fcec59bb0c5970463478988ae3d9082152e0173690d46ebf4082e7ac803dd4817bae1d355c0db
5783 languageName: node
5784 linkType: hard
5785
5659"http-cache-semantics@npm:^4.1.1": 5786"http-cache-semantics@npm:^4.1.1":
5660 version: 4.1.1 5787 version: 4.1.1
5661 resolution: "http-cache-semantics@npm:4.1.1" 5788 resolution: "http-cache-semantics@npm:4.1.1"
@@ -5776,9 +5903,9 @@ __metadata:
5776 linkType: hard 5903 linkType: hard
5777 5904
5778"ip@npm:^2.0.0": 5905"ip@npm:^2.0.0":
5779 version: 2.0.0 5906 version: 2.0.1
5780 resolution: "ip@npm:2.0.0" 5907 resolution: "ip@npm:2.0.1"
5781 checksum: 10c0/8d186cc5585f57372847ae29b6eba258c68862055e18a75cc4933327232cb5c107f89800ce29715d542eef2c254fbb68b382e780a7414f9ee7caf60b7a473958 5908 checksum: 10c0/cab8eb3e88d0abe23e4724829621ec4c4c5cb41a7f936a2e626c947128c1be16ed543448d42af7cca95379f9892bfcacc1ccd8d09bc7e8bea0e86d492ce33616
5782 languageName: node 5909 languageName: node
5783 linkType: hard 5910 linkType: hard
5784 5911
@@ -6260,6 +6387,31 @@ __metadata:
6260 languageName: node 6387 languageName: node
6261 linkType: hard 6388 linkType: hard
6262 6389
6390"jspdf@npm:*, jspdf@npm:^2.5.1":
6391 version: 2.5.1
6392 resolution: "jspdf@npm:2.5.1"
6393 dependencies:
6394 "@babel/runtime": "npm:^7.14.0"
6395 atob: "npm:^2.1.2"
6396 btoa: "npm:^1.2.1"
6397 canvg: "npm:^3.0.6"
6398 core-js: "npm:^3.6.0"
6399 dompurify: "npm:^2.2.0"
6400 fflate: "npm:^0.4.8"
6401 html2canvas: "npm:^1.0.0-rc.5"
6402 dependenciesMeta:
6403 canvg:
6404 optional: true
6405 core-js:
6406 optional: true
6407 dompurify:
6408 optional: true
6409 html2canvas:
6410 optional: true
6411 checksum: 10c0/dad15d4f53ead1d2e9d5f6fd9b6e72c7233ba5cbc30d98461eb0ef609aa908b28fd5eaaf2b763b55df945c7ecca2323097d9331f09fee1d6c23c06785520ab5f
6412 languageName: node
6413 linkType: hard
6414
6263"jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.3.5": 6415"jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.3.5":
6264 version: 3.3.5 6416 version: 3.3.5
6265 resolution: "jsx-ast-utils@npm:3.3.5" 6417 resolution: "jsx-ast-utils@npm:3.3.5"
@@ -6652,12 +6804,12 @@ __metadata:
6652 languageName: node 6804 languageName: node
6653 linkType: hard 6805 linkType: hard
6654 6806
6655"nanoid@npm:^5.0.5": 6807"nanoid@npm:^5.0.6":
6656 version: 5.0.5 6808 version: 5.0.6
6657 resolution: "nanoid@npm:5.0.5" 6809 resolution: "nanoid@npm:5.0.6"
6658 bin: 6810 bin:
6659 nanoid: bin/nanoid.js 6811 nanoid: bin/nanoid.js
6660 checksum: 10c0/b1f881b08d6e918bf70d37dece3e59fff8d99cc29b80a956e29d1852e09d3a7bf35a4884f4b9d5ff26963018937d0c91bef83842c15758a668d88c94704d2da7 6812 checksum: 10c0/6660f99b7bb3816f04fd9a14126859482e07d1705c02e1a6c1a722545c65186659f6f734eb21329f54e838b6409579bef687e2fb13661b716529dcefc5d86ec6
6661 languageName: node 6813 languageName: node
6662 linkType: hard 6814 linkType: hard
6663 6815
@@ -7004,6 +7156,13 @@ __metadata:
7004 languageName: node 7156 languageName: node
7005 linkType: hard 7157 linkType: hard
7006 7158
7159"performance-now@npm:^2.1.0":
7160 version: 2.1.0
7161 resolution: "performance-now@npm:2.1.0"
7162 checksum: 10c0/22c54de06f269e29f640e0e075207af57de5052a3d15e360c09b9a8663f393f6f45902006c1e71aa8a5a1cdfb1a47fe268826f8496d6425c362f00f5bc3e85d9
7163 languageName: node
7164 linkType: hard
7165
7007"picocolors@npm:^1.0.0": 7166"picocolors@npm:^1.0.0":
7008 version: 1.0.0 7167 version: 1.0.0
7009 resolution: "picocolors@npm:1.0.0" 7168 resolution: "picocolors@npm:1.0.0"
@@ -7119,6 +7278,15 @@ __metadata:
7119 languageName: node 7278 languageName: node
7120 linkType: hard 7279 linkType: hard
7121 7280
7281"raf@npm:^3.4.1":
7282 version: 3.4.1
7283 resolution: "raf@npm:3.4.1"
7284 dependencies:
7285 performance-now: "npm:^2.1.0"
7286 checksum: 10c0/337f0853c9e6a77647b0f499beedafea5d6facfb9f2d488a624f88b03df2be72b8a0e7f9118a3ff811377d534912039a3311815700d2b6d2313f82f736f9eb6e
7287 languageName: node
7288 linkType: hard
7289
7122"randombytes@npm:^2.1.0": 7290"randombytes@npm:^2.1.0":
7123 version: 2.1.0 7291 version: 2.1.0
7124 resolution: "randombytes@npm:2.1.0" 7292 resolution: "randombytes@npm:2.1.0"
@@ -7253,6 +7421,13 @@ __metadata:
7253 languageName: node 7421 languageName: node
7254 linkType: hard 7422 linkType: hard
7255 7423
7424"regenerator-runtime@npm:^0.13.7":
7425 version: 0.13.11
7426 resolution: "regenerator-runtime@npm:0.13.11"
7427 checksum: 10c0/12b069dc774001fbb0014f6a28f11c09ebfe3c0d984d88c9bced77fdb6fedbacbca434d24da9ae9371bfbf23f754869307fb51a4c98a8b8b18e5ef748677ca24
7428 languageName: node
7429 linkType: hard
7430
7256"regenerator-runtime@npm:^0.14.0": 7431"regenerator-runtime@npm:^0.14.0":
7257 version: 0.14.0 7432 version: 0.14.0
7258 resolution: "regenerator-runtime@npm:0.14.0" 7433 resolution: "regenerator-runtime@npm:0.14.0"
@@ -7413,6 +7588,13 @@ __metadata:
7413 languageName: node 7588 languageName: node
7414 linkType: hard 7589 linkType: hard
7415 7590
7591"rgbcolor@npm:^1.0.1":
7592 version: 1.0.1
7593 resolution: "rgbcolor@npm:1.0.1"
7594 checksum: 10c0/13af06c523351bac2854b85a22d1dfafd9310efd898e9bd96c8706f9aa09a3ddc8392ab00ae03d12950782164a97677f21834ffd84ffebf76ae106add319f956
7595 languageName: node
7596 linkType: hard
7597
7416"rimraf@npm:^3.0.2": 7598"rimraf@npm:^3.0.2":
7417 version: 3.0.2 7599 version: 3.0.2
7418 resolution: "rimraf@npm:3.0.2" 7600 resolution: "rimraf@npm:3.0.2"
@@ -7753,6 +7935,15 @@ __metadata:
7753 languageName: node 7935 languageName: node
7754 linkType: hard 7936 linkType: hard
7755 7937
7938"specificity@npm:^0.4.1":
7939 version: 0.4.1
7940 resolution: "specificity@npm:0.4.1"
7941 bin:
7942 specificity: ./bin/specificity
7943 checksum: 10c0/5da85a05052b55e344cb0f5bce5d07cbabbbe8945da176a481589db5a13e9fbcfa879ceb075cf564b94e680fae0a2ab14ea55cc87496b86a6d5122545946d7c2
7944 languageName: node
7945 linkType: hard
7946
7756"ssri@npm:^10.0.0": 7947"ssri@npm:^10.0.0":
7757 version: 10.0.5 7948 version: 10.0.5
7758 resolution: "ssri@npm:10.0.5" 7949 resolution: "ssri@npm:10.0.5"
@@ -7762,6 +7953,13 @@ __metadata:
7762 languageName: node 7953 languageName: node
7763 linkType: hard 7954 linkType: hard
7764 7955
7956"stackblur-canvas@npm:^2.0.0":
7957 version: 2.7.0
7958 resolution: "stackblur-canvas@npm:2.7.0"
7959 checksum: 10c0/df290d0629056d5bb43d37548d0b24cb8593c79d742650e68489abf61013db578c9980724c2508bb738d107204f2e2494ab94c3cf69d6b725caa9c63b8c7e272
7960 languageName: node
7961 linkType: hard
7962
7765"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": 7963"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3":
7766 version: 4.2.3 7964 version: 4.2.3
7767 resolution: "string-width@npm:4.2.3" 7965 resolution: "string-width@npm:4.2.3"
@@ -7931,6 +8129,34 @@ __metadata:
7931 languageName: node 8129 languageName: node
7932 linkType: hard 8130 linkType: hard
7933 8131
8132"svg-pathdata@npm:^6.0.3":
8133 version: 6.0.3
8134 resolution: "svg-pathdata@npm:6.0.3"
8135 checksum: 10c0/1ba4ad2fa81e86df37d6e78d3be9e664bbedf97773b725a863a85db384285be32dc37d9c0d61e477d89594ee95b967d2c53d6bee2d76420aab670ab4124a38b9
8136 languageName: node
8137 linkType: hard
8138
8139"svg2pdf.js@npm:^2.2.3":
8140 version: 2.2.3
8141 resolution: "svg2pdf.js@npm:2.2.3"
8142 dependencies:
8143 cssesc: "npm:^3.0.0"
8144 font-family-papandreou: "npm:^0.2.0-patch1"
8145 specificity: "npm:^0.4.1"
8146 svgpath: "npm:^2.3.0"
8147 peerDependencies:
8148 jspdf: ^2.0.0
8149 checksum: 10c0/9845b837152b255c94ac6ba12e6376e771b18cb7bbfab6b11b56a19f437bca2e077386e58b5aa3f57ada021797ab784c6379d5be8d1df911eb705402f45d0cb2
8150 languageName: node
8151 linkType: hard
8152
8153"svgpath@npm:^2.3.0":
8154 version: 2.6.0
8155 resolution: "svgpath@npm:2.6.0"
8156 checksum: 10c0/aed042d7cc0e2f3b55d618af2bee58faf8820d53f5d45490194e9bd1cd9ae7d57be641fd3692429a133be2ffb75ff43aad2e6b1c297359ffbc808016a106f199
8157 languageName: node
8158 linkType: hard
8159
7934"synckit@npm:^0.8.6": 8160"synckit@npm:^0.8.6":
7935 version: 0.8.8 8161 version: 0.8.8
7936 resolution: "synckit@npm:0.8.8" 8162 resolution: "synckit@npm:0.8.8"
@@ -7995,6 +8221,15 @@ __metadata:
7995 languageName: node 8221 languageName: node
7996 linkType: hard 8222 linkType: hard
7997 8223
8224"text-segmentation@npm:^1.0.3":
8225 version: 1.0.3
8226 resolution: "text-segmentation@npm:1.0.3"
8227 dependencies:
8228 utrie: "npm:^1.0.2"
8229 checksum: 10c0/8b9ae8524e3a332371060d0ca62f10ad49a13e954719ea689a6c3a8b8c15c8a56365ede2bb91c322fb0d44b6533785f0da603e066b7554d052999967fb72d600
8230 languageName: node
8231 linkType: hard
8232
7998"text-table@npm:^0.2.0": 8233"text-table@npm:^0.2.0":
7999 version: 0.2.0 8234 version: 0.2.0
8000 resolution: "text-table@npm:0.2.0" 8235 resolution: "text-table@npm:0.2.0"
@@ -8295,6 +8530,15 @@ __metadata:
8295 languageName: node 8530 languageName: node
8296 linkType: hard 8531 linkType: hard
8297 8532
8533"utrie@npm:^1.0.2":
8534 version: 1.0.2
8535 resolution: "utrie@npm:1.0.2"
8536 dependencies:
8537 base64-arraybuffer: "npm:^1.0.2"
8538 checksum: 10c0/eaffe645bd81a39e4bc3abb23df5895e9961dbdd49748ef3b173529e8b06ce9dd1163e9705d5309a1c61ee41ffcb825e2043bc0fd1659845ffbdf4b1515dfdb4
8539 languageName: node
8540 linkType: hard
8541
8298"vite-plugin-pwa@npm:^0.19.0": 8542"vite-plugin-pwa@npm:^0.19.0":
8299 version: 0.19.0 8543 version: 0.19.0
8300 resolution: "vite-plugin-pwa@npm:0.19.0" 8544 resolution: "vite-plugin-pwa@npm:0.19.0"
@@ -8316,9 +8560,9 @@ __metadata:
8316 languageName: node 8560 languageName: node
8317 linkType: hard 8561 linkType: hard
8318 8562
8319"vite@npm:^5.1.3": 8563"vite@npm:^5.1.4":
8320 version: 5.1.3 8564 version: 5.1.4
8321 resolution: "vite@npm:5.1.3" 8565 resolution: "vite@npm:5.1.4"
8322 dependencies: 8566 dependencies:
8323 esbuild: "npm:^0.19.3" 8567 esbuild: "npm:^0.19.3"
8324 fsevents: "npm:~2.3.3" 8568 fsevents: "npm:~2.3.3"
@@ -8352,7 +8596,7 @@ __metadata:
8352 optional: true 8596 optional: true
8353 bin: 8597 bin:
8354 vite: bin/vite.js 8598 vite: bin/vite.js
8355 checksum: 10c0/d3b19607d736de60b660f7daf4c0f86589edcbbc1fcb09f8aa36630f99518cc8a063062bb952899b8ccaed62f1314fac22c1df492dd035de3c65998ab27e2d2a 8599 checksum: 10c0/8f04c8bed33f266bde27f432412456a3b893b51fe1857f0b8cd259100b376c1393a7927db1dd6344a4376baed72ed179ec5b0428aef2ae8508f1f28f95acb908
8356 languageName: node 8600 languageName: node
8357 linkType: hard 8601 linkType: hard
8358 8602
diff --git a/yarn.lock.license b/yarn.lock.license
index 7a5a2a4b..062a2796 100644
--- a/yarn.lock.license
+++ b/yarn.lock.license
@@ -1,3 +1,3 @@
1SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> 1SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/>
2 2
3SPDX-License-Identifier: CC0-1.0 3SPDX-License-Identifier: CC0-1.0