@liiift-studio/sanity-font-manager 2.5.0 → 2.5.2
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/dist/UploadModal-2AAJXZJK.js +6 -0
- package/dist/{UploadModal-NME2W53V.mjs → UploadModal-DDTVJ2MA.mjs} +1 -1
- package/dist/{chunk-646WCBRR.mjs → chunk-FT7YTFZW.mjs} +617 -182
- package/dist/{chunk-FH4QKHOH.js → chunk-YMQEM4AO.js} +583 -148
- package/dist/index.js +109 -66
- package/dist/index.mjs +66 -23
- package/package.json +1 -1
- package/src/components/BatchUploadFonts.jsx +1 -1
- package/src/components/FontReviewCard.jsx +41 -1
- package/src/components/KeyValueReferenceInput.jsx +69 -61
- package/src/components/SingleUploaderTool.jsx +3 -3
- package/src/components/UploadModal.jsx +43 -7
- package/src/components/UploadStep1Settings.jsx +1 -1
- package/src/components/UploadStep2Review.jsx +2 -0
- package/src/components/UploadStep3Execute.jsx +1 -1
- package/src/components/UploadStep3bInstances.jsx +396 -0
- package/src/index.js +1 -0
- package/src/schema/stylesField.js +20 -0
- package/src/utils/buildUploadPlan.js +1 -0
- package/src/utils/executeUploadPlan.js +1 -8
- package/src/utils/parseVariableFontInstances.js +237 -147
- package/src/utils/processFontFiles.js +5 -4
- package/src/utils/updateTypefaceDocument.js +15 -2
- package/dist/UploadModal-6LIX7XOK.js +0 -6
package/dist/index.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
UploadStep1Settings,
|
|
13
13
|
UploadStep2Review,
|
|
14
14
|
UploadStep3Execute,
|
|
15
|
+
UploadStep3bInstances,
|
|
15
16
|
UploadSummary,
|
|
16
17
|
addItalicToFontTitle,
|
|
17
18
|
buildUploadPlan,
|
|
@@ -59,7 +60,7 @@ import {
|
|
|
59
60
|
sanitizeForSanityId,
|
|
60
61
|
sortFontObjects,
|
|
61
62
|
updateTypefaceDocument
|
|
62
|
-
} from "./chunk-
|
|
63
|
+
} from "./chunk-FT7YTFZW.mjs";
|
|
63
64
|
|
|
64
65
|
// src/components/BatchUploadFonts.jsx
|
|
65
66
|
import React3, { useCallback, useState as useState2, useMemo as useMemo2, useRef, useEffect, lazy, Suspense } from "react";
|
|
@@ -635,7 +636,7 @@ var updateTypefaceSubfamilies = async (doc_id, stylesObject, newSubfamiliesArray
|
|
|
635
636
|
};
|
|
636
637
|
|
|
637
638
|
// src/components/BatchUploadFonts.jsx
|
|
638
|
-
var UploadModal2 = lazy(() => import("./UploadModal-
|
|
639
|
+
var UploadModal2 = lazy(() => import("./UploadModal-DDTVJ2MA.mjs"));
|
|
639
640
|
var ACCEPTED_EXTENSIONS = ["ttf", "otf", "woff", "woff2", "eot", "svg"];
|
|
640
641
|
var formatElapsed = (s) => {
|
|
641
642
|
const m = Math.floor(s / 60);
|
|
@@ -1020,7 +1021,7 @@ var BatchUploadFonts = () => {
|
|
|
1020
1021
|
style: { fontFamily: "monospace", minWidth: "2.5rem", flexShrink: 0 }
|
|
1021
1022
|
},
|
|
1022
1023
|
ext
|
|
1023
|
-
), /* @__PURE__ */ React3.createElement(Box2, { style: { flex: 1, minWidth: 0
|
|
1024
|
+
), /* @__PURE__ */ React3.createElement(Box2, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React3.createElement(Text3, { size: 1 }, file.name))), /* @__PURE__ */ React3.createElement(
|
|
1024
1025
|
Button2,
|
|
1025
1026
|
{
|
|
1026
1027
|
mode: "bleed",
|
|
@@ -1937,18 +1938,18 @@ var SingleUploaderTool = (props) => {
|
|
|
1937
1938
|
const formatUpper = format.toUpperCase();
|
|
1938
1939
|
const hasFile = !!((_b2 = (_a2 = fileInput == null ? void 0 : fileInput[format]) == null ? void 0 : _a2.asset) == null ? void 0 : _b2._ref);
|
|
1939
1940
|
const fileUrl = hasFile ? `https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${fileInput[format].asset._ref.replace("file-", "").replace("-", ".")}` : null;
|
|
1940
|
-
return /* @__PURE__ */ React6.createElement(Card3, { border: true, radius: 1, paddingX: 2, paddingY: 3 }, /* @__PURE__ */ React6.createElement(Flex4, { justify: "space-between", align: "center", gap: 2 }, /* @__PURE__ */ React6.createElement(Flex4, { gap: 3, align: "center", style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement(Text6, { size: 0, style: { fontFamily: "monospace", minWidth: "2.5rem", flexShrink: 0, opacity: hasFile ? 1 : 0.5 } }, formatUpper), hasFile ? /* @__PURE__ */ React6.createElement(Box3, { style: { flex: 1, minWidth: 0
|
|
1941
|
+
return /* @__PURE__ */ React6.createElement(Card3, { border: true, radius: 1, paddingX: 2, paddingY: 3 }, /* @__PURE__ */ React6.createElement(Flex4, { justify: "space-between", align: "center", gap: 2 }, /* @__PURE__ */ React6.createElement(Flex4, { gap: 3, align: "center", style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement(Text6, { size: 0, style: { fontFamily: "monospace", minWidth: "2.5rem", flexShrink: 0, opacity: hasFile ? 1 : 0.5 } }, formatUpper), hasFile ? /* @__PURE__ */ React6.createElement(Box3, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement("a", { href: fileUrl, target: "_blank", rel: "noreferrer" }, (filenames == null ? void 0 : filenames[format]) || "File")) : /* @__PURE__ */ React6.createElement(Text6, { size: 1, muted: true }, "\u2014")), status === "ready" && /* @__PURE__ */ React6.createElement(Flex4, { gap: 1, align: "center", style: { flexShrink: 0 } }, buildSource && (fileInput == null ? void 0 : fileInput[buildSource]) && /* @__PURE__ */ React6.createElement(Button5, { mode: "ghost", tone: "primary", fontSize: 1, padding: 2, onClick: () => handleGenerateFontFile(format, fileInput[buildSource]), text: "Build" }), /* @__PURE__ */ React6.createElement(Button5, { as: "label", mode: "ghost", tone: "primary", fontSize: 1, padding: 2, style: { cursor: "pointer" } }, /* @__PURE__ */ React6.createElement(Text6, { size: 1 }, "Upload"), /* @__PURE__ */ React6.createElement("input", { ref, type: "file", hidden: true, onChange: (e) => handleUpload(e, format) })), hasFile && /* @__PURE__ */ React6.createElement(Button5, { mode: "bleed", tone: "critical", icon: TrashIcon2, padding: 2, onClick: () => handleDelete(format) }))));
|
|
1941
1942
|
};
|
|
1942
1943
|
const renderTopLevelAssetSection = (label, fieldName, assetRef, filename, onBuild) => {
|
|
1943
1944
|
const hasFile = !!assetRef;
|
|
1944
1945
|
const fileUrl = hasFile ? `https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${assetRef.replace("file-", "").replace("-", ".")}` : null;
|
|
1945
|
-
return /* @__PURE__ */ React6.createElement(Card3, { border: true, radius: 1, paddingX: 2, paddingY: 3 }, /* @__PURE__ */ React6.createElement(Flex4, { justify: "space-between", align: "center", gap: 2 }, /* @__PURE__ */ React6.createElement(Flex4, { gap: 3, align: "center", style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement(Text6, { size: 0, style: { fontFamily: "monospace", minWidth: "2.5rem", flexShrink: 0, opacity: hasFile ? 1 : 0.5 } }, label), hasFile ? /* @__PURE__ */ React6.createElement(Box3, { style: { flex: 1, minWidth: 0
|
|
1946
|
+
return /* @__PURE__ */ React6.createElement(Card3, { border: true, radius: 1, paddingX: 2, paddingY: 3 }, /* @__PURE__ */ React6.createElement(Flex4, { justify: "space-between", align: "center", gap: 2 }, /* @__PURE__ */ React6.createElement(Flex4, { gap: 3, align: "center", style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement(Text6, { size: 0, style: { fontFamily: "monospace", minWidth: "2.5rem", flexShrink: 0, opacity: hasFile ? 1 : 0.5 } }, label), hasFile ? /* @__PURE__ */ React6.createElement(Box3, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement("a", { href: fileUrl, target: "_blank", rel: "noreferrer" }, filename || "File")) : /* @__PURE__ */ React6.createElement(Text6, { size: 1, muted: true }, "\u2014")), status === "ready" && /* @__PURE__ */ React6.createElement(Flex4, { gap: 1, align: "center", style: { flexShrink: 0 } }, onBuild && (fileInput == null ? void 0 : fileInput.woff2) && /* @__PURE__ */ React6.createElement(Button5, { mode: "ghost", tone: "primary", fontSize: 1, padding: 2, onClick: onBuild, text: "Build" }), /* @__PURE__ */ React6.createElement(Button5, { as: "label", mode: "ghost", tone: "primary", fontSize: 1, padding: 2, style: { cursor: "pointer" } }, /* @__PURE__ */ React6.createElement(Text6, { size: 1 }, "Upload"), /* @__PURE__ */ React6.createElement("input", { type: "file", hidden: true, onChange: (e) => handleUploadTopLevelFile(e, fieldName) })), hasFile && /* @__PURE__ */ React6.createElement(Button5, { mode: "bleed", tone: "critical", icon: TrashIcon2, padding: 2, onClick: () => handleDeleteTopLevel(fieldName) }))));
|
|
1946
1947
|
};
|
|
1947
1948
|
const renderCssSection = () => {
|
|
1948
1949
|
var _a2, _b2;
|
|
1949
1950
|
const hasFile = !!((_b2 = (_a2 = fileInput == null ? void 0 : fileInput.css) == null ? void 0 : _a2.asset) == null ? void 0 : _b2._ref);
|
|
1950
1951
|
const fileUrl = hasFile ? `https://cdn.sanity.io/files/${process.env.SANITY_STUDIO_PROJECT_ID}/${process.env.SANITY_STUDIO_DATASET}/${fileInput.css.asset._ref.replace("file-", "").replace("-", ".")}` : null;
|
|
1951
|
-
return /* @__PURE__ */ React6.createElement(Card3, { border: true, radius: 1, paddingX: 2, paddingY: 3 }, /* @__PURE__ */ React6.createElement(Flex4, { justify: "space-between", align: "center", gap: 2 }, /* @__PURE__ */ React6.createElement(Flex4, { gap: 3, align: "center", style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement(Text6, { size: 0, style: { fontFamily: "monospace", minWidth: "2.5rem", flexShrink: 0, opacity: hasFile ? 1 : 0.5 } }, "CSS"), hasFile ? /* @__PURE__ */ React6.createElement(Box3, { style: { flex: 1, minWidth: 0
|
|
1952
|
+
return /* @__PURE__ */ React6.createElement(Card3, { border: true, radius: 1, paddingX: 2, paddingY: 3 }, /* @__PURE__ */ React6.createElement(Flex4, { justify: "space-between", align: "center", gap: 2 }, /* @__PURE__ */ React6.createElement(Flex4, { gap: 3, align: "center", style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement(Text6, { size: 0, style: { fontFamily: "monospace", minWidth: "2.5rem", flexShrink: 0, opacity: hasFile ? 1 : 0.5 } }, "CSS"), hasFile ? /* @__PURE__ */ React6.createElement(Box3, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement("a", { href: fileUrl, target: "_blank", rel: "noreferrer" }, (filenames == null ? void 0 : filenames.css) || "File")) : /* @__PURE__ */ React6.createElement(Text6, { size: 1, muted: true }, "\u2014")), status === "ready" && /* @__PURE__ */ React6.createElement(Flex4, { gap: 1, align: "center", style: { flexShrink: 0 } }, (fileInput == null ? void 0 : fileInput.woff2) && /* @__PURE__ */ React6.createElement(Button5, { mode: "ghost", tone: "primary", fontSize: 1, padding: 2, onClick: () => handleGenerateCssFile(), text: "Build" }), hasFile && /* @__PURE__ */ React6.createElement(Button5, { mode: "bleed", tone: "critical", icon: TrashIcon2, padding: 2, onClick: () => handleDelete("css") }))));
|
|
1952
1953
|
};
|
|
1953
1954
|
const renderDataSection = () => /* @__PURE__ */ React6.createElement(Card3, { border: true, radius: 1, paddingX: 2, paddingY: 3 }, /* @__PURE__ */ React6.createElement(Flex4, { justify: "space-between", align: "center", gap: 2 }, /* @__PURE__ */ React6.createElement(Flex4, { gap: 3, align: "center", style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement(Text6, { size: 0, style: { fontFamily: "monospace", minWidth: "2.5rem", flexShrink: 0, opacity: (doc_metaData == null ? void 0 : doc_metaData.version) ? 1 : 0.5 } }, "DATA"), (doc_metaData == null ? void 0 : doc_metaData.version) ? /* @__PURE__ */ React6.createElement(Text6, { size: 1 }, "v", doc_metaData.version, " ", /* @__PURE__ */ React6.createElement(Text6, { as: "span", size: 1, muted: true }, "(", doc_metaData.genDate, ")")) : /* @__PURE__ */ React6.createElement(Text6, { size: 1, muted: true }, "\u2014")), status === "ready" && (fileInput == null ? void 0 : fileInput.ttf) && /* @__PURE__ */ React6.createElement(Flex4, { gap: 1, align: "center", style: { flexShrink: 0 } }, /* @__PURE__ */ React6.createElement(Button5, { mode: "ghost", tone: "primary", fontSize: 1, padding: 2, onClick: () => handleGenerateFontData(), text: "Build" }))));
|
|
1954
1955
|
return /* @__PURE__ */ React6.createElement(Stack5, { space: 2 }, /* @__PURE__ */ React6.createElement(
|
|
@@ -2810,21 +2811,36 @@ function KeyValueReferenceInput(props) {
|
|
|
2810
2811
|
const pickerLabel = referenceType || valueTitle.toLowerCase();
|
|
2811
2812
|
return /* @__PURE__ */ React12.createElement(Stack9, { space: 3 }, topActions && /* @__PURE__ */ React12.createElement(Box4, { paddingBottom: 2 }, topActions), /* @__PURE__ */ React12.createElement(Box4, null, /* @__PURE__ */ React12.createElement(Stack9, { space: 2 }, pairs.map((pair, index) => {
|
|
2812
2813
|
var _a2;
|
|
2813
|
-
return /* @__PURE__ */ React12.createElement(
|
|
2814
|
+
return /* @__PURE__ */ React12.createElement(Flex7, { key: index, gap: 1, align: "center" }, /* @__PURE__ */ React12.createElement(Flex7, { direction: "column", style: { flexShrink: 0 } }, /* @__PURE__ */ React12.createElement(
|
|
2815
|
+
Button10,
|
|
2816
|
+
{
|
|
2817
|
+
mode: "bleed",
|
|
2818
|
+
icon: ArrowUpIcon2,
|
|
2819
|
+
padding: 1,
|
|
2820
|
+
fontSize: 0,
|
|
2821
|
+
onClick: () => handleMoveUp(index),
|
|
2822
|
+
disabled: index === 0,
|
|
2823
|
+
style: { cursor: index === 0 ? "default" : "pointer" }
|
|
2824
|
+
}
|
|
2825
|
+
), /* @__PURE__ */ React12.createElement(
|
|
2826
|
+
Button10,
|
|
2827
|
+
{
|
|
2828
|
+
mode: "bleed",
|
|
2829
|
+
icon: ArrowDownIcon2,
|
|
2830
|
+
padding: 1,
|
|
2831
|
+
fontSize: 0,
|
|
2832
|
+
onClick: () => handleMoveDown(index),
|
|
2833
|
+
disabled: index === pairs.length - 1,
|
|
2834
|
+
style: { cursor: index === pairs.length - 1 ? "default" : "pointer" }
|
|
2835
|
+
}
|
|
2836
|
+
)), /* @__PURE__ */ React12.createElement(Box4, { flex: 1 }, /* @__PURE__ */ React12.createElement(
|
|
2814
2837
|
TextInput3,
|
|
2815
2838
|
{
|
|
2816
2839
|
value: pair.key,
|
|
2817
2840
|
onChange: (e) => handlePairChange(index, "key", e.target.value),
|
|
2818
2841
|
placeholder: keyPlaceholder
|
|
2819
2842
|
}
|
|
2820
|
-
)), /* @__PURE__ */ React12.createElement(Box4, { flex: 1
|
|
2821
|
-
Text10,
|
|
2822
|
-
{
|
|
2823
|
-
size: 2,
|
|
2824
|
-
style: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: "90%" }
|
|
2825
|
-
},
|
|
2826
|
-
referenceData[pair.value._ref] || "Loading..."
|
|
2827
|
-
), /* @__PURE__ */ React12.createElement(
|
|
2843
|
+
)), /* @__PURE__ */ React12.createElement(Box4, { flex: 1 }, ((_a2 = pair.value) == null ? void 0 : _a2._ref) ? /* @__PURE__ */ React12.createElement(Card4, { radius: 2, tone: "primary", style: { paddingLeft: "0.75rem", height: "fit-content" } }, /* @__PURE__ */ React12.createElement(Flex7, { align: "center", justify: "space-between" }, /* @__PURE__ */ React12.createElement(Text10, { size: 2, style: { whiteSpace: "nowrap" } }, referenceData[pair.value._ref] || "Loading..."), /* @__PURE__ */ React12.createElement(
|
|
2828
2844
|
MenuButton2,
|
|
2829
2845
|
{
|
|
2830
2846
|
button: /* @__PURE__ */ React12.createElement(Button10, { icon: EllipsisHorizontalIcon, mode: "bleed", title: "Options" }),
|
|
@@ -2836,18 +2852,20 @@ function KeyValueReferenceInput(props) {
|
|
|
2836
2852
|
Box4,
|
|
2837
2853
|
{
|
|
2838
2854
|
padding: 2,
|
|
2839
|
-
style: {
|
|
2855
|
+
style: { border: "1px dashed #ccc", borderRadius: "4px", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer" },
|
|
2840
2856
|
onClick: () => openReferenceSelector(index)
|
|
2841
2857
|
},
|
|
2842
2858
|
/* @__PURE__ */ React12.createElement(Text10, { muted: true, size: 2 }, "Click to select a ", pickerLabel)
|
|
2843
|
-
))
|
|
2844
|
-
|
|
2859
|
+
)), /* @__PURE__ */ React12.createElement(
|
|
2860
|
+
Button10,
|
|
2845
2861
|
{
|
|
2846
|
-
|
|
2862
|
+
mode: "bleed",
|
|
2863
|
+
tone: "critical",
|
|
2864
|
+
icon: TrashIcon4,
|
|
2865
|
+
padding: 2,
|
|
2847
2866
|
onClick: () => handleRemovePair(index),
|
|
2848
|
-
style: {
|
|
2849
|
-
}
|
|
2850
|
-
/* @__PURE__ */ React12.createElement(TrashIcon4, null)
|
|
2867
|
+
style: { flexShrink: 0, cursor: "pointer" }
|
|
2868
|
+
}
|
|
2851
2869
|
));
|
|
2852
2870
|
}))), /* @__PURE__ */ React12.createElement(Button10, { tone: "primary", mode: "ghost", onClick: handleAddPair, icon: AddIcon2, text: `Add ${keyTitle}` }), isDialogOpen && /* @__PURE__ */ React12.createElement(
|
|
2853
2871
|
Dialog,
|
|
@@ -5907,7 +5925,9 @@ function createStylesField({
|
|
|
5907
5925
|
subfamilyPreferredStyle = false,
|
|
5908
5926
|
subfamilyFontFilter = false,
|
|
5909
5927
|
subfamilyPreview = false,
|
|
5910
|
-
pairs = true
|
|
5928
|
+
pairs = true,
|
|
5929
|
+
generateCollections = false,
|
|
5930
|
+
generateFullFamilyCollection = false
|
|
5911
5931
|
} = {}) {
|
|
5912
5932
|
const subfamilyFields = [
|
|
5913
5933
|
{
|
|
@@ -6051,6 +6071,28 @@ function createStylesField({
|
|
|
6051
6071
|
type: "array",
|
|
6052
6072
|
of: [subfamilyItem]
|
|
6053
6073
|
},
|
|
6074
|
+
...field(generateCollections, {
|
|
6075
|
+
title: "Generate Collections and Pairs",
|
|
6076
|
+
name: "generateCollections",
|
|
6077
|
+
type: "string",
|
|
6078
|
+
description: "Generate Collections and Pairs from the typeface's fonts.",
|
|
6079
|
+
components: { input: GenerateCollectionsPairsComponent },
|
|
6080
|
+
hidden: ({ parent }) => {
|
|
6081
|
+
var _a;
|
|
6082
|
+
return !((_a = parent == null ? void 0 : parent.fonts) == null ? void 0 : _a.length);
|
|
6083
|
+
}
|
|
6084
|
+
}),
|
|
6085
|
+
...field(generateFullFamilyCollection, {
|
|
6086
|
+
title: "Generate Full Family Collection",
|
|
6087
|
+
name: "generateCollectionGroup",
|
|
6088
|
+
type: "string",
|
|
6089
|
+
description: "Generate a Collection that includes all styles from this typeface.",
|
|
6090
|
+
components: { input: PrimaryCollectionGeneratorTypeface },
|
|
6091
|
+
hidden: ({ parent }) => {
|
|
6092
|
+
var _a;
|
|
6093
|
+
return !((_a = parent == null ? void 0 : parent.fonts) == null ? void 0 : _a.length);
|
|
6094
|
+
}
|
|
6095
|
+
}),
|
|
6054
6096
|
{
|
|
6055
6097
|
title: "Collections",
|
|
6056
6098
|
name: "collections",
|
|
@@ -6114,6 +6156,7 @@ export {
|
|
|
6114
6156
|
UploadStep1Settings,
|
|
6115
6157
|
UploadStep2Review,
|
|
6116
6158
|
UploadStep3Execute,
|
|
6159
|
+
UploadStep3bInstances,
|
|
6117
6160
|
UploadSummary,
|
|
6118
6161
|
VariableInstanceReferencesInput,
|
|
6119
6162
|
addItalicToFontTitle,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liiift-studio/sanity-font-manager",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.2",
|
|
4
4
|
"description": "Sanity Studio plugin — full font management suite with batch upload, format conversion, metadata extraction, CSS generation, collection/pair generation, and script variant support. Supports Sanity v3, v4, and v5.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Liiift Studio",
|
|
@@ -459,7 +459,7 @@ export const BatchUploadFonts = () => {
|
|
|
459
459
|
>
|
|
460
460
|
{ext}
|
|
461
461
|
</Text>
|
|
462
|
-
<Box style={{ flex: 1, minWidth: 0
|
|
462
|
+
<Box style={{ flex: 1, minWidth: 0 }}>
|
|
463
463
|
<Text size={1}>{file.name}</Text>
|
|
464
464
|
</Box>
|
|
465
465
|
</Flex>
|
|
@@ -15,9 +15,10 @@ const EXTENDED_TYPES = ['eot', 'svg', 'css', 'woff2_subset', 'woff2_web'];
|
|
|
15
15
|
* Collapsible review card for a single font in the upload plan.
|
|
16
16
|
* Table-style header row with weight/style/files/action columns.
|
|
17
17
|
*/
|
|
18
|
-
const FontReviewCard = memo(function FontReviewCard({ entry, dispatch, allExpanded }) {
|
|
18
|
+
const FontReviewCard = memo(function FontReviewCard({ entry, dispatch, allExpanded, typefaceTitle, price }) {
|
|
19
19
|
const [expanded, setExpanded] = useState(false);
|
|
20
20
|
const [showAllFileTypes, setShowAllFileTypes] = useState(false);
|
|
21
|
+
const [showDocPreview, setShowDocPreview] = useState(false);
|
|
21
22
|
|
|
22
23
|
// Sync with allExpanded toggle from BulkActions
|
|
23
24
|
useEffect(() => {
|
|
@@ -382,6 +383,45 @@ const FontReviewCard = memo(function FontReviewCard({ entry, dispatch, allExpand
|
|
|
382
383
|
dispatch={dispatch}
|
|
383
384
|
/>
|
|
384
385
|
|
|
386
|
+
{/* Document Preview — expandable view of all fields that will be written */}
|
|
387
|
+
<Stack space={2}>
|
|
388
|
+
<Button
|
|
389
|
+
mode="bleed"
|
|
390
|
+
fontSize={0}
|
|
391
|
+
padding={1}
|
|
392
|
+
text={showDocPreview ? 'Hide document preview' : 'Show document preview'}
|
|
393
|
+
onClick={() => setShowDocPreview(v => !v)}
|
|
394
|
+
style={{ cursor: 'pointer', alignSelf: 'flex-start' }}
|
|
395
|
+
/>
|
|
396
|
+
{showDocPreview && (
|
|
397
|
+
<Card border padding={3} radius={1} style={{ fontFamily: 'monospace', fontSize: 12 }}>
|
|
398
|
+
<Stack space={2}>
|
|
399
|
+
{[
|
|
400
|
+
['_id', entry.documentId],
|
|
401
|
+
['_type', 'font'],
|
|
402
|
+
['title', entry.title],
|
|
403
|
+
['slug', entry.documentId],
|
|
404
|
+
['typefaceName', typefaceTitle || '—'],
|
|
405
|
+
['weightName', entry.weightName || '—'],
|
|
406
|
+
['weight', entry.weight],
|
|
407
|
+
['style', entry.style],
|
|
408
|
+
['subfamily', entry.subfamily || '—'],
|
|
409
|
+
['variableFont', String(entry.variableFont)],
|
|
410
|
+
['price', price ?? '—'],
|
|
411
|
+
['sell', price > 0 ? 'true' : 'false'],
|
|
412
|
+
['normalWeight', 'true'],
|
|
413
|
+
['files', (entry.files || []).map(f => f.name).join(', ') || '—'],
|
|
414
|
+
].map(([key, value]) => (
|
|
415
|
+
<Flex key={key} gap={2}>
|
|
416
|
+
<Text size={0} muted style={{ width: 120, flexShrink: 0 }}>{key}</Text>
|
|
417
|
+
<Text size={0} style={{ wordBreak: 'break-all' }}>{String(value)}</Text>
|
|
418
|
+
</Flex>
|
|
419
|
+
))}
|
|
420
|
+
</Stack>
|
|
421
|
+
</Card>
|
|
422
|
+
)}
|
|
423
|
+
</Stack>
|
|
424
|
+
|
|
385
425
|
{/* Actions — only show reset if user has overridden suggestions */}
|
|
386
426
|
<Flex justify="flex-end" gap={2}>
|
|
387
427
|
{hasUserOverrides && (
|
|
@@ -148,72 +148,80 @@ export function KeyValueReferenceInput(props) {
|
|
|
148
148
|
<Box>
|
|
149
149
|
<Stack space={2}>
|
|
150
150
|
{pairs.map((pair, index) => (
|
|
151
|
-
<
|
|
151
|
+
<Flex key={index} gap={1} align="center">
|
|
152
152
|
{/* Reorder buttons */}
|
|
153
|
-
<
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
{/* Reference display or empty-state picker trigger */}
|
|
173
|
-
<Box flex={1} style={{ minHeight: '100%' }}>
|
|
174
|
-
{pair.value?._ref ? (
|
|
175
|
-
<Card className="referenceCard" radius={2} tone="primary" style={{ paddingLeft: '1rem', height: 'fit-content' }}>
|
|
176
|
-
<Flex align="center" justify="space-between">
|
|
177
|
-
<Text
|
|
178
|
-
size={2}
|
|
179
|
-
style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '90%' }}
|
|
180
|
-
>
|
|
181
|
-
{referenceData[pair.value._ref] || 'Loading...'}
|
|
182
|
-
</Text>
|
|
183
|
-
<MenuButton
|
|
184
|
-
button={<Button icon={EllipsisHorizontalIcon} mode="bleed" title="Options" />}
|
|
185
|
-
id={`ref-options-${index}`}
|
|
186
|
-
menu={
|
|
187
|
-
<Menu>
|
|
188
|
-
<MenuItem tone="critical" icon={TrashIcon} text="Remove" onClick={() => handlePairChange(index, 'value', null)} />
|
|
189
|
-
<MenuItem icon={SyncIcon} text="Replace" onClick={() => openReferenceSelector(index)} />
|
|
190
|
-
</Menu>
|
|
191
|
-
}
|
|
192
|
-
popover={{ portal: true, tone: 'default', placement: 'left' }}
|
|
193
|
-
/>
|
|
194
|
-
</Flex>
|
|
195
|
-
</Card>
|
|
196
|
-
) : (
|
|
197
|
-
<Box
|
|
198
|
-
padding={2}
|
|
199
|
-
style={{ minHeight: '100%', border: '1px dashed #ccc', borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
|
|
200
|
-
onClick={() => openReferenceSelector(index)}
|
|
201
|
-
>
|
|
202
|
-
<Text muted size={2}>Click to select a {pickerLabel}</Text>
|
|
203
|
-
</Box>
|
|
204
|
-
)}
|
|
205
|
-
</Box>
|
|
153
|
+
<Flex direction="column" style={{ flexShrink: 0 }}>
|
|
154
|
+
<Button
|
|
155
|
+
mode="bleed"
|
|
156
|
+
icon={ArrowUpIcon}
|
|
157
|
+
padding={1}
|
|
158
|
+
fontSize={0}
|
|
159
|
+
onClick={() => handleMoveUp(index)}
|
|
160
|
+
disabled={index === 0}
|
|
161
|
+
style={{ cursor: index === 0 ? 'default' : 'pointer' }}
|
|
162
|
+
/>
|
|
163
|
+
<Button
|
|
164
|
+
mode="bleed"
|
|
165
|
+
icon={ArrowDownIcon}
|
|
166
|
+
padding={1}
|
|
167
|
+
fontSize={0}
|
|
168
|
+
onClick={() => handleMoveDown(index)}
|
|
169
|
+
disabled={index === pairs.length - 1}
|
|
170
|
+
style={{ cursor: index === pairs.length - 1 ? 'default' : 'pointer' }}
|
|
171
|
+
/>
|
|
206
172
|
</Flex>
|
|
207
173
|
|
|
174
|
+
{/* Key input */}
|
|
175
|
+
<Box flex={1}>
|
|
176
|
+
<TextInput
|
|
177
|
+
value={pair.key}
|
|
178
|
+
onChange={(e) => handlePairChange(index, 'key', e.target.value)}
|
|
179
|
+
placeholder={keyPlaceholder}
|
|
180
|
+
/>
|
|
181
|
+
</Box>
|
|
182
|
+
|
|
183
|
+
{/* Reference display or empty-state picker trigger */}
|
|
184
|
+
<Box flex={1}>
|
|
185
|
+
{pair.value?._ref ? (
|
|
186
|
+
<Card radius={2} tone="primary" style={{ paddingLeft: '0.75rem', height: 'fit-content' }}>
|
|
187
|
+
<Flex align="center" justify="space-between">
|
|
188
|
+
<Text size={2} style={{ whiteSpace: 'nowrap' }}>
|
|
189
|
+
{referenceData[pair.value._ref] || 'Loading...'}
|
|
190
|
+
</Text>
|
|
191
|
+
<MenuButton
|
|
192
|
+
button={<Button icon={EllipsisHorizontalIcon} mode="bleed" title="Options" />}
|
|
193
|
+
id={`ref-options-${index}`}
|
|
194
|
+
menu={
|
|
195
|
+
<Menu>
|
|
196
|
+
<MenuItem tone="critical" icon={TrashIcon} text="Remove" onClick={() => handlePairChange(index, 'value', null)} />
|
|
197
|
+
<MenuItem icon={SyncIcon} text="Replace" onClick={() => openReferenceSelector(index)} />
|
|
198
|
+
</Menu>
|
|
199
|
+
}
|
|
200
|
+
popover={{ portal: true, tone: 'default', placement: 'left' }}
|
|
201
|
+
/>
|
|
202
|
+
</Flex>
|
|
203
|
+
</Card>
|
|
204
|
+
) : (
|
|
205
|
+
<Box
|
|
206
|
+
padding={2}
|
|
207
|
+
style={{ border: '1px dashed #ccc', borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
|
|
208
|
+
onClick={() => openReferenceSelector(index)}
|
|
209
|
+
>
|
|
210
|
+
<Text muted size={2}>Click to select a {pickerLabel}</Text>
|
|
211
|
+
</Box>
|
|
212
|
+
)}
|
|
213
|
+
</Box>
|
|
214
|
+
|
|
208
215
|
{/* Remove button */}
|
|
209
|
-
<
|
|
210
|
-
|
|
216
|
+
<Button
|
|
217
|
+
mode="bleed"
|
|
218
|
+
tone="critical"
|
|
219
|
+
icon={TrashIcon}
|
|
220
|
+
padding={2}
|
|
211
221
|
onClick={() => handleRemovePair(index)}
|
|
212
|
-
style={{
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
</button>
|
|
216
|
-
</Box>
|
|
222
|
+
style={{ flexShrink: 0, cursor: 'pointer' }}
|
|
223
|
+
/>
|
|
224
|
+
</Flex>
|
|
217
225
|
))}
|
|
218
226
|
</Stack>
|
|
219
227
|
</Box>
|
|
@@ -479,7 +479,7 @@ export const SingleUploaderTool = (props) => {
|
|
|
479
479
|
{formatUpper}
|
|
480
480
|
</Text>
|
|
481
481
|
{hasFile ? (
|
|
482
|
-
<Box style={{ flex: 1, minWidth: 0
|
|
482
|
+
<Box style={{ flex: 1, minWidth: 0 }}>
|
|
483
483
|
<a href={fileUrl} target="_blank" rel="noreferrer">{filenames?.[format] || 'File'}</a>
|
|
484
484
|
</Box>
|
|
485
485
|
) : (
|
|
@@ -520,7 +520,7 @@ export const SingleUploaderTool = (props) => {
|
|
|
520
520
|
{label}
|
|
521
521
|
</Text>
|
|
522
522
|
{hasFile ? (
|
|
523
|
-
<Box style={{ flex: 1, minWidth: 0
|
|
523
|
+
<Box style={{ flex: 1, minWidth: 0 }}>
|
|
524
524
|
<a href={fileUrl} target="_blank" rel="noreferrer">{filename || 'File'}</a>
|
|
525
525
|
</Box>
|
|
526
526
|
) : (
|
|
@@ -561,7 +561,7 @@ export const SingleUploaderTool = (props) => {
|
|
|
561
561
|
CSS
|
|
562
562
|
</Text>
|
|
563
563
|
{hasFile ? (
|
|
564
|
-
<Box style={{ flex: 1, minWidth: 0
|
|
564
|
+
<Box style={{ flex: 1, minWidth: 0 }}>
|
|
565
565
|
<a href={fileUrl} target="_blank" rel="noreferrer">{filenames?.css || 'File'}</a>
|
|
566
566
|
</Box>
|
|
567
567
|
) : (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Upload modal —
|
|
1
|
+
// Upload modal — multi-step state machine: Upload Files → Review → Execute → Map Instances → Summary
|
|
2
2
|
|
|
3
3
|
import React, { useReducer, useCallback, useState, useMemo, useRef, useEffect } from 'react';
|
|
4
4
|
import { Dialog, Box, Flex, Text, Badge, Button } from '@sanity/ui';
|
|
@@ -9,6 +9,7 @@ import { generateStyleKeywords } from '../utils/generateKeywords';
|
|
|
9
9
|
import UploadStep1Settings from './UploadStep1Settings';
|
|
10
10
|
import UploadStep2Review from './UploadStep2Review';
|
|
11
11
|
import UploadStep3Execute from './UploadStep3Execute';
|
|
12
|
+
import UploadStep3bInstances from './UploadStep3bInstances';
|
|
12
13
|
import UploadSummary from './UploadSummary';
|
|
13
14
|
|
|
14
15
|
/** Step labels for the step indicator */
|
|
@@ -16,6 +17,7 @@ const STEPS = [
|
|
|
16
17
|
{ key: 1, label: 'Upload Files' },
|
|
17
18
|
{ key: 2, label: 'Review' },
|
|
18
19
|
{ key: 3, label: 'Upload' },
|
|
20
|
+
{ key: 4, label: 'Map Instances' },
|
|
19
21
|
];
|
|
20
22
|
|
|
21
23
|
/** Maps plan phase to active step number */
|
|
@@ -49,11 +51,19 @@ export default function UploadModal({
|
|
|
49
51
|
const [processingCancelled, setProcessingCancelled] = useState(false);
|
|
50
52
|
const [executionResult, setExecutionResult] = useState(null);
|
|
51
53
|
const [retryTempIds, setRetryTempIds] = useState(null);
|
|
54
|
+
const [instanceMappingPhase, setInstanceMappingPhase] = useState(false);
|
|
55
|
+
const [instanceMappingResult, setInstanceMappingResult] = useState(null);
|
|
52
56
|
const cancelRef = useRef(false);
|
|
53
57
|
const focusRef = useRef(null);
|
|
54
58
|
|
|
55
59
|
const { weightKeywordList, italicKeywordList } = useMemo(() => generateStyleKeywords(), []);
|
|
56
|
-
const
|
|
60
|
+
const hasVFs = useMemo(() =>
|
|
61
|
+
Object.values(plan.fonts).some(f => f.variableFont && f.status !== 'error'),
|
|
62
|
+
[plan.fonts]
|
|
63
|
+
);
|
|
64
|
+
const baseStep = phaseToStep(plan.phase);
|
|
65
|
+
// Instance mapping is step 4 — only shown after execution completes with VFs
|
|
66
|
+
const currentStep = instanceMappingPhase ? 4 : (plan.phase === PLAN_PHASE.COMPLETE && !instanceMappingResult ? baseStep : baseStep);
|
|
57
67
|
const isExecuting = plan.phase === PLAN_PHASE.EXECUTING;
|
|
58
68
|
|
|
59
69
|
// Prevent accidental close during upload
|
|
@@ -85,7 +95,7 @@ export default function UploadModal({
|
|
|
85
95
|
|
|
86
96
|
/** Start processing — transition to Step 2 and build the plan */
|
|
87
97
|
const handleStartProcessing = useCallback(async (files, settings) => {
|
|
88
|
-
dispatch({ type: 'SET_SETTINGS', settings });
|
|
98
|
+
dispatch({ type: 'SET_SETTINGS', settings: { ...settings, typefaceTitle } });
|
|
89
99
|
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.PROCESSING, totalFiles: files.length });
|
|
90
100
|
cancelRef.current = false;
|
|
91
101
|
setProcessingCancelled(false);
|
|
@@ -135,10 +145,22 @@ export default function UploadModal({
|
|
|
135
145
|
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.EXECUTING });
|
|
136
146
|
}, []);
|
|
137
147
|
|
|
138
|
-
/** Receive execution result
|
|
148
|
+
/** Receive execution result — transition to instance mapping if VFs exist, otherwise complete */
|
|
139
149
|
const handleExecutionComplete = useCallback((result) => {
|
|
140
150
|
setExecutionResult(result);
|
|
141
|
-
|
|
151
|
+
if (hasVFs && result.success !== false) {
|
|
152
|
+
// Show instance mapping step before summary
|
|
153
|
+
setInstanceMappingPhase(true);
|
|
154
|
+
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.COMPLETE });
|
|
155
|
+
} else {
|
|
156
|
+
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.COMPLETE });
|
|
157
|
+
}
|
|
158
|
+
}, [hasVFs]);
|
|
159
|
+
|
|
160
|
+
/** Handle instance mapping completion */
|
|
161
|
+
const handleInstanceMappingComplete = useCallback((result) => {
|
|
162
|
+
setInstanceMappingResult(result);
|
|
163
|
+
setInstanceMappingPhase(false);
|
|
142
164
|
}, []);
|
|
143
165
|
|
|
144
166
|
if (!open) return null;
|
|
@@ -163,7 +185,7 @@ export default function UploadModal({
|
|
|
163
185
|
<Flex direction="column" gap={3} style={{ width: '100%' }}>
|
|
164
186
|
<Text weight="semibold" size={2}>Upload Fonts</Text>
|
|
165
187
|
<Flex gap={1} style={{ width: '100%' }}>
|
|
166
|
-
{STEPS.map((step, i) => {
|
|
188
|
+
{STEPS.filter(step => step.key !== 4 || hasVFs).map((step, i) => {
|
|
167
189
|
const isActive = currentStep === step.key;
|
|
168
190
|
const isCompleted = currentStep > step.key;
|
|
169
191
|
const isClickable = !isExecuting && step.key < currentStep;
|
|
@@ -245,15 +267,29 @@ export default function UploadModal({
|
|
|
245
267
|
/>
|
|
246
268
|
)}
|
|
247
269
|
|
|
270
|
+
{/* Step 4: Variable Font Instance Mapping (only if VFs in batch) */}
|
|
271
|
+
{plan.phase === PLAN_PHASE.COMPLETE && instanceMappingPhase && (
|
|
272
|
+
<UploadStep3bInstances
|
|
273
|
+
plan={plan}
|
|
274
|
+
executionResult={executionResult}
|
|
275
|
+
client={client}
|
|
276
|
+
typefaceTitle={typefaceTitle}
|
|
277
|
+
onComplete={handleInstanceMappingComplete}
|
|
278
|
+
/>
|
|
279
|
+
)}
|
|
280
|
+
|
|
248
281
|
{/* Post-completion Summary */}
|
|
249
|
-
{plan.phase === PLAN_PHASE.COMPLETE && (
|
|
282
|
+
{plan.phase === PLAN_PHASE.COMPLETE && !instanceMappingPhase && (
|
|
250
283
|
<UploadSummary
|
|
251
284
|
plan={plan}
|
|
252
285
|
result={executionResult}
|
|
286
|
+
instanceMappingResult={instanceMappingResult}
|
|
253
287
|
onClose={handleClose}
|
|
254
288
|
onRetry={(failedTempIds) => {
|
|
255
289
|
setRetryTempIds(failedTempIds || null);
|
|
256
290
|
setExecutionResult(null);
|
|
291
|
+
setInstanceMappingPhase(false);
|
|
292
|
+
setInstanceMappingResult(null);
|
|
257
293
|
dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.EXECUTING });
|
|
258
294
|
}}
|
|
259
295
|
client={client}
|
|
@@ -235,7 +235,7 @@ export default function UploadStep1Settings({ settings, onStartProcessing }) {
|
|
|
235
235
|
>
|
|
236
236
|
{ext.toUpperCase()}
|
|
237
237
|
</Badge>
|
|
238
|
-
<Text size={1} style={{ flex: 1
|
|
238
|
+
<Text size={1} style={{ flex: 1 }}>
|
|
239
239
|
{file.name}
|
|
240
240
|
</Text>
|
|
241
241
|
<Button
|
|
@@ -181,7 +181,7 @@ export default function UploadStep3Execute({
|
|
|
181
181
|
return (
|
|
182
182
|
<Card key={entry.tempId} border radius={1} padding={2}>
|
|
183
183
|
<Flex align="center" gap={2}>
|
|
184
|
-
<Text size={1} style={{
|
|
184
|
+
<Text size={1} style={{ flex: 1 }}>
|
|
185
185
|
{entry.title}
|
|
186
186
|
</Text>
|
|
187
187
|
<Box style={{ width: 120, flexShrink: 0, textAlign: 'right' }}>
|