npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@droplab/sanity-tools

v0.3.6

Published

Reusable Sanity schema helpers for common block and content patterns.

Readme

Sanity Tools

@droplab/sanity-tools

Reusable Sanity schema helpers for common block/content patterns. NOTE: Under heavy development as we are road testing this in a current project. Check back often our release cycles are fast at the moment!

Install (consumers)

pnpm add @droplab/sanity-tools

(Use npm install @droplab/sanity-tools or yarn add @droplab/sanity-tools if you do not use pnpm.)

Required peers: sanity, @sanity/ui, react, react-dom — install them in your Studio if they are not already present.

Optional peers (install when you use the related feature):

| Package | Used by | | --- | --- | | @sanity/color-input | BrandColorInput — inject Settings → Brand colours into type: 'color' fields | | @sanity/code-input | json field type in block_customData() / card Additional fields (jsonValue code editor) | | @sanity/orderable-document-list | helper_orderableList / singletons.orderableList() | | sanity-plugin-stl-table | block_table | | react-icons | getHelperIcon (falls back gracefully when omitted) |

Import schemas, types, and GROQ fragments from the package root:

import
{
	document_page,
	block_richText,
	block_richText_query,
	type block_hero,
} from '@droplab/sanity-tools'
import { groq } from 'next-sanity'
import { page_builder_query } from '@droplab/sanity-tools/groq'

export const PAGE_QUERY = groq`
  *[_type == "page" && _id == $id][0] {
    _id,
    title,
    "blocks": blocks[] { ${page_builder_query} }
  }
`

Each fragment expands references (->) and drops conditionally hidden / stale fields with defined() and inline condition => { ... } projections. See groq/README.md. Prefer @droplab/sanity-tools/groq for queries and @droplab/sanity-tools/utils for resolver helpers in Next.js App Router so Studio input modules are not pulled into the RSC graph; the package root re-exports the same symbols.

The published package exposes three entry points: @droplab/sanity-tools (Studio), @droplab/sanity-tools/groq, and @droplab/sanity-tools/utils (package.json exports). Studio inputs and other modules under dist/ are implementation details for those entries — they are not a public import path.

Shared field knobs (name, title, required, hidden, readOnly, description, group) live on BaseFieldOptions. Prefer an explicit name in production — omitting it auto-generates {prefix}_{uuid}, which is unstable across rebuilds.

Nested blocks inside block_section should pass configOptions: NESTED_IN_SECTION_CONFIG so only Enabled, List label, and ID appear (theme/layout stay on the section).


Catalog

Documents

| Helper | Default _type | What it is | | --- | --- | --- | | document_announcement | announcement | Sitewide announcement singleton (enabled, message, link) | | document_company | company | Sitewide company singleton (name, contact, logos, offices) | | document_settings | settings | Sitewide singleton (site, brand, SEO, navigation, links, taxonomy) | | document_page | page | Page with a page-builder array | | document_person | person | Person / author | | document_client | client | Client / brand with logo marks | | document_article | article | Article with rich text | | document_news | news | News item (internal body or external URL) |

Shared field factories used by those documents: documentFieldGroups, documentTitleAndSlugFields, documentTagsField, documentVariantsField, documentAuthorField, documentPublishDateField, documentListLabelField, documentEnabledField, documentEnvironmentField, documentVisibilityField.

Blocks

| Helper | Role | | --- | --- | | block_actionFields | Action type picker + payload (spread into fields) | | block_card | Card: variant, copy, image, background media, CTA, additional fields | | block_carousel | Carousel of image / video / card slides | | block_config | Enabled, list label, ID, theme, layout | | block_customData | Dynamic typed fields (richText, json, …) for one-off components | | block_grid | Column grid of image / video / card cells | | block_hero | Hero: tagline, blurb, CTA, background | | block_image | Image object (optional Settings tab) | | block_link | Internal / external / action link | | block_references | Array of document references | | block_richText | Portable Text with optional embeds | | block_section | Section header, background, and nested page-builder blocks | | block_spacer | Vertical spacing | | block_table | STL table object + Settings | | block_tableField | Thin stl_table field | | block_variantFields | Variant picker (spread into fields) | | block_video | Single- or multi-provider video |

Related: block_linkArrayMember, block_linkType, block_linkAnnotation, block_imagePortableMember, block_videoField, block_videoPortableMember, block_videoType, block_richTextType, block_gridPortableMember, block_tablePortableMember.

Helpers

| Helper | Role | | --- | --- | | brandColorPairingsChooser | Pick a Brand Color Pairing by pairingId | | helper_seo | Title, description, OG image, optional keywords | | helper_phone | Country code + number object | | helper_singletons | Structure pins + create/delete guards | | helper_orderableList | Searchable orderable desk lists (singleton-aware via singletons.orderableList) | | getHelperIcon | Studio list icons used by the helpers |

Validation: noDuplicates.


document_company

document_company() returns a document type for the sitewide company profile — it is not a page-builder block. Register it in schema.types and pin it as a singleton (document id company). Place it in structure directly above Settings.

Fields: companyName, companyEmail, companyPhone, companyLogo, companyLogoAlt, offices. Each office is { address, suite, city, state, postalCode, country, email, phone }address and city are required; per-office email/phone override company-level values.

Options (DocumentCompanyOptions): _type (default company), title (default Company), description, additionalFields.

Pin with helper_singletons. Fetch with document_company_query from @droplab/sanity-tools/groq.

Usage

import { document_company, document_settings, helper_singletons } from '@droplab/sanity-tools';

const singletons = helper_singletons();

// schema.types
document_company(),
document_settings({ referenceTypes: ['page', 'post'] }),

// structure — Company above Settings
singletons.documentByType(S, 'company'),
singletons.documentByType(S, 'settings'),

Fetch company (GROQ)

import { groq } from 'next-sanity';
import { document_company_query } from '@droplab/sanity-tools/groq';

export const COMPANY_QUERY = groq`
	*[_type == "company"][0]
	{
		${document_company_query}
	}
`;

document_settings

document_settings() returns a document type for sitewide global settings — it is not a page-builder block, so it goes straight into schema.types rather than into a fields array. Fields are split across six tabs and Studio's synthetic All fields tab is hidden.

| Tab | Fields | | --- | --- | | Site (default) | siteTitle, siteLogo, siteFavicons, copyright | | Brand | brandColors, brandColorPairings | | SEO | isSearchable (default true), seo (a helper_seo object), llmsTxt | | Navigation | mainNavigation, footerNavigation, redirects (Redirects fieldset) | | Links | socialLinks, generalLinks, legalLinks | | Taxonomy | variants, tags, actions |

