sanity-plugin-iconify 1.1.0 → 1.1.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.
@@ -0,0 +1,209 @@
1
+ import { BaseSchemaDefinition } from 'sanity';
2
+ import { IconifyInfo } from '@iconify/types';
3
+ import { Plugin as Plugin_2 } from 'sanity';
4
+
5
+ export { BaseSchemaDefinition };
6
+
7
+ /**
8
+ * Usage in `sanity.config.ts` (or .js)
9
+ *
10
+ * ```ts
11
+ * import { defineConfig } from 'sanity'
12
+ * import { iconify } from 'sanity-plugin-iconify'
13
+ *
14
+ * export default defineConfig({
15
+ * // ...
16
+ * plugins: [iconify()],
17
+ * })
18
+ * ```
19
+ */
20
+ export declare const iconify: Plugin_2<void | IconifyPluginConfig>;
21
+
22
+ export declare interface IconifyPluginConfig {
23
+ collections?: IconPrefix[];
24
+ showName?: boolean;
25
+ }
26
+
27
+ export declare interface IconifySearchResult {
28
+ icons: string[];
29
+ collections: Record<string, IconifyInfo>;
30
+ }
31
+
32
+ export declare interface IconOptions {
33
+ collections?: IconPrefix[];
34
+ showName?: boolean;
35
+ }
36
+
37
+ export declare type IconPrefix =
38
+ | 'material-symbols'
39
+ | 'material-symbols-light'
40
+ | 'ic'
41
+ | 'mdi'
42
+ | 'ph'
43
+ | 'solar'
44
+ | 'tabler'
45
+ | 'mingcute'
46
+ | 'ri'
47
+ | 'bi'
48
+ | 'carbon'
49
+ | 'iconamoon'
50
+ | 'iconoir'
51
+ | 'ion'
52
+ | 'lucide'
53
+ | 'uil'
54
+ | 'tdesign'
55
+ | 'teenyicons'
56
+ | 'clarity'
57
+ | 'bx'
58
+ | 'bxs'
59
+ | 'majesticons'
60
+ | 'ant-design'
61
+ | 'gg'
62
+ | 'octicon'
63
+ | 'memory'
64
+ | 'cil'
65
+ | 'mynaui'
66
+ | 'basil'
67
+ | 'pixelarticons'
68
+ | 'akar-icons'
69
+ | 'ci'
70
+ | 'system-uicons'
71
+ | 'typcn'
72
+ | 'radix-icons'
73
+ | 'zondicons'
74
+ | 'ep'
75
+ | 'circum'
76
+ | 'mdi-light'
77
+ | 'fe'
78
+ | 'eos-icons'
79
+ | 'charm'
80
+ | 'prime'
81
+ | 'humbleicons'
82
+ | 'uiw'
83
+ | 'uim'
84
+ | 'uit'
85
+ | 'uis'
86
+ | 'maki'
87
+ | 'gridicons'
88
+ | 'mi'
89
+ | 'quill'
90
+ | 'gala'
91
+ | 'fluent'
92
+ | 'icon-park-outline'
93
+ | 'icon-park-solid'
94
+ | 'icon-park-twotone'
95
+ | 'icon-park'
96
+ | 'vscode-icons'
97
+ | 'jam'
98
+ | 'heroicons'
99
+ | 'codicon'
100
+ | 'pajamas'
101
+ | 'pepicons-pop'
102
+ | 'pepicons-print'
103
+ | 'pepicons-pencil'
104
+ | 'bytesize'
105
+ | 'ei'
106
+ | 'streamline'
107
+ | 'guidance'
108
+ | 'fa6-solid'
109
+ | 'fa6-regular'
110
+ | 'ooui'
111
+ | 'nimbus'
112
+ | 'formkit'
113
+ | 'line-md'
114
+ | 'meteocons'
115
+ | 'svg-spinners'
116
+ | 'openmoji'
117
+ | 'twemoji'
118
+ | 'noto'
119
+ | 'fluent-emoji'
120
+ | 'fluent-emoji-flat'
121
+ | 'fluent-emoji-high-contrast'
122
+ | 'noto-v1'
123
+ | 'emojione'
124
+ | 'emojione-monotone'
125
+ | 'emojione-v1'
126
+ | 'fxemoji'
127
+ | 'streamline-emojis'
128
+ | 'bxl'
129
+ | 'logos'
130
+ | 'simple-icons'
131
+ | 'cib'
132
+ | 'fa6-brands'
133
+ | 'nonicons'
134
+ | 'arcticons'
135
+ | 'file-icons'
136
+ | 'devicon'
137
+ | 'devicon-plain'
138
+ | 'skill-icons'
139
+ | 'brandico'
140
+ | 'entypo-social'
141
+ | 'cryptocurrency'
142
+ | 'cryptocurrency-color'
143
+ | 'flag'
144
+ | 'circle-flags'
145
+ | 'flagpack'
146
+ | 'cif'
147
+ | 'gis'
148
+ | 'map'
149
+ | 'geo'
150
+ | 'game-icons'
151
+ | 'fad'
152
+ | 'academicons'
153
+ | 'wi'
154
+ | 'healthicons'
155
+ | 'medical-icon'
156
+ | 'covid'
157
+ | 'la'
158
+ | 'eva'
159
+ | 'dashicons'
160
+ | 'flat-color-icons'
161
+ | 'entypo'
162
+ | 'foundation'
163
+ | 'raphael'
164
+ | 'icons8'
165
+ | 'iwwa'
166
+ | 'heroicons-outline'
167
+ | 'heroicons-solid'
168
+ | 'fa-solid'
169
+ | 'fa-regular'
170
+ | 'fa-brands'
171
+ | 'fa'
172
+ | 'fluent-mdl2'
173
+ | 'fontisto'
174
+ | 'icomoon-free'
175
+ | 'subway'
176
+ | 'oi'
177
+ | 'wpf'
178
+ | 'simple-line-icons'
179
+ | 'et'
180
+ | 'el'
181
+ | 'vaadin'
182
+ | 'grommet-icons'
183
+ | 'whh'
184
+ | 'si-glyph'
185
+ | 'zmdi'
186
+ | 'ls'
187
+ | 'bpmn'
188
+ | 'flat-ui'
189
+ | 'vs'
190
+ | 'topcoat'
191
+ | 'il'
192
+ | 'websymbol'
193
+ | 'fontelico'
194
+ | 'ps'
195
+ | 'feather'
196
+ | 'mono-icons'
197
+ | 'pepicons';
198
+
199
+ export {};
200
+
201
+ declare module 'sanity' {
202
+ interface IconDefinition extends BaseSchemaDefinition {
203
+ type: 'icon';
204
+ options?: IconOptions;
205
+ }
206
+ interface IntrinsicDefinitions {
207
+ icon: IconDefinition;
208
+ }
209
+ }
@@ -0,0 +1,459 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { unset, set, definePlugin } from 'sanity';
3
+ import { Grid, Box, Card, Text, TextInput, Button, useToast, Popover, useTheme, studioTheme, ThemeProvider, Stack, Flex } from '@sanity/ui';
4
+ import { useState, useCallback, forwardRef, useId, useRef, useEffect, useMemo } from 'react';
5
+ import { Combobox } from '@headlessui/react';
6
+ import { match } from 'ts-pattern';
7
+ import { useQueryClient, useQuery, keepPreviousData, QueryClient, QueryClientProvider as QueryClientProvider$1 } from '@tanstack/react-query';
8
+ import { useDebounce } from 'use-debounce';
9
+ import styled from 'styled-components';
10
+ import { Icon } from '@iconify/react';
11
+ import { TrashIcon } from '@sanity/icons';
12
+ import { stringToIcon } from '@iconify/utils';
13
+ import { sentenceCase } from 'change-case';
14
+ const BASE_API_URL = "https://api.iconify.design";
15
+ function fetchJson(_ref) {
16
+ let {
17
+ url,
18
+ signal
19
+ } = _ref;
20
+ return fetch(url, {
21
+ signal
22
+ }).then(response => {
23
+ if (!response.ok) {
24
+ throw new Error("Network error: status ".concat(response.status));
25
+ }
26
+ return response.json();
27
+ }).then(result => result, error => {
28
+ if (error instanceof Error) {
29
+ throw error;
30
+ } else {
31
+ console.error("Unknown error: ".concat(error));
32
+ throw new Error("Something went wrong");
33
+ }
34
+ });
35
+ }
36
+ function useSearch(_ref2) {
37
+ let {
38
+ collections
39
+ } = _ref2;
40
+ const queryClient = useQueryClient();
41
+ const [term, setTerm] = useState("");
42
+ const [debouncedTerm, setDebouncedTerm] = useDebounce(term, 500);
43
+ const updateTerm = useCallback(function (newTerm) {
44
+ let updateImmediately = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
45
+ setTerm(newTerm);
46
+ if (updateImmediately) {
47
+ setDebouncedTerm(newTerm);
48
+ }
49
+ }, [setDebouncedTerm]);
50
+ const {
51
+ isLoading,
52
+ isError,
53
+ error,
54
+ data,
55
+ isPlaceholderData
56
+ } = useQuery({
57
+ queryKey: ["search", collections, debouncedTerm],
58
+ queryFn: async _ref3 => {
59
+ let {
60
+ signal
61
+ } = _ref3;
62
+ var _a;
63
+ const url = new URL("/search", BASE_API_URL);
64
+ url.searchParams.append("query", debouncedTerm);
65
+ url.searchParams.append("limit", "60");
66
+ if (collections) {
67
+ url.searchParams.append("prefixes", collections.join(","));
68
+ }
69
+ const result = debouncedTerm ? await fetchJson({
70
+ url,
71
+ signal
72
+ }) : null;
73
+ if (result) {
74
+ Object.entries(result.collections).forEach(_ref4 => {
75
+ let [prefix, info] = _ref4;
76
+ queryClient.setQueryData(["iconSetInfo", prefix], info);
77
+ });
78
+ }
79
+ return (_a = result == null ? void 0 : result.icons) != null ? _a : [];
80
+ },
81
+ enabled: debouncedTerm.length > 0,
82
+ placeholderData: keepPreviousData,
83
+ staleTime: 5 * 60 * 1e3
84
+ // 5 minutes
85
+ });
86
+
87
+ return {
88
+ term,
89
+ setTerm: updateTerm,
90
+ debouncedTerm,
91
+ isLoading,
92
+ isError,
93
+ error,
94
+ data,
95
+ isPreviousData: isPlaceholderData
96
+ };
97
+ }
98
+ function useIconSetInfo(_ref5) {
99
+ let {
100
+ prefix
101
+ } = _ref5;
102
+ return useQuery({
103
+ queryKey: ["iconSetInfo", prefix],
104
+ queryFn: async _ref6 => {
105
+ let {
106
+ signal
107
+ } = _ref6;
108
+ var _a;
109
+ if (!prefix) return null;
110
+ const url = new URL("/collection", BASE_API_URL);
111
+ url.searchParams.append("prefix", prefix);
112
+ url.searchParams.append("info", "true");
113
+ const result = await fetchJson({
114
+ url,
115
+ signal
116
+ });
117
+ return (_a = result == null ? void 0 : result.info) != null ? _a : null;
118
+ },
119
+ staleTime: Infinity
120
+ });
121
+ }
122
+ var __freeze = Object.freeze;
123
+ var __defProp = Object.defineProperty;
124
+ var __template = (cooked, raw) => __freeze(__defProp(cooked, "raw", {
125
+ value: __freeze(raw || cooked.slice())
126
+ }));
127
+ var _a, _b;
128
+ const ComboboxWrapper = styled(Grid)(_a || (_a = __template(["\n grid-template-columns: 1fr min-content;\n position: relative;\n"])));
129
+ const OptionsWrapper = styled(Box)(_b || (_b = __template(["\n box-sizing: border-box;\n padding: 0.5rem;\n\n & [role='listbox'] {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(min(100%, 2.5rem), 1fr));\n gap: 0.5rem;\n list-style: none;\n margin: 0;\n padding: 0;\n\n @media (min-width: 650px) {\n grid-template-columns: repeat(10, minmax(min(100%, 2.5rem), 1fr));\n }\n }\n\n & [role='option'] {\n display: grid;\n place-items: center;\n\n & button {\n aspect-ratio: 1;\n cursor: pointer;\n width: 100%;\n\n & > [data-ui='Box'] {\n display: flex;\n }\n }\n }\n"])));
130
+ function MessageWrapper(_ref7) {
131
+ let {
132
+ children
133
+ } = _ref7;
134
+ return /* @__PURE__ */jsx(Card, {
135
+ padding: 4,
136
+ children: /* @__PURE__ */jsx(Text, {
137
+ align: "center",
138
+ muted: true,
139
+ children
140
+ })
141
+ });
142
+ }
143
+ const SearchInput = forwardRef((props, ref) => {
144
+ const {
145
+ term,
146
+ selectedIcon,
147
+ onChange
148
+ } = props;
149
+ return /* @__PURE__ */jsx(Combobox.Input, {
150
+ as: TextInput,
151
+ ref,
152
+ inputMode: "search",
153
+ value: term,
154
+ onChange,
155
+ icon: selectedIcon ? /* @__PURE__ */jsx(Icon, {
156
+ icon: selectedIcon
157
+ }) : null,
158
+ placeholder: selectedIcon ? "Search and replace selected icon..." : "Search for an icon..."
159
+ });
160
+ });
161
+ SearchInput.displayName = "SearchInput";
162
+ function SearchResults(props) {
163
+ const {
164
+ state,
165
+ data
166
+ } = props;
167
+ if (state === "initial") {
168
+ return /* @__PURE__ */jsx(MessageWrapper, {
169
+ children: "Search for icons"
170
+ });
171
+ }
172
+ if (state === "loading") {
173
+ return /* @__PURE__ */jsx(MessageWrapper, {
174
+ children: "Searching..."
175
+ });
176
+ }
177
+ if (state === "error") {
178
+ return /* @__PURE__ */jsx(MessageWrapper, {
179
+ children: "Something went wrong..."
180
+ });
181
+ }
182
+ if (state === "empty") {
183
+ return /* @__PURE__ */jsx(MessageWrapper, {
184
+ children: "No icons found"
185
+ });
186
+ }
187
+ return /* @__PURE__ */jsx(Combobox.Options, {
188
+ style: {
189
+ opacity: state === "stale" ? 0.5 : 1
190
+ },
191
+ children: data.map(icon => /* @__PURE__ */jsx(Combobox.Option, {
192
+ value: icon,
193
+ children: _ref8 => {
194
+ let {
195
+ active
196
+ } = _ref8;
197
+ return /* @__PURE__ */jsx(Button, {
198
+ padding: 3,
199
+ mode: "bleed",
200
+ selected: active,
201
+ children: /* @__PURE__ */jsx(Icon, {
202
+ icon,
203
+ width: "100%",
204
+ height: "100%"
205
+ })
206
+ });
207
+ }
208
+ }, icon))
209
+ });
210
+ }
211
+ function UnsetButton(props) {
212
+ const {
213
+ onUnset
214
+ } = props;
215
+ return /* @__PURE__ */jsx(Card, {
216
+ border: true,
217
+ borderLeft: false,
218
+ padding: 1,
219
+ display: "flex",
220
+ children: /* @__PURE__ */jsx(Button, {
221
+ icon: /* @__PURE__ */jsx(TrashIcon, {}),
222
+ onClick: onUnset,
223
+ mode: "bleed",
224
+ fontSize: 1,
225
+ padding: 2
226
+ })
227
+ });
228
+ }
229
+ function IconifyCombobox(props) {
230
+ const {
231
+ selectedIcon,
232
+ onSelect: pushSelection,
233
+ collections
234
+ } = props;
235
+ const id = useId();
236
+ const toast = useToast();
237
+ const inputRef = useRef(null);
238
+ const {
239
+ term,
240
+ setTerm,
241
+ debouncedTerm,
242
+ isLoading,
243
+ isError,
244
+ error,
245
+ data,
246
+ isPreviousData
247
+ } = useSearch({
248
+ collections
249
+ });
250
+ const handleTermChange = useCallback(event => setTerm(event.target.value), [setTerm]);
251
+ const handleSelect = useCallback(icon => {
252
+ pushSelection(icon);
253
+ setTerm("", true);
254
+ }, [pushSelection, setTerm]);
255
+ const handleUnset = useCallback(() => handleSelect(""), [handleSelect]);
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 == null ? void 0 : error.message
264
+ });
265
+ }
266
+ }, [error, id, isError, toast]);
267
+ return /* @__PURE__ */jsxs(ComboboxWrapper, {
268
+ children: [/* @__PURE__ */jsx(Combobox, {
269
+ onChange: handleSelect,
270
+ children: _ref9 => {
271
+ let {
272
+ open
273
+ } = _ref9;
274
+ return /* @__PURE__ */jsxs(Fragment, {
275
+ children: [/* @__PURE__ */jsx(SearchInput, {
276
+ ref: inputRef,
277
+ term,
278
+ selectedIcon,
279
+ onChange: handleTermChange
280
+ }), /* @__PURE__ */jsx(Popover, {
281
+ open,
282
+ placement: "bottom",
283
+ arrow: false,
284
+ matchReferenceWidth: true,
285
+ portal: true,
286
+ constrainSize: true,
287
+ referenceElement: inputRef.current,
288
+ content: /* @__PURE__ */jsx(OptionsWrapper, {
289
+ children: /* @__PURE__ */jsx(SearchResults, {
290
+ state: match(true).returnType().with(isLoading, () => "loading").with(!debouncedTerm, () => "initial").with(isError, () => "error").with(!data || data.length === 0, () => "empty").with(isPreviousData, () => "stale").otherwise(() => "data"),
291
+ data
292
+ })
293
+ })
294
+ })]
295
+ });
296
+ }
297
+ }), selectedIcon ? /* @__PURE__ */jsx(UnsetButton, {
298
+ onUnset: handleUnset
299
+ }) : null]
300
+ });
301
+ }
302
+ const queryClient = new QueryClient();
303
+ function QueryClientProvider(props) {
304
+ return /* @__PURE__ */jsx(QueryClientProvider$1, {
305
+ client: queryClient,
306
+ children: props.children
307
+ });
308
+ }
309
+ function usePrettyIconName(props) {
310
+ var _a, _b;
311
+ const {
312
+ name
313
+ } = props;
314
+ const iconMeta = useMemo(() => {
315
+ var _a2;
316
+ return (_a2 = props.iconMeta) != null ? _a2 : name ? stringToIcon(name) : null;
317
+ }, [name, props.iconMeta]);
318
+ const iconSetInfo = useIconSetInfo({
319
+ prefix: (_a = iconMeta == null ? void 0 : iconMeta.prefix) != null ? _a : null
320
+ });
321
+ return useMemo(() => {
322
+ var _a2, _b2;
323
+ return iconMeta ? {
324
+ name: sentenceCase(iconMeta.name),
325
+ collection: (_b2 = (_a2 = iconSetInfo.data) == null ? void 0 : _a2.name) != null ? _b2 : iconMeta.prefix
326
+ } : null;
327
+ }, [iconMeta, (_b = iconSetInfo.data) == null ? void 0 : _b.name]);
328
+ }
329
+ function IconifyInput(props) {
330
+ var _a, _b, _c, _d, _e;
331
+ const {
332
+ config,
333
+ value,
334
+ onChange: pushChange,
335
+ schemaType
336
+ } = props;
337
+ const selectedIcon = (_a = value == null ? void 0 : value.name) != null ? _a : null;
338
+ const options = schemaType.options;
339
+ const collections = !!((_b = options == null ? void 0 : options.collections) == null ? void 0 : _b.length) && options.collections || !!((_c = config == null ? void 0 : config.collections) == null ? void 0 : _c.length) && config.collections || null;
340
+ const showName = (_e = (_d = options == null ? void 0 : options.showName) != null ? _d : config == null ? void 0 : config.showName) != null ? _e : false;
341
+ const parentTheme = useTheme();
342
+ const theme = useMemo(() => parentTheme ? {
343
+ ...parentTheme.sanity,
344
+ color: studioTheme.color
345
+ } : studioTheme, [parentTheme]);
346
+ const handleSelect = useCallback(icon => {
347
+ pushChange(icon === "" ? unset() : set(icon, ["name"]));
348
+ }, [pushChange]);
349
+ return /* @__PURE__ */jsx(QueryClientProvider, {
350
+ children: /* @__PURE__ */jsx(ThemeProvider, {
351
+ theme,
352
+ children: /* @__PURE__ */jsxs(Stack, {
353
+ space: 2,
354
+ children: [/* @__PURE__ */jsx(IconifyCombobox, {
355
+ selectedIcon,
356
+ onSelect: handleSelect,
357
+ collections
358
+ }), showName && selectedIcon ? /* @__PURE__ */jsx(IconifyNameDisplay, {
359
+ name: selectedIcon
360
+ }) : null]
361
+ })
362
+ })
363
+ });
364
+ }
365
+ function IconifyNameDisplay(props) {
366
+ var _a;
367
+ const {
368
+ name
369
+ } = props;
370
+ const prettyName = usePrettyIconName({
371
+ name
372
+ });
373
+ return /* @__PURE__ */jsxs(Flex, {
374
+ gap: 1,
375
+ children: [/* @__PURE__ */jsx(Text, {
376
+ size: 1,
377
+ muted: true,
378
+ children: "Selected:"
379
+ }), /* @__PURE__ */jsx(Text, {
380
+ size: 1,
381
+ weight: "semibold",
382
+ children: (_a = prettyName == null ? void 0 : prettyName.name) != null ? _a : name
383
+ }), (prettyName == null ? void 0 : prettyName.collection) && /* @__PURE__ */jsxs(Text, {
384
+ size: 1,
385
+ muted: true,
386
+ style: {
387
+ fontStyle: "italic"
388
+ },
389
+ children: ["by ", prettyName == null ? void 0 : prettyName.collection]
390
+ })]
391
+ });
392
+ }
393
+ function IconifyPreview(props) {
394
+ const {
395
+ title
396
+ } = props;
397
+ const iconName = typeof props.title === "string" ? stringToIcon(props.title) : null;
398
+ if (typeof title === "string" && iconName) {
399
+ return /* @__PURE__ */jsx(QueryClientProvider, {
400
+ children: /* @__PURE__ */jsx(IconifyPreviewInner, {
401
+ ...props,
402
+ iconName: title,
403
+ iconMeta: iconName
404
+ })
405
+ });
406
+ }
407
+ return props.renderDefault(props);
408
+ }
409
+ function IconifyPreviewInner(props) {
410
+ var _a;
411
+ const {
412
+ iconMeta,
413
+ iconName,
414
+ ...previewProps
415
+ } = props;
416
+ const prettyName = usePrettyIconName({
417
+ iconMeta
418
+ });
419
+ return props.renderDefault({
420
+ ...previewProps,
421
+ media: /* @__PURE__ */jsx(Icon, {
422
+ icon: iconName
423
+ }),
424
+ title: (_a = prettyName == null ? void 0 : prettyName.name) != null ? _a : iconName,
425
+ subtitle: prettyName == null ? void 0 : prettyName.collection
426
+ });
427
+ }
428
+ const iconify = definePlugin(function () {
429
+ let config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
430
+ return {
431
+ name: "sanity-plugin-iconify",
432
+ schema: {
433
+ types: [{
434
+ name: "icon",
435
+ title: "Icon",
436
+ type: "object",
437
+ fields: [{
438
+ name: "name",
439
+ title: "Name",
440
+ type: "string"
441
+ }],
442
+ components: {
443
+ input: props => /* @__PURE__ */jsx(IconifyInput, {
444
+ ...props,
445
+ config
446
+ }),
447
+ preview: IconifyPreview,
448
+ // This makes sure the input component is not indented
449
+ field: props => props.renderDefault({
450
+ ...props,
451
+ level: 0
452
+ })
453
+ }
454
+ }]
455
+ }
456
+ };
457
+ });
458
+ export { iconify };
459
+ //# sourceMappingURL=index.esm.js.map