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

@taylordb/forms-cms

v1.0.0

Published

Schema-bound form-builder layer for @taylordb/cms: custom TinaCMS field widgets (single-use column blocks, a non-native select with per-option info), a dependency-pure schema toolkit that binds each question to a real TaylorDB column, and generic block re

Downloads

671

Readme

@taylordb/forms-cms

The schema-bound form-builder layer for @taylordb/cms: lets a content editor assemble a TaylorDB-backed form (a @taylordb/forms-taylordb form) in the in-page CMS drawer — without ever being able to author a form that doesn't match the database schema.

The core idea is column-first templates: instead of generic "text question / choice question" blocks, the Tina collection gets one block template per bindable TaylorDB column. The template name is the column, and its "Answer type" select is limited to the answer-types actually compatible with that column's descriptor — the exact compatibility check defineTaylorForm enforces (which throws on a mismatch). The editor can only ever produce valid pairings:

taylorSchema column          generated block template
──────────────────           ─────────────────────────────────────────────
fullName  (singleLineText) → "Full name · text field (Short text / …)"
email     (email)          → "Email · email field (Email)"
jobs      (link)           → "Jobs · link field (linked records)"

Three entries

| Entry | Contents | Imports | | --- | --- | --- | | @taylordb/forms-cms | The React field widgets (unique-blocks, field-select) as CMS plugins | react, tinacms | | @taylordb/forms-cms/schema | The schema toolkit: compatibility, labels, Tina field/template factories | type-only tinacms, @taylordb/forms-taylordb | | @taylordb/forms-cms/typeform | The block renderer (CmsFormSteps): one block → Typeform-UI JSX with click-to-edit wiring | react, tinacms/dist/react, @taylordb/forms-ui-typeform (optional peer, ≥2.0.5) |

The split matters: tina/config.ts is bundled by the tinacms CLI (Node) and imported by the in-page editor (browser), so everything it needs lives in the dependency-pure /schema entry. The widget entry is only ever loaded by the editor at runtime, via @taylordb/cms's fields extension point. The /typeform entry is the only one that touches a renderer package — apps that draw the form themselves never import it.

Install

pnpm add @taylordb/forms-cms @taylordb/forms-taylordb tinacms

react and tinacms are peer dependencies — the host app owns them.

Quickstart

1. Derive your bindable columns (app code)

Only the app knows which columns are off-limits (primary key, system columns, completion flags), so it binds the generic scan to its real taylorSchema:

// src/shared/taylordb-schema-meta.ts
import { bindableColumns, type BindableColumn } from "@taylordb/forms-cms/schema"
import { taylorSchema } from "../server/taylordb/types"

const RESERVED = new Set(["id", "submitted", "createdAt", "updatedAt", "searchText"])

export function bindableSubmissionColumns(): BindableColumn[] {
  return bindableColumns(taylorSchema.submissions, { reserved: RESERVED, allowLink: true })
}
export function bindableJobColumns(): BindableColumn[] {
  return bindableColumns(taylorSchema.jobs, { reserved: new Set(["id", "submissions"]) })
}

bindableColumns skips reserved + attachment columns, turns link columns into a single linked_records binding (when allowLink), and drops anything with no renderer-compatible answer-type.

2. Generate the collection templates (tina/config.ts)

import {
  columnStepTemplates,
  screenTemplate,
  uniqueBlocksUi,
} from "@taylordb/forms-cms/schema"
import { bindableJobColumns, bindableSubmissionColumns } from "../src/shared/taylordb-schema-meta"

const screens = [
  screenTemplate("welcome", "Welcome screen"),
  screenTemplate("statement", "Statement (display-only)"),
  screenTemplate("end", "End screen"),
]

const stepTemplates = columnStepTemplates({
  columns: bindableSubmissionColumns(),   // one template per column
  linkedColumns: bindableJobColumns(),    // sub-fields of link-column steps
  screens,
})

// in the collection:
{
  type: "object",
  name: "steps",
  label: "Questions & screens",
  list: true,
  templates: stepTemplates,
  ui: uniqueBlocksUi({ reusable: ["statement"], pinFirst: "welcome", pinLast: "end" }),
}

uniqueBlocksUi opts the list into the unique-blocks widget: every column template is single-use (a used column drops out of the "+" menu), reusable templates stay addable any number of times, and pinFirst / pinLast are single-use and position-locked — the welcome screen is always first, the end screen always last, and their drag handles are disabled.

