@liiift-studio/sanity-font-manager 2.3.19 → 2.5.0

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.
Files changed (62) hide show
  1. package/README.md +437 -437
  2. package/dist/UploadModal-6LIX7XOK.js +6 -0
  3. package/dist/UploadModal-NME2W53V.mjs +6 -0
  4. package/dist/chunk-646WCBRR.mjs +7276 -0
  5. package/dist/chunk-FH4QKHOH.js +7276 -0
  6. package/dist/index.js +747 -1675
  7. package/dist/index.mjs +400 -1237
  8. package/package.json +85 -85
  9. package/src/components/BatchUploadFonts.jsx +653 -639
  10. package/src/components/BulkActions.jsx +99 -0
  11. package/src/components/ExistingDocumentResolver.jsx +152 -0
  12. package/src/components/FontReviewCard.jsx +415 -0
  13. package/src/components/FontScriptUploaderComponent.jsx +463 -463
  14. package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
  15. package/src/components/KeyValueInput.jsx +95 -95
  16. package/src/components/KeyValueReferenceInput.jsx +254 -254
  17. package/src/components/NestedObjectArraySelector.jsx +146 -146
  18. package/src/components/PriceInput.jsx +26 -26
  19. package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
  20. package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
  21. package/src/components/SetOTF.jsx +87 -87
  22. package/src/components/SingleUploaderTool.jsx +672 -673
  23. package/src/components/StatusDisplay.jsx +26 -26
  24. package/src/components/StyleCountInput.jsx +16 -16
  25. package/src/components/UpdateScriptsComponent.jsx +76 -76
  26. package/src/components/UploadButton.jsx +43 -43
  27. package/src/components/UploadModal.jsx +268 -0
  28. package/src/components/UploadScriptsComponent.jsx +539 -537
  29. package/src/components/UploadStep1Settings.jsx +272 -0
  30. package/src/components/UploadStep2Review.jsx +472 -0
  31. package/src/components/UploadStep3Execute.jsx +234 -0
  32. package/src/components/UploadSummary.jsx +196 -0
  33. package/src/components/VariableInstanceReferencesInput.jsx +190 -190
  34. package/src/hooks/useNestedObjects.js +92 -92
  35. package/src/hooks/useSanityClient.js +9 -9
  36. package/src/index.js +115 -70
  37. package/src/schema/openTypeField.js +1945 -1945
  38. package/src/schema/styleCountField.js +12 -12
  39. package/src/schema/stylesField.js +268 -268
  40. package/src/schema/stylisticSetField.js +301 -301
  41. package/src/utils/buildUploadPlan.js +325 -0
  42. package/src/utils/executeUploadPlan.js +437 -0
  43. package/src/utils/executionReducer.js +56 -0
  44. package/src/utils/fontHelpers.js +267 -0
  45. package/src/utils/generateCssFile.js +207 -205
  46. package/src/utils/generateFontData.js +98 -145
  47. package/src/utils/generateFontFile.js +38 -38
  48. package/src/utils/generateKeywords.js +185 -185
  49. package/src/utils/generateSubset.js +45 -45
  50. package/src/utils/getEmptyFontKit.js +101 -99
  51. package/src/utils/parseFont.js +55 -0
  52. package/src/utils/parseVariableFontInstances.js +211 -211
  53. package/src/utils/planReducer.js +517 -0
  54. package/src/utils/planTypes.js +183 -0
  55. package/src/utils/processFontFiles.js +529 -477
  56. package/src/utils/regenerateFontData.js +146 -146
  57. package/src/utils/resolveExistingFont.js +87 -0
  58. package/src/utils/sanitizeForSanityId.js +65 -65
  59. package/src/utils/updateFontPrices.js +94 -94
  60. package/src/utils/updateTypefaceDocument.js +149 -160
  61. package/src/utils/uploadFontFiles.js +405 -316
  62. package/src/utils/utils.js +24 -24