The SEO tab reuses helper_seo rather than redefining meta fields, so sitewide defaults and per-document SEO share one shape. Sitewide isSearchable (default true) sits at the top of the tab, and seo nests seo.title, seo.description, and the OG-validated seo.image. llmsTxt holds plain-text content for /llms.txt. Pass seoOptions to configure the nested object — for example { withKeywords: true } to add seo.keywords, or { imageRequired: true } to require the share image. withIsSearchable is forced off there so the flag is never duplicated.

Navigation stores mainNavigation, footerNavigation, and redirects at the root of the settings document (not nested under a navigation object). Each nav array accepts either a direct block_link or a Link Menu (label + nested links). document_settings_query projects those same root keys; empty footer navigation falls back to main. Announcement (enabled, message, link) lives on the announcement singleton — fetch with document_announcement_query. Links (socialLinks, generalLinks, legalLinks) are flat block_link lists. All of them use block_linkArrayMember, so referenceTypes controls which documents editors may target for internal links.

Brand Colors (brandColors) is an array of { name, colorId, color }colorId is a slug generated from the name, and color accepts hex (#RGB / #RRGGBB) or rgb() / rgba() strings. Items use BrandColorItemInput (live preview swatch beside the value). Use BrandColorInput on any type: 'color' field to inject those colours into @sanity/color-input's colorList. Use brandColorPairingsChooser() (or BrandColorPairingsChooserInput) on string fields that should pick a Brand Color Pairing by pairingIdblock_card bgColor, block_hero backgroundColor, and block_section backgroundColor do this when withBackgroundColor is enabled.

Brand Color Pairings (brandColorPairings) is an array of { pairingId, backgroundColor, foregroundColor } where colour values are Brand Color colorId slugs and pairingId is a required unique string for deterministic frontend lookups (e.g. brandColorPairings[pairingId == "lilac-x-charcoal"][0]). Items use BrandColorPairingInput with BrandColorPairingPreview in list/collapsed views. When empty, Pairing ID is seeded to {background}-x-{foreground} once both colours are chosen (and stays in sync with that pattern until manually edited). Editors pick colours from searchable swatch pills (SettingsListReactSelect / BrandColorSelectInput). Background and foreground must differ — the sibling value is hidden from each list, and validation rejects matching pairs. Duplicate pairingId values are rejected.

General links opt into block_linkArrayMember({ withLinkId: true }), which adds a required unique linkId slug (Generate prefers the link’s branch label). The frontend can resolve a link deterministically, e.g. generalLinks[linkId.current == "contact"][0]. Other link arrays leave withLinkId off (the default).

Each redirects entry is { label, origin, destination }, and duplicate origin values fail validation. Each variants and tags entry is { label, slug, description? }, with duplicate slugs rejected within each array. Each actions entry is { actionType, actionSlug, actionValues? }actionSlug is derived from actionType, and actionValues declares optional payload fields as { name, type }[] (types: string, number, boolean, url). block_link lists those actionType values on action.actionType (plus Custom, which reveals action.actionTypeCustom); choosing an action reveals typed inputs for each declared field, stored as action.actionValues[]{ name, type, value }. Values are required and validated by type (number / boolean / url / string). Duplicate action slugs and duplicate field names within an action are rejected. Duplicate brand colorId values are rejected. Brand colour pairings require a unique pairingId and must use two different colorId values. Company profile fields live on document_company.

Asset validation:

  • seo.image — inherited from helper_seo: JPG or PNG only, exactly 1200px wide and 620–630px tall.
  • siteFavicons (Site tab) — the PNG/SVG/ICO icon set, each entry labelled for the slot it fills (apple-touch-icon, favicon-192x192, …).

Options (see DocumentSettingsOptions): _type (default settings), title (default Settings), description, referenceTypes, seoOptions.

Pin Settings (with Company and Home) with helper_singletons. Fetch with document_settings_query from @droplab/sanity-tools/groq.

Usage

// sanity.config.ts
import { defineConfig } from 'sanity';
import { document_company, document_settings } from '@droplab/sanity-tools';

export default defineConfig({
	// projectId, dataset, plugins, ...
	schema: {
		types: [
			document_company(),
			document_settings({ referenceTypes: ['page', 'post'] }),
			// ...your other types
		],
	},
});

Fetch settings (GROQ)

document_settings_query projects fields at the document root, including mainNavigation, footerNavigation (falls back to main when empty), and redirects.

import { groq } from 'next-sanity';
import { document_settings_query } from '@droplab/sanity-tools/groq';

export const SETTINGS_QUERY = groq`
	*[_type == "settings"][0]
	{
		${document_settings_query}
	}
`;

document_page

document_page() returns a Page document with Content, SEO, and Settings tabs. Content holds title and a page-builder array (contentBlocks by default). Settings holds slug, list label, environment, visibility, author, publish date, tags, and variants.

blockMembers is required — the same defineArrayMember(block_*(…)) list you would pass to block_section. Do not include block_section inside itself; pages may include sections.

Options (DocumentPageOptions): _type (default page), title, description, blockMembers, blocksFieldName (default contentBlocks; pass pageBuilder if you prefer that name), blocksTitle, personType (default person), settingsType (default settings), seoOptions, slugValidation (prefer singletons.slugFieldValidation()).

Usage

import { defineArrayMember } from 'sanity';
import
{
	document_page,
	block_hero,
	block_richText,
	block_card,
	helper_singletons,
} from '@droplab/sanity-tools';

const singletons = helper_singletons();

export const pageSchema = document_page({
	blockMembers: [
		defineArrayMember(block_hero({ name: 'hero', title: 'Hero' })),
		defineArrayMember(block_richText({ name: 'body', title: 'Body' })),
		defineArrayMember(block_card({ name: 'card', title: 'Card' })),
	],
	slugValidation: singletons.slugFieldValidation(),
});

Register it in schema.types next to document_settings and document_person. Query the array with page_builder_query (retarget _type names if your member names differ from the playground).

document_person

document_person() returns a Person document with Content and Settings tabs (no SEO). Content: name, role, bio, image. Settings: list label.

Options (DocumentPersonOptions): _type (default person), title, description, imageOptions (forwarded to block_image; block config is off).

Usage

import { document_person } from '@droplab/sanity-tools';

export const personSchema = document_person();

export const authorSchema = document_person({
	_type: 'author',
	title: 'Author',
	imageOptions: { withCaption: false },
});

Pass personType: 'author' to document_page / document_article / document_news if you rename the type.

document_client

document_client() returns a Client document with Content, SEO, and Settings tabs. Content: name, slug, logo, logoAlt. Settings: enabled, list label, environment, visibility.

Options (DocumentClientOptions): _type (default client), title, description, logoOptions, logoAltOptions (forwarded to block_image; block config is off), seoOptions, slugValidation.

Usage

import { document_client } from '@droplab/sanity-tools';

export const clientSchema = document_client();

export const brandSchema = document_client({
	_type: 'brand',
	title: 'Brand',
	logoOptions: { withCaption: false },
});

document_article

document_article() returns an Article with Content, SEO, and Settings. Content: title, subtitle, summary, imageThumbnail, richText. Settings: slug, list label, environment, visibility, author, publish date, tags, variants.

Options (DocumentArticleOptions): _type (default article), title, description, personType, settingsType, seoOptions, richTextOptions (name is fixed to richText), imageOptions, referenceTypes (internal links in the body; default ['page', 'article', 'news']).

Usage

import { document_article } from '@droplab/sanity-tools';

export const articleSchema = document_article({
	referenceTypes: ['page', 'article'],
	richTextOptions: { withImage: true, withVideo: true },
	imageOptions: { withAlt: true },
});

document_news

document_news() returns a News document. Content: title, subtitle, summary, imageThumbnail, and a content type toggle — internal shows slug + rich text; external shows a URL. Settings: slug, list label, environment, author, publish date, tags, variants, visibility.

Options (DocumentNewsOptions) match document_article (richTextOptions apply when content type is internal).

Usage

import { document_news } from '@droplab/sanity-tools';

export const newsSchema = document_news({
	referenceTypes: ['page', 'article', 'news'],
	richTextOptions: { withImage: true },
});

Shared document fields

These factories build the tabs and Settings fields used by document_page / document_article / document_news / document_person. Use them when composing a custom document that should match that shape.

import { defineType } from 'sanity';
import
{
	documentFieldGroups,
	documentTitleAndSlugFields,
	documentTagsField,
	documentVariantsField,
	documentAuthorField,
	documentPublishDateField,
	documentListLabelField,
	documentEnvironmentField,
	documentVisibilityField,
	DOCUMENT_CONTENT_GROUP,
	DOCUMENT_SETTINGS_GROUP,
} from '@droplab/sanity-tools';

export const customDoc = defineType({
	name: 'resource',
	type: 'document',
	groups: documentFieldGroups(),
	fields: [
		...documentTitleAndSlugFields('title', DOCUMENT_CONTENT_GROUP),
		documentListLabelField(DOCUMENT_SETTINGS_GROUP),
		documentEnvironmentField(),
		documentVisibilityField(),
		documentAuthorField('person'),
		documentPublishDateField(),
		documentTagsField({ settingsType: 'settings', group: DOCUMENT_SETTINGS_GROUP }),
		documentVariantsField({ settingsType: 'settings', group: DOCUMENT_SETTINGS_GROUP }),
	],
});
  • documentFieldGroups({ withSeo: false }) — omit the SEO tab (person).
  • Tags / variants read the arrays on document_settings Taxonomy. Duplicate values are rejected.

block_actionFields

Returns the fields for an action picker: actionType (from Settings → Taxonomy actions, plus Custom), actionTypeCustom, and actionValues ({ name, type, value }[]). Spread into a parent fields array. Resolve the stored pair on the frontend with resolveBlockAction().

Options (BlockActionFieldsOptions): name (default actionType), title, description, customName, customTitle, settingsType (default settings), group.

Usage

import { defineType, defineField } from 'sanity';
import { block_actionFields } from '@droplab/sanity-tools';

export const ctaButton = defineType({
	name: 'ctaButton',
	type: 'object',
	fields: [
		...block_actionFields(),
		defineField({ name: 'label', type: 'string' }),
	],
});

block_link already nests this picker on the action branch. Use block_actionFields only when you need a standalone action object.

block_card

Card with variant (sitewide taxonomy from document_settings), tagline, blurb, image, background media (none / image / video / optional color), CTA (block_link with a None type), optional dynamic Additional fields (same typed name / type / value pattern and list previews as block_customData), and block_config.

Options (BlockCardOptions): variantOptions, withTagline, withBlurb, withImage, imageOptions, withAdditionalFields, additionalFieldTypes, additionalFieldsReferenceTypes, bgImageOptions, bgVideoOptions, ctaOptions, configOptions, withBackgroundColor (adds Color media + bgColor via brandColorPairingsChooser()).

GROQ: block_card_query.

Usage

import { defineType, defineField } from 'sanity';
import { block_card } from '@droplab/sanity-tools';

export const pageSchema = defineType({
	name: 'page',
	type: 'document',
	fields: [
		defineField({ name: 'title', type: 'string' }),
		block_card({
			name: 'featuredCard',
			title: 'Featured card',
			ctaOptions: { referenceTypes: ['page'] },
			bgVideoOptions: { provider: 'mux-plugin' },
			configOptions: { additionalThemes: ['brand-night'] },
		}),
	],
});

block_carousel

Carousel of image / video / card slides, plus title, blurb, carousel settings (transitionType: slide | fade), and optional block_config.

Options (BlockCarouselOptions): withConfig, configOptions, imageOptions, videoOptions, cardOptions, withTitle, withBlurb, itemsTitle, carouselSettingsTitle.

GROQ: block_carousel_query.

Usage

import { defineType, defineField } from 'sanity';
import { block_carousel } from '@droplab/sanity-tools';

export const pageSchema = defineType({
	name: 'page',
	type: 'document',
	fields: [
		defineField({ name: 'title', type: 'string' }),
		block_carousel({
			name: 'contentCarousel',
			title: 'Content carousel',
			videoOptions: { withMux: true },
			cardOptions: { ctaOptions: { referenceTypes: ['page'] } },
		}),
	],
});

block_config

Shared block metadata: enabled, Studio-only label, optional id (HTML / hash scroll target for page-builder blocks only — hidden when block_config is a direct document field; not mirrored to document Settings), optional theme (light / dark plus additionalThemes or a full themes list), optional layout (fullWidth | fullBleed | fill | inline). Most block_* objects already include this on a Settings tab.

Options (BlockConfigOptions): enabledInitialValue, themes, additionalThemes, themeInitialValue, layoutInitialValue, withTheme, withLayout, withLabel.

GROQ: block_config_query. Use label in preview.select; it is not site content. Use config.domId (projected from schema id) as the element id (or #{domId} in links). Frontends should still return disabled blocks (for preview) and hide them with config.enabled.

Usage

import { defineType, defineField } from 'sanity';
import { block_config } from '@droplab/sanity-tools';

export const ctaBlock = defineType({
	name: 'ctaBlock',
	type: 'object',
	fields: [
		block_config({
			name: 'config',
			title: 'Block settings',
			additionalThemes: ['high-contrast'],
		}),
		defineField({ name: 'buttonText', type: 'string' }),
	],
});

For a block nested in block_section, pass NESTED_IN_SECTION_CONFIG instead of a custom theme/layout list.

block_customData

block_customData() creates an object field for one-off component data: a stable id plus a dynamic fields array. Each field has name, fieldType, and a typed value branch (stringValue, numberValue, imageValue, richTextValue, jsonValue, etc.). Editors can add fields without schema changes; the frontend maps id to a React component and reads values by field name.

Stored shape (simplified):

{
  id: 'promoBanner',
  fields: [
    { name: 'headline', fieldType: 'string', stringValue: 'Hello' },
    { name: 'count', fieldType: 'number', numberValue: 3 },
    { name: 'body', fieldType: 'richText', richTextValue: [/* PT blocks */] },
    { name: 'config', fieldType: 'json', jsonValue: { code: '{ "a": 1 }', language: 'json' } },
  ],
  config: { enabled, label, theme, layout }
}

Field types: string, text, number, boolean, url, reference, image, video, richText, json. Pass fieldTypes to limit the list (default: all). referenceTypes controls which documents appear for reference fields (default ['page']).

| Type | Value field | Notes | | --- | --- | --- | | richText | richTextValue | Portable Text via block_richText({ withBlockConfig: false }) (text blocks + links) | | json | jsonValue | @sanity/code-input (type: 'code', language JSON). Studio stores { code, language }. Validation accepts JSON5-style syntax and requires a parsed object or array. Register codeInput() from @sanity/code-input in sanity.config. |

List previews: block title CUSTOM DATA: {label|id}. Each field item title is {TYPE}: {name} (uppercase type prefix, e.g. JSON: form_fields); subtitle shows a truncated value preview.

Frontend: register components by id. References stay as { _ref } unless your GROQ projection expands them. For json, parse jsonValue.code (or project a parsed value in GROQ).

Options (see BlockCustomDataOptions): referenceTypes, fieldTypes, configOptions, plus BaseFieldOptions. GROQ: block_customData_query (typed branches via dynamic_typed_fields_query).

Studio: register @sanity/code-input when using the json type:

import { defineConfig } from 'sanity';
import { codeInput } from '@sanity/code-input';

export default defineConfig({
	plugins: [codeInput(), /* … */],
});

Usage

import { defineType, defineField } from 'sanity';
import { block_customData } from '@droplab/sanity-tools';

export const pageSchema = defineType({
	name: 'page',
	type: 'document',
	fields: [
		defineField({ name: 'title', type: 'string' }),
		block_customData({
			name: 'promoData',
			title: 'Promo data',
			referenceTypes: ['page'],
			fieldTypes: [
				'string',
				'text',
				'number',
				'boolean',
				'url',
				'image',
				'reference',
				'richText',
				'json',
			],
		}),
	],
});

block_grid

Column grid (minColumns required; no max) of image, video, or card cells. Columns default to 3. Optional title, blurb, gutter, and block_config.

Options (BlockGridOptions): minColumns, withConfig, configOptions, imageOptions, videoOptions, cardOptions, withTitle, withBlurb, columnsTitle, itemsTitle, useGutterInitialValue.

GROQ: block_grid_query. block_gridPortableMember() is the Portable Text of member (_type: 'block_grid').

Usage

import { defineType, defineField } from 'sanity';
import { block_grid } from '@droplab/sanity-tools';

export const pageSchema = defineType({
	name: 'page',
	type: 'document',
	fields: [
		defineField({ name: 'title', type: 'string' }),
		block_grid({
			name: 'contentGrid',
			title: 'Content grid',
			minColumns: 1,
			videoOptions: { withMux: true },
			cardOptions: { ctaOptions: { referenceTypes: ['page'] } },
		}),
	],
});

block_hero

Hero with variant (sitewide taxonomy from document_settings), optional tagline, blurb, CTA link array, and background (none / image / video / optional color). Nested background video uses HLS / file / YouTube / Vimeo by default. Pass backgroundVideoOptions: { withMux: true } to include Mux (requires the Mux Studio plugin).

Options (BlockHeroOptions): variantOptions, configOptions, withTagline, withBlurb, withCta, ctaOptions, backgroundImageOptions, backgroundVideoOptions, withBackgroundColor (adds Color background + backgroundColor via brandColorPairingsChooser()).

GROQ: block_hero_query (includes the cta link array when present).

Usage

import { defineType, defineField } from 'sanity';
import { block_hero, NESTED_IN_SECTION_CONFIG } from '@droplab/sanity-tools';

export const pageSchema = defineType({
	name: 'page',
	type: 'document',
	fields: [
		defineField({ name: 'slug', type: 'slug' }),
		block_hero({ name: 'hero' }),
		block_hero({
			name: 'campaignHero',
			title: 'Campaign hero',
			withTagline: true,
			withBlurb: true,
			backgroundVideoOptions: {
				withMux: true,
				withLoop: true,
				withAutoplay: true,
				withControls: false,
			},
			configOptions: NESTED_IN_SECTION_CONFIG,
		}),
	],
});

block_image

Image field. By default this is an object with a Settings tab (block_config) and an image sub-field. Set withBlockConfig: false when a parent (hero, card, person) already owns config — then the field is a plain image.

Options (BlockImageOptions): withBlockConfig, configOptions, hotspot, withAlt, withCaption, withUnsplash (requires sanity-plugin-asset-source-unsplash).

GROQ: block_image_query. block_imagePortableMember() embeds the same image in Portable Text (_type: 'block_image').

Usage

import { defineType, defineField } from 'sanity';
import { block_image } from '@droplab/sanity-tools';

export const articleSchema = defineType({
	name: 'article',
	type: 'document',
	fields: [
		defineField({ name: 'title', type: 'string' }),
		block_image({ name: 'thumbnail' }),
		block_image({
			name: 'heroImage',
			title: 'Hero image',
			hotspot: true,
			withAlt: true,
			withCaption: true,
			required: true,
		}),
	],
});

Query a configured image as thumbnail.image and thumbnail.config.

block_link

Object for internal (document reference + optional anchor), external URL, or action (Settings taxonomy). Optional None type, variant picker, and block_config.

Options (BlockLinkOptions): referenceTypes, withBlockConfig, withNoneLinkType, linkTypes (internal | external | action), configOptions, variantOptions, actionOptions.

GROQ: block_link_query.

Related helpers:

  • block_linkArrayMember() — array of member (navigation, CTAs). withLinkId: true adds a unique slug.
  • block_linkType() — register once as schema.types, then defineField({ name: 'cta', type: 'block_link' }).
  • block_linkAnnotation() — Portable Text mark annotation (block_richText uses this when withLink: true).

Usage

import { defineType, defineField } from 'sanity';
import { block_link, block_linkArrayMember } from '@droplab/sanity-tools';

export const pageSchema = defineType({
	name: 'page',
	type: 'document',
	fields: [
		block_link({
			name: 'primaryLink',
			title: 'Primary link',
			referenceTypes: ['page', 'article'],
			required: true,
		}),
		defineField({
			name: 'links',
			type: 'array',
			of: [block_linkArrayMember({ referenceTypes: ['page'] })],
		}),
	],
});

block_references

block_references() creates an object field with an array of document references and a custom Studio input. Editors pick a document _type source (All types or one allowed type); the reference browser updates to match. Already-chosen documents are removed from the available list, and configured singletons are excluded by default (settings, plus page with slug.current == "home"). Duplicate refs also fail validation via noDuplicates().

When referenceTypes is omitted, every registered document type is offered (minus excludeTypes). Pass referenceTypes to restrict. The selected Document source is stored as sourceType.

Empty list auto-fill (GROQ): If references is empty, block_references_query loads public documents for that source (visibility public, or unset), ordered by tag priorityfeatured → everything else, each tier by publish date desc. When sourceType is set, auto-fill is restricted to that _type; otherwise public non-system documents are returned (excluding settings, sanity.*, system.*, and the homepage). Auto-fill is capped at 100 documents (GROQ cannot slice on a stored field). maxResults, usePagination, itemsPerPage, and paginationStyle are returned for the frontend to apply. Resolved docs include title and name so person/client documents render without a title field.

GROQ: block_references_query.

Usage

import { defineType, defineField } from 'sanity';
import { block_references } from '@droplab/sanity-tools';

export const pageSchema = defineType({
	name: 'page',
	type: 'document',
	fields: [
		defineField({ name: 'title', type: 'string' }),
		block_references({
			name: 'related',
			title: 'Related content',
			referenceTypes: ['page', 'article', 'news'],
			max: 8,
		}),
	],
});

block_richText

Portable Text field with optional link annotations, image / video / table / grid members, and a callout style.

Options (BlockRichTextOptions): styles (default normal, h2, h3, blockquote), lists, decorators, withLink, linkOptions, withImage, imageOptions, withVideo, videoFieldOptions, videoProviders, withTable, tableOptions, withGrid, gridOptions, withCallout, previewComponents, configOptions.

GROQ: block_richText_query / block_richText_content_query. block_richTextType() registers a reusable type named block_richText.

Usage

import { defineType, defineField } from 'sanity';
import { block_richText } from '@droplab/sanity-tools';

export const blogPostSchema = defineType({
	name: 'blogPost',
	type: 'document',
	fields: [
		defineField({ name: 'title', type: 'string' }),
		block_richText({ name: 'summary' }),
		block_richText({
			name: 'body',
			title: 'Body content',
			withLink: true,
			linkOptions: { referenceTypes: ['page', 'article'] },
			withImage: true,
			withVideo: true,
			videoFieldOptions: { withMux: true },
			withTable: true,
			withGrid: true,
			gridOptions: { minColumns: 1 },
		}),
	],
});

block_section

Object wrapper with block_config (theme/layout), optional header fields (variant, title, subtitle, blurb), a mutually exclusive Background (none | image | video | color via brandColorPairingsChooser), and a nested blocks array. Pass the same palette as the page builder excluding block_section (no recursion). Nested members should use NESTED_IN_SECTION_CONFIG.

Background video defaults to multi-provider HLS / file / YouTube / Vimeo (embedded playback). Pass backgroundVideoOptions: { withMux: true } for Mux, or provider to restrict to one backend.

Options (BlockSectionOptions): blockMembers (required), blocksFieldName (default blocks), blocksTitle, configOptions, variantOptions, backgroundImageOptions, backgroundVideoOptions, withBackgroundColor (default true), collapsible, initiallyCollapsed.

GROQ: block_section_query (projects header, backgroundType, conditional background branches, and nested blocks[] via page_builder_members_query).

Usage

import { defineArrayMember, defineType, defineField } from 'sanity';
import
{
	block_section,
	block_hero,
	block_richText,
	NESTED_IN_SECTION_CONFIG,
} from '@droplab/sanity-tools';

export const pageSchema = defineType({
	name: 'page',
	type: 'document',
	fields: [
		defineField({ name: 'title', type: 'string' }),
		block_section({
			name: 'intro',
			title: 'Intro',
			collapsible: true,
			withBackgroundColor: true,
			backgroundVideoOptions: { withLoop: true },
			blockMembers: [
				defineArrayMember(
					block_hero({
						name: 'hero',
						title: 'Hero',
						configOptions: NESTED_IN_SECTION_CONFIG,
					})
				),
				defineArrayMember(block_richText({ name: 'lead', title: 'Lead copy' })),
			],
		}),
	],
});

block_spacer

block_spacer() creates an object field for vertical spacing. Block settings on the Settings tab are Enabled and List label only (no theme/layout). Preset and custom size options:

  • small
  • medium
  • larger
  • custom (string, must be a valid CSS unit value like 24px, 2rem, 10vh, or 50%)

showLine (default false) draws a centered horizontal rule. GROQ: block_spacer_query.

Usage

import { defineType, defineField } from 'sanity';
import { block_spacer } from '@droplab/sanity-tools';

export const pageSchema = defineType({
	name: 'page',
	type: 'document',
	fields: [
		defineField({ name: 'title', type: 'string' }),
		block_spacer({ name: 'spaceSm', sizeInitialValue: 'small' }),
		block_spacer({
			name: 'spaceCustom',
			title: 'Custom spacing',
			sizeInitialValue: 'custom',
		}),
	],
});

STL Table Integration (block_table / block_tableField)

This package includes two helpers for the Sanity STL table plugin and the wider structured-table / STL ecosystem:

  • block_tableField() — thin field wrapper around type: 'stl_table'
  • block_table() — object block with nested table and optional config
  • block_tablePortableMember() — Portable Text of member (_type: 'block_table')

Peer dependency (optional in npm): sanity-plugin-stl-table — install it in the Studio that uses these fields.

GROQ: block_table_query / block_tableField_query.

1) Install the package in your Studio

pnpm add sanity-plugin-stl-table

(Use npm / yarn if you do not use pnpm.)

The current npm release ships a schema export (stlTableBlock) and does not provide a plugins: [stlTable()] function you register in sanity.config. Integration is: add the dependency, register a schema type (next section), and optionally add the Studio preview components (section 3).

2) Register a schema type named stl_table

