@driftime/sanity-plugin-icon 0.1.0 → 0.3.0

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/README.md CHANGED
@@ -1,38 +1,42 @@
1
1
  <div align="center">
2
2
  <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-icon/HEAD/.github/assets/icon-dark.svg" />
4
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-icon/HEAD/.github/assets/icon-light.svg" />
5
- <img src="https://raw.githubusercontent.com/driftime/sanity-plugin-icon/HEAD/.github/assets/icon-light.svg" alt="Icon plugin logo" width="48" />
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/icon/assets/icon-dark.svg" />
4
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/icon/assets/icon-light.svg" />
5
+ <img src="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/icon/assets/icon-light.svg" alt="Icon plugin logo" width="48" />
6
6
  </picture>
7
7
  <h1>Icon</h1>
8
8
  <p><strong>A Sanity Studio plugin by Driftime®</strong></p>
9
- <p>Lucide icons for Sanity Studio, stored as drawings and rendered without the library.</p>
9
+ <p>Lucide icons for Sanity Studio, stored as SVG and rendered without the library.</p>
10
+ <p>
11
+ <a href="https://www.npmjs.com/package/@driftime/sanity-plugin-icon"><img src="https://img.shields.io/npm/v/@driftime/sanity-plugin-icon?style=flat-square&labelColor=1a1a1a&color=666666" alt="npm version" /></a>
12
+ <a href="https://github.com/driftime/sanity-plugins/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@driftime/sanity-plugin-icon?style=flat-square&labelColor=1a1a1a&color=666666" alt="License: MIT" /></a>
13
+ </p>
10
14
  </div>
11
15
 
12
16
  <br />
13
17
 
14
18
  ## Overview
15
19
 
16
- Icon adds a field type to Sanity Studio that opens the whole Lucide library as a searchable grid. Authors find an icon by its name or by what it depicts, and the drawing itself is stored on the document alongside the name it was chosen by.
20
+ Icons are part of a site's visual language, chosen with the same care as its typography and its colors, and Icon makes them part of its content too. The whole [Lucide](https://lucide.dev) library sits behind a field, searchable by name or by what an icon depicts, so the icon beside a heading, a feature, or a link is chosen where the heading is written.
17
21
 
18
- Rendering costs one component and **under 2 kB gzipped**, with no icon library in your application. That works because the document holds the drawing rather than a reference to it, so nothing needs resolving by name at runtime.
22
+ When an icon is chosen, its drawing is stored on the document alongside its name. The site renders that drawing with one component and no icon library, so nothing is looked up at runtime, and an icon keeps rendering through every Lucide release, whatever that release renames or removes.
19
23
 
20
24
  <br />
21
25
 
22
26
  <figure>
23
27
  <picture>
24
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-icon/HEAD/.github/assets/icon-selector-dark.png" />
25
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-icon/HEAD/.github/assets/icon-selector-light.png" />
26
- <img src="https://raw.githubusercontent.com/driftime/sanity-plugin-icon/HEAD/.github/assets/icon-selector-light.png" alt="The icon selector open in Sanity Studio, showing a search field above a scrolling grid of the complete Lucide library alongside a count of every icon it offers" />
28
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/icon/assets/icon-selector-dark.png" />
29
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/icon/assets/icon-selector-light.png" />
30
+ <img src="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/icon/assets/icon-selector-light.png" alt="The icon picker open in Sanity Studio, showing a search field above a scrolling grid of every Lucide icon" />
27
31
  </picture>
28
- <p align="center"><sub><em>Every Lucide icon, searchable from inside the Studio.</em></sub></p>
32
+ <p align="center"><sub><em>The icon picker, with the whole library in a searchable grid.</em></sub></p>
29
33
  </figure>
30
34
 
31
35
  <br />
32
36
 
33
37
  ## Installation
34
38
 
35
- Icon is built for Sanity Studio 6 and React 19, and declares both as peer dependencies, so your Studio must already be on those versions. You'll need Node 22.12 or later.
39
+ Icon is built for Sanity Studio 6.10 and React 19 and declares both as peer dependencies, so the Studio needs to be on those versions already. Node 22.12 or later is required.
36
40
 
37
41
  ```bash
38
42
  bun add -E @driftime/sanity-plugin-icon
@@ -40,101 +44,178 @@ bun add -E @driftime/sanity-plugin-icon
40
44
 
41
45
  <br />
42
46
 
43
- ## Basic Setup
47
+ ## Studio
44
48
 
45
- Add the plugin to your Sanity configuration. It takes no options.
49
+ In the Studio, the plugin registers an `icon` type and takes the icons every field offers.
50
+
51
+ <br />
52
+
53
+ ### Registering the Plugin
54
+
55
+ Nothing is required, and a plugin registered on its own offers every icon in the installed Lucide release.
46
56
 
47
57
  ```typescript
48
58
  import { defineConfig } from "sanity";
49
59
  import { iconPlugin } from "@driftime/sanity-plugin-icon";
50
- // ...
51
60
 
52
61
  export default defineConfig({
53
62
  // ...
54
- plugins: [iconPlugin()],
63
+ plugins: [
64
+ // ...
65
+ iconPlugin(),
66
+ ],
55
67
  });
