@liiift-studio/sanity-font-manager 2.3.19 → 2.5.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.
- package/README.md +437 -437
- package/dist/UploadModal-6LIX7XOK.js +6 -0
- package/dist/UploadModal-NME2W53V.mjs +6 -0
- package/dist/chunk-646WCBRR.mjs +7276 -0
- package/dist/chunk-FH4QKHOH.js +7276 -0
- package/dist/index.js +747 -1675
- package/dist/index.mjs +400 -1237
- package/package.json +85 -85
- package/src/components/BatchUploadFonts.jsx +653 -639
- package/src/components/BulkActions.jsx +99 -0
- package/src/components/ExistingDocumentResolver.jsx +152 -0
- package/src/components/FontReviewCard.jsx +415 -0
- package/src/components/FontScriptUploaderComponent.jsx +463 -463
- package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
- package/src/components/KeyValueInput.jsx +95 -95
- package/src/components/KeyValueReferenceInput.jsx +254 -254
- package/src/components/NestedObjectArraySelector.jsx +146 -146
- package/src/components/PriceInput.jsx +26 -26
- package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
- package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
- package/src/components/SetOTF.jsx +87 -87
- package/src/components/SingleUploaderTool.jsx +672 -673
- package/src/components/StatusDisplay.jsx +26 -26
- package/src/components/StyleCountInput.jsx +16 -16
- package/src/components/UpdateScriptsComponent.jsx +76 -76
- package/src/components/UploadButton.jsx +43 -43
- package/src/components/UploadModal.jsx +268 -0
- package/src/components/UploadScriptsComponent.jsx +539 -537
- package/src/components/UploadStep1Settings.jsx +272 -0
- package/src/components/UploadStep2Review.jsx +472 -0
- package/src/components/UploadStep3Execute.jsx +234 -0
- package/src/components/UploadSummary.jsx +196 -0
- package/src/components/VariableInstanceReferencesInput.jsx +190 -190
- package/src/hooks/useNestedObjects.js +92 -92
- package/src/hooks/useSanityClient.js +9 -9
- package/src/index.js +115 -70
- package/src/schema/openTypeField.js +1945 -1945
- package/src/schema/styleCountField.js +12 -12
- package/src/schema/stylesField.js +268 -268
- package/src/schema/stylisticSetField.js +301 -301
- package/src/utils/buildUploadPlan.js +325 -0
- package/src/utils/executeUploadPlan.js +437 -0
- package/src/utils/executionReducer.js +56 -0
- package/src/utils/fontHelpers.js +267 -0
- package/src/utils/generateCssFile.js +207 -205
- package/src/utils/generateFontData.js +98 -145
- package/src/utils/generateFontFile.js +38 -38
- package/src/utils/generateKeywords.js +185 -185
- package/src/utils/generateSubset.js +45 -45
- package/src/utils/getEmptyFontKit.js +101 -99
- package/src/utils/parseFont.js +55 -0
- package/src/utils/parseVariableFontInstances.js +211 -211
- package/src/utils/planReducer.js +517 -0
- package/src/utils/planTypes.js +183 -0
- package/src/utils/processFontFiles.js +529 -477
- package/src/utils/regenerateFontData.js +146 -146
- package/src/utils/resolveExistingFont.js +87 -0
- package/src/utils/sanitizeForSanityId.js +65 -65
- package/src/utils/updateFontPrices.js +94 -94
- package/src/utils/updateTypefaceDocument.js +149 -160
- package/src/utils/uploadFontFiles.js +405 -316
- package/src/utils/utils.js +24 -24
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
// Shared status bar — shows status message in green/red with an optional action slot on the far right
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { Flex, Text } from '@sanity/ui';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Shows an upload/operation status string coloured green on success and red on error.
|
|
8
|
-
* Accepts an optional `action` element rendered on the far right.
|
|
9
|
-
* @param {Object} props
|
|
10
|
-
* @param {string} props.status - Status message to display
|
|
11
|
-
* @param {boolean} props.error - Whether the current status represents an error
|
|
12
|
-
* @param {React.ReactNode} [props.action] - Optional element to render on the far right
|
|
13
|
-
*/
|
|
14
|
-
const StatusDisplay = ({ status, error, action }) => {
|
|
15
|
-
return (
|
|
16
|
-
<Flex paddingTop={1} paddingBottom={3} align="center" justify="space-between">
|
|
17
|
-
<Flex align="center" gap={2}>
|
|
18
|
-
<Text size={1}>Status:</Text>
|
|
19
|
-
<Text size={1} style={{ color: error ? 'red' : 'green' }}>{status}</Text>
|
|
20
|
-
</Flex>
|
|
21
|
-
{action && action}
|
|
22
|
-
</Flex>
|
|
23
|
-
);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export default StatusDisplay;
|
|
1
|
+
// Shared status bar — shows status message in green/red with an optional action slot on the far right
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Flex, Text } from '@sanity/ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shows an upload/operation status string coloured green on success and red on error.
|
|
8
|
+
* Accepts an optional `action` element rendered on the far right.
|
|
9
|
+
* @param {Object} props
|
|
10
|
+
* @param {string} props.status - Status message to display
|
|
11
|
+
* @param {boolean} props.error - Whether the current status represents an error
|
|
12
|
+
* @param {React.ReactNode} [props.action] - Optional element to render on the far right
|
|
13
|
+
*/
|
|
14
|
+
const StatusDisplay = ({ status, error, action }) => {
|
|
15
|
+
return (
|
|
16
|
+
<Flex paddingTop={1} paddingBottom={3} align="center" justify="space-between">
|
|
17
|
+
<Flex align="center" gap={2}>
|
|
18
|
+
<Text size={1}>Status:</Text>
|
|
19
|
+
<Text size={1} style={{ color: error ? 'red' : 'green' }}>{status}</Text>
|
|
20
|
+
</Flex>
|
|
21
|
+
{action && action}
|
|
22
|
+
</Flex>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default StatusDisplay;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
// Displays the total count of static and variable font styles linked to a typeface document
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { Text } from '@sanity/ui';
|
|
5
|
-
import { useFormValue } from 'sanity';
|
|
6
|
-
|
|
7
|
-
/** Reads styles.fonts and styles.variableFont arrays and displays the combined count. */
|
|
8
|
-
export const StyleCountInput = (props) => {
|
|
9
|
-
const styles = useFormValue(['styles', 'fonts']) || [];
|
|
10
|
-
const vfStyles = useFormValue(['styles', 'variableFont']) || [];
|
|
11
|
-
const count = styles.length + vfStyles.length;
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<Text size={1}>{count}</Text>
|
|
15
|
-
);
|
|
16
|
-
};
|
|
1
|
+
// Displays the total count of static and variable font styles linked to a typeface document
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Text } from '@sanity/ui';
|
|
5
|
+
import { useFormValue } from 'sanity';
|
|
6
|
+
|
|
7
|
+
/** Reads styles.fonts and styles.variableFont arrays and displays the combined count. */
|
|
8
|
+
export const StyleCountInput = (props) => {
|
|
9
|
+
const styles = useFormValue(['styles', 'fonts']) || [];
|
|
10
|
+
const vfStyles = useFormValue(['styles', 'variableFont']) || [];
|
|
11
|
+
const count = styles.length + vfStyles.length;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Text size={1}>{count}</Text>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
// Updates and re-links existing script font variant references on font documents
|
|
2
|
-
|
|
3
|
-
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
4
|
-
import { Stack, Text, Button } from '@sanity/ui';
|
|
5
|
-
import { useFormValue, set } from 'sanity';
|
|
6
|
-
|
|
7
|
-
import { useSanityClient } from '../hooks/useSanityClient';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Wraps the default Sanity scripts array input with a button that reads
|
|
11
|
-
* scriptFileInput from all linked font documents and syncs the list.
|
|
12
|
-
* @param {Object} props - Sanity input component props
|
|
13
|
-
*/
|
|
14
|
-
export const UpdateScriptsComponent = (props) => {
|
|
15
|
-
const { onChange } = props;
|
|
16
|
-
|
|
17
|
-
const client = useSanityClient();
|
|
18
|
-
const scripts = useFormValue(['scripts']) || [];
|
|
19
|
-
const fonts = useFormValue(['styles', 'fonts']);
|
|
20
|
-
|
|
21
|
-
const isReadyRef = useRef(false);
|
|
22
|
-
const [message, setMessage] = useState('');
|
|
23
|
-
|
|
24
|
-
// Delay ready flag to avoid triggering onChange during initial mount
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
const timer = setTimeout(() => { isReadyRef.current = true; }, 100);
|
|
27
|
-
return () => clearTimeout(timer);
|
|
28
|
-
}, []);
|
|
29
|
-
|
|
30
|
-
/** Fetches all linked font documents and derives the unique script list from their scriptFileInput fields. */
|
|
31
|
-
const updateFromFonts = useCallback(async () => {
|
|
32
|
-
if (!fonts || fonts.length === 0) {
|
|
33
|
-
setMessage('No fonts found to extract scripts from');
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const fontRefs = fonts.map(font => font._ref);
|
|
38
|
-
|
|
39
|
-
let result;
|
|
40
|
-
try {
|
|
41
|
-
result = await client.fetch(
|
|
42
|
-
`*[_type == "font" && _id in $fontRefs]{ _id, scriptFileInput }`,
|
|
43
|
-
{ fontRefs }
|
|
44
|
-
);
|
|
45
|
-
} catch (err) {
|
|
46
|
-
console.error('Failed to fetch font documents:', err);
|
|
47
|
-
setMessage('Error updating scripts: ' + err.message);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const newScripts = result.reduce((acc, font) => {
|
|
52
|
-
if (!font?.scriptFileInput) return acc;
|
|
53
|
-
for (const language of Object.keys(font.scriptFileInput)) {
|
|
54
|
-
if (!acc.includes(language)) acc.push(language);
|
|
55
|
-
}
|
|
56
|
-
return acc;
|
|
57
|
-
}, []);
|
|
58
|
-
|
|
59
|
-
if (isReadyRef.current) onChange(set(newScripts));
|
|
60
|
-
setMessage('Scripts updated');
|
|
61
|
-
}, [onChange, fonts, client]);
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<Stack space={3}>
|
|
65
|
-
<Button
|
|
66
|
-
mode="ghost"
|
|
67
|
-
tone="primary"
|
|
68
|
-
width="fill"
|
|
69
|
-
text="Update Scripts from Font Files"
|
|
70
|
-
onClick={updateFromFonts}
|
|
71
|
-
/>
|
|
72
|
-
{message && <Text size={1} style={{ color: 'green' }}>{message}</Text>}
|
|
73
|
-
{props.renderDefault(props)}
|
|
74
|
-
</Stack>
|
|
75
|
-
);
|
|
76
|
-
};
|
|
1
|
+
// Updates and re-links existing script font variant references on font documents
|
|
2
|
+
|
|
3
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
4
|
+
import { Stack, Text, Button } from '@sanity/ui';
|
|
5
|
+
import { useFormValue, set } from 'sanity';
|
|
6
|
+
|
|
7
|
+
import { useSanityClient } from '../hooks/useSanityClient';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Wraps the default Sanity scripts array input with a button that reads
|
|
11
|
+
* scriptFileInput from all linked font documents and syncs the list.
|
|
12
|
+
* @param {Object} props - Sanity input component props
|
|
13
|
+
*/
|
|
14
|
+
export const UpdateScriptsComponent = (props) => {
|
|
15
|
+
const { onChange } = props;
|
|
16
|
+
|
|
17
|
+
const client = useSanityClient();
|
|
18
|
+
const scripts = useFormValue(['scripts']) || [];
|
|
19
|
+
const fonts = useFormValue(['styles', 'fonts']);
|
|
20
|
+
|
|
21
|
+
const isReadyRef = useRef(false);
|
|
22
|
+
const [message, setMessage] = useState('');
|
|
23
|
+
|
|
24
|
+
// Delay ready flag to avoid triggering onChange during initial mount
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const timer = setTimeout(() => { isReadyRef.current = true; }, 100);
|
|
27
|
+
return () => clearTimeout(timer);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
/** Fetches all linked font documents and derives the unique script list from their scriptFileInput fields. */
|
|
31
|
+
const updateFromFonts = useCallback(async () => {
|
|
32
|
+
if (!fonts || fonts.length === 0) {
|
|
33
|
+
setMessage('No fonts found to extract scripts from');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const fontRefs = fonts.map(font => font._ref);
|
|
38
|
+
|
|
39
|
+
let result;
|
|
40
|
+
try {
|
|
41
|
+
result = await client.fetch(
|
|
42
|
+
`*[_type == "font" && _id in $fontRefs]{ _id, scriptFileInput }`,
|
|
43
|
+
{ fontRefs }
|
|
44
|
+
);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error('Failed to fetch font documents:', err);
|
|
47
|
+
setMessage('Error updating scripts: ' + err.message);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const newScripts = result.reduce((acc, font) => {
|
|
52
|
+
if (!font?.scriptFileInput) return acc;
|
|
53
|
+
for (const language of Object.keys(font.scriptFileInput)) {
|
|
54
|
+
if (!acc.includes(language)) acc.push(language);
|
|
55
|
+
}
|
|
56
|
+
return acc;
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
if (isReadyRef.current) onChange(set(newScripts));
|
|
60
|
+
setMessage('Scripts updated');
|
|
61
|
+
}, [onChange, fonts, client]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Stack space={3}>
|
|
65
|
+
<Button
|
|
66
|
+
mode="ghost"
|
|
67
|
+
tone="primary"
|
|
68
|
+
width="fill"
|
|
69
|
+
text="Update Scripts from Font Files"
|
|
70
|
+
onClick={updateFromFonts}
|
|
71
|
+
/>
|
|
72
|
+
{message && <Text size={1} style={{ color: 'green' }}>{message}</Text>}
|
|
73
|
+
{props.renderDefault(props)}
|
|
74
|
+
</Stack>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
// Label-wrapped button that triggers a hidden file input
|
|
2
|
-
|
|
3
|
-
import React, { forwardRef } from 'react';
|
|
4
|
-
import { Button, Text } from '@sanity/ui';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Primary button with a transparent full-size file input overlay.
|
|
8
|
-
* The ref is forwarded to the hidden <input> element.
|
|
9
|
-
* @param {Object} props
|
|
10
|
-
* @param {Function} props.handleUpload - onChange handler for the file input
|
|
11
|
-
*/
|
|
12
|
-
const UploadButton = forwardRef(({ handleUpload }, ref) => {
|
|
13
|
-
return (
|
|
14
|
-
<Button
|
|
15
|
-
mode="ghost"
|
|
16
|
-
tone="primary"
|
|
17
|
-
width="fill"
|
|
18
|
-
padding={3}
|
|
19
|
-
style={{ position: 'relative' }}
|
|
20
|
-
>
|
|
21
|
-
<Text align="center">Upload (ttf/otf/woff/woff2/etc...)</Text>
|
|
22
|
-
<input
|
|
23
|
-
ref={ref}
|
|
24
|
-
type="file"
|
|
25
|
-
multiple
|
|
26
|
-
style={{
|
|
27
|
-
position: 'absolute',
|
|
28
|
-
top: 0,
|
|
29
|
-
left: 0,
|
|
30
|
-
width: '100%',
|
|
31
|
-
height: '100%',
|
|
32
|
-
opacity: 0,
|
|
33
|
-
cursor: 'pointer',
|
|
34
|
-
}}
|
|
35
|
-
onChange={handleUpload}
|
|
36
|
-
/>
|
|
37
|
-
</Button>
|
|
38
|
-
);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
UploadButton.displayName = 'UploadButton';
|
|
42
|
-
|
|
43
|
-
export default UploadButton;
|
|
1
|
+
// Label-wrapped button that triggers a hidden file input
|
|
2
|
+
|
|
3
|
+
import React, { forwardRef } from 'react';
|
|
4
|
+
import { Button, Text } from '@sanity/ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Primary button with a transparent full-size file input overlay.
|
|
8
|
+
* The ref is forwarded to the hidden <input> element.
|
|
9
|
+
* @param {Object} props
|
|
10
|
+
* @param {Function} props.handleUpload - onChange handler for the file input
|
|
11
|
+
*/
|
|
12
|
+
const UploadButton = forwardRef(({ handleUpload }, ref) => {
|
|
13
|
+
return (
|
|
14
|
+
<Button
|
|
15
|
+
mode="ghost"
|
|
16
|
+
tone="primary"
|
|
17
|
+
width="fill"
|
|
18
|
+
padding={3}
|
|
19
|
+
style={{ position: 'relative' }}
|
|
20
|
+
>
|
|
21
|
+
<Text align="center">Upload (ttf/otf/woff/woff2/etc...)</Text>
|
|
22
|
+
<input
|
|
23
|
+
ref={ref}
|
|
24
|
+
type="file"
|
|
25
|
+
multiple
|
|
26
|
+
style={{
|
|
27
|
+
position: 'absolute',
|
|
28
|
+
top: 0,
|
|
29
|
+
left: 0,
|
|
30
|
+
width: '100%',
|
|
31
|
+
height: '100%',
|
|
32
|
+
opacity: 0,
|
|
33
|
+
cursor: 'pointer',
|
|
34
|
+
}}
|
|
35
|
+
onChange={handleUpload}
|
|
36
|
+
/>
|
|
37
|
+
</Button>
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
UploadButton.displayName = 'UploadButton';
|
|
42
|
+
|
|
43
|
+
export default UploadButton;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Upload modal — 3-step state machine: Settings → Review → Execute
|
|
2
|
+
|
|
3
|
+
import React, { useReducer, useCallback, useState, useMemo, useRef, useEffect } from 'react';
|
|
4
|
+
import { Dialog, Box, Flex, Text, Badge, Button } from '@sanity/ui';
|
|
5
|
+
import { planReducer } from '../utils/planReducer';
|
|
6
|
+
import { createEmptyPlan, PLAN_PHASE } from '../utils/planTypes';
|
|
7
|
+
import { buildUploadPlan } from '../utils/buildUploadPlan';
|
|
8
|
+
import { generateStyleKeywords } from '../utils/generateKeywords';
|
|
9
|
+
import UploadStep1Settings from './UploadStep1Settings';
|
|
10
|
+
import UploadStep2Review from './UploadStep2Review';
|
|
11
|
+
import UploadStep3Execute from './UploadStep3Execute';
|
|
12
|
+
import UploadSummary from './UploadSummary';
|
|
13
|
+
|
|
14
|
+
/** Step labels for the step indicator */
|
|
15
|
+
const STEPS = [
|
|
16
|
+
{ key: 1, label: 'Upload Files' },
|
|
17
|
+
{ key: 2, label: 'Review' },
|
|
18
|
+
{ key: 3, label: 'Upload' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/** Maps plan phase to active step number */
|
|
22
|
+
function phaseToStep(phase) {
|
|
23
|
+
switch (phase) {
|
|
24
|
+
case PLAN_PHASE.IDLE: return 1;
|
|
25
|
+
case PLAN_PHASE.PROCESSING: return 2;
|
|
26
|
+
case PLAN_PHASE.REVIEWING: return 2;
|
|
27
|
+
case PLAN_PHASE.READY: return 2;
|
|
28
|
+
case PLAN_PHASE.EXECUTING: return 3;
|
|
29
|
+
case PLAN_PHASE.COMPLETE: return 3;
|
|
30
|
+
case PLAN_PHASE.ERROR: return 3;
|
|
31
|
+
default: return 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Upload modal — wraps the 3-step upload workflow in a Sanity UI Dialog.
|
|
37
|
+
*/
|
|
38
|
+
export default function UploadModal({
|
|
39
|
+
open,
|
|
40
|
+
onClose,
|
|
41
|
+
client,
|
|
42
|
+
docId,
|
|
43
|
+
typefaceTitle,
|
|
44
|
+
stylesObject,
|
|
45
|
+
preferredStyleRef,
|
|
46
|
+
slug,
|
|
47
|
+
}) {
|
|
48
|
+
const [plan, dispatch] = useReducer(planReducer, null, () => createEmptyPlan());
|
|
49
|
+
const [processingCancelled, setProcessingCancelled] = useState(false);
|
|
50
|
+
const [executionResult, setExecutionResult] = useState(null);
|
|
51
|
+
const [retryTempIds, setRetryTempIds] = useState(null);
|
|
52
|
+
const cancelRef = useRef(false);
|
|
53
|
+
const focusRef = useRef(null);
|
|
54
|
+
|
|
55
|
+
const { weightKeywordList, italicKeywordList } = useMemo(() => generateStyleKeywords(), []);
|
|
56
|
+
const currentStep = phaseToStep(plan.phase);
|
|
57
|
+
const isExecuting = plan.phase === PLAN_PHASE.EXECUTING;
|
|
58
|
+
|
|
59
|
+
// Prevent accidental close during upload
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!open || !isExecuting) return;
|
|
62
|
+
const handler = (e) => { e.preventDefault(); e.returnValue = ''; };
|
|
63
|
+
window.addEventListener('beforeunload', handler);
|
|
64
|
+
return () => window.removeEventListener('beforeunload', handler);
|
|
65
|
+
}, [open, isExecuting]);
|
|
66
|
+
|
|
67
|
+
// Focus management on step transitions
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (focusRef.current) {
|
|
70
|
+
focusRef.current.focus();
|
|
71
|
+
}
|
|
72
|
+
}, [currentStep]);
|
|
73
|
+
|
|
74
|
+
/** Handle close — confirm if plan has data, block during execution */
|
|
75
|
+
const handleClose = useCallback(() => {
|
|
76
|
+
if (isExecuting) return;
|
|
77
|
+
const hasFonts = Object.keys(plan.fonts).length > 0;
|
|
78
|
+
if (hasFonts && plan.phase !== PLAN_PHASE.COMPLETE) {
|
|
79
|
+
if (!window.confirm('Close the upload modal? All progress will be lost.')) return;
|
|
80
|
+
}
|
|
81
|
+
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.IDLE });
|
|
82
|
+
setExecutionResult(null);
|
|
83
|
+
onClose();
|
|
84
|
+
}, [plan, isExecuting, onClose]);
|
|
85
|
+
|
|
86
|
+
/** Start processing — transition to Step 2 and build the plan */
|
|
87
|
+
const handleStartProcessing = useCallback(async (files, settings) => {
|
|
88
|
+
dispatch({ type: 'SET_SETTINGS', settings });
|
|
89
|
+
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.PROCESSING, totalFiles: files.length });
|
|
90
|
+
cancelRef.current = false;
|
|
91
|
+
setProcessingCancelled(false);
|
|
92
|
+
setExecutionResult(null);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const builtPlan = await buildUploadPlan({
|
|
96
|
+
files,
|
|
97
|
+
typefaceTitle,
|
|
98
|
+
docId,
|
|
99
|
+
settings,
|
|
100
|
+
client,
|
|
101
|
+
stylesObject,
|
|
102
|
+
weightKeywordList,
|
|
103
|
+
italicKeywordList,
|
|
104
|
+
onProgress: (event) => {
|
|
105
|
+
if (cancelRef.current) return;
|
|
106
|
+
// Update progress counter only — don't add entries yet (they haven't been merged by documentId)
|
|
107
|
+
if (event.type === 'font-processed' || event.type === 'font-error') {
|
|
108
|
+
dispatch({ type: 'UPDATE_PROCESSING_PROGRESS', progress: event.progress });
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!cancelRef.current) {
|
|
114
|
+
// Dispatch the final merged plan entries (files with the same documentId are already grouped)
|
|
115
|
+
for (const [tempId, entry] of Object.entries(builtPlan.fonts)) {
|
|
116
|
+
dispatch({ type: 'ADD_PROCESSED_FONT', tempId, fontEntry: entry });
|
|
117
|
+
}
|
|
118
|
+
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.REVIEWING });
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error('Processing failed:', err);
|
|
122
|
+
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.REVIEWING });
|
|
123
|
+
}
|
|
124
|
+
}, [typefaceTitle, docId, client, stylesObject, weightKeywordList, italicKeywordList]);
|
|
125
|
+
|
|
126
|
+
/** Cancel processing and return to Step 1 */
|
|
127
|
+
const handleCancelProcessing = useCallback(() => {
|
|
128
|
+
cancelRef.current = true;
|
|
129
|
+
setProcessingCancelled(true);
|
|
130
|
+
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.IDLE });
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
/** Transition to execution */
|
|
134
|
+
const handleStartExecution = useCallback(() => {
|
|
135
|
+
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.EXECUTING });
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
/** Receive execution result and mark complete */
|
|
139
|
+
const handleExecutionComplete = useCallback((result) => {
|
|
140
|
+
setExecutionResult(result);
|
|
141
|
+
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.COMPLETE });
|
|
142
|
+
}, []);
|
|
143
|
+
|
|
144
|
+
if (!open) return null;
|
|
145
|
+
|
|
146
|
+
/** Navigate to a step by clicking the step indicator */
|
|
147
|
+
const handleStepClick = useCallback((stepKey) => {
|
|
148
|
+
if (isExecuting) return;
|
|
149
|
+
if (stepKey === currentStep) return;
|
|
150
|
+
// Can only go back to step 1 (reset to settings)
|
|
151
|
+
if (stepKey === 1 && currentStep > 1) {
|
|
152
|
+
if (Object.keys(plan.fonts).length > 0) {
|
|
153
|
+
if (!window.confirm('Go back to settings? Current review progress will be lost.')) return;
|
|
154
|
+
}
|
|
155
|
+
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.IDLE });
|
|
156
|
+
}
|
|
157
|
+
}, [currentStep, isExecuting, plan.fonts, dispatch]);
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<Dialog
|
|
161
|
+
id="upload-modal"
|
|
162
|
+
header={
|
|
163
|
+
<Flex direction="column" gap={3} style={{ width: '100%' }}>
|
|
164
|
+
<Text weight="semibold" size={2}>Upload Fonts</Text>
|
|
165
|
+
<Flex gap={1} style={{ width: '100%' }}>
|
|
166
|
+
{STEPS.map((step, i) => {
|
|
167
|
+
const isActive = currentStep === step.key;
|
|
168
|
+
const isCompleted = currentStep > step.key;
|
|
169
|
+
const isClickable = !isExecuting && step.key < currentStep;
|
|
170
|
+
return (
|
|
171
|
+
<Box
|
|
172
|
+
key={step.key}
|
|
173
|
+
as={isClickable ? 'button' : 'div'}
|
|
174
|
+
onClick={isClickable ? () => handleStepClick(step.key) : undefined}
|
|
175
|
+
style={{
|
|
176
|
+
flex: 1,
|
|
177
|
+
padding: '10px 12px',
|
|
178
|
+
border: 'none',
|
|
179
|
+
borderRadius: 4,
|
|
180
|
+
cursor: isClickable ? 'pointer' : 'default',
|
|
181
|
+
background: isActive
|
|
182
|
+
? 'var(--card-badge-primary-bg-color)'
|
|
183
|
+
: isCompleted
|
|
184
|
+
? 'var(--card-badge-positive-bg-color)'
|
|
185
|
+
: 'var(--card-muted-bg-color)',
|
|
186
|
+
color: isActive || isCompleted
|
|
187
|
+
? '#fff'
|
|
188
|
+
: 'var(--card-muted-fg-color)',
|
|
189
|
+
textAlign: 'center',
|
|
190
|
+
transition: 'background 0.15s ease',
|
|
191
|
+
opacity: !isActive && !isCompleted ? 0.6 : 1,
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
<Text
|
|
195
|
+
size={1}
|
|
196
|
+
weight={isActive ? 'bold' : 'medium'}
|
|
197
|
+
style={{ color: 'inherit' }}
|
|
198
|
+
>
|
|
199
|
+
{step.key}. {step.label}
|
|
200
|
+
</Text>
|
|
201
|
+
</Box>
|
|
202
|
+
);
|
|
203
|
+
})}
|
|
204
|
+
</Flex>
|
|
205
|
+
</Flex>
|
|
206
|
+
}
|
|
207
|
+
width={2}
|
|
208
|
+
onClose={isExecuting ? undefined : handleClose}
|
|
209
|
+
onClickOutside={() => {}}
|
|
210
|
+
>
|
|
211
|
+
<Box padding={4}>
|
|
212
|
+
{/* Step 1: Settings & File Selection */}
|
|
213
|
+
{currentStep === 1 && (
|
|
214
|
+
<UploadStep1Settings
|
|
215
|
+
settings={plan.settings}
|
|
216
|
+
onStartProcessing={handleStartProcessing}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Step 2: Processing & Review */}
|
|
221
|
+
{currentStep === 2 && (
|
|
222
|
+
<UploadStep2Review
|
|
223
|
+
plan={plan}
|
|
224
|
+
dispatch={dispatch}
|
|
225
|
+
onCancelProcessing={handleCancelProcessing}
|
|
226
|
+
onStartExecution={handleStartExecution}
|
|
227
|
+
processingCancelled={processingCancelled}
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{/* Step 3: Upload Execution */}
|
|
232
|
+
{currentStep === 3 && plan.phase !== PLAN_PHASE.COMPLETE && (
|
|
233
|
+
<UploadStep3Execute
|
|
234
|
+
key={retryTempIds ? 'retry' : 'initial'}
|
|
235
|
+
plan={plan}
|
|
236
|
+
client={client}
|
|
237
|
+
docId={docId}
|
|
238
|
+
stylesObject={stylesObject}
|
|
239
|
+
preferredStyleRef={preferredStyleRef}
|
|
240
|
+
retryTempIds={retryTempIds}
|
|
241
|
+
onComplete={(result) => {
|
|
242
|
+
setRetryTempIds(null);
|
|
243
|
+
handleExecutionComplete(result);
|
|
244
|
+
}}
|
|
245
|
+
/>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{/* Post-completion Summary */}
|
|
249
|
+
{plan.phase === PLAN_PHASE.COMPLETE && (
|
|
250
|
+
<UploadSummary
|
|
251
|
+
plan={plan}
|
|
252
|
+
result={executionResult}
|
|
253
|
+
onClose={handleClose}
|
|
254
|
+
onRetry={(failedTempIds) => {
|
|
255
|
+
setRetryTempIds(failedTempIds || null);
|
|
256
|
+
setExecutionResult(null);
|
|
257
|
+
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.EXECUTING });
|
|
258
|
+
}}
|
|
259
|
+
client={client}
|
|
260
|
+
docId={docId}
|
|
261
|
+
stylesObject={stylesObject}
|
|
262
|
+
preferredStyleRef={preferredStyleRef}
|
|
263
|
+
/>
|
|
264
|
+
)}
|
|
265
|
+
</Box>
|
|
266
|
+
</Dialog>
|
|
267
|
+
);
|
|
268
|
+
}
|