@liiift-studio/sanity-font-manager 2.3.10 → 2.3.12

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
@@ -37,6 +37,7 @@ __export(index_exports, {
37
37
  HtmlDescription: () => HtmlDescription,
38
38
  KeyValueInput: () => KeyValueInput,
39
39
  KeyValueReferenceInput: () => KeyValueReferenceInput,
40
+ NestedObjectArraySelector: () => NestedObjectArraySelector,
40
41
  PriceInput: () => PriceInput_default,
41
42
  PrimaryCollectionGeneratorTypeface: () => PrimaryCollectionGeneratorTypeface,
42
43
  RegenerateSubfamiliesComponent: () => RegenerateSubfamiliesComponent,
@@ -82,6 +83,7 @@ __export(index_exports, {
82
83
  updateFontPrices: () => updateFontPrices,
83
84
  updateTypefaceDocument: () => updateTypefaceDocument,
84
85
  uploadFontFiles: () => uploadFontFiles,
86
+ useNestedObjects: () => useNestedObjects,
85
87
  useSanityClient: () => useSanityClient
86
88
  });
87
89
  module.exports = __toCommonJS(index_exports);
@@ -4127,6 +4129,151 @@ var StyleCountInput = (props) => {
4127
4129
  return /* @__PURE__ */ import_react18.default.createElement(import_ui16.Text, { size: 1 }, count);
4128
4130
  };
4129
4131
 
4132
+ // src/components/NestedObjectArraySelector.jsx
4133
+ var import_react20 = __toESM(require("react"));
4134
+ var import_ui17 = require("@sanity/ui");
4135
+
4136
+ // src/hooks/useNestedObjects.js
4137
+ var import_react19 = require("react");
4138
+ var import_sanity15 = require("sanity");
4139
+ function useNestedObjects({
4140
+ sourceType,
4141
+ nestedField,
4142
+ titleField,
4143
+ valueField,
4144
+ filter = "",
4145
+ sortBy = ""
4146
+ }) {
4147
+ const client = (0, import_sanity15.useClient)({ apiVersion: "2023-01-01" });
4148
+ const [objects, setObjects] = (0, import_react19.useState)([]);
4149
+ const [loading, setLoading] = (0, import_react19.useState)(true);
4150
+ const [error, setError] = (0, import_react19.useState)(null);
4151
+ (0, import_react19.useEffect)(() => {
4152
+ if (!sourceType || !nestedField || !titleField || !valueField) {
4153
+ setError(new Error("Missing required configuration"));
4154
+ setLoading(false);
4155
+ return;
4156
+ }
4157
+ const fetchData = async () => {
4158
+ try {
4159
+ setLoading(true);
4160
+ setError(null);
4161
+ const filterClause = filter ? ` && ${filter}` : "";
4162
+ const query = `
4163
+ *[_type == "${sourceType}"${filterClause}] {
4164
+ "${nestedField}": ${nestedField}[] {
4165
+ "title": ${titleField},
4166
+ "value": ${valueField}
4167
+ }
4168
+ }
4169
+ `;
4170
+ const result = await client.fetch(query);
4171
+ if (!result || result.length === 0) {
4172
+ setObjects([]);
4173
+ setLoading(false);
4174
+ return;
4175
+ }
4176
+ const flattened = result.flatMap((doc) => doc[nestedField] || []);
4177
+ const uniqueMap = /* @__PURE__ */ new Map();
4178
+ flattened.forEach((item) => {
4179
+ if (item.value && item.title) uniqueMap.set(item.value, item);
4180
+ });
4181
+ let unique = Array.from(uniqueMap.values());
4182
+ if (sortBy) {
4183
+ const [sortField, sortOrder = "asc"] = sortBy.split(" ");
4184
+ unique = unique.sort((a, b) => {
4185
+ const aVal = a[sortField] || a.title;
4186
+ const bVal = b[sortField] || b.title;
4187
+ const comparison = aVal.localeCompare(bVal);
4188
+ return sortOrder === "desc" ? -comparison : comparison;
4189
+ });
4190
+ }
4191
+ setObjects(unique);
4192
+ } catch (err) {
4193
+ console.error("useNestedObjects fetch error:", err);
4194
+ setError(err);
4195
+ } finally {
4196
+ setLoading(false);
4197
+ }
4198
+ };
4199
+ fetchData();
4200
+ }, [sourceType, nestedField, titleField, valueField, filter, sortBy, client]);
4201
+ return { objects, loading, error };
4202
+ }
4203
+
4204
+ // src/components/NestedObjectArraySelector.jsx
4205
+ var import_sanity16 = require("sanity");
4206
+ var NestedObjectArraySelector = import_react20.default.forwardRef((props, ref) => {
4207
+ const { value = [], onChange, schemaType } = props;
4208
+ const options = (schemaType == null ? void 0 : schemaType.options) || {};
4209
+ const {
4210
+ sourceType,
4211
+ nestedField,
4212
+ titleField,
4213
+ valueField,
4214
+ filter = "",
4215
+ sortBy = "title asc",
4216
+ emptyMessage = "No options found",
4217
+ searchPlaceholder = "Search..."
4218
+ } = options;
4219
+ const [searchTerm, setSearchTerm] = (0, import_react20.useState)("");
4220
+ const { objects, loading, error } = useNestedObjects({ sourceType, nestedField, titleField, valueField, filter, sortBy });
4221
+ const filteredObjects = (0, import_react20.useMemo)(() => {
4222
+ if (!searchTerm) return objects;
4223
+ const lower = searchTerm.toLowerCase();
4224
+ return objects.filter((obj) => {
4225
+ var _a;
4226
+ return (_a = obj.title) == null ? void 0 : _a.toLowerCase().includes(lower);
4227
+ });
4228
+ }, [objects, searchTerm]);
4229
+ const handleToggle = (itemValue) => {
4230
+ const currentValue = value || [];
4231
+ const isSelected = currentValue.includes(itemValue);
4232
+ if (isSelected) {
4233
+ const newValue = currentValue.filter((v) => v !== itemValue);
4234
+ onChange(newValue.length > 0 ? (0, import_sanity16.set)(newValue) : (0, import_sanity16.unset)());
4235
+ } else {
4236
+ onChange((0, import_sanity16.set)([...currentValue, itemValue]));
4237
+ }
4238
+ };
4239
+ if (!sourceType || !nestedField || !titleField || !valueField) {
4240
+ return /* @__PURE__ */ import_react20.default.createElement(import_ui17.Card, { padding: 3, tone: "critical", border: true }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1 }, "Configuration error: Missing required options (sourceType, nestedField, titleField, or valueField)"));
4241
+ }
4242
+ if (loading) {
4243
+ return /* @__PURE__ */ import_react20.default.createElement(import_ui17.Card, { padding: 3, border: true }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Flex, { align: "center", justify: "center", padding: 4 }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Spinner, null), /* @__PURE__ */ import_react20.default.createElement(import_ui17.Box, { marginLeft: 3 }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1 }, "Loading options..."))));
4244
+ }
4245
+ if (error) {
4246
+ return /* @__PURE__ */ import_react20.default.createElement(import_ui17.Card, { padding: 3, tone: "critical", border: true }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1 }, "Error loading options: ", error.message));
4247
+ }
4248
+ if (objects.length === 0) {
4249
+ return /* @__PURE__ */ import_react20.default.createElement(import_ui17.Card, { padding: 3, tone: "transparent", border: true }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1, muted: true }, emptyMessage));
4250
+ }
4251
+ return /* @__PURE__ */ import_react20.default.createElement(import_ui17.Card, { padding: 0, border: true, ref }, objects.length > 5 && /* @__PURE__ */ import_react20.default.createElement(import_ui17.Box, { padding: 3, style: { borderBottom: "1px solid var(--card-border-color)" } }, /* @__PURE__ */ import_react20.default.createElement(
4252
+ "input",
4253
+ {
4254
+ type: "text",
4255
+ placeholder: searchPlaceholder,
4256
+ value: searchTerm,
4257
+ onChange: (e) => setSearchTerm(e.target.value),
4258
+ style: { width: "100%", padding: "8px 12px", border: "1px solid var(--card-border-color)", borderRadius: "4px", fontSize: "13px", fontFamily: "inherit" }
4259
+ }
4260
+ )), /* @__PURE__ */ import_react20.default.createElement(import_ui17.Stack, { space: 0 }, filteredObjects.length === 0 ? /* @__PURE__ */ import_react20.default.createElement(import_ui17.Box, { padding: 3 }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1, muted: true }, 'No results found for "', searchTerm, '"')) : filteredObjects.map((obj, index) => {
4261
+ const isSelected = value == null ? void 0 : value.includes(obj.value);
4262
+ const isLast = index === filteredObjects.length - 1;
4263
+ return /* @__PURE__ */ import_react20.default.createElement(
4264
+ import_ui17.Box,
4265
+ {
4266
+ key: obj.value,
4267
+ padding: 3,
4268
+ style: { borderBottom: isLast ? "none" : "1px solid var(--card-border-color)", cursor: "pointer", backgroundColor: isSelected ? "var(--card-muted-fg-color)" : "transparent", transition: "background-color 0.2s" },
4269
+ onClick: () => handleToggle(obj.value)
4270
+ },
4271
+ /* @__PURE__ */ import_react20.default.createElement(import_ui17.Flex, { align: "center", gap: 3 }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Checkbox, { checked: isSelected, readOnly: true, style: { pointerEvents: "none" } }), /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1, weight: isSelected ? "semibold" : "regular" }, obj.title))
4272
+ );
4273
+ })), (value == null ? void 0 : value.length) > 0 && /* @__PURE__ */ import_react20.default.createElement(import_ui17.Box, { padding: 2, paddingX: 3, style: { borderTop: "1px solid var(--card-border-color)", backgroundColor: "var(--card-muted-fg-color)" } }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1, muted: true }, value.length, " selected")));
4274
+ });
4275
+ NestedObjectArraySelector.displayName = "NestedObjectArraySelector";
4276
+
4130
4277
  // src/utils/getEmptyFontKit.js
