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/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.0.0",
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": "./dist/index.js",
25
- "require": "./dist/index.cjs",
26
- "default": "./dist/index.js"
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.js",
32
- "types": "./dist/index.d.ts",
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:clean build:verify build:bundle build:check",
41
- "build:bundle": "pkg-utils build --strict",
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": "pkg-utils watch --strict"
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.0",
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.1.3",
65
- "@tanstack/react-query": "^5.89.0",
67
+ "@sanity/ui": "^3.2.0",
68
+ "@tanstack/react-query": "^5.101.0",
66
69
  "change-case": "^5.4.4",
67
- "downshift": "^9.0.13",
70
+ "downshift": "^9.3.6",
68
71
  "ts-pattern": "^5.9.0",
69
- "use-debounce": "^10.0.6"
72
+ "use-debounce": "^10.1.1"
70
73
  },
71
74
  "devDependencies": {
72
- "@commitlint/cli": "^20.2.0",
73
- "@commitlint/config-conventional": "^20.2.0",
74
- "@iconify/json": "^2.2.420",
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.58.2",
77
- "@release-it/conventional-changelog": "^10.0.4",
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.89.0",
81
- "@tanstack/react-query-devtools": "^5.89.0",
82
- "@types/react": "^19.2.7",
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": "^5.1.2",
85
- "@vitest/ui": "^4.1.0",
86
- "@waspeer/config": "^2.4.5",
87
- "eslint": "9.39.2",
88
- "lefthook": "^2.0.12",
89
- "npm-run-all2": "^8.0.4",
90
- "prettier": "^3.7.4",
91
- "react": "^19.2.3",
92
- "react-dom": "^19.2.3",
93
- "react-is": "^19.2.3",
94
- "release-it": "^19.1.0",
95
- "rimraf": "^6.1.2",
96
- "sanity": "^5.0.1",
97
- "tsx": "^4.21.0",
98
- "typescript": "^5.9.3",
99
- "vite": "^7.3.0",
100
- "vitest": "^4.1.0"
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": ">=20.19"
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
- "esbuild@<=0.24.2": ">=0.25.0",
118
- "eslint": "^9",
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
- "prismjs@<1.30.0": ">=1.30.0"
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' | 'roentgen' | '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' | 'maki' | 'temaki' | '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' | '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' | 'gis' | 'map' | 'geo' | 'game-icons' | 'fad' | 'academicons' | 'wi' | 'meteocons' | 'healthicons' | 'medical-icon' | 'covid' | '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';
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';