@rivm-loculus-tools/common-frontend 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +288 -0
  3. package/eslint.config.js +16 -0
  4. package/package.json +156 -3
  5. package/scripts/update-exports.js +9 -0
  6. package/src/.gitkeep +0 -0
  7. package/src/@types/vite-env.d.ts +3 -0
  8. package/src/classes/services/FastaDataEventBusService/FastaDataEventBusService.ts +22 -0
  9. package/src/classes/services/FastaDataEventBusService/index.ts +1 -0
  10. package/src/components/FastaDataImportDialog/FastaDataImportDialog.tsx +364 -0
  11. package/src/components/FastaDataImportDialog/index.ts +1 -0
  12. package/src/components/FastaDataImportDialogLoader/FastaDataImportDialogLoader.tsx +124 -0
  13. package/src/components/FastaDataImportDialogLoader/index.ts +1 -0
  14. package/src/components/FastaNotLoaded/FastaNotLoaded.tsx +62 -0
  15. package/src/components/FastaNotLoaded/index.ts +1 -0
  16. package/src/components/FastaPageHeaderCenterComponent/FastaPageHeaderCenterComponent.tsx +50 -0
  17. package/src/components/FastaPageHeaderCenterComponent/index.ts +1 -0
  18. package/src/components/FastaPageHeaderImportButton/FastaPageHeaderImportButton.tsx +28 -0
  19. package/src/components/FastaPageHeaderImportButton/index.ts +1 -0
  20. package/src/components/FastaPageHeaderResetButton/FastaPageHeaderResetButton.tsx +50 -0
  21. package/src/components/FastaPageHeaderResetButton/index.ts +1 -0
  22. package/src/components/I18nWrapper/I18nWrapper.tsx +39 -0
  23. package/src/components/I18nWrapper/index.ts +1 -0
  24. package/src/components/PageHeaderDocumentationButton/PageHeaderDocumentationButton.tsx +25 -0
  25. package/src/components/PageHeaderDocumentationButton/index.ts +1 -0
  26. package/src/components/SearchBar/SearchBar.tsx +207 -0
  27. package/src/components/SearchBar/SearchBarNodeCounter.tsx +23 -0
  28. package/src/components/SearchBar/index.ts +1 -0
  29. package/src/components/page/ErrorPageCommon.tsx +51 -0
  30. package/src/components/page/PageCommon.tsx +76 -0
  31. package/src/components/page/PageCommonContext.ts +15 -0
  32. package/src/components/page/PageCommonContextProvider.tsx +30 -0
  33. package/src/components/page/PageCommonDrawer.tsx +35 -0
  34. package/src/components/page/PageCommonHeader.tsx +99 -0
  35. package/src/components/page/index.ts +6 -0
  36. package/src/constants/page.ts +1 -0
  37. package/src/index.ts +21 -0
  38. package/src/models/fasta.ts +22 -0
  39. package/src/models/metadata.ts +10 -0
  40. package/src/stores/fastaDataStore/fastaDataStore.ts +103 -0
  41. package/src/stores/fastaDataStore/index.ts +1 -0
  42. package/src/theme/fonts/RobotoFlex.woff2 +0 -0
  43. package/src/theme/index.ts +1 -0
  44. package/src/theme/theme.ts +368 -0
  45. package/src/utils/DownloadUtil/DownloadUtil.ts +10 -0
  46. package/src/utils/DownloadUtil/index.ts +1 -0
  47. package/src/utils/ErrorUtil/ErrorUtil.ts +5 -0
  48. package/src/utils/ErrorUtil/index.ts +1 -0
  49. package/src/utils/FastaUtil/FastaUtil.ts +109 -0
  50. package/src/utils/FastaUtil/index.ts +1 -0
  51. package/src/utils/MetaDataUtil/MetaDataUtil.ts +292 -0
  52. package/src/utils/MetaDataUtil/index.ts +1 -0
  53. package/src/utils/UrlUtil/UrlUtil.ts +35 -0
  54. package/src/utils/UrlUtil/index.ts +1 -0
  55. package/tsconfig.build.json +14 -0
  56. package/tsconfig.json +32 -0
  57. package/vite.config.ts +137 -0
  58. package/README.md +0 -3