sanity-plugin-stl-table exports stlTableBlock (a block named stlTableBlock with stlString, caption, etc.). The helpers in this package expect a registered type with name: 'stl_table' and the same field shape, so the Studio can render the plugin’s TableInput for that type.

Add a type that re-exports the plugin block under stl_table and list it in schema.types:

import { defineType } from 'sanity';
import { stlTableBlock } from 'sanity-plugin-stl-table';

export const stlTableType = defineType({
	...stlTableBlock,
	name: 'stl_table',
	title: 'STL table',
	type: 'object',
});

If TypeScript complains about the spread, narrow stlTableBlock to ObjectDefinition (omit name / title) before passing it to defineType.

You can also register the plugin’s stlTableBlock in addition to stl_table if you want that name inside portable text; block_table / block_tableField only need stl_table.

3) In-Studio table preview (structured-table React components)

The plugin’s built-in preview in the editor uses structured-table and expects a React renderer to be registered. Without the generated components, the STL editor can still work, but the live preview inside Studio may look empty or break.

Do this in the Sanity Studio app.

A. Install the CLI

pnpm add -D structured-table-cli

B. Generate the React table components (from the directory where sanity.config lives)

Standalone Studio (a pnpm-lock.yaml next to that package.json):

pnpm exec stl-cli add react

