@opencloning/ui 1.4.2 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/package.json +3 -3
- package/src/components/assembler/Assembler.jsx +3 -1
- package/src/components/assembler/ExistingSyntaxDialog.cy.jsx +28 -43
- package/src/components/assembler/ExistingSyntaxDialog.jsx +1 -2
- package/src/components/assembler/assembler_utils.js +10 -1
- package/src/components/assembler/assembler_utils.test.js +28 -2
- package/src/components/assembler/graph_utils.js +5 -4
- package/src/components/assembler/graph_utils.test.js +9 -9
- package/src/components/primers/primer_design/SequenceTabComponents/RestrictionSpacerForm.jsx +3 -3
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 1.4.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#624](https://github.com/manulera/OpenCloning_frontend/pull/624) [`c333aa3`](https://github.com/manulera/OpenCloning_frontend/commit/c333aa3f5eda8ab7a65799d589ba6e9b376e2d24) Thanks [@manulera](https://github.com/manulera)! - Assigning plasmids to categories handles palindromic parts
|
|
8
|
+
|
|
9
|
+
- Updated dependencies []:
|
|
10
|
+
- @opencloning/store@1.4.4
|
|
11
|
+
- @opencloning/utils@1.4.4
|
|
12
|
+
|
|
13
|
+
## 1.4.3
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#622](https://github.com/manulera/OpenCloning_frontend/pull/622) [`03cec1c`](https://github.com/manulera/OpenCloning_frontend/commit/03cec1c53e6e8365bb8bf9617ed3128b65ab18cc) Thanks [@manulera](https://github.com/manulera)! - Fix loading external assemblies in syntax builder by extracting config out of ExistingSyntaxDialog component
|
|
18
|
+
|
|
19
|
+
- Updated dependencies []:
|
|
20
|
+
- @opencloning/store@1.4.3
|
|
21
|
+
- @opencloning/utils@1.4.3
|
|
22
|
+
|
|
3
23
|
## 1.4.2
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencloning/ui",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"@emotion/styled": "^11.14.0",
|
|
26
26
|
"@mui/icons-material": "^5.15.17",
|
|
27
27
|
"@mui/material": "^5.15.17",
|
|
28
|
-
"@opencloning/store": "1.4.
|
|
29
|
-
"@opencloning/utils": "1.4.
|
|
28
|
+
"@opencloning/store": "1.4.4",
|
|
29
|
+
"@opencloning/utils": "1.4.4",
|
|
30
30
|
"@teselagen/bio-parsers": "^0.4.34",
|
|
31
31
|
"@teselagen/ove": "^0.8.34",
|
|
32
32
|
"@teselagen/range-utils": "^0.3.20",
|
|
@@ -17,6 +17,7 @@ import useBackendRoute from '../../hooks/useBackendRoute';
|
|
|
17
17
|
import useHttpClient from '../../hooks/useHttpClient';
|
|
18
18
|
import useAlerts from '../../hooks/useAlerts';
|
|
19
19
|
import UploadPlasmidsButton from './UploadPlasmidsButton';
|
|
20
|
+
import { useConfig } from '../../providers';
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
const { setState: setCloningState, setCurrentTab: setCurrentTabAction } = cloningActions;
|
|
@@ -240,6 +241,7 @@ function categoriesFromSyntaxAndPlasmids(syntax, plasmids) {
|
|
|
240
241
|
function LoadSyntaxButton({ setSyntax, addPlasmids }) {
|
|
241
242
|
const [existingSyntaxDialogOpen, setExistingSyntaxDialogOpen] = React.useState(false)
|
|
242
243
|
const httpClient = useHttpClient();
|
|
244
|
+
const { staticContentPath } = useConfig();
|
|
243
245
|
const backendRoute = useBackendRoute();
|
|
244
246
|
const { addAlert } = useAlerts();
|
|
245
247
|
const onSyntaxSelect = React.useCallback(async (syntax, plasmids) => {
|
|
@@ -257,7 +259,7 @@ function LoadSyntaxButton({ setSyntax, addPlasmids }) {
|
|
|
257
259
|
}, [setSyntax, addPlasmids, httpClient, backendRoute, addAlert])
|
|
258
260
|
return <>
|
|
259
261
|
<Button color="success" onClick={() => setExistingSyntaxDialogOpen(true)}>Load Syntax</Button>
|
|
260
|
-
{existingSyntaxDialogOpen && <ExistingSyntaxDialog onClose={() => setExistingSyntaxDialogOpen(false)} onSyntaxSelect={onSyntaxSelect}/>}
|
|
262
|
+
{existingSyntaxDialogOpen && <ExistingSyntaxDialog staticContentPath={staticContentPath} onClose={() => setExistingSyntaxDialogOpen(false)} onSyntaxSelect={onSyntaxSelect}/>}
|
|
261
263
|
</>
|
|
262
264
|
}
|
|
263
265
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
|
|
3
2
|
import ExistingSyntaxDialog from './ExistingSyntaxDialog';
|
|
4
3
|
|
|
5
4
|
// Test config
|
|
@@ -50,12 +49,10 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
50
49
|
const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
|
|
51
50
|
|
|
52
51
|
cy.mount(
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
/>
|
|
58
|
-
</ConfigProvider>,
|
|
52
|
+
<ExistingSyntaxDialog
|
|
53
|
+
onClose={onCloseSpy}
|
|
54
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
55
|
+
/>,
|
|
59
56
|
);
|
|
60
57
|
|
|
61
58
|
cy.wait('@getSyntaxes');
|
|
@@ -82,12 +79,10 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
82
79
|
}).as('getPlasmidsData');
|
|
83
80
|
|
|
84
81
|
cy.mount(
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
/>
|
|
90
|
-
</ConfigProvider>,
|
|
82
|
+
<ExistingSyntaxDialog
|
|
83
|
+
onClose={onCloseSpy}
|
|
84
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
85
|
+
/>,
|
|
91
86
|
);
|
|
92
87
|
|
|
93
88
|
cy.wait('@getSyntaxes');
|
|
@@ -111,12 +106,10 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
111
106
|
}).as('getSyntaxDataError');
|
|
112
107
|
|
|
113
108
|
cy.mount(
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/>
|
|
119
|
-
</ConfigProvider>,
|
|
109
|
+
<ExistingSyntaxDialog
|
|
110
|
+
onClose={onCloseSpy}
|
|
111
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
112
|
+
/>,
|
|
120
113
|
);
|
|
121
114
|
|
|
122
115
|
cy.wait('@getSyntaxes');
|
|
@@ -146,12 +139,10 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
146
139
|
}).as('getPlasmidsDataError');
|
|
147
140
|
|
|
148
141
|
cy.mount(
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
/>
|
|
154
|
-
</ConfigProvider>,
|
|
142
|
+
<ExistingSyntaxDialog
|
|
143
|
+
onClose={onCloseSpy}
|
|
144
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
145
|
+
/>,
|
|
155
146
|
);
|
|
156
147
|
|
|
157
148
|
cy.wait('@getSyntaxes');
|
|
@@ -187,12 +178,10 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
187
178
|
}).as('getPlasmidsData2');
|
|
188
179
|
|
|
189
180
|
cy.mount(
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
/>
|
|
195
|
-
</ConfigProvider>,
|
|
181
|
+
<ExistingSyntaxDialog
|
|
182
|
+
onClose={onCloseSpy}
|
|
183
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
184
|
+
/>,
|
|
196
185
|
);
|
|
197
186
|
|
|
198
187
|
cy.wait('@getSyntaxes');
|
|
@@ -232,12 +221,10 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
232
221
|
}
|
|
233
222
|
|
|
234
223
|
cy.mount(
|
|
235
|
-
<
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
/>
|
|
240
|
-
</ConfigProvider>,
|
|
224
|
+
<ExistingSyntaxDialog
|
|
225
|
+
onClose={onCloseSpy}
|
|
226
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
227
|
+
/>,
|
|
241
228
|
);
|
|
242
229
|
|
|
243
230
|
cy.wait('@getSyntaxes');
|
|
@@ -262,12 +249,10 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
262
249
|
}
|
|
263
250
|
|
|
264
251
|
cy.mount(
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
/>
|
|
270
|
-
</ConfigProvider>,
|
|
252
|
+
<ExistingSyntaxDialog
|
|
253
|
+
onClose={onCloseSpy}
|
|
254
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
255
|
+
/>,
|
|
271
256
|
);
|
|
272
257
|
|
|
273
258
|
cy.wait('@getSyntaxes');
|
|
@@ -28,13 +28,12 @@ function LocalSyntaxDialog({ onClose, onSyntaxSelect }) {
|
|
|
28
28
|
)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function ExistingSyntaxDialog({ onClose, onSyntaxSelect }) {
|
|
31
|
+
function ExistingSyntaxDialog({ staticContentPath, onClose, onSyntaxSelect }) {
|
|
32
32
|
const [syntaxes, setSyntaxes] = React.useState([]);
|
|
33
33
|
const [connectAttempt, setConnectAttempt] = React.useState(0);
|
|
34
34
|
const [requestStatus, setRequestStatus] = React.useState({ status: 'loading' });
|
|
35
35
|
const [loadError, setLoadError] = React.useState(null);
|
|
36
36
|
const fileInputRef = React.useRef(null);
|
|
37
|
-
const { staticContentPath } = useConfig();
|
|
38
37
|
const [localDialogOpen, setLocalDialogOpen] = React.useState(false);
|
|
39
38
|
|
|
40
39
|
React.useEffect(() => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isRangeWithinRange } from '@teselagen/range-utils';
|
|
2
2
|
import { getComplementSequenceString, getAminoAcidFromSequenceTriplet, getDigestFragmentsForRestrictionEnzymes, getReverseComplementSequenceString } from '@teselagen/sequence-utils';
|
|
3
3
|
import { allSimplePaths } from 'graphology-simple-path';
|
|
4
|
+
import { openCycleAtNode } from './graph_utils';
|
|
4
5
|
|
|
5
6
|
export function tripletsToTranslation(triplets) {
|
|
6
7
|
if (!triplets) return ''
|
|
@@ -100,6 +101,13 @@ export function getSimplifiedDigestFragments(sequenceData, enzymes) {
|
|
|
100
101
|
return simplifiedDigestFragments.concat(simplifiedDigestFragmentsRc);
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
export function isFragmentPalindromic(fragment) {
|
|
105
|
+
return (
|
|
106
|
+
(fragment.left.ovhg === getReverseComplementSequenceString(fragment.left.ovhg)) &&
|
|
107
|
+
(fragment.right.ovhg === getReverseComplementSequenceString(fragment.right.ovhg))
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
export function assignSequenceToSyntaxPart(sequenceData, enzymes, graph) {
|
|
104
112
|
// Something that is important to understand here is the meaning of forward and reverse.
|
|
105
113
|
// It does not mean whether the overhang is 5' or 3', the value on the top strand is always
|
|
@@ -111,7 +119,8 @@ export function assignSequenceToSyntaxPart(sequenceData, enzymes, graph) {
|
|
|
111
119
|
simplifiedDigestFragments
|
|
112
120
|
.filter(f => f.left.forward && !f.right.forward && graph.hasNode(f.left.ovhg) && graph.hasNode(f.right.ovhg))
|
|
113
121
|
.forEach(fragment => {
|
|
114
|
-
const
|
|
122
|
+
const graphForPaths = isFragmentPalindromic(fragment) ? openCycleAtNode(graph, graph.nodes()[0]) : graph;
|
|
123
|
+
const paths = allSimplePaths(graphForPaths, fragment.left.ovhg, fragment.right.ovhg);
|
|
115
124
|
if (paths.length > 0) {
|
|
116
125
|
foundParts.push({left_overhang: fragment.left.ovhg, right_overhang: fragment.right.ovhg, longestFeature: fragment.longestFeature});
|
|
117
126
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { aliasedEnzymesByName, getDigestFragmentsForRestrictionEnzymes, getReverseComplementSequenceString, getComplementSequenceString } from "@teselagen/sequence-utils";
|
|
2
|
-
import
|
|
1
|
+
import { aliasedEnzymesByName, getDigestFragmentsForRestrictionEnzymes, getReverseComplementSequenceString, getComplementSequenceString, getReverseComplementSequenceAndAnnotations } from "@teselagen/sequence-utils";
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { assignSequenceToSyntaxPart, simplifyDigestFragment, reverseComplementSimplifiedDigestFragment, tripletsToTranslation, partDataToDisplayData, arrayCombinations, getSimplifiedDigestFragments } from "./assembler_utils";
|
|
3
4
|
import { partsToEdgesGraph } from "./graph_utils";
|
|
5
|
+
import { genbankToJson } from '@teselagen/bio-parsers';
|
|
4
6
|
|
|
5
7
|
const sequenceBsaI = 'tgggtctcaTACTagagtcacacaggactactaAATGagagacctac';
|
|
6
8
|
const sequenceBsaI2 = 'tgggtctcaAATGagagtcacacaggactactaAGGTagagacctac'
|
|
@@ -28,6 +30,30 @@ describe('reverseComplementSimplifiedDigestFragment', () => {
|
|
|
28
30
|
});
|
|
29
31
|
});
|
|
30
32
|
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
it('handles palindromic fragments', () => {
|
|
36
|
+
|
|
37
|
+
const parts = [
|
|
38
|
+
{left_overhang: 'AATT', right_overhang: 'AGCT'}, // This part is palindromic, should only be picked up in AATT-AGCT, not AGCT-AATT
|
|
39
|
+
{left_overhang: 'AGCT', right_overhang: 'GGAG'},
|
|
40
|
+
{left_overhang: 'GGAG', right_overhang: 'AATT'},
|
|
41
|
+
]
|
|
42
|
+
// Read file
|
|
43
|
+
const sequence = 'tgggtctcaAATTagagtcacacaggactactaAGCTagagacctac'
|
|
44
|
+
const seqData = { sequence, circular: true };
|
|
45
|
+
const result = assignSequenceToSyntaxPart(seqData, [aliasedEnzymesByName["bsai"]], partsToEdgesGraph(parts));
|
|
46
|
+
|
|
47
|
+
expect(result).toEqual([{left_overhang: 'AATT', right_overhang: 'AGCT', longestFeature: null}]);
|
|
48
|
+
|
|
49
|
+
const seqDataRc = getReverseComplementSequenceAndAnnotations(seqData);
|
|
50
|
+
const resultRc = assignSequenceToSyntaxPart(seqDataRc, [aliasedEnzymesByName["bsai"]], partsToEdgesGraph(parts));
|
|
51
|
+
|
|
52
|
+
expect(resultRc).toEqual([{left_overhang: 'AATT', right_overhang: 'AGCT', longestFeature: null}]);
|
|
53
|
+
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
|
|
31
57
|
it('shows the meaning of forward and reverse', () => {
|
|
32
58
|
const sequence = 'aaGGTCTCaTACTaaa'
|
|
33
59
|
const digestFragments = getDigestFragmentsForRestrictionEnzymes(
|
|
@@ -40,9 +40,11 @@ export function partsToEdgesGraph(parts) {
|
|
|
40
40
|
|
|
41
41
|
// Break cycles by removing incoming edges to a node
|
|
42
42
|
export function openCycleAtNode(graph, cutNode) {
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const newGraph = graph.copy();
|
|
44
|
+
for (const edge of newGraph.inEdges(cutNode)) {
|
|
45
|
+
newGraph.dropEdge(edge);
|
|
45
46
|
}
|
|
47
|
+
return newGraph;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
// Convert DAG to MSA-like matrix (rows = paths, columns = topological generations)
|
|
@@ -142,8 +144,7 @@ function minimumCoveringRows(msa) {
|
|
|
142
144
|
|
|
143
145
|
export function graphToMSA(graph) {
|
|
144
146
|
if (graph.nodes().length === 0) return [];
|
|
145
|
-
const newGraph = graph.
|
|
146
|
-
openCycleAtNode(newGraph, newGraph.nodes()[0]);
|
|
147
|
+
const newGraph = openCycleAtNode(graph, graph.nodes()[0]);
|
|
147
148
|
return minimumCoveringRows(dagToMSA(newGraph));
|
|
148
149
|
}
|
|
149
150
|
|
|
@@ -114,9 +114,9 @@ describe('openCycleAtNode', () => {
|
|
|
114
114
|
graph.addEdge('C', 'B'); // Creates cycle
|
|
115
115
|
|
|
116
116
|
expect(graph.inDegree('B')).toBe(2);
|
|
117
|
-
openCycleAtNode(graph, 'B');
|
|
118
|
-
expect(
|
|
119
|
-
expect(
|
|
117
|
+
const newGraph = openCycleAtNode(graph, 'B');
|
|
118
|
+
expect(newGraph.inDegree('B')).toBe(0);
|
|
119
|
+
expect(newGraph.outDegree('B')).toBe(1);
|
|
120
120
|
|
|
121
121
|
});
|
|
122
122
|
|
|
@@ -127,9 +127,9 @@ describe('openCycleAtNode', () => {
|
|
|
127
127
|
graph.addEdge('A', 'B');
|
|
128
128
|
graph.addEdge('B', 'A'); // Cycle
|
|
129
129
|
|
|
130
|
-
openCycleAtNode(graph, 'A');
|
|
131
|
-
expect(
|
|
132
|
-
expect(
|
|
130
|
+
const newGraph = openCycleAtNode(graph, 'A');
|
|
131
|
+
expect(newGraph.outDegree('A')).toBe(1);
|
|
132
|
+
expect(newGraph.hasEdge('A', 'B')).toBe(true);
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
it('handles node with no incoming edges', () => {
|
|
@@ -138,9 +138,9 @@ describe('openCycleAtNode', () => {
|
|
|
138
138
|
graph.addNode('B');
|
|
139
139
|
graph.addEdge('A', 'B');
|
|
140
140
|
|
|
141
|
-
openCycleAtNode(graph, 'A');
|
|
142
|
-
expect(
|
|
143
|
-
expect(
|
|
141
|
+
const newGraph = openCycleAtNode(graph, 'A');
|
|
142
|
+
expect(newGraph.inDegree('A')).toBe(0);
|
|
143
|
+
expect(newGraph.hasEdge('A', 'B')).toBe(true);
|
|
144
144
|
});
|
|
145
145
|
});
|
|
146
146
|
|
package/src/components/primers/primer_design/SequenceTabComponents/RestrictionSpacerForm.jsx
CHANGED
|
@@ -6,7 +6,7 @@ import { updateEditor } from '@teselagen/ove';
|
|
|
6
6
|
import EnzymeMultiSelect from '../../../form/EnzymeMultiSelect';
|
|
7
7
|
import { stringIsNotDNA } from '@opencloning/store/cloning_utils';
|
|
8
8
|
import { usePrimerDesign } from './PrimerDesignContext';
|
|
9
|
-
import {
|
|
9
|
+
import { isEnzymePalindromic } from '@opencloning/utils/enzyme_utils';
|
|
10
10
|
|
|
11
11
|
function RestrictionSpacerForm() {
|
|
12
12
|
const { primerDesignSettings } = usePrimerDesign();
|
|
@@ -39,7 +39,7 @@ function RestrictionSpacerForm() {
|
|
|
39
39
|
<FormControl sx={{ width: '10em', mt: 1.5, mr: 2 }}>
|
|
40
40
|
<EnzymeMultiSelect value={leftEnzyme} setEnzymes={(v) => updateEnzymeSettings({ left_enzyme: v })} label="Left enzyme" multiple={false} />
|
|
41
41
|
</FormControl>
|
|
42
|
-
{leftEnzyme && !
|
|
42
|
+
{leftEnzyme && !isEnzymePalindromic(leftEnzyme) && (
|
|
43
43
|
<FormControlLabel
|
|
44
44
|
control={<Checkbox checked={leftEnzymeInverted} onChange={(e) => updateEnzymeSettings({ left_enzyme_inverted: e.target.checked })} />}
|
|
45
45
|
label="Invert site"
|
|
@@ -50,7 +50,7 @@ function RestrictionSpacerForm() {
|
|
|
50
50
|
<FormControl sx={{ width: '10em', mt: 1.5, mr: 2 }}>
|
|
51
51
|
<EnzymeMultiSelect value={rightEnzyme} setEnzymes={(v) => updateEnzymeSettings({ right_enzyme: v })} label="Right enzyme" multiple={false} />
|
|
52
52
|
</FormControl>
|
|
53
|
-
{rightEnzyme && !
|
|
53
|
+
{rightEnzyme && !isEnzymePalindromic(rightEnzyme) && (
|
|
54
54
|
<FormControlLabel
|
|
55
55
|
control={<Checkbox checked={rightEnzymeInverted} onChange={(e) => updateEnzymeSettings({ right_enzyme_inverted: e.target.checked })} />}
|
|
56
56
|
label="Invert site"
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Version placeholder - replaced at publish time via prepack script
|
|
2
|
-
export const version = "1.4.
|
|
2
|
+
export const version = "1.4.4";
|