56
68
  ```
57
69
 
58
- That registers an `icon` object type, available alongside Sanity's own. Use it on any document or object.
70
+ | Option | Type | Default | Purpose |
71
+ | ------- | ------------------ | ------- | --------------------------------------------------------------------------------------------------- |
72
+ | `icons` | `SanityIconName[]` | all | Icons every field offers, in the order listed. See [Restricting the Icons](#restricting-the-icons). |
73
+
74
+ <br />
75
+
76
+ ### Adding an Icon Field
77
+
78
+ A field of type `icon` is defined like any other.
59
79
 
60
80
  ```typescript
61
81
  defineField({
62
82
  name: "icon",
63
83
  type: "icon",
64
- description: "Shown beside the heading.",
84
+ description: "Icon shown beside the heading.",
65
85
  });
66
86
  ```
67
87
 
68
- The type is registered as `icon` and the name is not configurable, so a Studio that already defines its own `icon` type will need to rename that one.
88
+ | Option | Type | Default | Purpose |
89
+ | ------- | ------------------ | ----------------- | ----------------------------------------------------------------------------- |
90
+ | `icons` | `SanityIconName[]` | the plugin's list | Icons this field offers. See [Restricting the Icons](#restricting-the-icons). |
91
+
92
+ The type name is always `icon`, so a Studio that already has a type by that name needs to rename it before installing.
69
93
 
70
94
  <br />
71
95
 
72
- ## Picking an Icon
96
+ ### Restricting the Icons
97
+
98
+ A field offers the whole library unless restricted, and most sites want a smaller set. Pass the icons the site uses to the plugin, and every field offers those, in the order they are listed.
99
+
100
+ ```typescript
101
+ iconPlugin({
102
+ icons: ["sun", "wind", "droplets", "leaf", "sprout", "mountain", "tent", "compass"],
103
+ });
104
+ ```
105
+
106
+ A single field sometimes needs a list of its own, for example a contact card whose icon should only ever be a mail, phone, or globe symbol. Pass `icons` in the field's `options`.
107
+
108
+ ```typescript
109
+ defineField({
110
+ name: "icon",
111
+ type: "icon",
112
+ description: "Icon shown beside the contact method.",
113
+ options: { icons: ["mail", "phone", "globe"] },
114
+ });
115
+ ```
116
+
117
+ A field's list **replaces** the plugin's list, so a field can offer any icon regardless of the plugin's, and a name the installed Lucide release does not have is skipped. Restricting a field later is safe, because a stored icon carries its own drawing and still renders when the picker no longer offers it.
118
+
119
+ Names are typed against the Lucide release installed in the project, so they complete as they are typed. A list shared between the plugin and a field, or between several fields, is typed with `SanityIconName`.
120
+
121
+ ```typescript
122
+ import type { SanityIconName } from "@driftime/sanity-plugin-icon";
73
123
 
74
- - Search matches an icon's name and the terms it is tagged with, so "next" finds `arrow-right`.
75
- - The complete Lucide set is offered, and the count reflects whatever the current release holds.
76
- - The last eight icons chosen are remembered, per browser rather than per dataset.
124
+ export const contactIcons: SanityIconName[] = ["mail", "phone", "globe"];
125
+ ```
77
126
 
78
127
  <br />
79
128
 
80
129
  <figure>
81
130
  <picture>
82
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-icon/HEAD/.github/assets/icon-search-dark.png" />
83
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-icon/HEAD/.github/assets/icon-search-light.png" />
84
- <img src="https://raw.githubusercontent.com/driftime/sanity-plugin-icon/HEAD/.github/assets/icon-search-light.png" alt="The icon selector filtered by the term next, showing thirteen arrow and forward icons matched on what they depict rather than on their names" />
131
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/icon/assets/icon-search-dark.png" />
132
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/icon/assets/icon-search-light.png" />
133
+ <img src="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/icon/assets/icon-search-light.png" alt="The icon picker filtered by the search term next, showing every arrow and forward icon in the library" />
85
134
  </picture>
86
- <p align="center"><sub><em>Searching matches what an icon depicts, so “next” finds every arrow.</em></sub></p>
135
+ <p align="center"><sub><em>Search matches an icon's name and the terms Lucide tags it with, so "next" finds every arrow.</em></sub></p>
87
136
  </figure>
88
137
 
89
138
  <br />
90
139
 
91
- ## Rendering Icons
140
+ ### Document Previews
92
141
 
93
- `Icon` is exported from a separate import path that holds no Studio code, so it can be used anywhere outside the Studio, in server and client components alike. It needs no icon library of its own.
142
+ A document's preview shows the icon its type was given in the schema, which is the same for every document of that type. `createIconPreview` shows the icon the author chose instead. Pass it the stored value in `prepare`.
94
143
 
95
- ```tsx
96
- import { Icon } from "@driftime/sanity-plugin-icon/render";
144
+ ```typescript
145
+ import { createIconPreview } from "@driftime/sanity-plugin-icon";
97
146
 
98
- <Icon value={home.icon} className="size-8" />;
147
+ defineType({
148
+ // ...
149
+ preview: {
150
+ select: { title: "title", icon: "icon" },
151
+ prepare({ title, icon }) {
152
+ return { title, media: createIconPreview(icon) };
153
+ },
154
+ },
155
+ });
99
156
  ```