By default the CLI creates a folder of components; you can pick the output path:

pnpm exec stl-cli add react --path ./schemaTypes/stl-table-react

(npx stl-cli is fine too if you prefer; use the same package manager your Studio uses so installs stay consistent.)

In a pnpm (or yarn) workspace, structured-table-cli only looks for a lockfile in the current working directory. Run it from the Studio package directory that has (or can see) that lockfile so the CLI does not pick the wrong package manager.

C. Register the generated register module in sanity.config.ts

Add an import that runs the registration side effects before the rest of the config. Path must match the folder you generated (look for a register file inside it):

import './schemaTypes/stl-table-react/register';
import { defineConfig } from 'sanity';
// ... other imports

export default defineConfig({
	// projectId, dataset, plugins, schema, ...
});

If you used the CLI default output layout, the path is often ./components/stl-render-react/latest/register instead.

D. Restart the Studio (sanity dev or your package script) so Vite loads the new files.

4) Use the helpers in your schema

Use either helper in your schema:

import { defineType, defineField } from 'sanity';
import { block_table, block_tableField } from '@droplab/sanity-tools';

export const pageSchema = defineType({
	name: 'page',
	type: 'document',
	fields: [
		defineField({ name: 'title', type: 'string' }),

		// Thin wrapper: direct stl_table field
		block_tableField({
			name: 'pricingTable',
			title: 'Pricing table',
		}),

		// Rich block: nested table + optional shared config
		block_table({
			name: 'comparisonTable',
			title: 'Comparison table',
			withConfig: true,
			configOptions: { additionalThemes: ['brand-night'] },
		}),
	],
});

