@liiift-studio/sanity-font-manager 2.3.1 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -75,6 +75,25 @@ One-click generator for Full Family, Uprights, Italics, and Subfamily collection
75
75
  import { GenerateCollectionsPairsComponent } from '@liiift-studio/sanity-font-manager';
76
76
  ```
77
77
 
78
+ ### `PrimaryCollectionGeneratorTypeface`
79
+
80
+ One-click generator for a single full-family collection that includes all fonts linked to the typeface. Prepends the new collection to the existing `styles.collections` array — non-destructive. Uses `SANITY_STUDIO_DEFAULT_COLLECTION_PRICE` as the default price, falling back to `100`.
81
+
82
+ Wire it up on a `string` field in the typeface schema:
83
+
84
+ ```jsx
85
+ import { PrimaryCollectionGeneratorTypeface } from '@liiift-studio/sanity-font-manager';
86
+
87
+ {
88
+ name: 'generateCollectionGroup',
89
+ type: 'string',
90
+ title: 'Generate Full Family Collection',
91
+ description: 'Generate a collection that includes all the styles from this typeface.',
92
+ components: { input: PrimaryCollectionGeneratorTypeface },
93
+ hidden: ({ parent }) => !parent?.styles?.fonts?.length,
94
+ }
95
+ ```
96
+
78
97
  ### `FontScriptUploaderComponent`
79
98
 
80
99
  Script-aware uploader for per-script font file variants (Latin, Arabic, Hebrew, etc.) stored in `scriptFileInput` on the font document.
@@ -91,6 +110,39 @@ Updates and re-links existing script font variant references on font documents
91
110
 
92
111
  Recalculates and patches the `subfamily` field on all fonts linked to a typeface, based on the typeface's defined subfamily groups — without re-uploading any files.
93
112
 
113
+ ### `SetOTF`
114
+
115
+ Detects which configured OpenType feature keys are supported by the typeface's first linked font. Reads `opentypeFeatures.chars` from the font document (populated by `generateFontData`) and patches the `features` array on the field. Shows a feature count when features are detected, and clear error messages when font data is missing.
116
+
117
+ Wire it up on the `openType` object field in the typeface schema:
118
+
119
+ ```jsx
120
+ import { SetOTF } from '@liiift-studio/sanity-font-manager';
121
+
122
+ {
123
+ name: 'openType',
124
+ type: 'object',
125
+ components: { input: SetOTF },
126
+ options: { collapsible: true },
127
+ fields: [ /* feature fields — each with a `feature` string e.g. 'liga', 'smcp' */ ],
128
+ }
129
+ ```
130
+
131
+ ### `StyleCountInput`
132
+
133
+ Displays the total number of font styles (static + variable) linked to a typeface. Reads `styles.fonts` and `styles.variableFont` arrays from the form context. Useful as a read-only display field in the typeface schema.
134
+
135
+ ```jsx
136
+ import { StyleCountInput } from '@liiift-studio/sanity-font-manager';
137
+
138
+ {
139
+ name: 'styleCount',
140
+ type: 'number',
141
+ readOnly: true,
142
+ components: { input: StyleCountInput },
143
+ }
144
+ ```
145
+
94
146
  ### `KeyValueInput`
95
147
 
96
148
  Generic ordered key-value editor where both keys and values are plain strings. Supports add, remove, and reorder (up/down arrows). Values are stored as an array of `{ key, value }` objects.
package/dist/index.js CHANGED
@@ -36,11 +36,14 @@ __export(index_exports, {
36
36
  KeyValueInput: () => KeyValueInput,
37
37
  KeyValueReferenceInput: () => KeyValueReferenceInput,
38
38
  PriceInput: () => PriceInput_default,
39
+ PrimaryCollectionGeneratorTypeface: () => PrimaryCollectionGeneratorTypeface,
39
40
  RegenerateSubfamiliesComponent: () => RegenerateSubfamiliesComponent,
40
41
  SCRIPTS: () => SCRIPTS,
41
42
  SCRIPTS_OBJECT: () => SCRIPTS_OBJECT,
43
+ SetOTF: () => SetOTF,
42
44
  SingleUploaderTool: () => SingleUploaderTool,
43
45
  StatusDisplay: () => StatusDisplay_default,
46
+ StyleCountInput: () => StyleCountInput,
44
47
  UpdateScriptsComponent: () => UpdateScriptsComponent,
45
48
  UploadButton: () => UploadButton_default,
46
49
  UploadScriptsComponent: () => UploadScriptsComponent,
@@ -3964,6 +3967,156 @@ function VariableInstanceReferencesInput(props) {
3964
3967
  ));
3965
3968
  }
3966
3969
 
3970
+ // src/components/PrimaryCollectionGeneratorTypeface.jsx
3971
+ var import_react16 = __toESM(require("react"));
3972
+ var import_ui14 = require("@sanity/ui");
3973
+ var import_sanity12 = require("sanity");
3974
+ var import_nanoid10 = require("nanoid");
3975
+ var PrimaryCollectionGeneratorTypeface = () => {
3976
+ const client = useSanityClient();
3977
+ const [status, setStatus] = (0, import_react16.useState)("ready");
3978
+ const [ready, setReady] = (0, import_react16.useState)(true);
3979
+ const [price, setPrice] = (0, import_react16.useState)(
3980
+ process.env.SANITY_STUDIO_DEFAULT_COLLECTION_PRICE || 100
3981
+ );
3982
+ const fonts = (0, import_sanity12.useFormValue)(["styles", "fonts"]);
3983
+ const title = (0, import_sanity12.useFormValue)(["title"]);
3984
+ const preferredStyle = (0, import_sanity12.useFormValue)(["preferredStyle"]);
3985
+ const docId = (0, import_sanity12.useFormValue)(["_id"]);
3986
+ const styles = (0, import_sanity12.useFormValue)(["styles"]);
3987
+ const generateCollection = (0, import_react16.useCallback)(async () => {
3988
+ setStatus("Generating collection...");
3989
+ setReady(false);
3990
+ let id = title.toLowerCase().replace(/\s+/g, "-").slice(0, 200);
3991
+ if (!id.includes("collection")) id += "-collection";
3992
+ const colTitle = id.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
3993
+ const collectionDoc = {
3994
+ _key: (0, import_nanoid10.nanoid)(),
3995
+ _id: id,
3996
+ title: colTitle,
3997
+ slug: { _type: "slug", current: id },
3998
+ price: Number(price) || 0,
3999
+ fonts: Object.values(fonts),
4000
+ preferredStyle,
4001
+ _type: "collection",
4002
+ type: "collection"
4003
+ };
4004
+ try {
4005
+ const sanityCollection = await client.createOrReplace(collectionDoc);
4006
+ const collections = styles.collections || [];
4007
+ await client.patch(docId).setIfMissing({ styles: {} }).set({
4008
+ styles: {
4009
+ ...styles,
4010
+ collections: [{
4011
+ _type: "reference",
4012
+ _key: (0, import_nanoid10.nanoid)(),
4013
+ _ref: sanityCollection._id,
4014
+ _weak: true
4015
+ }, ...collections]
4016
+ }
4017
+ }).commit();
4018
+ setStatus("Collection generated");
4019
+ } catch (err) {
4020
+ console.error("Error creating collection:", err.message);
4021
+ setStatus("Error generating collection");
4022
+ }
4023
+ setReady(true);
4024
+ }, [docId, fonts, price, preferredStyle, styles, title, client]);
4025
+ if (!title || !fonts) return null;
4026
+ return /* @__PURE__ */ import_react16.default.createElement(import_ui14.Stack, { space: 2 }, /* @__PURE__ */ import_react16.default.createElement(StatusDisplay_default, { status, error: false }), /* @__PURE__ */ import_react16.default.createElement(import_ui14.Card, { border: true, padding: 2, shadow: 1, radius: 2 }, ready ? /* @__PURE__ */ import_react16.default.createElement(import_ui14.Stack, { space: 3 }, /* @__PURE__ */ import_react16.default.createElement(import_ui14.Flex, { align: "center", gap: 2, marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ import_react16.default.createElement(import_ui14.Text, { size: 1, muted: true }, "Price"), /* @__PURE__ */ import_react16.default.createElement(import_ui14.Text, { size: 1, muted: true }, "$"), /* @__PURE__ */ import_react16.default.createElement(
4027
+ "input",
4028
+ {
4029
+ value: price,
4030
+ onChange: (e) => setPrice(e.target.value),
4031
+ type: "number",
4032
+ style: { textAlign: "end", padding: "5px", maxWidth: "75px" }
4033
+ }
4034
+ ), /* @__PURE__ */ import_react16.default.createElement(import_ui14.Text, { size: 1, muted: true }, "per full family")), /* @__PURE__ */ import_react16.default.createElement(
4035
+ import_ui14.Button,
4036
+ {
4037
+ mode: "ghost",
4038
+ tone: "primary",
4039
+ style: { width: "100%" },
4040
+ onClick: generateCollection,
4041
+ text: "Generate Full Family Collection"
4042
+ }
4043
+ )) : /* @__PURE__ */ import_react16.default.createElement(import_ui14.Flex, { align: "center", justify: "center", gap: 3, padding: 4 }, /* @__PURE__ */ import_react16.default.createElement(import_ui14.Spinner, null), /* @__PURE__ */ import_react16.default.createElement(import_ui14.Text, { muted: true, size: 1 }, status))));
4044
+ };
4045
+
4046
+ // src/components/SetOTF.jsx
4047
+ var import_react17 = require("react");
4048
+ var import_sanity13 = require("sanity");
4049
+ var import_ui15 = require("@sanity/ui");
4050
+ var SetOTF = (props) => {
4051
+ var _a, _b;
4052
+ const { onChange, value = {} } = props;
4053
+ const client = useSanityClient();
4054
+ const stylesObject = (0, import_sanity13.useFormValue)(["styles"]);
4055
+ const [message, setMessage] = (0, import_react17.useState)("");
4056
+ const detect = async () => {
4057
+ var _a2, _b2, _c;
4058
+ if (!((_a2 = stylesObject == null ? void 0 : stylesObject.fonts) == null ? void 0 : _a2.length)) {
4059
+ setMessage("Error: No fonts found in styles. Please add at least one font first.");
4060
+ setTimeout(() => setMessage(""), 5e3);
4061
+ return;
4062
+ }
4063
+ const fontRef = (_b2 = stylesObject.fonts[0]) == null ? void 0 : _b2._ref;
4064
+ if (!fontRef) {
4065
+ setMessage("Error: Invalid font reference in styles.");
4066
+ setTimeout(() => setMessage(""), 5e3);
4067
+ return;
4068
+ }
4069
+ try {
4070
+ const font = await client.fetch('*[_type == "font" && _id == $id][0]', { id: fontRef });
4071
+ if (!font) {
4072
+ setMessage("Error: Could not find the referenced font.");
4073
+ setTimeout(() => setMessage(""), 5e3);
4074
+ return;
4075
+ }
4076
+ if (!((_c = font.opentypeFeatures) == null ? void 0 : _c.chars)) {
4077
+ setMessage(`Error: No OpenType feature data found in "${font.title || "this font"}". Generate font data first.`);
4078
+ setTimeout(() => setMessage(""), 5e3);
4079
+ return;
4080
+ }
4081
+ const features = [];
4082
+ Object.keys(value).forEach((key) => {
4083
+ var _a3;
4084
+ if (key !== "features" && ((_a3 = value[key]) == null ? void 0 : _a3.feature)) {
4085
+ const requiredFeatures = value[key].feature.split(" ");
4086
+ const approved = requiredFeatures.every((v) => font.opentypeFeatures.chars.includes(v));
4087
+ if (approved) features.push(key);
4088
+ }
4089
+ });
4090
+ onChange((0, import_sanity13.set)({ ...value, features }));
4091
+ setMessage(`Features detected: ${features.length ? features.join(", ") : "none"}.`);
4092
+ setTimeout(() => setMessage(""), 5e3);
4093
+ } catch (err) {
4094
+ setMessage("Error detecting features. Check the console for details.");
4095
+ console.error("SetOTF detect error:", err);
4096
+ }
4097
+ };
4098
+ return /* @__PURE__ */ React.createElement(import_ui15.Stack, { className: "openType" }, ((_a = value == null ? void 0 : value.features) == null ? void 0 : _a.length) > 0 && /* @__PURE__ */ React.createElement(import_ui15.Text, { muted: true, size: 1, style: { marginBottom: "0.5rem" } }, "Number of features: ", value.features.length), !!((_b = stylesObject == null ? void 0 : stylesObject.fonts) == null ? void 0 : _b.length) && /* @__PURE__ */ React.createElement(
4099
+ import_ui15.Button,
4100
+ {
4101
+ text: "Detect OTF",
4102
+ mode: "ghost",
4103
+ onClick: detect,
4104
+ style: { borderRadius: "0 3px 0 0", marginBottom: "1rem" }
4105
+ }
4106
+ ), !!message && /* @__PURE__ */ React.createElement(import_ui15.Text, { muted: true, size: 1 }, /* @__PURE__ */ React.createElement("br", null), message, /* @__PURE__ */ React.createElement("br", null), /* @__PURE__ */ React.createElement("br", null)), props.renderDefault(props));
4107
+ };
4108
+
4109
+ // src/components/StyleCountInput.jsx
4110
+ var import_react18 = __toESM(require("react"));
4111
+ var import_ui16 = require("@sanity/ui");
4112
+ var import_sanity14 = require("sanity");
4113
+ var StyleCountInput = (props) => {
4114
+ const styles = (0, import_sanity14.useFormValue)(["styles", "fonts"]) || [];
4115
+ const vfStyles = (0, import_sanity14.useFormValue)(["styles", "variableFont"]) || [];
4116
+ const count = styles.length + vfStyles.length;
4117
+ return /* @__PURE__ */ import_react18.default.createElement(import_ui16.Text, { size: 1 }, count);
4118
+ };
4119
+
3967
4120
  // src/utils/getEmptyFontKit.js
3968
4121
  var fontkit7 = __toESM(require("fontkit"));
3969
4122
  var import_slugify4 = __toESM(require("slugify"));
@@ -4050,11 +4203,14 @@ var readFontFile2 = (file) => {
4050
4203
  KeyValueInput,
4051
4204
  KeyValueReferenceInput,
4052
4205
  PriceInput,
4206
+ PrimaryCollectionGeneratorTypeface,
4053
4207
  RegenerateSubfamiliesComponent,
4054
4208
  SCRIPTS,
4055
4209
  SCRIPTS_OBJECT,
4210
+ SetOTF,
4056
4211
  SingleUploaderTool,
4057
4212
  StatusDisplay,
4213
+ StyleCountInput,
4058
4214
  UpdateScriptsComponent,
4059
4215
  UploadButton,
4060
4216
  UploadScriptsComponent,