100
157
 
101
- It takes the stored field value directly and renders nothing when that value is absent or unreadable. Props extend `ComponentProps<"svg">`, so `className`, `style`, `aria-label` and the rest pass through to the element.
158
+ When nothing readable is stored, it returns `undefined` and the preview falls back to the type's own icon.
159
+
160
+ <br />
102
161
 
103
- Type the field with `SanityIcon`, available from either import path.
162
+ ### Keeping the Library Current
104
163
 
105
- ```typescript
106
- import type { SanityIcon } from "@driftime/sanity-plugin-icon";
164
+ The plugin depends on Lucide with a caret range, so a newer release installs without a plugin release. The project's lockfile pins whichever release was installed, and there are two ways to move it forward. Removing and re-adding the plugin resolves the range again.
107
165
 
108
- export interface SanityHome {
109
- title: string;
110
- icon?: SanityIcon;
111
- }
166
+ ```bash
167
+ bun remove @driftime/sanity-plugin-icon
168
+ bun add -E @driftime/sanity-plugin-icon
112
169
  ```
113
170
 
171
+ Adding `lucide-static` to the project's own dependencies puts the release under the project's control, and the plugin resolves to that copy from then on.
172
+
114
173
  <br />
115
174
 
116
- ## Document Previews
175
+ ## Site
117
176
 
118
- A document's preview shows the icon its _type_ defines, which is the same glyph for every document of that type. `createIconPreview` builds preview media from the icon an author chose instead.
177
+ On the site, one component draws a stored icon, and it is imported from `@driftime/sanity-plugin-icon/render`. That path carries no Studio code, so it is safe in server components and anywhere else on the site.
119
178
 
120
- ```typescript
121
- import { createIconPreview } from "@driftime/sanity-plugin-icon";
179
+ <br />
122
180
 
123
- preview: {
124
- select: { title: "title", icon: "icon" },
125
- prepare({ title, icon }) {
126
- return { title, media: createIconPreview(icon) };
127
- },
128
- },
181
+ ### Rendering Icons
182
+
183
+ `Icon` draws the shapes held in the stored value, so no icon library is installed on the site. The markup is an `svg` element, and the component renders nothing when the value is missing or unreadable.
184
+
185
+ ```tsx
186
+ import { Icon } from "@driftime/sanity-plugin-icon/render";
187
+ import type { SanityIcon } from "@driftime/sanity-plugin-icon/render";
188
+
189
+ interface FeatureProps {
190
+ icon: SanityIcon | undefined;
191
+ title: string;
192
+ }
193
+
194
+ export function Feature({ icon, title }: FeatureProps) {
195
+ return (
196
+ <article>
197
+ <Icon value={icon} className="size-8" />
198
+ <h3>{title}</h3>
199
+ </article>
200
+ );
201
+ }
129
202
  ```
130
203
 
131
- It returns `undefined` when there is nothing to draw, so the preview falls back to Sanity's own placeholder.
204
+ | Prop | Type | Purpose |
205
+ | ------- | ------------------------- | ------------------------------------------------------ |
206
+ | `value` | `SanityIcon \| undefined` | The stored icon. Required, though it may hold nothing. |
207
+
208
+ Every other prop is an `svg` element's and is spread onto the root element after the plugin's own attributes, so `className`, `style`, and `strokeWidth` all apply. The icon is drawn on Lucide's frame, 24 by 24 with a stroke of 2, and `aria-hidden`, so assistive technology treats it as decoration beside its text. An icon that carries meaning on its own takes `aria-hidden={false}` and an `aria-label`.
209
+
210
+ ```tsx
211
+ <Icon value={link.icon} aria-hidden={false} aria-label="Opens in a new tab" />
212
+ ```
132
213
 
133
214
  <br />
134
215
 
135
- ## Querying
216
+ ### Querying
136
217
 
137
- A stored icon needs no special handling in a query. It comes down as a plain object alongside the rest of the document, with nothing to dereference or expand.
218
+ An icon needs nothing special in a query. It comes back as a plain object with the rest of the document.
138
219
 
139
220
  ```groq
140
221
  *[_type == "home"][0] { title, icon }
@@ -142,47 +223,74 @@ A stored icon needs no special handling in a query. It comes down as a plain obj
142
223
 
143
224
  <br />
144
225
 
145
- ## What Gets Stored
226
+ ## Reference
227
+
228
+ The rest of this document covers the shape of a stored icon and everything the package exports.
229
+
230
+ <br />
231
+
232
+ ### Stored Icons
233
+
234
+ `SanityIcon` is the type of a stored icon.
235
+
236
+ ```typescript
237
+ import type { SanityIcon } from "@driftime/sanity-plugin-icon/render";
238
+
239
+ export interface SanityFeature {
240
+ icon?: SanityIcon;
241
+ }
242
+ ```
146
243
 
147
244
  ```json