@@ -1,254 +1,254 @@
1
- // Generic key-value pair editor where values are weak Sanity document references — add, remove, reorder, and searchable picker
2
-
3
- import React, { useState, useCallback, useEffect } from 'react';
4
- import { Button, Stack, TextInput, Box, Card, Flex, Text, Dialog, Menu, MenuButton, MenuItem, Autocomplete } from '@sanity/ui';
5
- import { AddIcon, ArrowDownIcon, ArrowUpIcon, TrashIcon, SyncIcon, EllipsisHorizontalIcon } from '@sanity/icons';
6
- import { set, useFormValue } from 'sanity';
7
- import { useSanityClient } from '../hooks/useSanityClient.js';
8
- import { nanoid } from 'nanoid';
9
-
10
- /**
11
- * Generic key-value pair editor where values are weak references to Sanity documents.
12
- * Handles add/remove/reorder, a searchable reference picker dialog, and cached title display.
13
- * @param {Array} value - Current array of { _key, key, value } pairs
14
- * @param {Function} onChange - Sanity onChange callback
15
- * @param {string} referenceType - Display label for the referenced document type (e.g. 'font')
16
- * @param {Function} fetchReferences - async (client, doc) => [{ _id, title }] — populates the picker
17
- * @param {ReactNode} topActions - Optional slot rendered above the pairs list (e.g. autofill buttons)
18
- * @param {Object} schemaType - Sanity schemaType passed automatically by the Studio
19
- */
20
- export function KeyValueReferenceInput(props) {
21
- const { value = [], onChange, schemaType, referenceType, fetchReferences, topActions } = props;
22
-
23
- const [pairs, setPairs] = useState(value);
24
- const [referenceData, setReferenceData] = useState({});
25
- const [isDialogOpen, setIsDialogOpen] = useState(false);
26
- const [editingIndex, setEditingIndex] = useState(null);
27
- const [availableReferences, setAvailableReferences] = useState([]);
28
-
29
- const sanityClient = useSanityClient();
30
- const formDocument = useFormValue([]);
31
-
32
- /** Fetches and caches display titles for all referenced documents whenever pairs change */
33
- useEffect(() => {
34
- const refIds = pairs.filter(p => p.value?._ref).map(p => p.value._ref);
35
- if (refIds.length === 0) return;
36
-
37
- if (!sanityClient) {
38
- const fallback = {};
39
- refIds.forEach(id => { fallback[id] = `Reference (${id.substring(0, 6)}...)`; });
40
- setReferenceData(fallback);
41
- return;
42
- }
43
-
44
- sanityClient.fetch(`*[_id in $ids]{_id, title}`, { ids: refIds })
45
- .then(result => {
46
- const map = {};
47
- result.forEach(item => { map[item._id] = item.title; });
48
- setReferenceData(map);
49
- })
50
- .catch(err => {
51
- console.error('Error fetching reference data:', err);
52
- const fallback = {};
53
- refIds.forEach(id => { fallback[id] = `Reference (${id.substring(0, 6)}...)`; });
54
- setReferenceData(fallback);
55
- });
56
- }, [pairs, sanityClient]);
57
-
58
- /** Updates a field on a pair at the given index and syncs to Sanity */
59
- const handlePairChange = useCallback((index, field, fieldValue) => {
60
- const updatedPairs = pairs.map((pair, idx) => idx === index ? { ...pair, [field]: fieldValue } : pair);
61
- setPairs(updatedPairs);
62
- onChange(set(updatedPairs));
63
- }, [pairs, onChange]);
64
-
65
- /** Appends a new empty pair */
66
- const handleAddPair = useCallback(() => {
67
- const updatedPairs = [...pairs, { key: '', value: null, _key: nanoid() }];
68
- setPairs(updatedPairs);
69
- onChange(set(updatedPairs));
70
- }, [pairs, onChange]);
71
-
72
- /** Removes the pair at the given index */
73
- const handleRemovePair = useCallback((index) => {
74
- const updatedPairs = pairs.filter((_, idx) => idx !== index);
75
- setPairs(updatedPairs);
76
- onChange(set(updatedPairs));
77
- }, [pairs, onChange]);
78
-
79
- /** Swaps a pair with the one above it */
80
- const handleMoveUp = useCallback((index) => {
81
- if (index === 0) return;
82
- const updatedPairs = [...pairs];
83
- [updatedPairs[index], updatedPairs[index - 1]] = [updatedPairs[index - 1], updatedPairs[index]];
84
- setPairs(updatedPairs);
85
- onChange(set(updatedPairs));
86
- }, [pairs, onChange]);
87
-
88
- /** Swaps a pair with the one below it */
89
- const handleMoveDown = useCallback((index) => {
90
- if (index === pairs.length - 1) return;
91
- const updatedPairs = [...pairs];
92
- [updatedPairs[index], updatedPairs[index + 1]] = [updatedPairs[index + 1], updatedPairs[index]];
93
- setPairs(updatedPairs);
94
- onChange(set(updatedPairs));
95
- }, [pairs, onChange]);
96
-
97
- /** Opens the reference picker, calling fetchReferences to populate the list */
98
- const openReferenceSelector = useCallback(async (index) => {
99
- setEditingIndex(index);
100
- if (!sanityClient) {
101
- console.error('KeyValueReferenceInput: Sanity client not available');
102
- return;
103
- }
104
- try {
105
- let refs;
106
- if (fetchReferences) {
107
- refs = await fetchReferences(sanityClient, formDocument);
108
- } else if (referenceType) {
109
- refs = await sanityClient.fetch(`*[_type == $type]{_id, title}`, { type: referenceType });
110
- } else {
111
- console.warn('KeyValueReferenceInput: provide a fetchReferences prop or referenceType');
112
- refs = [];
113
- }
114
- setAvailableReferences(refs);
115
- setIsDialogOpen(true);
116
- } catch (err) {
117
- console.error('Error fetching available references:', err);
118
- }
119
- }, [sanityClient, fetchReferences, referenceType, formDocument]);
120
-
121
- /** Closes the picker dialog and clears editing state */
122
- const closeDialog = useCallback(() => {
123
- setIsDialogOpen(false);
124
- setEditingIndex(null);
125
- }, []);
126
-
127
- /** Writes the selected item as a weak reference and closes the dialog */
128
- const handleReferenceSelect = useCallback((reference) => {
129
- if (editingIndex === null) return;
130
- handlePairChange(editingIndex, 'value', { _type: 'reference', _ref: reference._id, _weak: true });
131
- closeDialog();
132
- }, [editingIndex, handlePairChange, closeDialog]);
133
-
134
- const referenceOptions = availableReferences.map(ref => ({ value: ref._id, title: ref.title }));
135
-
136
- // Infer labels from schemaType if available
137
- const keyField = schemaType?.options?.of?.[0]?.fields?.find(f => f.name === 'key');
138
- const valueField = schemaType?.options?.of?.[0]?.fields?.find(f => f.name === 'value');
139
- const keyTitle = keyField?.title || 'Key';
140
- const valueTitle = valueField?.title || 'Value';
141
- const keyPlaceholder = keyField?.placeholder || 'Enter key';
142
- const pickerLabel = referenceType || valueTitle.toLowerCase();
143
-
144
- return (
145
- <Stack space={3}>
146
- {topActions && <Box paddingBottom={2}>{topActions}</Box>}
147
-
148
- <Box>
149
- <Stack space={2}>
150
- {pairs.map((pair, index) => (
151
- <Box key={index} style={{ position: 'relative' }}>
152
- {/* Reorder buttons */}
153
- <div style={{ position: 'absolute', height: '100%', top: '0', left: '-5px', width: 'min-content', transform: 'translate(-100%, 0%)' }}>
154
- <button className="manualButton manualButtonUp" style={{ fontSize: '15px', height: '50%' }} onClick={() => handleMoveUp(index)}>
155
- <ArrowUpIcon />
156
- </button>
157
- <button className="manualButton manualButtonDown" style={{ fontSize: '15px', height: '50%' }} onClick={() => handleMoveDown(index)}>
158
- <ArrowDownIcon />
159
- </button>
160
- </div>
161
-
162
- <Flex gap={2} align="flex-start">
163
- {/* Key input */}
164
- <Box flex={1}>
165
- <TextInput
166
- value={pair.key}
167
- onChange={(e) => handlePairChange(index, 'key', e.target.value)}
168
- placeholder={keyPlaceholder}
169
- />
170
- </Box>
171
-
172
- {/* Reference display or empty-state picker trigger */}
173
- <Box flex={1} style={{ minHeight: '100%' }}>
174
- {pair.value?._ref ? (
175
- <Card className="referenceCard" radius={2} tone="primary" style={{ paddingLeft: '1rem', height: 'fit-content' }}>
176
- <Flex align="center" justify="space-between">
177
- <Text
178
- size={2}
179
- style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '90%' }}
180
- >
181
- {referenceData[pair.value._ref] || 'Loading...'}
182
- </Text>
183
- <MenuButton
184
- button={<Button icon={EllipsisHorizontalIcon} mode="bleed" title="Options" />}
185
- id={`ref-options-${index}`}
186
- menu={
187
- <Menu>
188
- <MenuItem tone="critical" icon={TrashIcon} text="Remove" onClick={() => handlePairChange(index, 'value', null)} />
189
- <MenuItem icon={SyncIcon} text="Replace" onClick={() => openReferenceSelector(index)} />
190
- </Menu>
191
- }
192
- popover={{ portal: true, tone: 'default', placement: 'left' }}
193
- />
194
- </Flex>
195
- </Card>
196
- ) : (
197
- <Box
198
- padding={2}
199
- style={{ minHeight: '100%', border: '1px dashed #ccc', borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
200
- onClick={() => openReferenceSelector(index)}
201
- >
202
- <Text muted size={2}>Click to select a {pickerLabel}</Text>
203
- </Box>
204
- )}
205
- </Box>
206
- </Flex>
207
-
208
- {/* Remove button */}
209
- <button
210
- className="manualButton"
211
- onClick={() => handleRemovePair(index)}
212
- style={{ position: 'absolute', top: '0', right: '-7px', transform: 'translate(100%, 0%)' }}
213
- >
214
- <TrashIcon />
215
- </button>
216
- </Box>
217
- ))}
218
- </Stack>
219
- </Box>
220
-
221
- <Button tone="primary" mode="ghost" onClick={handleAddPair} icon={AddIcon} text={`Add ${keyTitle}`} />
222
-
223
- {/* Reference picker dialog */}
224
- {isDialogOpen && (
225
- <Dialog
226
- header={`Select a ${pickerLabel}`}
227
- id="reference-selector-dialog"
228
- onClose={closeDialog}
229
- width={1}
230
- >
231
- <Box padding={4}>
232
- <Autocomplete
233
- id="reference-autocomplete"
234
- options={referenceOptions}
235
- placeholder={`Search ${pickerLabel}s...`}
236
- renderOption={(option) => (
237
- <Card key={option.value} padding={3} radius={2} tone="default" style={{ cursor: 'pointer' }}>
238
- <Text size={2}>{option.title}</Text>
239
- </Card>
240
- )}
241
- renderValue={(val) => referenceOptions.find(o => o.value === val)?.title || ''}
242
- onChange={(newValue) => {
243
- const selected = availableReferences.find(r => r._id === newValue);
244
- if (selected) handleReferenceSelect(selected);
245
- }}
246
- openButton
247
- fontSize={2}
248
- />
249
- </Box>
250
- </Dialog>
251
- )}
252
- </Stack>
253
- );
254
- }
1
+ // Generic key-value pair editor where values are weak Sanity document references — add, remove, reorder, and searchable picker
2
+
3
+ import React, { useState, useCallback, useEffect } from 'react';
4
+ import { Button, Stack, TextInput, Box, Card, Flex, Text, Dialog, Menu, MenuButton, MenuItem, Autocomplete } from '@sanity/ui';
5
+ import { AddIcon, ArrowDownIcon, ArrowUpIcon, TrashIcon, SyncIcon, EllipsisHorizontalIcon } from '@sanity/icons';
6
+ import { set, useFormValue } from 'sanity';
7
+ import { useSanityClient } from '../hooks/useSanityClient.js';
8
+ import { nanoid } from 'nanoid';
9
+
10
+ /**
11
+ * Generic key-value pair editor where values are weak references to Sanity documents.
12
+ * Handles add/remove/reorder, a searchable reference picker dialog, and cached title display.
13
+ * @param {Array} value - Current array of { _key, key, value } pairs
14
+ * @param {Function} onChange - Sanity onChange callback
15
+ * @param {string} referenceType - Display label for the referenced document type (e.g. 'font')
16
+ * @param {Function} fetchReferences - async (client, doc) => [{ _id, title }] — populates the picker
17
+ * @param {ReactNode} topActions - Optional slot rendered above the pairs list (e.g. autofill buttons)
18
+ * @param {Object} schemaType - Sanity schemaType passed automatically by the Studio
19
+ */
20
+ export function KeyValueReferenceInput(props) {
21
+ const { value = [], onChange, schemaType, referenceType, fetchReferences, topActions } = props;
22
+
23
+ const [pairs, setPairs] = useState(value);
24
+ const [referenceData, setReferenceData] = useState({});
25
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
26
+ const [editingIndex, setEditingIndex] = useState(null);
27
+ const [availableReferences, setAvailableReferences] = useState([]);
28
+
29
+ const sanityClient = useSanityClient();
30
+ const formDocument = useFormValue([]);
31
+
32
+ /** Fetches and caches display titles for all referenced documents whenever pairs change */
33
+ useEffect(() => {
34
+ const refIds = pairs.filter(p => p.value?._ref).map(p => p.value._ref);
35
+ if (refIds.length === 0) return;
36
+
37
+ if (!sanityClient) {
38
+ const fallback = {};
39
+ refIds.forEach(id => { fallback[id] = `Reference (${id.substring(0, 6)}...)`; });
40
+ setReferenceData(fallback);
41
+ return;
42
+ }
43
+
44
+ sanityClient.fetch(`*[_id in $ids]{_id, title}`, { ids: refIds })
45
+ .then(result => {
46
+ const map = {};
47
+ result.forEach(item => { map[item._id] = item.title; });
48
+ setReferenceData(map);
49
+ })
50
+ .catch(err => {
51
+ console.error('Error fetching reference data:', err);
52
+ const fallback = {};
53
+ refIds.forEach(id => { fallback[id] = `Reference (${id.substring(0, 6)}...)`; });
54
+ setReferenceData(fallback);
55
+ });
56
+ }, [pairs, sanityClient]);
57
+
58
+ /** Updates a field on a pair at the given index and syncs to Sanity */
59
+ const handlePairChange = useCallback((index, field, fieldValue) => {
60
+ const updatedPairs = pairs.map((pair, idx) => idx === index ? { ...pair, [field]: fieldValue } : pair);
61
+ setPairs(updatedPairs);
62
+ onChange(set(updatedPairs));
63
+ }, [pairs, onChange]);
64
+
65
+ /** Appends a new empty pair */
66
+ const handleAddPair = useCallback(() => {
67
+ const updatedPairs = [...pairs, { key: '', value: null, _key: nanoid() }];
68
+ setPairs(updatedPairs);
69
+ onChange(set(updatedPairs));
70
+ }, [pairs, onChange]);
71
+
72
+ /** Removes the pair at the given index */
73
+ const handleRemovePair = useCallback((index) => {
74
+ const updatedPairs = pairs.filter((_, idx) => idx !== index);
75
+ setPairs(updatedPairs);
76
+ onChange(set(updatedPairs));
77
+ }, [pairs, onChange]);
78
+
79
+ /** Swaps a pair with the one above it */
80
+ const handleMoveUp = useCallback((index) => {
81
+ if (index === 0) return;
82
+ const updatedPairs = [...pairs];
83
+ [updatedPairs[index], updatedPairs[index - 1]] = [updatedPairs[index - 1], updatedPairs[index]];
84
+ setPairs(updatedPairs);
85
+ onChange(set(updatedPairs));
86
+ }, [pairs, onChange]);
87
+
88
+ /** Swaps a pair with the one below it */
89
+ const handleMoveDown = useCallback((index) => {
90
+ if (index === pairs.length - 1) return;
91
+ const updatedPairs = [...pairs];
92
+ [updatedPairs[index], updatedPairs[index + 1]] = [updatedPairs[index + 1], updatedPairs[index]];
93
+ setPairs(updatedPairs);
94
+ onChange(set(updatedPairs));
95
+ }, [pairs, onChange]);
96
+
97
+ /** Opens the reference picker, calling fetchReferences to populate the list */
98
+ const openReferenceSelector = useCallback(async (index) => {
99
+ setEditingIndex(index);
100
+ if (!sanityClient) {
101
+ console.error('KeyValueReferenceInput: Sanity client not available');
102
+ return;
103
+ }
104
+ try {
105
+ let refs;
106
+ if (fetchReferences) {
107
+ refs = await fetchReferences(sanityClient, formDocument);
108
+ } else if (referenceType) {
109
+ refs = await sanityClient.fetch(`*[_type == $type]{_id, title}`, { type: referenceType });
110
+ } else {
111
+ console.warn('KeyValueReferenceInput: provide a fetchReferences prop or referenceType');
112
+ refs = [];
113
+ }
114
+ setAvailableReferences(refs);
115
+ setIsDialogOpen(true);
116
+ } catch (err) {
117
+ console.error('Error fetching available references:', err);
118
+ }
119
+ }, [sanityClient, fetchReferences, referenceType, formDocument]);
120
+
121
+ /** Closes the picker dialog and clears editing state */
122
+ const closeDialog = useCallback(() => {
123
+ setIsDialogOpen(false);
124
+ setEditingIndex(null);
125
+ }, []);
126
+
127
+ /** Writes the selected item as a weak reference and closes the dialog */
128
+ const handleReferenceSelect = useCallback((reference) => {
129
+ if (editingIndex === null) return;
130
+ handlePairChange(editingIndex, 'value', { _type: 'reference', _ref: reference._id, _weak: true });
131
+ closeDialog();
132
+ }, [editingIndex, handlePairChange, closeDialog]);
133
+
134
+ const referenceOptions = availableReferences.map(ref => ({ value: ref._id, title: ref.title }));
135
+
136
+ // Infer labels from schemaType if available
137
+ const keyField = schemaType?.options?.of?.[0]?.fields?.find(f => f.name === 'key');
138
+ const valueField = schemaType?.options?.of?.[0]?.fields?.find(f => f.name === 'value');
139
+ const keyTitle = keyField?.title || 'Key';
140
+ const valueTitle = valueField?.title || 'Value';
141
+ const keyPlaceholder = keyField?.placeholder || 'Enter key';
142
+ const pickerLabel = referenceType || valueTitle.toLowerCase();
143
+
144
+ return (
145
+ <Stack space={3}>
146
+ {topActions && <Box paddingBottom={2}>{topActions}</Box>}
147
+
148
+ <Box>
149
+ <Stack space={2}>
150
+ {pairs.map((pair, index) => (
151
+ <Box key={index} style={{ position: 'relative' }}>
152
+ {/* Reorder buttons */}
153
+ <div style={{ position: 'absolute', height: '100%', top: '0', left: '-5px', width: 'min-content', transform: 'translate(-100%, 0%)' }}>
154
+ <button className="manualButton manualButtonUp" style={{ fontSize: '15px', height: '50%' }} onClick={() => handleMoveUp(index)}>
155
+ <ArrowUpIcon />
156
+ </button>
157
+ <button className="manualButton manualButtonDown" style={{ fontSize: '15px', height: '50%' }} onClick={() => handleMoveDown(index)}>
158
+ <ArrowDownIcon />
159
+ </button>
160
+ </div>
161
+
162
+ <Flex gap={2} align="flex-start">
163
+ {/* Key input */}
164
+ <Box flex={1}>
165
+ <TextInput
166
+ value={pair.key}
167
+ onChange={(e) => handlePairChange(index, 'key', e.target.value)}
168
+ placeholder={keyPlaceholder}
169
+ />
170
+ </Box>
171
+
172
+ {/* Reference display or empty-state picker trigger */}
173
+ <Box flex={1} style={{ minHeight: '100%' }}>
174
+ {pair.value?._ref ? (
175
+ <Card className="referenceCard" radius={2} tone="primary" style={{ paddingLeft: '1rem', height: 'fit-content' }}>
176
+ <Flex align="center" justify="space-between">
177
+ <Text
178
+ size={2}
179
+ style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '90%' }}
180
+ >
181
+ {referenceData[pair.value._ref] || 'Loading...'}
182
+ </Text>
183
+ <MenuButton
184
+ button={<Button icon={EllipsisHorizontalIcon} mode="bleed" title="Options" />}
185
+ id={`ref-options-${index}`}
186
+ menu={
187
+ <Menu>
188
+ <MenuItem tone="critical" icon={TrashIcon} text="Remove" onClick={() => handlePairChange(index, 'value', null)} />
189
+ <MenuItem icon={SyncIcon} text="Replace" onClick={() => openReferenceSelector(index)} />
190
+ </Menu>
191
+ }
192
+ popover={{ portal: true, tone: 'default', placement: 'left' }}
193
+ />
194
+ </Flex>
195
+ </Card>
196
+ ) : (
197
+ <Box
198
+ padding={2}
199
+ style={{ minHeight: '100%', border: '1px dashed #ccc', borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
200
+ onClick={() => openReferenceSelector(index)}
201
+ >
202
+ <Text muted size={2}>Click to select a {pickerLabel}</Text>
203
+ </Box>
204
+ )}
205
+ </Box>
206
+ </Flex>
207
+
208
+ {/* Remove button */}
209
+ <button
210
+ className="manualButton"
211
+ onClick={() => handleRemovePair(index)}
212
+ style={{ position: 'absolute', top: '0', right: '-7px', transform: 'translate(100%, 0%)' }}
213
+ >
214
+ <TrashIcon />
215
+ </button>
216
+ </Box>
217
+ ))}
218
+ </Stack>
219
+ </Box>
220
+
221
+ <Button tone="primary" mode="ghost" onClick={handleAddPair} icon={AddIcon} text={`Add ${keyTitle}`} />
222
+
223
+ {/* Reference picker dialog */}
224
+ {isDialogOpen && (
225
+ <Dialog
226
+ header={`Select a ${pickerLabel}`}
227
+ id="reference-selector-dialog"
228
+ onClose={closeDialog}
229
+ width={1}
230
+ >
231
+ <Box padding={4}>
232
+ <Autocomplete
233
+ id="reference-autocomplete"
234
+ options={referenceOptions}
235
+ placeholder={`Search ${pickerLabel}s...`}
236
+ renderOption={(option) => (
237
+ <Card key={option.value} padding={3} radius={2} tone="default" style={{ cursor: 'pointer' }}>
238
+ <Text size={2}>{option.title}</Text>
239
+ </Card>
240
+ )}
241
+ renderValue={(val) => referenceOptions.find(o => o.value === val)?.title || ''}
242
+ onChange={(newValue) => {
243
+ const selected = availableReferences.find(r => r._id === newValue);
244
+ if (selected) handleReferenceSelect(selected);
245
+ }}
246
+ openButton
247
+ fontSize={2}
248
+ />
249
+ </Box>
250
+ </Dialog>
251
+ )}
252
+ </Stack>
253
+ );
254
+ }