@liiift-studio/sanity-font-manager 2.5.9 → 2.7.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/dist/index.js CHANGED
@@ -12536,7 +12536,18 @@ function getCharacterSet(font2) {
12536
12536
  const cmap2 = (_b = (_a = font2.opentype) == null ? void 0 : _a.tables) == null ? void 0 : _b.cmap;
12537
12537
  if (!cmap2) return [];
12538
12538
  try {
12539
- return cmap2.getSupportedCharCodes(3, 1);
12539
+ const raw = cmap2.getSupportedCharCodes(3, 1);
12540
+ if (!Array.isArray(raw) || raw.length === 0) return [];
12541
+ if (typeof raw[0] === "object" && raw[0].start !== void 0) {
12542
+ const codes = [];
12543
+ for (const range of raw) {
12544
+ for (let i = range.start; i <= range.end; i++) {
12545
+ codes.push(i);
12546
+ }
12547
+ }
12548
+ return codes;
12549
+ }
12550
+ return raw;
12540
12551
  } catch {
12541
12552
  return [];
12542
12553
  }
@@ -12599,6 +12610,8 @@ function getFontMetadata(font2) {
12599
12610
  fullName: getNameString(font2, 4),
12600
12611
  familyName: getNameString(font2, 1),
12601
12612
  subfamilyName: getNameString(font2, 2),
12613
+ preferredFamily: getNameString(font2, 16),
12614
+ preferredSubfamily: getNameString(font2, 17),
12602
12615
  copyright: getNameString(font2, 0),
12603
12616
  version: getNameString(font2, 5),
12604
12617
  genDate: (/* @__PURE__ */ new Date()).toISOString()
@@ -13888,6 +13901,102 @@ var init_planTypes = __esm({
13888
13901
  }
13889
13902
  });
13890
13903
 
13904
+ // src/utils/retitleFontEntries.js
13905
+ function retitleFontEntry(entry, preserveShortenedNames, typefaceTitle) {
13906
+ var _a, _b, _c, _d;
13907
+ if (!entry || !entry.parsedMetadata) return entry;
13908
+ if ((_b = (_a = entry.decisions) == null ? void 0 : _a.title) == null ? void 0 : _b.userOverride) return entry;
13909
+ const meta2 = entry.parsedMetadata;
13910
+ const fullName = meta2.fullName || "";
13911
+ const familyName = meta2.familyName || "";
13912
+ const trimmedTitle = (typefaceTitle || "").trim();
13913
+ let weightName = ((_d = (_c = entry.decisions) == null ? void 0 : _c.weightName) == null ? void 0 : _d.detected) || entry.weightName || "";
13914
+ if (!preserveShortenedNames) {
13915
+ weightName = expandAbbreviations(weightName);
13916
+ }
13917
+ const nameId4Remainder = fullName ? fullName.replace(trimmedTitle, "").trim() : "";
13918
+ const nameId1Remainder = familyName ? familyName.replace(trimmedTitle, "").trim() : "";
13919
+ let subfamilyName = nameId4Remainder || nameId1Remainder;
13920
+ if (!preserveShortenedNames) {
13921
+ subfamilyName = expandAbbreviations(subfamilyName);
13922
+ }
13923
+ subfamilyName = processSubfamilyName(subfamilyName, WEIGHT_KW, ITALIC_KW, preserveShortenedNames);
13924
+ subfamilyName = subfamilyName.replace(/\b(Italic|Slant|Slanted|Oblique|Backslant|Roman|Upright)\b/gi, "").replace(/\s+/g, " ").trim();
13925
+ if (subfamilyName !== "") {
13926
+ weightName = weightName.replace(`${subfamilyName} `, "").replace(` ${subfamilyName}`, "").trim();
13927
+ }
13928
+ let fontTitle = fullName.trim() || "";
13929
+ fontTitle = formatFontTitle(fontTitle, preserveShortenedNames);
13930
+ if (entry.variableFont) {
13931
+ if (!fontTitle.toLowerCase().includes("vf")) {
13932
+ fontTitle = fontTitle + " VF";
13933
+ }
13934
+ subfamilyName = "";
13935
+ }
13936
+ if (!(entry.variableFont && fontTitle.toLowerCase().includes("italic"))) {
13937
+ const italicKW = entry.parsedMetadata.italicAngle !== 0 ? "Italic" : "";
13938
+ fontTitle = addItalicToFontTitle(
13939
+ { opentype: { tables: { post: { italicAngle: entry.parsedMetadata.italicAngle || 0 } } } },
13940
+ fontTitle,
13941
+ italicKW,
13942
+ entry.style,
13943
+ preserveShortenedNames
13944
+ );
13945
+ }
13946
+ const documentId = sanitizeForSanityId(fontTitle);
13947
+ if (entry.title === fontTitle && entry.documentId === documentId && entry.weightName === weightName && entry.subfamily === (entry.variableFont ? "" : subfamilyName || "Regular")) {
13948
+ return entry;
13949
+ }
13950
+ const finalSubfamily = entry.variableFont ? "" : subfamilyName || "Regular";
13951
+ return {
13952
+ ...entry,
13953
+ title: fontTitle,
13954
+ documentId,
13955
+ weightName,
13956
+ subfamily: finalSubfamily,
13957
+ decisions: {
13958
+ ...entry.decisions,
13959
+ title: { ...entry.decisions.title, processed: fontTitle },
13960
+ documentId: { ...entry.decisions.documentId, generated: documentId },
13961
+ weightName: { ...entry.decisions.weightName, detected: weightName },
13962
+ subfamily: { ...entry.decisions.subfamily, detected: finalSubfamily }
13963
+ }
13964
+ };
13965
+ }
13966
+ function retitleAllFonts(fonts, preserveShortenedNames, typefaceTitle) {
13967
+ const updated = {};
13968
+ for (const [tempId, entry] of Object.entries(fonts)) {
13969
+ updated[tempId] = retitleFontEntry(entry, preserveShortenedNames, typefaceTitle);
13970
+ }
13971
+ const idMap = {};
13972
+ for (const [tempId, font2] of Object.entries(updated)) {
13973
+ updated[tempId] = { ...font2, _idConflict: false };
13974
+ const docId = font2.documentId;
13975
+ if (!idMap[docId]) {
13976
+ idMap[docId] = [tempId];
13977
+ } else {
13978
+ idMap[docId].push(tempId);
13979
+ }
13980
+ }
13981
+ for (const ids of Object.values(idMap)) {
13982
+ if (ids.length > 1) {
13983
+ for (const id of ids) {
13984
+ updated[id] = { ...updated[id], _idConflict: true };
13985
+ }
13986
+ }
13987
+ }
13988
+ return updated;
13989
+ }
13990
+ var WEIGHT_KW, ITALIC_KW;
13991
+ var init_retitleFontEntries = __esm({
13992
+ "src/utils/retitleFontEntries.js"() {
13993
+ init_generateKeywords();
13994
+ init_processFontFiles();
13995
+ init_sanitizeForSanityId();
13996
+ ({ weightKeywordList: WEIGHT_KW, italicKeywordList: ITALIC_KW } = generateStyleKeywords());
13997
+ }
13998
+ });
13999
+
13891
14000
  // src/utils/planReducer.js
13892
14001
  function planReducer(state, action) {
13893
14002
  switch (action.type) {
@@ -13920,7 +14029,16 @@ function planReducer(state, action) {
13920
14029
  console.warn("SET_SETTINGS blocked \u2014 settings locked during processing/execution");
13921
14030
  return state;
13922
14031
  }
13923
- return { ...state, settings: { ...state.settings, ...action.settings } };
14032
+ const newSettings = { ...state.settings, ...action.settings };
14033
+ let newState = { ...state, settings: newSettings };
14034
+ const shortenedChanged = "preserveShortenedNames" in action.settings && action.settings.preserveShortenedNames !== state.settings.preserveShortenedNames;
14035
+ if (shortenedChanged && Object.keys(state.fonts).length > 0) {
14036
+ const typefaceTitle = action.typefaceTitle || state.settings.typefaceTitle || "";
14037
+ const retitled = retitleAllFonts(state.fonts, newSettings.preserveShortenedNames, typefaceTitle);
14038
+ const subfamilyGroups = rebuildSubfamilyGroups(retitled);
14039
+ newState = { ...newState, fonts: retitled, subfamilyGroups };
14040
+ }
14041
+ return newState;
13924
14042
  }
13925
14043
  // ---------------------------------------------------------------
13926
14044
  // Processing (Phase 1) — dispatched by buildUploadPlan callbacks
@@ -14303,6 +14421,7 @@ var init_planReducer = __esm({
14303
14421
  "src/utils/planReducer.js"() {
14304
14422
  init_planTypes();
14305
14423
  init_sanitizeForSanityId();
14424
+ init_retitleFontEntries();
14306
14425
  VALID_TRANSITIONS = {
14307
14426
  [PLAN_PHASE.IDLE]: [PLAN_PHASE.PROCESSING],
14308
14427
  [PLAN_PHASE.PROCESSING]: [PLAN_PHASE.REVIEWING],
@@ -15504,8 +15623,13 @@ function UploadStep2Review({
15504
15623
  {
15505
15624
  checked: localPreserveShortenedNames,
15506
15625
  onChange: (e) => {
15626
+ var _a2;
15507
15627
  setLocalPreserveShortenedNames(e.target.checked);
15508
- dispatch({ type: "SET_SETTINGS", settings: { preserveShortenedNames: e.target.checked } });
15628
+ dispatch({
15629
+ type: "SET_SETTINGS",
15630
+ settings: { preserveShortenedNames: e.target.checked },
15631
+ typefaceTitle: ((_a2 = plan.settings) == null ? void 0 : _a2.typefaceTitle) || ""
15632
+ });
15509
15633
  }
15510
15634
  }
15511
15635
  ), /* @__PURE__ */ import_react9.default.createElement(
@@ -16562,9 +16686,14 @@ function UploadModal({
16562
16686
  typefaceTitle,
16563
16687
  stylesObject,
16564
16688
  preferredStyleRef,
16565
- slug
16689
+ slug,
16690
+ defaults = {}
16566
16691
  }) {
16567
- const [plan, dispatch] = (0, import_react13.useReducer)(planReducer, null, () => createEmptyPlan());
16692
+ const [plan, dispatch] = (0, import_react13.useReducer)(planReducer, null, () => createEmptyPlan({
16693
+ price: defaults.price ?? 0,
16694
+ preserveShortenedNames: defaults.preserveShortenedNames ?? false,
16695
+ preserveFileNames: defaults.preserveFileNames ?? false
16696
+ }));
16568
16697
  const [processingCancelled, setProcessingCancelled] = (0, import_react13.useState)(false);
16569
16698
  const [executionResult, setExecutionResult] = (0, import_react13.useState)(null);
16570
16699
  const [retryTempIds, setRetryTempIds] = (0, import_react13.useState)(null);
@@ -17500,13 +17629,15 @@ var formatElapsed2 = (s) => {
17500
17629
  const sec = s % 60;
17501
17630
  return m > 0 ? `${m}m ${sec}s` : `${sec}s`;
17502
17631
  };
17503
- var BatchUploadFonts = () => {
17632
+ var BatchUploadFonts = (props) => {
17633
+ var _a, _b;
17634
+ const defaults = ((_b = (_a = props == null ? void 0 : props.schemaType) == null ? void 0 : _a.options) == null ? void 0 : _b.defaults) || {};
17504
17635
  const [status, setStatus] = (0, import_react14.useState)("ready");
17505
17636
  const [ready, setReady] = (0, import_react14.useState)(true);
17506
- const [inputPrice, setInputPrice] = (0, import_react14.useState)("0");
17637
+ const [inputPrice, setInputPrice] = (0, import_react14.useState)(String(defaults.price ?? "0"));
17507
17638
  const [error, setError] = (0, import_react14.useState)(false);
17508
- const [preserveShortenedNames, setPreserveShortenedNames] = (0, import_react14.useState)(true);
17509
- const [preserveFileNames, setPreserveFileNames] = (0, import_react14.useState)(false);
17639
+ const [preserveShortenedNames, setPreserveShortenedNames] = (0, import_react14.useState)(defaults.preserveShortenedNames ?? true);
17640
+ const [preserveFileNames, setPreserveFileNames] = (0, import_react14.useState)(defaults.preserveFileNames ?? false);
17510
17641
  const [showUtilities, setShowUtilities] = (0, import_react14.useState)(false);
17511
17642
  const [pendingFiles, setPendingFiles] = (0, import_react14.useState)([]);
17512
17643
  const [isDragging, setIsDragging] = (0, import_react14.useState)(false);
@@ -17545,9 +17676,9 @@ var BatchUploadFonts = () => {
17545
17676
  }
17546
17677
  }, [ready]);
17547
17678
  (0, import_react14.useEffect)(() => {
17548
- var _a;
17679
+ var _a2;
17549
17680
  if (ready !== true) {
17550
- (_a = navigator.wakeLock) == null ? void 0 : _a.request("screen").then((lock) => {
17681
+ (_a2 = navigator.wakeLock) == null ? void 0 : _a2.request("screen").then((lock) => {
17551
17682
  wakeLockRef.current = lock;
17552
17683
  }).catch(() => {
17553
17684
  });
@@ -17588,8 +17719,8 @@ var BatchUploadFonts = () => {
17588
17719
  files: failedFiles,
17589
17720
  names: failedFiles.map((f) => f.name),
17590
17721
  metadata: failedFiles.map((f) => {
17591
- var _a, _b;
17592
- return (_b = (_a = f == null ? void 0 : f.fk) == null ? void 0 : _a.name) == null ? void 0 : _b.records;
17722
+ var _a2, _b2;
17723
+ return (_b2 = (_a2 = f == null ? void 0 : f.fk) == null ? void 0 : _a2.name) == null ? void 0 : _b2.records;
17593
17724
  })
17594
17725
  });
17595
17726
  setError2(true);
@@ -17714,7 +17845,7 @@ var BatchUploadFonts = () => {
17714
17845
  setReady(true);
17715
17846
  }, [title, slug, client, doc_id, inputPrice]);
17716
17847
  const handleRegenerateCssFiles = (0, import_react14.useCallback)(async () => {
17717
- var _a, _b, _c, _d;
17848
+ var _a2, _b2, _c, _d;
17718
17849
  try {
17719
17850
  setStatus("Regenerating CSS files...");
17720
17851
  setReady("css");
@@ -17741,7 +17872,7 @@ var BatchUploadFonts = () => {
17741
17872
  setReady(true);
17742
17873
  return false;
17743
17874
  }
17744
- if (!((_b = (_a = typeface.styles) == null ? void 0 : _a.fonts) == null ? void 0 : _b.length)) {
17875
+ if (!((_b2 = (_a2 = typeface.styles) == null ? void 0 : _a2.fonts) == null ? void 0 : _b2.length)) {
17745
17876
  setStatus("No fonts found in typeface");
17746
17877
  setError(true);
17747
17878
  setReady(true);
@@ -17851,8 +17982,8 @@ var BatchUploadFonts = () => {
17851
17982
  padding: 2,
17852
17983
  text: "Browse files",
17853
17984
  onClick: () => {
17854
- var _a;
17855
- return (_a = fileInputRef.current) == null ? void 0 : _a.click();
17985
+ var _a2;
17986
+ return (_a2 = fileInputRef.current) == null ? void 0 : _a2.click();
17856
17987
  }
17857
17988
  }
17858
17989
  )))
@@ -17924,8 +18055,8 @@ var BatchUploadFonts = () => {
17924
18055
  padding: 1,
17925
18056
  text: "browse",
17926
18057
  onClick: () => {
17927
- var _a;
17928
- return (_a = fileInputRef.current) == null ? void 0 : _a.click();
18058
+ var _a2;
18059
+ return (_a2 = fileInputRef.current) == null ? void 0 : _a2.click();
17929
18060
  }
17930
18061
  }
17931
18062
  ))
@@ -17982,7 +18113,8 @@ var BatchUploadFonts = () => {
17982
18113
  typefaceTitle: title,
17983
18114
  stylesObject,
17984
18115
  preferredStyleRef,
17985
- slug
18116
+ slug,
18117
+ defaults
17986
18118
  }
17987
18119
  )), showUtilities && /* @__PURE__ */ import_react14.default.createElement(import_ui13.Card, { border: true, padding: 3, shadow: 1, radius: 2, marginTop: 3 }, /* @__PURE__ */ import_react14.default.createElement(import_ui13.Stack, { space: 4 }, /* @__PURE__ */ import_react14.default.createElement(import_ui13.Stack, { space: 2 }, /* @__PURE__ */ import_react14.default.createElement(import_ui13.Text, { size: 1, weight: "semibold", style: { lineHeight: 1.6 } }, "Regenerate Subfamilies"), /* @__PURE__ */ import_react14.default.createElement(RegenerateSubfamiliesComponent, null)), /* @__PURE__ */ import_react14.default.createElement(import_ui13.Stack, { space: 3 }, /* @__PURE__ */ import_react14.default.createElement(import_ui13.Text, { size: 1, weight: "semibold", style: { lineHeight: 1.6 } }, "Rename Fonts (name table, Full Name)"), /* @__PURE__ */ import_react14.default.createElement(import_ui13.Flex, { align: "center", gap: 2 }, /* @__PURE__ */ import_react14.default.createElement(
17988
18120
  import_ui13.Switch,
@@ -22805,6 +22937,14 @@ var stylisticSetField = {
22805
22937
  // src/schema/stylesField.js
22806
22938
  var import_react30 = __toESM(require("react"));
22807
22939
  var import_sanity_advanced_reference_array = require("@liiift-studio/sanity-advanced-reference-array");
22940
+ var inlineStyleCountField = {
22941
+ name: "styleCount",
22942
+ type: "number",
22943
+ components: {
22944
+ input: StyleCountInput
22945
+ },
22946
+ readOnly: true
22947
+ };
22808
22948
  var typefaceParams = (doc) => ({ typefaceName: (doc == null ? void 0 : doc.title) || "" });
22809
22949
  var FontsRefArray = (props) => import_react30.default.createElement(import_sanity_advanced_reference_array.AdvancedRefArray, {
22810
22950
  ...props,
@@ -22865,7 +23005,8 @@ function createStylesField({
22865
23005
  subfamilyPreview = false,
22866
23006
  pairs = true,
22867
23007
  generateCollections = false,
22868
- generateFullFamilyCollection = false
23008
+ generateFullFamilyCollection = false,
23009
+ styleCount = false
22869
23010
  } = {}) {
22870
23011
  const subfamilyFields = [
22871
23012
  {
@@ -22965,6 +23106,7 @@ function createStylesField({
22965
23106
  initialValue: false,
22966
23107
  description: "Check if this typeface includes serif letterforms. Used for typeface overview serif/sans filters. Frontend automatically treats non-serif typefaces as sans serif."
22967
23108
  }),
23109
+ ...field(styleCount, inlineStyleCountField),
22968
23110
  {
22969
23111
  title: "Fonts",
22970
23112
  name: "fonts",
package/dist/index.mjs CHANGED
@@ -12541,7 +12541,18 @@ function getCharacterSet(font2) {
12541
12541
  const cmap2 = (_b = (_a = font2.opentype) == null ? void 0 : _a.tables) == null ? void 0 : _b.cmap;
12542
12542
  if (!cmap2) return [];
12543
12543
  try {
12544
- return cmap2.getSupportedCharCodes(3, 1);
12544
+ const raw = cmap2.getSupportedCharCodes(3, 1);
12545
+ if (!Array.isArray(raw) || raw.length === 0) return [];
12546
+ if (typeof raw[0] === "object" && raw[0].start !== void 0) {
12547
+ const codes = [];
12548
+ for (const range of raw) {
12549
+ for (let i = range.start; i <= range.end; i++) {
12550
+ codes.push(i);
12551
+ }
12552
+ }
12553
+ return codes;
12554
+ }
12555
+ return raw;
12545
12556
  } catch {
12546
12557
  return [];
12547
12558
  }
@@ -12604,6 +12615,8 @@ function getFontMetadata(font2) {
12604
12615
  fullName: getNameString(font2, 4),
12605
12616
  familyName: getNameString(font2, 1),
12606
12617
  subfamilyName: getNameString(font2, 2),
12618
+ preferredFamily: getNameString(font2, 16),
12619
+ preferredSubfamily: getNameString(font2, 17),
12607
12620
  copyright: getNameString(font2, 0),
12608
12621
  version: getNameString(font2, 5),
12609
12622
  genDate: (/* @__PURE__ */ new Date()).toISOString()
@@ -13892,6 +13905,102 @@ var init_planTypes = __esm({
13892
13905
  }
13893
13906
  });
13894
13907
 
13908
+ // src/utils/retitleFontEntries.js
13909
+ function retitleFontEntry(entry, preserveShortenedNames, typefaceTitle) {
13910
+ var _a, _b, _c, _d;
13911
+ if (!entry || !entry.parsedMetadata) return entry;
13912
+ if ((_b = (_a = entry.decisions) == null ? void 0 : _a.title) == null ? void 0 : _b.userOverride) return entry;
13913
+ const meta2 = entry.parsedMetadata;
13914
+ const fullName = meta2.fullName || "";
13915
+ const familyName = meta2.familyName || "";
13916
+ const trimmedTitle = (typefaceTitle || "").trim();
13917
+ let weightName = ((_d = (_c = entry.decisions) == null ? void 0 : _c.weightName) == null ? void 0 : _d.detected) || entry.weightName || "";
13918
+ if (!preserveShortenedNames) {
13919
+ weightName = expandAbbreviations(weightName);
13920
+ }
13921
+ const nameId4Remainder = fullName ? fullName.replace(trimmedTitle, "").trim() : "";
13922
+ const nameId1Remainder = familyName ? familyName.replace(trimmedTitle, "").trim() : "";
13923
+ let subfamilyName = nameId4Remainder || nameId1Remainder;
13924
+ if (!preserveShortenedNames) {
13925
+ subfamilyName = expandAbbreviations(subfamilyName);
13926
+ }
13927
+ subfamilyName = processSubfamilyName(subfamilyName, WEIGHT_KW, ITALIC_KW, preserveShortenedNames);
13928
+ subfamilyName = subfamilyName.replace(/\b(Italic|Slant|Slanted|Oblique|Backslant|Roman|Upright)\b/gi, "").replace(/\s+/g, " ").trim();
13929
+ if (subfamilyName !== "") {
13930
+ weightName = weightName.replace(`${subfamilyName} `, "").replace(` ${subfamilyName}`, "").trim();
13931
+ }
13932
+ let fontTitle = fullName.trim() || "";
13933
+ fontTitle = formatFontTitle(fontTitle, preserveShortenedNames);
13934
+ if (entry.variableFont) {
13935
+ if (!fontTitle.toLowerCase().includes("vf")) {
13936
+ fontTitle = fontTitle + " VF";
13937
+ }
13938
+ subfamilyName = "";
13939
+ }
13940
+ if (!(entry.variableFont && fontTitle.toLowerCase().includes("italic"))) {
13941
+ const italicKW = entry.parsedMetadata.italicAngle !== 0 ? "Italic" : "";
13942
+ fontTitle = addItalicToFontTitle(
13943
+ { opentype: { tables: { post: { italicAngle: entry.parsedMetadata.italicAngle || 0 } } } },
13944
+ fontTitle,
13945
+ italicKW,
13946
+ entry.style,
13947
+ preserveShortenedNames
13948
+ );
13949
+ }
13950
+ const documentId = sanitizeForSanityId(fontTitle);
13951
+ if (entry.title === fontTitle && entry.documentId === documentId && entry.weightName === weightName && entry.subfamily === (entry.variableFont ? "" : subfamilyName || "Regular")) {
13952
+ return entry;
13953
+ }
13954
+ const finalSubfamily = entry.variableFont ? "" : subfamilyName || "Regular";
13955
+ return {
13956
+ ...entry,
13957
+ title: fontTitle,
13958
+ documentId,
13959
+ weightName,
13960
+ subfamily: finalSubfamily,
13961
+ decisions: {
13962
+ ...entry.decisions,
13963
+ title: { ...entry.decisions.title, processed: fontTitle },
13964
+ documentId: { ...entry.decisions.documentId, generated: documentId },
13965
+ weightName: { ...entry.decisions.weightName, detected: weightName },
13966
+ subfamily: { ...entry.decisions.subfamily, detected: finalSubfamily }
13967
+ }
13968
+ };
13969
+ }
13970
+ function retitleAllFonts(fonts, preserveShortenedNames, typefaceTitle) {
13971
+ const updated = {};
13972
+ for (const [tempId, entry] of Object.entries(fonts)) {
13973
+ updated[tempId] = retitleFontEntry(entry, preserveShortenedNames, typefaceTitle);
13974
+ }
13975
+ const idMap = {};
13976
+ for (const [tempId, font2] of Object.entries(updated)) {
13977
+ updated[tempId] = { ...font2, _idConflict: false };
13978
+ const docId = font2.documentId;
13979
+ if (!idMap[docId]) {
13980
+ idMap[docId] = [tempId];
13981
+ } else {
13982
+ idMap[docId].push(tempId);
13983
+ }
13984
+ }
13985
+ for (const ids of Object.values(idMap)) {
13986
+ if (ids.length > 1) {
13987
+ for (const id of ids) {
13988
+ updated[id] = { ...updated[id], _idConflict: true };
13989
+ }
13990
+ }
13991
+ }
13992
+ return updated;
13993
+ }
13994
+ var WEIGHT_KW, ITALIC_KW;
13995
+ var init_retitleFontEntries = __esm({
13996
+ "src/utils/retitleFontEntries.js"() {
13997
+ init_generateKeywords();
13998
+ init_processFontFiles();
13999
+ init_sanitizeForSanityId();
14000
+ ({ weightKeywordList: WEIGHT_KW, italicKeywordList: ITALIC_KW } = generateStyleKeywords());
14001
+ }
14002
+ });
14003
+
13895
14004
  // src/utils/planReducer.js
13896
14005
  function planReducer(state, action) {
13897
14006
  switch (action.type) {
@@ -13924,7 +14033,16 @@ function planReducer(state, action) {
13924
14033
  console.warn("SET_SETTINGS blocked \u2014 settings locked during processing/execution");
13925
14034
  return state;
13926
14035
  }
13927
- return { ...state, settings: { ...state.settings, ...action.settings } };
14036
+ const newSettings = { ...state.settings, ...action.settings };
14037
+ let newState = { ...state, settings: newSettings };
14038
+ const shortenedChanged = "preserveShortenedNames" in action.settings && action.settings.preserveShortenedNames !== state.settings.preserveShortenedNames;
14039
+ if (shortenedChanged && Object.keys(state.fonts).length > 0) {
14040
+ const typefaceTitle = action.typefaceTitle || state.settings.typefaceTitle || "";
14041
+ const retitled = retitleAllFonts(state.fonts, newSettings.preserveShortenedNames, typefaceTitle);
14042
+ const subfamilyGroups = rebuildSubfamilyGroups(retitled);
14043
+ newState = { ...newState, fonts: retitled, subfamilyGroups };
14044
+ }
14045
+ return newState;
13928
14046
  }
13929
14047
  // ---------------------------------------------------------------
13930
14048
  // Processing (Phase 1) — dispatched by buildUploadPlan callbacks
@@ -14307,6 +14425,7 @@ var init_planReducer = __esm({
14307
14425
  "src/utils/planReducer.js"() {
14308
14426
  init_planTypes();
14309
14427
  init_sanitizeForSanityId();
14428
+ init_retitleFontEntries();
14310
14429
  VALID_TRANSITIONS = {
14311
14430
  [PLAN_PHASE.IDLE]: [PLAN_PHASE.PROCESSING],
14312
14431
  [PLAN_PHASE.PROCESSING]: [PLAN_PHASE.REVIEWING],
@@ -15508,8 +15627,13 @@ function UploadStep2Review({
15508
15627
  {
15509
15628
  checked: localPreserveShortenedNames,
15510
15629
  onChange: (e) => {
15630
+ var _a2;
15511
15631
  setLocalPreserveShortenedNames(e.target.checked);
15512
- dispatch({ type: "SET_SETTINGS", settings: { preserveShortenedNames: e.target.checked } });
15632
+ dispatch({
15633
+ type: "SET_SETTINGS",
15634
+ settings: { preserveShortenedNames: e.target.checked },
15635
+ typefaceTitle: ((_a2 = plan.settings) == null ? void 0 : _a2.typefaceTitle) || ""
15636
+ });
15513
15637
  }
15514
15638
  }
15515
15639
  ), /* @__PURE__ */ React8.createElement(
@@ -16561,9 +16685,14 @@ function UploadModal({
16561
16685
  typefaceTitle,
16562
16686
  stylesObject,
16563
16687
  preferredStyleRef,
16564
- slug
16688
+ slug,
16689
+ defaults = {}
16565
16690
  }) {
16566
- const [plan, dispatch] = useReducer2(planReducer, null, () => createEmptyPlan());
16691
+ const [plan, dispatch] = useReducer2(planReducer, null, () => createEmptyPlan({
16692
+ price: defaults.price ?? 0,
16693
+ preserveShortenedNames: defaults.preserveShortenedNames ?? false,
16694
+ preserveFileNames: defaults.preserveFileNames ?? false
16695
+ }));
16567
16696
  const [processingCancelled, setProcessingCancelled] = useState8(false);
16568
16697
  const [executionResult, setExecutionResult] = useState8(null);
16569
16698
  const [retryTempIds, setRetryTempIds] = useState8(null);
@@ -17399,13 +17528,15 @@ var formatElapsed2 = (s) => {
17399
17528
  const sec = s % 60;
17400
17529
  return m > 0 ? `${m}m ${sec}s` : `${sec}s`;
17401
17530
  };
17402
- var BatchUploadFonts = () => {
17531
+ var BatchUploadFonts = (props) => {
17532
+ var _a, _b;
17533
+ const defaults = ((_b = (_a = props == null ? void 0 : props.schemaType) == null ? void 0 : _a.options) == null ? void 0 : _b.defaults) || {};
17403
17534
  const [status, setStatus] = useState9("ready");
17404
17535
  const [ready, setReady] = useState9(true);
17405
- const [inputPrice, setInputPrice] = useState9("0");
17536
+ const [inputPrice, setInputPrice] = useState9(String(defaults.price ?? "0"));
17406
17537
  const [error, setError] = useState9(false);
17407
- const [preserveShortenedNames, setPreserveShortenedNames] = useState9(true);
17408
- const [preserveFileNames, setPreserveFileNames] = useState9(false);
17538
+ const [preserveShortenedNames, setPreserveShortenedNames] = useState9(defaults.preserveShortenedNames ?? true);
17539
+ const [preserveFileNames, setPreserveFileNames] = useState9(defaults.preserveFileNames ?? false);
17409
17540
  const [showUtilities, setShowUtilities] = useState9(false);
17410
17541
  const [pendingFiles, setPendingFiles] = useState9([]);
17411
17542
  const [isDragging, setIsDragging] = useState9(false);
@@ -17444,9 +17575,9 @@ var BatchUploadFonts = () => {
17444
17575
  }
17445
17576
  }, [ready]);
17446
17577
  useEffect6(() => {
17447
- var _a;
17578
+ var _a2;
17448
17579
  if (ready !== true) {
17449
- (_a = navigator.wakeLock) == null ? void 0 : _a.request("screen").then((lock) => {
17580
+ (_a2 = navigator.wakeLock) == null ? void 0 : _a2.request("screen").then((lock) => {
17450
17581
  wakeLockRef.current = lock;
17451
17582
  }).catch(() => {
17452
17583
  });
@@ -17487,8 +17618,8 @@ var BatchUploadFonts = () => {
17487
17618
  files: failedFiles,
17488
17619
  names: failedFiles.map((f) => f.name),
17489
17620
  metadata: failedFiles.map((f) => {
17490
- var _a, _b;
17491
- return (_b = (_a = f == null ? void 0 : f.fk) == null ? void 0 : _a.name) == null ? void 0 : _b.records;
17621
+ var _a2, _b2;
17622
+ return (_b2 = (_a2 = f == null ? void 0 : f.fk) == null ? void 0 : _a2.name) == null ? void 0 : _b2.records;
17492
17623
  })
17493
17624
  });
17494
17625
  setError2(true);
@@ -17613,7 +17744,7 @@ var BatchUploadFonts = () => {
17613
17744
  setReady(true);
17614
17745
  }, [title, slug, client, doc_id, inputPrice]);
17615
17746
  const handleRegenerateCssFiles = useCallback7(async () => {
17616
- var _a, _b, _c, _d;
17747
+ var _a2, _b2, _c, _d;
17617
17748
  try {
17618
17749
  setStatus("Regenerating CSS files...");
17619
17750
  setReady("css");
@@ -17640,7 +17771,7 @@ var BatchUploadFonts = () => {
17640
17771
  setReady(true);
17641
17772
  return false;
17642
17773
  }
17643
- if (!((_b = (_a = typeface.styles) == null ? void 0 : _a.fonts) == null ? void 0 : _b.length)) {
17774
+ if (!((_b2 = (_a2 = typeface.styles) == null ? void 0 : _a2.fonts) == null ? void 0 : _b2.length)) {
17644
17775
  setStatus("No fonts found in typeface");
17645
17776
  setError(true);
17646
17777
  setReady(true);
@@ -17750,8 +17881,8 @@ var BatchUploadFonts = () => {
17750
17881
  padding: 2,
17751
17882
  text: "Browse files",
17752
17883
  onClick: () => {
17753
- var _a;
17754
- return (_a = fileInputRef.current) == null ? void 0 : _a.click();
17884
+ var _a2;
17885
+ return (_a2 = fileInputRef.current) == null ? void 0 : _a2.click();
17755
17886
  }
17756
17887
  }
17757
17888
  )))
@@ -17823,8 +17954,8 @@ var BatchUploadFonts = () => {
17823
17954
  padding: 1,
17824
17955
  text: "browse",
17825
17956
  onClick: () => {
17826
- var _a;
17827
- return (_a = fileInputRef.current) == null ? void 0 : _a.click();
17957
+ var _a2;
17958
+ return (_a2 = fileInputRef.current) == null ? void 0 : _a2.click();
17828
17959
  }
17829
17960
  }
17830
17961
  ))
@@ -17881,7 +18012,8 @@ var BatchUploadFonts = () => {
17881
18012
  typefaceTitle: title,
17882
18013
  stylesObject,
17883
18014
  preferredStyleRef,
17884
- slug
18015
+ slug,
18016
+ defaults
17885
18017
  }
17886
18018
  )), showUtilities && /* @__PURE__ */ React13.createElement(Card8, { border: true, padding: 3, shadow: 1, radius: 2, marginTop: 3 }, /* @__PURE__ */ React13.createElement(Stack9, { space: 4 }, /* @__PURE__ */ React13.createElement(Stack9, { space: 2 }, /* @__PURE__ */ React13.createElement(Text13, { size: 1, weight: "semibold", style: { lineHeight: 1.6 } }, "Regenerate Subfamilies"), /* @__PURE__ */ React13.createElement(RegenerateSubfamiliesComponent, null)), /* @__PURE__ */ React13.createElement(Stack9, { space: 3 }, /* @__PURE__ */ React13.createElement(Text13, { size: 1, weight: "semibold", style: { lineHeight: 1.6 } }, "Rename Fonts (name table, Full Name)"), /* @__PURE__ */ React13.createElement(Flex12, { align: "center", gap: 2 }, /* @__PURE__ */ React13.createElement(
17887
18019
  Switch3,
@@ -22704,6 +22836,14 @@ var stylisticSetField = {
22704
22836
  // src/schema/stylesField.js
22705
22837
  import React28 from "react";
22706
22838
  import { AdvancedRefArray } from "@liiift-studio/sanity-advanced-reference-array";
22839
+ var inlineStyleCountField = {
22840
+ name: "styleCount",
22841
+ type: "number",
22842
+ components: {
22843
+ input: StyleCountInput
22844
+ },
22845
+ readOnly: true
22846
+ };
22707
22847
  var typefaceParams = (doc) => ({ typefaceName: (doc == null ? void 0 : doc.title) || "" });
22708
22848
  var FontsRefArray = (props) => React28.createElement(AdvancedRefArray, {
22709
22849
  ...props,
@@ -22764,7 +22904,8 @@ function createStylesField({
22764
22904
  subfamilyPreview = false,
22765
22905
  pairs = true,
22766
22906
  generateCollections = false,
22767
- generateFullFamilyCollection = false
22907
+ generateFullFamilyCollection = false,
22908
+ styleCount = false
22768
22909
  } = {}) {
22769
22910
  const subfamilyFields = [
22770
22911
  {
@@ -22864,6 +23005,7 @@ function createStylesField({
22864
23005
  initialValue: false,
22865
23006
  description: "Check if this typeface includes serif letterforms. Used for typeface overview serif/sans filters. Frontend automatically treats non-serif typefaces as sans serif."
22866
23007
  }),
23008
+ ...field(styleCount, inlineStyleCountField),
22867
23009
  {
22868
23010
  title: "Fonts",
22869
23011
  name: "fonts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liiift-studio/sanity-font-manager",
3
- "version": "2.5.9",
3
+ "version": "2.7.0",
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",
@@ -30,13 +30,14 @@ const formatElapsed = (s) => {
30
30
  return m > 0 ? `${m}m ${sec}s` : `${sec}s`;
31
31
  };
32
32
 
33
- export const BatchUploadFonts = () => {
33
+ export const BatchUploadFonts = (props) => {
34
+ const defaults = props?.schemaType?.options?.defaults || {};
34
35
  const [status, setStatus] = useState('ready');
35
36
  const [ready, setReady] = useState(true);
36
- const [inputPrice, setInputPrice] = useState('0');
37
+ const [inputPrice, setInputPrice] = useState(String(defaults.price ?? '0'));
37
38
  const [error, setError] = useState(false);
38
- const [preserveShortenedNames, setPreserveShortenedNames] = useState(true);
39
- const [preserveFileNames, setPreserveFileNames] = useState(false);
39
+ const [preserveShortenedNames, setPreserveShortenedNames] = useState(defaults.preserveShortenedNames ?? true);
40
+ const [preserveFileNames, setPreserveFileNames] = useState(defaults.preserveFileNames ?? false);
40
41
  const [showUtilities, setShowUtilities] = useState(false);
41
42
  const [pendingFiles, setPendingFiles] = useState([]);
42
43
  const [isDragging, setIsDragging] = useState(false);
@@ -587,6 +588,7 @@ export const BatchUploadFonts = () => {
587
588
  stylesObject={stylesObject}
588
589
  preferredStyleRef={preferredStyleRef}
589
590
  slug={slug}
591
+ defaults={defaults}
590
592
  />
591
593
  </Suspense>
592
594
  )}
@@ -46,8 +46,13 @@ export default function UploadModal({
46
46
  stylesObject,
47
47
  preferredStyleRef,
48
48
  slug,
49
+ defaults = {},
49
50
  }) {
50
- const [plan, dispatch] = useReducer(planReducer, null, () => createEmptyPlan());
51
+ const [plan, dispatch] = useReducer(planReducer, null, () => createEmptyPlan({
52
+ price: defaults.price ?? 0,
53
+ preserveShortenedNames: defaults.preserveShortenedNames ?? false,
54
+ preserveFileNames: defaults.preserveFileNames ?? false,
55
+ }));
51
56
  const [processingCancelled, setProcessingCancelled] = useState(false);
52
57
  const [executionResult, setExecutionResult] = useState(null);
53
58
  const [retryTempIds, setRetryTempIds] = useState(null);
@@ -313,7 +313,11 @@ export default function UploadStep2Review({
313
313
  checked={localPreserveShortenedNames}
314
314
  onChange={(e) => {
315
315
  setLocalPreserveShortenedNames(e.target.checked);
316
- dispatch({ type: 'SET_SETTINGS', settings: { preserveShortenedNames: e.target.checked } });
316
+ dispatch({
317
+ type: 'SET_SETTINGS',
318
+ settings: { preserveShortenedNames: e.target.checked },
319
+ typefaceTitle: plan.settings?.typefaceTitle || '',
320
+ });
317
321
  }}
318
322
  />
319
323
  <Tooltip
@@ -4,6 +4,17 @@ import { AdvancedRefArray } from '@liiift-studio/sanity-advanced-reference-array
4
4
  import { RegenerateSubfamiliesComponent } from '../components/RegenerateSubfamiliesComponent.jsx';
5
5
  import { GenerateCollectionsPairsComponent } from '../components/GenerateCollectionsPairsComponent.jsx';
6
6
  import { PrimaryCollectionGeneratorTypeface } from '../components/PrimaryCollectionGeneratorTypeface.jsx';
7
+ import { StyleCountInput } from '../components/StyleCountInput.jsx';
8
+
9
+ // Inline style count field — injected into the styles object when styleCount option is true
10
+ const inlineStyleCountField = {
11
+ name: 'styleCount',
12
+ type: 'number',
13
+ components: {
14
+ input: StyleCountInput,
15
+ },
16
+ readOnly: true,
17
+ };
7
18
 
8
19
  // Returns extra GROQ params scoped to the current typeface document
9
20
  const typefaceParams = (doc) => ({ typefaceName: doc?.title || '' });
@@ -80,6 +91,7 @@ const subfamilyPreferredStyleFilter = async ({ getClient, document, parent }) =>
80
91
  * @param {boolean} [options.subfamilyFontFilter=false] - Filter subfamily font picker to typeface fonts only
81
92
  * @param {boolean} [options.subfamilyPreview=false] - Include preview on subfamily array items
82
93
  * @param {boolean} [options.pairs=true] - Show pairs array to editors
94
+ * @param {boolean} [options.styleCount=false] - Include style count display above fonts array
83
95
  */
84
96
  export function createStylesField({
85
97
  free = false,
@@ -97,6 +109,7 @@ export function createStylesField({
97
109
  pairs = true,
98
110
  generateCollections = false,
99
111
  generateFullFamilyCollection = false,
112
+ styleCount = false,
100
113
  } = {}) {
101
114
 
102
115
  const subfamilyFields = [
@@ -199,6 +212,7 @@ export function createStylesField({
199
212
  initialValue: false,
200
213
  description: 'Check if this typeface includes serif letterforms. Used for typeface overview serif/sans filters. Frontend automatically treats non-serif typefaces as sans serif.',
201
214
  }),
215
+ ...field(styleCount, inlineStyleCountField),
202
216
  {
203
217
  title: 'Fonts',
204
218
  name: 'fonts',
@@ -80,7 +80,19 @@ export function getCharacterSet(font) {
80
80
  const cmap = font.opentype?.tables?.cmap;
81
81
  if (!cmap) return [];
82
82
  try {
83
- return cmap.getSupportedCharCodes(3, 1);
83
+ const raw = cmap.getSupportedCharCodes(3, 1);
84
+ if (!Array.isArray(raw) || raw.length === 0) return [];
85
+ // getSupportedCharCodes may return range objects { start, end } — expand to individual codepoints
86
+ if (typeof raw[0] === 'object' && raw[0].start !== undefined) {
87
+ const codes = [];
88
+ for (const range of raw) {
89
+ for (let i = range.start; i <= range.end; i++) {
90
+ codes.push(i);
91
+ }
92
+ }
93
+ return codes;
94
+ }
95
+ return raw;
84
96
  } catch {
85
97
  return [];
86
98
  }
@@ -175,6 +187,8 @@ export function getFontMetadata(font) {
175
187
  fullName: getNameString(font, 4),
176
188
  familyName: getNameString(font, 1),
177
189
  subfamilyName: getNameString(font, 2),
190
+ preferredFamily: getNameString(font, 16),
191
+ preferredSubfamily: getNameString(font, 17),
178
192
  copyright: getNameString(font, 0),
179
193
  version: getNameString(font, 5),
180
194
  genDate: new Date().toISOString(),
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { PLAN_PHASE, FONT_STATUS, PROCESSING_OWNED_FIELDS } from './planTypes';
4
4
  import { sanitizeForSanityId } from './sanitizeForSanityId';
5
+ import { retitleAllFonts } from './retitleFontEntries';
5
6
 
6
7
  /** Valid phase transitions — any phase can transition to 'idle' (reset) */
7
8
  const VALID_TRANSITIONS = {
@@ -57,7 +58,20 @@ export function planReducer(state, action) {
57
58
  console.warn('SET_SETTINGS blocked — settings locked during processing/execution');
58
59
  return state;
59
60
  }
60
- return { ...state, settings: { ...state.settings, ...action.settings } };
61
+ const newSettings = { ...state.settings, ...action.settings };
62
+ let newState = { ...state, settings: newSettings };
63
+
64
+ // Retitle fonts when preserveShortenedNames changes during review
65
+ const shortenedChanged = 'preserveShortenedNames' in action.settings
66
+ && action.settings.preserveShortenedNames !== state.settings.preserveShortenedNames;
67
+ if (shortenedChanged && Object.keys(state.fonts).length > 0) {
68
+ const typefaceTitle = action.typefaceTitle || state.settings.typefaceTitle || '';
69
+ const retitled = retitleAllFonts(state.fonts, newSettings.preserveShortenedNames, typefaceTitle);
70
+ const subfamilyGroups = rebuildSubfamilyGroups(retitled);
71
+ newState = { ...newState, fonts: retitled, subfamilyGroups };
72
+ }
73
+
74
+ return newState;
61
75
  }
62
76
 
63
77
  // ---------------------------------------------------------------
@@ -0,0 +1,154 @@
1
+ // Lightweight retitle pass — re-derives title, documentId, weightName, and
2
+ // subfamilyName from cached parsedMetadata when preserveShortenedNames changes.
3
+ // No fontkit parsing, no file I/O, no Sanity queries — pure string manipulation.
4
+
5
+ import { expandAbbreviations, generateStyleKeywords } from './generateKeywords.js';
6
+ import { formatFontTitle, addItalicToFontTitle, processSubfamilyName } from './processFontFiles.js';
7
+ import { sanitizeForSanityId } from './sanitizeForSanityId.js';
8
+
9
+ const { weightKeywordList: WEIGHT_KW, italicKeywordList: ITALIC_KW } = generateStyleKeywords();
10
+
11
+ /**
12
+ * Re-derive title, documentId, weightName, and subfamilyName for a single
13
+ * font entry using cached metadata and the new preserveShortenedNames setting.
14
+ * Skips entries with user overrides on title (user edits take precedence).
15
+ *
16
+ * @param {object} entry - Font plan entry with parsedMetadata and decisions
17
+ * @param {boolean} preserveShortenedNames - New setting value
18
+ * @param {string} typefaceTitle - Parent typeface title (for prefix stripping)
19
+ * @returns {object} Updated entry (shallow copy if changed, same ref if unchanged)
20
+ */
21
+ export function retitleFontEntry(entry, preserveShortenedNames, typefaceTitle) {
22
+ if (!entry || !entry.parsedMetadata) return entry;
23
+
24
+ // Skip entries with user overrides on title — user edits take precedence
25
+ if (entry.decisions?.title?.userOverride) return entry;
26
+
27
+ const meta = entry.parsedMetadata;
28
+ const fullName = meta.fullName || '';
29
+ const familyName = meta.familyName || '';
30
+ const trimmedTitle = (typefaceTitle || '').trim();
31
+
32
+ // Re-derive weightName from the original detected value
33
+ let weightName = entry.decisions?.weightName?.detected || entry.weightName || '';
34
+ if (!preserveShortenedNames) {
35
+ weightName = expandAbbreviations(weightName);
36
+ }
37
+
38
+ // Re-derive subfamilyName
39
+ const nameId4Remainder = fullName ? fullName.replace(trimmedTitle, '').trim() : '';
40
+ const nameId1Remainder = familyName ? familyName.replace(trimmedTitle, '').trim() : '';
41
+ let subfamilyName = nameId4Remainder || nameId1Remainder;
42
+
43
+ if (!preserveShortenedNames) {
44
+ subfamilyName = expandAbbreviations(subfamilyName);
45
+ }
46
+
47
+ subfamilyName = processSubfamilyName(subfamilyName, WEIGHT_KW, ITALIC_KW, preserveShortenedNames);
48
+
49
+ // Strip style-only names from subfamily
50
+ subfamilyName = subfamilyName
51
+ .replace(/\b(Italic|Slant|Slanted|Oblique|Backslant|Roman|Upright)\b/gi, '')
52
+ .replace(/\s+/g, ' ')
53
+ .trim();
54
+
55
+ // Strip subfamily from weightName to avoid duplication
56
+ if (subfamilyName !== '') {
57
+ weightName = weightName
58
+ .replace(`${subfamilyName} `, '')
59
+ .replace(` ${subfamilyName}`, '')
60
+ .trim();
61
+ }
62
+
63
+ // Re-derive title
64
+ let fontTitle = fullName.trim() || '';
65
+ fontTitle = formatFontTitle(fontTitle, preserveShortenedNames);
66
+
67
+ // Variable fonts get " VF" suffix
68
+ if (entry.variableFont) {
69
+ if (!fontTitle.toLowerCase().includes('vf')) {
70
+ fontTitle = fontTitle + ' VF';
71
+ }
72
+ subfamilyName = '';
73
+ }
74
+
75
+ // Add italic suffix if needed
76
+ if (!(entry.variableFont && fontTitle.toLowerCase().includes('italic'))) {
77
+ // Build a minimal font-like object for addItalicToFontTitle
78
+ const italicKW = entry.parsedMetadata.italicAngle !== 0 ? 'Italic' : '';
79
+ fontTitle = addItalicToFontTitle(
80
+ { opentype: { tables: { post: { italicAngle: entry.parsedMetadata.italicAngle || 0 } } } },
81
+ fontTitle,
82
+ italicKW,
83
+ entry.style,
84
+ preserveShortenedNames
85
+ );
86
+ }
87
+
88
+ const documentId = sanitizeForSanityId(fontTitle);
89
+
90
+ // Only return a new object if something actually changed
91
+ if (
92
+ entry.title === fontTitle &&
93
+ entry.documentId === documentId &&
94
+ entry.weightName === weightName &&
95
+ entry.subfamily === (entry.variableFont ? '' : (subfamilyName || 'Regular'))
96
+ ) {
97
+ return entry;
98
+ }
99
+
100
+ const finalSubfamily = entry.variableFont ? '' : (subfamilyName || 'Regular');
101
+
102
+ return {
103
+ ...entry,
104
+ title: fontTitle,
105
+ documentId,
106
+ weightName,
107
+ subfamily: finalSubfamily,
108
+ decisions: {
109
+ ...entry.decisions,
110
+ title: { ...entry.decisions.title, processed: fontTitle },
111
+ documentId: { ...entry.decisions.documentId, generated: documentId },
112
+ weightName: { ...entry.decisions.weightName, detected: weightName },
113
+ subfamily: { ...entry.decisions.subfamily, detected: finalSubfamily },
114
+ },
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Retitle all font entries in a plan. Returns a new fonts map.
120
+ * Runs collision detection after retitling.
121
+ *
122
+ * @param {object} fonts - plan.fonts map
123
+ * @param {boolean} preserveShortenedNames
124
+ * @param {string} typefaceTitle
125
+ * @returns {object} New fonts map with updated titles and _idConflict flags
126
+ */
127
+ export function retitleAllFonts(fonts, preserveShortenedNames, typefaceTitle) {
128
+ const updated = {};
129
+ for (const [tempId, entry] of Object.entries(fonts)) {
130
+ updated[tempId] = retitleFontEntry(entry, preserveShortenedNames, typefaceTitle);
131
+ }
132
+
133
+ // Run collision detection
134
+ const idMap = {};
135
+ for (const [tempId, font] of Object.entries(updated)) {
136
+ updated[tempId] = { ...font, _idConflict: false };
137
+ const docId = font.documentId;
138
+ if (!idMap[docId]) {
139
+ idMap[docId] = [tempId];
140
+ } else {
141
+ idMap[docId].push(tempId);
142
+ }
143
+ }
144
+
145
+ for (const ids of Object.values(idMap)) {
146
+ if (ids.length > 1) {
147
+ for (const id of ids) {
148
+ updated[id] = { ...updated[id], _idConflict: true };
149
+ }
150
+ }
151
+ }
152
+
153
+ return updated;
154
+ }