sanity-plugin-iconify 3.0.0 → 4.0.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/README.md +1 -1
- package/dist/index.cjs +336 -296
- package/dist/index.d.cts +89 -363
- package/dist/index.d.mts +89 -0
- package/dist/index.mjs +369 -0
- package/package.json +71 -49
- package/src/combobox/search-input.tsx +12 -2
- package/src/lib/api.ts +1 -0
- package/src/lib/icon-types.gen.ts +1 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts +0 -363
- package/dist/index.js +0 -362
- package/dist/index.js.map +0 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { definePlugin, set, unset } from "sanity";
|
|
2
|
+
import { Box, Button, Card, Flex, Grid, Popover, Stack, Text, TextInput, ThemeProvider, useToast } from "@sanity/ui";
|
|
3
|
+
import { buildTheme } from "@sanity/ui/theme";
|
|
4
|
+
import { forwardRef, memo, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { useCombobox } from "downshift";
|
|
6
|
+
import { match } from "ts-pattern";
|
|
7
|
+
import { QueryClient, QueryClientProvider, keepPreviousData, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
8
|
+
import { useDebounce } from "use-debounce";
|
|
9
|
+
import styled from "styled-components";
|
|
10
|
+
import { Icon } from "@iconify/react";
|
|
11
|
+
import { TrashIcon } from "@sanity/icons";
|
|
12
|
+
import { stringToIcon } from "@iconify/utils";
|
|
13
|
+
import { sentenceCase } from "change-case";
|
|
14
|
+
//#region src/lib/api.ts
|
|
15
|
+
const BASE_API_URL = "https://api.iconify.design";
|
|
16
|
+
function fetchJson({ url, signal }) {
|
|
17
|
+
return fetch(url, { signal }).then((response) => {
|
|
18
|
+
if (!response.ok) throw new Error(`Network error: status ${response.status}`);
|
|
19
|
+
return response.json();
|
|
20
|
+
}).then((result) => result, (error) => {
|
|
21
|
+
if (error instanceof Error) throw error;
|
|
22
|
+
else {
|
|
23
|
+
console.error(`Unknown error: ${error}`);
|
|
24
|
+
throw new Error("Something went wrong");
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function useSearch({ collections }) {
|
|
29
|
+
const queryClient = useQueryClient();
|
|
30
|
+
const [term, setTerm] = useState("");
|
|
31
|
+
const [debouncedTerm, setDebouncedTerm] = useDebounce(term, 500);
|
|
32
|
+
const updateTerm = useCallback((newTerm, updateImmediately = false) => {
|
|
33
|
+
setTerm(newTerm);
|
|
34
|
+
if (updateImmediately) setDebouncedTerm(newTerm);
|
|
35
|
+
}, [setDebouncedTerm]);
|
|
36
|
+
const { isLoading, isError, error, data, isPlaceholderData } = useQuery({
|
|
37
|
+
queryKey: [
|
|
38
|
+
"search",
|
|
39
|
+
collections,
|
|
40
|
+
debouncedTerm
|
|
41
|
+
],
|
|
42
|
+
queryFn: async ({ signal }) => {
|
|
43
|
+
const url = new URL(`/search`, BASE_API_URL);
|
|
44
|
+
url.searchParams.append("query", debouncedTerm);
|
|
45
|
+
url.searchParams.append("limit", "60");
|
|
46
|
+
if (collections) url.searchParams.append("prefixes", collections.join(","));
|
|
47
|
+
const result = debouncedTerm ? await fetchJson({
|
|
48
|
+
url,
|
|
49
|
+
signal
|
|
50
|
+
}) : null;
|
|
51
|
+
if (result) Object.entries(result.collections).forEach(([prefix, info]) => {
|
|
52
|
+
queryClient.setQueryData(["iconSetInfo", prefix], info);
|
|
53
|
+
});
|
|
54
|
+
return result?.icons ?? [];
|
|
55
|
+
},
|
|
56
|
+
enabled: debouncedTerm.length > 0,
|
|
57
|
+
placeholderData: keepPreviousData,
|
|
58
|
+
staleTime: 300 * 1e3
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
term,
|
|
62
|
+
setTerm: updateTerm,
|
|
63
|
+
debouncedTerm,
|
|
64
|
+
isLoading,
|
|
65
|
+
isError,
|
|
66
|
+
error,
|
|
67
|
+
data,
|
|
68
|
+
isPreviousData: isPlaceholderData
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function useIconSetInfo({ prefix }) {
|
|
72
|
+
return useQuery({
|
|
73
|
+
queryKey: ["iconSetInfo", prefix],
|
|
74
|
+
queryFn: async ({ signal }) => {
|
|
75
|
+
if (!prefix) return null;
|
|
76
|
+
const url = new URL("/collection", BASE_API_URL);
|
|
77
|
+
url.searchParams.append("prefix", prefix);
|
|
78
|
+
url.searchParams.append("info", "true");
|
|
79
|
+
return (await fetchJson({
|
|
80
|
+
url,
|
|
81
|
+
signal
|
|
82
|
+
}))?.info ?? null;
|
|
83
|
+
},
|
|
84
|
+
staleTime: Infinity
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
styled(Grid)`
|
|
88
|
+
grid-template-columns: 1fr min-content;
|
|
89
|
+
position: relative;
|
|
90
|
+
`;
|
|
91
|
+
const OptionsWrapper = styled(Box)`
|
|
92
|
+
box-sizing: border-box;
|
|
93
|
+
padding: 0.5rem;
|
|
94
|
+
|
|
95
|
+
& [role='listbox'] {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-wrap: wrap;
|
|
98
|
+
gap: 0.5rem;
|
|
99
|
+
margin: 0;
|
|
100
|
+
padding: 0;
|
|
101
|
+
list-style: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
& [role='option'] {
|
|
105
|
+
display: grid;
|
|
106
|
+
place-items: center;
|
|
107
|
+
width: clamp(3rem, 10vw, 4rem);
|
|
108
|
+
|
|
109
|
+
& button {
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
width: 100%;
|
|
112
|
+
|
|
113
|
+
& > [data-ui='Box'] {
|
|
114
|
+
display: flex;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
& svg {
|
|
118
|
+
aspect-ratio: 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
function MessageWrapper({ children }) {
|
|
124
|
+
return <Card padding={4}>
|
|
125
|
+
<Text align="center" muted>
|
|
126
|
+
{children}
|
|
127
|
+
</Text>
|
|
128
|
+
</Card>;
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/combobox/search-input.tsx
|
|
132
|
+
const SearchInput = memo(forwardRef((props, ref) => {
|
|
133
|
+
const { selectedIcon, suffix, onChange, ...rest } = props;
|
|
134
|
+
return <TextInput {...rest} onChange={onChange} ref={ref} inputMode="search" icon={selectedIcon ? <Icon icon={selectedIcon} /> : null} placeholder={selectedIcon ? "Search and replace selected icon..." : "Search for an icon..."} suffix={suffix} />;
|
|
135
|
+
}));
|
|
136
|
+
SearchInput.displayName = "SearchInput";
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/combobox/search-result.tsx
|
|
139
|
+
const SearchResults = memo(forwardRef((props, ref) => {
|
|
140
|
+
const { state, data, getItemProps, highlightedIndex, ...rest } = props;
|
|
141
|
+
return <>
|
|
142
|
+
{(() => {
|
|
143
|
+
switch (state) {
|
|
144
|
+
case "initial": return <MessageWrapper>Search for icons</MessageWrapper>;
|
|
145
|
+
case "loading": return <MessageWrapper>Searching...</MessageWrapper>;
|
|
146
|
+
case "error": return <MessageWrapper>Something went wrong...</MessageWrapper>;
|
|
147
|
+
case "empty": return <MessageWrapper>No icons found</MessageWrapper>;
|
|
148
|
+
}
|
|
149
|
+
})()}
|
|
150
|
+
|
|
151
|
+
<ul {...rest} ref={ref} data-testid="iconify-results" style={{ opacity: state === "stale" ? .5 : 1 }}>
|
|
152
|
+
{(state === "data" || state === "stale") && data?.map((icon, index) => <li key={icon} {...getItemProps({
|
|
153
|
+
item: icon,
|
|
154
|
+
index
|
|
155
|
+
})}>
|
|
156
|
+
<Button padding={3} mode="bleed" selected={index === highlightedIndex}>
|
|
157
|
+
<Icon icon={icon} width="100%" height="100%" />
|
|
158
|
+
</Button>
|
|
159
|
+
</li>)}
|
|
160
|
+
</ul>
|
|
161
|
+
</>;
|
|
162
|
+
}));
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/combobox/unset-button.tsx
|
|
165
|
+
function UnsetButton(props) {
|
|
166
|
+
const { onUnset } = props;
|
|
167
|
+
return <Card border borderLeft={false} padding={1} display="flex" radius={2}>
|
|
168
|
+
<Button data-testid="iconify-unset" icon={<TrashIcon />} onClick={onUnset} mode="bleed" fontSize={1} padding={2} />
|
|
169
|
+
</Card>;
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/combobox/iconify-combobox.tsx
|
|
173
|
+
const IconifyCombobox = memo(function IconifyCombobox(props) {
|
|
174
|
+
const { selectedIcon, onSelect: pushSelection, collections, studioElementProps, fieldFocused } = props;
|
|
175
|
+
const id = useId();
|
|
176
|
+
const toast = useToast();
|
|
177
|
+
const inputRef = useRef(null);
|
|
178
|
+
const composedInputRef = useCallback((node) => {
|
|
179
|
+
inputRef.current = node;
|
|
180
|
+
if (studioElementProps?.ref) studioElementProps.ref.current = node;
|
|
181
|
+
}, [studioElementProps?.ref]);
|
|
182
|
+
const suppressNextBlur = useRef(false);
|
|
183
|
+
const handleFocus = useCallback((event) => {
|
|
184
|
+
suppressNextBlur.current = true;
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
suppressNextBlur.current = false;
|
|
187
|
+
}, 0);
|
|
188
|
+
studioElementProps?.onFocus?.(event);
|
|
189
|
+
}, [studioElementProps]);
|
|
190
|
+
const handleBlur = useCallback((event) => {
|
|
191
|
+
if (suppressNextBlur.current) {
|
|
192
|
+
suppressNextBlur.current = false;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
studioElementProps?.onBlur?.(event);
|
|
196
|
+
}, [studioElementProps]);
|
|
197
|
+
const { term, setTerm, debouncedTerm, isLoading, isError, error, data, isPreviousData } = useSearch({ collections });
|
|
198
|
+
const { isOpen, getMenuProps, getInputProps, highlightedIndex, getItemProps, selectItem, setInputValue, closeMenu } = useCombobox({
|
|
199
|
+
items: data ?? [],
|
|
200
|
+
inputValue: term,
|
|
201
|
+
onInputValueChange({ inputValue, selectedItem }) {
|
|
202
|
+
if (inputValue !== selectedItem) setTerm(inputValue);
|
|
203
|
+
},
|
|
204
|
+
onSelectedItemChange({ selectedItem }) {
|
|
205
|
+
if (selectedItem) {
|
|
206
|
+
pushSelection(selectedItem);
|
|
207
|
+
setTerm("", true);
|
|
208
|
+
setInputValue("");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
if (!fieldFocused) closeMenu();
|
|
214
|
+
}, [fieldFocused, closeMenu]);
|
|
215
|
+
const handleUnset = useCallback(() => {
|
|
216
|
+
pushSelection("");
|
|
217
|
+
setTerm("", true);
|
|
218
|
+
selectItem("");
|
|
219
|
+
}, [
|
|
220
|
+
pushSelection,
|
|
221
|
+
setTerm,
|
|
222
|
+
selectItem
|
|
223
|
+
]);
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (isError) {
|
|
226
|
+
console.error("Iconify input error:", error);
|
|
227
|
+
toast.push({
|
|
228
|
+
id,
|
|
229
|
+
status: "error",
|
|
230
|
+
title: "Iconify input error",
|
|
231
|
+
description: error?.message
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}, [
|
|
235
|
+
error,
|
|
236
|
+
id,
|
|
237
|
+
isError,
|
|
238
|
+
toast
|
|
239
|
+
]);
|
|
240
|
+
const { onBlur: _downshiftOnBlur, ...inputProps } = getInputProps({
|
|
241
|
+
ref: composedInputRef,
|
|
242
|
+
id: studioElementProps?.id,
|
|
243
|
+
"aria-describedby": studioElementProps?.["aria-describedby"],
|
|
244
|
+
onFocus: handleFocus
|
|
245
|
+
});
|
|
246
|
+
return <div>
|
|
247
|
+
<SearchInput {...inputProps} onBlur={handleBlur} selectedIcon={selectedIcon} suffix={selectedIcon ? <UnsetButton onUnset={handleUnset} /> : null} />
|
|
248
|
+
|
|
249
|
+
<Popover open={true} style={{ display: isOpen ? "block" : "none" }} placement="bottom" arrow={false} matchReferenceWidth constrainSize referenceElement={inputRef.current} content={<OptionsWrapper>
|
|
250
|
+
<SearchResults {...getMenuProps()} state={match(true).returnType().with(isLoading, () => "loading").with(!debouncedTerm, () => "initial").with(isError, () => "error").with(!data || data.length === 0, () => "empty").with(isPreviousData, () => "stale").otherwise(() => "data")} data={data} getItemProps={getItemProps} highlightedIndex={highlightedIndex} />
|
|
251
|
+
</OptionsWrapper>} />
|
|
252
|
+
</div>;
|
|
253
|
+
});
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/lib/query-client.tsx
|
|
256
|
+
const queryClient = new QueryClient();
|
|
257
|
+
function QueryClientProvider$1(props) {
|
|
258
|
+
return <QueryClientProvider client={queryClient}>{props.children}</QueryClientProvider>;
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/lib/use-pretty-icon-name.ts
|
|
262
|
+
function usePrettyIconName(props) {
|
|
263
|
+
const { name } = props;
|
|
264
|
+
const iconMeta = useMemo(() => props.iconMeta ?? (name ? stringToIcon(name) : null), [name, props.iconMeta]);
|
|
265
|
+
const iconSetInfo = useIconSetInfo({ prefix: iconMeta?.prefix ?? null });
|
|
266
|
+
return useMemo(() => iconMeta ? {
|
|
267
|
+
name: sentenceCase(iconMeta.name),
|
|
268
|
+
collection: iconSetInfo.data?.name ?? iconMeta.prefix
|
|
269
|
+
} : null, [iconMeta, iconSetInfo.data?.name]);
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/iconify-input.tsx
|
|
273
|
+
const theme = buildTheme();
|
|
274
|
+
const IconifyInput = memo(function IconifyInput(props) {
|
|
275
|
+
const { config, value, onChange: pushChange, schemaType, elementProps, focused } = props;
|
|
276
|
+
const selectedIcon = value?.name ?? null;
|
|
277
|
+
const options = schemaType.options;
|
|
278
|
+
const collections = !!options?.collections?.length && options.collections || !!config?.collections?.length && config.collections || null;
|
|
279
|
+
const showName = options?.showName ?? config?.showName ?? false;
|
|
280
|
+
const handleSelect = useCallback((icon) => {
|
|
281
|
+
pushChange(icon === "" ? unset() : set(icon, ["name"]));
|
|
282
|
+
}, [pushChange]);
|
|
283
|
+
return <QueryClientProvider$1>
|
|
284
|
+
<ThemeProvider theme={theme}>
|
|
285
|
+
<Stack space={2}>
|
|
286
|
+
<IconifyCombobox selectedIcon={selectedIcon} onSelect={handleSelect} collections={collections} studioElementProps={elementProps} fieldFocused={focused} />
|
|
287
|
+
|
|
288
|
+
{showName && selectedIcon ? <IconifyNameDisplay name={selectedIcon} /> : null}
|
|
289
|
+
</Stack>
|
|
290
|
+
</ThemeProvider>
|
|
291
|
+
</QueryClientProvider$1>;
|
|
292
|
+
});
|
|
293
|
+
function IconifyNameDisplay(props) {
|
|
294
|
+
const { name } = props;
|
|
295
|
+
const prettyName = usePrettyIconName({ name });
|
|
296
|
+
return <Flex gap={1}>
|
|
297
|
+
<Text size={1} muted>
|
|
298
|
+
Selected:
|
|
299
|
+
</Text>
|
|
300
|
+
|
|
301
|
+
<Text size={1} weight="semibold">
|
|
302
|
+
{prettyName?.name ?? name}
|
|
303
|
+
</Text>
|
|
304
|
+
|
|
305
|
+
{prettyName?.collection && <Text size={1} muted style={{ fontStyle: "italic" }}>
|
|
306
|
+
by {prettyName?.collection}
|
|
307
|
+
</Text>}
|
|
308
|
+
</Flex>;
|
|
309
|
+
}
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/iconify-preview.tsx
|
|
312
|
+
const IconifyPreview = memo(function IconifyPreview(props) {
|
|
313
|
+
const { title } = props;
|
|
314
|
+
const iconName = typeof props.title === "string" ? stringToIcon(props.title) : null;
|
|
315
|
+
if (typeof title === "string" && iconName) return <QueryClientProvider$1>
|
|
316
|
+
<IconifyPreviewInner {...props} iconName={title} iconMeta={iconName} />
|
|
317
|
+
</QueryClientProvider$1>;
|
|
318
|
+
return props.renderDefault(props);
|
|
319
|
+
});
|
|
320
|
+
function IconifyPreviewInner(props) {
|
|
321
|
+
const { iconMeta, iconName, ...previewProps } = props;
|
|
322
|
+
const prettyName = usePrettyIconName({ iconMeta });
|
|
323
|
+
return props.renderDefault({
|
|
324
|
+
...previewProps,
|
|
325
|
+
media: <Icon icon={iconName} />,
|
|
326
|
+
title: prettyName?.name ?? iconName,
|
|
327
|
+
subtitle: prettyName?.collection
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/iconify-plugin.tsx
|
|
332
|
+
/**
|
|
333
|
+
* Usage in `sanity.config.ts` (or .js)
|
|
334
|
+
*
|
|
335
|
+
* ```ts
|
|
336
|
+
* import { defineConfig } from 'sanity'
|
|
337
|
+
* import { iconify } from 'sanity-plugin-iconify'
|
|
338
|
+
*
|
|
339
|
+
* export default defineConfig({
|
|
340
|
+
* // ...
|
|
341
|
+
* plugins: [iconify()],
|
|
342
|
+
* })
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
const iconify = definePlugin((config = {}) => {
|
|
346
|
+
return {
|
|
347
|
+
name: "sanity-plugin-iconify",
|
|
348
|
+
schema: { types: [{
|
|
349
|
+
name: "icon",
|
|
350
|
+
title: "Icon",
|
|
351
|
+
type: "object",
|
|
352
|
+
fields: [{
|
|
353
|
+
name: "name",
|
|
354
|
+
title: "Name",
|
|
355
|
+
type: "string"
|
|
356
|
+
}],
|
|
357
|
+
components: {
|
|
358
|
+
input: (props) => <IconifyInput {...props} config={config} />,
|
|
359
|
+
preview: IconifyPreview,
|
|
360
|
+
field: (props) => props.renderDefault({
|
|
361
|
+
...props,
|
|
362
|
+
level: 0
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
}] }
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
//#endregion
|
|
369
|
+
export { iconify };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanity-plugin-iconify",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Icon picker based on Iconify",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -21,15 +21,20 @@
|
|
|
21
21
|
"exports": {
|
|
22
22
|
".": {
|
|
23
23
|
"source": "./src/index.ts",
|
|
24
|
-
"import":
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
"import": {
|
|
25
|
+
"types": "./dist/index.d.mts",
|
|
26
|
+
"default": "./dist/index.mjs"
|
|
27
|
+
},
|
|
28
|
+
"require": {
|
|
29
|
+
"types": "./dist/index.d.cts",
|
|
30
|
+
"default": "./dist/index.cjs"
|
|
31
|
+
}
|
|
27
32
|
},
|
|
28
33
|
"./package.json": "./package.json"
|
|
29
34
|
},
|
|
30
35
|
"main": "./dist/index.cjs",
|
|
31
|
-
"module": "./dist/index.
|
|
32
|
-
"types": "./dist/index.d.
|
|
36
|
+
"module": "./dist/index.mjs",
|
|
37
|
+
"types": "./dist/index.d.mts",
|
|
33
38
|
"files": [
|
|
34
39
|
"dist",
|
|
35
40
|
"sanity.json",
|
|
@@ -37,10 +42,8 @@
|
|
|
37
42
|
"v2-incompatible.js"
|
|
38
43
|
],
|
|
39
44
|
"scripts": {
|
|
40
|
-
"build": "run-s build:
|
|
41
|
-
"build:bundle": "
|
|
42
|
-
"build:check": "pkg-utils --strict",
|
|
43
|
-
"build:clean": "rimraf dist",
|
|
45
|
+
"build": "run-s build:verify build:bundle",
|
|
46
|
+
"build:bundle": "tsdown",
|
|
44
47
|
"build:verify": "plugin-kit verify-package --silent",
|
|
45
48
|
"dev": "vite dev --config dev/vite.config.ts",
|
|
46
49
|
"format": "prettier --write --cache --ignore-unknown .",
|
|
@@ -53,78 +56,97 @@
|
|
|
53
56
|
"test:e2e:ui": "playwright test --ui",
|
|
54
57
|
"test:ui": "vitest --ui",
|
|
55
58
|
"test:watch": "vitest",
|
|
56
|
-
"watch": "
|
|
59
|
+
"watch": "tsdown --watch"
|
|
57
60
|
},
|
|
58
61
|
"browserslist": "extends @sanity/browserslist-config",
|
|
59
62
|
"dependencies": {
|
|
60
63
|
"@iconify/react": "^6.0.2",
|
|
61
|
-
"@iconify/utils": "^3.1.
|
|
64
|
+
"@iconify/utils": "^3.1.3",
|
|
62
65
|
"@sanity/icons": "^3.7.4",
|
|
63
66
|
"@sanity/incompatible-plugin": "^1.0.5",
|
|
64
|
-
"@sanity/ui": "^3.
|
|
65
|
-
"@tanstack/react-query": "^5.
|
|
67
|
+
"@sanity/ui": "^3.2.0",
|
|
68
|
+
"@tanstack/react-query": "^5.101.0",
|
|
66
69
|
"change-case": "^5.4.4",
|
|
67
|
-
"downshift": "^9.
|
|
70
|
+
"downshift": "^9.3.6",
|
|
68
71
|
"ts-pattern": "^5.9.0",
|
|
69
|
-
"use-debounce": "^10.
|
|
72
|
+
"use-debounce": "^10.1.1"
|
|
70
73
|
},
|
|
71
74
|
"devDependencies": {
|
|
72
|
-
"@commitlint/cli": "^
|
|
73
|
-
"@commitlint/config-conventional": "^
|
|
74
|
-
"@iconify/json": "^2.2.
|
|
75
|
+
"@commitlint/cli": "^21.0.2",
|
|
76
|
+
"@commitlint/config-conventional": "^21.0.2",
|
|
77
|
+
"@iconify/json": "^2.2.486",
|
|
75
78
|
"@iconify/types": "^2.0.0",
|
|
76
|
-
"@playwright/test": "^1.
|
|
77
|
-
"@release-it/conventional-changelog": "^
|
|
78
|
-
"@sanity/pkg-utils": "^10.2.3",
|
|
79
|
+
"@playwright/test": "^1.61.0",
|
|
80
|
+
"@release-it/conventional-changelog": "^11.0.1",
|
|
79
81
|
"@sanity/plugin-kit": "^4.0.20",
|
|
80
|
-
"@tanstack/eslint-plugin-query": "^5.
|
|
81
|
-
"@tanstack/react-query-devtools": "^5.
|
|
82
|
-
"@types/react": "^19.2.
|
|
82
|
+
"@tanstack/eslint-plugin-query": "^5.101.0",
|
|
83
|
+
"@tanstack/react-query-devtools": "^5.101.0",
|
|
84
|
+
"@types/react": "^19.2.17",
|
|
83
85
|
"@types/styled-components": "^5.1.36",
|
|
84
|
-
"@vitejs/plugin-react": "^
|
|
85
|
-
"@vitest/ui": "^4.1.
|
|
86
|
-
"@waspeer/config": "^
|
|
87
|
-
"eslint": "
|
|
88
|
-
"lefthook": "^2.
|
|
89
|
-
"npm-run-all2": "^
|
|
90
|
-
"prettier": "^3.
|
|
91
|
-
"
|
|
92
|
-
"react
|
|
93
|
-
"react-
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
86
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
87
|
+
"@vitest/ui": "^4.1.9",
|
|
88
|
+
"@waspeer/config": "^3.0.0",
|
|
89
|
+
"eslint": "^10.1.0",
|
|
90
|
+
"lefthook": "^2.1.9",
|
|
91
|
+
"npm-run-all2": "^9.0.2",
|
|
92
|
+
"prettier": "^3.8.4",
|
|
93
|
+
"publint": "^0.3.18",
|
|
94
|
+
"react": "^19.2.7",
|
|
95
|
+
"react-dom": "^19.2.7",
|
|
96
|
+
"react-is": "^19.2.7",
|
|
97
|
+
"release-it": "^20.2.0",
|
|
98
|
+
"rimraf": "^6.1.3",
|
|
99
|
+
"sanity": "^6.0.0",
|
|
100
|
+
"tsdown": "^0.22.2",
|
|
101
|
+
"tsx": "^4.22.4",
|
|
102
|
+
"typescript": "^6.0.2",
|
|
103
|
+
"vite": "^8.0.2",
|
|
104
|
+
"vitest": "^4.1.9"
|
|
101
105
|
},
|
|
102
106
|
"peerDependencies": {
|
|
103
107
|
"react": "^19.2",
|
|
104
|
-
"sanity": "^5.0.0-0",
|
|
108
|
+
"sanity": "^5.0.0-0 || ^6.0.0-0",
|
|
105
109
|
"styled-components": "^6"
|
|
106
110
|
},
|
|
107
111
|
"packageManager": "pnpm@10.17.0",
|
|
108
112
|
"engines": {
|
|
109
|
-
"node": ">=
|
|
113
|
+
"node": ">=22.12"
|
|
110
114
|
},
|
|
111
115
|
"pnpm": {
|
|
112
116
|
"onlyBuiltDependencies": [
|
|
113
117
|
"esbuild",
|
|
114
|
-
"lefthook"
|
|
118
|
+
"lefthook",
|
|
119
|
+
"unrs-resolver"
|
|
115
120
|
],
|
|
116
121
|
"overrides": {
|
|
117
|
-
"
|
|
118
|
-
"
|
|
122
|
+
"@babel/core@<7.29.6": ">=7.29.6",
|
|
123
|
+
"@isaacs/brace-expansion@<5.0.1": ">=5.0.1",
|
|
124
|
+
"@typescript-eslint/utils": "^8.61.1",
|
|
125
|
+
"brace-expansion@>=4.0.0 <5.0.6": ">=5.0.6",
|
|
126
|
+
"brace-expansion@<2.0.3": ">=2.0.3",
|
|
127
|
+
"esbuild@<0.28.1": ">=0.28.1",
|
|
128
|
+
"follow-redirects@<1.16.0": ">=1.16.0",
|
|
129
|
+
"lodash@<4.17.23": ">=4.17.23",
|
|
119
130
|
"micromatch@<4.0.8": ">=4.0.8",
|
|
120
|
-
"
|
|
131
|
+
"minimatch@<10.2.3": ">=10.2.3",
|
|
132
|
+
"picomatch@<4.0.4": ">=4.0.4",
|
|
133
|
+
"postcss@<8.5.10": ">=8.5.10",
|
|
134
|
+
"prismjs@<1.30.0": ">=1.30.0",
|
|
135
|
+
"rollup@<4.59.0": ">=4.59.0",
|
|
136
|
+
"serialize-javascript@<7.0.5": ">=7.0.5",
|
|
137
|
+
"shell-quote@<1.8.4": ">=1.8.4",
|
|
138
|
+
"tmp@<0.2.6": ">=0.2.6",
|
|
139
|
+
"uuid@>=11.0.0 <11.1.1": ">=11.1.1"
|
|
121
140
|
}
|
|
122
141
|
},
|
|
123
142
|
"sanityPlugin": {
|
|
124
143
|
"verifyPackage": {
|
|
125
144
|
"scripts": false,
|
|
126
145
|
"eslintImports": false,
|
|
127
|
-
"nodeEngine": false
|
|
146
|
+
"nodeEngine": false,
|
|
147
|
+
"pkg-utils": false,
|
|
148
|
+
"rollupConfig": false,
|
|
149
|
+
"tsconfig": false
|
|
128
150
|
}
|
|
129
151
|
}
|
|
130
152
|
}
|
|
@@ -2,18 +2,28 @@ import { Icon } from '@iconify/react';
|
|
|
2
2
|
import { TextInput } from '@sanity/ui';
|
|
3
3
|
import { forwardRef, memo } from 'react';
|
|
4
4
|
|
|
5
|
-
interface SearchInputProps extends React.HTMLAttributes<HTMLInputElement> {
|
|
5
|
+
interface SearchInputProps extends Omit<React.HTMLAttributes<HTMLInputElement>, 'onChange'> {
|
|
6
|
+
// downshift's getInputProps() returns onChange typed against the generic
|
|
7
|
+
// `Element` (React.ChangeEventHandler with no element parameter), which is
|
|
8
|
+
// wider than HTMLAttributes' onChange. Accept that signature so the props
|
|
9
|
+
// spread from getInputProps() stays type-compatible.
|
|
10
|
+
onChange?: React.ChangeEventHandler;
|
|
6
11
|
selectedIcon: string | null;
|
|
7
12
|
suffix?: React.ReactNode;
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
export const SearchInput = memo(
|
|
11
16
|
forwardRef<HTMLInputElement, SearchInputProps>((props, ref) => {
|
|
12
|
-
const { selectedIcon, suffix, ...rest } = props;
|
|
17
|
+
const { selectedIcon, suffix, onChange, ...rest } = props;
|
|
13
18
|
|
|
14
19
|
return (
|
|
15
20
|
<TextInput
|
|
16
21
|
{...rest}
|
|
22
|
+
// downshift types onChange against the generic `Element`; Sanity UI's
|
|
23
|
+
// TextInput expects an HTMLInputElement handler. The handler is only
|
|
24
|
+
// forwarded to the underlying <input>, so the element-generic
|
|
25
|
+
// difference is purely a type-variance mismatch with no runtime effect.
|
|
26
|
+
onChange={onChange as React.ChangeEventHandler<HTMLInputElement> | undefined}
|
|
17
27
|
ref={ref}
|
|
18
28
|
inputMode="search"
|
|
19
29
|
icon={selectedIcon ? <Icon icon={selectedIcon} /> : null}
|
package/src/lib/api.ts
CHANGED
|
@@ -44,6 +44,7 @@ export function useSearch({ collections }: { collections: string[] | null }) {
|
|
|
44
44
|
[setDebouncedTerm],
|
|
45
45
|
);
|
|
46
46
|
|
|
47
|
+
// eslint-disable-next-line @tanstack/query/exhaustive-deps -- queryClient from useQueryClient() is a stable reference, not a query dependency
|
|
47
48
|
const { isLoading, isError, error, data, isPlaceholderData } = useQuery<string[], Error>({
|
|
48
49
|
queryKey: ['search', collections, debouncedTerm],
|
|
49
50
|
queryFn: async ({ signal }) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type IconPrefix = 'material-symbols' | 'material-symbols-light' | 'ic' | 'mdi' | 'mdi-light' | 'line-md' | 'solar' | 'tabler' | 'boxicons' | 'mingcute' | 'ri' | 'mynaui' | 'iconamoon' | 'iconoir' | 'lucide' | 'lucide-lab' | 'uil' | 'tdesign' | 'si' | 'majesticons' | 'gg' | 'flowbite' | 'basil' | 'pixelarticons' | 'pixel' | 'akar-icons' | 'ci' | 'proicons' | 'typcn' | 'meteor-icons' | 'prime' | 'circum' | 'fe' | 'eos-icons' | 'bitcoin-icons' | 'humbleicons' | 'uim' | 'uit' | 'uis' | 'gridicons' | 'mi' | 'cuida' | 'weui' | 'duo-icons' | 'svg-spinners' | 'hugeicons' | 'lets-icons' | 'streamline-ultimate' | 'streamline-plump' | 'streamline-sharp' | 'mage' | 'stash' | 'lineicons' | 'wordpress' | 'icon-park-outline' | 'icon-park-solid' | 'icon-park-twotone' | 'jam' | 'streamline-cyber' | 'guidance' | 'carbon' | 'ion' | 'famicons' | 'ant-design' | 'lsicon' | 'gravity-ui' | 'cil' | '
|
|
1
|
+
export type IconPrefix = 'material-symbols' | 'material-symbols-light' | 'ic' | 'mdi' | 'mdi-light' | 'line-md' | 'solar' | 'tabler' | 'boxicons' | 'mingcute' | 'ri' | 'mynaui' | 'iconamoon' | 'iconoir' | 'lucide' | 'lucide-lab' | 'uil' | 'tdesign' | 'si' | 'majesticons' | 'gg' | 'flowbite' | 'basil' | 'pixelarticons' | 'pixel' | 'akar-icons' | 'ci' | 'proicons' | 'typcn' | 'meteor-icons' | 'prime' | 'circum' | 'fe' | 'eos-icons' | 'bitcoin-icons' | 'humbleicons' | 'uim' | 'uit' | 'uis' | 'gridicons' | 'mi' | 'cuida' | 'weui' | 'duo-icons' | 'svg-spinners' | 'hugeicons' | 'lets-icons' | 'streamline-ultimate' | 'streamline-plump' | 'streamline-sharp' | 'mage' | 'stash' | 'lineicons' | 'wordpress' | 'icon-park-outline' | 'icon-park-solid' | 'icon-park-twotone' | 'jam' | 'streamline-cyber' | 'guidance' | 'carbon' | 'ion' | 'famicons' | 'ant-design' | 'lsicon' | 'gravity-ui' | 'cil' | 'at-icons' | 'ep' | 'charm' | 'quill' | 'bytesize' | 'bi' | 'streamline-pixel' | 'streamline-block' | 'rivet-icons' | 'nimbus' | 'formkit' | 'fluent' | 'ph' | 'glyphs' | 'glyphs-poly' | 'teenyicons' | 'clarity' | 'streamline-freehand' | 'ix' | 'octicon' | 'memory' | 'system-uicons' | 'radix-icons' | 'zondicons' | 'uiw' | 'codex' | 'ei' | 'heroicons' | 'sidekickicons' | 'pepicons-pop' | 'pepicons-print' | 'pepicons-pencil' | 'f7' | 'pajamas' | 'garden' | 'streamline' | 'streamline-flex' | 'fa7-solid' | 'fa7-regular' | 'picon' | 'ooui' | 'oui' | 'nrk' | 'dinkie-icons' | 'qlementine-icons' | 'streamline-ultimate-color' | 'streamline-plump-color' | 'streamline-freehand-color' | 'streamline-kameleon-color' | 'streamline-stickies-color' | 'fluent-color' | 'streamline-color' | 'streamline-flex-color' | 'streamline-sharp-color' | 'streamline-cyber-color' | 'icon-park' | 'marketeq' | 'vscode-icons' | 'codicon' | 'material-icon-theme' | 'file-icons' | 'devicon' | 'devicon-plain' | 'catppuccin' | 'skill-icons' | 'gcp' | 'unjs' | 'simple-icons' | 'logos' | 'streamline-logos' | 'cib' | 'fa7-brands' | 'bxl' | 'nonicons' | 'arcticons' | 'cbi' | 'brandico' | 'entypo-social' | 'token' | 'token-branded' | 'cryptocurrency' | 'cryptocurrency-color' | 'openmoji' | 'twemoji' | 'noto' | 'fluent-emoji-flat' | 'fluent-emoji-high-contrast' | 'noto-v1' | 'emojione' | 'emojione-monotone' | 'emojione-v1' | 'fxemoji' | 'streamline-emojis' | 'circle-flags' | 'flag' | 'flagpack' | 'cif' | 'pinhead' | 'roentgen' | 'maki' | 'temaki' | 'gis' | 'map' | 'geo' | 'game-icons' | 'fad' | 'academicons' | 'wi' | 'meteocons' | 'healthicons' | 'medical-icon' | 'covid' | 'ginetex' | 'la' | 'eva' | 'dashicons' | 'flat-color-icons' | 'entypo' | 'foundation' | 'raphael' | 'icons8' | 'iwwa' | 'gala' | 'heroicons-outline' | 'heroicons-solid' | 'bx' | 'bxs' | 'fa6-solid' | 'fa6-regular' | 'fa6-brands' | 'fa-solid' | 'fa-regular' | 'fa-brands' | 'fa' | 'fluent-mdl2' | 'fontisto' | 'icomoon-free' | 'subway' | 'oi' | 'wpf' | 'simple-line-icons' | 'et' | 'el' | 'vaadin' | 'grommet-icons' | 'whh' | 'si-glyph' | 'zmdi' | 'ls' | 'bpmn' | 'flat-ui' | 'vs' | 'topcoat' | 'il' | 'websymbol' | 'fontelico' | 'ps' | 'feather' | 'mono-icons' | 'pepicons' | 'fluent-emoji';
|