@liiift-studio/sanity-font-manager 2.3.0 → 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 +92 -0
- package/dist/index.js +79 -1
- package/dist/index.mjs +78 -1
- package/package.json +1 -1
- package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -0
- package/src/components/SingleUploaderTool.jsx +1 -1
- package/src/index.js +1 -0
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,78 @@ 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
|
+
### `KeyValueInput`
|
|
114
|
+
|
|
115
|
+
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.
|
|
116
|
+
|
|
117
|
+
```jsx
|
|
118
|
+
import { KeyValueInput } from '@liiift-studio/sanity-font-manager';
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
name: 'aliases',
|
|
122
|
+
type: 'array',
|
|
123
|
+
of: [{ type: 'object', fields: [{ name: 'key', type: 'string' }, { name: 'value', type: 'string' }] }],
|
|
124
|
+
components: { input: KeyValueInput },
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `KeyValueReferenceInput`
|
|
129
|
+
|
|
130
|
+
Generic key-value editor where keys are plain strings and values are weak Sanity document references. Supports searching by title via a popover picker, add/remove/reorder, and an optional `topActions` slot for action buttons above the list.
|
|
131
|
+
|
|
132
|
+
| Prop | Type | Description |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `fetchReferences` | `async (client, doc) => [{_id, title}]` | Async function that returns candidate references for the picker. Receives the Sanity client and the current document. |
|
|
135
|
+
| `topActions` | `ReactNode` | Optional content rendered above the key-value rows (e.g. autofill buttons). |
|
|
136
|
+
| `referenceType` | `string` | Document type for the created weak references (default: `'font'`). |
|
|
137
|
+
|
|
138
|
+
```jsx
|
|
139
|
+
import { KeyValueReferenceInput } from '@liiift-studio/sanity-font-manager';
|
|
140
|
+
|
|
141
|
+
{
|
|
142
|
+
name: 'instanceMap',
|
|
143
|
+
type: 'array',
|
|
144
|
+
of: [{ type: 'object', fields: [{ name: 'key', type: 'string' }, { name: 'value', type: 'reference', weak: true, to: [{ type: 'font' }] }] }],
|
|
145
|
+
components: { input: KeyValueReferenceInput },
|
|
146
|
+
// Pass props via options or a wrapper component:
|
|
147
|
+
options: {
|
|
148
|
+
fetchReferences: async (client, doc) => client.fetch('*[_type == "font"]{_id, title}'),
|
|
149
|
+
referenceType: 'font',
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `VariableInstanceReferencesInput`
|
|
155
|
+
|
|
156
|
+
Font-specific wrapper around `KeyValueReferenceInput` for mapping variable font instance names to their matching static font documents. Provides:
|
|
157
|
+
|
|
158
|
+
- A picker filtered to fonts sharing the same `typefaceName`, excluding variable fonts
|
|
159
|
+
- **Autofill with Matching** — calls `parseVariableFontInstances` to match instance names to existing font documents by weight/style heuristics
|
|
160
|
+
- **Autofill Keys Only** — populates instance name keys from the font's `variableInstances` metadata without resolving references
|
|
161
|
+
- Autofill buttons are shown only when the document is a variable font with parsed instance data
|
|
162
|
+
- Replace/merge confirmation dialog when pairs already exist
|
|
163
|
+
|
|
164
|
+
```jsx
|
|
165
|
+
import { VariableInstanceReferencesInput } from '@liiift-studio/sanity-font-manager';
|
|
166
|
+
|
|
167
|
+
{
|
|
168
|
+
name: 'variableInstanceReferences',
|
|
169
|
+
title: 'Variable Font Instances',
|
|
170
|
+
type: 'array',
|
|
171
|
+
hidden: ({ parent }) => !parent.variableFont,
|
|
172
|
+
of: [
|
|
173
|
+
{
|
|
174
|
+
type: 'object',
|
|
175
|
+
fields: [
|
|
176
|
+
{ name: 'key', type: 'string', title: 'Instance Name' },
|
|
177
|
+
{ name: 'value', type: 'reference', weak: true, to: [{ type: 'font' }], title: 'Matching Font' },
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
components: { input: VariableInstanceReferencesInput },
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
94
185
|
### `StatusDisplay`
|
|
95
186
|
|
|
96
187
|
Shared status bar used by all components. Shows `Status: [message]` in green on success and red on error, with an optional `action` element slot on the far right (used for the advanced toggle in `SingleUploaderTool`).
|
|
@@ -209,6 +300,7 @@ const client = useSanityClient();
|
|
|
209
300
|
| `glyphCount` | `number` | Total number of glyphs |
|
|
210
301
|
| `opentypeFeatures` | `object` | Available OpenType feature tags |
|
|
211
302
|
| `characterSet` | `object` | Array of Unicode code points covered by the font |
|
|
303
|
+
| `variableInstanceReferences` | `array<object>` | Maps variable font instance names to static font document references — `[{ key: string, value: reference }]` |
|
|
212
304
|
|
|
213
305
|
### Typeface document (`typeface`)
|
|
214
306
|
|
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,
|
|
@@ -2914,7 +2915,7 @@ var SingleUploaderTool = (props) => {
|
|
|
2914
2915
|
}
|
|
2915
2916
|
)
|
|
2916
2917
|
}
|
|
2917
|
-
), renderFontSection("ttf"), status === "ready" && (fileInput == null ? void 0 : fileInput.ttf) && /* @__PURE__ */ import_react8.default.createElement(import_ui7.Grid, { columns: [2], gap: 2 }, /* @__PURE__ */ import_react8.default.createElement(
|
|
2918
|
+
), renderFontSection("ttf"), status === "ready" && (fileInput == null ? void 0 : fileInput.ttf) && /* @__PURE__ */ import_react8.default.createElement(import_ui7.Grid, { columns: [1, 2], gap: 2 }, /* @__PURE__ */ import_react8.default.createElement(
|
|
2918
2919
|
import_ui7.Button,
|
|
2919
2920
|
{
|
|
2920
2921
|
mode: "ghost",
|
|
@@ -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
|
@@ -2835,7 +2835,7 @@ var SingleUploaderTool = (props) => {
|
|
|
2835
2835
|
}
|
|
2836
2836
|
)
|
|
2837
2837
|
}
|
|
2838
|
-
), renderFontSection("ttf"), status === "ready" && (fileInput == null ? void 0 : fileInput.ttf) && /* @__PURE__ */ React7.createElement(Grid3, { columns: [2], gap: 2 }, /* @__PURE__ */ React7.createElement(
|
|
2838
|
+
), renderFontSection("ttf"), status === "ready" && (fileInput == null ? void 0 : fileInput.ttf) && /* @__PURE__ */ React7.createElement(Grid3, { columns: [1, 2], gap: 2 }, /* @__PURE__ */ React7.createElement(
|
|
2839
2839
|
Button5,
|
|
2840
2840
|
{
|
|
2841
2841
|
mode: "ghost",
|
|
@@ -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.
|
|
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';
|