@driftime/sanity-plugin-handbook 0.3.1 → 0.4.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,52 +1,69 @@
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 icon" 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
- <h1>Handbook — A Sanity Plugin</h1>
7
+ <h1>Handbook</h1>
8
+ <p><strong>A Sanity Studio plugin by Driftime®</strong></p>
8
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>
9
14
  </div>
10
15
 
11
16
  <br />
12
17
 
13
18
  ## Overview
14
19
 
15
- 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 cannot hold: what a field is for, which values work best, and how the team writes. Handbook gives that knowledge a place in the Studio, as a tool in the navigation, so an author reaches the guidance beside the content it describes.
21
+
22
+ The first part of the Handbook is generated from the schema. Every document type is listed with its fields, and each field shows its type, its description, an example value, and any tips or cautions written alongside its definition, so the documentation stays in step with the editor it describes. The second part is written by the team. Guides are Portable Text documents for house style, editorial process, and anything too long for a field description, arranged into groups in the same sidebar.
16
23
 
17
24
  <br />
18
25
 
19
26
  <figure>
20
27
  <picture>
21
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-fields-dark.png" />
22
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-fields-light.png" />
23
- <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" />
28
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-how-to-use-dark.png" />
29
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-how-to-use-light.png" />
30
+ <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" />
24
31
  </picture>
25
- <p align="center"><sub><em>Field documentation generated automatically from your schema definitions.</em></sub></p>
32
+ <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>
26
33
  </figure>
27
34
 
28
35
  <br />
29
36
 
30
37
  ## Installation
31
38
 
32
- 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.
39
+ 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 22.12 or later is required.
33
40
 
34
41
  ```bash
35
42
  bun add -E @driftime/sanity-plugin-handbook
36
43
  ```
37
44
 
38
- ## Basic Setup
45
+ <br />
46
+
47
+ ## Documentation
48
+
49
+ 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.
50
+
51
+ <br />
52
+
53
+ ### Registering the Plugin
39
54
 
40
- 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
+ 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.
41
56
 
42
57
  ```typescript
43
58
  import { defineConfig } from "sanity";
44
59
  import { handbookPlugin } from "@driftime/sanity-plugin-handbook";
45
- // ...
60
+
61
+ import { home, navigation, page, settings, templates } from "@/schemas";
46
62
 
47
63
  export default defineConfig({
48
64
  // ...
49
65
  plugins: [
66
+ // ...
50
67
  handbookPlugin({
51
68
  roles: [
52
69
  {
@@ -70,35 +87,45 @@ export default defineConfig({
70
87
  });
71
88
  ```
72
89
 