3. Register the widgets on the editor

import { TaylorCMSDev } from "@taylordb/cms/dev"

<TaylorCMSDev
  load={async () => ({
    config: (await import("../tina/config")).default,
    schemaJson: (await import("virtual:tina-schema")).default,
    fields: (await import("@taylordb/forms-cms")).formFieldPlugins, // ← the widgets
  })}
/>

Or imperatively, given a CMS instance: registerFormFields(cms).

4. Consume the saved document

The saved doc's steps array is the single source of truth for both the form schema (defineTaylorForm) and the rendered UI. Two package helpers matter here:

import { normalizeBlocks } from "@taylordb/forms-cms/schema"

// Tina's live GraphQL union members carry `__typename` ("TaylordbStepsPhone_number")
// instead of `_template`; raw JSON already has `_template`. Normalize both:
const blocks = normalizeBlocks<MyBlock>(doc.steps, "TaylordbSteps")

The typenamePrefix is collection + list field in PascalCase (collection taylordb, field steps"TaylordbSteps").

Pre-filter before building the form. defineTaylorForm throws on an invalid column/answer-type pairing, and a doc can be briefly incomplete mid-edit (a just-added block has no questionType yet). Filter the block list against the same schema rules before it reaches the builder — see the playground's taylordb-form-blocks.ts (usableTaylordbBlocks) for the pattern.

The widgets (@taylordb/forms-cms)

Both are generic — no schema knowledge; they only shape Tina's own blocks / select fields. A schema opts in by name via ui.component (use the factories below rather than hand-writing the ui objects).

unique-blocks

A drop-in replacement for Tina's blocks field (it delegates rendering, drag-drop and the "+" menu to Tina's own component) that adds:

  • Single-use templates — once added, a template disappears from the "+" menu. This is what turns the list into an "add a question for an unused column" flow.
  • reusableTemplates — exempt names (e.g. a statement screen), addable any number of times.
  • pinFirst / pinLast — one template each that is single-use and position-locked: always the first / last item (an order violation is corrected immediately), with its drag handle disabled and dimmed.
  • A helper line under the list ("… N of M fields still available").

Registered name: unique-blocks (UNIQUE_BLOCKS_COMPONENT). Configure via uniqueBlocksUi({ reusable?, pinFirst?, pinLast? }).

field-select

