sanity-plugin-iconify 3.0.0 → 4.0.1

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,434 @@
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 { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ import { Icon } from "@iconify/react";
12
+ import { TrashIcon } from "@sanity/icons";
13
+ import { stringToIcon } from "@iconify/utils";
14
+ import { sentenceCase } from "change-case";
15
+ //#region src/lib/api.ts
16
+ const BASE_API_URL = "https://api.iconify.design";
17
+ function fetchJson({ url, signal }) {
18
+ return fetch(url, { signal }).then((response) => {
19
+ if (!response.ok) throw new Error(`Network error: status ${response.status}`);
20
+ return response.json();
21
+ }).then((result) => result, (error) => {
22
+ if (error instanceof Error) throw error;
23
+ else {
24
+ console.error(`Unknown error: ${error}`);
25
+ throw new Error("Something went wrong");
26
+ }
27
+ });
28
+ }
29
+ function useSearch({ collections }) {
30
+ const queryClient = useQueryClient();
31
+ const [term, setTerm] = useState("");
32
+ const [debouncedTerm, setDebouncedTerm] = useDebounce(term, 500);
33
+ const updateTerm = useCallback((newTerm, updateImmediately = false) => {
34
+ setTerm(newTerm);
35
+ if (updateImmediately) setDebouncedTerm(newTerm);
36
+ }, [setDebouncedTerm]);
37
+ const { isLoading, isError, error, data, isPlaceholderData } = useQuery({
38
+ queryKey: [
39
+ "search",
40
+ collections,
41
+ debouncedTerm
42
+ ],
43
+ queryFn: async ({ signal }) => {
44
+ const url = new URL(`/search`, BASE_API_URL);
45
+ url.searchParams.append("query", debouncedTerm);
46
+ url.searchParams.append("limit", "60");
47
+ if (collections) url.searchParams.append("prefixes", collections.join(","));
48
+ const result = debouncedTerm ? await fetchJson({
49
+ url,
50
+ signal
51
+ }) : null;
52
+ if (result) Object.entries(result.collections).forEach(([prefix, info]) => {
53
+ queryClient.setQueryData(["iconSetInfo", prefix], info);
54
+ });
55
+ return result?.icons ?? [];
56
+ },
57
+ enabled: debouncedTerm.length > 0,
58
+ placeholderData: keepPreviousData,
59
+ staleTime: 300 * 1e3
60
+ });
61
+ return {
62
+ term,
63
+ setTerm: updateTerm,
64
+ debouncedTerm,
65
+ isLoading,
66
+ isError,
67
+ error,
68
+ data,
69
+ isPreviousData: isPlaceholderData
70
+ };
71
+ }
72
+ function useIconSetInfo({ prefix }) {
73
+ return useQuery({
74
+ queryKey: ["iconSetInfo", prefix],
75
+ queryFn: async ({ signal }) => {
76
+ if (!prefix) return null;
77
+ const url = new URL("/collection", BASE_API_URL);
78
+ url.searchParams.append("prefix", prefix);
79
+ url.searchParams.append("info", "true");
80
+ return (await fetchJson({
81
+ url,
82
+ signal
83
+ }))?.info ?? null;
84
+ },
85
+ staleTime: Infinity
86
+ });
87
+ }
88
+ styled(Grid)`
89
+ grid-template-columns: 1fr min-content;
90
+ position: relative;
91
+ `;
92
+ const OptionsWrapper = styled(Box)`
93
+ box-sizing: border-box;
94
+ padding: 0.5rem;
95
+
96
+ & [role='listbox'] {
97
+ display: flex;
98
+ flex-wrap: wrap;
99
+ gap: 0.5rem;
100
+ margin: 0;
101
+ padding: 0;
102
+ list-style: none;
103
+ }
104
+
105
+ & [role='option'] {
106
+ display: grid;
107
+ place-items: center;
108
+ width: clamp(3rem, 10vw, 4rem);
109
+
110
+ & button {
111
+ cursor: pointer;
112
+ width: 100%;
113
+
114
+ & > [data-ui='Box'] {
115
+ display: flex;
116
+ }
117
+
118
+ & svg {
119
+ aspect-ratio: 1;
120
+ }
121
+ }
122
+ }
123
+ `;
124
+ function MessageWrapper({ children }) {
125
+ return /* @__PURE__ */ jsx(Card, {
126
+ padding: 4,
127
+ children: /* @__PURE__ */ jsx(Text, {
128
+ align: "center",
129
+ muted: true,
130
+ children
131
+ })
132
+ });
133
+ }
134
+ //#endregion
135
+ //#region src/combobox/search-input.tsx
136
+ const SearchInput = memo(forwardRef((props, ref) => {
137
+ const { selectedIcon, suffix, onChange, ...rest } = props;
138
+ return /* @__PURE__ */ jsx(TextInput, {
139
+ ...rest,
140
+ onChange,
141
+ ref,
142
+ inputMode: "search",
143
+ icon: selectedIcon ? /* @__PURE__ */ jsx(Icon, { icon: selectedIcon }) : null,
144
+ placeholder: selectedIcon ? "Search and replace selected icon..." : "Search for an icon...",
145
+ suffix
146
+ });
147
+ }));
148
+ SearchInput.displayName = "SearchInput";
149
+ //#endregion
150
+ //#region src/combobox/search-result.tsx
151
+ const SearchResults = memo(forwardRef((props, ref) => {
152
+ const { state, data, getItemProps, highlightedIndex, ...rest } = props;
153
+ return /* @__PURE__ */ jsxs(Fragment, { children: [(() => {
154
+ switch (state) {
155
+ case "initial": return /* @__PURE__ */ jsx(MessageWrapper, { children: "Search for icons" });
156
+ case "loading": return /* @__PURE__ */ jsx(MessageWrapper, { children: "Searching..." });
157
+ case "error": return /* @__PURE__ */ jsx(MessageWrapper, { children: "Something went wrong..." });
158
+ case "empty": return /* @__PURE__ */ jsx(MessageWrapper, { children: "No icons found" });
159
+ }
160
+ })(), /* @__PURE__ */ jsx("ul", {
161
+ ...rest,
162
+ ref,
163
+ "data-testid": "iconify-results",
164
+ style: { opacity: state === "stale" ? .5 : 1 },
165
+ children: (state === "data" || state === "stale") && data?.map((icon, index) => /* @__PURE__ */ jsx("li", {
166
+ ...getItemProps({
167
+ item: icon,
168
+ index
169
+ }),
170
+ children: /* @__PURE__ */ jsx(Button, {
171
+ padding: 3,
172
+ mode: "bleed",
173
+ selected: index === highlightedIndex,
174
+ children: /* @__PURE__ */ jsx(Icon, {
175
+ icon,
176
+ width: "100%",
177
+ height: "100%"
178
+ })
179
+ })
180
+ }, icon))
181
+ })] });
182
+ }));
183
+ //#endregion
184
+ //#region src/combobox/unset-button.tsx
185
+ function UnsetButton(props) {
186
+ const { onUnset } = props;
187
+ return /* @__PURE__ */ jsx(Card, {
188
+ border: true,
189
+ borderLeft: false,
190
+ padding: 1,
191
+ display: "flex",
192
+ radius: 2,
193
+ children: /* @__PURE__ */ jsx(Button, {
194
+ "data-testid": "iconify-unset",
195
+ icon: /* @__PURE__ */ jsx(TrashIcon, {}),
196
+ onClick: onUnset,
197
+ mode: "bleed",
198
+ fontSize: 1,
199
+ padding: 2
200
+ })
201
+ });
202
+ }
203
+ //#endregion
204
+ //#region src/combobox/iconify-combobox.tsx
205
+ const IconifyCombobox = memo(function IconifyCombobox(props) {
206
+ const { selectedIcon, onSelect: pushSelection, collections, studioElementProps, fieldFocused } = props;
207
+ const id = useId();
208
+ const toast = useToast();
209
+ const inputRef = useRef(null);
210
+ const composedInputRef = useCallback((node) => {
211
+ inputRef.current = node;
212
+ if (studioElementProps?.ref) studioElementProps.ref.current = node;
213
+ }, [studioElementProps?.ref]);
214
+ const suppressNextBlur = useRef(false);
215
+ const handleFocus = useCallback((event) => {
216
+ suppressNextBlur.current = true;
217
+ setTimeout(() => {
218
+ suppressNextBlur.current = false;
219
+ }, 0);
220
+ studioElementProps?.onFocus?.(event);
221
+ }, [studioElementProps]);
222
+ const handleBlur = useCallback((event) => {
223
+ if (suppressNextBlur.current) {
224
+ suppressNextBlur.current = false;
225
+ return;
226
+ }
227
+ studioElementProps?.onBlur?.(event);
228
+ }, [studioElementProps]);
229
+ const { term, setTerm, debouncedTerm, isLoading, isError, error, data, isPreviousData } = useSearch({ collections });
230
+ const { isOpen, getMenuProps, getInputProps, highlightedIndex, getItemProps, selectItem, setInputValue, closeMenu } = useCombobox({
231
+ items: data ?? [],
232
+ inputValue: term,
233
+ onInputValueChange({ inputValue, selectedItem }) {
234
+ if (inputValue !== selectedItem) setTerm(inputValue);
235
+ },
236
+ onSelectedItemChange({ selectedItem }) {
237
+ if (selectedItem) {
238
+ pushSelection(selectedItem);
239
+ setTerm("", true);
240
+ setInputValue("");
241
+ }
242
+ }
243
+ });
244
+ useEffect(() => {
245
+ if (!fieldFocused) closeMenu();
246
+ }, [fieldFocused, closeMenu]);
247
+ const handleUnset = useCallback(() => {
248
+ pushSelection("");
249
+ setTerm("", true);
250
+ selectItem("");
251
+ }, [
252
+ pushSelection,
253
+ setTerm,
254
+ selectItem
255
+ ]);
256
+ useEffect(() => {
257
+ if (isError) {
258
+ console.error("Iconify input error:", error);
259
+ toast.push({
260
+ id,
261
+ status: "error",
262
+ title: "Iconify input error",
263
+ description: error?.message
264
+ });
265
+ }
266
+ }, [
267
+ error,
268
+ id,
269
+ isError,
270
+ toast
271
+ ]);
272
+ const { onBlur: _downshiftOnBlur, ...inputProps } = getInputProps({
273
+ ref: composedInputRef,
274
+ id: studioElementProps?.id,
275
+ "aria-describedby": studioElementProps?.["aria-describedby"],
276
+ onFocus: handleFocus
277
+ });
278
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(SearchInput, {
279
+ ...inputProps,
280
+ onBlur: handleBlur,
281
+ selectedIcon,
282
+ suffix: selectedIcon ? /* @__PURE__ */ jsx(UnsetButton, { onUnset: handleUnset }) : null
283
+ }), /* @__PURE__ */ jsx(Popover, {
284
+ open: true,
285
+ style: { display: isOpen ? "block" : "none" },
286
+ placement: "bottom",
287
+ arrow: false,
288
+ matchReferenceWidth: true,
289
+ constrainSize: true,
290
+ referenceElement: inputRef.current,
291
+ content: /* @__PURE__ */ jsx(OptionsWrapper, { children: /* @__PURE__ */ jsx(SearchResults, {
292
+ ...getMenuProps(),
293
+ state: match(true).returnType().with(isLoading, () => "loading").with(!debouncedTerm, () => "initial").with(isError, () => "error").with(!data || data.length === 0, () => "empty").with(isPreviousData, () => "stale").otherwise(() => "data"),
294
+ data,
295
+ getItemProps,
296
+ highlightedIndex
297
+ }) })
298
+ })] });
299
+ });
300
+ //#endregion
301
+ //#region src/lib/query-client.tsx
302
+ const queryClient = new QueryClient();
303
+ function QueryClientProvider$1(props) {
304
+ return /* @__PURE__ */ jsx(QueryClientProvider, {
305
+ client: queryClient,
306
+ children: props.children
307
+ });
308
+ }
309
+ //#endregion
310
+ //#region src/lib/use-pretty-icon-name.ts
311
+ function usePrettyIconName(props) {
312
+ const { name } = props;
313
+ const iconMeta = useMemo(() => props.iconMeta ?? (name ? stringToIcon(name) : null), [name, props.iconMeta]);
314
+ const iconSetInfo = useIconSetInfo({ prefix: iconMeta?.prefix ?? null });
315
+ return useMemo(() => iconMeta ? {
316
+ name: sentenceCase(iconMeta.name),
317
+ collection: iconSetInfo.data?.name ?? iconMeta.prefix
318
+ } : null, [iconMeta, iconSetInfo.data?.name]);
319
+ }
320
+ //#endregion
321
+ //#region src/iconify-input.tsx
322
+ const theme = buildTheme();
323
+ const IconifyInput = memo(function IconifyInput(props) {
324
+ const { config, value, onChange: pushChange, schemaType, elementProps, focused } = props;
325
+ const selectedIcon = value?.name ?? null;
326
+ const options = schemaType.options;
327
+ const collections = !!options?.collections?.length && options.collections || !!config?.collections?.length && config.collections || null;
328
+ const showName = options?.showName ?? config?.showName ?? false;
329
+ return /* @__PURE__ */ jsx(QueryClientProvider$1, { children: /* @__PURE__ */ jsx(ThemeProvider, {
330
+ theme,
331
+ children: /* @__PURE__ */ jsxs(Stack, {
332
+ space: 2,
333
+ children: [/* @__PURE__ */ jsx(IconifyCombobox, {
334
+ selectedIcon,
335
+ onSelect: useCallback((icon) => {
336
+ pushChange(icon === "" ? unset() : set(icon, ["name"]));
337
+ }, [pushChange]),
338
+ collections,
339
+ studioElementProps: elementProps,
340
+ fieldFocused: focused
341
+ }), showName && selectedIcon ? /* @__PURE__ */ jsx(IconifyNameDisplay, { name: selectedIcon }) : null]
342
+ })
343
+ }) });
344
+ });
345
+ function IconifyNameDisplay(props) {
346
+ const { name } = props;
347
+ const prettyName = usePrettyIconName({ name });
348
+ return /* @__PURE__ */ jsxs(Flex, {
349
+ gap: 1,
350
+ children: [
351
+ /* @__PURE__ */ jsx(Text, {
352
+ size: 1,
353
+ muted: true,
354
+ children: "Selected:"
355
+ }),
356
+ /* @__PURE__ */ jsx(Text, {
357
+ size: 1,
358
+ weight: "semibold",
359
+ children: prettyName?.name ?? name
360
+ }),
361
+ prettyName?.collection && /* @__PURE__ */ jsxs(Text, {
362
+ size: 1,
363
+ muted: true,
364
+ style: { fontStyle: "italic" },
365
+ children: ["by ", prettyName?.collection]
366
+ })
367
+ ]
368
+ });
369
+ }
370
+ //#endregion
371
+ //#region src/iconify-preview.tsx
372
+ const IconifyPreview = memo(function IconifyPreview(props) {
373
+ const { title } = props;
374
+ const iconName = typeof props.title === "string" ? stringToIcon(props.title) : null;
375
+ if (typeof title === "string" && iconName) return /* @__PURE__ */ jsx(QueryClientProvider$1, { children: /* @__PURE__ */ jsx(IconifyPreviewInner, {
376
+ ...props,
377
+ iconName: title,
378
+ iconMeta: iconName
379
+ }) });
380
+ return props.renderDefault(props);
381
+ });
382
+ function IconifyPreviewInner(props) {
383
+ const { iconMeta, iconName, ...previewProps } = props;
384
+ const prettyName = usePrettyIconName({ iconMeta });
385
+ return props.renderDefault({
386
+ ...previewProps,
387
+ media: /* @__PURE__ */ jsx(Icon, { icon: iconName }),
388
+ title: prettyName?.name ?? iconName,
389
+ subtitle: prettyName?.collection
390
+ });
391
+ }
392
+ //#endregion
393
+ //#region src/iconify-plugin.tsx
394
+ /**
395
+ * Usage in `sanity.config.ts` (or .js)
396
+ *
397
+ * ```ts
398
+ * import { defineConfig } from 'sanity'
399
+ * import { iconify } from 'sanity-plugin-iconify'
400
+ *
401
+ * export default defineConfig({
402
+ * // ...
403
+ * plugins: [iconify()],
404
+ * })
405
+ * ```
406
+ */
407
+ const iconify = definePlugin((config = {}) => {
408
+ return {
409
+ name: "sanity-plugin-iconify",
410
+ schema: { types: [{
411
+ name: "icon",
412
+ title: "Icon",
413
+ type: "object",
414
+ fields: [{
415
+ name: "name",
416
+ title: "Name",
417
+ type: "string"
418
+ }],
419
+ components: {
420
+ input: (props) => /* @__PURE__ */ jsx(IconifyInput, {
421
+ ...props,
422
+ config
423
+ }),
424
+ preview: IconifyPreview,
425
+ field: (props) => props.renderDefault({
426
+ ...props,
427
+ level: 0
428
+ })
429
+ }
430
+ }] }
431
+ };
432
+ });
433
+ //#endregion
434
+ 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.1",
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,94 +42,115 @@
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 build:check",
46
+ "build:bundle": "tsdown",
47
+ "build:check": "node scripts/check-dist.mjs",
44
48
  "build:verify": "plugin-kit verify-package --silent",
