@liiift-studio/sanity-font-manager 2.3.1 → 2.3.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/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.
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ __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,
@@ -3964,6 +3965,82 @@ function VariableInstanceReferencesInput(props) {
3964
3965
  ));
3965
3966
  }
3966
3967
 
3968
+ // src/components/PrimaryCollectionGeneratorTypeface.jsx
3969
+ var import_react16 = __toESM(require("react"));
3970
+ var import_ui14 = require("@sanity/ui");
3971
+ var import_sanity12 = require("sanity");
3972
+ var import_nanoid10 = require("nanoid");
3973
+ var PrimaryCollectionGeneratorTypeface = () => {
3974
+ const client = useSanityClient();
3975
+ const [status, setStatus] = (0, import_react16.useState)("ready");
3976
+ const [ready, setReady] = (0, import_react16.useState)(true);
3977
+ const [price, setPrice] = (0, import_react16.useState)(
3978
+ process.env.SANITY_STUDIO_DEFAULT_COLLECTION_PRICE || 100
3979
+ );
3980
+ const fonts = (0, import_sanity12.useFormValue)(["styles", "fonts"]);
3981
+ const title = (0, import_sanity12.useFormValue)(["title"]);
3982
+ const preferredStyle = (0, import_sanity12.useFormValue)(["preferredStyle"]);
3983
+ const docId = (0, import_sanity12.useFormValue)(["_id"]);
3984
+ const styles = (0, import_sanity12.useFormValue)(["styles"]);
3985
+ const generateCollection = (0, import_react16.useCallback)(async () => {
3986
+ setStatus("Generating collection...");
3987
+ setReady(false);
3988
+ let id = title.toLowerCase().replace(/\s+/g, "-").slice(0, 200);
3989
+ if (!id.includes("collection")) id += "-collection";
3990
+ const colTitle = id.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
3991
+ const collectionDoc = {
3992
+ _key: (0, import_nanoid10.nanoid)(),
3993
+ _id: id,
3994
+ title: colTitle,
3995
+ slug: { _type: "slug", current: id },
3996
+ price: Number(price) || 0,
3997
+ fonts: Object.values(fonts),
3998
+ preferredStyle,
3999
+ _type: "collection",
4000
+ type: "collection"
4001
+ };
4002
+ try {
4003
+ const sanityCollection = await client.createOrReplace(collectionDoc);
4004
+ const collections = styles.collections || [];
4005
+ await client.patch(docId).setIfMissing({ styles: {} }).set({
4006
+ styles: {
4007
+ ...styles,
4008
+ collections: [{
4009
+ _type: "reference",
4010
+ _key: (0, import_nanoid10.nanoid)(),
4011
+ _ref: sanityCollection._id,
4012
+ _weak: true
4013
+ }, ...collections]
4014
+ }
4015
+ }).commit();
4016
+ setStatus("Collection generated");
4017
+ } catch (err) {
4018
+ console.error("Error creating collection:", err.message);
4019
+ setStatus("Error generating collection");
4020
+ }
4021
+ setReady(true);
4022
+ }, [docId, fonts, price, preferredStyle, styles, title, client]);
4023
+ if (!title || !fonts) return null;
4024
+ 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(
4025
+ "input",
4026
+ {
4027
+ value: price,
4028
+ onChange: (e) => setPrice(e.target.value),
4029
+ type: "number",
4030
+ style: { textAlign: "end", padding: "5px", maxWidth: "75px" }
4031
+ }
4032
+ ), /* @__PURE__ */ import_react16.default.createElement(import_ui14.Text, { size: 1, muted: true }, "per full family")), /* @__PURE__ */ import_react16.default.createElement(
4033
+ import_ui14.Button,
4034
+ {
4035
+ mode: "ghost",
4036
+ tone: "primary",
4037
+ style: { width: "100%" },
4038
+ onClick: generateCollection,
4039
+ text: "Generate Full Family Collection"
4040
+ }
4041
+ )) : /* @__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))));
4042
+ };
4043
+
3967
4044
  // src/utils/getEmptyFontKit.js
3968
4045
  var fontkit7 = __toESM(require("fontkit"));
3969
4046
  var import_slugify4 = __toESM(require("slugify"));
@@ -4050,6 +4127,7 @@ var readFontFile2 = (file) => {
4050
4127
  KeyValueInput,
4051
4128
  KeyValueReferenceInput,
4052
4129
  PriceInput,
4130
+ PrimaryCollectionGeneratorTypeface,
4053
4131
  RegenerateSubfamiliesComponent,
4054
4132
  SCRIPTS,
4055
4133
  SCRIPTS_OBJECT,
package/dist/index.mjs CHANGED
@@ -3885,6 +3885,82 @@ function VariableInstanceReferencesInput(props) {
3885
3885
  ));
3886
3886
  }
3887
3887
 
3888
+ // src/components/PrimaryCollectionGeneratorTypeface.jsx
3889
+ import React15, { useCallback as useCallback10, useState as useState11 } from "react";
3890
+ import { Stack as Stack11, Flex as Flex10, Text as Text13, Button as Button12, Card as Card5, Spinner as Spinner3 } from "@sanity/ui";
3891
+ import { useFormValue as useFormValue10 } from "sanity";
3892
+ import { nanoid as nanoid10 } from "nanoid";
3893
+ var PrimaryCollectionGeneratorTypeface = () => {
3894
+ const client = useSanityClient();
3895
+ const [status, setStatus] = useState11("ready");
3896
+ const [ready, setReady] = useState11(true);
3897
+ const [price, setPrice] = useState11(
3898
+ process.env.SANITY_STUDIO_DEFAULT_COLLECTION_PRICE || 100
3899
+ );
3900
+ const fonts = useFormValue10(["styles", "fonts"]);
3901
+ const title = useFormValue10(["title"]);
3902
+ const preferredStyle = useFormValue10(["preferredStyle"]);
3903
+ const docId = useFormValue10(["_id"]);
3904
+ const styles = useFormValue10(["styles"]);
3905
+ const generateCollection = useCallback10(async () => {
3906
+ setStatus("Generating collection...");
3907
+ setReady(false);
3908
+ let id = title.toLowerCase().replace(/\s+/g, "-").slice(0, 200);
3909
+ if (!id.includes("collection")) id += "-collection";
3910
+ const colTitle = id.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
3911
+ const collectionDoc = {
3912
+ _key: nanoid10(),
3913
+ _id: id,
3914
+ title: colTitle,
3915
+ slug: { _type: "slug", current: id },
3916
+ price: Number(price) || 0,
3917
+ fonts: Object.values(fonts),
3918
+ preferredStyle,
3919
+ _type: "collection",
3920
+ type: "collection"
3921
+ };
3922
+ try {
3923
+ const sanityCollection = await client.createOrReplace(collectionDoc);
3924
+ const collections = styles.collections || [];
3925
+ await client.patch(docId).setIfMissing({ styles: {} }).set({
3926
+ styles: {
3927
+ ...styles,
3928
+ collections: [{
3929
+ _type: "reference",
3930
+ _key: nanoid10(),
3931
+ _ref: sanityCollection._id,
3932
+ _weak: true
3933
+ }, ...collections]
3934
+ }
3935
+ }).commit();
3936
+ setStatus("Collection generated");
3937
+ } catch (err) {
3938
+ console.error("Error creating collection:", err.message);
3939
+ setStatus("Error generating collection");
3940
+ }
3941
+ setReady(true);
3942
+ }, [docId, fonts, price, preferredStyle, styles, title, client]);
3943
+ if (!title || !fonts) return null;
3944
+ return /* @__PURE__ */ React15.createElement(Stack11, { space: 2 }, /* @__PURE__ */ React15.createElement(StatusDisplay_default, { status, error: false }), /* @__PURE__ */ React15.createElement(Card5, { border: true, padding: 2, shadow: 1, radius: 2 }, ready ? /* @__PURE__ */ React15.createElement(Stack11, { space: 3 }, /* @__PURE__ */ React15.createElement(Flex10, { align: "center", gap: 2, marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ React15.createElement(Text13, { size: 1, muted: true }, "Price"), /* @__PURE__ */ React15.createElement(Text13, { size: 1, muted: true }, "$"), /* @__PURE__ */ React15.createElement(
3945
+ "input",
3946
+ {
3947
+ value: price,
3948
+ onChange: (e) => setPrice(e.target.value),
3949
+ type: "number",
3950
+ style: { textAlign: "end", padding: "5px", maxWidth: "75px" }
3951
+ }
3952
+ ), /* @__PURE__ */ React15.createElement(Text13, { size: 1, muted: true }, "per full family")), /* @__PURE__ */ React15.createElement(
3953
+ Button12,
3954
+ {
3955
+ mode: "ghost",
3956
+ tone: "primary",
3957
+ style: { width: "100%" },
3958
+ onClick: generateCollection,
3959
+ text: "Generate Full Family Collection"
3960
+ }
3961
+ )) : /* @__PURE__ */ React15.createElement(Flex10, { align: "center", justify: "center", gap: 3, padding: 4 }, /* @__PURE__ */ React15.createElement(Spinner3, null), /* @__PURE__ */ React15.createElement(Text13, { muted: true, size: 1 }, status))));
3962
+ };
3963
+
3888
3964
  // src/utils/getEmptyFontKit.js