73
- 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.
90
+ | Option | Type | Default | Purpose |
91
+ | -------------------------- | --------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------- |
92
+ | `title` | `string` | `"Handbook"` | The tool's name in the Studio navigation. See [Naming the Tool](#naming-the-tool). |
93
+ | `sidebarTitle` | `string` | `"Handbook"` | The heading at the top of the sidebar. See [Naming the Tool](#naming-the-tool). |
94
+ | `roles` | `SanityHandbookDocumentRole[]` | required | The document types to document, grouped by role, in the order listed. |
95
+ | `blocks` | `SanityHandbookBlockDefinition[]` | none | Blocks added to guide bodies. See [Adding Custom Blocks](#adding-custom-blocks). |
96
+ | `editors` | `string[]` | everyone | Email addresses of the people who write guides. See [Choosing the Editors](#choosing-the-editors). |
97
+ | `undocumentedFieldMessage` | `string` | a prompt to ask the team | Shown for a field with no description. See [Documenting Fields](#documenting-fields). |
98
+
99
+ 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.
100
+
101
+ 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.
74
102
 
75
103
  <br />
76
104
 
77
105
  <figure>
78
106
  <picture>
79
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-document-types-dark.png" />
80
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-document-types-light.png" />
81
- <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" />
107
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-document-types-dark.png" />
108
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-document-types-light.png" />
109
+ <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" />
82
110
  </picture>
83
- <p align="center"><sub><em>Document types organised by role, browsable from the sidebar.</em></sub></p>
111
+ <p align="center"><sub><em>The Document Types overview, listing each role with its description and the types it holds.</em></sub></p>
84
112
  </figure>
85
113
 
86
114
  <br />
87
115
 
88
- ## Handbook Metadata
116
+ ### Documenting Fields
89
117
 
90
- The plugin augments Sanity's `FieldDefinitionBase` and `DocumentDefinition` interfaces, adding a `handbook` property that TypeScript recognises automatically.
118
+ 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.
91
119
 
92
120
  ```typescript
93
121
  import { defineField, defineType } from "sanity";
94
122
 
95
- export const pageType = defineType({
123
+ export const page = defineType({
96
124
  name: "page",
97
125
  type: "document",
98
126
  title: "Pages",
99
127
  description: "General-purpose pages.",
100
128
  handbook: {
101
- title: "Page",
102
129
  description:
103
130
  "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.",
104
131
  },
@@ -109,134 +136,112 @@ export const pageType = defineType({
109
136
  description: "Appears in navigation, browser tabs, and search results.",
110
137
  handbook: {
111
138
  description:
112
- "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.",
139
+ "The name this document goes by wherever it is referred to rather than read — navigation menus, browser tabs, search results, and the Studio's own lists. Every document needs one, and it stands behind both the slug and the SEO title whenever those are left blank.",
113
140
  example: "About Acme Inc.",
114
- tip: "Keep it short enough to sit comfortably in a navigation menu, and put any fuller opening line in the heading instead.",
115
- info: "The slug is generated from the title once, so renaming a page later leaves its address unchanged.",
116
- caution: "Required. Publishing is blocked until it is filled in.",
141
+ tip: "Keep it short enough to sit comfortably in a navigation menu.",
142
+ },
143
+ }),
144
+ defineField({
145
+ name: "slug",
146
+ type: "slug",
147
+ description: "Last part of the page's web address.",
148
+ options: { source: "title" },
149
+ handbook: {
150
+ description:
151
+ "The last part of the page's web address. Generate it from the title and then edit it if the result reads awkwardly — shorter is almost always better.",
152
+ example: "about-acme-inc",
153
+ caution: "Changing the slug of a published page breaks every link that already points at it.",
117
154
  },
118
155
  }),
119
156
  ],
120
157
  });
121
158
  ```
122
159
 
123
- The `handbook` property supports the following keys:
124
-
125
- | Key | Purpose |
126
- | ------------- | ----------------------------------------------------------------------- |
127
- | `title` | Display title override for the field or document. |
128
- | `description` | Description shown beneath the field or document heading. |
129
- | `example` | Illustrative example value, displayed in italics. |
130
- | `tip` | Helpful guidance shown as a popover hint. |
131
- | `info` | Additional context shown as a popover hint. |
132
- | `caution` | Warning about constraints or potential issues, shown as a popover hint. |
133
-
134
- 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)).
135
-
136
- ### Subfield Browsing
137
-
138
- 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.
160
+ | Key | Type | Purpose |
161
+ | ------------- | -------- | --------------------------------------------------------------------------- |
162
+ | `title` | `string` | Replaces the field or document title in the Handbook. |
163
+ | `description` | `string` | Replaces the description, shown under the heading. |
164
+ | `example` | `string` | An example value, shown in italics beneath the description. |
165
+ | `tip` | `string` | Guidance for the author, shown as a hint icon that opens on hover or click. |
166
+ | `info` | `string` | Extra context about how the field is used, shown the same way. |
167
+ | `caution` | `string` | A warning about constraints or pitfalls, shown the same way. |
139
168
 
140
- ## Guide Content
169
+ 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.
141
170
 
142
- 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
+ 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.
143
172
 
144
- 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:
145
-
146
- - Paragraph, heading, and blockquote styles
147
- - Bullet and numbered lists
148
- - Bold, italic, strikethrough, and inline code formatting
149
- - Inline links
150
- - Image blocks with caption and alternative text
151
- - Video blocks with caption
152
- - Syntax-highlighted code blocks with language selection
153
- - Callout blocks with tip, information, and warning variants
154
- - Horizontal rule dividers
155
-
156
- The Handbook tool displays guides in the sidebar alongside your document types. Selecting a guide opens its content in the main panel.
173
+ ```typescript
174
+ handbookPlugin({
175
+ // ...
176
+ undocumentedFieldMessage: "Not documented yet. Ask in the content channel.",
177
+ });
178
+ ```
157
179
 
158
180
  <br />
159
181
 
160
182
  <figure>
161
183
  <picture>
162
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-guide-dark.png" />
163
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-guide-light.png" />
164
- <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" />
184
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-fields-dark.png" />
185
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-fields-light.png" />
186
+ <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" />
165
187
  </picture>
166
- <p align="center"><sub><em>Editorial guides authored with Portable Text and rendered directly in the tool.</em></sub></p>
188
+ <p align="center"><sub><em>A document type's fields, each with its type, description, example, and hints.</em></sub></p>
167
189
  </figure>
168
190
 
169
191
  <br />
170
192
 
171
- ## Custom Blocks
193
+ ### Nested Fields
172
194
 
173
- 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.
195
+ An object or array field has a control beneath 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.
174
196
 
175
- ```tsx
176
- import { defineConfig, defineField, defineType } from "sanity";
177
- import type { PortableTextObject } from "sanity";
178
- import { handbookPlugin } from "@driftime/sanity-plugin-handbook";
179
- // ...
197
+ <br />
180
198
 
181
- const exampleSchema = defineType({
182
- name: "handbook.example",
183
- type: "object",
184
- title: "Example",
185
- fields: [
186
- defineField({
187
- name: "label",
188
- type: "string",
189
- description: "Text displayed inside the block.",
190
- }),
191
- ],
192
- });
199
+ ### Naming the Tool
193
200
 
194
- function ExampleBlock({ value }: { value: PortableTextObject & { label?: string } }) {
195
- return <p>{value.label}</p>;
196
- }
201
+ 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.
197
202
 
198
- export default defineConfig({
203
+ ```typescript
204
+ handbookPlugin({
199
205
  // ...
200
- plugins: [
201
- handbookPlugin({
202
- // ...
203
- blocks: [
204
- {
205
- schema: exampleSchema,
206
- component: ExampleBlock,
207
- },
208
- ],
209
- }),
210
- ],
206
+ title: "Guide",
207
+ sidebarTitle: "Acme Inc. Guide",
211
208
  });
212
209
  ```
213
210
 
214
- ## Editor Permissions
211
+ <br />
215
212
 
216
- Restrict who can create and edit Handbook documents by providing a list of email addresses.
213
+ ## Guides
217
214
 
218
- ```typescript
219
- handbookPlugin({
220
- // ...
221
- editors: ["admin@acme.com", "editor@acme.com"],
222
- });
223
- ```
215
+ 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.
224
216
 
225
- 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.
217
+ A guide has a title, an optional description, and a Portable Text body offering:
226
218
 
227
- 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.
219
+ - Paragraphs, headings, and blockquotes
220
+ - Bullet and numbered lists
221
+ - Bold, italic, strikethrough, and inline code
222
+ - Links, to `http`, `https`, `mailto`, and `tel` addresses
223
+ - Images with a caption and alternative text
224
+ - Videos with a caption
225
+ - Code blocks with syntax highlighting for CSS, GROQ, HTML, JavaScript, JSON, React, Shell, TypeScript, and plain text
226
+ - Callouts in tip, information, and warning styles
227
+ - Horizontal rules
228
228
 
229
- 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.
229
+ <br />
230
230
 
231
- ```typescript
232
- import { useIsHandbookEditor } from "@driftime/sanity-plugin-handbook";
231
+ <figure>
232
+ <picture>
233
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-guide-dark.png" />
234
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-guide-light.png" />
235
+ <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" />
236
+ </picture>
237
+ <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>
238
+ </figure>
233
239
 
234
- const isEditor = useIsHandbookEditor();
235
- ```
240
+ <br />
236
241
 
237
- ## Structure Integration
242
+ ### Adding Guides to the Structure
238
243
 
239
- 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.
244
+ 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. 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.
240
245
 
241
246
  ```typescript
242
247
  import { handbookStructure } from "@driftime/sanity-plugin-handbook";
@@ -258,91 +263,240 @@ export const structure: StructureResolver = (structureBuilder, context) => {
258
263
  };
259
264
  ```
260
265
 
261
- 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.
262
-
263
- 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.
266
+ 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 put beneath 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.
264
267
 
265
- 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.
268
+ <br />
266
269
 
267
- 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.
270
+ <figure>
271
+ <picture>
272
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-groups-dark.png" />
273
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-groups-light.png" />
274
+ <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 two guide groups with reorder handles and the guides referenced in each" />
275
+ </picture>
276
+ <p align="center"><sub><em>The Handbook singleton in the Structure tool, where groups and their order are arranged.</em></sub></p>
277
+ </figure>
268
278
 
269
279
  <br />
270
280
 
271
281
  <figure>
272
282
  <picture>
273
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-groups-dark.png" />
274
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-groups-light.png" />
275
- <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" />
283
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-editor-dark.png" />
284
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/handbook/assets/handbook-editor-light.png" />
285
+ <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 title, description, and rich text editor" />
276
286
  </picture>
277
- <p align="center"><sub><em>The Handbook singleton where groups and their ordering are managed.</em></sub></p>
287
+ <p align="center"><sub><em>The guides list in the Structure tool beside an open guide.</em></sub></p>
278
288
  </figure>
279
289
 
280
290
  <br />
281
291
 
282
- 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.
292
+ ### Choosing the Editors
293
+
294
+ 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.
295
+
296
+ ```typescript
297
+ handbookPlugin({
298
+ // ...
299
+ editors: ["editor@acme.com", "lead@acme.com"],
300
+ });
301
+ ```
302
+
303
+ 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.
304
+
305
+ 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.
306
+
307
+ ```typescript
308
+ import { useIsHandbookEditor } from "@driftime/sanity-plugin-handbook";
309
+
310
+ const isEditor = useIsHandbookEditor();
311
+ const isAcmeEditor = useIsHandbookEditor(["editor@acme.com", "lead@acme.com"]);
312
+ ```
283
313
 
284
314
  <br />
285
315
 
286
- <figure>
287
- <picture>
288
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-editor-dark.png" />
289
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugin-handbook/HEAD/.github/assets/handbook-editor-light.png" />
290
- <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" />
291
- </picture>
292
- <p align="center"><sub><em>Handbook Guides listed in the Structure tool for authoring and editing.</em></sub></p>
293
- </figure>
316
+ ### Adding Custom Blocks
317
+
318
+ 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`.
319
+
320
+ ```tsx
321
+ import { defineConfig, defineField, defineType } from "sanity";
322
+ import type { PortableTextObject } from "sanity";
323
+ import { handbookPlugin } from "@driftime/sanity-plugin-handbook";
324
+
325
+ const shortcutType = defineType({
326
+ name: "handbook.shortcut",
327
+ type: "object",
328
+ title: "Keyboard Shortcut",
329
+ description: "Key combination shown as it appears on the keyboard.",
330
+ fields: [
331
+ defineField({
332
+ name: "keys",
333
+ type: "string",
334
+ description: "Keys to press, separated by plus signs.",
335
+ }),
336
+ ],
337
+ preview: {
338
+ select: { title: "keys" },
339
+ },
340
+ });
341
+
342
+ function ShortcutBlock({ value }: { value: PortableTextObject }) {
343
+ const { keys } = value;
344
+
345
+ return <kbd>{typeof keys === "string" && keys}</kbd>;
346
+ }
347
+
348
+ export default defineConfig({
349
+ // ...
350
+ plugins: [
351
+ // ...
352
+ handbookPlugin({
353
+ // ...
354
+ blocks: [{ schema: shortcutType, component: ShortcutBlock }],
355
+ }),
356
+ ],
357
+ });
358
+ ```
359
+
360
+ The plugin registers the block's schema type itself, so it is not added to the Studio's own `schema.types` as well.
294
361
 
295
362
  <br />
296
363
 
297
- ## Configuration
364
+ ### Querying
365
+
366
+ 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.
367
+
368
+ ```groq
369
+ *[_type == "handbook.handbook"][0] {
370
+ ...,
371
+ groups[] {
372
+ ...,
373
+ guides[] {
374
+ ...,
375
+ ...@-> {
376
+ ...,
377
+ content[] {
378
+ ...,
379
+ _type == "handbook.image" => { ..., "asset": { "url": asset.asset->url } },
380
+ _type == "handbook.video" => { ..., "asset": { "url": asset.asset->url } }
381
+ }
382
+ }
383
+ }
384
+ }
385
+ }
386
+ ```
298
387
 
299
- Only `roles` is required, since it tells the plugin which document types to cover. Everything else falls back to a sensible default.
388
+ <br />
300
389
 
301
- | Option | Type | Default | Purpose |
302
- | -------------------------- | --------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
303
- | `title` | `string` | `"Handbook"` | Title shown in the Studio tool navigation. |
304
- | `sidebarTitle` | `string` | `"Handbook"` | Heading displayed at the top of the sidebar. |
305
- | `roles` | `SanityHandbookDocumentRole[]` | — (required) | Document roles, each shown as a labelled section in the sidebar. |
306
- | `blocks` | `SanityHandbookBlockDefinition[]` | `[]` | Custom Portable Text block definitions for guide content. |
307
- | `editors` | `string[]` | `undefined` | Email addresses permitted to edit Handbook documents. Unrestricted when omitted. |
308
- | `undocumentedFieldMessage` | `string` | `"This field has not been documented yet. Contact your development team for guidance."` | Fallback message shown when a field has no description. |
390
+ ## Reference
309
391
 
310
- ## Exported Types
392
+ 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`.
311
393
 
312
- These types are exported for typing your own configuration and for querying Handbook content outside the Studio.
394
+ <br />
313
395
 
314
- Configuration you supply:
396
+ ### Stored Guides
315
397
 
316
- | Type | Purpose |
317
- | ------------------------------- | --------------------------------------------------------------------------- |
318
- | `SanityHandbookMetadata` | The `handbook` property added to field and document definitions. |
319
- | `SanityHandbookDocumentRole` | A named role, holding the document types that fill it. |
320
- | `SanityHandbookBlockDefinition` | A custom Portable Text block, pairing a schema definition with a component. |
321
- | `SanityHandbookConfig` | The full plugin configuration accepted by `handbookPlugin`. |
398
+ `SanityHandbook` is the type of the singleton, and `SanityHandbookGuide` the type of a guide once its reference has been followed.
322
399
 
323
- Content the dataset stores:
400
+ ```typescript
401
+ import type { SanityHandbookGuide } from "@driftime/sanity-plugin-handbook";
402
+
403
+ interface GuidePageProps {
404
+ guide: SanityHandbookGuide;
405
+ }
406
+ ```
407
+
408
+ ```json
409
+ {
410
+ "_type": "handbook.handbook",
411
+ "groups": [
412
+ {
413
+ "_key": "a1b2",
414
+ "title": "Content Guidelines",
415
+ "guides": [{ "_key": "c3d4", "_type": "handbook.guide", "title": "Writing for the Web", "content": [] }]
416
+ }
417
+ ]
418
+ }
419
+ ```
324
420
 
325
- | Type | Purpose |
326
- | ------------------------------ | ------------------------------------------------------------------------- |
327
- | `SanityHandbook` | The Handbook singleton document, defining the groups and their ordering. |
328
- | `SanityHandbookGuideGroup` | A group within the Handbook singleton, holding an ordered list of guides. |
329
- | `SanityHandbookGuide` | A guide document, with its title, description, and Portable Text content. |
330
- | `SanityHandbookCallout` | A callout block, with its variant and Portable Text body. |
331
- | `SanityHandbookCode` | A code block, with its source and language. |
332
- | `SanityHandbookHorizontalRule` | A horizontal rule block. |
333
- | `SanityHandbookImage` | An image block, with its asset, caption, and alternative text. |
334
- | `SanityHandbookVideo` | A video block, with its asset and caption. |
335
- | `SanityHandbookLink` | A link annotation, with the web address it points to. |
421
+ | Field | Type | Document | Purpose |
422
+ | ------------- | ---------------------------- | ------------------- | -------------------------------------------------------------------- |
423
+ | `_type` | `string` | both | `handbook.handbook` for the singleton, `handbook.guide` for a guide. |
424
+ | `groups` | `SanityHandbookGuideGroup[]` | `handbook.handbook` | The groups in sidebar order, each with a `title` and its `guides`. |
425
+ | `title` | `string` | `handbook.guide` | The guide's title, shown in the sidebar and as its heading. |
426
+ | `description` | `string` | `handbook.guide` | An optional introduction shown beneath the heading. |
427
+ | `content` | `PortableTextBlock[]` | `handbook.guide` | The body, holding text blocks and the blocks below. |
428
+
429
+ 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.
430
+
431
+ | Field | Type | Block | Purpose |
432
+ | ---------- | ------------------------------ | ------------------ | ---------------------------------------------------------------------------- |
433
+ | `asset` | `{ url?: string }` | `handbook.image` | The image, once the query resolves its reference to a URL. |
434
+ | `caption` | `string` | `handbook.image` | An optional caption shown beneath the image. |
435
+ | `alt` | `string` | `handbook.image` | Optional alternative text for the image. |
436
+ | `asset` | `{ url?: string }` | `handbook.video` | The video, once the query resolves its reference to a URL. |
437
+ | `caption` | `string` | `handbook.video` | An optional caption shown beneath the video. |
438
+ | `code` | `string` | `handbook.code` | The source code. |
439
+ | `language` | `string` | `handbook.code` | The language it is highlighted as. |
440
+ | `variant` | `SanityHandbookCalloutVariant` | `handbook.callout` | `tip`, `info`, or `warning`. |
441
+ | `body` | `PortableTextBlock[]` | `handbook.callout` | The callout's text, with inline formatting and links but no lists or blocks. |
442
+ | `href` | `string` | `link` | The address a run of text links to. |
443
+
444
+ <br />
445
+
446
+ ### API
447
+
448
+ | Export | Purpose |
449
+ | -------------------------------------------------------- | -------------------------------------------------------------- |
450
+ | `handbookPlugin(config)` | Registers the tool, its document types, and its blocks. |
451
+ | `handbookStructure(structureBuilder, context, editors?)` | Returns Structure items for the singleton and the guides list. |
452
+ | `useIsHandbookEditor(editors?)` | Whether the current user can edit guides. |
453
+
454
+ <br />
455
+
456
+ ### Types
457
+
458
+ | Type | Purpose |
459
+ | ------------------------------- | --------------------------------------------------------------- |
460
+ | `SanityHandbookConfig` | Everything `handbookPlugin` accepts. |
461
+ | `SanityHandbookMetadata` | The `handbook` property on field and document definitions. |
462
+ | `SanityHandbookDocumentRole` | A role, with its title, description, and document definitions. |
463
+ | `SanityHandbookBlockDefinition` | A custom block, as a schema definition paired with a component. |
464
+ | `SanityHandbook` | The singleton document. |
465
+ | `SanityHandbookGuideGroup` | A group in the singleton, with its ordered guides. |
466
+ | `SanityHandbookGuide` | A guide document, with its title, description, and body. |
467
+ | `SanityHandbookImage` | An image block. |
468
+ | `SanityHandbookVideo` | A video block. |
469
+ | `SanityHandbookCode` | A code block. |
470
+ | `SanityHandbookCallout` | A callout block. |
471
+ | `SanityHandbookCalloutVariant` | `"tip"`, `"info"`, or `"warning"`. |
472
+ | `SanityHandbookHorizontalRule` | A horizontal rule block. |
473
+ | `SanityHandbookLink` | The link annotation. |
474
+
475
+ <br />
476
+
477
+ ## License
478
+
479
+ MIT © [Driftime®](https://driftime.com). See [LICENSE](https://github.com/driftime/sanity-plugins/blob/main/LICENSE).
480
+
481
+ <br />
336
482
 
337
483
  ## Acknowledgements
338
484
 
339
- 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).
485
+ 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).
340
486
 
341
487
  <br />
342
488
  <br />
343
489
 
344
490
  <div align="center">
345
- <a href="https://driftime.com" target="_blank">
491
+ <p><strong>Built alongside <a href="https://cairn.driftime.com">Cairn</a>, a starting point for responsible web experiences.</strong></p>
492
+ <p>Part of a suite of Sanity Studio plugins by Driftime®</p>
493
+ <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>
494
+ </div>
495
+
496
+ <br />
497
+
498
+ <div align="center">
499
+ <a href="https://driftime.com">
346
500
  <picture>
347
501
  <source media="(prefers-color-scheme: dark)" srcset="https://driftime.com/driftime-github-logo-dark.svg" />
348
502
  <source media="(prefers-color-scheme: light)" srcset="https://driftime.com/driftime-github-logo.svg" />