5) Fetch table data (GROQ)

Minimal GROQ shape for frontend rendering:

*[_type == "page" && slug.current == $slug][0]{
	title,
	pricingTable,
	comparisonTable{
		config,
		table
	}
}
  • Use pricingTable directly when using block_tableField.
  • Use comparisonTable.table when using block_table.
  • Use comparisonTable.config for optional render decisions (theme/layout/enabled).

6) Frontend usage on your site (structured-table)

The stl_table value is intended to be passed into your table renderer from the structured-table ecosystem.

import { TableView } from '@/components/stl-render-react/latest';

type PageData = {
	pricingTable?: unknown;
	comparisonTable?: {
		config?: {
			enabled?: boolean;
			theme?: string;
			layout?: 'fullWidth' | 'fullBleed' | 'fill' | 'inline';
		};
		table?: unknown;
	};
};

export function PageTables({ data }: { data: PageData })
{
	const tableData = data.comparisonTable?.table ?? data.pricingTable;
	const isEnabled = data.comparisonTable?.config?.enabled !== false;

	if (!isEnabled || !tableData) return null;

	return <TableView data={tableData} />;
}

Notes:

  • The exact table JSON shape is managed by sanity-plugin-stl-table and consumed by your chosen structured-table renderer.
  • For SSR frameworks, keep table querying server-side and pass the resolved table data into your component tree.