45
49
  "dev": "vite dev --config dev/vite.config.ts",
50
+ "dev:dist": "vite build --config dev/vite.config.ts && vite preview --config dev/vite.config.ts",
46
51
  "format": "prettier --write --cache --ignore-unknown .",
47
52
  "generate-types": "node src/lib/generate-types",
48
53
  "lint": "eslint .",
54
+ "prepublishOnly": "node scripts/check-dist.mjs",
49
55
  "release": "release-it",
50
56
  "test": "vitest run",
51
57
  "test:e2e": "playwright test",
58
+ "test:e2e:dist": "PLUGIN_TARGET=dist playwright test",
52
59
  "test:e2e:setup": "tsx playwright/auth-setup.ts",
53
60
  "test:e2e:ui": "playwright test --ui",
54
61
  "test:ui": "vitest --ui",
55
62
  "test:watch": "vitest",
56
- "watch": "pkg-utils watch --strict"
63
+ "watch": "tsdown --watch"
57
64
  },
58
65
  "browserslist": "extends @sanity/browserslist-config",
59
66
  "dependencies": {
60
67
  "@iconify/react": "^6.0.2",
61
- "@iconify/utils": "^3.1.0",
68
+ "@iconify/utils": "^3.1.3",
62
69
  "@sanity/icons": "^3.7.4",
63
70
  "@sanity/incompatible-plugin": "^1.0.5",
64
- "@sanity/ui": "^3.1.3",
65
- "@tanstack/react-query": "^5.89.0",
71
+ "@sanity/ui": "^3.2.0",
72
+ "@tanstack/react-query": "^5.101.0",
66
73
  "change-case": "^5.4.4",
67
- "downshift": "^9.0.13",
74
+ "downshift": "^9.3.6",
68
75
  "ts-pattern": "^5.9.0",
69
- "use-debounce": "^10.0.6"
76
+ "use-debounce": "^10.1.1"
70
77
  },
