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.
@@ -1,25 +1,27 @@
1
1
  import { Icon } from '@iconify/react';
2
2
  import { TextInput } from '@sanity/ui';
3
- import { forwardRef } from 'react';
3
+ import { forwardRef, memo } from 'react';
4
4
 
5
5
  interface SearchInputProps extends React.HTMLAttributes<HTMLInputElement> {
6
6
  selectedIcon: string | null;
7
7
  suffix?: React.ReactNode;
8
8
  }
9
9
 
10
- export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>((props, ref) => {
11
- const { selectedIcon, suffix, ...rest } = props;
10
+ export const SearchInput = memo(
11
+ forwardRef<HTMLInputElement, SearchInputProps>((props, ref) => {
12
+ const { selectedIcon, suffix, ...rest } = props;
12
13
 
13
- return (
14
- <TextInput
15
- {...rest}
16
- ref={ref}
17
- inputMode="search"
18
- icon={selectedIcon ? <Icon icon={selectedIcon} /> : null}
19
- placeholder={selectedIcon ? 'Search and replace selected icon...' : 'Search for an icon...'}
20
- suffix={suffix}
21
- />
22
- );
23
- });
14
+ return (
15
+ <TextInput
16
+ {...rest}
17
+ ref={ref}
18
+ inputMode="search"
19
+ icon={selectedIcon ? <Icon icon={selectedIcon} /> : null}
20
+ placeholder={selectedIcon ? 'Search and replace selected icon...' : 'Search for an icon...'}
21
+ suffix={suffix}
22
+ />
23
+ );
24
+ }),
25
+ );
24
26
 
25
27
  SearchInput.displayName = 'SearchInput';
@@ -1,7 +1,7 @@
1
1
  import { Icon } from '@iconify/react';
2
2
  import { Button } from '@sanity/ui';
3
3
  import type { UseComboboxGetItemPropsOptions, UseComboboxGetItemPropsReturnValue } from 'downshift';
4
- import { forwardRef } from 'react';
4
+ import { forwardRef, memo } from 'react';
5
5
  import { MessageWrapper } from './iconify-combobox.styles';
6
6
 
7
7
  type GetItemProps = (
@@ -15,34 +15,41 @@ export interface SearchResultsProps extends React.HTMLAttributes<HTMLUListElemen
15
15
  highlightedIndex: number;
16
16
  }
17
17
 
