@driftime/sanity-plugin-icon 0.2.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 Lucide library as a searchable grid, either in full or limited to a set you choose. 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,139 +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.
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). |
69
91
 
70
- See [Configuration](#configuration) for the options the plugin takes.
92
+ The type name is always `icon`, so a Studio that already has a type by that name needs to rename it before installing.
71
93
 
72
94
  <br />
73
95
 
74
- ## Restricting the Icon Set
96
+ ### Restricting the Icons
75
97
 
76
- By default the picker offers every icon in the library. Pass `icons` to limit it, and every field takes the set you name.
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.
77
99
 
78
100
  ```typescript
79
- export default defineConfig({
80
- // ...
81
- plugins: [iconPlugin({ icons: ["mail", "phone", "globe"] })],
101
+ iconPlugin({
102
+ icons: ["sun", "wind", "droplets", "leaf", "sprout", "mountain", "tent", "compass"],
82
103
  });
83
104
  ```
84
105
 
85
- The plugin augments Sanity's `IntrinsicDefinitions` interface, adding an `icons` option that TypeScript recognises automatically. A field can name its own set, which replaces the one the plugin was given rather than narrowing it.
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`.
86
107
 
87
108
  ```typescript
88
109
  defineField({
89
110
  name: "icon",
90
111
  type: "icon",
91
- description: "Shown on the feature card.",
92
- options: { icons: ["leaf", "shield", "zap"] },
112
+ description: "Icon shown beside the contact method.",
113
+ options: { icons: ["mail", "phone", "globe"] },
93
114
  });
94
115
  ```
95
116
 
96
- Names are typed against the Lucide version your project resolved, so they autocomplete as you write them. Icons appear in the order you list them, and a name that version does not recognise is skipped.
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.
97
118
 
98
- Use `SanityIconName` to type a set you want to share between fields.
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`.
99
120
 
100
121
  ```typescript
101
122
  import type { SanityIconName } from "@driftime/sanity-plugin-icon";
102
123
 
103
- export const clientIcons: SanityIconName[] = ["mail", "phone", "globe"];
124
+ export const contactIcons: SanityIconName[] = ["mail", "phone", "globe"];
104
125
  ```
105
126
 
106
- Restricting a set does not affect what has already been stored. The document holds the drawing, so an icon saved earlier still renders even though the picker no longer offers it.
107
-
108
- <br />
109
-
110
- ## Picking an Icon
111
-
112
- - Search matches an icon's name and the terms it is tagged with, so "next" finds `arrow-right`.
113
- - The complete Lucide set is offered unless you restrict it, and the count reflects whatever is available.
114
- - The last eight icons chosen are remembered, per browser rather than per dataset.
115
-
116
127
  <br />
117
128
 
118
129
  <figure>
119
130
  <picture>
120
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-icon/HEAD/.github/assets/icon-search-dark.png" />
121
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-icon/HEAD/.github/assets/icon-search-light.png" />
122
- <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" />
123
134
  </picture>
124
- <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>
125
136
  </figure>
126
137
 
127
138
  <br />
128
139
 
129
- ## Rendering Icons
140
+ ### Document Previews
130
141
 
131
- `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`.
132
143
 
133
- ```tsx
134
- import { Icon } from "@driftime/sanity-plugin-icon/render";
144
+ ```typescript
145
+ import { createIconPreview } from "@driftime/sanity-plugin-icon";
135
146
 
136
- <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
+ });
137
156
  ```
138
157
 
139
- 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.
140
159
 
141
- Type the field with `SanityIcon`, available from either import path.
160
+ <br />
142
161
 
143
- ```typescript
144
- import type { SanityIcon } from "@driftime/sanity-plugin-icon";
162
+ ### Keeping the Library Current
145
163
 
146
- export interface SanityHome {
147
- title: string;
148
- icon?: SanityIcon;
149
- }
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.
165
+
166
+ ```bash
167
+ bun remove @driftime/sanity-plugin-icon
168
+ bun add -E @driftime/sanity-plugin-icon
150
169
  ```
151
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
+
152
173
  <br />
153
174
 
154
- ## Document Previews
175
+ ## Site
155
176
 
156
- 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.
157
178
 
158
- ```typescript
159
- import { createIconPreview } from "@driftime/sanity-plugin-icon";
179
+ <br />
160
180
 
161
- preview: {
162
- select: { title: "title", icon: "icon" },
163
- prepare({ title, icon }) {
164
- return { title, media: createIconPreview(icon) };
165
- },
166
- },
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
+ }
167
202
  ```
168
203
 
169
- 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
+ ```
170
213
 
171
214
  <br />
172
215
 
173
- ## Querying
216
+ ### Querying
174
217
 
175
- 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.
176
219
 
177
220
  ```groq
178
221
  *[_type == "home"][0] { title, icon }
@@ -180,81 +223,74 @@ A stored icon needs no special handling in a query. It comes down as a plain obj
180
223
 
181
224
  <br />
182
225
 
183
- ## What Gets Stored
226
+ ## Reference
184
227
 
185
- ```json
186
- {
187
- "_type": "icon",
188
- "name": "house",
189
- "node": "[[\"path\",{\"d\":\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\"}], …]"
190
- }
191
- ```
192
-
193
- 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.
228
+ The rest of this document covers the shape of a stored icon and everything the package exports.
194
229
 
195
230
  <br />
196
231
 
197
- ## Keeping the Icon Set Current
198
-
199
- 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.
232
+ ### Stored Icons
200
233
 
