sanity-plugin-iconify 2.1.0 → 3.0.0-beta.2

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.js CHANGED
@@ -2,7 +2,7 @@ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { unset, set, definePlugin } from "sanity";
3
3
  import { Grid, Box, Card, Text, TextInput, Button, useToast, Popover, ThemeProvider, Stack, Flex } from "@sanity/ui";
4
4
  import { buildTheme } from "@sanity/ui/theme";
5
- import { useState, useCallback, forwardRef, useId, useRef, useEffect, useMemo } from "react";
5
+ import { useState, useCallback, memo, forwardRef, useId, useRef, useEffect, useMemo } from "react";
6
6
  import { useCombobox } from "downshift";
7
7
  import { match } from "ts-pattern";
8
8
  import { useQueryClient, useQuery, keepPreviousData, QueryClient, QueryClientProvider as QueryClientProvider$1 } from "@tanstack/react-query";
@@ -107,45 +107,95 @@ const OptionsWrapper = styled(Box)`
107
107
  function MessageWrapper({ children }) {
108
108
  return /* @__PURE__ */ jsx(Card, { padding: 4, children: /* @__PURE__ */ jsx(Text, { align: "center", muted: !0, children }) });
109
109
  }
110
- const SearchInput = forwardRef((props, ref) => {
111
- const { selectedIcon, suffix, ...rest } = props;
112
- return /* @__PURE__ */ jsx(
113
- TextInput,
114
- {
115
- ...rest,
116
- ref,
117
- inputMode: "search",
118
- icon: selectedIcon ? /* @__PURE__ */ jsx(Icon, { icon: selectedIcon }) : null,
119
- placeholder: selectedIcon ? "Search and replace selected icon..." : "Search for an icon...",
120
- suffix
121
- }
122
- );
123
- });
124
- SearchInput.displayName = "SearchInput";
125
- const SearchResults = forwardRef((props, ref) => {
126
- const { state, data, getItemProps, highlightedIndex, ...rest } = props;
127
- return /* @__PURE__ */ jsxs(Fragment, { children: [
128
- (() => {
129
- switch (state) {
130
- case "initial":
131
- return /* @__PURE__ */ jsx(MessageWrapper, { children: "Search for icons" });
132
- case "loading":
133
- return /* @__PURE__ */ jsx(MessageWrapper, { children: "Searching..." });
134
- case "error":
135
- return /* @__PURE__ */ jsx(MessageWrapper, { children: "Something went wrong..." });
136
- case "empty":
137
- return /* @__PURE__ */ jsx(MessageWrapper, { children: "No icons found" });
110
+ const SearchInput = memo(
111
+ forwardRef((props, ref) => {
112
+ const { selectedIcon, suffix, ...rest } = props;
113
+ return /* @__PURE__ */ jsx(
114
+ TextInput,
115
+ {
116
+ ...rest,
117
+ ref,
118
+ inputMode: "search",
119
+ icon: selectedIcon ? /* @__PURE__ */ jsx(Icon, { icon: selectedIcon }) : null,
120
+ placeholder: selectedIcon ? "Search and replace selected icon..." : "Search for an icon...",
121
+ suffix
138
122
  }
139
- })(),
140
- /* @__PURE__ */ jsx("ul", { ...rest, ref, style: { opacity: state === "stale" ? 0.5 : 1 }, children: (state === "data" || state === "stale") && data?.map((icon, index) => /* @__PURE__ */ jsx("li", { ...getItemProps({ item: icon, index }), children: /* @__PURE__ */ jsx(Button, { padding: 3, mode: "bleed", selected: index === highlightedIndex, children: /* @__PURE__ */ jsx(Icon, { icon, width: "100%", height: "100%" }) }) }, icon)) })
141
- ] });
142
- });
123
+ );
124
+ })
125
+ );
126
+ SearchInput.displayName = "SearchInput";
127
+ const SearchResults = memo(
128
+ forwardRef((props, ref) => {
129
+ const { state, data, getItemProps, highlightedIndex, ...rest } = props;
130
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
131
+ (() => {
132
+ switch (state) {
133
+ case "initial":
134
+ return /* @__PURE__ */ jsx(MessageWrapper, { children: "Search for icons" });
135
+ case "loading":
136
+ return /* @__PURE__ */ jsx(MessageWrapper, { children: "Searching..." });
137
+ case "error":
138
+ return /* @__PURE__ */ jsx(MessageWrapper, { children: "Something went wrong..." });
139
+ case "empty":
140
+ return /* @__PURE__ */ jsx(MessageWrapper, { children: "No icons found" });
141
+ }
142
+ })(),
143
+ /* @__PURE__ */ jsx(
144
+ "ul",
145
+ {
146
+ ...rest,
147
+ ref,
148
+ "data-testid": "iconify-results",
149
+ style: { opacity: state === "stale" ? 0.5 : 1 },
150
+ children: (state === "data" || state === "stale") && data?.map((icon, index) => /* @__PURE__ */ jsx("li", { ...getItemProps({ item: icon, index }), children: /* @__PURE__ */ jsx(Button, { padding: 3, mode: "bleed", selected: index === highlightedIndex, children: /* @__PURE__ */ jsx(Icon, { icon, width: "100%", height: "100%" }) }) }, icon))
151
+ }
152
+ )
153
+ ] });
154
+ })
155
+ );
143
156
  function UnsetButton(props) {
144
157
  const { onUnset } = props;
145
- return /* @__PURE__ */ jsx(Card, { border: !0, borderLeft: !1, padding: 1, display: "flex", radius: 2, children: /* @__PURE__ */ jsx(Button, { icon: /* @__PURE__ */ jsx(TrashIcon, {}), onClick: onUnset, mode: "bleed", fontSize: 1, padding: 2 }) });
158
+ return /* @__PURE__ */ jsx(Card, { border: !0, borderLeft: !1, padding: 1, display: "flex", radius: 2, children: /* @__PURE__ */ jsx(
159
+ Button,
160
+ {
161
+ "data-testid": "iconify-unset",
162
+ icon: /* @__PURE__ */ jsx(TrashIcon, {}),
163
+ onClick: onUnset,
164
+ mode: "bleed",
165
+ fontSize: 1,
166
+ padding: 2
167
+ }
168
+ ) });
146
169
  }
147
- function IconifyCombobox(props) {
148
- const { selectedIcon, onSelect: pushSelection, collections } = props, id = useId(), toast = useToast(), inputRef = useRef(null), { term, setTerm, debouncedTerm, isLoading, isError, error, data, isPreviousData } = useSearch({
170
+ const IconifyCombobox = memo(function(props) {
171
+ const {
172
+ selectedIcon,
173
+ onSelect: pushSelection,
174
+ collections,
175
+ studioElementProps,
176
+ fieldFocused
177
+ } = props, id = useId(), toast = useToast(), inputRef = useRef(null), composedInputRef = useCallback(
178
+ (node) => {
179
+ inputRef.current = node, studioElementProps?.ref && (studioElementProps.ref.current = node);
180
+ },
181
+ [studioElementProps?.ref]
182
+ ), suppressNextBlur = useRef(!1), handleFocus = useCallback(
183
+ (event) => {
184
+ suppressNextBlur.current = !0, setTimeout(() => {
185
+ suppressNextBlur.current = !1;
186
+ }, 0), studioElementProps?.onFocus?.(event);
187
+ },
188
+ [studioElementProps]
189
+ ), handleBlur = useCallback(
190
+ (event) => {
191
+ if (suppressNextBlur.current) {
192
+ suppressNextBlur.current = !1;
193
+ return;
194
+ }
195
+ studioElementProps?.onBlur?.(event);
196
+ },
197
+ [studioElementProps]
198
+ ), { term, setTerm, debouncedTerm, isLoading, isError, error, data, isPreviousData } = useSearch({
149
199
  collections
150
200
  }), {
151
201
  isOpen,
@@ -154,7 +204,8 @@ function IconifyCombobox(props) {
154
204
  highlightedIndex,
155
205
  getItemProps,
156
206
  selectItem,
157
- setInputValue
207
+ setInputValue,
208
+ closeMenu
158
209
  } = useCombobox({
159
210
  items: data ?? [],
160
211
  inputValue: term,
@@ -164,21 +215,33 @@ function IconifyCombobox(props) {
164
215
  onSelectedItemChange({ selectedItem }) {
165
216
  selectedItem && (pushSelection(selectedItem), setTerm("", !0), setInputValue(""));
166
217
  }
167
- }), handleUnset = useCallback(() => {
218
+ });
219
+ useEffect(() => {
220
+ fieldFocused || closeMenu();
221
+ }, [fieldFocused, closeMenu]);
222
+ const handleUnset = useCallback(() => {
168
223
  pushSelection(""), setTerm("", !0), selectItem("");
169
- }, []);
170
- return useEffect(() => {
224
+ }, [pushSelection, setTerm, selectItem]);
225
+ useEffect(() => {
171
226
  isError && (console.error("Iconify input error:", error), toast.push({
172
227
  id,
173
228
  status: "error",
174
229
  title: "Iconify input error",
175
230
  description: error?.message
176
231
  }));
177
- }, [error, id, isError, toast]), /* @__PURE__ */ jsxs("div", { children: [
232
+ }, [error, id, isError, toast]);
233
+ const { onBlur: _downshiftOnBlur, ...inputProps } = getInputProps({
234
+ ref: composedInputRef,
235
+ id: studioElementProps?.id,
236
+ "aria-describedby": studioElementProps?.["aria-describedby"],
237
+ onFocus: handleFocus
238
+ });
239
+ return /* @__PURE__ */ jsxs("div", { children: [
178
240
  /* @__PURE__ */ jsx(
179
241
  SearchInput,
180
242
  {
181
- ...getInputProps({ ref: inputRef }),
243
+ ...inputProps,
244
+ onBlur: handleBlur,
182
245
  selectedIcon,
183
246
  suffix: selectedIcon ? /* @__PURE__ */ jsx(UnsetButton, { onUnset: handleUnset }) : null
184
247
  }
@@ -206,8 +269,7 @@ function IconifyCombobox(props) {
206
269
  }
207
270
  )
208
271
  ] });
209
- }
210
- const queryClient = new QueryClient();
272
+ }), queryClient = new QueryClient();
211
273
  function QueryClientProvider(props) {
212
274
  return /* @__PURE__ */ jsx(QueryClientProvider$1, { client: queryClient, children: props.children });
213
275
  }
@@ -224,25 +286,27 @@ function usePrettyIconName(props) {
224
286
  [iconMeta, iconSetInfo.data?.name]
225
287
  );
226
288
  }
227
- function IconifyInput(props) {
228
- const { config, value, onChange: pushChange, schemaType } = props, selectedIcon = value?.name ?? null, options = schemaType.options, collections = !!options?.collections?.length && options.collections || !!config?.collections?.length && config.collections || null, showName = options?.showName ?? config?.showName ?? !1, handleSelect = useCallback(
289
+ const theme = buildTheme(), IconifyInput = memo(function(props) {
290
+ const { config, value, onChange: pushChange, schemaType, elementProps, focused } = props, selectedIcon = value?.name ?? null, options = schemaType.options, collections = !!options?.collections?.length && options.collections || !!config?.collections?.length && config.collections || null, showName = options?.showName ?? config?.showName ?? !1, handleSelect = useCallback(
229
291
  (icon) => {
230
292
  pushChange(icon === "" ? unset() : set(icon, ["name"]));
231
293
  },
232
294
  [pushChange]
233
295
  );
234
- return /* @__PURE__ */ jsx(QueryClientProvider, { children: /* @__PURE__ */ jsx(ThemeProvider, { theme: buildTheme(), children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
296
+ return /* @__PURE__ */ jsx(QueryClientProvider, { children: /* @__PURE__ */ jsx(ThemeProvider, { theme, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
235
297
  /* @__PURE__ */ jsx(
236
298
  IconifyCombobox,
237
299
  {
238
300
  selectedIcon,
239
301
  onSelect: handleSelect,
240
- collections
302
+ collections,
303
+ studioElementProps: elementProps,
304
+ fieldFocused: focused
241
305
  }
242
306
  ),
243
307
  showName && selectedIcon ? /* @__PURE__ */ jsx(IconifyNameDisplay, { name: selectedIcon }) : null
244
308
  ] }) }) });
245
- }
309
+ });
246
310
  function IconifyNameDisplay(props) {
247
311
  const { name } = props, prettyName = usePrettyIconName({ name });
248
312
  return /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
@@ -254,10 +318,10 @@ function IconifyNameDisplay(props) {
254
318
  ] })
255
319
  ] });
256
320
  }
257
- function IconifyPreview(props) {
321
+ const IconifyPreview = memo(function(props) {
258
322
  const { title } = props, iconName = typeof props.title == "string" ? stringToIcon(props.title) : null;
259
323
  return typeof title == "string" && iconName ? /* @__PURE__ */ jsx(QueryClientProvider, { children: /* @__PURE__ */ jsx(IconifyPreviewInner, { ...props, iconName: title, iconMeta: iconName }) }) : props.renderDefault(props);
260
- }
324
+ });
261
325
  function IconifyPreviewInner(props) {
262
326
  const { iconMeta, iconName, ...previewProps } = props, prettyName = usePrettyIconName({ iconMeta });
263
327
  return props.renderDefault({
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/lib/api.ts","../src/combobox/iconify-combobox.styles.tsx","../src/combobox/search-input.tsx","../src/combobox/search-result.tsx","../src/combobox/unset-button.tsx","../src/combobox/iconify-combobox.tsx","../src/lib/query-client.tsx","../src/lib/use-pretty-icon-name.ts","../src/iconify-input.tsx","../src/iconify-preview.tsx","../src/iconify-plugin.tsx"],"sourcesContent":["import type { IconifyInfo } from '@iconify/types';\nimport { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { useCallback, useState } from 'react';\nimport { useDebounce } from 'use-debounce';\nimport type { IconifySearchResult } from './types';\n\nconst BASE_API_URL = 'https://api.iconify.design';\n\nfunction fetchJson<T>({ url, signal }: { url: string | URL; signal?: AbortSignal }): Promise<T> {\n return fetch(url, { signal })\n .then((response) => {\n if (!response.ok) {\n throw new Error(`Network error: status ${response.status}`);\n }\n\n return response.json();\n })\n .then(\n (result) => result as T,\n (error) => {\n if (error instanceof Error) {\n throw error;\n } else {\n console.error(`Unknown error: ${error}`);\n throw new Error('Something went wrong');\n }\n },\n );\n}\n\nexport function useSearch({ collections }: { collections: string[] | null }) {\n const queryClient = useQueryClient();\n const [term, setTerm] = useState('');\n const [debouncedTerm, setDebouncedTerm] = useDebounce(term, 500);\n\n const updateTerm = useCallback(\n (newTerm: string, updateImmediately = false) => {\n setTerm(newTerm);\n\n if (updateImmediately) {\n setDebouncedTerm(newTerm);\n }\n },\n [setDebouncedTerm],\n );\n\n const { isLoading, isError, error, data, isPlaceholderData } = useQuery<string[], Error>({\n queryKey: ['search', collections, debouncedTerm],\n queryFn: async ({ signal }) => {\n const url = new URL(`/search`, BASE_API_URL);\n\n url.searchParams.append('query', debouncedTerm);\n url.searchParams.append('limit', '60');\n\n if (collections) {\n url.searchParams.append('prefixes', collections.join(','));\n }\n\n const result = debouncedTerm ? await fetchJson<IconifySearchResult>({ url, signal }) : null;\n\n if (result) {\n // Cache the info for each collection\n Object.entries(result.collections).forEach(([prefix, info]) => {\n queryClient.setQueryData<IconifyInfo>(['iconSetInfo', prefix], info);\n });\n }\n\n return result?.icons ?? [];\n },\n enabled: debouncedTerm.length > 0,\n placeholderData: keepPreviousData,\n staleTime: 5 * 60 * 1000, // 5 minutes\n });\n\n return {\n term,\n setTerm: updateTerm,\n debouncedTerm,\n isLoading,\n isError,\n error,\n data,\n isPreviousData: isPlaceholderData,\n };\n}\n\nexport function useIconSetInfo({ prefix }: { prefix?: string | null }) {\n return useQuery<IconifyInfo | null, Error>({\n queryKey: ['iconSetInfo', prefix],\n queryFn: async ({ signal }) => {\n if (!prefix) return null;\n\n const url = new URL('/collection', BASE_API_URL);\n\n url.searchParams.append('prefix', prefix);\n url.searchParams.append('info', 'true');\n\n const result = await fetchJson<{ info: IconifyInfo }>({ url, signal });\n\n return result?.info ?? null;\n },\n staleTime: Infinity,\n });\n}\n","import { Box, Card, Grid, Text } from '@sanity/ui';\nimport type { ReactNode } from 'react';\nimport styled from 'styled-components';\n\nexport const ComboboxWrapper = styled(Grid)`\n grid-template-columns: 1fr min-content;\n position: relative;\n`;\n\nexport const OptionsWrapper = styled(Box)`\n box-sizing: border-box;\n padding: 0.5rem;\n\n & [role='listbox'] {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin: 0;\n padding: 0;\n list-style: none;\n }\n\n & [role='option'] {\n display: grid;\n place-items: center;\n width: clamp(3rem, 10vw, 4rem);\n\n & button {\n cursor: pointer;\n width: 100%;\n\n & > [data-ui='Box'] {\n display: flex;\n }\n\n & svg {\n aspect-ratio: 1;\n }\n }\n }\n`;\n\nexport function MessageWrapper({ children }: { children: ReactNode }) {\n return (\n <Card padding={4}>\n <Text align=\"center\" muted>\n {children}\n </Text>\n </Card>\n );\n}\n","import { Icon } from '@iconify/react';\nimport { TextInput } from '@sanity/ui';\nimport { forwardRef } from 'react';\n\ninterface SearchInputProps extends React.HTMLAttributes<HTMLInputElement> {\n selectedIcon: string | null;\n suffix?: React.ReactNode;\n}\n\nexport const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>((props, ref) => {\n const { selectedIcon, suffix, ...rest } = props;\n\n return (\n <TextInput\n {...rest}\n ref={ref}\n inputMode=\"search\"\n icon={selectedIcon ? <Icon icon={selectedIcon} /> : null}\n placeholder={selectedIcon ? 'Search and replace selected icon...' : 'Search for an icon...'}\n suffix={suffix}\n />\n );\n});\n\nSearchInput.displayName = 'SearchInput';\n","import { Icon } from '@iconify/react';\nimport { Button } from '@sanity/ui';\nimport type { UseComboboxGetItemPropsOptions, UseComboboxGetItemPropsReturnValue } from 'downshift';\nimport { forwardRef } from 'react';\nimport { MessageWrapper } from './iconify-combobox.styles';\n\ntype GetItemProps = (\n options: UseComboboxGetItemPropsOptions<string>,\n) => UseComboboxGetItemPropsReturnValue;\n\nexport interface SearchResultsProps extends React.HTMLAttributes<HTMLUListElement> {\n state: 'initial' | 'loading' | 'error' | 'empty' | 'data' | 'stale';\n data?: string[];\n getItemProps: GetItemProps;\n highlightedIndex: number;\n}\n\nexport const SearchResults = forwardRef<HTMLUListElement, SearchResultsProps>((props, ref) => {\n const { state, data, getItemProps, highlightedIndex, ...rest } = props;\n\n return (\n <>\n {(() => {\n switch (state) {\n case 'initial':\n return <MessageWrapper>Search for icons</MessageWrapper>;\n case 'loading':\n return <MessageWrapper>Searching...</MessageWrapper>;\n case 'error':\n return <MessageWrapper>Something went wrong...</MessageWrapper>;\n case 'empty':\n return <MessageWrapper>No icons found</MessageWrapper>;\n }\n })()}\n\n <ul {...rest} ref={ref} style={{ opacity: state === 'stale' ? 0.5 : 1 }}>\n {(state === 'data' || state === 'stale') &&\n data?.map((icon, index) => (\n <li key={icon} {...getItemProps({ item: icon, index })}>\n <Button padding={3} mode=\"bleed\" selected={index === highlightedIndex}>\n <Icon icon={icon} width=\"100%\" height=\"100%\" />\n </Button>\n </li>\n ))}\n </ul>\n </>\n );\n});\n","import { TrashIcon } from '@sanity/icons';\nimport { Button, Card } from '@sanity/ui';\n\n// ------------ //\n// UNSET BUTTON //\n// ------------ //\n\ninterface UnsetButtonProps {\n onUnset: () => void;\n}\n\nexport function UnsetButton(props: UnsetButtonProps) {\n const { onUnset } = props;\n\n return (\n <Card border borderLeft={false} padding={1} display=\"flex\" radius={2}>\n <Button icon={<TrashIcon />} onClick={onUnset} mode=\"bleed\" fontSize={1} padding={2} />\n </Card>\n );\n}\n","import { Popover, useToast } from '@sanity/ui';\nimport { useCombobox } from 'downshift';\nimport { useCallback, useEffect, useId, useRef } from 'react';\nimport { match } from 'ts-pattern';\nimport { useSearch } from '../lib/api';\nimport { OptionsWrapper } from './iconify-combobox.styles';\nimport { SearchInput } from './search-input';\nimport type { SearchResultsProps } from './search-result';\nimport { SearchResults } from './search-result';\nimport { UnsetButton } from './unset-button';\n\nexport interface IconifyComboboxProps {\n selectedIcon: string | null;\n onSelect: (newValue: string) => void;\n collections: string[] | null;\n}\n\nexport function IconifyCombobox(props: IconifyComboboxProps) {\n const { selectedIcon, onSelect: pushSelection, collections } = props;\n\n const id = useId();\n const toast = useToast();\n const inputRef = useRef<HTMLInputElement>(null);\n\n const { term, setTerm, debouncedTerm, isLoading, isError, error, data, isPreviousData } =\n useSearch({\n collections,\n });\n\n const {\n isOpen,\n getMenuProps,\n getInputProps,\n highlightedIndex,\n getItemProps,\n selectItem,\n setInputValue,\n } = useCombobox({\n items: data ?? [],\n inputValue: term,\n onInputValueChange({ inputValue, selectedItem }) {\n if (inputValue !== selectedItem) {\n setTerm(inputValue);\n }\n },\n onSelectedItemChange({ selectedItem }) {\n if (selectedItem) {\n pushSelection(selectedItem);\n setTerm('', true);\n setInputValue('');\n }\n },\n });\n\n const handleUnset = useCallback(() => {\n pushSelection('');\n setTerm('', true);\n selectItem('');\n }, []);\n\n useEffect(() => {\n if (isError) {\n console.error('Iconify input error:', error);\n\n toast.push({\n id,\n status: 'error',\n title: 'Iconify input error',\n description: error?.message,\n });\n }\n }, [error, id, isError, toast]);\n\n return (\n <div>\n {/* @ts-expect-error wrong typings */}\n <SearchInput\n {...getInputProps({ ref: inputRef })}\n selectedIcon={selectedIcon}\n suffix={selectedIcon ? <UnsetButton onUnset={handleUnset} /> : null}\n />\n\n <Popover\n open={true}\n style={{ display: isOpen ? 'block' : 'none' }}\n placement=\"bottom\"\n arrow={false}\n matchReferenceWidth\n constrainSize\n referenceElement={inputRef.current}\n content={\n <OptionsWrapper>\n <SearchResults\n {...getMenuProps()}\n state={match<boolean>(true)\n .returnType<SearchResultsProps['state']>()\n .with(isLoading, () => 'loading')\n .with(!debouncedTerm, () => 'initial')\n .with(isError, () => 'error')\n .with(!data || data.length === 0, () => 'empty')\n .with(isPreviousData, () => 'stale')\n .otherwise(() => 'data')}\n data={data}\n getItemProps={getItemProps}\n highlightedIndex={highlightedIndex}\n />\n </OptionsWrapper>\n }\n />\n </div>\n );\n}\n","import {\n QueryClient,\n QueryClientProvider as ReactQueryClientProvider,\n} from '@tanstack/react-query';\nimport type { ReactNode } from 'react';\n\nexport const queryClient = new QueryClient();\n\nexport function QueryClientProvider(props: { children: ReactNode }) {\n return <ReactQueryClientProvider client={queryClient}>{props.children}</ReactQueryClientProvider>;\n}\n","import type { IconifyIconName } from '@iconify/utils';\nimport { stringToIcon } from '@iconify/utils';\nimport { sentenceCase } from 'change-case';\nimport { useMemo } from 'react';\nimport { useIconSetInfo } from './api';\n\ninterface UsePrettyIconNameProps {\n name?: string | null;\n iconMeta?: IconifyIconName | null;\n}\n\nexport function usePrettyIconName(props: UsePrettyIconNameProps) {\n const { name } = props;\n const iconMeta = useMemo(\n () => props.iconMeta ?? (name ? stringToIcon(name) : null),\n [name, props.iconMeta],\n );\n const iconSetInfo = useIconSetInfo({ prefix: iconMeta?.prefix ?? null });\n\n return useMemo(\n () =>\n iconMeta\n ? {\n name: sentenceCase(iconMeta.name),\n collection: iconSetInfo.data?.name ?? iconMeta.prefix,\n }\n : null,\n [iconMeta, iconSetInfo.data?.name],\n );\n}\n","import { Flex, Stack, Text, ThemeProvider } from '@sanity/ui';\nimport { buildTheme } from '@sanity/ui/theme';\nimport { useCallback } from 'react';\nimport type { ObjectInputProps } from 'sanity';\nimport { set, unset } from 'sanity';\nimport { IconifyCombobox } from './combobox';\nimport { QueryClientProvider } from './lib/query-client';\nimport type { IconifyPluginConfig, IconOptions } from './lib/types';\nimport { usePrettyIconName } from './lib/use-pretty-icon-name';\n\ninterface IconifyInputProps extends ObjectInputProps {\n config: IconifyPluginConfig;\n}\n\nexport function IconifyInput(props: IconifyInputProps) {\n const { config, value, onChange: pushChange, schemaType } = props;\n\n const selectedIcon: string | null = value?.name ?? null;\n\n const options: IconOptions = schemaType.options;\n const collections =\n (!!options?.collections?.length && options.collections) ||\n (!!config?.collections?.length && config.collections) ||\n null;\n const showName = options?.showName ?? config?.showName ?? false;\n\n const handleSelect = useCallback(\n (icon: string) => {\n pushChange(icon === '' ? unset() : set(icon, ['name']));\n },\n [pushChange],\n );\n\n return (\n <QueryClientProvider>\n <ThemeProvider theme={buildTheme()}>\n <Stack space={2}>\n <IconifyCombobox\n selectedIcon={selectedIcon}\n onSelect={handleSelect}\n collections={collections}\n />\n\n {showName && selectedIcon ? <IconifyNameDisplay name={selectedIcon} /> : null}\n </Stack>\n </ThemeProvider>\n </QueryClientProvider>\n );\n}\n\ninterface IconifyNameDisplayProps {\n name?: string | null;\n}\n\nexport function IconifyNameDisplay(props: IconifyNameDisplayProps) {\n const { name } = props;\n const prettyName = usePrettyIconName({ name });\n\n return (\n <Flex gap={1}>\n <Text size={1} muted>\n Selected:\n </Text>\n\n <Text size={1} weight=\"semibold\">\n {prettyName?.name ?? name}\n </Text>\n\n {prettyName?.collection && (\n <Text size={1} muted style={{ fontStyle: 'italic' }}>\n by {prettyName?.collection}\n </Text>\n )}\n </Flex>\n );\n}\n","import { Icon } from '@iconify/react';\nimport type { IconifyIconName } from '@iconify/utils';\nimport { stringToIcon } from '@iconify/utils';\nimport type { PreviewProps } from 'sanity';\nimport { QueryClientProvider } from './lib/query-client';\nimport { usePrettyIconName } from './lib/use-pretty-icon-name';\n\n// --------------- //\n// ICONIFY PREVIEW //\n// --------------- //\n\nexport function IconifyPreview(props: PreviewProps) {\n const { title } = props;\n const iconName = typeof props.title === 'string' ? stringToIcon(props.title) : null;\n\n // We check this double to avoid the TS error\n if (typeof title === 'string' && iconName) {\n return (\n <QueryClientProvider>\n <IconifyPreviewInner {...props} iconName={title} iconMeta={iconName} />\n </QueryClientProvider>\n );\n }\n\n return props.renderDefault(props);\n}\n\n// --------------------- //\n// ICONIFY PREVIEW INNER //\n// --------------------- //\n\ninterface IconifyPreviewInnerProps extends PreviewProps {\n iconName: string;\n iconMeta: IconifyIconName;\n}\n\nfunction IconifyPreviewInner(props: IconifyPreviewInnerProps) {\n const { iconMeta, iconName, ...previewProps } = props;\n const prettyName = usePrettyIconName({ iconMeta });\n\n return props.renderDefault({\n ...previewProps,\n media: <Icon icon={iconName} />,\n title: prettyName?.name ?? iconName,\n subtitle: prettyName?.collection,\n });\n}\n","import type { FieldProps, ObjectInputProps } from 'sanity';\nimport { definePlugin } from 'sanity';\nimport { IconifyInput } from './iconify-input';\nimport { IconifyPreview } from './iconify-preview';\nimport type { IconifyPluginConfig } from './lib/types';\n\n/**\n * Usage in `sanity.config.ts` (or .js)\n *\n * ```ts\n * import { defineConfig } from 'sanity'\n * import { iconify } from 'sanity-plugin-iconify'\n *\n * export default defineConfig({\n * // ...\n * plugins: [iconify()],\n * })\n * ```\n */\nexport const iconify = definePlugin<IconifyPluginConfig | void>((config = {}) => {\n return {\n name: 'sanity-plugin-iconify',\n schema: {\n types: [\n {\n name: 'icon',\n title: 'Icon',\n type: 'object',\n fields: [\n {\n name: 'name',\n title: 'Name',\n type: 'string',\n },\n ],\n components: {\n input: (props: ObjectInputProps) => <IconifyInput {...props} config={config!} />,\n preview: IconifyPreview,\n\n // This makes sure the input component is not indented\n field: (props: FieldProps) => props.renderDefault({ ...props, level: 0 }),\n },\n },\n ],\n },\n };\n});\n"],"names":["queryClient","ReactQueryClientProvider"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/lib/api.ts","../src/combobox/iconify-combobox.styles.tsx","../src/combobox/search-input.tsx","../src/combobox/search-result.tsx","../src/combobox/unset-button.tsx","../src/combobox/iconify-combobox.tsx","../src/lib/query-client.tsx","../src/lib/use-pretty-icon-name.ts","../src/iconify-input.tsx","../src/iconify-preview.tsx","../src/iconify-plugin.tsx"],"sourcesContent":["import type { IconifyInfo } from '@iconify/types';\nimport { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { useCallback, useState } from 'react';\nimport { useDebounce } from 'use-debounce';\nimport type { IconifySearchResult } from './types';\n\nconst BASE_API_URL = 'https://api.iconify.design';\n\nfunction fetchJson<T>({ url, signal }: { url: string | URL; signal?: AbortSignal }): Promise<T> {\n return fetch(url, { signal })\n .then((response) => {\n if (!response.ok) {\n throw new Error(`Network error: status ${response.status}`);\n }\n\n return response.json();\n })\n .then(\n (result) => result as T,\n (error) => {\n if (error instanceof Error) {\n throw error;\n } else {\n console.error(`Unknown error: ${error}`);\n throw new Error('Something went wrong');\n }\n },\n );\n}\n\nexport function useSearch({ collections }: { collections: string[] | null }) {\n const queryClient = useQueryClient();\n const [term, setTerm] = useState('');\n const [debouncedTerm, setDebouncedTerm] = useDebounce(term, 500);\n\n const updateTerm = useCallback(\n (newTerm: string, updateImmediately = false) => {\n setTerm(newTerm);\n\n if (updateImmediately) {\n setDebouncedTerm(newTerm);\n }\n },\n [setDebouncedTerm],\n );\n\n const { isLoading, isError, error, data, isPlaceholderData } = useQuery<string[], Error>({\n queryKey: ['search', collections, debouncedTerm],\n queryFn: async ({ signal }) => {\n const url = new URL(`/search`, BASE_API_URL);\n\n url.searchParams.append('query', debouncedTerm);\n url.searchParams.append('limit', '60');\n\n if (collections) {\n url.searchParams.append('prefixes', collections.join(','));\n }\n\n const result = debouncedTerm ? await fetchJson<IconifySearchResult>({ url, signal }) : null;\n\n if (result) {\n // Cache the info for each collection\n Object.entries(result.collections).forEach(([prefix, info]) => {\n queryClient.setQueryData<IconifyInfo>(['iconSetInfo', prefix], info);\n });\n }\n\n return result?.icons ?? [];\n },\n enabled: debouncedTerm.length > 0,\n placeholderData: keepPreviousData,\n staleTime: 5 * 60 * 1000, // 5 minutes\n });\n\n return {\n term,\n setTerm: updateTerm,\n debouncedTerm,\n isLoading,\n isError,\n error,\n data,\n isPreviousData: isPlaceholderData,\n };\n}\n\nexport function useIconSetInfo({ prefix }: { prefix?: string | null }) {\n return useQuery<IconifyInfo | null, Error>({\n queryKey: ['iconSetInfo', prefix],\n queryFn: async ({ signal }) => {\n if (!prefix) return null;\n\n const url = new URL('/collection', BASE_API_URL);\n\n url.searchParams.append('prefix', prefix);\n url.searchParams.append('info', 'true');\n\n const result = await fetchJson<{ info: IconifyInfo }>({ url, signal });\n\n return result?.info ?? null;\n },\n staleTime: Infinity,\n });\n}\n","import { Box, Card, Grid, Text } from '@sanity/ui';\nimport type { ReactNode } from 'react';\nimport styled from 'styled-components';\n\nexport const ComboboxWrapper = styled(Grid)`\n grid-template-columns: 1fr min-content;\n position: relative;\n`;\n\nexport const OptionsWrapper = styled(Box)`\n box-sizing: border-box;\n padding: 0.5rem;\n\n & [role='listbox'] {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin: 0;\n padding: 0;\n list-style: none;\n }\n\n & [role='option'] {\n display: grid;\n place-items: center;\n width: clamp(3rem, 10vw, 4rem);\n\n & button {\n cursor: pointer;\n width: 100%;\n\n & > [data-ui='Box'] {\n display: flex;\n }\n\n & svg {\n aspect-ratio: 1;\n }\n }\n }\n`;\n\nexport function MessageWrapper({ children }: { children: ReactNode }) {\n return (\n <Card padding={4}>\n <Text align=\"center\" muted>\n {children}\n </Text>\n </Card>\n );\n}\n","import { Icon } from '@iconify/react';\nimport { TextInput } from '@sanity/ui';\nimport { forwardRef, memo } from 'react';\n\ninterface SearchInputProps extends React.HTMLAttributes<HTMLInputElement> {\n selectedIcon: string | null;\n suffix?: React.ReactNode;\n}\n\nexport const SearchInput = memo(\n forwardRef<HTMLInputElement, SearchInputProps>((props, ref) => {\n const { selectedIcon, suffix, ...rest } = props;\n\n return (\n <TextInput\n {...rest}\n ref={ref}\n inputMode=\"search\"\n icon={selectedIcon ? <Icon icon={selectedIcon} /> : null}\n placeholder={selectedIcon ? 'Search and replace selected icon...' : 'Search for an icon...'}\n suffix={suffix}\n />\n );\n }),\n);\n\nSearchInput.displayName = 'SearchInput';\n","import { Icon } from '@iconify/react';\nimport { Button } from '@sanity/ui';\nimport type { UseComboboxGetItemPropsOptions, UseComboboxGetItemPropsReturnValue } from 'downshift';\nimport { forwardRef, memo } from 'react';\nimport { MessageWrapper } from './iconify-combobox.styles';\n\ntype GetItemProps = (\n options: UseComboboxGetItemPropsOptions<string>,\n) => UseComboboxGetItemPropsReturnValue;\n\nexport interface SearchResultsProps extends React.HTMLAttributes<HTMLUListElement> {\n state: 'initial' | 'loading' | 'error' | 'empty' | 'data' | 'stale';\n data?: string[];\n getItemProps: GetItemProps;\n highlightedIndex: number;\n}\n\nexport const SearchResults = memo(\n forwardRef<HTMLUListElement, SearchResultsProps>((props, ref) => {\n const { state, data, getItemProps, highlightedIndex, ...rest } = props;\n\n return (\n <>\n {(() => {\n switch (state) {\n case 'initial':\n return <MessageWrapper>Search for icons</MessageWrapper>;\n case 'loading':\n return <MessageWrapper>Searching...</MessageWrapper>;\n case 'error':\n return <MessageWrapper>Something went wrong...</MessageWrapper>;\n case 'empty':\n return <MessageWrapper>No icons found</MessageWrapper>;\n }\n })()}\n\n <ul\n {...rest}\n ref={ref}\n data-testid=\"iconify-results\"\n style={{ opacity: state === 'stale' ? 0.5 : 1 }}\n >\n {(state === 'data' || state === 'stale') &&\n data?.map((icon, index) => (\n <li key={icon} {...getItemProps({ item: icon, index })}>\n <Button padding={3} mode=\"bleed\" selected={index === highlightedIndex}>\n <Icon icon={icon} width=\"100%\" height=\"100%\" />\n </Button>\n </li>\n ))}\n </ul>\n </>\n );\n }),\n);\n","import { TrashIcon } from '@sanity/icons';\nimport { Button, Card } from '@sanity/ui';\n\n// ------------ //\n// UNSET BUTTON //\n// ------------ //\n\ninterface UnsetButtonProps {\n onUnset: () => void;\n}\n\nexport function UnsetButton(props: UnsetButtonProps) {\n const { onUnset } = props;\n\n return (\n <Card border borderLeft={false} padding={1} display=\"flex\" radius={2}>\n <Button\n data-testid=\"iconify-unset\"\n icon={<TrashIcon />}\n onClick={onUnset}\n mode=\"bleed\"\n fontSize={1}\n padding={2}\n />\n </Card>\n );\n}\n","import { Popover, useToast } from '@sanity/ui';\nimport { useCombobox } from 'downshift';\nimport { memo, useCallback, useEffect, useId, useRef } from 'react';\nimport type { ObjectInputProps } from 'sanity';\nimport { match } from 'ts-pattern';\nimport { useSearch } from '../lib/api';\nimport { OptionsWrapper } from './iconify-combobox.styles';\nimport { SearchInput } from './search-input';\nimport type { SearchResultsProps } from './search-result';\nimport { SearchResults } from './search-result';\nimport { UnsetButton } from './unset-button';\n\nexport interface IconifyComboboxProps {\n selectedIcon: string | null;\n onSelect: (newValue: string) => void;\n collections: string[] | null;\n studioElementProps?: ObjectInputProps['elementProps'];\n fieldFocused?: boolean;\n}\n\nexport const IconifyCombobox = memo(function IconifyCombobox(props: IconifyComboboxProps) {\n const {\n selectedIcon,\n onSelect: pushSelection,\n collections,\n studioElementProps,\n fieldFocused,\n } = props;\n\n const id = useId();\n const toast = useToast();\n const inputRef = useRef<HTMLInputElement>(null);\n\n // Compose our inputRef (used by the Popover for positioning) with Studio's focusRef.\n // Standard Sanity inputs spread all of elementProps onto the native element, which\n // lets Studio programmatically re-focus the field and track focus via onPathFocus/onPathBlur.\n const composedInputRef = useCallback(\n (node: HTMLInputElement | null) => {\n inputRef.current = node;\n if (studioElementProps?.ref) {\n (studioElementProps.ref as React.RefObject<HTMLInputElement | null>).current = node;\n }\n },\n [studioElementProps?.ref],\n );\n\n // Studio steals focus to its field-actions-trigger immediately after field activation, then\n // re-focuses the actual input — all within the same macrotask as the original focus event.\n // We suppress the onPathBlur for this activation blur so Studio's focused state stays accurate.\n // The flag is reset via setTimeout so any blur in a later macrotask (real user navigation) is\n // forwarded normally.\n const suppressNextBlur = useRef(false);\n\n const handleFocus = useCallback(\n (event: React.FocusEvent<HTMLInputElement>) => {\n suppressNextBlur.current = true;\n setTimeout(() => {\n suppressNextBlur.current = false;\n }, 0);\n studioElementProps?.onFocus?.(event as unknown as React.FocusEvent<HTMLDivElement>);\n },\n [studioElementProps],\n );\n\n const handleBlur = useCallback(\n (event: React.FocusEvent<HTMLInputElement>) => {\n if (suppressNextBlur.current) {\n suppressNextBlur.current = false;\n return;\n }\n studioElementProps?.onBlur?.(event as unknown as React.FocusEvent<HTMLDivElement>);\n },\n [studioElementProps],\n );\n\n const { term, setTerm, debouncedTerm, isLoading, isError, error, data, isPreviousData } =\n useSearch({\n collections,\n });\n\n const {\n isOpen,\n getMenuProps,\n getInputProps,\n highlightedIndex,\n getItemProps,\n selectItem,\n setInputValue,\n closeMenu,\n } = useCombobox({\n items: data ?? [],\n inputValue: term,\n onInputValueChange({ inputValue, selectedItem }) {\n if (inputValue !== selectedItem) {\n setTerm(inputValue);\n }\n },\n onSelectedItemChange({ selectedItem }) {\n if (selectedItem) {\n pushSelection(selectedItem);\n setTerm('', true);\n setInputValue('');\n }\n },\n });\n\n // Close the menu when Studio considers the field unfocused. This is the state→UI equivalent\n // of the old stateReducer: instead of intercepting downshift's blur handling imperatively,\n // we let Studio's focused state drive whether the menu should be open.\n useEffect(() => {\n if (!fieldFocused) closeMenu();\n }, [fieldFocused, closeMenu]);\n\n const handleUnset = useCallback(() => {\n pushSelection('');\n setTerm('', true);\n selectItem('');\n }, [pushSelection, setTerm, selectItem]);\n\n useEffect(() => {\n if (isError) {\n console.error('Iconify input error:', error);\n\n toast.push({\n id,\n status: 'error',\n title: 'Iconify input error',\n description: error?.message,\n });\n }\n }, [error, id, isError, toast]);\n\n // Destructure onBlur out so downshift's InputBlur doesn't close the menu —\n // menu lifecycle is now driven by fieldFocused via the effect above.\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { onBlur: _downshiftOnBlur, ...inputProps } = getInputProps({\n ref: composedInputRef,\n id: studioElementProps?.id,\n 'aria-describedby': studioElementProps?.['aria-describedby'],\n onFocus: handleFocus,\n });\n\n return (\n <div>\n {/* @ts-expect-error wrong typings */}\n <SearchInput\n {...inputProps}\n onBlur={handleBlur}\n selectedIcon={selectedIcon}\n suffix={selectedIcon ? <UnsetButton onUnset={handleUnset} /> : null}\n />\n\n <Popover\n open={true}\n style={{ display: isOpen ? 'block' : 'none' }}\n placement=\"bottom\"\n arrow={false}\n matchReferenceWidth\n constrainSize\n referenceElement={inputRef.current}\n content={\n <OptionsWrapper>\n <SearchResults\n {...getMenuProps()}\n state={match<boolean>(true)\n .returnType<SearchResultsProps['state']>()\n .with(isLoading, () => 'loading')\n .with(!debouncedTerm, () => 'initial')\n .with(isError, () => 'error')\n .with(!data || data.length === 0, () => 'empty')\n .with(isPreviousData, () => 'stale')\n .otherwise(() => 'data')}\n data={data}\n getItemProps={getItemProps}\n highlightedIndex={highlightedIndex}\n />\n </OptionsWrapper>\n }\n />\n </div>\n );\n});\n","import {\n QueryClient,\n QueryClientProvider as ReactQueryClientProvider,\n} from '@tanstack/react-query';\nimport type { ReactNode } from 'react';\n\nexport const queryClient = new QueryClient();\n\nexport function QueryClientProvider(props: { children: ReactNode }) {\n return <ReactQueryClientProvider client={queryClient}>{props.children}</ReactQueryClientProvider>;\n}\n","import type { IconifyIconName } from '@iconify/utils';\nimport { stringToIcon } from '@iconify/utils';\nimport { sentenceCase } from 'change-case';\nimport { useMemo } from 'react';\nimport { useIconSetInfo } from './api';\n\ninterface UsePrettyIconNameProps {\n name?: string | null;\n iconMeta?: IconifyIconName | null;\n}\n\nexport function usePrettyIconName(props: UsePrettyIconNameProps) {\n const { name } = props;\n const iconMeta = useMemo(\n () => props.iconMeta ?? (name ? stringToIcon(name) : null),\n [name, props.iconMeta],\n );\n const iconSetInfo = useIconSetInfo({ prefix: iconMeta?.prefix ?? null });\n\n return useMemo(\n () =>\n iconMeta\n ? {\n name: sentenceCase(iconMeta.name),\n collection: iconSetInfo.data?.name ?? iconMeta.prefix,\n }\n : null,\n [iconMeta, iconSetInfo.data?.name],\n );\n}\n","import { Flex, Stack, Text, ThemeProvider } from '@sanity/ui';\nimport { buildTheme } from '@sanity/ui/theme';\nimport { memo, useCallback } from 'react';\nimport type { ObjectInputProps } from 'sanity';\nimport { set, unset } from 'sanity';\nimport { IconifyCombobox } from './combobox';\nimport { QueryClientProvider } from './lib/query-client';\nimport type { IconifyPluginConfig, IconOptions } from './lib/types';\nimport { usePrettyIconName } from './lib/use-pretty-icon-name';\n\nconst theme = buildTheme();\n\ninterface IconifyInputProps extends ObjectInputProps {\n config: IconifyPluginConfig;\n}\n\nexport const IconifyInput = memo(function IconifyInput(props: IconifyInputProps) {\n const { config, value, onChange: pushChange, schemaType, elementProps, focused } = props;\n\n const selectedIcon: string | null = value?.name ?? null;\n\n const options: IconOptions = schemaType.options;\n const collections =\n (!!options?.collections?.length && options.collections) ||\n (!!config?.collections?.length && config.collections) ||\n null;\n const showName = options?.showName ?? config?.showName ?? false;\n\n const handleSelect = useCallback(\n (icon: string) => {\n pushChange(icon === '' ? unset() : set(icon, ['name']));\n },\n [pushChange],\n );\n\n return (\n <QueryClientProvider>\n <ThemeProvider theme={theme}>\n <Stack space={2}>\n <IconifyCombobox\n selectedIcon={selectedIcon}\n onSelect={handleSelect}\n collections={collections}\n studioElementProps={elementProps}\n fieldFocused={focused}\n />\n\n {showName && selectedIcon ? <IconifyNameDisplay name={selectedIcon} /> : null}\n </Stack>\n </ThemeProvider>\n </QueryClientProvider>\n );\n});\n\ninterface IconifyNameDisplayProps {\n name?: string | null;\n}\n\nexport function IconifyNameDisplay(props: IconifyNameDisplayProps) {\n const { name } = props;\n const prettyName = usePrettyIconName({ name });\n\n return (\n <Flex gap={1}>\n <Text size={1} muted>\n Selected:\n </Text>\n\n <Text size={1} weight=\"semibold\">\n {prettyName?.name ?? name}\n </Text>\n\n {prettyName?.collection && (\n <Text size={1} muted style={{ fontStyle: 'italic' }}>\n by {prettyName?.collection}\n </Text>\n )}\n </Flex>\n );\n}\n","import { Icon } from '@iconify/react';\nimport type { IconifyIconName } from '@iconify/utils';\nimport { stringToIcon } from '@iconify/utils';\nimport { memo } from 'react';\nimport type { PreviewProps } from 'sanity';\nimport { QueryClientProvider } from './lib/query-client';\nimport { usePrettyIconName } from './lib/use-pretty-icon-name';\n\n// --------------- //\n// ICONIFY PREVIEW //\n// --------------- //\n\nexport const IconifyPreview = memo(function IconifyPreview(props: PreviewProps) {\n const { title } = props;\n const iconName = typeof props.title === 'string' ? stringToIcon(props.title) : null;\n\n // We check this double to avoid the TS error\n if (typeof title === 'string' && iconName) {\n return (\n <QueryClientProvider>\n <IconifyPreviewInner {...props} iconName={title} iconMeta={iconName} />\n </QueryClientProvider>\n );\n }\n\n return props.renderDefault(props);\n});\n\n// --------------------- //\n// ICONIFY PREVIEW INNER //\n// --------------------- //\n\ninterface IconifyPreviewInnerProps extends PreviewProps {\n iconName: string;\n iconMeta: IconifyIconName;\n}\n\nfunction IconifyPreviewInner(props: IconifyPreviewInnerProps) {\n const { iconMeta, iconName, ...previewProps } = props;\n const prettyName = usePrettyIconName({ iconMeta });\n\n return props.renderDefault({\n ...previewProps,\n media: <Icon icon={iconName} />,\n title: prettyName?.name ?? iconName,\n subtitle: prettyName?.collection,\n });\n}\n","import type { FieldProps, ObjectInputProps } from 'sanity';\nimport { definePlugin } from 'sanity';\nimport { IconifyInput } from './iconify-input';\nimport { IconifyPreview } from './iconify-preview';\nimport type { IconifyPluginConfig } from './lib/types';\n\n/**\n * Usage in `sanity.config.ts` (or .js)\n *\n * ```ts\n * import { defineConfig } from 'sanity'\n * import { iconify } from 'sanity-plugin-iconify'\n *\n * export default defineConfig({\n * // ...\n * plugins: [iconify()],\n * })\n * ```\n */\nexport const iconify = definePlugin<IconifyPluginConfig | void>((config = {}) => {\n return {\n name: 'sanity-plugin-iconify',\n schema: {\n types: [\n {\n name: 'icon',\n title: 'Icon',\n type: 'object',\n fields: [\n {\n name: 'name',\n title: 'Name',\n type: 'string',\n },\n ],\n components: {\n input: (props: ObjectInputProps) => <IconifyInput {...props} config={config!} />,\n preview: IconifyPreview,\n\n // This makes sure the input component is not indented\n field: (props: FieldProps) => props.renderDefault({ ...props, level: 0 }),\n },\n },\n ],\n },\n };\n});\n"],"names":["queryClient","ReactQueryClientProvider"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-iconify",
3
- "version": "2.1.0",
3
+ "version": "3.0.0-beta.2",
4
4
  "description": "Icon picker based on Iconify",
5
5
  "keywords": [
6
6
  "sanity",
@@ -42,56 +42,66 @@
42
42
  "build:check": "pkg-utils --strict",
43
43
  "build:clean": "rimraf dist",
44
44
  "build:verify": "plugin-kit verify-package --silent",
45
- "dev": "tsx dev/vite.ts",
45
+ "dev": "vite dev --config dev/vite.config.ts",
46
46
  "format": "prettier --write --cache --ignore-unknown .",
47
47
  "generate-types": "node src/lib/generate-types",
48
48
  "lint": "eslint .",
49
49
  "release": "release-it",
50
+ "test": "vitest run",
51
+ "test:e2e": "playwright test",
52
+ "test:e2e:setup": "tsx playwright/auth-setup.ts",
53
+ "test:e2e:ui": "playwright test --ui",
54
+ "test:ui": "vitest --ui",
55
+ "test:watch": "vitest",
50
56
  "watch": "pkg-utils watch --strict"
51
57
  },
52
58
  "browserslist": "extends @sanity/browserslist-config",
53
59
  "dependencies": {
54
60
  "@iconify/react": "^6.0.2",
55
- "@iconify/utils": "^3.0.2",
61
+ "@iconify/utils": "^3.1.0",
56
62
  "@sanity/icons": "^3.7.4",
57
63
  "@sanity/incompatible-plugin": "^1.0.5",
58
64
  "@sanity/ui": "^3.1.3",
59
65
  "@tanstack/react-query": "^5.89.0",
60
66
  "change-case": "^5.4.4",
61
- "downshift": "^9.0.10",
62
- "ts-pattern": "^5.8.0",
67
+ "downshift": "^9.0.13",
68
+ "ts-pattern": "^5.9.0",
63
69
  "use-debounce": "^10.0.6"
64
70
  },
65
71
  "devDependencies": {
66
- "@commitlint/cli": "^19.8.1",
67
- "@commitlint/config-conventional": "^19.8.1",
68
- "@iconify/json": "^2.2.385",
72
+ "@commitlint/cli": "^20.2.0",
73
+ "@commitlint/config-conventional": "^20.2.0",
74
+ "@iconify/json": "^2.2.420",
69
75
  "@iconify/types": "^2.0.0",
70
- "@release-it/conventional-changelog": "^10.0.1",
71
- "@sanity/pkg-utils": "^8.1.12",
72
- "@sanity/plugin-kit": "^4.0.19",
76
+ "@playwright/test": "^1.58.2",
77
+ "@release-it/conventional-changelog": "^10.0.4",
78
+ "@sanity/pkg-utils": "^10.2.3",
79
+ "@sanity/plugin-kit": "^4.0.20",
73
80
  "@tanstack/eslint-plugin-query": "^5.89.0",
74
81
  "@tanstack/react-query-devtools": "^5.89.0",
75
- "@types/react": "^19.1.13",
76
- "@types/styled-components": "^5.1.34",
82
+ "@types/react": "^19.2.7",
83
+ "@types/styled-components": "^5.1.36",
84
+ "@vitejs/plugin-react": "^5.1.2",
85
+ "@vitest/ui": "^4.1.0",
77
86
  "@waspeer/config": "^2.4.5",
78
- "eslint": "^9.35.0",
79
- "lefthook": "^1.13.1",
87
+ "eslint": "^9.39.2",
88
+ "lefthook": "^2.0.12",
80
89
  "npm-run-all2": "^8.0.4",
81
- "prettier": "^3.6.2",
82
- "react": "^19.1.1",
83
- "react-dom": "^19.1.1",
84
- "react-is": "^19.1.1",
85
- "release-it": "^19.0.5",
86
- "rimraf": "^6.0.1",
87
- "sanity": "^4.9.0",
88
- "tsx": "^4.20.5",
89
- "typescript": "^5.9.2",
90
- "vite": "^7.1.6"
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"
91
101
  },
92
102
  "peerDependencies": {
93
- "react": "^18 || ^19",
94
- "sanity": "^3 || ^4",
103
+ "react": "^19.2",
104
+ "sanity": "^5.0.0-0",
95
105
  "styled-components": "^6"
96
106
  },
97
107
  "packageManager": "pnpm@10.17.0",
@@ -1,6 +1,7 @@
1
1
  import { Popover, useToast } from '@sanity/ui';
2
2
  import { useCombobox } from 'downshift';
3
- import { useCallback, useEffect, useId, useRef } from 'react';
3
+ import { memo, useCallback, useEffect, useId, useRef } from 'react';
4
+ import type { ObjectInputProps } from 'sanity';
4
5
  import { match } from 'ts-pattern';
5
6
  import { useSearch } from '../lib/api';
6
7
  import { OptionsWrapper } from './iconify-combobox.styles';
@@ -13,15 +14,65 @@ export interface IconifyComboboxProps {
13
14
  selectedIcon: string | null;
14
15
  onSelect: (newValue: string) => void;
15
16
  collections: string[] | null;
17
+ studioElementProps?: ObjectInputProps['elementProps'];
18
+ fieldFocused?: boolean;
16
19
  }
17
20
 
18
- export function IconifyCombobox(props: IconifyComboboxProps) {
19
- const { selectedIcon, onSelect: pushSelection, collections } = props;
21
+ export const IconifyCombobox = memo(function IconifyCombobox(props: IconifyComboboxProps) {
22
+ const {
23
+ selectedIcon,
24
+ onSelect: pushSelection,
25
+ collections,
26
+ studioElementProps,
27
+ fieldFocused,
28
+ } = props;
20
29
 
21
30
  const id = useId();
22
31
  const toast = useToast();
23
32
  const inputRef = useRef<HTMLInputElement>(null);
24
33
 
34
+ // Compose our inputRef (used by the Popover for positioning) with Studio's focusRef.
35
+ // Standard Sanity inputs spread all of elementProps onto the native element, which
36
+ // lets Studio programmatically re-focus the field and track focus via onPathFocus/onPathBlur.
37
+ const composedInputRef = useCallback(
38
+ (node: HTMLInputElement | null) => {
39
+ inputRef.current = node;
40
+ if (studioElementProps?.ref) {
41
+ (studioElementProps.ref as React.RefObject<HTMLInputElement | null>).current = node;
42
+ }
43
+ },
44
+ [studioElementProps?.ref],
45
+ );
46
+
47
+ // Studio steals focus to its field-actions-trigger immediately after field activation, then
48
+ // re-focuses the actual input — all within the same macrotask as the original focus event.
49
+ // We suppress the onPathBlur for this activation blur so Studio's focused state stays accurate.
50
+ // The flag is reset via setTimeout so any blur in a later macrotask (real user navigation) is
51
+ // forwarded normally.
52
+ const suppressNextBlur = useRef(false);
53
+
54
+ const handleFocus = useCallback(
55
+ (event: React.FocusEvent<HTMLInputElement>) => {
56
+ suppressNextBlur.current = true;
57
+ setTimeout(() => {
58
+ suppressNextBlur.current = false;
59
+ }, 0);
60
+ studioElementProps?.onFocus?.(event as unknown as React.FocusEvent<HTMLDivElement>);
61
+ },
62
+ [studioElementProps],
63
+ );
64
+
65
+ const handleBlur = useCallback(
66
+ (event: React.FocusEvent<HTMLInputElement>) => {
67
+ if (suppressNextBlur.current) {
68
+ suppressNextBlur.current = false;
69
+ return;
70
+ }
71
+ studioElementProps?.onBlur?.(event as unknown as React.FocusEvent<HTMLDivElement>);
72
+ },
73
+ [studioElementProps],
74
+ );
75
+
25
76
  const { term, setTerm, debouncedTerm, isLoading, isError, error, data, isPreviousData } =
26
77
  useSearch({
27
78
  collections,
@@ -35,6 +86,7 @@ export function IconifyCombobox(props: IconifyComboboxProps) {
35
86
  getItemProps,
36
87
  selectItem,
37
88
  setInputValue,
89
+ closeMenu,
38
90
  } = useCombobox({
39
91
  items: data ?? [],
40
92
  inputValue: term,
@@ -52,11 +104,18 @@ export function IconifyCombobox(props: IconifyComboboxProps) {
52
104
  },
53
105
  });
54
106
 
107
+ // Close the menu when Studio considers the field unfocused. This is the state→UI equivalent
108
+ // of the old stateReducer: instead of intercepting downshift's blur handling imperatively,
109
+ // we let Studio's focused state drive whether the menu should be open.
110
+ useEffect(() => {
111
+ if (!fieldFocused) closeMenu();
112
+ }, [fieldFocused, closeMenu]);
113
+
55
114
  const handleUnset = useCallback(() => {
56
115
  pushSelection('');
57
116
  setTerm('', true);
58
117
  selectItem('');
59
- }, []);
118
+ }, [pushSelection, setTerm, selectItem]);
60
119
 
61
120
  useEffect(() => {
62
121
  if (isError) {
@@ -71,11 +130,22 @@ export function IconifyCombobox(props: IconifyComboboxProps) {
71
130
  }
72
131
  }, [error, id, isError, toast]);
73
132
 
133
+ // Destructure onBlur out so downshift's InputBlur doesn't close the menu —
134
+ // menu lifecycle is now driven by fieldFocused via the effect above.
135
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
136
+ const { onBlur: _downshiftOnBlur, ...inputProps } = getInputProps({
137
+ ref: composedInputRef,
138
+ id: studioElementProps?.id,
139
+ 'aria-describedby': studioElementProps?.['aria-describedby'],
140
+ onFocus: handleFocus,
141
+ });
142
+
74
143
  return (
75
144
  <div>
76
145
  {/* @ts-expect-error wrong typings */}
77
146
  <SearchInput
78
- {...getInputProps({ ref: inputRef })}
147
+ {...inputProps}
148
+ onBlur={handleBlur}
79
149
  selectedIcon={selectedIcon}
80
150
  suffix={selectedIcon ? <UnsetButton onUnset={handleUnset} /> : null}
81
151
  />
@@ -109,4 +179,4 @@ export function IconifyCombobox(props: IconifyComboboxProps) {
109
179
  />
110
180
  </div>
111
181
  );
112
- }
182
+ });