18
- export const SearchResults = forwardRef<HTMLUListElement, SearchResultsProps>((props, ref) => {
19
- const { state, data, getItemProps, highlightedIndex, ...rest } = props;
18
+ export const SearchResults = memo(
19
+ forwardRef<HTMLUListElement, SearchResultsProps>((props, ref) => {
20
+ const { state, data, getItemProps, highlightedIndex, ...rest } = props;
20
21
 
21
- return (
22
- <>
23
- {(() => {
24
- switch (state) {
25
- case 'initial':
26
- return <MessageWrapper>Search for icons</MessageWrapper>;
27
- case 'loading':
28
- return <MessageWrapper>Searching...</MessageWrapper>;
29
- case 'error':
30
- return <MessageWrapper>Something went wrong...</MessageWrapper>;
31
- case 'empty':
32
- return <MessageWrapper>No icons found</MessageWrapper>;
33
- }
34
- })()}
22
+ return (
23
+ <>
24
+ {(() => {
25
+ switch (state) {
26
+ case 'initial':
27
+ return <MessageWrapper>Search for icons</MessageWrapper>;
28
+ case 'loading':
29
+ return <MessageWrapper>Searching...</MessageWrapper>;
30
+ case 'error':
31
+ return <MessageWrapper>Something went wrong...</MessageWrapper>;
32
+ case 'empty':
33
+ return <MessageWrapper>No icons found</MessageWrapper>;
34
+ }
35
+ })()}
35
36
 
36
- <ul {...rest} ref={ref} style={{ opacity: state === 'stale' ? 0.5 : 1 }}>
37
- {(state === 'data' || state === 'stale') &&
38
- data?.map((icon, index) => (
39
- <li key={icon} {...getItemProps({ item: icon, index })}>
40
- <Button padding={3} mode="bleed" selected={index === highlightedIndex}>
41
- <Icon icon={icon} width="100%" height="100%" />
42
- </Button>
43
- </li>
44
- ))}
45
- </ul>
46
- </>
47
- );
48
- });
37
+ <ul
38
+ {...rest}
39
+ ref={ref}
40
+ data-testid="iconify-results"
41
+ style={{ opacity: state === 'stale' ? 0.5 : 1 }}
42
+ >
43
+ {(state === 'data' || state === 'stale') &&
44
+ data?.map((icon, index) => (
45
+ <li key={icon} {...getItemProps({ item: icon, index })}>
46
+ <Button padding={3} mode="bleed" selected={index === highlightedIndex}>
47
+ <Icon icon={icon} width="100%" height="100%" />
48
+ </Button>
49
+ </li>
50
+ ))}
51
+ </ul>
52
+ </>
53
+ );
54
+ }),
55
+ );
@@ -14,7 +14,14 @@ export function UnsetButton(props: UnsetButtonProps) {
14
14
 
15
15
  return (
16
16
  <Card border borderLeft={false} padding={1} display="flex" radius={2}>
17
- <Button icon={<TrashIcon />} onClick={onUnset} mode="bleed" fontSize={1} padding={2} />
17
+ <Button
18
+ data-testid="iconify-unset"
19
+ icon={<TrashIcon />}
20
+ onClick={onUnset}
21
+ mode="bleed"
22
+ fontSize={1}
23
+ padding={2}
24
+ />
18
25
  </Card>
19
26
  );
20
27
  }
@@ -1,6 +1,6 @@
1
1
  import { Flex, Stack, Text, ThemeProvider } from '@sanity/ui';
2
2
  import { buildTheme } from '@sanity/ui/theme';
3
- import { useCallback } from 'react';
3
+ import { memo, useCallback } from 'react';
4
4
  import type { ObjectInputProps } from 'sanity';
5
5
  import { set, unset } from 'sanity';
6
6
  import { IconifyCombobox } from './combobox';
@@ -8,12 +8,14 @@ import { QueryClientProvider } from './lib/query-client';
8
8
  import type { IconifyPluginConfig, IconOptions } from './lib/types';
9
9
  import { usePrettyIconName } from './lib/use-pretty-icon-name';
10
10
 
11
+ const theme = buildTheme();
12
+
11
13
  interface IconifyInputProps extends ObjectInputProps {
12
14
  config: IconifyPluginConfig;
13
15
  }
14
16
 
