@liiift-studio/sanity-font-manager 2.7.0 → 2.8.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/LICENSE +21 -0
  2. package/README.md +695 -437
  3. package/dist/index.js +2330 -7084
  4. package/dist/index.mjs +2331 -7085
  5. package/package.json +84 -83
  6. package/src/components/BatchUploadFonts.jsx +655 -655
  7. package/src/components/BulkActions.jsx +99 -99
  8. package/src/components/ExistingDocumentResolver.jsx +152 -152
  9. package/src/components/FontReviewCard.jsx +455 -455
  10. package/src/components/FontScriptUploaderComponent.jsx +463 -463
  11. package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
  12. package/src/components/KeyValueInput.jsx +95 -95
  13. package/src/components/KeyValueReferenceInput.jsx +267 -267
  14. package/src/components/NestedObjectArraySelector.jsx +146 -146
  15. package/src/components/PriceInput.jsx +26 -26
  16. package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
  17. package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
  18. package/src/components/SetOTF.jsx +87 -87
  19. package/src/components/SingleUploaderTool.jsx +674 -674
  20. package/src/components/StatusDisplay.jsx +26 -26
  21. package/src/components/StyleCountInput.jsx +16 -16
  22. package/src/components/UpdateScriptsComponent.jsx +76 -76
  23. package/src/components/UploadButton.jsx +43 -43
  24. package/src/components/UploadModal.jsx +309 -309
  25. package/src/components/UploadScriptsComponent.jsx +539 -539
  26. package/src/components/UploadStep1Settings.jsx +272 -272
  27. package/src/components/UploadStep2Review.jsx +478 -478
  28. package/src/components/UploadStep3Execute.jsx +234 -234
  29. package/src/components/UploadStep3bInstances.jsx +396 -396
  30. package/src/components/UploadSummary.jsx +196 -196
  31. package/src/components/VariableInstanceReferencesInput.jsx +190 -190
  32. package/src/hooks/useNestedObjects.js +92 -92
  33. package/src/hooks/useSanityClient.js +9 -9
  34. package/src/index.js +120 -120
  35. package/src/schema/openTypeField.js +1995 -1995
  36. package/src/schema/styleCountField.js +12 -12
  37. package/src/schema/stylesField.js +302 -302
  38. package/src/schema/stylisticSetField.js +301 -301
  39. package/src/utils/buildUploadPlan.js +326 -326
  40. package/src/utils/executeUploadPlan.js +430 -430
  41. package/src/utils/executionReducer.js +56 -56
  42. package/src/utils/fontHelpers.js +281 -281
  43. package/src/utils/generateCssFile.js +207 -207
  44. package/src/utils/generateFontData.js +98 -98
  45. package/src/utils/generateFontFile.js +38 -38
  46. package/src/utils/generateKeywords.js +185 -185
  47. package/src/utils/generateSubset.js +45 -45
  48. package/src/utils/getEmptyFontKit.js +101 -101
  49. package/src/utils/parseFont.js +56 -56
  50. package/src/utils/parseVariableFontInstances.js +301 -301
  51. package/src/utils/planReducer.js +531 -531
  52. package/src/utils/planTypes.js +183 -183
  53. package/src/utils/processFontFiles.js +530 -530
  54. package/src/utils/regenerateFontData.js +146 -146
  55. package/src/utils/resolveExistingFont.js +87 -87
  56. package/src/utils/retitleFontEntries.js +154 -154
  57. package/src/utils/sanitizeForSanityId.js +65 -65
  58. package/src/utils/setupDecompressors.js +27 -27
  59. package/src/utils/updateFontPrices.js +94 -94
  60. package/src/utils/updateTypefaceDocument.js +162 -162
  61. package/src/utils/uploadFontFiles.js +405 -405
  62. package/src/utils/utils.js +24 -24