block_variantFields

Returns variant (an array of slugs from Settings → Taxonomy variants, plus Custom) and variantCustom. Spread into a parent fields array.

GROQ: block_variant_query resolves both custom and Settings-backed selections into the same shape as Settings taxonomy entries — { label, slug, description? }[] — so frontends never need a separate variantCustom branch. resolveBlockVariant() still returns a string[] of slugs from either the resolved objects or the raw Studio pair. Use blockVariantDataAttribute() for a space-separated data-variant attribute.

Options (BlockVariantFieldsOptions): name (default variant), title, description, customName, customTitle, settingsType, group.

block_link already nests this on links. Use the factory for a custom object that needs the same picker.

Usage

import { defineType, defineField } from 'sanity';
import { block_variantFields } from '@droplab/sanity-tools';

export const promoBlock = defineType({
	name: 'promoBlock',
	type: 'object',
	fields: [
		...block_variantFields(),
		defineField({ name: 'headline', type: 'string' }),
	],
});

block_video

Video object. Pass provider for a single backend (hls | file | youtube | vimeo, or mux-plugin when opted in), or omit it for a multi-provider union (same as block_videoField()). Default backends are HLS, local file, YouTube, and Vimeo. Playback toggles: withAutoplay, withLoop, withControls. embedded: true sets ambient defaults (muted, autoplay, loop, no controls) — block_hero / block_card backgrounds do this automatically.

Options (BlockVideoOptions / BlockVideoFieldOptions): withBlockConfig, configOptions, provider / providers, withMux, withAutoplay, withLoop, withControls, deferLeafRequired, embedded.

GROQ: block_video_query.

Related:

  • block_videoField() — always multi-provider (limit with providers).
  • block_videoPortableMember() — Portable Text of member.
  • block_videoType() — register as schema.types, then defineField({ name: 'video', type: 'block_video' }).

Mux is opt-in. It registers Sanity type mux.video, which does not exist unless the Mux Studio plugin is installed (sanity-plugin-mux-input or @sanity/asset-source-mux). Pass withMux: true, include 'mux-plugin' in providers, or use provider: 'mux-plugin'. HLS (hls) takes a public .m3u8 playlist URL and an optional poster image.

Usage