A custom replacement for Tina's native <select>:

  • Renders its label / description / error through Tina's wrapFieldsWithMeta, so it sits visually flush with the built-in fields.
  • An optional per-option ⓘ tooltip (optionInfo: { value: text }) — used to show a column's field type or an answer-type's description.
  • Optional sibling de-duplication (dedupeSiblings: true): hides values already chosen by other items in the same list (the item's own value stays).
  • The dropdown expands inline (in document flow, never clipped by the drawer's scroll container) and closes on outside click / Escape.

Registered name: field-select (FIELD_SELECT_COMPONENT). Configure via answerTypeSelect(...) / columnSelect(...).

Registration exports

import {
  formFieldPlugins,      // both plugins, ready for @taylordb/cms's `fields` prop
  registerFormFields,    // (cms) => void — imperative alternative
  fieldSelectFieldPlugin, uniqueBlocksFieldPlugin, // individually
  FieldSelect, UniqueBlocks,                       // raw components
} from "@taylordb/forms-cms"

The renderer (@taylordb/forms-cms/typeform)

CmsFormSteps renders an (already gated) block list into @taylordb/forms-ui-typeform steps, in document order — screens, questions per answer-type, linked-records, file uploads (mediaKind: 'video' renders the in-browser recorder). Because it maps over the same list the app turns into sharedSteps, step ids (block._template, the column) and order stay in lockstep by construction.

import { CmsFormSteps } from "@taylordb/forms-cms/typeform"

<Form sharedSteps={form.sharedSteps} …>
  <CmsFormSteps blocks={usableBlocks(blocks)} />
</Form>
  • Pass gated blocks. The renderer draws what it's given; run the doc through your app's usableBlocks pre-filter first (only the app knows its taylorSchema). Mid-edit/incomplete blocks are then skipped until valid.

  • Click-to-edit is built in: titles/descriptions carry data-tina-field; the primary action button gets the attribute on the <button> element itself (via the renderer packages' buttonProps pass-through — needs forms-ui/forms-ui-typeform ≥0.4.5/2.0.5), so it works whether the label is a buttonText override or a locale default — clicking a default-labelled button opens the empty buttonText field. Inputs are wrapped in an attributed element mapping to placeholder (text-ish types) or the block itself.

  • renderInput overrides replace the control for individual answer-types without forking the renderer:

    <CmsFormSteps blocks={blocks} renderInput={{ rating: (block) => <MyRating max={block.max} /> }} />
  • Locale note: doc copy (titles, placeholders, buttonText overrides) is single-language — an override wins over the locale default in every locale. For multilingual forms, use one doc per locale and pick relativePath by locale.

The schema toolkit (@taylordb/forms-cms/schema)

Compatibility & metadata

| Export | What it is | | --- | --- | | RENDERER_QUESTION_TYPES | The answer-types a Typeform-style renderer can draw (file_upload intentionally omitted until attachment upload is wired). Compatibility is intersected with this list. | | LINKED_RECORDS_TYPE | 'linked_records' — the forced answer-type for link columns. | | compatibleQuestionTypes(descriptor) | Renderer-supported answer-types compatible with a column descriptor (via describeQuestionDescriptorCompatibility — the same check defineTaylorForm runs). | | bindableColumns(columns, { reserved?, allowLink? }) | Scan a table's descriptors into BindableColumn[] (see Quickstart). | | BindableColumn | { column, descriptor, allowedTypes }. |

Labels

| Export | What it is | | --- | --- | | QUESTION_TYPE_LABELS / questionTypeLabel(type) | Short human titles for answer-types (phone_number → "Phone number") — used as select option labels; the slug stays the stored value. | | QUESTION_TYPE_DESCRIPTIONS / questionTypeInfo(types) | Longer per-type descriptions — the ⓘ tooltip copy. | | fieldTypeLabel(descriptor) | Friendly name for a column's data type ("phone field", "multi-select field"). | | columnTitle(column) | Humanize a column slug (phoneNumber → "Phone number"). TaylorDB descriptors carry no display-name metadata, so titles are derived; acronym tails lower-case (websiteURL → "Website url"). |

Field factories (widget contract in one place)

| Export | Produces | | --- | --- | | answerTypeSelect({ allowedTypes, name?, label?, required? }) | The "Answer type" field-select (human-titled options + ⓘ descriptions, no dedupe — questions may share a type). | | columnSelect({ columns, name?, label?, required?, dedupeSiblings? }) | A column picker field-select (humanized titles, field-type ⓘ, optional sibling dedupe). | | uniqueBlocksUi({ reusable?, pinFirst?, pinLast? }) | The ui object for a blocks list using unique-blocks. | | placeholderField, maxLengthField | Common presentation fields. | | choiceOptionsField({ image?, description? }) | value/label(+sub-label)(+image) options list for choice questions. | | inputShapingFields(allowedTypes) | The presentation fields relevant to a set of answer-types (placeholder, min/max, countries, choice options, …). |

Template generation

| Export | Produces | | --- | --- | | screenTemplate(name, label) | A display-only screen template (id/label/description/buttonText). | | columnQuestionTemplate(col) | One column-bound question template — name = column, label = "Title · field type (answer types)", fields = common + answerTypeSelect + inputShapingFields. | | linkedRecordsTemplate(col, linkedColumns) | A link column → repeatable linked-records step whose linkedFields items pick from the linked table's columns (columnSelect with sibling dedupe). | | columnStepTemplates({ columns, linkedColumns?, screens? }) | The full list: screens first, then one template per column (link columns become linked-records templates). This is the one call most configs need. |

Payload normalization

| Export | What it does | | --- | --- | | normalizeBlocks<T>(rawSteps, typenamePrefix) | Decode Tina's live-GraphQL __typename union members into _template blocks (raw JSON passes through). Top level only — nested lists keep their __typename. |

How the guarantees compose

  1. bindableColumns only surfaces columns with ≥1 compatible answer-type.
  2. columnQuestionTemplate limits each block's questionType select to those types — the editor can't pick anything else.
  3. unique-blocks prevents binding the same column twice.
  4. Your app-side pre-filter (see Quickstart step 4) drops incomplete/stale blocks from saved docs before defineTaylorForm — which remains the final, throwing authority on schema validity.

Full working example: apps/forms/playground (tina/config.ts, src/shared/taylordb-schema-meta.ts, src/shared/taylordb-form-blocks.ts, the /taylordb route).