@@ -1,99 +1,99 @@
1
- // Bulk actions bar — expand/collapse all, search, filter with counts
2
-
3
- import React, { useMemo } from 'react';
4
- import { Flex, Box, Button, TextInput, Select, Text, Label } from '@sanity/ui';
5
- import { SearchIcon } from '@sanity/icons';
6
- import { FONT_STATUS, RECOMMENDATION } from '../utils/planTypes';
7
-
8
- /** Determines whether a font entry will create or update a document */
9
- function isUpdateEntry(entry) {
10
- const d = entry.decisions?.existingDocument;
11
- const choice = d?.userChoice;
12
- const rec = d?.recommendation;
13
- return choice === 'update' || (!choice && (rec === RECOMMENDATION.USE_EXACT || rec === RECOMMENDATION.USE_CANDIDATE));
14
- }
15
-
16
- /**
17
- * Sticky bulk actions bar for the review step.
18
- */
19
- export default function BulkActions({
20
- fonts,
21
- dispatch,
22
- searchQuery,
23
- onSearchChange,
24
- filterBy,
25
- onFilterChange,
26
- allExpanded,
27
- onToggleExpandAll,
28
- visibleTempIds,
29
- }) {
30
- const fontEntries = useMemo(() => Object.values(fonts), [fonts]);
31
- const fontCount = fontEntries.length;
32
- const visibleCount = visibleTempIds.length;
33
-
34
- // Compute counts for each filter category
35
- const filterCounts = useMemo(() => {
36
- const createCount = fontEntries.filter(f => f.status !== FONT_STATUS.ERROR && !isUpdateEntry(f)).length;
37
- const updateCount = fontEntries.filter(f => f.status !== FONT_STATUS.ERROR && isUpdateEntry(f)).length;
38
- const errorCount = fontEntries.filter(f => f.status === FONT_STATUS.ERROR).length;
39
- const conflictCount = fontEntries.filter(f => f._idConflict).length;
40
- const italicCount = fontEntries.filter(f => f.style === 'Italic' && f.status !== FONT_STATUS.ERROR).length;
41
- const regularCount = fontEntries.filter(f => f.style === 'Regular' && f.status !== FONT_STATUS.ERROR).length;
42
-
43
- // Subfamily counts
44
- const subfamilyCounts = {};
45
- fontEntries.forEach(f => {
46
- if (f.status === FONT_STATUS.ERROR) return;
47
- const sf = f.subfamily || 'Regular';
48
- subfamilyCounts[sf] = (subfamilyCounts[sf] || 0) + 1;
49
- });
50
-
51
- return { createCount, updateCount, errorCount, conflictCount, italicCount, regularCount, subfamilyCounts };
52
- }, [fontEntries]);
53
-
54
- const subfamilies = useMemo(() =>
55
- Object.keys(filterCounts.subfamilyCounts).sort((a, b) => {
56
- if (a === 'Regular') return -1;
57
- if (b === 'Regular') return 1;
58
- return a.localeCompare(b);
59
- }),
60
- [filterCounts]
61
- );
62
-
63
- return (
64
- <Flex gap={2} align="center" wrap="wrap" style={{ position: 'sticky', top: 0, zIndex: 10, background: 'var(--card-bg-color)', paddingBottom: 8, paddingTop: 4 }}>
65
- {/* Search */}
66
- <Box style={{ flex: 1, minWidth: 150 }}>
67
- <TextInput
68
- icon={SearchIcon}
69
- placeholder="Search fonts..."
70
- value={searchQuery}
71
- onChange={(e) => onSearchChange(e.target.value)}
72
- fontSize={1}
73
- />
74
- </Box>
75
-
76
- {/* Filter by */}
77
- <Flex align="center" gap={1}>
78
- <Label size={0} style={{ whiteSpace: 'nowrap' }}>Filter</Label>
79
- <Select value={filterBy} onChange={(e) => onFilterChange(e.target.value)} fontSize={1} style={{ minWidth: 140 }}>
80
- <option value="all">All ({fontCount})</option>
81
- {filterCounts.createCount > 0 && <option value="create">Create ({filterCounts.createCount})</option>}
82
- {filterCounts.updateCount > 0 && <option value="update">Update ({filterCounts.updateCount})</option>}
83
- {filterCounts.regularCount > 0 && <option value="style:regular">Regular ({filterCounts.regularCount})</option>}
84
- {filterCounts.italicCount > 0 && <option value="style:italic">Italic ({filterCounts.italicCount})</option>}
85
- {filterCounts.errorCount > 0 && <option value="error">Errors ({filterCounts.errorCount})</option>}
86
- {filterCounts.conflictCount > 0 && <option value="conflict">Conflicts ({filterCounts.conflictCount})</option>}
87
- {subfamilies.length > 1 && subfamilies.map(sf => (
88
- <option key={sf} value={`sf:${sf}`}>{sf} ({filterCounts.subfamilyCounts[sf]})</option>
89
- ))}
90
- </Select>
91
- </Flex>
92
-
93
- {/* Visible count */}
94
- {visibleCount !== fontCount && (
95
- <Text size={0} muted>{visibleCount} of {fontCount}</Text>
96
- )}
97
- </Flex>
98
- );
99
- }
1
+ // Bulk actions bar — expand/collapse all, search, filter with counts
2
+
3
+ import React, { useMemo } from 'react';
4
+ import { Flex, Box, Button, TextInput, Select, Text, Label } from '@sanity/ui';
5
+ import { SearchIcon } from '@sanity/icons';
6
+ import { FONT_STATUS, RECOMMENDATION } from '../utils/planTypes';
7
+
8
+ /** Determines whether a font entry will create or update a document */
9
+ function isUpdateEntry(entry) {
10
+ const d = entry.decisions?.existingDocument;
11
+ const choice = d?.userChoice;
12
+ const rec = d?.recommendation;
13
+ return choice === 'update' || (!choice && (rec === RECOMMENDATION.USE_EXACT || rec === RECOMMENDATION.USE_CANDIDATE));
14
+ }
15
+
16
+ /**
17
+ * Sticky bulk actions bar for the review step.
18
+ */
19
+ export default function BulkActions({
20
+ fonts,
21
+ dispatch,
22
+ searchQuery,
23
+ onSearchChange,
24
+ filterBy,
25
+ onFilterChange,
26
+ allExpanded,
27
+ onToggleExpandAll,
28
+ visibleTempIds,
29
+ }) {
30
+ const fontEntries = useMemo(() => Object.values(fonts), [fonts]);
31
+ const fontCount = fontEntries.length;
32
+ const visibleCount = visibleTempIds.length;
33
+
34
+ // Compute counts for each filter category
35
+ const filterCounts = useMemo(() => {
36
+ const createCount = fontEntries.filter(f => f.status !== FONT_STATUS.ERROR && !isUpdateEntry(f)).length;
37
+ const updateCount = fontEntries.filter(f => f.status !== FONT_STATUS.ERROR && isUpdateEntry(f)).length;
38
+ const errorCount = fontEntries.filter(f => f.status === FONT_STATUS.ERROR).length;
39
+ const conflictCount = fontEntries.filter(f => f._idConflict).length;
40
+ const italicCount = fontEntries.filter(f => f.style === 'Italic' && f.status !== FONT_STATUS.ERROR).length;
41
+ const regularCount = fontEntries.filter(f => f.style === 'Regular' && f.status !== FONT_STATUS.ERROR).length;
42
+
43
+ // Subfamily counts
44
+ const subfamilyCounts = {};
45
+ fontEntries.forEach(f => {
46
+ if (f.status === FONT_STATUS.ERROR) return;
47
+ const sf = f.subfamily || 'Regular';
48
+ subfamilyCounts[sf] = (subfamilyCounts[sf] || 0) + 1;
49
+ });
50
+
51
+ return { createCount, updateCount, errorCount, conflictCount, italicCount, regularCount, subfamilyCounts };
52
+ }, [fontEntries]);
53
+
54
+ const subfamilies = useMemo(() =>
55
+ Object.keys(filterCounts.subfamilyCounts).sort((a, b) => {
56
+ if (a === 'Regular') return -1;
57
+ if (b === 'Regular') return 1;
58
+ return a.localeCompare(b);
59
+ }),
60
+ [filterCounts]
61
+ );
62
+
63
+ return (
64
+ <Flex gap={2} align="center" wrap="wrap" style={{ position: 'sticky', top: 0, zIndex: 10, background: 'var(--card-bg-color)', paddingBottom: 8, paddingTop: 4 }}>
65
+ {/* Search */}
66
+ <Box style={{ flex: 1, minWidth: 150 }}>
67
+ <TextInput
68
+ icon={SearchIcon}
69
+ placeholder="Search fonts..."
70
+ value={searchQuery}
71
+ onChange={(e) => onSearchChange(e.target.value)}
72
+ fontSize={1}
73
+ />
74
+ </Box>
75
+
76
+ {/* Filter by */}
77
+ <Flex align="center" gap={1}>
78
+ <Label size={0} style={{ whiteSpace: 'nowrap' }}>Filter</Label>
79
+ <Select value={filterBy} onChange={(e) => onFilterChange(e.target.value)} fontSize={1} style={{ minWidth: 140 }}>
80
+ <option value="all">All ({fontCount})</option>
81
+ {filterCounts.createCount > 0 && <option value="create">Create ({filterCounts.createCount})</option>}
82
+ {filterCounts.updateCount > 0 && <option value="update">Update ({filterCounts.updateCount})</option>}
83
+ {filterCounts.regularCount > 0 && <option value="style:regular">Regular ({filterCounts.regularCount})</option>}
84
+ {filterCounts.italicCount > 0 && <option value="style:italic">Italic ({filterCounts.italicCount})</option>}
85
+ {filterCounts.errorCount > 0 && <option value="error">Errors ({filterCounts.errorCount})</option>}
86
+ {filterCounts.conflictCount > 0 && <option value="conflict">Conflicts ({filterCounts.conflictCount})</option>}
87
+ {subfamilies.length > 1 && subfamilies.map(sf => (
88
+ <option key={sf} value={`sf:${sf}`}>{sf} ({filterCounts.subfamilyCounts[sf]})</option>
89
+ ))}
90
+ </Select>
91
+ </Flex>
92
+
93
+ {/* Visible count */}
94
+ {visibleCount !== fontCount && (
95
+ <Text size={0} muted>{visibleCount} of {fontCount}</Text>
96
+ )}
97
+ </Flex>
98
+ );
99
+ }
@@ -1,152 +1,152 @@
1
- // Existing document resolution UI — toggle between update existing and create new
2
-
3
- import React from 'react';
4
- import { Card, Stack, Flex, Text, Badge, Button, Label, Switch, Tooltip, Box } from '@sanity/ui';
5
- import { InfoOutlineIcon } from '@sanity/icons';
6
- import { RECOMMENDATION } from '../utils/planTypes';
7
-
8
- /**
9
- * Displays the existing document resolution decision as a toggle.
10
- * When "update" is on, shows the matched document. When off, indicates a new document will be created.
11
- */
12
- export default function ExistingDocumentResolver({ decision, tempId, dispatch }) {
13
- if (!decision) return null;
14
-
15
- const { recommendation, exact, candidates, userChoice, selectedCandidate, lookupFailed } = decision;
16
-
17
- const effectiveAction = userChoice ||
18
- (recommendation === RECOMMENDATION.USE_EXACT || recommendation === RECOMMENDATION.USE_CANDIDATE ? 'update' : 'create');
19
- const isUpdating = effectiveAction === 'update';
20
- const hasMatch = exact || candidates?.length > 0;
21
-
22
- const handleToggle = () => {
23
- dispatch({ type: 'SET_FONT_ACTION', tempId, decision: isUpdating ? 'create' : 'update' });
24
- };
25
-
26
- const handleSelectCandidate = (candidate) => {
27
- dispatch({ type: 'SET_FONT_CANDIDATE', tempId, candidate });
28
- };
29
-
30
- // Lookup failed
31
- if (lookupFailed) {
32
- return (
33
- <Card tone="caution" border padding={2} radius={1}>
34
- <Text size={0}>Could not check for existing documents — will create new.</Text>
35
- </Card>
36
- );
37
- }
38
-
39
- // No match at all — just creating
40
- if (recommendation === RECOMMENDATION.CREATE && !userChoice) {
41
- return (
42
- <Stack space={2}>
43
- <Label size={0}>Existing Document</Label>
44
- <Card tone="default" border padding={2} radius={1}>
45
- <Text size={1}>No existing document found — will create new.</Text>
46
- </Card>
47
- </Stack>
48
- );
49
- }
50
-
51
- // Has a match (exact, candidate, or ambiguous) — show toggle
52
- if (hasMatch) {
53
- const matchDoc = exact || selectedCandidate || candidates?.[0];
54
- const matchType = exact ? 'Exact Match' : candidates?.length > 1 ? 'Multiple Matches' : 'Likely Match';
55
- const matchTone = exact ? 'positive' : 'caution';
56
-
57
- return (
58
- <Stack space={2}>
59
- <Label size={0}>Existing Document</Label>
60
-
61
- {/* Toggle */}
62
- <Flex align="center" gap={2}>
63
- <Switch
64
- checked={isUpdating}
65
- onChange={handleToggle}
66
- style={{ cursor: 'pointer' }}
67
- />
68
- <Stack space={1}>
69
- <Text size={1} weight="semibold">
70
- {isUpdating ? 'Update existing document' : 'Create new document'}
71
- </Text>
72
- <Text size={0} muted>
73
- {isUpdating
74
- ? 'Files will be uploaded to the matched document below.'
75
- : 'A new document will be created. You may need to update the Document ID above to avoid conflicts.'
76
- }
77
- </Text>
78
- </Stack>
79
- </Flex>
80
-
81
- {/* Match card — greyed out when not updating */}
82
- <Card
83
- tone={isUpdating ? matchTone : 'default'}
84
- border
85
- padding={2}
86
- radius={1}
87
- style={{ opacity: isUpdating ? 1 : 0.5, transition: 'opacity 0.15s ease' }}
88
- >
89
- <Stack space={2}>
90
- <Flex align="center" gap={2}>
91
- <Badge tone={isUpdating ? matchTone : 'default'} fontSize={0}>{matchType}</Badge>
92
- <Text size={1} style={{ fontFamily: 'monospace' }}>{matchDoc?._id}</Text>
93
- </Flex>
94
- <Text size={0} muted>
95
- {matchDoc?.title} · {matchDoc?.weightName} · {matchDoc?.style}
96
- {matchDoc?.subfamily ? ` · ${matchDoc.subfamily}` : ''}
97
- </Text>
98
- </Stack>
99
- </Card>
100
-
101
- {/* Multiple candidates — show selector when updating */}
102
- {isUpdating && candidates?.length > 1 && (
103
- <Stack space={1}>
104
- <Text size={0} muted>Select which document to update:</Text>
105
- {candidates.map((candidate) => (
106
- <Card
107
- key={candidate._id}
108
- border
109
- radius={1}
110
- padding={2}
111
- tone={selectedCandidate?._id === candidate._id ? 'positive' : 'default'}
112
- style={{ cursor: 'pointer' }}
113
- onClick={() => handleSelectCandidate(candidate)}
114
- >
115
- <Flex align="center" gap={2}>
116
- <input
117
- type="radio"
118
- name={`candidate-${tempId}`}
119
- checked={selectedCandidate?._id === candidate._id}
120
- onChange={() => handleSelectCandidate(candidate)}
121
- style={{ cursor: 'pointer' }}
122
- />
123
- <Stack space={1} style={{ flex: 1 }}>
124
- <Text size={1} style={{ fontFamily: 'monospace' }}>{candidate._id}</Text>
125
- <Text size={0} muted>
126
- {candidate.title} · {candidate.weightName} · {candidate.style}
127
- {candidate.subfamily ? ` · ${candidate.subfamily}` : ''}
128
- </Text>
129
- </Stack>
130
- </Flex>
131
- </Card>
132
- ))}
133
- </Stack>
134
- )}
135
- </Stack>
136
- );
137
- }
138
-
139
- // User chose create after initially having a match (fallback)
140
- if (userChoice === 'create') {
141
- return (
142
- <Stack space={2}>
143
- <Label size={0}>Existing Document</Label>
144
- <Card border padding={2} radius={1}>
145
- <Text size={1}>Will create new document</Text>
146
- </Card>
147
- </Stack>
148
- );
149
- }
150
-
151
- return null;
152
- }
1
+ // Existing document resolution UI — toggle between update existing and create new
2
+
3
+ import React from 'react';
4
+ import { Card, Stack, Flex, Text, Badge, Button, Label, Switch, Tooltip, Box } from '@sanity/ui';
5
+ import { InfoOutlineIcon } from '@sanity/icons';
6
+ import { RECOMMENDATION } from '../utils/planTypes';
7
+
8
+ /**
9
+ * Displays the existing document resolution decision as a toggle.
10
+ * When "update" is on, shows the matched document. When off, indicates a new document will be created.
11
+ */
12
+ export default function ExistingDocumentResolver({ decision, tempId, dispatch }) {
13
+ if (!decision) return null;
14
+
15
+ const { recommendation, exact, candidates, userChoice, selectedCandidate, lookupFailed } = decision;
16
+
17
+ const effectiveAction = userChoice ||
18
+ (recommendation === RECOMMENDATION.USE_EXACT || recommendation === RECOMMENDATION.USE_CANDIDATE ? 'update' : 'create');
19
+ const isUpdating = effectiveAction === 'update';
20
+ const hasMatch = exact || candidates?.length > 0;
21
+
22
+ const handleToggle = () => {
23
+ dispatch({ type: 'SET_FONT_ACTION', tempId, decision: isUpdating ? 'create' : 'update' });
24
+ };
25
+
26
+ const handleSelectCandidate = (candidate) => {
27
+ dispatch({ type: 'SET_FONT_CANDIDATE', tempId, candidate });
28
+ };
29
+
30
+ // Lookup failed
31
+ if (lookupFailed) {
32
+ return (
33
+ <Card tone="caution" border padding={2} radius={1}>
34
+ <Text size={0}>Could not check for existing documents — will create new.</Text>
35
+ </Card>
36
+ );
37
+ }
38
+
39
+ // No match at all — just creating
40
+ if (recommendation === RECOMMENDATION.CREATE && !userChoice) {
41
+ return (
42
+ <Stack space={2}>
43
+ <Label size={0}>Existing Document</Label>
44
+ <Card tone="default" border padding={2} radius={1}>
45
+ <Text size={1}>No existing document found — will create new.</Text>
46
+ </Card>
47
+ </Stack>
48
+ );
49
+ }
50
+
51
+ // Has a match (exact, candidate, or ambiguous) — show toggle
52
+ if (hasMatch) {
53
+ const matchDoc = exact || selectedCandidate || candidates?.[0];
54
+ const matchType = exact ? 'Exact Match' : candidates?.length > 1 ? 'Multiple Matches' : 'Likely Match';
55
+ const matchTone = exact ? 'positive' : 'caution';
56
+
57
+ return (
58
+ <Stack space={2}>
59
+ <Label size={0}>Existing Document</Label>
60
+
61
+ {/* Toggle */}
62
+ <Flex align="center" gap={2}>
63
+ <Switch
64
+ checked={isUpdating}
65
+ onChange={handleToggle}
66
+ style={{ cursor: 'pointer' }}
67
+ />
68
+ <Stack space={1}>
69
+ <Text size={1} weight="semibold">
70
+ {isUpdating ? 'Update existing document' : 'Create new document'}
71
+ </Text>
72
+ <Text size={0} muted>
73
+ {isUpdating
74
+ ? 'Files will be uploaded to the matched document below.'
75
+ : 'A new document will be created. You may need to update the Document ID above to avoid conflicts.'
76
+ }
77
+ </Text>
78
+ </Stack>
79
+ </Flex>
80
+
81
+ {/* Match card — greyed out when not updating */}
82
+ <Card
83
+ tone={isUpdating ? matchTone : 'default'}
84
+ border
85
+ padding={2}
86
+ radius={1}
87
+ style={{ opacity: isUpdating ? 1 : 0.5, transition: 'opacity 0.15s ease' }}
88
+ >
89
+ <Stack space={2}>
90
+ <Flex align="center" gap={2}>
91
+ <Badge tone={isUpdating ? matchTone : 'default'} fontSize={0}>{matchType}</Badge>
92
+ <Text size={1} style={{ fontFamily: 'monospace' }}>{matchDoc?._id}</Text>
93
+ </Flex>
94
+ <Text size={0} muted>
95
+ {matchDoc?.title} · {matchDoc?.weightName} · {matchDoc?.style}
96
+ {matchDoc?.subfamily ? ` · ${matchDoc.subfamily}` : ''}
97
+ </Text>
98
+ </Stack>
99
+ </Card>
100
+
101
+ {/* Multiple candidates — show selector when updating */}
102
+ {isUpdating && candidates?.length > 1 && (
103
+ <Stack space={1}>
104
+ <Text size={0} muted>Select which document to update:</Text>
105
+ {candidates.map((candidate) => (
106
+ <Card
107
+ key={candidate._id}
108
+ border
109
+ radius={1}
110
+ padding={2}
111
+ tone={selectedCandidate?._id === candidate._id ? 'positive' : 'default'}
112
+ style={{ cursor: 'pointer' }}
113
+ onClick={() => handleSelectCandidate(candidate)}
114
+ >
115
+ <Flex align="center" gap={2}>
116
+ <input
117
+ type="radio"
118
+ name={`candidate-${tempId}`}
119
+ checked={selectedCandidate?._id === candidate._id}
120
+ onChange={() => handleSelectCandidate(candidate)}
121
+ style={{ cursor: 'pointer' }}
122
+ />
123
+ <Stack space={1} style={{ flex: 1 }}>
124
+ <Text size={1} style={{ fontFamily: 'monospace' }}>{candidate._id}</Text>
125
+ <Text size={0} muted>
126
+ {candidate.title} · {candidate.weightName} · {candidate.style}
127
+ {candidate.subfamily ? ` · ${candidate.subfamily}` : ''}
128
+ </Text>
129
+ </Stack>
130
+ </Flex>
131
+ </Card>
132
+ ))}
133
+ </Stack>
134
+ )}
135
+ </Stack>
136
+ );
137
+ }
138
+
139
+ // User chose create after initially having a match (fallback)
140
+ if (userChoice === 'create') {
141
+ return (
142
+ <Stack space={2}>
143
+ <Label size={0}>Existing Document</Label>
144
+ <Card border padding={2} radius={1}>
145
+ <Text size={1}>Will create new document</Text>
146
+ </Card>
147
+ </Stack>
148
+ );
149
+ }
150
+
151
+ return null;
152
+ }