201
- ```bash
202
- bun remove @driftime/sanity-plugin-icon
203
- bun add -E @driftime/sanity-plugin-icon
204
- ```
234
+ `SanityIcon` is the type of a stored icon.
205
235
 
206
- `bun update lucide-static` also works, and adds the icon library to your own dependencies so you control the version from then on.
236
+ ```typescript
237
+ import type { SanityIcon } from "@driftime/sanity-plugin-icon/render";
207
238
 
208
- <br />
239
+ export interface SanityFeature {
240
+ icon?: SanityIcon;
241
+ }
242
+ ```
209
243
 
210
- ## API
244
+ ```json
245
+ {
246
+ "_type": "icon",
247
+ "name": "house",
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\"}]]"
249
+ }
250
+ ```
211
251
 
212
- What the package exports at runtime, and which import path each one comes from.
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. |
213
257
 
214
- | Export | Import from | Purpose |
215
- | -------------------------- | ------------------------------------- | ------------------------------------------ |
216
- | `iconPlugin(config?)` | `@driftime/sanity-plugin-icon` | Registers the `icon` type with the Studio. |
217
- | `createIconPreview(value)` | `@driftime/sanity-plugin-icon` | Builds preview media from a stored icon. |
218
- | `Icon` | `@driftime/sanity-plugin-icon/render` | Draws a stored icon. |
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.
219
259
 
220
260
  <br />
221
261
 
222
- ## Configuration
262
+ ### API
223
263
 
224
- Nothing is required. Without `icons`, the picker offers every icon in the library.
225
-
226
- | Option | Type | Default | Purpose |
227
- | ------- | ------------------ | ----------- | --------------------------------------------------------------------------------------- |
228
- | `icons` | `SanityIconName[]` | `undefined` | Icons an author may choose from, in the order they are listed. Every icon when omitted. |
229
-
230
- A field accepts the same option under `options`, where it replaces the set the plugin was given.
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. |
231
269
 
232
270
  <br />
233
271
 
234
- ## Exported Types
272
+ ### Types
235
273
 
236
- These types are exported for typing your own configuration and for querying stored icons outside the Studio.
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. |
237
282
 
238
- Configuration you supply:
239
-
240
- | Type | Purpose |
241
- | ---------------------- | ------------------------------------------------------- |
242
- | `SanityIconName` | The name of an icon in the installed Lucide version. |
243
- | `SanityIconOptions` | The `options` an icon field accepts. |
244
- | `SanityIconDefinition` | The shape of a field or array member of type `icon`. |
245
- | `SanityIconConfig` | The full plugin configuration accepted by `iconPlugin`. |
283
+ <br />
246
284
 
247
- Content the dataset stores:
285
+ ## License
248
286
 
249
- | Type | Purpose |
250
- | ------------ | ------------------------------------------------------------------------------------------------------------ |
251
- | `SanityIcon` | A stored icon, holding the name it was chosen by and the shapes it draws. Available from either import path. |
287
+ MIT © [Driftime®](https://driftime.com). See [LICENSE](https://github.com/driftime/sanity-plugins/blob/main/LICENSE).
252
288
 
253
289
  <br />
254
290
 
255
291
  ## Acknowledgements
256
292
 
257
- 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.
258
294
 
259
295
  <br />
260
296
  <br />
@@ -262,7 +298,7 @@ Icons come from [Lucide](https://lucide.dev), distributed under the [ISC License
262
298
  <div align="center">
263
299
  <p><strong>Built alongside <a href="https://cairn.driftime.com">Cairn</a>, a starting point for responsible web experiences.</strong></p>
264
300
  <p>Part of a suite of Sanity Studio plugins by Driftime®</p>
265
- <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>
266
302
  </div>
267
303
 
268
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
@@ -49,7 +49,7 @@ interface SanityIconConfig {
49
49
  * @returns Sanity plugin definition.
50
50
  * @public
51
51
  */
52
- declare function iconPlugin(config?: SanityIconConfig): import("sanity").PluginOptions;
52
+ export declare function iconPlugin(config?: SanityIconConfig): import("sanity").PluginOptions;
53
53
  /** Elements an icon's drawing may be built from, narrowed to those that describe a shape. */
54
54
  declare const iconElements: readonly ["circle", "ellipse", "g", "line", "path", "polygon", "polyline", "rect"];
55
55
  /** Single shape in an icon's drawing, paired with the attributes it is drawn with. */
@@ -67,6 +67,6 @@ type DrawingProps = Omit<ComponentProps<"svg">, "children"> & {
67
67
  * @returns The media component, or undefined when nothing readable was stored.
68
68
  * @public
69
69
  */
70
- declare function createIconPreview(value: SanityIcon | undefined): (() => import("react").FunctionComponentElement<DrawingProps>) | undefined;
71
- export { type SanityIcon, type SanityIconConfig, type SanityIconDefinition, type SanityIconName, type SanityIconOptions, createIconPreview, iconPlugin };
70
+ export declare function createIconPreview(value: SanityIcon | undefined): (() => import("react").FunctionComponentElement<DrawingProps>) | undefined;
71
+ export type { SanityIconConfig, SanityIconDefinition, SanityIconName, SanityIconOptions };
72
72
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
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;;;;;;;;;;iBAuBM,WAAW,SAAQ,oCAAqB;;cClElD;;KAGD,eAAe,iBAAiB,uBAAuB,YAAY;;KAG5D,WAAW;KCDX,eAAe,KAAK;EAAuC,MAAM;;;;;;;;;;iBCM7D,kBAAkB,OAAO,gDAAsB,yBAAtB"}
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"}