71
78
  "devDependencies": {
72
- "@commitlint/cli": "^20.2.0",
73
- "@commitlint/config-conventional": "^20.2.0",
74
- "@iconify/json": "^2.2.420",
79
+ "@commitlint/cli": "^21.0.2",
80
+ "@commitlint/config-conventional": "^21.0.2",
81
+ "@iconify/json": "^2.2.486",
75
82
  "@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",
83
+ "@playwright/test": "^1.61.0",
84
+ "@release-it/conventional-changelog": "^11.0.1",
79
85
  "@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",
86
+ "@tanstack/eslint-plugin-query": "^5.101.0",
87
+ "@tanstack/react-query-devtools": "^5.101.0",
88
+ "@types/react": "^19.2.17",
83
89
  "@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"
90
+ "@vitejs/plugin-react": "^6.0.1",
91
+ "@vitest/ui": "^4.1.9",
92
+ "@waspeer/config": "^3.0.0",
93
+ "eslint": "^10.1.0",
94
+ "lefthook": "^2.1.9",
95
+ "npm-run-all2": "^9.0.2",
96
+ "prettier": "^3.8.4",
97
+ "publint": "^0.3.18",
98
+ "react": "^19.2.7",
99
+ "react-dom": "^19.2.7",
100
+ "react-is": "^19.2.7",
101
+ "release-it": "^20.2.0",
102
+ "rimraf": "^6.1.3",
103
+ "sanity": "^6.0.0",
104
+ "tsdown": "^0.22.2",
105
+ "tsx": "^4.22.4",
106
+ "typescript": "^6.0.2",
107
+ "vite": "^8.0.2",
108
+ "vitest": "^4.1.9"
101
109
  },