@@ -0,0 +1,103 @@
1
+ import type { StoreApi } from 'zustand';
2
+
3
+ import type { FastaParseResult } from '../../models/fasta';
4
+ import { LOADING_STATUS } from '../../models/fasta';
5
+ import type { MetadataParseResult } from '../../models/metadata';
6
+ import { FastaUtil } from '../../utils/FastaUtil';
7
+ import { MetaDataUtil } from '../../utils/MetaDataUtil';
8
+
9
+ export type FastaDataStore = FastaDataStoreActions & FastaDataStoreState;
10
+
11
+ export interface FastaDataStoreActions {
12
+ resetDataset: () => void;
13
+ setColumnMatchCounts: (counts: Record<string, number>) => void;
14
+ setData: (fasta: string, tsv?: string) => Promise<{ fastaResult: FastaParseResult; metadataResult?: MetadataParseResult }>;
15
+ setLoadingStatus: (status: LOADING_STATUS) => void;
16
+ setMatchKeyField: (field: null | string) => void;
17
+ }
18
+
19
+ export interface FastaDataStoreState {
20
+ columnMatchCounts: Record<string, number>;
21
+ loadingError: null | string;
22
+ loadingStatus: LOADING_STATUS;
23
+ matchKeyField: null | string;
24
+ parsedFasta: FastaParseResult | null;
25
+ parsedMetadata: MetadataParseResult | null;
26
+ rawFasta: string;
27
+ rawMetadata: string;
28
+ }
29
+
30
+ export const createFastaDataStoreInitialState = (): FastaDataStoreState => ({
31
+ columnMatchCounts: {},
32
+ loadingError: null,
33
+ loadingStatus: LOADING_STATUS.IDLE,
34
+ matchKeyField: null,
35
+ parsedFasta: null,
36
+ parsedMetadata: null,
37
+ rawFasta: '',
38
+ rawMetadata: '',
39
+ });
40
+
41
+
42
+ type Get<T extends FastaDataStore> = StoreApi<T>['getState'];
43
+ type Set<T extends FastaDataStore> = StoreApi<T>['setState'];
44
+
45
+ export const createFastaDataStoreActions = <T extends FastaDataStore>(kwArgs: { get: Get<T>; set: Set<T> }): FastaDataStoreActions => {
46
+ const { get, set } = kwArgs;
47
+ const setBaseState = (partial: Partial<FastaDataStoreState>) => set(partial as Partial<T>);
48
+
49
+ return {
50
+ resetDataset: () => setBaseState(createFastaDataStoreInitialState()),
51
+ setColumnMatchCounts: (counts: Record<string, number>) => setBaseState({ columnMatchCounts: counts }),
52
+ setData: async (fasta: string, tsv?: string) => {
53
+ const parsedFasta = FastaUtil.parseFasta(fasta);
54
+
55
+ if (parsedFasta.error) {
56
+ setBaseState({
57
+ loadingError: parsedFasta.error,
58
+ loadingStatus: LOADING_STATUS.ERROR,
59
+ });
60
+ throw new Error(parsedFasta.error);
61
+ }
62
+
63
+ setBaseState({
64
+ parsedFasta,
65
+ rawFasta: fasta,
66
+ });
67
+
68
+ let parsedMetadata: MetadataParseResult | undefined;
69
+ if (tsv) {
70
+ parsedMetadata = MetaDataUtil.parseTsv(tsv);
71
+ if (parsedMetadata.error) {
72
+ setBaseState({
73
+ loadingError: parsedMetadata.error,
74
+ loadingStatus: LOADING_STATUS.ERROR,
75
+ });
76
+ throw new Error(parsedMetadata.error);
77
+ }
78
+
79
+ setBaseState({
80
+ parsedMetadata,
81
+ rawMetadata: tsv,
82
+ });
83
+
84
+ const fastaIds = parsedFasta.sequences.map((s) => s.id);
85
+ const { bestColumn, matchCounts } = MetaDataUtil.findBestMatchingColumn(
86
+ parsedMetadata.columns,
87
+ parsedMetadata.recordsByColumn,
88
+ fastaIds,
89
+ );
90
+ get().setColumnMatchCounts(matchCounts);
91
+ get().setMatchKeyField(bestColumn);
92
+ }
93
+
94
+ setBaseState({
95
+ loadingError: null,
96
+ loadingStatus: LOADING_STATUS.SUCCESS,
97
+ });
98
+ return Promise.resolve({ fastaResult: parsedFasta, metadataResult: parsedMetadata });
99
+ },
100
+ setLoadingStatus: (status: LOADING_STATUS) => setBaseState({ loadingStatus: status }),
101
+ setMatchKeyField: (field: null | string) => setBaseState({ matchKeyField: field }),
102
+ };
103
+ };
@@ -0,0 +1 @@
1
+ export * from './fastaDataStore';
Binary file
@@ -0,0 +1 @@
1
+ export * from './theme';
@@ -0,0 +1,368 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention */
2
+ import { createTheme } from '@mui/material';
3
+
4
+ import robotoFlexWoff2 from './fonts/RobotoFlex.woff2';
5
+
6
+ const FONT_SIZE = 16;
7
+
8
+ const MuiCssBaselineStyleOverrides = `
9
+ /**
10
+ *:focus-visible, .Mui-focusVisible {
11
+ outline: 2px dotted #000 !important;
12
+ outline-offset: 0 !important;
13
+ box-shadow: 0 0 0 2px #fff !important;
14
+ }
15
+ **/
16
+
17
+ html, body {
18
+ font-size: ${FONT_SIZE}px;
19
+ height: 100%;
20
+ overscroll-behavior: none;
21
+ }
22
+ #root {
23
+ height: 100%;
24
+ }
25
+
26
+ /* cyrillic-ext */
27
+ @font-face {
28
+ font-family: 'Roboto Flex';
29
+ font-style: normal;
30
+ font-weight: 100 1000;
31
+ font-stretch: 100%;
32
+ font-display: swap;
33
+ src: url(${robotoFlexWoff2}) format('woff2');
34
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
35
+ }
36
+ /* cyrillic */
37
+ @font-face {
38
+ font-family: 'Roboto Flex';
39
+ font-style: normal;
40
+ font-weight: 100 1000;
41
+ font-stretch: 100%;
42
+ font-display: swap;
43
+ src: url(${robotoFlexWoff2}) format('woff2');
44
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
45
+ }
46
+ /* greek */
47
+ @font-face {
48
+ font-family: 'Roboto Flex';
49
+ font-style: normal;
50
+ font-weight: 100 1000;
51
+ font-stretch: 100%;
52
+ font-display: swap;
53
+ src: url(${robotoFlexWoff2}) format('woff2');
54
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
55
+ }
56
+ /* vietnamese */
57
+ @font-face {
58
+ font-family: 'Roboto Flex';
59
+ font-style: normal;
60
+ font-weight: 100 1000;
61
+ font-stretch: 100%;
62
+ font-display: swap;
63
+ src: url(${robotoFlexWoff2}) format('woff2');
64
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
65
+ }
66
+ /* latin-ext */
67
+ @font-face {
68
+ font-family: 'Roboto Flex';
69
+ font-style: normal;
70
+ font-weight: 100 1000;
71
+ font-stretch: 100%;
72
+ font-display: swap;
73
+ src: url(${robotoFlexWoff2}) format('woff2');
74
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
75
+ }
76
+ /* latin */
77
+ @font-face {
78
+ font-family: 'Roboto Flex';
79
+ font-style: normal;
80
+ font-weight: 100 1000;
81
+ font-stretch: 100%;
82
+ font-display: swap;
83
+ src: url(${robotoFlexWoff2}) format('woff2');
84
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
85
+ }
86
+ `;
87
+
88
+ export const theme = createTheme({
89
+ breakpoints: {
90
+ values: {
91
+ lg: 1340,
92
+ md: 1024,
93
+ sm: 640,
94
+ xl: 1920,
95
+ xs: 0,
96
+ },
97
+ },
98
+ components: {
99
+ MuiAlertTitle: {
100
+ styleOverrides: {
101
+ root: {
102
+ color: 'inherit',
103
+ fontWeight: 'bold',
104
+ marginBottom: 0,
105
+ },
106
+ },
107
+ },
108
+ MuiButton: {
109
+ defaultProps: {
110
+ color: 'primary',
111
+ variant: 'contained',
112
+ },
113
+ styleOverrides: {
114
+ root: {
115
+ fontWeight: 'bold',
116
+ },
117
+ },
118
+ },
119
+ MuiCssBaseline: {
120
+ styleOverrides: MuiCssBaselineStyleOverrides,
121
+ },
122
+ MuiDivider: {
123
+ styleOverrides: {
124
+ root: {
125
+ backgroundColor: '#c9defc',
126
+ },
127
+ },
128
+ },
129
+ MuiFab: {
130
+ styleOverrides: {
131
+ root: {
132
+ fontWeight: 'bold',
133
+ },
134
+ },
135
+ },
136
+ MuiFilledInput: {
137
+ styleOverrides: {
138
+ input: {
139
+ color: '#282245',
140
+ },
141
+ root: {
142
+ '&:hover': {
143
+ backgroundColor: 'rgba(186,214,237,0.6)',
144
+ },
145
+ '&.Mui-disabled': {
146
+ backgroundColor: 'rgba(186,214,237,0.1)',
147
+ },
148
+ '&.Mui-error': {
149
+ '&:after': {
150
+ borderBottomColor: '#a45c4f',
151
+ },
152
+ '&:hover': {
153
+ backgroundColor: 'rgba(240,180,186,0.6)',
154
+ },
155
+ '&.Mui-disabled': {
156
+ backgroundColor: 'rgba(240,180,186,0.1)',
157
+ },
158
+ '&.Mui-focused': {
159
+ backgroundColor: 'rgba(240,180,186,0.6)',
160
+ },
161
+ backgroundColor: 'rgba(240,180,186,0.4)',
162
+ },
163
+ '&.Mui-focused': {
164
+ backgroundColor: 'rgba(186,214,237,0.3)',
165
+ },
166
+ '&.Mui-warning': {
167
+ '&:after': {
168
+ borderBottomColor: 'rgb(119,80,21)',
169
+ transform: 'scaleX(1)',
170
+ },
171
+ '&:hover': {
172
+ '@media (hover: none)': {
173
+ backgroundColor: 'rgb(248,214,140,0.3)',
174
+ },
175
+ backgroundColor: 'rgb(248,214,140,0.6)',
176
+ },
177
+ '&.Mui-disabled': {
178
+ backgroundColor: 'rgb(248,214,140,0.1)',
179
+ },
180
+ '&.Mui-focused': {
181
+ backgroundColor: 'rgb(248,214,140,0.3)',
182
+ },
183
+ backgroundColor: 'rgb(248,214,140,0.3)',
184
+ },
185
+ backgroundColor: 'rgba(186,214,237,0.2)',
186
+ },
187
+ },
188
+ },
189
+ MuiFormControlLabel: {
190
+ styleOverrides: {
191
+ label: {
192
+ color: '#282245',
193
+ },
194
+ },
195
+ },
196
+ MuiFormHelperText: {
197
+ styleOverrides: {
198
+ root: {
199
+ '&.Mui-error': {
200
+ color: '#ba2818',
201
+ },
202
+ '&.Mui-warning': {
203
+ color: 'rgb(119,80,21)',
204
+ },
205
+ color: '#453e6d',
206
+ },
207
+ },
208
+ },
209
+ MuiFormLabel: {
210
+ styleOverrides: {
211
+ asterisk: {
212
+ color: '#D90000',
213
+ },
214
+ root: {
215
+ '&.Mui-error': {
216
+ color: '#ba2818',
217
+ },
218
+ '&.Mui-warning': {
219
+ color: 'rgb(119,80,21)',
220
+ },
221
+ color: '#453e6d',
222
+ },
223
+ },
224
+ },
225
+ MuiInputLabel: {
226
+ styleOverrides: {
227
+ root: {
228
+ '&.Mui-error': {
229
+ color: '#a45c4f',
230
+ },
231
+ '&.Mui-warning': {
232
+ color: 'rgb(119,80,21)',
233
+ },
234
+ color: '#453e6d',
235
+ },
236
+ },
237
+ },
238
+ MuiLink: {
239
+ styleOverrides: {
240
+ root: {
241
+ '&:hover': {
242
+ textDecoration: 'underline',
243
+ },
244
+ textDecoration: 'underline',
245
+ },
246
+ },
247
+ },
248
+ MuiOutlinedInput: {
249
+ styleOverrides: {
250
+ root: {
251
+ '& input': {
252
+ padding: '7.5px 4px 7.5px 5px',
253
+ },
254
+ padding: '9px',
255
+ },
256
+ },
257
+ },
258
+ MuiSelect: {
259
+ styleOverrides: {
260
+ select: {
261
+ padding: '7.5px 32px 7.5px 5px',
262
+ },
263
+ },
264
+ },
265
+ MuiTableCell: {
266
+ styleOverrides: {
267
+ root: {
268
+ borderColor: '#c9defc',
269
+ },
270
+ },
271
+ },
272
+ MuiTooltip: {
273
+ defaultProps: {
274
+ enterDelay: 500,
275
+ enterNextDelay: 500,
276
+ },
277
+ },
278
+ MuiTypography: {
279
+ styleOverrides: {
280
+ body1: {
281
+ color: 'rgba(0,0,0,0.87)',
282
+ fontSize: '1rem', // 14
283
+ },
284
+ body2: {
285
+ color: 'rgba(0,0,0,0.87)',
286
+ fontSize: '1rem', // 14
287
+ },
288
+ caption: {
289
+ fontStyle: 'italic',
290
+ },
291
+ h1: {
292
+ color: '#171c42',
293
+ fontSize: '2.143rem', // 30
294
+ fontWeight: 700,
295
+ },
296
+ h2: {
297
+ color: '#171c42',
298
+ fontSize: '1.857rem', // 26
299
+ fontWeight: 400,
300
+ },
301
+ h3: {
302
+ color: '#171c42',
303
+ fontSize: '1.714rem', // 24
304
+ fontWeight: 700,
305
+ },
306
+ h4: {
307
+ color: '#171c42',
308
+ fontSize: '1.429rem', // 20
309
+ fontWeight: 700,
310
+ },
311
+ h5: {
312
+ color: '#171c42',
313
+ fontSize: '1.286rem', // 16
314
+ fontWeight: 700,
315
+ },
316
+ h6: {
317
+ color: '#171c42',
318
+ fontSize: '1.143rem', // 16
319
+ fontWeight: 400,
320
+ },
321
+ subtitle1: {
322
+ color: 'rgba(0,0,0,0.87)',
323
+ fontSize: '1.286rem', // 18
324
+ fontWeight: 400,
325
+ },
326
+ subtitle2: {
327
+ color: '#171c42',
328
+ fontSize: '1.143rem', // 16
329
+ fontWeight: 700,
330
+ },
331
+ },
332
+ },
333
+ },
334
+ palette: {
335
+ divider: '#eeeeee',
336
+ error: {
337
+ contrastText: '#fff',
338
+ main: '#cf0101',
339
+ },
340
+ grey: {
341
+ 100: '#eeeeee',
342
+ 500: '#777777',
343
+ },
344
+ mode: 'light',
345
+ primary: {
346
+ contrastText: '#fff',
347
+ main: '#154273',
348
+ },
349
+ secondary: {
350
+ contrastText: '#fff',
351
+ main: '#2a2b33',
352
+ },
353
+ success: {
354
+ contrastText: '#fff',
355
+ main: '#05B803',
356
+ },
357
+ },
358
+ spacing: 8,
359
+ typography: {
360
+ button: {
361
+ fontWeight: 600,
362
+ textTransform: 'none',
363
+ },
364
+ fontFamily: 'Roboto Flex',
365
+ fontSize: FONT_SIZE,
366
+ htmlFontSize: FONT_SIZE,
367
+ },
368
+ });
@@ -0,0 +1,10 @@
1
+ export class DownloadUtil {
2
+ public static datasetFilename(label: string): string {
3
+ const withoutExtension = label.trim().replace(/\.[^.]+$/, '');
4
+ const safeLabel = withoutExtension
5
+ .replace(/[^a-zA-Z0-9._-]+/g, '_')
6
+ .replace(/^\.+|\.+$/g, '')
7
+ .slice(0, 80);
8
+ return `${safeLabel || 'alignment'}.fasta`;
9
+ }
10
+ }
@@ -0,0 +1 @@
1
+ export * from './DownloadUtil';
@@ -0,0 +1,5 @@
1
+ export class ErrorUtil {
2
+ public static getErrorMessage(error: unknown, fallback: string): string {
3
+ return error instanceof Error ? error.message : fallback;
4
+ }
5
+ }
@@ -0,0 +1 @@
1
+ export * from './ErrorUtil';
@@ -0,0 +1,109 @@
1
+ import type {
2
+ FastaParseResult,
3
+ FastaSequence,
4
+ } from '../../models/fasta';
5
+
6
+ export class FastaUtil {
7
+ public static parseFasta(content: string): FastaParseResult {
8
+ if (!content?.trim()) {
9
+ return FastaUtil.createErrorResult('The FASTA input is empty.');
10
+ }
11
+
12
+ const lines = content.split(/\r?\n/);
13
+ const sequences: FastaSequence[] = [];
14
+ let currentHeader = '';
15
+ let currentSeqParts: string[] = [];
16
+
17
+ const commitSequence = (): FastaParseResult | null => {
18
+ if (currentHeader) {
19
+ const fullSeq = currentSeqParts.join('').replace(/\s/g, '').toUpperCase();
20
+ if (!fullSeq.length) {
21
+ return FastaUtil.createErrorResult(`FASTA record "${currentHeader}" has no sequence.`);
22
+ }
23
+
24
+ // Extract ID: part before pipe or tab, sanitized for Newick compatibility
25
+ const firstPart = currentHeader.split(/[|\t]/)[0].trim();
26
+ const safeId = firstPart.replace(/[():;,']/g, '_');
27
+
28
+ sequences.push({
29
+ accessionVersion: safeId,
30
+ header: currentHeader,
31
+ id: safeId,
32
+ length: fullSeq.length,
33
+ sequence: fullSeq,
34
+ });
35
+ }
36
+ return null;
37
+ };
38
+
39
+ for (const sourceLine of lines) {
40
+ const line = sourceLine.trim();
41
+ if (!line) {
42
+ continue;
43
+ }
44
+
45
+ if (line.startsWith('>')) {
46
+ const error = commitSequence();
47
+ if (error) {
48
+ return error;
49
+ }
50
+ currentHeader = line.slice(1).trim();
51
+ if (!currentHeader) {
52
+ return FastaUtil.createErrorResult('Every FASTA record needs a non-empty header.');
53
+ }
54
+ currentSeqParts = [];
55
+ } else {
56
+ if (!currentHeader) {
57
+ return FastaUtil.createErrorResult('FASTA sequence data appeared before its first header.');
58
+ }
59
+ currentSeqParts.push(line);
60
+ }
61
+ }
62
+ const error = commitSequence();
63
+ if (error) {
64
+ return error;
65
+ }
66
+
67
+ if (sequences.length === 0) {
68
+ return FastaUtil.createErrorResult('No FASTA records were found.');
69
+ }
70
+
71
+ // Pre-alignment validation: verify all sequence lengths are equal
72
+ const firstLength = sequences[0].length;
73
+ const invalidSequences = sequences.filter((s) => s.length !== firstLength).map((s) => ({ id: s.id, length: s.length }));
74
+
75
+ const isAligned = invalidSequences.length === 0;
76
+
77
+ return {
78
+ expectedLength: firstLength,
79
+ invalidSequences: isAligned ? undefined : invalidSequences,
80
+ isAligned,
81
+ sequences,
82
+ };
83
+ }
84
+
85
+ public static stripFastaHeaders(fastaContent: string): string {
86
+ const lines = fastaContent.split('\n');
87
+ const strippedLines = lines.map((line) => {
88
+ if (line.startsWith('>')) {
89
+ const cleanHeader = line.substring(1).trim();
90
+ const firstPart = cleanHeader.split(/[|\t]/)[0].trim();
91
+ const safeId = firstPart.replace(/[():;,']/g, '_');
92
+ return `>${safeId}`;
93
+ }
94
+ return line;
95
+ });
96
+ return strippedLines.join('\n');
97
+ }
98
+
99
+
100
+ private static createErrorResult(error: string): FastaParseResult {
101
+ return {
102
+ error,
103
+ expectedLength: 0,
104
+ isAligned: false,
105
+ sequences: [],
106
+ };
107
+ }
108
+
109
+ }
@@ -0,0 +1 @@
1
+ export * from './FastaUtil';