@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/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-646WCBRR.mjs";
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-NME2W53V.mjs"));
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, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, /* @__PURE__ */ React3.createElement(Text3, { size: 1 }, file.name))), /* @__PURE__ */ React3.createElement(
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, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, /* @__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
+ 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, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, /* @__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
+ 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, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, /* @__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
+ 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(Box4, { key: index, style: { position: "relative" } }, /* @__PURE__ */ React12.createElement("div", { style: { position: "absolute", height: "100%", top: "0", left: "-5px", width: "min-content", transform: "translate(-100%, 0%)" } }, /* @__PURE__ */ React12.createElement("button", { className: "manualButton manualButtonUp", style: { fontSize: "15px", height: "50%" }, onClick: () => handleMoveUp(index) }, /* @__PURE__ */ React12.createElement(ArrowUpIcon2, null)), /* @__PURE__ */ React12.createElement("button", { className: "manualButton manualButtonDown", style: { fontSize: "15px", height: "50%" }, onClick: () => handleMoveDown(index) }, /* @__PURE__ */ React12.createElement(ArrowDownIcon2, null))), /* @__PURE__ */ React12.createElement(Flex7, { gap: 2, align: "flex-start" }, /* @__PURE__ */ React12.createElement(Box4, { flex: 1 }, /* @__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, style: { minHeight: "100%" } }, ((_a2 = pair.value) == null ? void 0 : _a2._ref) ? /* @__PURE__ */ React12.createElement(Card4, { className: "referenceCard", radius: 2, tone: "primary", style: { paddingLeft: "1rem", height: "fit-content" } }, /* @__PURE__ */ React12.createElement(Flex7, { align: "center", justify: "space-between" }, /* @__PURE__ */ React12.createElement(
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: { minHeight: "100%", border: "1px dashed #ccc", borderRadius: "4px", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer" },
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
- ))), /* @__PURE__ */ React12.createElement(
2844
- "button",
2859
+ )), /* @__PURE__ */ React12.createElement(
2860
+ Button10,
2845
2861
  {
2846
- className: "manualButton",
2862
+ mode: "bleed",
2863
+ tone: "critical",
2864
+ icon: TrashIcon4,
2865
+ padding: 2,
2847
2866
  onClick: () => handleRemovePair(index),
2848
- style: { position: "absolute", top: "0", right: "-7px", transform: "translate(100%, 0%)" }
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.0",
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, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
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
- <Box key={index} style={{ position: 'relative' }}>
151
+ <Flex key={index} gap={1} align="center">
152
152
  {/* Reorder buttons */}
153
- <div style={{ position: 'absolute', height: '100%', top: '0', left: '-5px', width: 'min-content', transform: 'translate(-100%, 0%)' }}>
154
- <button className="manualButton manualButtonUp" style={{ fontSize: '15px', height: '50%' }} onClick={() => handleMoveUp(index)}>
155
- <ArrowUpIcon />
156
- </button>
157
- <button className="manualButton manualButtonDown" style={{ fontSize: '15px', height: '50%' }} onClick={() => handleMoveDown(index)}>
158
- <ArrowDownIcon />
159
- </button>
160
- </div>
161
-
162
- <Flex gap={2} align="flex-start">
163
- {/* Key input */}
164
- <Box flex={1}>
165
- <TextInput
166
- value={pair.key}
167
- onChange={(e) => handlePairChange(index, 'key', e.target.value)}
168
- placeholder={keyPlaceholder}
169
- />
170
- </Box>
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
- <button
210
- className="manualButton"
216
+ <Button
217
+ mode="bleed"
218
+ tone="critical"
219
+ icon={TrashIcon}
220
+ padding={2}
211
221
  onClick={() => handleRemovePair(index)}
212
- style={{ position: 'absolute', top: '0', right: '-7px', transform: 'translate(100%, 0%)' }}
213
- >
214
- <TrashIcon />
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, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
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, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
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, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
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 — 3-step state machine: Settings → Review → Execute
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 currentStep = phaseToStep(plan.phase);
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 and mark complete */
148
+ /** Receive execution result transition to instance mapping if VFs exist, otherwise complete */
139
149
  const handleExecutionComplete = useCallback((result) => {
140
150
  setExecutionResult(result);
141
- dispatch({ type: 'SET_PHASE', phase: PLAN_PHASE.COMPLETE });
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, textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
238
+ <Text size={1} style={{ flex: 1 }}>
239
239
  {file.name}
240
240
  </Text>
241
241
  <Button
@@ -431,6 +431,8 @@ export default function UploadStep2Review({
431
431
  entry={entry}
432
432
  dispatch={dispatch}
433
433
  allExpanded={allExpanded}
434
+ typefaceTitle={plan.settings?.typefaceTitle}
435
+ price={plan.settings?.price}
434
436
  />
435
437
  ))}
436
438
  </Stack>
@@ -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={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 }}>
184
+ <Text size={1} style={{ flex: 1 }}>
185
185
  {entry.title}
186
186
  </Text>
187
187
  <Box style={{ width: 120, flexShrink: 0, textAlign: 'right' }}>