4131
4278
  var fontkit7 = __toESM(require("fontkit"));
4132
4279
  var import_slugify4 = __toESM(require("slugify"));
@@ -6659,7 +6806,6 @@ var stylisticSetField = {
6659
6806
  };
6660
6807
 
6661
6808
  // src/schema/stylesField.js
6662
- var import_sanity_advanced_reference_array = require("sanity-advanced-reference-array");
6663
6809
  var field = (condition, def) => condition ? [def] : [];
6664
6810
  var fontsFilter = async ({ getClient, document, parent }) => {
6665
6811
  const client = getClient({ apiVersion: "2022-11-09" });
@@ -6734,7 +6880,6 @@ function createStylesField({
6734
6880
  title: "Fonts",
6735
6881
  name: "fonts",
6736
6882
  type: "array",
6737
- components: { input: import_sanity_advanced_reference_array.AdvancedRefArray },
6738
6883
  of: [{ type: "reference", weak: true, to: [{ type: "font" }] }],
6739
6884
  options: {
6740
6885
  sortable: true,
@@ -6811,7 +6956,6 @@ function createStylesField({
6811
6956
  title: "Fonts",
6812
6957
  name: "fonts",
6813
6958
  type: "array",
6814
- components: { input: import_sanity_advanced_reference_array.AdvancedRefArray },
6815
6959
  of: [{
6816
6960
  type: "reference",
6817
6961
  weak: true,
@@ -6824,7 +6968,6 @@ function createStylesField({
6824
6968
  title: "Variable Fonts",
6825
6969
  name: "variableFont",
6826
6970
  type: "array",
6827
- components: { input: import_sanity_advanced_reference_array.AdvancedRefArray },
6828
6971
  of: [{
6829
6972
  type: "reference",
6830
6973
  weak: true,
@@ -6855,7 +6998,6 @@ function createStylesField({
6855
6998
  title: "Collections",
6856
6999
  name: "collections",
6857
7000
  type: "array",
6858
- components: { input: import_sanity_advanced_reference_array.AdvancedRefArray },
6859
7001
  of: [{ type: "reference", weak: true, to: [{ type: "collection" }] }],
6860
7002
  options: { sortable: true },
6861
7003
  validation: (Rule) => Rule.unique()
@@ -6864,7 +7006,6 @@ function createStylesField({
6864
7006
  title: "Pairs",
6865
7007
  name: "pairs",
6866
7008
  type: "array",
6867
- components: { input: import_sanity_advanced_reference_array.AdvancedRefArray },
6868
7009
  of: [{ type: "reference", weak: true, to: [{ type: "pair" }] }],
6869
7010
  options: { sortable: true },
6870
7011
  validation: (Rule) => Rule.unique(),
@@ -6890,6 +7031,7 @@ function createStylesField({
6890
7031
  HtmlDescription,
6891
7032
  KeyValueInput,
6892
7033
  KeyValueReferenceInput,
7034
+ NestedObjectArraySelector,
6893
7035
  PriceInput,
6894
7036
  PrimaryCollectionGeneratorTypeface,
6895
7037
  RegenerateSubfamiliesComponent,
@@ -6935,5 +7077,6 @@ function createStylesField({
6935
7077
  updateFontPrices,
6936
7078
  updateTypefaceDocument,
6937
7079
  uploadFontFiles,
7080
+ useNestedObjects,
6938
7081
  useSanityClient
6939
7082
  });
package/dist/index.mjs CHANGED
@@ -4039,6 +4039,151 @@ var StyleCountInput = (props) => {
4039
4039
  return /* @__PURE__ */ React17.createElement(Text15, { size: 1 }, count);
4040
4040
  };
4041
4041
 
4042
+ // src/components/NestedObjectArraySelector.jsx
4043
+ import React18, { useMemo as useMemo5, useState as useState14 } from "react";
4044
+ import { Stack as Stack13, Card as Card6, Text as Text16, Checkbox, Box as Box6, Spinner as Spinner4, Flex as Flex11 } from "@sanity/ui";
4045
+
4046
+ // src/hooks/useNestedObjects.js
4047
+ import { useState as useState13, useEffect as useEffect7 } from "react";
4048
+ import { useClient as useClient2 } from "sanity";
4049
+ function useNestedObjects({
4050
+ sourceType,
4051
+ nestedField,
4052
+ titleField,
4053
+ valueField,
4054
+ filter = "",
4055
+ sortBy = ""
4056
+ }) {
4057
+ const client = useClient2({ apiVersion: "2023-01-01" });
4058
+ const [objects, setObjects] = useState13([]);
4059
+ const [loading, setLoading] = useState13(true);
4060
+ const [error, setError] = useState13(null);
4061
+ useEffect7(() => {
4062
+ if (!sourceType || !nestedField || !titleField || !valueField) {
4063
+ setError(new Error("Missing required configuration"));
4064
+ setLoading(false);
4065
+ return;
4066
+ }
4067
+ const fetchData = async () => {
4068
+ try {
4069
+ setLoading(true);
4070
+ setError(null);
4071
+ const filterClause = filter ? ` && ${filter}` : "";
4072
+ const query = `
4073
+ *[_type == "${sourceType}"${filterClause}] {
4074
+ "${nestedField}": ${nestedField}[] {
4075
+ "title": ${titleField},
4076
+ "value": ${valueField}
4077
+ }
4078
+ }
4079
+ `;
4080
+ const result = await client.fetch(query);
4081
+ if (!result || result.length === 0) {
4082
+ setObjects([]);
4083
+ setLoading(false);
4084
+ return;
4085
+ }
4086
+ const flattened = result.flatMap((doc) => doc[nestedField] || []);
4087
+ const uniqueMap = /* @__PURE__ */ new Map();
4088
+ flattened.forEach((item) => {
4089
+ if (item.value && item.title) uniqueMap.set(item.value, item);
4090
+ });
4091
+ let unique = Array.from(uniqueMap.values());
4092
+ if (sortBy) {
4093
+ const [sortField, sortOrder = "asc"] = sortBy.split(" ");
4094
+ unique = unique.sort((a, b) => {
4095
+ const aVal = a[sortField] || a.title;
4096
+ const bVal = b[sortField] || b.title;
4097
+ const comparison = aVal.localeCompare(bVal);
4098
+ return sortOrder === "desc" ? -comparison : comparison;
4099
+ });
4100
+ }
4101
+ setObjects(unique);
4102
+ } catch (err) {
4103
+ console.error("useNestedObjects fetch error:", err);
4104
+ setError(err);
4105
+ } finally {
4106
+ setLoading(false);
4107
+ }
4108
+ };
4109
+ fetchData();
4110
+ }, [sourceType, nestedField, titleField, valueField, filter, sortBy, client]);
4111
+ return { objects, loading, error };
4112
+ }
4113
+
4114
+ // src/components/NestedObjectArraySelector.jsx
4115
+ import { set as set8, unset as unset3 } from "sanity";
4116
+ var NestedObjectArraySelector = React18.forwardRef((props, ref) => {
4117
+ const { value = [], onChange, schemaType } = props;
4118
+ const options = (schemaType == null ? void 0 : schemaType.options) || {};
4119
+ const {
4120
+ sourceType,
4121
+ nestedField,
4122
+ titleField,
4123
+ valueField,
4124
+ filter = "",
4125
+ sortBy = "title asc",
4126
+ emptyMessage = "No options found",
4127
+ searchPlaceholder = "Search..."
4128
+ } = options;
4129
+ const [searchTerm, setSearchTerm] = useState14("");
4130
+ const { objects, loading, error } = useNestedObjects({ sourceType, nestedField, titleField, valueField, filter, sortBy });
4131
+ const filteredObjects = useMemo5(() => {
4132
+ if (!searchTerm) return objects;
4133
+ const lower = searchTerm.toLowerCase();
4134
+ return objects.filter((obj) => {
4135
+ var _a;
4136
+ return (_a = obj.title) == null ? void 0 : _a.toLowerCase().includes(lower);
4137
+ });
4138
+ }, [objects, searchTerm]);
4139
+ const handleToggle = (itemValue) => {
4140
+ const currentValue = value || [];
4141
+ const isSelected = currentValue.includes(itemValue);
4142
+ if (isSelected) {
4143
+ const newValue = currentValue.filter((v) => v !== itemValue);
4144
+ onChange(newValue.length > 0 ? set8(newValue) : unset3());
4145
+ } else {
4146
+ onChange(set8([...currentValue, itemValue]));
4147
+ }
4148
+ };
4149
+ if (!sourceType || !nestedField || !titleField || !valueField) {
4150
+ return /* @__PURE__ */ React18.createElement(Card6, { padding: 3, tone: "critical", border: true }, /* @__PURE__ */ React18.createElement(Text16, { size: 1 }, "Configuration error: Missing required options (sourceType, nestedField, titleField, or valueField)"));
4151
+ }
4152
+ if (loading) {
4153
+ return /* @__PURE__ */ React18.createElement(Card6, { padding: 3, border: true }, /* @__PURE__ */ React18.createElement(Flex11, { align: "center", justify: "center", padding: 4 }, /* @__PURE__ */ React18.createElement(Spinner4, null), /* @__PURE__ */ React18.createElement(Box6, { marginLeft: 3 }, /* @__PURE__ */ React18.createElement(Text16, { size: 1 }, "Loading options..."))));
4154
+ }
4155
+ if (error) {
4156
+ return /* @__PURE__ */ React18.createElement(Card6, { padding: 3, tone: "critical", border: true }, /* @__PURE__ */ React18.createElement(Text16, { size: 1 }, "Error loading options: ", error.message));
4157
+ }
4158
+ if (objects.length === 0) {
4159
+ return /* @__PURE__ */ React18.createElement(Card6, { padding: 3, tone: "transparent", border: true }, /* @__PURE__ */ React18.createElement(Text16, { size: 1, muted: true }, emptyMessage));
4160
+ }
4161
+ return /* @__PURE__ */ React18.createElement(Card6, { padding: 0, border: true, ref }, objects.length > 5 && /* @__PURE__ */ React18.createElement(Box6, { padding: 3, style: { borderBottom: "1px solid var(--card-border-color)" } }, /* @__PURE__ */ React18.createElement(
4162
+ "input",
4163
+ {
4164
+ type: "text",
4165
+ placeholder: searchPlaceholder,
4166
+ value: searchTerm,
4167
+ onChange: (e) => setSearchTerm(e.target.value),
4168
+ style: { width: "100%", padding: "8px 12px", border: "1px solid var(--card-border-color)", borderRadius: "4px", fontSize: "13px", fontFamily: "inherit" }
4169
+ }
4170
+ )), /* @__PURE__ */ React18.createElement(Stack13, { space: 0 }, filteredObjects.length === 0 ? /* @__PURE__ */ React18.createElement(Box6, { padding: 3 }, /* @__PURE__ */ React18.createElement(Text16, { size: 1, muted: true }, 'No results found for "', searchTerm, '"')) : filteredObjects.map((obj, index) => {
4171
+ const isSelected = value == null ? void 0 : value.includes(obj.value);
4172
+ const isLast = index === filteredObjects.length - 1;
4173
+ return /* @__PURE__ */ React18.createElement(
4174
+ Box6,
4175
+ {
4176
+ key: obj.value,
4177
+ padding: 3,
4178
+ style: { borderBottom: isLast ? "none" : "1px solid var(--card-border-color)", cursor: "pointer", backgroundColor: isSelected ? "var(--card-muted-fg-color)" : "transparent", transition: "background-color 0.2s" },
4179
+ onClick: () => handleToggle(obj.value)
4180
+ },
4181
+ /* @__PURE__ */ React18.createElement(Flex11, { align: "center", gap: 3 }, /* @__PURE__ */ React18.createElement(Checkbox, { checked: isSelected, readOnly: true, style: { pointerEvents: "none" } }), /* @__PURE__ */ React18.createElement(Text16, { size: 1, weight: isSelected ? "semibold" : "regular" }, obj.title))
4182
+ );
4183
+ })), (value == null ? void 0 : value.length) > 0 && /* @__PURE__ */ React18.createElement(Box6, { padding: 2, paddingX: 3, style: { borderTop: "1px solid var(--card-border-color)", backgroundColor: "var(--card-muted-fg-color)" } }, /* @__PURE__ */ React18.createElement(Text16, { size: 1, muted: true }, value.length, " selected")));
4184
+ });
4185
+ NestedObjectArraySelector.displayName = "NestedObjectArraySelector";
4186
+
4042
4187
  // src/utils/getEmptyFontKit.js
4043
4188
  import * as fontkit7 from "fontkit";
4044
4189
  import slugify4 from "slugify";
@@ -6571,7 +6716,6 @@ var stylisticSetField = {
6571
6716
  };
6572
6717
 
6573
6718
  // src/schema/stylesField.js
6574
- import { AdvancedRefArray } from "sanity-advanced-reference-array";
6575
6719
  var field = (condition, def) => condition ? [def] : [];
6576
6720
  var fontsFilter = async ({ getClient, document, parent }) => {
6577
6721
  const client = getClient({ apiVersion: "2022-11-09" });
@@ -6646,7 +6790,6 @@ function createStylesField({
6646
6790
  title: "Fonts",
6647
6791
  name: "fonts",
6648
6792
  type: "array",
6649
- components: { input: AdvancedRefArray },
6650
6793
  of: [{ type: "reference", weak: true, to: [{ type: "font" }] }],
6651
6794
  options: {
6652
6795
  sortable: true,
@@ -6723,7 +6866,6 @@ function createStylesField({
6723
6866
  title: "Fonts",
6724
6867
  name: "fonts",
6725
6868
  type: "array",
6726
- components: { input: AdvancedRefArray },
6727
6869
  of: [{
6728
6870
  type: "reference",
6729
6871
  weak: true,
@@ -6736,7 +6878,6 @@ function createStylesField({
6736
6878
  title: "Variable Fonts",
6737
6879
  name: "variableFont",
6738
6880
  type: "array",
6739
- components: { input: AdvancedRefArray },
6740
6881
  of: [{
6741
6882
  type: "reference",
6742
6883
  weak: true,
@@ -6767,7 +6908,6 @@ function createStylesField({
6767
6908
  title: "Collections",
6768
6909
  name: "collections",
6769
6910
  type: "array",
6770
- components: { input: AdvancedRefArray },
6771
6911
  of: [{ type: "reference", weak: true, to: [{ type: "collection" }] }],
6772
6912
  options: { sortable: true },
6773
6913
  validation: (Rule) => Rule.unique()
@@ -6776,7 +6916,6 @@ function createStylesField({
6776
6916
  title: "Pairs",
6777
6917
  name: "pairs",
6778
6918
  type: "array",
6779
- components: { input: AdvancedRefArray },
6780
6919
  of: [{ type: "reference", weak: true, to: [{ type: "pair" }] }],
6781
6920
  options: { sortable: true },
6782
6921
  validation: (Rule) => Rule.unique(),
@@ -6801,6 +6940,7 @@ export {
6801
6940
  HtmlDescription,
6802
6941
  KeyValueInput,
6803
6942
  KeyValueReferenceInput,
6943
+ NestedObjectArraySelector,
6804
6944
  PriceInput_default as PriceInput,
6805
6945
  PrimaryCollectionGeneratorTypeface,
6806
6946
  RegenerateSubfamiliesComponent,
@@ -6846,5 +6986,6 @@ export {
6846
6986
  updateFontPrices,
6847
6987
  updateTypefaceDocument,
6848
6988
  uploadFontFiles,
6989
+ useNestedObjects,
6849
6990
  useSanityClient
6850
6991
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liiift-studio/sanity-font-manager",
3
- "version": "2.3.10",
3
+ "version": "2.3.12",
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",
@@ -59,13 +59,7 @@
59
59
  "@sanity/icons": ">=3",
60
60
  "@sanity/ui": ">=3",
61
61
  "react": ">=18",
62
- "sanity": ">=3",
63
- "sanity-advanced-reference-array": ">=1"
64
- },
65
- "peerDependenciesMeta": {
66
- "sanity-advanced-reference-array": {
67
- "optional": true
68
- }
62
+ "sanity": ">=3"
69
63
  },
70
64
  "devDependencies": {
71
65
  "@sanity/icons": "^3",
@@ -0,0 +1,146 @@
1
+ // Generic checkbox-list selector for nested objects from Sanity documents — configured via schema options
2
+
3
+ import React, { useMemo, useState } from 'react';
4
+ import { Stack, Card, Text, Checkbox, Box, Spinner, Flex } from '@sanity/ui';
5
+ import { useNestedObjects } from '../hooks/useNestedObjects';
6
+ import { set, unset } from 'sanity';
7
+
8
+ /**
9
+ * Sanity input component that renders a searchable checkbox list of items fetched
10
+ * from nested arrays inside Sanity documents.
11
+ *
12
+ * Configure via schema options:
13
+ * ```js
14
+ * components: { input: NestedObjectArraySelector },
15
+ * options: {
16
+ * sourceType: 'licenseGroup', // document type to query
17
+ * nestedField: 'sections', // array field to extract
18
+ * titleField: 'title', // GROQ expression for display text
19
+ * valueField: 'slug.current', // GROQ expression for stored value
20
+ * filter: 'state == "published"', // optional GROQ filter
21
+ * sortBy: 'title asc', // optional sort field + direction
22
+ * emptyMessage: 'No options found', // optional empty state text
23
+ * searchPlaceholder: 'Search...', // optional search input placeholder
24
+ * }
25
+ * ```
26
+ */
27
+ export const NestedObjectArraySelector = React.forwardRef((props, ref) => {
28
+ const { value = [], onChange, schemaType } = props;
29
+
30
+ const options = schemaType?.options || {};
31
+ const {
32
+ sourceType,
33
+ nestedField,
34
+ titleField,
35
+ valueField,
36
+ filter = '',
37
+ sortBy = 'title asc',
38
+ emptyMessage = 'No options found',
39
+ searchPlaceholder = 'Search...',
40
+ } = options;
41
+
42
+ const [searchTerm, setSearchTerm] = useState('');
43
+ const { objects, loading, error } = useNestedObjects({ sourceType, nestedField, titleField, valueField, filter, sortBy });
44
+
45
+ const filteredObjects = useMemo(() => {
46
+ if (!searchTerm) return objects;
47
+ const lower = searchTerm.toLowerCase();
48
+ return objects.filter(obj => obj.title?.toLowerCase().includes(lower));
49
+ }, [objects, searchTerm]);
50
+
51
+ const handleToggle = (itemValue) => {
52
+ const currentValue = value || [];
53
+ const isSelected = currentValue.includes(itemValue);
54
+ if (isSelected) {
55
+ const newValue = currentValue.filter(v => v !== itemValue);
56
+ onChange(newValue.length > 0 ? set(newValue) : unset());
57
+ } else {
58
+ onChange(set([...currentValue, itemValue]));
59
+ }
60
+ };
61
+
62
+ if (!sourceType || !nestedField || !titleField || !valueField) {
63
+ return (
64
+ <Card padding={3} tone="critical" border>
65
+ <Text size={1}>Configuration error: Missing required options (sourceType, nestedField, titleField, or valueField)</Text>
66
+ </Card>
67
+ );
68
+ }
69
+
70
+ if (loading) {
71
+ return (
72
+ <Card padding={3} border>
73
+ <Flex align="center" justify="center" padding={4}>
74
+ <Spinner />
75
+ <Box marginLeft={3}><Text size={1}>Loading options...</Text></Box>
76
+ </Flex>
77
+ </Card>
78
+ );
79
+ }
80
+
81
+ if (error) {
82
+ return (
83
+ <Card padding={3} tone="critical" border>
84
+ <Text size={1}>Error loading options: {error.message}</Text>
85
+ </Card>
86
+ );
87
+ }
88
+
89
+ if (objects.length === 0) {
90
+ return (
91
+ <Card padding={3} tone="transparent" border>
92
+ <Text size={1} muted>{emptyMessage}</Text>
93
+ </Card>
94
+ );
95
+ }
96
+
97
+ return (
98
+ <Card padding={0} border ref={ref}>
99
+ {objects.length > 5 && (
100
+ <Box padding={3} style={{ borderBottom: '1px solid var(--card-border-color)' }}>
101
+ <input
102
+ type="text"
103
+ placeholder={searchPlaceholder}
104
+ value={searchTerm}
105
+ onChange={(e) => setSearchTerm(e.target.value)}
106
+ style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--card-border-color)', borderRadius: '4px', fontSize: '13px', fontFamily: 'inherit' }}
107
+ />
108
+ </Box>
109
+ )}
110
+
111
+ <Stack space={0}>
112
+ {filteredObjects.length === 0 ? (
113
+ <Box padding={3}>
114
+ <Text size={1} muted>No results found for "{searchTerm}"</Text>
115
+ </Box>
116
+ ) : (
117
+ filteredObjects.map((obj, index) => {
118
+ const isSelected = value?.includes(obj.value);
119
+ const isLast = index === filteredObjects.length - 1;
120
+ return (
121
+ <Box
122
+ key={obj.value}
123
+ padding={3}
124
+ style={{ borderBottom: isLast ? 'none' : '1px solid var(--card-border-color)', cursor: 'pointer', backgroundColor: isSelected ? 'var(--card-muted-fg-color)' : 'transparent', transition: 'background-color 0.2s' }}
125
+ onClick={() => handleToggle(obj.value)}
126
+ >
127
+ <Flex align="center" gap={3}>
128
+ <Checkbox checked={isSelected} readOnly style={{ pointerEvents: 'none' }} />
129
+ <Text size={1} weight={isSelected ? 'semibold' : 'regular'}>{obj.title}</Text>
130
+ </Flex>
131
+ </Box>
132
+ );
133
+ })
134
+ )}
135
+ </Stack>
136
+
137
+ {value?.length > 0 && (
138
+ <Box padding={2} paddingX={3} style={{ borderTop: '1px solid var(--card-border-color)', backgroundColor: 'var(--card-muted-fg-color)' }}>
139
+ <Text size={1} muted>{value.length} selected</Text>
140
+ </Box>
141
+ )}
142
+ </Card>
143
+ );
144
+ });
145
+
146
+ NestedObjectArraySelector.displayName = 'NestedObjectArraySelector';
@@ -0,0 +1,92 @@
1
+ // Hook for fetching nested objects from Sanity documents — used by NestedObjectArraySelector
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { useClient } from 'sanity';
5
+
6
+ /**
7
+ * Fetches and flattens nested arrays from a Sanity document type into a list of selectable items.
8
+ * @param {Object} config
9
+ * @param {string} config.sourceType - Document type to query (e.g. 'licenseGroup')
10
+ * @param {string} config.nestedField - Array field to extract from (e.g. 'sections')
11
+ * @param {string} config.titleField - GROQ expression for display text (e.g. 'title')
12
+ * @param {string} config.valueField - GROQ expression for stored value (e.g. 'slug.current')
13
+ * @param {string} [config.filter] - Optional GROQ filter clause (e.g. 'state == "published"')
14
+ * @param {string} [config.sortBy] - Optional sort field and direction (e.g. 'title asc')
15
+ * @returns {{ objects: Array, loading: boolean, error: Error|null }}
16
+ */
17
+ export function useNestedObjects({
18
+ sourceType,
19
+ nestedField,
20
+ titleField,
21
+ valueField,
22
+ filter = '',
23
+ sortBy = '',
24
+ }) {
25
+ const client = useClient({ apiVersion: '2023-01-01' });
26
+ const [objects, setObjects] = useState([]);
27
+ const [loading, setLoading] = useState(true);
28
+ const [error, setError] = useState(null);
29
+
30
+ useEffect(() => {
31
+ if (!sourceType || !nestedField || !titleField || !valueField) {
32
+ setError(new Error('Missing required configuration'));
33
+ setLoading(false);
34
+ return;
35
+ }
36
+
37
+ const fetchData = async () => {
38
+ try {
39
+ setLoading(true);
40
+ setError(null);
41
+
42
+ const filterClause = filter ? ` && ${filter}` : '';
43
+ const query = `
44
+ *[_type == "${sourceType}"${filterClause}] {
45
+ "${nestedField}": ${nestedField}[] {
46
+ "title": ${titleField},
47
+ "value": ${valueField}
48
+ }
49
+ }
50
+ `;
51
+
52
+ const result = await client.fetch(query);
53
+
54
+ if (!result || result.length === 0) {
55
+ setObjects([]);
56
+ setLoading(false);
57
+ return;
58
+ }
59
+
60
+ // Flatten nested arrays from all documents and deduplicate by value
61
+ const flattened = result.flatMap(doc => doc[nestedField] || []);
62
+ const uniqueMap = new Map();
63
+ flattened.forEach(item => {
64
+ if (item.value && item.title) uniqueMap.set(item.value, item);
65
+ });
66
+
67
+ let unique = Array.from(uniqueMap.values());
68
+
69
+ if (sortBy) {
70
+ const [sortField, sortOrder = 'asc'] = sortBy.split(' ');
71
+ unique = unique.sort((a, b) => {
72
+ const aVal = a[sortField] || a.title;
73
+ const bVal = b[sortField] || b.title;
74
+ const comparison = aVal.localeCompare(bVal);
75
+ return sortOrder === 'desc' ? -comparison : comparison;
76
+ });
77
+ }
78
+
79
+ setObjects(unique);
80
+ } catch (err) {
81
+ console.error('useNestedObjects fetch error:', err);
82
+ setError(err);
83
+ } finally {
84
+ setLoading(false);
85
+ }
86
+ };
87
+
88
+ fetchData();
89
+ }, [sourceType, nestedField, titleField, valueField, filter, sortBy, client]);
90
+
91
+ return { objects, loading, error };
92
+ }
package/src/index.js CHANGED
@@ -17,9 +17,11 @@ export { VariableInstanceReferencesInput } from './components/VariableInstanceRe
17
17
  export { PrimaryCollectionGeneratorTypeface } from './components/PrimaryCollectionGeneratorTypeface.jsx';
18
18
  export { SetOTF } from './components/SetOTF.jsx';
19
19
  export { StyleCountInput } from './components/StyleCountInput.jsx';
20
+ export { NestedObjectArraySelector } from './components/NestedObjectArraySelector.jsx';
20
21
 
21
22
  // Hooks
22
23
  export { useSanityClient } from './hooks/useSanityClient.js';
24
+ export { useNestedObjects } from './hooks/useNestedObjects.js';
23
25
 
24
26
  // Core utilities
25
27
  export { default as generateCssFile } from './utils/generateCssFile.js';
@@ -1,5 +1,4 @@
1
1
  // Sanity schema factory function for the Styles object field — call createStylesField(options) to generate the field definition for a typeface document
2
- import { AdvancedRefArray } from 'sanity-advanced-reference-array';
3
2
  import { RegenerateSubfamiliesComponent } from '../components/RegenerateSubfamiliesComponent.jsx';
4
3
 
5
4
  // Conditionally includes a field definition in an array
@@ -102,7 +101,6 @@ export function createStylesField({
102
101
  title: 'Fonts',
103
102
  name: 'fonts',
104
103
  type: 'array',
105
- components: { input: AdvancedRefArray },
106
104
  of: [{ type: 'reference', weak: true, to: [{ type: 'font' }] }],
107
105
  options: {
108
106
  sortable: true,
@@ -181,7 +179,6 @@ export function createStylesField({
181
179
  title: 'Fonts',
182
180
  name: 'fonts',
183
181
  type: 'array',
184
- components: { input: AdvancedRefArray },
185
182
  of: [{
186
183
  type: 'reference',
187
184
  weak: true,
@@ -194,7 +191,6 @@ export function createStylesField({
194
191
  title: 'Variable Fonts',
195
192
  name: 'variableFont',
196
193
  type: 'array',
197
- components: { input: AdvancedRefArray },
198
194
  of: [{
199
195
  type: 'reference',
200
196
  weak: true,
@@ -222,7 +218,6 @@ export function createStylesField({
222
218
  title: 'Collections',
223
219
  name: 'collections',
224
220
  type: 'array',
225
- components: { input: AdvancedRefArray },
226
221
  of: [{ type: 'reference', weak: true, to: [{ type: 'collection' }] }],
227
222
  options: { sortable: true },
228
223
  validation: Rule => Rule.unique(),
@@ -231,7 +226,6 @@ export function createStylesField({
231
226
  title: 'Pairs',
232
227
  name: 'pairs',
233
228
  type: 'array',
234
- components: { input: AdvancedRefArray },
235
229
  of: [{ type: 'reference', weak: true, to: [{ type: 'pair' }] }],
236
230
  options: { sortable: true },
237
231
  validation: Rule => Rule.unique(),