@driftime/sanity-plugin-handbook 0.3.2 → 0.4.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.
package/README.md CHANGED
@@ -1,36 +1,44 @@
1
1
  <div align="center">
2
2
  <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/icon-dark.svg" />
4
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/icon-light.svg" />
5
- <img src="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/icon-light.svg" alt="Handbook plugin logo" width="48" />
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/icon-dark.svg" />
4
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/icon-light.svg" />
5
+ <img src="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/icon-light.svg" alt="Handbook plugin logo" width="48" />
6
6
  </picture>
7
7
  <h1>Handbook</h1>
8
8
  <p><strong>A Sanity Studio plugin by Driftime®</strong></p>
9
9
  <p>Schema-driven documentation and editorial guides, built right into Sanity Studio.</p>
10
+ <p>
11
+ <a href="https://www.npmjs.com/package/@driftime/sanity-plugin-handbook"><img src="https://img.shields.io/npm/v/@driftime/sanity-plugin-handbook?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-handbook?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
- Handbook adds a dedicated tool to Sanity Studio that serves two purposes: it automatically generates browsable documentation from your document type schemas, and it provides a rich text guide authoring system for editorial teams. Field descriptions, examples, tips, notes, and warnings are pulled directly from your schema definitions, so your documentation stays in sync with your content model.
20
+ Every content model comes with knowledge the schema alone can't hold: what a field is for, which values work best, and how the site's content should be written. Handbook gives that knowledge a place in the Studio, so authors find the guidance right beside the content it describes.
21
+
22
+ Most of it needs no writing at all. Handbook reads the documentation straight from the schema, listing every document type and every field with its description, an example value, and any tips or cautions defined alongside it. When the schema changes, the Handbook changes with it.
23
+
24
+ The rest is written by the site's editors. Guides cover house style, editorial process, and anything too long for a field description, arranged into groups in the same sidebar.
17
25
 
18
26
  <br />
19
27
 
20
28
  <figure>
21
29
  <picture>
22
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-fields-dark.png" />
23
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-fields-light.png" />
24
- <img src="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-fields-light.png" alt="The Handbook tool displaying field documentation for a Pages document type, with field names, types, descriptions, example values, and an expanded subfields section" />
30
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-how-to-use-dark.png" />
31
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-how-to-use-light.png" />
32
+ <img src="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-how-to-use-light.png" alt="The Handbook tool in Sanity Studio: a sidebar listing introductory pages, document types grouped by role, and the team's own guides, with the How to use the Handbook page open" />
25
33
  </picture>
26
- <p align="center"><sub><em>Field documentation generated automatically from your schema definitions.</em></sub></p>
34
+ <p align="center"><sub><em>The Handbook tool, with introductory pages, document types by role, and the team's guides in one sidebar.</em></sub></p>
27
35
  </figure>
28
36
 
29
37
  <br />
30
38
 
31
39
  ## Installation
32
40
 
33
- Handbook 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.
41
+ Handbook 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 20.19 or later is required, or 22.12 or later on Node 22.
34
42
 
35
43
  ```bash
36
44
  bun add -E @driftime/sanity-plugin-handbook
@@ -38,18 +46,26 @@ bun add -E @driftime/sanity-plugin-handbook
38
46
 
39
47
  <br />
40
48
 
41
- ## Basic Setup
49
+ ## Documentation
50
+
51
+ In the Studio, the plugin registers a Handbook tool, takes the document types it documents grouped by the role each plays in the content model, and reads a `handbook` property it adds to field and document definitions.
52
+
53
+ <br />
42
54
 
43
- Add the plugin to your Sanity configuration and pass in the document types you want to document, grouped by the role they play in your content model. Each role appears as a labelled section in the sidebar.
55
+ ### Registering the Plugin
56
+
57
+ The plugin needs to know which document types to document and how to group them. Register it with a list of roles, each holding the document definitions that play that part in the content model, and each role becomes a section of the sidebar in the order listed.
44
58
 
45
59
  ```typescript
46
60
  import { defineConfig } from "sanity";
47
61
  import { handbookPlugin } from "@driftime/sanity-plugin-handbook";
48
- // ...
62
+
63
+ import { home, navigation, page, settings, templates } from "@/schemas";
49
64
 