148
245
  {
149
246
  "_type": "icon",
150
247
  "name": "house",
151
- "node": "[[\"path\",{\"d\":\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\"}], …]"
248
+ "node": "[[\"path\",{\"d\":\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\"}],[\"path\",{\"d\":\"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"}]]"
152
249
  }
153
250
  ```
154
251
 
155
- The `name` records which icon was chosen, and the `node` holds the shapes it is drawn from. Both are written when the icon is picked, which is what lets it render without an icon library.
252
+ | Field | Type | Purpose |
253
+ | ------- | -------- | ------------------------------------------------------------------------------------- |
254
+ | `_type` | `string` | Always `icon`. |
255
+ | `name` | `string` | The icon's name in the Lucide release it was chosen from. |
256
+ | `node` | `string` | The shapes inside the icon's `svg` element, as JSON. Written when the icon is chosen. |
257
+
258
+ Both fields are written together, which is what lets an icon render without a library. A stored icon with a name but no drawing fails validation with a prompt to select it again, so a document cannot be published with an icon that renders as nothing.
156
259
 
157
260
  <br />
158
261
 
159
- ## Keeping the Icon Set Current
262
+ ### API
160
263
 
161
- The plugin tracks Lucide within its major version, so new icons arrive without waiting for a plugin release. Your lockfile pins whichever version resolved at install, and removing and re-adding the plugin resolves it afresh.
264
+ | Export | Import from | Purpose |
265
+ | -------------------------- | ------------------------------------- | ---------------------------------------- |
266
+ | `iconPlugin(config?)` | `@driftime/sanity-plugin-icon` | Registers the `icon` type. |
267
+ | `createIconPreview(value)` | `@driftime/sanity-plugin-icon` | Builds preview media from a stored icon. |
268
+ | `Icon` | `@driftime/sanity-plugin-icon/render` | Draws a stored icon. |
162
269
 
163
- ```bash
164
- bun remove @driftime/sanity-plugin-icon
165
- bun add -E @driftime/sanity-plugin-icon
166
- ```
270
+ <br />
271
+
272
+ ### Types
167
273
 
168
- `bun update lucide-static` also works, and adds the icon library to your own dependencies so you control the version from then on.
274
+ | Type | Import from | Purpose |
275
+ | ---------------------- | ------------------------------------- | ---------------------------------------------------------------- |
276
+ | `SanityIconConfig` | `@driftime/sanity-plugin-icon` | Everything `iconPlugin` accepts. |
277
+ | `SanityIconOptions` | `@driftime/sanity-plugin-icon` | The `options` an icon field accepts. |
278
+ | `SanityIconDefinition` | `@driftime/sanity-plugin-icon` | A field or array member of type `icon`. |
279
+ | `SanityIconName` | `@driftime/sanity-plugin-icon` | The name of an icon in the installed Lucide release. |
280
+ | `SanityIconProps` | `@driftime/sanity-plugin-icon/render` | Everything `Icon` accepts: an `svg` element's props and `value`. |
281
+ | `SanityIcon` | `@driftime/sanity-plugin-icon/render` | A stored icon, with its name and its drawing. |
169
282
 
170
283
  <br />
171
284
 
172
- ## API
285
+ ## License
173
286
 
174
- | Export | Import from | Purpose |
175
- | -------------------------- | ------------------------------------- | ------------------------------------------ |
176
- | `iconPlugin()` | `@driftime/sanity-plugin-icon` | Registers the `icon` type with the Studio. |
177
- | `createIconPreview(value)` | `@driftime/sanity-plugin-icon` | Builds preview media from a stored icon. |
178
- | `Icon` | `@driftime/sanity-plugin-icon/render` | Draws a stored icon. |
179
- | `SanityIcon` | either | Type of the stored value. |
287
+ MIT © [Driftime®](https://driftime.com). See [LICENSE](https://github.com/driftime/sanity-plugins/blob/main/LICENSE).
180
288
 
181
289
  <br />
182
290
 
183
291
  ## Acknowledgements
184
292
 
185
- Icons come from [Lucide](https://lucide.dev), distributed under the [ISC License](https://github.com/lucide-icons/lucide/blob/main/LICENSE). Icons for controls the Studio already draws come from `@sanity/icons` so they match their surroundings.
293
+ Icons are from [Lucide](https://lucide.dev), distributed under the [ISC License](https://github.com/lucide-icons/lucide/blob/main/LICENSE). Icons for the Studio's own controls come from `@sanity/icons`, so they match the rest of the Studio.
186
294
 
187
295
  <br />
188
296
  <br />
@@ -190,7 +298,7 @@ Icons come from [Lucide](https://lucide.dev), distributed under the [ISC License
190
298
  <div align="center">
191
299
  <p><strong>Built alongside <a href="https://cairn.driftime.com">Cairn</a>, a starting point for responsible web experiences.</strong></p>
192
300
  <p>Part of a suite of Sanity Studio plugins by Driftime®</p>
193
- <p><a href="https://github.com/driftime/sanity-plugin-handbook">Handbook</a> · <a href="https://github.com/driftime/sanity-plugin-icon">Icon</a></p>
301
+ <p><a href="https://github.com/driftime/sanity-plugins#readme">Sanity Plugins</a> · <a href="https://github.com/driftime/sanity-plugins/tree/main/packages/handbook#readme">Handbook</a> · <a href="https://github.com/driftime/sanity-plugins/tree/main/packages/icon#readme">Icon</a> · <a href="https://github.com/driftime/sanity-plugins/tree/main/packages/color#readme">Color</a> · <a href="https://github.com/driftime/sanity-plugins/tree/main/packages/link#readme">Link</a></p>
194
302
  </div>
195
303
 
196
304
  <br />
@@ -1,5 +1,5 @@
1
- import { jsx } from "react/jsx-runtime";
2
1
  import { createElement } from "react";
2
+ import { jsx } from "react/jsx-runtime";
3
3
  import { stegaClean } from "@sanity/client/stega";
4
4
  /**
5
5
  * Checks whether a value is defined and non-empty, treating `false`, empty strings, and objects with
@@ -38,6 +38,15 @@ function convertCase(value, format) {
38
38
  default: return value;
39
39
  }
40
40
  }
41
+ /**
42
+ * Checks whether a value can be read by key, narrowing what arrives untyped from outside the plugin.
43
+ *
44
+ * @param value - The value to check.
45
+ * @returns True if the value has keys to read.
46
+ */
47
+ function isRecord(value) {
48
+ return typeof value == "object" && !!value;
49
+ }
41
50
  /** Elements an icon's drawing may be built from, narrowed to those that describe a shape. */
42
51
  const iconElements = [
43
52
  "circle",
@@ -67,7 +76,7 @@ const iconElements = [
67
76
  * @returns True if the value is a drawing.
68
77
  */
69
78
  function isIconNode(value) {
70
- return Array.isArray(value) ? value.every((element) => Array.isArray(element) && element.length === 2 && iconElements.some((name) => name === element[0]) && typeof element[1] == "object" && element[1] !== null) : !1;
79
+ return Array.isArray(value) ? value.every((element) => Array.isArray(element) && element.length === 2 && iconElements.some((name) => name === element[0]) && isRecord(element[1])) : !1;
71
80
  }
72
81
  /**
73
82
  * Reads an icon's drawing from an already-parsed value, for a library handing over its drawings as
@@ -101,7 +110,7 @@ function resolveIconNode(value) {
101
110
  * @param node - The drawing to store.
102
111
  * @returns The drawing ready to store.
103
112
  */
104
- function serialiseIconNode(node) {
113
+ function serializeIconNode(node) {
105
114
  return JSON.stringify(node);
106
115
  }
107
116
  function Drawing({ node, ...props }) {
@@ -115,6 +124,6 @@ function Drawing({ node, ...props }) {
115
124
  }))
116
125
  }) : null;
117
126
  }
118
- export { convertCase as a, serialiseIconNode as i, parseIconNode as n, isDefined as o, resolveIconNode as r, Drawing as t };
127
+ export { convertCase as a, serializeIconNode as i, parseIconNode as n, isDefined as o, resolveIconNode as r, Drawing as t };
119
128
 
120
- //# sourceMappingURL=drawing-Bo0HTICv.js.map
129
+ //# sourceMappingURL=drawing-BThiR2bN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drawing-BThiR2bN.js","names":["Nullable","T","isDefined","value","undefined","trim","Array","isArray","some","element","prototype","Reflect","getPrototypeOf","Object","keys","length","convertCase","format","words","replaceAll","toLowerCase","_match","character","toUpperCase","isRecord","Record","readPath","path","current","segment","isDefined","isRecord","stegaClean","iconElements","const","IconElement","element","attributes","Record","IconNode","iconRootAttributes","xmlns","width","height","viewBox","fill","stroke","strokeWidth","strokeLinecap","strokeLinejoin","isIconNode","value","Array","isArray","every","length","some","name","parseIconNode","undefined","resolveIconNode","cleaned","parsed","JSON","parse","serializeIconNode","node","stringify","isDefined","ComponentProps","createElement","IconNode","iconRootAttributes","DrawingProps","Omit","node","Drawing","props","map","element","attributes","index","key","String"],"sources":["../../@repo/lib/utils.ts","../src/lib/nodes.ts","../src/components/drawing.tsx"],"sourcesContent":["/** A value that may be absent, covering both null and undefined. */\ntype Nullable<T> = T | null | undefined;\n\n/**\n * Checks whether a value is defined and non-empty, treating `false`, empty strings, and objects with\n * no keys as absent. An array is absent unless one of its elements is itself present, while anything\n * built from a class counts as present on existence alone. Not intended for boolean flags.\n *\n * @param value - The value to check.\n * @returns True if the value is defined and not empty.\n */\nexport function isDefined<T>(value: Nullable<T> | false): value is T {\n if (value === undefined || value === null || value === false) return false;\n if (typeof value === \"string\") return value.trim() !== \"\";\n if (Array.isArray(value)) return value.some((element) => isDefined(element));\n\n if (typeof value === \"object\") {\n const prototype = Reflect.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) return true;\n\n return Object.keys(value).length > 0;\n }\n\n return true;\n}\n\n/**\n * Converts a string between different casing formats.\n *\n * @param value - The string to convert.\n * @param format - The target casing format.\n * @returns The converted string.\n */\nexport function convertCase(value: string, format: \"kebab\" | \"snake\" | \"camel\" | \"pascal\" | \"title\" | \"sentence\") {\n const words = value\n .replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, \"$<lower> $<upper>\")\n .replaceAll(/[-_\\s]+/gu, \" \")\n .trim()\n .toLowerCase();\n\n switch (format) {\n case \"kebab\": {\n return words.replaceAll(/\\s+/gu, \"-\");\n }\n case \"snake\": {\n return words.replaceAll(/\\s+/gu, \"_\");\n }\n case \"camel\": {\n return words.replaceAll(/\\s+(?<character>.)/gu, (_match, character: string) => character.toUpperCase());\n }\n case \"pascal\": {\n return words.replaceAll(/(?:^|\\s+)(?<character>.)/gu, (_match, character: string) => character.toUpperCase());\n }\n case \"title\": {\n return words.replaceAll(/\\b\\w/gu, (character) => character.toUpperCase());\n }\n case \"sentence\": {\n return words.replaceAll(/^(?<character>.)/gu, (character) => character.toUpperCase());\n }\n default: {\n return value;\n }\n }\n}\n\n/**\n * Checks whether a value can be read by key, narrowing what arrives untyped from outside the plugin.\n *\n * @param value - The value to check.\n * @returns True if the value has keys to read.\n */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Reads a value out of nested records by following a path of keys, so a caller can name something\n * inside a shape it does not otherwise know.\n *\n * @param value - The object to read from.\n * @param path - Keys leading to the value.\n * @returns The value at the path, or undefined when a key is missing or a step holds no keys to read.\n */\nexport function readPath(value: unknown, path: string[]) {\n let current: unknown = value;\n\n for (const segment of path) {\n if (!isRecord(current)) return undefined;\n\n current = current[segment];\n }\n\n return current;\n}\n","import { isDefined, isRecord } from \"@repo/lib/utils\";\nimport { stegaClean } from \"@sanity/client/stega\";\n\n/** Elements an icon's drawing may be built from, narrowed to those that describe a shape. */\nconst iconElements = [\"circle\", \"ellipse\", \"g\", \"line\", \"path\", \"polygon\", \"polyline\", \"rect\"] as const;\n\n/** Single shape in an icon's drawing, paired with the attributes it is drawn with. */\ntype IconElement = [element: (typeof iconElements)[number], attributes: Record<string, string>];\n\n/** An icon's drawing, held as the shapes it is built from rather than as a component. */\nexport type IconNode = IconElement[];\n\n/**\n * Attributes given to the SVG an icon's shapes are drawn inside, mirroring the grid and stroke Lucide\n * draws on. A library working to different conventions would need its own frame, which is why a\n * stored icon carries only what sits inside this one.\n */\nexport const iconRootAttributes = {\n xmlns: \"http://www.w3.org/2000/svg\",\n width: 24,\n height: 24,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 2,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n} as const;\n\n/**\n * Checks whether a parsed value is an icon's drawing, since it arrives as whatever happened to be\n * stored. Shapes are matched against the known elements so nothing else reaches the renderer.\n *\n * @param value - The value to check.\n * @returns True if the value is a drawing.\n */\nfunction isIconNode(value: unknown): value is IconNode {\n if (!Array.isArray(value)) return false;\n\n return value.every(\n (element: unknown) =>\n Array.isArray(element) &&\n element.length === 2 &&\n iconElements.some((name) => name === element[0]) &&\n isRecord(element[1]),\n );\n}\n\n/**\n * Reads an icon's drawing from an already-parsed value, for a library handing over its drawings as\n * data rather than as a document field.\n *\n * @param value - The value to read.\n * @returns The drawing, or undefined when the value is not one.\n */\nexport function parseIconNode(value: unknown) {\n return isIconNode(value) ? value : undefined;\n}\n\n/**\n * Reads an icon's drawing back from the form it is stored in, clearing the stega characters a Sanity\n * fetch leaves behind.\n *\n * @param value - The stored drawing.\n * @returns The drawing, or undefined when nothing readable was stored.\n */\nexport function resolveIconNode(value: string | undefined) {\n const cleaned = stegaClean(value);\n if (!isDefined(cleaned)) return undefined;\n\n try {\n const parsed: unknown = JSON.parse(cleaned);\n\n return parseIconNode(parsed);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Writes an icon's drawing into the form it is stored in, so the shapes survive a round trip through\n * a document rather than a component reference that only resolves at build time.\n *\n * @param node - The drawing to store.\n * @returns The drawing ready to store.\n */\nexport function serializeIconNode(node: IconNode) {\n return JSON.stringify(node);\n}\n","// Runs inside a consumer's server components, where the compiler's memo cache hook cannot.\n\"use no memo\";\n\nimport { isDefined } from \"@repo/lib/utils\";\nimport type { ComponentProps } from \"react\";\nimport { createElement } from \"react\";\n\nimport type { IconNode } from \"@/lib/nodes\";\nimport { iconRootAttributes } from \"@/lib/nodes\";\n\nexport type DrawingProps = Omit<ComponentProps<\"svg\">, \"children\"> & { node: IconNode | undefined };\n\nexport function Drawing({ node, ...props }: DrawingProps) {\n if (!isDefined(node)) return null;\n\n return (\n <svg {...iconRootAttributes} aria-hidden=\"true\" {...props}>\n {node.map(([element, attributes], index) => createElement(element, { ...attributes, key: String(index) }))}\n </svg>\n );\n}\n"],"mappings":";;;;;;;;;;;AAWA,SAAgBE,UAAaC,OAAwC;CACnE,IAAIA,SAAiC,QAAQA,UAAU,IAAO,OAAO;CACrE,IAAI,OAAOA,SAAU,UAAU,OAAOA,MAAME,KAAK,MAAM;CACvD,IAAIC,MAAMC,QAAQJ,KAAK,GAAG,OAAOA,MAAMK,MAAMC,YAAYP,UAAUO,OAAO,CAAC;CAE3E,IAAI,OAAON,SAAU,UAAU;EAC7B,IAAMO,YAAYC,QAAQC,eAAeT,KAAK;EAG9C,OAFIO,cAAcG,OAAOH,aAAaA,cAAc,QAE7CG,OAAOC,KAAKX,KAAK,CAAC,CAACY,SAAS;CACrC;CAEA,OAAO;AACT;;;;;;;;AASA,SAAgBC,YAAYb,OAAec,QAAuE;CAChH,IAAMC,QAAQf,MACXgB,WAAW,uCAAuC,mBAAmB,CAAC,CACtEA,WAAW,aAAa,GAAG,CAAC,CAC5Bd,KAAK,CAAC,CACNe,YAAY;CAEf,QAAQH,QAAR;EACE,KAAK,SACH,OAAOC,MAAMC,WAAW,SAAS,GAAG;EAEtC,KAAK,SACH,OAAOD,MAAMC,WAAW,SAAS,GAAG;EAEtC,KAAK,SACH,OAAOD,MAAMC,WAAW,yBAAyBE,QAAQC,cAAsBA,UAAUC,YAAY,CAAC;EAExG,KAAK,UACH,OAAOL,MAAMC,WAAW,+BAA+BE,QAAQC,cAAsBA,UAAUC,YAAY,CAAC;EAE9G,KAAK,SACH,OAAOL,MAAMC,WAAW,WAAWG,cAAcA,UAAUC,YAAY,CAAC;EAE1E,KAAK,YACH,OAAOL,MAAMC,WAAW,uBAAuBG,cAAcA,UAAUC,YAAY,CAAC;EAEtF,SACE,OAAOpB;CAEX;AACF;;;;;;;AAQA,SAAgBqB,SAASrB,OAAkD;CACzE,OAAO,OAAOA,SAAU,cAAYA;AACtC;;ACrEA,MAAM8B,eAAe;CAAC;CAAU;CAAW;CAAK;CAAQ;CAAQ;CAAW;CAAY;AAAM,GAahFO,qBAAqB;CAChCC,OAAO;CACPC,OAAO;CACPC,QAAQ;CACRC,SAAS;CACTC,MAAM;CACNC,QAAQ;CACRC,aAAa;CACbC,eAAe;CACfC,gBAAgB;AAClB;;;;;;;;AASA,SAASC,WAAWC,OAAmC;CAGrD,OAFKC,MAAMC,QAAQF,KAAK,IAEjBA,MAAMG,OACVlB,YACCgB,MAAMC,QAAQjB,OAAO,KACrBA,QAAQmB,WAAW,KACnBtB,aAAauB,MAAMC,SAASA,SAASrB,QAAQ,EAAE,KAC/CL,SAASK,QAAQ,EAAE,CACvB,IARkC;AASpC;;;;;;;;AASA,SAAgBsB,cAAcP,OAAgB;CAC5C,OAAOD,WAAWC,KAAK,IAAIA,QAAQQ,KAAAA;AACrC;;;;;;;;AASA,SAAgBC,gBAAgBT,OAA2B;CACzD,IAAMU,UAAU7B,WAAWmB,KAAK;CAC3BrB,cAAU+B,OAAO,GAEtB,IAAI;EAGF,OAAOH,cAFiBK,KAAKC,MAAMH,OAEdC,CAAM;CAC7B,QAAQ;EACN;CACF;AACF;;;;;;;;AASA,SAAgBG,kBAAkBC,MAAgB;CAChD,OAAOH,KAAKI,UAAUD,IAAI;AAC5B;AC5EA,SAAgBU,QAAQ,EAAED,MAAM,GAAGE,SAAuB;CAGxD,OAFKT,UAAUO,IAAI,IAGjB,oBAAC,OAAD;EAAK,GAAIH;EAAoB,eAAY;EAAO,GAAIK;EACjDF,UAAAA,KAAKG,KAAK,CAACC,SAASC,aAAaC,UAAUX,cAAcS,SAAS;GAAE,GAAGC;GAAYE,KAAKC,OAAOF,KAAK;EAAE,CAAC,CAAC;CACtG,CAAA,IALsB;AAO/B"}
package/dist/index.d.ts CHANGED
@@ -1,12 +1,64 @@
1
- import { t as SanityIcon } from "./types-nQUVfUHK.js";
1
+ import { n as iconTypeName, t as SanityIcon } from "./types-_D2jGm25.js";
2
+ import { ObjectOptions, TypeAliasDefinition } from "sanity";
3
+ import { ComponentProps } from "react";
4
+ import iconNodes from "lucide-static/icon-nodes.json";
5
+ declare module "@sanity/types" {
6
+ interface IntrinsicDefinitions {
7
+ icon: SanityIconDefinition;
8
+ }
9
+ }
10
+ /**
11
+ * Names of every icon the library offers, read from the Lucide release a project resolved rather than
12
+ * from a list this plugin ships, so an icon added since the last plugin release still completes.
13
+ *
14
+ * @public
15
+ */
16
+ type SanityIconName = keyof typeof iconNodes;
17
+ /**
18
+ * Options an icon field takes, narrowing what its picker offers.
19
+ *
20
+ * @public
21
+ */
22
+ interface SanityIconOptions extends ObjectOptions {
23
+ /** Icons an author may choose from, offered in the order they are named. Every icon when omitted. */
24
+ icons?: SanityIconName[];
25
+ }
26
+ /**
27
+ * Shape of a field or array member holding an icon, so its options are completed and checked the way
28
+ * a built-in type's are.
29
+ *
30
+ * @public
31
+ */
32
+ interface SanityIconDefinition extends Omit<TypeAliasDefinition<typeof iconTypeName, undefined>, "options"> {
33
+ options?: SanityIconOptions;
34
+ }
35
+ /**
36
+ * Everything the plugin accepts, all of which may be left out.
37
+ *
38
+ * @public
39
+ */
40
+ interface SanityIconConfig {
41
+ /** Icons an author may choose from, offered in the order they are named. Every icon when omitted. */
42
+ icons?: SanityIconName[];
43
+ }
2
44
  /**
3
45
  * Creates the icon field type for Sanity Studio, offering the Lucide library through a searchable
4
46
  * picker and storing the chosen drawing ready to render.
5
47
  *
48
+ * @param config - Plugin configuration.
6
49
  * @returns Sanity plugin definition.
7
50
  * @public
8
51
  */
9
- declare const iconPlugin: import("sanity").Plugin<void>;
52
+ export declare function iconPlugin(config?: SanityIconConfig): import("sanity").PluginOptions;
53
+ /** Elements an icon's drawing may be built from, narrowed to those that describe a shape. */
54
+ declare const iconElements: readonly ["circle", "ellipse", "g", "line", "path", "polygon", "polyline", "rect"];
55
+ /** Single shape in an icon's drawing, paired with the attributes it is drawn with. */
56
+ type IconElement = [element: (typeof iconElements)[number], attributes: Record<string, string>];
57
+ /** An icon's drawing, held as the shapes it is built from rather than as a component. */
58
+ type IconNode = IconElement[];
59
+ type DrawingProps = Omit<ComponentProps<"svg">, "children"> & {
60
+ node: IconNode | undefined;
61
+ };
10
62
  /**
11
63
  * Builds the media a stored icon previews with, so a document lists the drawing an author chose
12
64
  * rather than one placeholder standing in for every icon alike.
@@ -15,6 +67,6 @@ declare const iconPlugin: import("sanity").Plugin<void>;
15
67
  * @returns The media component, or undefined when nothing readable was stored.
16
68
  * @public
17
69
  */
18
- declare function createIconPreview(value: SanityIcon | undefined): (() => import("react").JSX.Element) | undefined;
19
- export { type SanityIcon, createIconPreview, iconPlugin };
70
+ export declare function createIconPreview(value: SanityIcon | undefined): (() => import("react").FunctionComponentElement<DrawingProps>) | undefined;
71
+ export type { SanityIconConfig, SanityIconDefinition, SanityIconName, SanityIconOptions };
20
72
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugin.ts","../src/components/preview.tsx"],"mappings":";;;;;;;;cAWa,6BAAU;;;;;;;;;iBCGP,kBAAkB,OAAO,gDAAsB,IAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugin.ts","../src/lib/nodes.ts","../src/components/drawing.tsx","../src/lib/preview.ts"],"mappings":";;;;;YAQY;IACR,MAAM;;;;;;;;;KAUE,8BAA8B;;;;;;UAOzB,0BAA0B;;EAEzC,QAAQ;;;;;;;;UASO,6BAA6B,KAAK,2BAA2B;EAC5E,UAAU;;;;;;;UAQK;;EAEf,QAAQ;;;;;;;;;;wBAkBM,WAAW,SAAQ,oCAAqB;;cC9DlD;;KAGD,eAAe,iBAAiB,uBAAuB,YAAY;;KAG5D,WAAW;KCAX,eAAe,KAAK;EAAuC,MAAM;;;;;;;;;;wBCM7D,kBAAkB,OAAO,gDAAsB,yBAAtB"}