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

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.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",
@@ -3499,6 +3499,392 @@ var UploadButton = forwardRef(({ handleUpload }, ref) => {
3499
3499
  UploadButton.displayName = "UploadButton";
3500
3500
  var UploadButton_default = UploadButton;
3501
3501
 
3502
+ // src/components/KeyValueInput.jsx
3503
+ import React12, { useState as useState8, useCallback as useCallback7 } from "react";
3504
+ import { Button as Button9, Grid as Grid5, Stack as Stack8, TextInput as TextInput2 } from "@sanity/ui";
3505
+ import { AddIcon, ArrowDownIcon, ArrowUpIcon, TrashIcon as TrashIcon3 } from "@sanity/icons";
3506
+ import { set as set4 } from "sanity";
3507
+ function KeyValueInput({ value = [], onChange }) {
3508
+ const [pairs, setPairs] = useState8(value);
3509
+ const handlePairChange = useCallback7((index, field, fieldValue) => {
3510
+ const updatedPairs = pairs.map((pair, idx) => idx === index ? { ...pair, [field]: fieldValue } : pair);
3511
+ setPairs(updatedPairs);
3512
+ onChange(set4(updatedPairs));
3513
+ }, [pairs, onChange]);
3514
+ const handleAddPair = useCallback7(() => {
3515
+ const newPair = { key: "", value: "", _key: Math.random().toString(36).substr(2, 9) };
3516
+ const updatedPairs = [...pairs, newPair];
3517
+ setPairs(updatedPairs);
3518
+ onChange(set4(updatedPairs));
3519
+ }, [pairs, onChange]);
3520
+ const handleRemovePair = useCallback7((index) => {
3521
+ const updatedPairs = pairs.filter((_, idx) => idx !== index);
3522
+ setPairs(updatedPairs);
3523
+ onChange(set4(updatedPairs));
3524
+ }, [pairs, onChange]);
3525
+ const handleMoveUp = useCallback7((index) => {
3526
+ if (index === 0) return;
3527
+ const updatedPairs = [...pairs];
3528
+ [updatedPairs[index], updatedPairs[index - 1]] = [updatedPairs[index - 1], updatedPairs[index]];
3529
+ setPairs(updatedPairs);
3530
+ onChange(set4(updatedPairs));
3531
+ }, [pairs, onChange]);
3532
+ const handleMoveDown = useCallback7((index) => {
3533
+ if (index === pairs.length - 1) return;
3534
+ const updatedPairs = [...pairs];
3535
+ [updatedPairs[index], updatedPairs[index + 1]] = [updatedPairs[index + 1], updatedPairs[index]];
3536
+ setPairs(updatedPairs);
3537
+ onChange(set4(updatedPairs));
3538
+ }, [pairs, onChange]);
3539
+ return /* @__PURE__ */ React12.createElement(Stack8, { space: 3 }, pairs.map((pair, index) => /* @__PURE__ */ React12.createElement(Grid5, { className: "manualButtonWrap", columns: [2], key: index, gap: 0, style: { position: "relative" } }, /* @__PURE__ */ React12.createElement("div", { style: { position: "absolute", height: "100%", top: "0", left: "-10px", width: "min-content", transform: "translate(-100%, 0%)" } }, /* @__PURE__ */ React12.createElement("button", { className: "manualButton manualButtonUp", style: { fontSize: "15px", height: "50%" }, onClick: () => handleMoveUp(index) }, /* @__PURE__ */ React12.createElement(ArrowUpIcon, null)), /* @__PURE__ */ React12.createElement("button", { className: "manualButton manualButtonDown", style: { fontSize: "15px", height: "50%" }, onClick: () => handleMoveDown(index) }, /* @__PURE__ */ React12.createElement(ArrowDownIcon, null))), /* @__PURE__ */ React12.createElement(
3540
+ TextInput2,
3541
+ {
3542
+ value: pair.key,
3543
+ onChange: (e) => handlePairChange(index, "key", e.target.value),
3544
+ placeholder: "Key"
3545
+ }
3546
+ ), /* @__PURE__ */ React12.createElement("div", { style: { marginLeft: "-1px" } }, /* @__PURE__ */ React12.createElement(
3547
+ TextInput2,
3548
+ {
3549
+ value: pair.value,
3550
+ onChange: (e) => handlePairChange(index, "value", e.target.value),
3551
+ placeholder: "Value"
3552
+ }
3553
+ )), /* @__PURE__ */ React12.createElement(
3554
+ "button",
3555
+ {
3556
+ className: "manualButton",
3557
+ onClick: () => handleRemovePair(index),
3558
+ style: { position: "absolute", top: "0", right: "-10px", transform: "translate(100%, 0%)" }
3559
+ },
3560
+ /* @__PURE__ */ React12.createElement(TrashIcon3, null)
3561
+ ))), /* @__PURE__ */ React12.createElement(Button9, { tone: "primary", onClick: handleAddPair, icon: AddIcon, text: "Add Row" }));
3562
+ }
3563
+
3564
+ // src/components/KeyValueReferenceInput.jsx
3565
+ import React13, { useState as useState9, useCallback as useCallback8, useEffect as useEffect6 } from "react";
3566
+ import { Button as Button10, Stack as Stack9, TextInput as TextInput3, Box as Box4, Card as Card4, Flex as Flex8, Text as Text11, Dialog, Menu as Menu2, MenuButton as MenuButton2, MenuItem as MenuItem2, Autocomplete } from "@sanity/ui";
3567
+ import { AddIcon as AddIcon2, ArrowDownIcon as ArrowDownIcon2, ArrowUpIcon as ArrowUpIcon2, TrashIcon as TrashIcon4, SyncIcon, EllipsisHorizontalIcon } from "@sanity/icons";
3568
+ import { set as set5, useFormValue as useFormValue8 } from "sanity";
3569
+ import { nanoid as nanoid8 } from "nanoid";
3570
+ function KeyValueReferenceInput(props) {
3571
+ var _a, _b, _c, _d, _e, _f, _g, _h;
3572
+ const { value = [], onChange, schemaType, referenceType, fetchReferences, topActions } = props;
3573
+ const [pairs, setPairs] = useState9(value);
3574
+ const [referenceData, setReferenceData] = useState9({});
3575
+ const [isDialogOpen, setIsDialogOpen] = useState9(false);
3576
+ const [editingIndex, setEditingIndex] = useState9(null);
3577
+ const [availableReferences, setAvailableReferences] = useState9([]);
3578
+ const sanityClient = useSanityClient();
3579
+ const formDocument = useFormValue8([]);
3580
+ useEffect6(() => {
3581
+ const refIds = pairs.filter((p) => {
3582
+ var _a2;
3583
+ return (_a2 = p.value) == null ? void 0 : _a2._ref;
3584
+ }).map((p) => p.value._ref);
3585
+ if (refIds.length === 0) return;
3586
+ if (!sanityClient) {
3587
+ const fallback = {};
3588
+ refIds.forEach((id) => {
3589
+ fallback[id] = `Reference (${id.substring(0, 6)}...)`;
3590
+ });
3591
+ setReferenceData(fallback);
3592
+ return;
3593
+ }
3594
+ sanityClient.fetch(`*[_id in $ids]{_id, title}`, { ids: refIds }).then((result) => {
3595
+ const map = {};
3596
+ result.forEach((item) => {
3597
+ map[item._id] = item.title;
3598
+ });
3599
+ setReferenceData(map);
3600
+ }).catch((err) => {
3601
+ console.error("Error fetching reference data:", err);
3602
+ const fallback = {};
3603
+ refIds.forEach((id) => {
3604
+ fallback[id] = `Reference (${id.substring(0, 6)}...)`;
3605
+ });
3606
+ setReferenceData(fallback);
3607
+ });
3608
+ }, [pairs, sanityClient]);
3609
+ const handlePairChange = useCallback8((index, field, fieldValue) => {
3610
+ const updatedPairs = pairs.map((pair, idx) => idx === index ? { ...pair, [field]: fieldValue } : pair);
3611
+ setPairs(updatedPairs);
3612
+ onChange(set5(updatedPairs));
3613
+ }, [pairs, onChange]);
3614
+ const handleAddPair = useCallback8(() => {
3615
+ const updatedPairs = [...pairs, { key: "", value: null, _key: nanoid8() }];
3616
+ setPairs(updatedPairs);
3617
+ onChange(set5(updatedPairs));
3618
+ }, [pairs, onChange]);
3619
+ const handleRemovePair = useCallback8((index) => {
3620
+ const updatedPairs = pairs.filter((_, idx) => idx !== index);
3621
+ setPairs(updatedPairs);
3622
+ onChange(set5(updatedPairs));
3623
+ }, [pairs, onChange]);
3624
+ const handleMoveUp = useCallback8((index) => {
3625
+ if (index === 0) return;
3626
+ const updatedPairs = [...pairs];
3627
+ [updatedPairs[index], updatedPairs[index - 1]] = [updatedPairs[index - 1], updatedPairs[index]];
3628
+ setPairs(updatedPairs);
3629
+ onChange(set5(updatedPairs));
3630
+ }, [pairs, onChange]);
3631
+ const handleMoveDown = useCallback8((index) => {
3632
+ if (index === pairs.length - 1) return;
3633
+ const updatedPairs = [...pairs];
3634
+ [updatedPairs[index], updatedPairs[index + 1]] = [updatedPairs[index + 1], updatedPairs[index]];
3635
+ setPairs(updatedPairs);
3636
+ onChange(set5(updatedPairs));
3637
+ }, [pairs, onChange]);
3638
+ const openReferenceSelector = useCallback8(async (index) => {
3639
+ setEditingIndex(index);
3640
+ if (!sanityClient) {
3641
+ console.error("KeyValueReferenceInput: Sanity client not available");
3642
+ return;
3643
+ }
3644
+ try {
3645
+ let refs;
3646
+ if (fetchReferences) {
3647
+ refs = await fetchReferences(sanityClient, formDocument);
3648
+ } else if (referenceType) {
3649
+ refs = await sanityClient.fetch(`*[_type == $type]{_id, title}`, { type: referenceType });
3650
+ } else {
3651
+ console.warn("KeyValueReferenceInput: provide a fetchReferences prop or referenceType");
3652
+ refs = [];
3653
+ }
3654
+ setAvailableReferences(refs);
3655
+ setIsDialogOpen(true);
3656
+ } catch (err) {
3657
+ console.error("Error fetching available references:", err);
3658
+ }
3659
+ }, [sanityClient, fetchReferences, referenceType, formDocument]);
3660
+ const closeDialog = useCallback8(() => {
3661
+ setIsDialogOpen(false);
3662
+ setEditingIndex(null);
3663
+ }, []);
3664
+ const handleReferenceSelect = useCallback8((reference) => {
3665
+ if (editingIndex === null) return;
3666
+ handlePairChange(editingIndex, "value", { _type: "reference", _ref: reference._id, _weak: true });
3667
+ closeDialog();
3668
+ }, [editingIndex, handlePairChange, closeDialog]);
3669
+ const referenceOptions = availableReferences.map((ref) => ({ value: ref._id, title: ref.title }));
3670
+ const keyField = (_d = (_c = (_b = (_a = schemaType == null ? void 0 : schemaType.options) == null ? void 0 : _a.of) == null ? void 0 : _b[0]) == null ? void 0 : _c.fields) == null ? void 0 : _d.find((f) => f.name === "key");
3671
+ const valueField = (_h = (_g = (_f = (_e = schemaType == null ? void 0 : schemaType.options) == null ? void 0 : _e.of) == null ? void 0 : _f[0]) == null ? void 0 : _g.fields) == null ? void 0 : _h.find((f) => f.name === "value");
3672
+ const keyTitle = (keyField == null ? void 0 : keyField.title) || "Key";
3673
+ const valueTitle = (valueField == null ? void 0 : valueField.title) || "Value";
3674
+ const keyPlaceholder = (keyField == null ? void 0 : keyField.placeholder) || "Enter key";
3675
+ const pickerLabel = referenceType || valueTitle.toLowerCase();
3676
+ return /* @__PURE__ */ React13.createElement(Stack9, { space: 3 }, topActions && /* @__PURE__ */ React13.createElement(Box4, { paddingBottom: 2 }, topActions), /* @__PURE__ */ React13.createElement(Box4, null, /* @__PURE__ */ React13.createElement(Stack9, { space: 2 }, pairs.map((pair, index) => {
3677
+ var _a2;
3678
+ return /* @__PURE__ */ React13.createElement(Box4, { key: index, style: { position: "relative" } }, /* @__PURE__ */ React13.createElement("div", { style: { position: "absolute", height: "100%", top: "0", left: "-5px", width: "min-content", transform: "translate(-100%, 0%)" } }, /* @__PURE__ */ React13.createElement("button", { className: "manualButton manualButtonUp", style: { fontSize: "15px", height: "50%" }, onClick: () => handleMoveUp(index) }, /* @__PURE__ */ React13.createElement(ArrowUpIcon2, null)), /* @__PURE__ */ React13.createElement("button", { className: "manualButton manualButtonDown", style: { fontSize: "15px", height: "50%" }, onClick: () => handleMoveDown(index) }, /* @__PURE__ */ React13.createElement(ArrowDownIcon2, null))), /* @__PURE__ */ React13.createElement(Flex8, { gap: 2, align: "flex-start" }, /* @__PURE__ */ React13.createElement(Box4, { flex: 1 }, /* @__PURE__ */ React13.createElement(
3679
+ TextInput3,
3680
+ {
3681
+ value: pair.key,
3682
+ onChange: (e) => handlePairChange(index, "key", e.target.value),
3683
+ placeholder: keyPlaceholder
3684
+ }
3685
+ )), /* @__PURE__ */ React13.createElement(Box4, { flex: 1, style: { minHeight: "100%" } }, ((_a2 = pair.value) == null ? void 0 : _a2._ref) ? /* @__PURE__ */ React13.createElement(Card4, { className: "referenceCard", radius: 2, tone: "primary", style: { paddingLeft: "1rem", height: "fit-content" } }, /* @__PURE__ */ React13.createElement(Flex8, { align: "center", justify: "space-between" }, /* @__PURE__ */ React13.createElement(
3686
+ Text11,
3687
+ {
3688
+ size: 2,
3689
+ style: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: "90%" }
3690
+ },
3691
+ referenceData[pair.value._ref] || "Loading..."
3692
+ ), /* @__PURE__ */ React13.createElement(
3693
+ MenuButton2,
3694
+ {
3695
+ button: /* @__PURE__ */ React13.createElement(Button10, { icon: EllipsisHorizontalIcon, mode: "bleed", title: "Options" }),
3696
+ id: `ref-options-${index}`,
3697
+ menu: /* @__PURE__ */ React13.createElement(Menu2, null, /* @__PURE__ */ React13.createElement(MenuItem2, { tone: "critical", icon: TrashIcon4, text: "Remove", onClick: () => handlePairChange(index, "value", null) }), /* @__PURE__ */ React13.createElement(MenuItem2, { icon: SyncIcon, text: "Replace", onClick: () => openReferenceSelector(index) })),
3698
+ popover: { portal: true, tone: "default", placement: "left" }
3699
+ }
3700
+ ))) : /* @__PURE__ */ React13.createElement(
3701
+ Box4,
3702
+ {
3703
+ padding: 2,
3704
+ style: { minHeight: "100%", border: "1px dashed #ccc", borderRadius: "4px", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer" },
3705
+ onClick: () => openReferenceSelector(index)
3706
+ },
3707
+ /* @__PURE__ */ React13.createElement(Text11, { muted: true, size: 2 }, "Click to select a ", pickerLabel)
3708
+ ))), /* @__PURE__ */ React13.createElement(
3709
+ "button",
3710
+ {
3711
+ className: "manualButton",
3712
+ onClick: () => handleRemovePair(index),
3713
+ style: { position: "absolute", top: "0", right: "-7px", transform: "translate(100%, 0%)" }
3714
+ },
3715
+ /* @__PURE__ */ React13.createElement(TrashIcon4, null)
3716
+ ));
3717
+ }))), /* @__PURE__ */ React13.createElement(Button10, { tone: "primary", mode: "ghost", onClick: handleAddPair, icon: AddIcon2, text: `Add ${keyTitle}` }), isDialogOpen && /* @__PURE__ */ React13.createElement(
3718
+ Dialog,
3719
+ {
3720
+ header: `Select a ${pickerLabel}`,
3721
+ id: "reference-selector-dialog",
3722
+ onClose: closeDialog,
3723
+ width: 1
3724
+ },
3725
+ /* @__PURE__ */ React13.createElement(Box4, { padding: 4 }, /* @__PURE__ */ React13.createElement(
3726
+ Autocomplete,
3727
+ {
3728
+ id: "reference-autocomplete",
3729
+ options: referenceOptions,
3730
+ placeholder: `Search ${pickerLabel}s...`,
3731
+ renderOption: (option) => /* @__PURE__ */ React13.createElement(Card4, { key: option.value, padding: 3, radius: 2, tone: "default", style: { cursor: "pointer" } }, /* @__PURE__ */ React13.createElement(Text11, { size: 2 }, option.title)),
3732
+ renderValue: (val) => {
3733
+ var _a2;
3734
+ return ((_a2 = referenceOptions.find((o) => o.value === val)) == null ? void 0 : _a2.title) || "";
3735
+ },
3736
+ onChange: (newValue) => {
3737
+ const selected = availableReferences.find((r) => r._id === newValue);
3738
+ if (selected) handleReferenceSelect(selected);
3739
+ },
3740
+ openButton: true,
3741
+ fontSize: 2
3742
+ }
3743
+ ))
3744
+ ));
3745
+ }
3746
+
3747
+ // src/components/VariableInstanceReferencesInput.jsx
3748
+ import React14, { useState as useState10, useCallback as useCallback9 } from "react";
3749
+ import { Button as Button11, Flex as Flex9, Dialog as Dialog2, Box as Box5, Stack as Stack10, Text as Text12 } from "@sanity/ui";
3750
+ import { SyncIcon as SyncIcon2, DocumentTextIcon } from "@sanity/icons";
3751
+ import { set as set6, useFormValue as useFormValue9 } from "sanity";
3752
+ import { nanoid as nanoid9 } from "nanoid";
3753
+ function VariableInstanceReferencesInput(props) {
3754
+ const { value = [], onChange } = props;
3755
+ const [isAutofilling, setIsAutofilling] = useState10(false);
3756
+ const [showConfirmDialog, setShowConfirmDialog] = useState10(false);
3757
+ const [pendingAction, setPendingAction] = useState10(null);
3758
+ const formDocument = useFormValue9([]);
3759
+ const sanityClient = useSanityClient();
3760
+ const fetchReferences = useCallback9(async (client, doc) => {
3761
+ const typefaceName = doc == null ? void 0 : doc.typefaceName;
3762
+ if (!typefaceName) {
3763
+ return client.fetch(`*[_type == 'font' && variableFont != true]{_id, title}`, {});
3764
+ }
3765
+ return client.fetch(
3766
+ `*[_type == 'font' && typefaceName == $typefaceName && variableFont != true]{_id, title}`,
3767
+ { typefaceName }
3768
+ );
3769
+ }, []);
3770
+ const performAutofillWithMatching = useCallback9(async (mode) => {
3771
+ setIsAutofilling(true);
3772
+ try {
3773
+ if (!(formDocument == null ? void 0 : formDocument.variableInstances)) {
3774
+ console.warn("Cannot autofill: no variableInstances data on this document");
3775
+ return;
3776
+ }
3777
+ const mappings = await parseVariableFontInstances(formDocument, sanityClient);
3778
+ if (mappings.length === 0) {
3779
+ console.warn("No variable instances could be parsed from this font");
3780
+ return;
3781
+ }
3782
+ const updatedPairs = mode === "replace" ? mappings : [...value, ...mappings.filter((m) => !value.some((p) => p.key === m.key))];
3783
+ onChange(set6(updatedPairs));
3784
+ } catch (err) {
3785
+ console.error("Error during autofill with matching:", err);
3786
+ } finally {
3787
+ setIsAutofilling(false);
3788
+ }
3789
+ }, [formDocument, sanityClient, value, onChange]);
3790
+ const performAutofillKeysOnly = useCallback9(async (mode) => {
3791
+ setIsAutofilling(true);
3792
+ try {
3793
+ if (!(formDocument == null ? void 0 : formDocument.variableInstances)) {
3794
+ console.warn("Cannot autofill: no variableInstances data on this document");
3795
+ return;
3796
+ }
3797
+ let instances;
3798
+ try {
3799
+ instances = JSON.parse(formDocument.variableInstances);
3800
+ } catch {
3801
+ console.error("Invalid variableInstances JSON on this document");
3802
+ return;
3803
+ }
3804
+ const keys = Object.keys(instances);
3805
+ if (keys.length === 0) {
3806
+ console.warn("No variable instances found in JSON");
3807
+ return;
3808
+ }
3809
+ const keyOnlyPairs = keys.map((key) => ({ key, value: null, _key: nanoid9() }));
3810
+ const updatedPairs = mode === "replace" ? keyOnlyPairs : [...value, ...keyOnlyPairs.filter((p) => !value.some((existing) => existing.key === p.key))];
3811
+ onChange(set6(updatedPairs));
3812
+ } catch (err) {
3813
+ console.error("Error during keys-only autofill:", err);
3814
+ } finally {
3815
+ setIsAutofilling(false);
3816
+ }
3817
+ }, [formDocument, value, onChange]);
3818
+ const handleAutofillWithMatching = useCallback9(() => {
3819
+ if (value.length > 0) {
3820
+ setPendingAction("matching");
3821
+ setShowConfirmDialog(true);
3822
+ return;
3823
+ }
3824
+ performAutofillWithMatching("replace");
3825
+ }, [value, performAutofillWithMatching]);
3826
+ const handleAutofillKeysOnly = useCallback9(() => {
3827
+ if (value.length > 0) {
3828
+ setPendingAction("keysOnly");
3829
+ setShowConfirmDialog(true);
3830
+ return;
3831
+ }
3832
+ performAutofillKeysOnly("replace");
3833
+ }, [value, performAutofillKeysOnly]);
3834
+ const handleConfirmChoice = useCallback9(async (choice) => {
3835
+ setShowConfirmDialog(false);
3836
+ if (pendingAction === "matching") await performAutofillWithMatching(choice);
3837
+ else if (pendingAction === "keysOnly") await performAutofillKeysOnly(choice);
3838
+ setPendingAction(null);
3839
+ }, [pendingAction, performAutofillWithMatching, performAutofillKeysOnly]);
3840
+ const handleConfirmCancel = useCallback9(() => {
3841
+ setShowConfirmDialog(false);
3842
+ setPendingAction(null);
3843
+ }, []);
3844
+ const showAutofill = !!((formDocument == null ? void 0 : formDocument.variableFont) && (formDocument == null ? void 0 : formDocument.variableInstances));
3845
+ const topActions = showAutofill ? /* @__PURE__ */ React14.createElement(Flex9, { gap: 2 }, /* @__PURE__ */ React14.createElement(
3846
+ Button11,
3847
+ {
3848
+ tone: "primary",
3849
+ mode: "ghost",
3850
+ onClick: handleAutofillWithMatching,
3851
+ icon: SyncIcon2,
3852
+ text: "Autofill with Matching",
3853
+ disabled: isAutofilling,
3854
+ loading: isAutofilling
3855
+ }
3856
+ ), /* @__PURE__ */ React14.createElement(
3857
+ Button11,
3858
+ {
3859
+ tone: "default",
3860
+ mode: "ghost",
3861
+ onClick: handleAutofillKeysOnly,
3862
+ icon: DocumentTextIcon,
3863
+ text: "Autofill Keys Only",
3864
+ disabled: isAutofilling,
3865
+ loading: isAutofilling
3866
+ }
3867
+ )) : null;
3868
+ return /* @__PURE__ */ React14.createElement(React14.Fragment, null, /* @__PURE__ */ React14.createElement(
3869
+ KeyValueReferenceInput,
3870
+ {
3871
+ ...props,
3872
+ referenceType: "font",
3873
+ fetchReferences,
3874
+ topActions
3875
+ }
3876
+ ), showConfirmDialog && /* @__PURE__ */ React14.createElement(
3877
+ Dialog2,
3878
+ {
3879
+ header: "Existing entries found",
3880
+ id: "autofill-confirm-dialog",
3881
+ onClose: handleConfirmCancel,
3882
+ width: 1
3883
+ },
3884
+ /* @__PURE__ */ React14.createElement(Box5, { padding: 4 }, /* @__PURE__ */ React14.createElement(Stack10, { space: 4 }, /* @__PURE__ */ React14.createElement(Text12, null, "You already have ", value.length, " ", value.length === 1 ? "entry" : "entries", ". How would you like to proceed?"), /* @__PURE__ */ React14.createElement(Flex9, { gap: 2, justify: "flex-end" }, /* @__PURE__ */ React14.createElement(Button11, { text: "Cancel", mode: "ghost", onClick: handleConfirmCancel }), /* @__PURE__ */ React14.createElement(Button11, { text: "Merge (Add New)", tone: "primary", mode: "ghost", onClick: () => handleConfirmChoice("merge") }), /* @__PURE__ */ React14.createElement(Button11, { text: "Replace All", tone: "critical", onClick: () => handleConfirmChoice("replace") }))))
3885
+ ));
3886
+ }
3887
+
3502
3888
  // src/utils/getEmptyFontKit.js
3503
3889
  import * as fontkit7 from "fontkit";
3504
3890
  import slugify4 from "slugify";
@@ -3581,6 +3967,8 @@ export {
3581
3967
  FontScriptUploaderComponent,
3582
3968
  GenerateCollectionsPairsComponent,
3583
3969
  HtmlDescription,
3970
+ KeyValueInput,
3971
+ KeyValueReferenceInput,
3584
3972
  PriceInput_default as PriceInput,
3585
3973
  RegenerateSubfamiliesComponent,
3586
3974
  SCRIPTS,
@@ -3590,6 +3978,7 @@ export {
3590
3978
  UpdateScriptsComponent,
3591
3979
  UploadButton_default as UploadButton,
3592
3980
  UploadScriptsComponent,
3981
+ VariableInstanceReferencesInput,
3593
3982
  addItalicToFontTitle,
3594
3983
  createFontObject,
3595
3984
  determineWeight,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liiift-studio/sanity-font-manager",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
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,95 @@
1
+ // Ordered key-value string pair editor for Sanity Studio — add, remove, and reorder rows
2
+
3
+ import React, { useState, useCallback } from 'react';
4
+ import { Button, Grid, Stack, TextInput } from '@sanity/ui';
5
+ import { AddIcon, ArrowDownIcon, ArrowUpIcon, TrashIcon } from '@sanity/icons';
6
+ import { set } from 'sanity';
7
+
8
+ /**
9
+ * Ordered key-value string pair editor with add, remove, and reorder controls.
10
+ * Writes an array of { _key, key, value } objects to Sanity.
11
+ * @param {Array} value - Current array of { _key, key, value } pairs
12
+ * @param {Function} onChange - Sanity onChange callback
13
+ */
14
+ export function KeyValueInput({ value = [], onChange }) {
15
+ const [pairs, setPairs] = useState(value);
16
+
17
+ /** Updates a specific field for a pair at the given index */
18
+ const handlePairChange = useCallback((index, field, fieldValue) => {
19
+ const updatedPairs = pairs.map((pair, idx) => idx === index ? { ...pair, [field]: fieldValue } : pair);
20
+ setPairs(updatedPairs);
21
+ onChange(set(updatedPairs));
22
+ }, [pairs, onChange]);
23
+
24
+ /** Appends a new empty pair */
25
+ const handleAddPair = useCallback(() => {
26
+ const newPair = { key: '', value: '', _key: Math.random().toString(36).substr(2, 9) };
27
+ const updatedPairs = [...pairs, newPair];
28
+ setPairs(updatedPairs);
29
+ onChange(set(updatedPairs));
30
+ }, [pairs, onChange]);
31
+
32
+ /** Removes the pair at the given index */
33
+ const handleRemovePair = useCallback((index) => {
34
+ const updatedPairs = pairs.filter((_, idx) => idx !== index);
35
+ setPairs(updatedPairs);
36
+ onChange(set(updatedPairs));
37
+ }, [pairs, onChange]);
38
+
39
+ /** Swaps a pair with the one above it */
40
+ const handleMoveUp = useCallback((index) => {
41
+ if (index === 0) return;
42
+ const updatedPairs = [...pairs];
43
+ [updatedPairs[index], updatedPairs[index - 1]] = [updatedPairs[index - 1], updatedPairs[index]];
44
+ setPairs(updatedPairs);
45
+ onChange(set(updatedPairs));
46
+ }, [pairs, onChange]);
47
+
48
+ /** Swaps a pair with the one below it */
49
+ const handleMoveDown = useCallback((index) => {
50
+ if (index === pairs.length - 1) return;
51
+ const updatedPairs = [...pairs];
52
+ [updatedPairs[index], updatedPairs[index + 1]] = [updatedPairs[index + 1], updatedPairs[index]];
53
+ setPairs(updatedPairs);
54
+ onChange(set(updatedPairs));
55
+ }, [pairs, onChange]);
56
+
57
+ return (
58
+ <Stack space={3}>
59
+ {pairs.map((pair, index) => (
60
+ <Grid className="manualButtonWrap" columns={[2]} key={index} gap={0} style={{ position: 'relative' }}>
61
+ <div style={{ position: 'absolute', height: '100%', top: '0', left: '-10px', width: 'min-content', transform: 'translate(-100%, 0%)' }}>
62
+ <button className="manualButton manualButtonUp" style={{ fontSize: '15px', height: '50%' }} onClick={() => handleMoveUp(index)}>
63
+ <ArrowUpIcon />
64
+ </button>
65
+ <button className="manualButton manualButtonDown" style={{ fontSize: '15px', height: '50%' }} onClick={() => handleMoveDown(index)}>
66
+ <ArrowDownIcon />
67
+ </button>
68
+ </div>
69
+
70
+ <TextInput
71
+ value={pair.key}
72
+ onChange={(e) => handlePairChange(index, 'key', e.target.value)}
73
+ placeholder="Key"
74
+ />
75
+ <div style={{ marginLeft: '-1px' }}>
76
+ <TextInput
77
+ value={pair.value}
78
+ onChange={(e) => handlePairChange(index, 'value', e.target.value)}
79
+ placeholder="Value"
80
+ />
81
+ </div>
82
+
83
+ <button
84
+ className="manualButton"
85
+ onClick={() => handleRemovePair(index)}
86
+ style={{ position: 'absolute', top: '0', right: '-10px', transform: 'translate(100%, 0%)' }}
87
+ >
88
+ <TrashIcon />
89
+ </button>
90
+ </Grid>
91
+ ))}
92
+ <Button tone="primary" onClick={handleAddPair} icon={AddIcon} text="Add Row" />
93
+ </Stack>
94
+ );
95
+ }