import { defineType } from 'sanity';
import { block_video, block_videoField } from '@droplab/sanity-tools';

export const videoPageSchema = defineType({
	name: 'videoPage',
	type: 'document',
	fields: [
		block_video({
			name: 'featuredVideo',
			provider: 'youtube',
			withControls: true,
		}),
		block_videoField({
			name: 'video',
			withMux: true,
			withAutoplay: false,
		}),
	],
});

helper_seo

Object for title, description, OG image (JPEG/PNG, 1200×620–630), optional keywords, and optional Is Searchable. Used on documents and nested on document_settings as seo.

Options (HelperSeoOptions): withKeywords (default false), withIsSearchable (default true; forced off inside settings), imageRequired, imageOptions.

GROQ: helper_seo_query.

Usage

import { defineType } from 'sanity';
import { helper_seo } from '@droplab/sanity-tools';

export const siteSettingsSchema = defineType({
	name: 'siteSettings',
	type: 'document',
	fields: [
		helper_seo({
			name: 'defaultSeo',
			title: 'Default SEO',
			withKeywords: true,
			imageRequired: false,
		}),
	],
});

brandColorPairingsChooser

String field that lists Brand Color Pairings from document_settings (Brand tab). Stores the pairing’s pairingId for frontend lookups such as brandColorPairings[pairingId == $id][0].

Options (BrandColorPairingsChooserOptions): settingsType (default settings), plus shared field knobs (name, title, required, group, …).

Related Studio inputs (also exported from the package root for custom schemas):

| Export | Role | | --- | --- | | BrandColorInput | @sanity/color-input wrapper — lists Settings → Brand colours as swatches | | BrandColorItemInput | Array item editor for brandColors entries | | BrandColorPairingInput | Array item editor for brandColorPairings entries | | BrandColorPairingPreview | List/collapsed preview for pairing items | | BrandColorSelectInput | Searchable swatch picker for a single Brand Color colorId | | BrandColorPairingsChooserInput | Pairing picker used by brandColorPairingsChooser() | | brandColorStringValidation / cssBrandColor | Hex / rgb validation and CSS helpers |

block_card({ withBackgroundColor: true }) wires this as bgColor on the Background media → Color branch. block_hero({ withBackgroundColor: true }) and block_section({ withBackgroundColor: true }) wire it as backgroundColor on the Background → Color branch.

GROQ / frontend: block queries project the stored pairingId string when the colour background branch is active (mediaType == "color" on cards, backgroundType == "color" on hero/section). Resolve colours from Settings:

const pairing = settings.brandColorPairings?.find((p) => p.pairingId === block.bgColor);
const bg = settings.brandColors?.find((c) => c.colorId === pairing?.backgroundColor);

Usage

import { defineField } from 'sanity';
import { brandColorPairingsChooser } from '@droplab/sanity-tools';

defineField({
	name: 'section',
	type: 'object',
	fields: [
		brandColorPairingsChooser({
			name: 'themePairing',
			title: 'Theme pairing',
		}),
	],
});

helper_phone

Object { countryCode, number } with a combined Studio input. Default dial-code list is PHONE_COUNTRY_CODES (override with countryCodes).

Options (HelperPhoneOptions): countryCodes, countryCodeInitialValue.

Usage

import { defineType, defineField } from 'sanity';
import { helper_phone } from '@droplab/sanity-tools';

export const contactSchema = defineType({
	name: 'contact',
	type: 'document',
	fields: [
		defineField({ name: 'title', type: 'string' }),
		helper_phone({
			name: 'phone',
			title: 'Phone',
			required: true,
			countryCodeInitialValue: '+1',
		}),
	],
});

document_company already uses this for company and per-office phones.

helper_singletons

Factory for Studio structure + document-config helpers around singleton documents. It treats two kinds of singleton:

  • Type-level — one document for the whole schema type (document_company / document_settings, document ids company / settings). Hidden from documentTypeListItems() and Create menus.
  • Document-scoped — one document inside a multi-document type (document_page matched by slug.current == "home" and/or _id == "home"). Hidden from that type’s document list.

helper_singletons() with no arguments uses those defaults (Company and Settings as type-level; Home as document-scoped). A document array replaces the homepage default only — Company and Settings stay as type-level singletons unless you pass types.

import { helper_singletons, getHelperIcon } from '@droplab/sanity-tools';
import type { StructureResolver } from 'sanity/structure';

const singletons = helper_singletons();

export const structure: StructureResolver = (S, context) =>
	S.list()
		.title('Content')
		.items([
			singletons.documentBySlug(S, 'home'),
			// Prefer orderableList for drag-and-drop + search; see helper_orderableList.
			singletons.orderableList({
				type: 'page',
				title: 'Pages',
				icon: getHelperIcon('page'),
				S,
				context,
			}),
			...singletons.filterDocumentTypeListItems(
				S.documentTypeListItems().filter((item) => item.getId() !== 'page')
			),
			S.divider(),
			singletons.documentByType(S, 'company'),
			singletons.documentByType(S, 'settings'),
		]);

documentByType, documentBySlug, and documentById share a three-argument shape: (S, stringValue, options?).

singletons.documentByType(S, 'settings')
singletons.documentByType(S, 'company', {
	title: 'Company',
	icon: getHelperIcon('company'),
})
singletons.documentBySlug(S, 'home', { icon: getHelperIcon('page') })
singletons.documentById(S, 'landing', { icon: getHelperIcon('page') })

Wire create guards so editors cannot spawn another Settings doc or reuse the Home slug:

// sanity.config.ts
plugins: [singletons.plugin(), structureTool({ structure })]

// page schema
document_page({
	blockMembers,
	slugValidation: singletons.slugFieldValidation(),
})

singletons.plugin() hides type-level singletons (company, settings) from Create menus and templates, and blocks duplicate / delete / unpublish on all singleton docs (type-level and document-scoped). slugFieldValidation() rejects reserved slugs (e.g. home) on any document that is not the configured singleton _id.

Each document entry needs slug and/or id (id defaults to slug; type defaults to page). Pass a document array, or { types, slugs } for full control — empty arrays disable that category:

const singletons = helper_singletons([
	{ type: 'page', slug: 'home', id: 'home', title: 'Home' },
	{ type: 'page', id: 'landing', title: 'Landing' },
])

