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.cjs +115 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -7
- package/dist/index.d.ts +43 -7
- package/dist/index.js +115 -51
- package/dist/index.js.map +1 -1
- package/package.json +37 -27
- package/src/combobox/iconify-combobox.tsx +76 -6
- package/src/combobox/search-input.tsx +16 -14
- package/src/combobox/search-result.tsx +37 -30
- package/src/combobox/unset-button.tsx +8 -1
- package/src/iconify-input.tsx +9 -5
- package/src/iconify-preview.tsx +3 -2
- package/src/lib/icon-types.gen.ts +1 -1
- package/src/lib/types.test.ts +57 -0
- package/src/lib/types.ts +44 -8
|
@@ -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 =
|
|
11
|
-
|
|
10
|
+
export const SearchInput = memo(
|
|
11
|
+
forwardRef<HTMLInputElement, SearchInputProps>((props, ref) => {
|
|
12
|
+
const { selectedIcon, suffix, ...rest } = props;
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 =
|
|
19
|
-
|
|
18
|
+
export const SearchResults = memo(
|
|
19
|
+
forwardRef<HTMLUListElement, SearchResultsProps>((props, ref) => {
|
|
20
|
+
const { state, data, getItemProps, highlightedIndex, ...rest } = props;
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
}
|
package/src/iconify-input.tsx
CHANGED
|
@@ -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={
|
|
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;
|
package/src/iconify-preview.tsx
CHANGED
|
@@ -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' | '
|
|
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 {
|
|
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
|
|
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
|
}
|