15
- export function IconifyInput(props: IconifyInputProps) {
16
- const { config, value, onChange: pushChange, schemaType } = props;
17
+ export const IconifyInput = memo(function IconifyInput(props: IconifyInputProps) {
18
+ const { config, value, onChange: pushChange, schemaType, elementProps, focused } = props;
17
19
 
18
20
  const selectedIcon: string | null = value?.name ?? null;
19
21
 
@@ -33,12 +35,14 @@ export function IconifyInput(props: IconifyInputProps) {
33
35
 
34
36
  return (
35
37
  <QueryClientProvider>
36
- <ThemeProvider theme={buildTheme()}>
38
+ <ThemeProvider theme={theme}>
37
39
  <Stack space={2}>
38
40
  <IconifyCombobox
39
41
  selectedIcon={selectedIcon}
40
42
  onSelect={handleSelect}
41
43
  collections={collections}
44
+ studioElementProps={elementProps}
45
+ fieldFocused={focused}
42
46
  />
43
47
 
44
48
  {showName && selectedIcon ? <IconifyNameDisplay name={selectedIcon} /> : null}
@@ -46,7 +50,7 @@ export function IconifyInput(props: IconifyInputProps) {
46
50
  </ThemeProvider>
47
51
  </QueryClientProvider>
48
52
  );
49
- }
53
+ });
50
54
 
51
55
  interface IconifyNameDisplayProps {
52
56
  name?: string | null;
@@ -1,6 +1,7 @@
1
1
  import { Icon } from '@iconify/react';
2
2
  import type { IconifyIconName } from '@iconify/utils';
3
3
  import { stringToIcon } from '@iconify/utils';
4
+ import { memo } from 'react';
4
5
  import type { PreviewProps } from 'sanity';
5
6
  import { QueryClientProvider } from './lib/query-client';
6
7
  import { usePrettyIconName } from './lib/use-pretty-icon-name';
@@ -9,7 +10,7 @@ import { usePrettyIconName } from './lib/use-pretty-icon-name';
9
10
  // ICONIFY PREVIEW //
10
11
  // --------------- //
11
12
 
12
- export function IconifyPreview(props: PreviewProps) {
13
+ export const IconifyPreview = memo(function IconifyPreview(props: PreviewProps) {
13
14
  const { title } = props;
14
15
  const iconName = typeof props.title === 'string' ? stringToIcon(props.title) : null;
15
16
 
@@ -23,7 +24,7 @@ export function IconifyPreview(props: PreviewProps) {
23
24
  }
24
25
 
25
26
  return props.renderDefault(props);
26
- }
27
+ });
27
28
 
28
29
  // --------------------- //
29
30
  // ICONIFY PREVIEW INNER //
@@ -1 +1 @@
1
- export type IconPrefix = 'material-symbols' | 'material-symbols-light' | 'ic' | 'mdi' | 'mdi-light' | 'line-md' | 'solar' | 'tabler' | 'mingcute' | 'ri' | 'mynaui' | 'iconamoon' | 'iconoir' | 'lucide' | 'lucide-lab' | 'uil' | 'tdesign' | 'si' | 'bx' | 'bxs' | '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' | 'icon-park-outline' | 'icon-park-solid' | 'icon-park-twotone' | 'jam' | 'streamline-cyber' | 'guidance' | 'carbon' | 'ion' | 'famicons' | 'ant-design' | 'lsicon' | 'gravity-ui' | 'cil' | 'ep' | 'charm' | 'quill' | 'bytesize' | 'bi' | 'streamline-pixel' | 'streamline-block' | 'rivet-icons' | 'nimbus' | 'formkit' | 'fluent' | 'ph' | 'teenyicons' | 'clarity' | 'streamline-freehand' | 'ix' | 'octicon' | 'memory' | 'system-uicons' | 'radix-icons' | 'zondicons' | 'uiw' | 'maki' | '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' | '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' | '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' | 'mingcute' | 'ri' | 'mynaui' | 'iconamoon' | 'iconoir' | 'lucide' | 'lucide-lab' | 'uil' | 'tdesign' | 'si' | 'bx' | 'bxs' | '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' | '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' | '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' | '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';
@@ -0,0 +1,57 @@
1
+ import { defineField } from 'sanity';
2
+ import { describe, expectTypeOf, it } from 'vitest';
3
+ import type { IconValue } from './types';
4
+
5
+ // These are type-level tests: they fail at compile time if types are wrong,
6
+ // not at runtime. Credit: danilo-arioli (PR #13).
7
+
8
+ describe('IconDefinition type safety', () => {
9
+ it('types value as IconValue in validation callbacks', () => {
10
+ defineField({
11
+ name: 'icon',
12
+ type: 'icon',
13
+ validation: (Rule) =>
14
+ Rule.custom((value) => {
15
+ expectTypeOf(value).toExtend<IconValue>();
16
+ expectTypeOf(value?.name).toExtend<string | undefined>();
17
+ return value?.name ? true : 'Icon is required';
18
+ }),
19
+ });
20
+ });
21
+
22
+ it('types value as IconValue in hidden callbacks', () => {
23
+ defineField({
24
+ name: 'icon',
25
+ type: 'icon',
26
+ hidden: ({ value, document }) => {
27
+ expectTypeOf(value).toExtend<IconValue>();
28
+ expectTypeOf(value?.name).toExtend<string | undefined>();
29
+ return value?.name === 'hidden-icon' || document?._type === 'simple';
30
+ },
31
+ });
32
+ });
33
+
34
+ it('types value as IconValue in readOnly callbacks', () => {
35
+ defineField({
36
+ name: 'icon',
37
+ type: 'icon',
38
+ readOnly: ({ value, document }) => {
39
+ expectTypeOf(value).toExtend<IconValue>();
40
+ expectTypeOf(value?.name).toExtend<string | undefined>();
41
+ return document?.published === true && (value?.name?.startsWith('system:') ?? false);
42
+ },
43
+ });
44
+ });
45
+
46
+ it('accepts collapsible and collapsed options', () => {
47
+ defineField({
48
+ name: 'icon',
49
+ type: 'icon',
50
+ options: {
51
+ collapsible: true,
52
+ collapsed: false,
53
+ collections: ['mdi'],
54
+ },
55
+ });
56
+ });
57
+ });
package/src/lib/types.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  import type { IconifyInfo } from '@iconify/types';
2
- import type { BaseSchemaDefinition } from 'sanity';
2
+ import type {
3
+ BaseSchemaDefinition,
4
+ ConditionalPropertyCallbackContext,
5
+ FieldGroupDefinition,
6
+ FieldsetDefinition,
7
+ InitialValueProperty,
8
+ ObjectOptions,
9
+ RuleDef,
10
+ ValidationBuilder,
11
+ } from 'sanity';
3
12
 
4
13
  // If this line errors, the type definitions have not been generated.
5
14
  import type { IconPrefix } from './icon-types.gen';
@@ -8,10 +17,10 @@ import type { IconPrefix } from './icon-types.gen';
8
17
  export type { BaseSchemaDefinition } from 'sanity';
9
18
  export type { IconPrefix };
10
19
 
11
- export interface IconOptions {
20
+ export type IconOptions = {
12
21
  collections?: IconPrefix[];
13
22
  showName?: boolean;
14
- }
23
+ } & Pick<ObjectOptions, 'collapsed' | 'collapsible'>;
15
24
 
16
25
  export interface IconifyPluginConfig {
17
26
  collections?: IconPrefix[];
@@ -23,13 +32,40 @@ export interface IconifySearchResult {
23
32
  collections: Record<string, IconifyInfo>;
24
33
  }
25
34
 
35
+ export type IconValue =
36
+ | {
37
+ name?: string;
38
+ }
39
+ | undefined;
40
+
41
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
42
+ export interface IconRule extends RuleDef<IconRule, IconValue> {}
43
+
44
+ export type IconConditionalPropertyCallbackContext = Omit<
45
+ ConditionalPropertyCallbackContext,
46
+ 'value'
47
+ > & {
48
+ value: IconValue;
49
+ };
50
+
51
+ export type IconConditionalProperty =
52
+ | boolean
53
+ | ((context: IconConditionalPropertyCallbackContext) => boolean)
54
+ | undefined;
55
+
56
+ export interface IconDefinition extends Omit<BaseSchemaDefinition, 'hidden' | 'readOnly'> {
57
+ type: 'icon';
58
+ groups?: FieldGroupDefinition[];
59
+ fieldsets?: FieldsetDefinition[];
60
+ options?: IconOptions;
61
+ hidden?: IconConditionalProperty;
62
+ readOnly?: IconConditionalProperty;
63
+ validation?: ValidationBuilder<IconRule, IconValue>;
64
+ initialValue?: InitialValueProperty<any, IconValue>;
65
+ }
66
+
26
67
  // Extend the Sanity schema types to include our custom type
27
68
  declare module 'sanity' {
28
- interface IconDefinition extends BaseSchemaDefinition {
29
- type: 'icon';
30
- options?: IconOptions;
31
- }
32
-
33
69
  export interface IntrinsicDefinitions {
34
70
  icon: IconDefinition;
35
71
  }