50
65
  export default defineConfig({
51
66
  // ...
52
67
  plugins: [
68
+ // ...
53
69
  handbookPlugin({
54
70
  roles: [
55
71
  {
@@ -73,37 +89,47 @@ export default defineConfig({
73
89
  });
74
90
  ```
75
91
 
76
- This registers a Handbook tool in the Studio navigation. Opening it shows a sidebar that starts with a built-in Getting Started section: a page on how to read the Handbook, and an overview of every role and its document types. Your own roles follow, one labelled section each. Selecting a document type shows its fields and their descriptions, with the `icon` from its schema. See [Configuration](#configuration) for all available options.
92
+ | Option | Type | Default | Purpose |
93
+ | -------------------------- | --------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------- |
94
+ | `title` | `string` | `"Handbook"` | The tool's name in the Studio navigation. See [Naming the Tool](#naming-the-tool). |
95
+ | `sidebarTitle` | `string` | `"Handbook"` | The heading at the top of the sidebar. See [Naming the Tool](#naming-the-tool). |
96
+ | `roles` | `SanityHandbookDocumentRole[]` | required | The document types to document, grouped by role, in the order listed. |
97
+ | `blocks` | `SanityHandbookBlockDefinition[]` | none | Blocks added to guide bodies. See [Adding Custom Blocks](#adding-custom-blocks). |
98
+ | `editors` | `string[]` | everyone | Email addresses of the people who write guides. See [Choosing the Editors](#choosing-the-editors). |
99
+ | `undocumentedFieldMessage` | `string` | a prompt to ask the team | Shown for a field with no description. See [Documenting Fields](#documenting-fields). |
100
+
101
+ A role takes a `title`, an optional `description` shown on the Document Types overview, and the `documents` that fill it, passed as definitions rather than names. The sidebar opens with a Getting Started section, holding a page on how to use the Handbook and the Document Types overview, followed by a section for each role and then the guides.
102
+
103
+ The plugin also registers two document types of its own for the guides, `handbook.handbook` and `handbook.guide`, along with an object type for each block a guide can hold, all under the `handbook.` prefix, and the tool itself is named `handbook`. A structure or a query that lists every registered type sees those types too, and a Studio that already has a tool or a type by one of these names needs to rename it before installing.
77
104
 
78
105
  <br />
79
106
 
80
107
  <figure>
81
108
  <picture>
82
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-document-types-dark.png" />
83
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-document-types-light.png" />
84
- <img src="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-document-types-light.png" alt="The Handbook tool showing the Document Types overview, with Singletons, Collections, and Globals roles each listing their document types and descriptions" />
109
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-document-types-dark.png" />
110
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-document-types-light.png" />
111
+ <img src="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-document-types-light.png" alt="The Document Types overview in the Handbook tool, listing each role with its description and the document types it contains" />
85
112
  </picture>
86
- <p align="center"><sub><em>Document types organised by role, browsable from the sidebar.</em></sub></p>
113
+ <p align="center"><sub><em>The Document Types overview, listing each role with its description and the types it holds.</em></sub></p>
87
114
  </figure>
88
115
 
89
116
  <br />
90
117
 
91
- ## Handbook Metadata
118
+ ### Documenting Fields
92
119
 
93
- The plugin augments Sanity's `FieldDefinitionBase` and `DocumentDefinition` interfaces, adding a `handbook` property that TypeScript recognises automatically.
120
+ Every field is documented from what the schema already says. Its title comes from `title`, or from its name in Title Case, and its description from `description`, so an existing schema is a usable handbook from the start. The `handbook` property carries the documentation the editor has no room for. The plugin declares it on Sanity's own field and document definitions, so it is typed and completes like any other property.
94
121
 
95
122
  ```typescript
96
123
  import { defineField, defineType } from "sanity";
97
124
 
98
- export const pageType = defineType({
125
+ export const page = defineType({
99
126
  name: "page",
100
127
  type: "document",
101
128
  title: "Pages",
102
129
  description: "General-purpose pages.",
103
130
  handbook: {
104
- title: "Page",
105
131
  description:
106
- "Standalone pages for anything that does not warrant a document type of its own. Each is reached at its own address and assembled from blocks rather than following a fixed layout.",
132
+ "Standalone pages for any content that doesn't need a document type of its own. Each page has its own address and is built from blocks rather than following a fixed layout.",
107
133
  },
108
134
  fields: [
109
135
  defineField({
@@ -112,140 +138,112 @@ export const pageType = defineType({
112
138
  description: "Appears in navigation, browser tabs, and search results.",
113
139
  handbook: {
114
140
  description:
115
- "The name this page goes by wherever it is referred to rather than read — navigation menus, browser tabs, and search results. Every page needs one, and it stands behind the slug whenever that is left blank.",
141
+ "What the document is called wherever it's listed: navigation menus, browser tabs, search results, and the Studio's own lists. Every document needs one, and it's also used to generate the slug and as the SEO title when those fields are left empty.",
116
142
  example: "About Acme Inc.",
117
- tip: "Keep it short enough to sit comfortably in a navigation menu, and put any fuller opening line in the heading instead.",
118
- info: "The slug is generated from the title once, so renaming a page later leaves its address unchanged.",
119
- caution: "Required. Publishing is blocked until it is filled in.",
143
+ tip: "Keep it short enough to fit in a navigation menu without wrapping.",
144
+ },
145
+ }),
146
+ defineField({
147
+ name: "slug",
148
+ type: "slug",
149
+ description: "Last part of the page's web address.",
150
+ options: { source: "title" },
151
+ handbook: {
152
+ description:
153
+ "The last part of the page's web address. Generate it from the title, then edit it if the result is long or unclear. Shorter slugs are usually better.",
154
+ example: "about-acme-inc",
155
+ caution: "Changing the slug of a published page breaks every link that already points at it.",
120
156
  },
121
157
  }),
122
158
  ],
123
159
  });
124
160
  ```
125
161
 
126
- The `handbook` property supports the following keys:
127
-
128
- | Key | Purpose |
129
- | ------------- | ----------------------------------------------------------------------- |
130
- | `title` | Display title override for the field or document. |
131
- | `description` | Description shown beneath the field or document heading. |
132
- | `example` | Illustrative example value, displayed in italics. |
133
- | `tip` | Helpful guidance shown as a popover hint. |
134
- | `info` | Additional context shown as a popover hint. |
135
- | `caution` | Warning about constraints or potential issues, shown as a popover hint. |
136
-
137
- If no `handbook` property is provided, the tool falls back to the field's `title` and `description` properties. If neither exists, the field name is converted to title case. Fields without any description display a configurable fallback message (see `undocumentedFieldMessage` in [Configuration](#configuration)).
138
-
139
- ### Subfield Browsing
140
-
141
- Fields with nested structure display a collapsible section that editors can expand to explore subfields. Custom types extending built-in types like `image` or `file` show only the fields you add. Inherited fields are excluded automatically. Circular type references are detected and labelled rather than rendering infinitely.
162
+ | Key | Type | Purpose |
163
+ | ------------- | -------- | --------------------------------------------------------------------------- |
164
+ | `title` | `string` | Replaces the field or document title in the Handbook. |
165
+ | `description` | `string` | Replaces the description, shown under the heading. |
166
+ | `example` | `string` | An example value, shown in italics below the description. |
167
+ | `tip` | `string` | Guidance for the author, shown as a hint icon that opens on hover or click. |
168
+ | `info` | `string` | Extra context about how the field is used, shown the same way. |
169
+ | `caution` | `string` | A warning about constraints or pitfalls, shown the same way. |
142
170
 
143
- <br />
144
-
145
- ## Guide Content
146
-
147
- Handbook includes a guide authoring system powered by Portable Text. The plugin automatically registers a `handbook.handbook` singleton and a `handbook.guide` document type. No additional schema setup is required.
171
+ Every key is optional, and so is `handbook` itself. On a document, only `title` and `description` are read. Hints appear beside the field name in the order tip, info, caution, and each popover is labeled with its kind.
148
172
 
149
- The singleton defines the groups and ordering of your guides. Each group contains references to guide documents, which carry a title, an optional description shown beneath the heading, and a rich text body with built-in support for:
150
-
151
- - Paragraph, heading, and blockquote styles
152
- - Bullet and numbered lists
153
- - Bold, italic, strikethrough, and inline code formatting
154
- - Inline links
155
- - Image blocks with caption and alternative text
156
- - Video blocks with caption
157
- - Syntax-highlighted code blocks with language selection
158
- - Callout blocks with tip, information, and warning variants
159
- - Horizontal rule dividers
173
+ A field with neither a `description` nor a `handbook.description` shows a message saying it has not been documented yet. Pass `undocumentedFieldMessage` to the plugin to replace that message for every field.
160
174
 
161
- The Handbook tool displays guides in the sidebar alongside your document types. Selecting a guide opens its content in the main panel.
175
+ ```typescript
176
+ handbookPlugin({
177
+ // ...
178
+ undocumentedFieldMessage: "Not documented yet. Ask in the content channel.",
179
+ });
180
+ ```
162
181
 
163
182
  <br />
164
183
 
165
184
  <figure>
166
185
  <picture>
167
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-guide-dark.png" />
168
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-guide-light.png" />
169
- <img src="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-guide-light.png" alt="A guide displayed in the Handbook tool with headings, paragraphs, a blockquote, a bullet list, a callout, and a captioned image" />
186
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-fields-dark.png" />
187
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-fields-light.png" />
188
+ <img src="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-fields-light.png" alt="A document type's fields in the Handbook tool, each with its type, description, an example value in italics, and hint icons" />
170
189
  </picture>
171
- <p align="center"><sub><em>Editorial guides authored with Portable Text and rendered directly in the tool.</em></sub></p>
190
+ <p align="center"><sub><em>A document type's fields, each with its type, description, example, and hints.</em></sub></p>
172
191
  </figure>
173
192
 
174
193
  <br />
175
194
 
176
- ## Custom Blocks
177
-
178
- Register custom Portable Text blocks for use in guide content by providing a schema definition and a React component. Custom blocks appear as insertion options in the guide content editor and are rendered using the provided component in the Handbook viewer.
179
-
180
- ```tsx
181
- import { defineConfig, defineField, defineType } from "sanity";
182
- import type { PortableTextObject } from "sanity";
183
- import { handbookPlugin } from "@driftime/sanity-plugin-handbook";
184
- // ...
185
-
186
- const exampleSchema = defineType({
187
- name: "handbook.example",
188
- type: "object",
189
- title: "Example",
190
- fields: [
191
- defineField({
192
- name: "label",
193
- type: "string",
194
- description: "Text displayed inside the block.",
195
- }),
196
- ],
197
- });
198
-
199
- function ExampleBlock({ value }: { value: PortableTextObject & { label?: string } }) {
200
- return <p>{value.label}</p>;
201
- }
195
+ ### Nested Fields
202
196
 
203
- export default defineConfig({
204
- // ...
205
- plugins: [
206
- handbookPlugin({
207
- // ...
208
- blocks: [
209
- {
210
- schema: exampleSchema,
211
- component: ExampleBlock,
212
- },
213
- ],
214
- }),
215
- ],
216
- });
217
- ```
197
+ An object or array field has a control below its description that counts its subfields and expands to list them, each documented the way a top-level field is. An array with one member type lists that type's fields, and an array with several lists the members themselves. Sanity's own types, such as `image`, `file`, and `slug`, are not expanded, so a custom type built on one of them shows only the fields added to it. A type that contains itself is listed once, with a note referring back to the first occurrence in place of a second expansion.
218
198
 
219
199
  <br />
220
200
 
221
- ## Editor Permissions
201
+ ### Naming the Tool
222
202
 
223
- Restrict who can create and edit Handbook documents by providing a list of email addresses.
203
+ The tool is called Handbook in the Studio navigation and at the top of its sidebar. The two names are set separately, so a Studio can rename the tool without changing the sidebar heading, or give each its own wording.
224
204
 
225
205
  ```typescript
226
206
  handbookPlugin({
227
207
  // ...
228
- editors: ["admin@acme.com", "editor@acme.com"],
208
+ title: "Guide",
209
+ sidebarTitle: "Acme Inc. Guide",
229
210
  });
230
211
  ```
231
212
 
232
- When an editors list is provided, only users whose email appears in the list can create or manage Handbook documents. Users not in the list are excluded entirely. If no editors list is provided, all users have full access.
213
+ <br />
233
214
 
234
- Independently of that list, the Handbook singleton is locked for everyone: it never appears in the create-new menu, and its delete, duplicate, and unpublish actions are removed. Anyone the list permits can still create guides.
215
+ ## Guides
235
216
 
236
- The `useIsHandbookEditor` hook is exported for use in your own components if you need to conditionally render UI based on editor permissions. It reads the `editors` you gave the plugin, and takes a list as an optional argument to override that.
217
+ Guides are documents in the dataset, and the plugin registers everything they need, so no schema setup is involved. A `handbook.guide` document holds one guide, and a `handbook.handbook` singleton arranges the guides into groups and sets their order in the sidebar. The tool fetches both and fetches them again whenever either changes, so a published edit appears without a reload. Because there is only ever one singleton, it is never offered in the create menu and cannot be deleted, duplicated, or unpublished.
237
218
 
238
- ```typescript
239
- import { useIsHandbookEditor } from "@driftime/sanity-plugin-handbook";
219
+ A guide has a title, an optional description, and a Portable Text body offering:
240
220
 
241
- const isEditor = useIsHandbookEditor();
242
- ```
221
+ - Paragraphs, headings, and blockquotes
222
+ - Bullet and numbered lists
223
+ - Bold, italic, strikethrough, and inline code
224
+ - Links, to `http`, `https`, `mailto`, and `tel` addresses
225
+ - Images with a caption and alternative text
226
+ - Videos with a caption
227
+ - Code blocks with syntax highlighting for CSS, GROQ, HTML, JavaScript, JSON, React, Shell, TypeScript, and plain text
228
+ - Callouts in tip, information, and warning styles
229
+ - Horizontal rules
230
+
231
+ <br />
232
+
233
+ <figure>
234
+ <picture>
235
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-guide-dark.png" />
236
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-guide-light.png" />
237
+ <img src="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-guide-light.png" alt="A guide open in the Handbook tool, showing a heading, paragraphs, a blockquote, a list, a callout, and an image with a caption" />
238
+ </picture>
239
+ <p align="center"><sub><em>A guide in the tool, with a heading, a blockquote, a list, a callout, and a captioned image.</em></sub></p>
240
+ </figure>
243
241
 
244
242
  <br />
245
243
 
246
- ## Structure Integration
244
+ ### Adding Guides to the Structure
247
245
 
248
- The Handbook tool displays guides, but you author them in the Structure tool. Nothing appears there until you add it. Use the `handbookStructure` helper to add a Handbook singleton editor and a Handbook Guides list, both restricted to the same editors you gave the plugin.
246
+ Guides are read in the Handbook tool and written in the Structure tool, and they appear in the Structure tool only once its resolver adds them. `handbookStructure` returns the two list items for that, an editor for the singleton and a list of the guides in the order they were created. It reads the current user from the Structure context, and returns an empty array for anyone who is not an editor, so the resolver needs both of its arguments.
249
247
 
250
248
  ```typescript
251
249
  import { handbookStructure } from "@driftime/sanity-plugin-handbook";
@@ -267,89 +265,226 @@ export const structure: StructureResolver = (structureBuilder, context) => {
267
265
  };
268
266
  ```
269
267
 
270
- The helper needs two things from your structure. It reads the current user from the resolver's second argument, so accept both parameters even if the rest of your structure only needs the builder. It also returns an empty array for users outside the editors list, so only add the divider when there are items. Otherwise those users see a divider with nothing under it.
271
-
272
- The helper reads the `editors` you gave the plugin, so you don't declare them twice. Pass a list as the optional third argument to override that, which you need if your Studio runs several workspaces with different editors.
268
+ The filter keeps the two Handbook document types out of a list built from every registered type, and the divider is added only when there is something to show below it. A Studio running several workspaces with different editors passes the workspace's own list as a third argument, since the plugin otherwise reads the list from whichever workspace was configured last.
273
269
 
274
- The filter is only needed where a structure draws on `documentTypeListItems()`, which lists every registered type and so includes the two the plugin adds. A structure built from your own document definitions won't include them, so you can drop the filter.
270
+ <br />
275
271
 
276
- The Handbook singleton is where you define guide groups and their ordering. Each group contains references to individual guide documents that editors can reorder as needed.
272
+ <figure>
273
+ <picture>
274
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-groups-dark.png" />
275
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-groups-light.png" />
276
+ <img src="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-groups-light.png" alt="The Handbook singleton open in the Structure tool, showing three guide groups with reorder handles and the guides referenced in each" />
277
+ </picture>
278
+ <p align="center"><sub><em>The Handbook singleton in the Structure tool, where groups and their order are arranged.</em></sub></p>
279
+ </figure>
277
280
 
278
281
  <br />
279
282
 
280
283
  <figure>
281
284
  <picture>
282
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-groups-dark.png" />
283
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-groups-light.png" />
284
- <img src="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-groups-light.png" alt="The Handbook singleton editor in the Structure tool, showing reorderable groups each containing guide references" />
285
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-editor-dark.png" />
286
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-editor-light.png" />
287
+ <img src="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-editor-light.png" alt="The Handbook Guides list in the Structure tool beside an open guide, showing its rich text editor and toolbar" />
285
288
  </picture>
286
- <p align="center"><sub><em>The Handbook singleton where groups and their ordering are managed.</em></sub></p>
289
+ <p align="center"><sub><em>The guides list in the Structure tool beside an open guide.</em></sub></p>
287
290
  </figure>
288
291
 
289
292
  <br />
290
293
 
291
- Individual guides are authored as `handbook.guide` documents, which can be created and edited directly from the Structure tool using the built-in Portable Text editor.
294
+ ### Choosing the Editors
295
+
296
+ Everyone in the Studio can read the Handbook, and a smaller group usually writes it. The editors are the people who create guides and arrange them into groups on the singleton, and the plugin takes their email addresses.
297
+
298
+ ```typescript
299
+ handbookPlugin({
300
+ // ...
301
+ editors: ["editor@acme.com", "lead@acme.com"],
302
+ });
303
+ ```
304
+
305
+ For an editor, `handbookStructure` returns its items and the create menu offers a new guide. For everyone else, the structure items are empty and both Handbook document types are left out of the create menu. When no list is given, everyone is an editor. The list decides what the Studio shows rather than what the dataset allows, so a project's own roles remain the place where write access is granted or withheld.
306
+
307
+ The same answer is available to a Studio's own components through `useIsHandbookEditor`, which checks the current user against the plugin's list, or against a list passed to it for a Studio running several workspaces.
308
+
309
+ ```typescript
310
+ import { useIsHandbookEditor } from "@driftime/sanity-plugin-handbook";
311
+
312
+ const isEditor = useIsHandbookEditor();
313
+ const isAcmeEditor = useIsHandbookEditor(["editor@acme.com", "lead@acme.com"]);
314
+ ```
292
315
 
293
316
  <br />
294
317
 
295
- <figure>
296
- <picture>
297
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-editor-dark.png" />
298
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-editor-light.png" />
299
- <img src="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-editor-light.png" alt="The Handbook Guides list in the Structure tool with a guide open beside it, showing its title, description, and Portable Text content editor" />
300
- </picture>
301
- <p align="center"><sub><em>Handbook Guides listed in the Structure tool for authoring and editing.</em></sub></p>
302
- </figure>
318
+ ### Adding Custom Blocks
319
+
320
+ A guide body can hold custom blocks alongside the built-in ones. Each needs a schema definition for the editor and a component for the tool to render it with, and the component receives the stored block as `value`.
321
+
322
+ ```tsx
323
+ import { defineConfig, defineField, defineType } from "sanity";
324
+ import type { PortableTextObject } from "sanity";
325
+ import { handbookPlugin } from "@driftime/sanity-plugin-handbook";
326
+
327
+ const shortcutType = defineType({
328
+ name: "handbook.shortcut",
329
+ type: "object",
330
+ title: "Keyboard Shortcut",
331
+ description: "Key combination shown as it appears on the keyboard.",
332
+ fields: [
333
+ defineField({
334
+ name: "keys",
335
+ type: "string",
336
+ description: "Keys to press, separated by plus signs.",
337
+ }),
338
+ ],
339
+ preview: {
340
+ select: { title: "keys" },
341
+ },
342
+ });
343
+
344
+ function ShortcutBlock({ value }: { value: PortableTextObject }) {
345
+ const { keys } = value;
346
+
347
+ return <kbd>{typeof keys === "string" && keys}</kbd>;
348
+ }
349
+
350
+ export default defineConfig({
351
+ // ...
352
+ plugins: [
353
+ // ...
354
+ handbookPlugin({
355
+ // ...
356
+ blocks: [{ schema: shortcutType, component: ShortcutBlock }],
357
+ }),
358
+ ],
359
+ });
360
+ ```
361
+
362
+ The plugin registers the block's schema type itself, so it is not added to the Studio's own `schema.types` as well.
303
363
 
304
364
  <br />
305
365
 
306
- ## Configuration
366
+ ### Querying
367
+
368
+ Guides are written and read inside the Studio, and the tool fetches everything it shows, so a site needs no query for the Handbook at all. A site that does want to publish the guides queries the singleton, follows its references, and resolves each image and video block's asset to a URL, which is the shape the exported types describe.
369
+
370
+ ```groq
371
+ *[_type == "handbook.handbook"][0] {
372
+ ...,
373
+ groups[] {
374
+ ...,
375
+ guides[] {
376
+ ...,
377
+ ...@-> {
378
+ ...,
379
+ content[] {
380
+ ...,
381
+ _type == "handbook.image" => { ..., "asset": { "url": asset.asset->url } },
382
+ _type == "handbook.video" => { ..., "asset": { "url": asset.asset->url } }
383
+ }
384
+ }
385
+ }
386
+ }
387
+ }
388
+ ```
307
389
 
308
- Only `roles` is required, since it tells the plugin which document types to cover. Everything else falls back to a sensible default.
390
+ <br />
309
391
 
310
- | Option | Type | Default | Purpose |
311
- | -------------------------- | --------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
312
- | `title` | `string` | `"Handbook"` | Title shown in the Studio tool navigation. |
313
- | `sidebarTitle` | `string` | `"Handbook"` | Heading displayed at the top of the sidebar. |
314
- | `roles` | `SanityHandbookDocumentRole[]` | — (required) | Document roles, each shown as a labelled section in the sidebar. |
315
- | `blocks` | `SanityHandbookBlockDefinition[]` | `[]` | Custom Portable Text block definitions for guide content. |
316
- | `editors` | `string[]` | `undefined` | Email addresses permitted to edit Handbook documents. Unrestricted when omitted. |
317
- | `undocumentedFieldMessage` | `string` | `"This field has not been documented yet. Contact your development team for guidance."` | Fallback message shown when a field has no description. |
392
+ ## Reference
393
+
394
+ The rest of this document covers the shape of the stored guides and everything the package exports. Everything is imported from `@driftime/sanity-plugin-handbook`.
318
395
 
319
396
  <br />
320
397
 
321
- ## Exported Types
398
+ ### Stored Guides
399
+
400
+ `SanityHandbook` is the type of the singleton, and `SanityHandbookGuide` the type of a guide once its reference has been followed.
322
401
 
323
- These types are exported for typing your own configuration and for querying Handbook content outside the Studio.
402
+ ```typescript
403
+ import type { SanityHandbookGuide } from "@driftime/sanity-plugin-handbook";
404
+
405
+ interface GuidePageProps {
406
+ guide: SanityHandbookGuide;
407
+ }
408
+ ```
409
+
410
+ ```json
411
+ {
412
+ "_type": "handbook.handbook",
413
+ "groups": [
414
+ {
415
+ "_key": "a1b2",
416
+ "title": "Content Guidelines",
417
+ "guides": [{ "_key": "c3d4", "_type": "handbook.guide", "title": "Writing for the Web", "content": [] }]
418
+ }
419
+ ]
420
+ }
421
+ ```
324
422
 
325
- Configuration you supply:
423
+ | Field | Type | Document | Purpose |
424
+ | ------------- | ---------------------------- | ------------------- | -------------------------------------------------------------------- |
425
+ | `_type` | `string` | both | `handbook.handbook` for the singleton, `handbook.guide` for a guide. |
426
+ | `groups` | `SanityHandbookGuideGroup[]` | `handbook.handbook` | The groups in sidebar order, each with a `title` and its `guides`. |
427
+ | `title` | `string` | `handbook.guide` | The guide's title, shown in the sidebar and as its heading. |
428
+ | `description` | `string` | `handbook.guide` | An optional introduction shown below the heading. |
429
+ | `content` | `PortableTextBlock[]` | `handbook.guide` | The body, holding text blocks and the blocks below. |
430
+
431
+ The blocks a body can hold each have a type of their own, and the link annotation is stored in a text block's `markDefs`. A horizontal rule, `handbook.horizontalRule`, carries no fields.
432
+
433
+ | Field | Type | Block | Purpose |
434
+ | ---------- | ------------------------------ | ------------------ | ---------------------------------------------------------------------------- |
435
+ | `asset` | `{ url?: string }` | `handbook.image` | The image, once the query resolves its reference to a URL. |
436
+ | `caption` | `string` | `handbook.image` | An optional caption shown below the image. |
437
+ | `alt` | `string` | `handbook.image` | Optional alternative text for the image. |
438
+ | `asset` | `{ url?: string }` | `handbook.video` | The video, once the query resolves its reference to a URL. |
439
+ | `caption` | `string` | `handbook.video` | An optional caption shown below the video. |
440
+ | `code` | `string` | `handbook.code` | The source code. |
441
+ | `language` | `string` | `handbook.code` | The language it is highlighted as. |
442
+ | `variant` | `SanityHandbookCalloutVariant` | `handbook.callout` | `tip`, `info`, or `warning`. |
443
+ | `body` | `PortableTextBlock[]` | `handbook.callout` | The callout's text, with inline formatting and links but no lists or blocks. |
444
+ | `href` | `string` | `link` | The address a run of text links to. |
326
445
 
327
- | Type | Purpose |
328
- | ------------------------------- | --------------------------------------------------------------------------- |
329
- | `SanityHandbookMetadata` | The `handbook` property added to field and document definitions. |
330
- | `SanityHandbookDocumentRole` | A named role, holding the document types that fill it. |
331
- | `SanityHandbookBlockDefinition` | A custom Portable Text block, pairing a schema definition with a component. |
332
- | `SanityHandbookConfig` | The full plugin configuration accepted by `handbookPlugin`. |
446
+ <br />
447
+
448
+ ### API
449
+
450
+ | Export | Purpose |
451
+ | -------------------------------------------------------- | -------------------------------------------------------------- |
452
+ | `handbookPlugin(config)` | Registers the tool, its document types, and its blocks. |
453
+ | `handbookStructure(structureBuilder, context, editors?)` | Returns Structure items for the singleton and the guides list. |
454
+ | `useIsHandbookEditor(editors?)` | Whether the current user can edit guides. |
455
+
456
+ <br />
457
+
458
+ ### Types
459
+
460
+ | Type | Purpose |
461
+ | ------------------------------- | --------------------------------------------------------------- |
462
+ | `SanityHandbookConfig` | Everything `handbookPlugin` accepts. |
463
+ | `SanityHandbookMetadata` | The `handbook` property on field and document definitions. |
464
+ | `SanityHandbookDocumentRole` | A role, with its title, description, and document definitions. |
465
+ | `SanityHandbookBlockDefinition` | A custom block, as a schema definition paired with a component. |
466
+ | `SanityHandbook` | The singleton document. |
467
+ | `SanityHandbookGuideGroup` | A group in the singleton, with its ordered guides. |
468
+ | `SanityHandbookGuide` | A guide document, with its title, description, and body. |
469
+ | `SanityHandbookImage` | An image block. |
470
+ | `SanityHandbookVideo` | A video block. |
471
+ | `SanityHandbookCode` | A code block. |
472
+ | `SanityHandbookCallout` | A callout block. |
473
+ | `SanityHandbookCalloutVariant` | `"tip"`, `"info"`, or `"warning"`. |
474
+ | `SanityHandbookHorizontalRule` | A horizontal rule block. |
475
+ | `SanityHandbookLink` | The link annotation. |
476
+
477
+ <br />
333
478
 
334
- Content the dataset stores:
479
+ ## License
335
480
 
336
- | Type | Purpose |
337
- | ------------------------------ | ------------------------------------------------------------------------- |
338
- | `SanityHandbook` | The Handbook singleton document, defining the groups and their ordering. |
339
- | `SanityHandbookGuideGroup` | A group within the Handbook singleton, holding an ordered list of guides. |
340
- | `SanityHandbookGuide` | A guide document, with its title, description, and Portable Text content. |
341
- | `SanityHandbookCallout` | A callout block, with its variant and Portable Text body. |
342
- | `SanityHandbookCode` | A code block, with its source and language. |
343
- | `SanityHandbookHorizontalRule` | A horizontal rule block. |
344
- | `SanityHandbookImage` | An image block, with its asset, caption, and alternative text. |
345
- | `SanityHandbookVideo` | A video block, with its asset and caption. |
346
- | `SanityHandbookLink` | A link annotation, with the web address it points to. |
481
+ MIT © [Driftime®](https://driftime.com). See [LICENSE](https://github.com/driftime/sanity-plugins/blob/main/LICENSE).
347
482
 
348
483
  <br />
349
484
 
350
485
  ## Acknowledgements
351
486
 
352
- The icons in `src/icons` are derived from [Lucide](https://lucide.dev) and redrawn as standalone components, so the plugin ships its own icons without depending on the Lucide package. Icons for controls the Studio already draws, such as disclosure arrows, come from `@sanity/icons` so they match their surroundings. Lucide is distributed under the [ISC License](https://github.com/lucide-icons/lucide/blob/main/LICENSE).
487
+ Icons for the Studio's own controls come from `@sanity/icons`, so they match the rest of the Studio. The plugin's own icons are based on [Lucide](https://lucide.dev), distributed under the [ISC License](https://github.com/lucide-icons/lucide/blob/main/LICENSE).
353
488
 
354
489
  <br />
355
490
  <br />
@@ -357,7 +492,7 @@ The icons in `src/icons` are derived from [Lucide](https://lucide.dev) and redra
357
492
  <div align="center">
358
493
  <p><strong>Built alongside <a href="https://cairn.driftime.com">Cairn</a>, a starting point for responsible web experiences.</strong></p>
359
494
  <p>Part of a suite of Sanity Studio plugins by Driftime®</p>
360
- <p><a href="https://github.com/driftime/sanity-plugin-handbook">Handbook</a> · <a href="https://github.com/driftime/sanity-plugin-icon">Icon</a></p>
495
+ <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>
361
496
  </div>
362
497
 
363
498
  <br />