export const structure: StructureResolver = (S) =>
	S.list().title('Content').items([
		singletons.documentBySlug(S, 'home'),
		singletons.documentById(S, 'landing'),
		S.listItem()
			.title('Custom pin')
			.child(singletons.documentById(S, 'abc-123', { as: 'document' })),
		S.listItem()
			.title('Pages')
			.child(singletons.documentTypeList(S, 'page', 'Pages')),
		S.divider(),
		singletons.documentByType(S, 'company'),
		singletons.documentByType(S, 'settings'),
	])
  • documentBySlug / documentById / documentByType — structure list items ((S, stringValue, options?); pass as: 'document' for .child(…))
  • documentTypeList — type list with matching document singletons removed (by slug and/or _id)
  • orderableList — searchable orderable desk item that auto-excludes document singletons for that type (see helper_orderableList)
  • plugin() — hides type-level singletons from Create menus + templates; blocks duplicate / delete / unpublish on all singleton docs
  • slugFieldValidation() — only the configured singleton _id may use reserved slugs (e.g. home)
  • document{ newDocumentOptions, actions } if you prefer spreading into defineConfig manually
  • filterDocumentTypeListItems — drop type-level singletons from S.documentTypeListItems()
  • newDocumentOptions / actions / filterTemplates / canHandleIntent — individual guards
  • referenceFilter — GROQ filter so reference pickers cannot select configured singletons

helper_orderableList

Wraps @sanity/orderable-document-list’s orderableDocumentListDeskItem with:

  • a live search field at the top of the list
  • optional singleton exclusion (same GROQ filters as documentTypeList)

Prefer singletons.orderableList({ … }) so document-scoped singletons (e.g. Home on page) are always filtered out. Use helper_orderableList directly when you do not need singleton handling, or pass singletons yourself. The underlying list UI is SearchableOrderableDocumentList (also exported for custom structure if needed).

Requirements

  1. Install the peer plugin in your Studio:
pnpm add @sanity/orderable-document-list
  1. Add orderRankField to the document schema (required by the plugin):
import { orderRankField } from '@sanity/orderable-document-list';
import { document_page } from '@droplab/sanity-tools';

export const page = document_page({
	blockMembers,
	slugValidation: singletons.slugFieldValidation(),
	additionalFields: [
		orderRankField({ type: 'page' }),
	],
});
  1. Use a structure resolver that receives context ((S, context) => …) and pass both S and context into the helper.

On first load, open the list menu → Reset Order so documents get ranks.

Usage

import { helper_singletons, getHelperIcon } from '@droplab/sanity-tools';
import type { StructureResolver } from 'sanity/structure';

const singletons = helper_singletons();

export const structure: StructureResolver = (S, context) =>
	S.list()
		.title('Content')
		.items([
			singletons.documentBySlug(S, 'home'),
			singletons.orderableList({
				type: 'page',
				title: 'Pages',
				icon: getHelperIcon('page'),
				// Appended to defaults; duplicates are dropped
				additionalSearchFields: ['seo.title'],
				S,
				context,
			}),
			S.divider(),
			singletons.documentByType(S, 'company'),
			singletons.documentByType(S, 'settings'),
		]);

Without singletons (or with an explicit instance):

import { helper_orderableList } from '@droplab/sanity-tools';

helper_orderableList({
	type: 'article',
	title: 'Articles',
	S,
	context,
	// singletons, // optional — same exclusion as documentTypeList
})

Search

Typing in the search box updates the list in realtime via GROQ match.

Default fields: title, name, label, slug.current (see DEFAULT_ORDERABLE_SEARCH_FIELDS).

| Option | Description | | --- | --- | | additionalSearchFields | Extra GROQ paths appended to the defaults (or to searchFields when that is an array). Trimmed and de-duplicated. | | searchFields | Replace the base list entirely, or pass false to disable search and use the stock orderable list. | | searchPlaceholder | Placeholder text for the search input. |

Options (HelperOrderableListOptions)

| Option | Description | | --- | --- | | type | Schema _type (required). | | title | Sidebar item and list pane header title (default Orderable ${type}). | | id | Structure list id (default orderable-${type}). Required when you have multiple orderable lists of the same type. | | icon | List icon (default getHelperIcon(type)). | | filter / params | Extra GROQ filter (AND’d with singleton exclusion when applicable). | | menuItems | Extra structure menu items. | | createIntent | When false, hide “Create new”. | | S / context | From structure: (S, context) => … (required). | | singletons | When set (always for singletons.orderableList), excludes configured document singletons for type. | | searchFields | Replace the base searchable fields, or false to disable search. | | additionalSearchFields | Extra fields appended to the defaults (de-duplicated). | | searchPlaceholder | Search input placeholder. |

Query ordered documents with GROQ: *[_type == "page"]|order(orderRank).

getHelperIcon / getHelperIconType

Studio icons used by the document and block helpers (page, person, hero, card, …). getHelperIcon(key) returns a theme-aware React component; getHelperIconType(key) returns the raw react-icons component. Keys also match prefixes (block_herohero).

Usage

import { defineType } from 'sanity';
import { getHelperIcon } from '@droplab/sanity-tools';

export const landing = defineType({
	name: 'landing',
	type: 'document',
	icon: getHelperIcon('page'),
	fields: [],
});

Utilities

noDuplicates()

Array validation that rejects repeated entries. Unlike the built-in rule.unique(), it compares items after stripping _key and blank members, so object and reference items are actually caught. Blank entries are skipped (pair with rule.required() if they should be flagged), and each error is attached to the offending array item so the Studio highlights it.

import { defineArrayMember, defineField } from 'sanity';
import { noDuplicates, noDuplicatesValidator } from '@droplab/sanity-tools';

defineField({
	name: 'tags',
	type: 'array',
	of: [defineArrayMember({ type: 'string' })],
	validation: noDuplicates(),
});

defineField({
	name: 'offices',
	type: 'array',
	of: [defineArrayMember({ type: 'office' })],
	// Compare specific fields instead of the whole item.
	validation: noDuplicates({ key: ['city', 'state'], label: 'Office' }),
});

defineField({
	name: 'related',
	type: 'array',
	of: [defineArrayMember({ type: 'reference', to: [{ type: 'page' }] })],
	// Chain alongside other rules.
	validation: (rule) => rule.required().min(1).custom(noDuplicatesValidator()),
});

Options (NoDuplicatesOptions):

| Option | Description | | --- | --- | | key | Field path ('label', 'link.href'), array of paths for a compound key, or a function deriving the compared value. Defaults to the reference _ref when present, otherwise the whole item. | | caseSensitive | Compare strings case-sensitively. Defaults to false. | | label | Noun used in the message, e.g. Tag "news" is listed more than once. | | message | String, or a function receiving DuplicateInfo (value, index, firstIndex, display, label). | | warning | Report as a warning rather than an error. |

noDuplicatesValidator(options) returns the bare custom validator for chaining onto an existing rule.