102
110
  "peerDependencies": {
103
111
  "react": "^19.2",
104
- "sanity": "^5.0.0-0",
112
+ "sanity": "^5.0.0-0 || ^6.0.0-0",
105
113
  "styled-components": "^6"
106
114
  },
107
115
  "packageManager": "pnpm@10.17.0",
108
116
  "engines": {
109
- "node": ">=20.19"
117
+ "node": ">=22.12"
110
118
  },
111
119
  "pnpm": {
112
120
  "onlyBuiltDependencies": [
113
121
  "esbuild",
114
- "lefthook"
122
+ "lefthook",
123
+ "unrs-resolver"
115
124
  ],
116
125
  "overrides": {
117
- "esbuild@<=0.24.2": ">=0.25.0",
118
- "eslint": "^9",
126
+ "@babel/core@<7.29.6": ">=7.29.6",
127
+ "@isaacs/brace-expansion@<5.0.1": ">=5.0.1",
128
+ "@typescript-eslint/utils": "^8.61.1",
129
+ "brace-expansion@>=4.0.0 <5.0.6": ">=5.0.6",
130
+ "brace-expansion@<2.0.3": ">=2.0.3",
131
+ "esbuild@<0.28.1": ">=0.28.1",
132
+ "follow-redirects@<1.16.0": ">=1.16.0",
133
+ "lodash@<4.17.23": ">=4.17.23",
119
134
  "micromatch@<4.0.8": ">=4.0.8",
120
- "prismjs@<1.30.0": ">=1.30.0"
135
+ "minimatch@<10.2.3": ">=10.2.3",
136
+ "picomatch@<4.0.4": ">=4.0.4",
137
+ "postcss@<8.5.10": ">=8.5.10",
138
+ "prismjs@<1.30.0": ">=1.30.0",
139
+ "rollup@<4.59.0": ">=4.59.0",
140
+ "serialize-javascript@<7.0.5": ">=7.0.5",
141
+ "shell-quote@<1.8.4": ">=1.8.4",
142
+ "tmp@<0.2.6": ">=0.2.6",
143
+ "uuid@>=11.0.0 <11.1.1": ">=11.1.1"
121
144
  }
122
145
  },
123
146
  "sanityPlugin": {
124
147
  "verifyPackage": {
125
148
  "scripts": false,
126
149
  "eslintImports": false,
127
- "nodeEngine": false
150
+ "nodeEngine": false,
151
+ "pkg-utils": false,
152
+ "rollupConfig": false,
153
+ "tsconfig": false
128
154
  }
129
155
  }
130
156
  }
@@ -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 }) => {