3889
3965
  import * as fontkit7 from "fontkit";
3890
3966
  import slugify4 from "slugify";
@@ -3970,6 +4046,7 @@ export {
3970
4046
  KeyValueInput,
3971
4047
  KeyValueReferenceInput,
3972
4048
  PriceInput_default as PriceInput,
4049
+ PrimaryCollectionGeneratorTypeface,
3973
4050
  RegenerateSubfamiliesComponent,
3974
4051
  SCRIPTS,
3975
4052
  SCRIPTS_OBJECT,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liiift-studio/sanity-font-manager",
3
- "version": "2.3.1",
3
+ "version": "2.3.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",
@@ -0,0 +1,116 @@
1
+ // Generates a single primary full-family collection from a typeface's linked fonts and prepends it to the existing collections array
2
+
3
+ import React, { useCallback, useState } from 'react';
4
+ import { Stack, Flex, Text, Button, Card, Spinner } from '@sanity/ui';
5
+ import { useFormValue } from 'sanity';
6
+ import { nanoid } from 'nanoid';
7
+
8
+ import { useSanityClient } from '../hooks/useSanityClient';
9
+ import StatusDisplay from './StatusDisplay';
10
+
11
+ /**
12
+ * Generates a full-family collection document from the typeface's linked fonts
13
+ * and prepends it to the existing styles.collections array.
14
+ */
15
+ export const PrimaryCollectionGeneratorTypeface = () => {
16
+ const client = useSanityClient();
17
+
18
+ const [status, setStatus] = useState('ready');
19
+ const [ready, setReady] = useState(true);
20
+ const [price, setPrice] = useState(
21
+ process.env.SANITY_STUDIO_DEFAULT_COLLECTION_PRICE || 100
22
+ );
23
+
24
+ const fonts = useFormValue(['styles', 'fonts']);
25
+ const title = useFormValue(['title']);
26
+ const preferredStyle = useFormValue(['preferredStyle']);
27
+ const docId = useFormValue(['_id']);
28
+ const styles = useFormValue(['styles']);
29
+
30
+ /** Creates or replaces the full-family collection document and prepends it to the typeface's collections array. */
31
+ const generateCollection = useCallback(async () => {
32
+ setStatus('Generating collection...');
33
+ setReady(false);
34
+
35
+ let id = title.toLowerCase().replace(/\s+/g, '-').slice(0, 200);
36
+ if (!id.includes('collection')) id += '-collection';
37
+
38
+ const colTitle = id.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
39
+
40
+ const collectionDoc = {
41
+ _key: nanoid(),
42
+ _id: id,
43
+ title: colTitle,
44
+ slug: { _type: 'slug', current: id },
45
+ price: Number(price) || 0,
46
+ fonts: Object.values(fonts),
47
+ preferredStyle: preferredStyle,
48
+ _type: 'collection',
49
+ type: 'collection',
50
+ };
51
+
52
+ try {
53
+ const sanityCollection = await client.createOrReplace(collectionDoc);
54
+ const collections = styles.collections || [];
55
+
56
+ await client.patch(docId)
57
+ .setIfMissing({ styles: {} })
58
+ .set({
59
+ styles: {
60
+ ...styles,
61
+ collections: [{
62
+ _type: 'reference',
63
+ _key: nanoid(),
64
+ _ref: sanityCollection._id,
65
+ _weak: true,
66
+ }, ...collections],
67
+ },
68
+ })
69
+ .commit();
70
+
71
+ setStatus('Collection generated');
72
+ } catch (err) {
73
+ console.error('Error creating collection:', err.message);
74
+ setStatus('Error generating collection');
75
+ }
76
+
77
+ setReady(true);
78
+ }, [docId, fonts, price, preferredStyle, styles, title, client]);
79
+
80
+ if (!title || !fonts) return null;
81
+
82
+ return (
83
+ <Stack space={2}>
84
+ <StatusDisplay status={status} error={false} />
85
+ <Card border padding={2} shadow={1} radius={2}>
86
+ {ready ? (
87
+ <Stack space={3}>
88
+ <Flex align="center" gap={2} marginTop={1} marginBottom={1}>
89
+ <Text size={1} muted>Price</Text>
90
+ <Text size={1} muted>$</Text>
91
+ <input
92
+ value={price}
93
+ onChange={(e) => setPrice(e.target.value)}
94
+ type="number"
95
+ style={{ textAlign: 'end', padding: '5px', maxWidth: '75px' }}
96
+ />
97
+ <Text size={1} muted>per full family</Text>
98
+ </Flex>
99
+ <Button
100
+ mode="ghost"
101
+ tone="primary"
102
+ style={{ width: '100%' }}
103
+ onClick={generateCollection}
104
+ text="Generate Full Family Collection"
105
+ />
106
+ </Stack>
107
+ ) : (
108
+ <Flex align="center" justify="center" gap={3} padding={4}>
109
+ <Spinner />
110
+ <Text muted size={1}>{status}</Text>
111
+ </Flex>
112
+ )}
113
+ </Card>
114
+ </Stack>
115
+ );
116
+ };
package/src/index.js CHANGED
@@ -14,6 +14,7 @@ export { default as UploadButton } from './components/UploadButton.jsx';
14
14
  export { KeyValueInput } from './components/KeyValueInput.jsx';
15
15
  export { KeyValueReferenceInput } from './components/KeyValueReferenceInput.jsx';
16
16
  export { VariableInstanceReferencesInput } from './components/VariableInstanceReferencesInput.jsx';
17
+ export { PrimaryCollectionGeneratorTypeface } from './components/PrimaryCollectionGeneratorTypeface.jsx';
17
18
 
18
19
  // Hooks
19
20
  export { useSanityClient } from './hooks/useSanityClient.js';