@liiift-studio/sanity-font-manager 2.4.0 → 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.
- package/dist/UploadModal-6LIX7XOK.js +6 -0
- package/dist/UploadModal-NME2W53V.mjs +6 -0
- package/dist/chunk-646WCBRR.mjs +7276 -0
- package/dist/chunk-FH4QKHOH.js +7276 -0
- package/dist/index.js +664 -1647
- package/dist/index.mjs +317 -1209
- package/package.json +5 -5
- package/src/components/BatchUploadFonts.jsx +57 -44
- package/src/components/BulkActions.jsx +99 -0
- package/src/components/ExistingDocumentResolver.jsx +152 -0
- package/src/components/FontReviewCard.jsx +415 -0
- package/src/components/SingleUploaderTool.jsx +3 -4
- package/src/components/UploadModal.jsx +268 -0
- package/src/components/UploadScriptsComponent.jsx +23 -21
- package/src/components/UploadStep1Settings.jsx +272 -0
- package/src/components/UploadStep2Review.jsx +472 -0
- package/src/components/UploadStep3Execute.jsx +234 -0
- package/src/components/UploadSummary.jsx +196 -0
- package/src/index.js +45 -0
- package/src/utils/buildUploadPlan.js +325 -0
- package/src/utils/executeUploadPlan.js +437 -0
- package/src/utils/executionReducer.js +56 -0
- package/src/utils/fontHelpers.js +267 -0
- package/src/utils/generateCssFile.js +79 -77
- package/src/utils/generateFontData.js +47 -94
- package/src/utils/getEmptyFontKit.js +19 -17
- package/src/utils/parseFont.js +55 -0
- package/src/utils/planReducer.js +517 -0
- package/src/utils/planTypes.js +183 -0
- package/src/utils/processFontFiles.js +120 -78
- package/src/utils/regenerateFontData.js +2 -2
- package/src/utils/resolveExistingFont.js +87 -0
- package/src/utils/uploadFontFiles.js +405 -405
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liiift-studio/sanity-font-manager",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
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",
|
|
@@ -51,17 +51,17 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"base-64": "^1.0.0",
|
|
54
|
-
"
|
|
55
|
-
"fontkit": "^2.0.2",
|
|
54
|
+
"lib-font": "^3.0.1",
|
|
56
55
|
"nanoid": "^5.0.0",
|
|
56
|
+
"pako": "^2.1.0",
|
|
57
57
|
"slugify": "^1.6.6"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
+
"@liiift-studio/sanity-advanced-reference-array": ">=1",
|
|
60
61
|
"@sanity/icons": ">=3",
|
|
61
62
|
"@sanity/ui": ">=3",
|
|
62
63
|
"react": ">=18",
|
|
63
|
-
"sanity": ">=3"
|
|
64
|
-
"@liiift-studio/sanity-advanced-reference-array": ">=1"
|
|
64
|
+
"sanity": ">=3"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"@liiift-studio/sanity-advanced-reference-array": {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Batch font uploader — drag-and-drop file list, confirm-to-upload, elapsed timer, Wake Lock, and beforeunload guard for long uploads
|
|
2
2
|
|
|
3
|
-
import React, { useCallback, useState, useMemo, useRef, useEffect } from 'react';
|
|
3
|
+
import React, { useCallback, useState, useMemo, useRef, useEffect, lazy, Suspense } from 'react';
|
|
4
4
|
import { Card, Box, Flex, Grid, Text, Label, Switch, Button, Spinner, Tooltip, Stack } from '@sanity/ui';
|
|
5
5
|
import { ControlsIcon, InfoOutlineIcon, TrashIcon, UploadIcon, WarningOutlineIcon } from '@sanity/icons';
|
|
6
6
|
import { useFormValue } from 'sanity';
|
|
7
7
|
|
|
8
|
+
const UploadModal = lazy(() => import('./UploadModal'));
|
|
9
|
+
|
|
8
10
|
import { useSanityClient } from '../hooks/useSanityClient';
|
|
9
11
|
import { processFontFiles } from '../utils/processFontFiles';
|
|
10
12
|
import { uploadFontFiles } from '../utils/uploadFontFiles';
|
|
@@ -39,6 +41,7 @@ export const BatchUploadFonts = () => {
|
|
|
39
41
|
const [pendingFiles, setPendingFiles] = useState([]);
|
|
40
42
|
const [isDragging, setIsDragging] = useState(false);
|
|
41
43
|
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
|
44
|
+
const [showUploadModal, setShowUploadModal] = useState(false);
|
|
42
45
|
|
|
43
46
|
const fileInputRef = useRef(null);
|
|
44
47
|
const elapsedTimerRef = useRef(null);
|
|
@@ -522,9 +525,28 @@ export const BatchUploadFonts = () => {
|
|
|
522
525
|
);
|
|
523
526
|
};
|
|
524
527
|
|
|
528
|
+
const hasRequiredFields = title && title !== '' && slug && slug !== '';
|
|
529
|
+
|
|
525
530
|
return (
|
|
526
531
|
<>
|
|
527
|
-
{
|
|
532
|
+
{!hasRequiredFields && (
|
|
533
|
+
<Card border padding={4} radius={2} tone="caution">
|
|
534
|
+
<Flex align="center" gap={3}>
|
|
535
|
+
<Text size={2}>
|
|
536
|
+
<WarningOutlineIcon />
|
|
537
|
+
</Text>
|
|
538
|
+
<Stack space={2}>
|
|
539
|
+
<Text size={1} weight="semibold">
|
|
540
|
+
{!title || title === '' ? 'Title required to use font uploader' : 'Slug required to use font uploader'}
|
|
541
|
+
</Text>
|
|
542
|
+
<Text size={1} muted>
|
|
543
|
+
Add a {!title || title === '' ? 'title' : 'slug'} to this typeface document, then return to the Styles tab to upload fonts.
|
|
544
|
+
</Text>
|
|
545
|
+
</Stack>
|
|
546
|
+
</Flex>
|
|
547
|
+
</Card>
|
|
548
|
+
)}
|
|
549
|
+
{hasRequiredFields &&
|
|
528
550
|
<>
|
|
529
551
|
<StatusDisplay
|
|
530
552
|
status={status}
|
|
@@ -542,9 +564,37 @@ export const BatchUploadFonts = () => {
|
|
|
542
564
|
}
|
|
543
565
|
/>
|
|
544
566
|
|
|
545
|
-
<
|
|
546
|
-
|
|
547
|
-
|
|
567
|
+
<Button
|
|
568
|
+
mode="default"
|
|
569
|
+
tone="primary"
|
|
570
|
+
icon={UploadIcon}
|
|
571
|
+
text="Upload Fonts"
|
|
572
|
+
fontSize={2}
|
|
573
|
+
padding={4}
|
|
574
|
+
onClick={() => setShowUploadModal(true)}
|
|
575
|
+
style={{ width: '100%' }}
|
|
576
|
+
/>
|
|
577
|
+
|
|
578
|
+
{/* New upload modal */}
|
|
579
|
+
{showUploadModal && (
|
|
580
|
+
<Suspense fallback={<Spinner />}>
|
|
581
|
+
<UploadModal
|
|
582
|
+
open={showUploadModal}
|
|
583
|
+
onClose={() => setShowUploadModal(false)}
|
|
584
|
+
client={client}
|
|
585
|
+
docId={doc_id}
|
|
586
|
+
typefaceTitle={title}
|
|
587
|
+
stylesObject={stylesObject}
|
|
588
|
+
preferredStyleRef={preferredStyleRef}
|
|
589
|
+
slug={slug}
|
|
590
|
+
/>
|
|
591
|
+
</Suspense>
|
|
592
|
+
)}
|
|
593
|
+
|
|
594
|
+
{/* Utilities panel — toggled via the Utilities button */}
|
|
595
|
+
{showUtilities && (
|
|
596
|
+
<Card border padding={3} shadow={1} radius={2} marginTop={3}>
|
|
597
|
+
<Stack space={4}>
|
|
548
598
|
|
|
549
599
|
{/* Regenerate Subfamilies */}
|
|
550
600
|
<Stack space={2}>
|
|
@@ -594,45 +644,8 @@ export const BatchUploadFonts = () => {
|
|
|
594
644
|
</Stack>
|
|
595
645
|
|
|
596
646
|
</Stack>
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
? <>
|
|
600
|
-
<Grid columns={[2]} gap={4} marginTop={1} marginBottom={1}>
|
|
601
|
-
{/* Left: price */}
|
|
602
|
-
<Box>
|
|
603
|
-
<PriceInput inputPrice={inputPrice} handleInputChange={handleInputChange} />
|
|
604
|
-
</Box>
|
|
605
|
-
{/* Right: toggles */}
|
|
606
|
-
<Stack space={3}>
|
|
607
|
-
<Flex align="center" gap={2}>
|
|
608
|
-
<Switch
|
|
609
|
-
checked={preserveShortenedNames}
|
|
610
|
-
onChange={(e) => setPreserveShortenedNames(e.target.checked)}
|
|
611
|
-
/>
|
|
612
|
-
{renderTooltipLabel(
|
|
613
|
-
'Preserve shortened names',
|
|
614
|
-
'Abbreviations in font names are kept as-is (e.g. "XNarrow" stays "XNarrow", "Bd" stays "Bd").'
|
|
615
|
-
)}
|
|
616
|
-
</Flex>
|
|
617
|
-
<Flex align="center" gap={2}>
|
|
618
|
-
<Switch
|
|
619
|
-
checked={preserveFileNames}
|
|
620
|
-
onChange={(e) => setPreserveFileNames(e.target.checked)}
|
|
621
|
-
/>
|
|
622
|
-
{renderTooltipLabel(
|
|
623
|
-
'Preserve file names',
|
|
624
|
-
'Original filename capitalisation is used for asset naming instead of the normalised font title.'
|
|
625
|
-
)}
|
|
626
|
-
</Flex>
|
|
627
|
-
</Stack>
|
|
628
|
-
</Grid>
|
|
629
|
-
<Box marginTop={3}>
|
|
630
|
-
{pendingFiles.length === 0 ? renderDropZone() : renderFileList()}
|
|
631
|
-
</Box>
|
|
632
|
-
</>
|
|
633
|
-
: renderProcessing()
|
|
634
|
-
)}
|
|
635
|
-
</Card>
|
|
647
|
+
</Card>
|
|
648
|
+
)}
|
|
636
649
|
</>
|
|
637
650
|
}
|
|
638
651
|
</>
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|