From 1769d31e63dcc0f6eb223306fc1880d82459e41b Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Tue, 28 Apr 2026 16:26:55 +0200 Subject: [PATCH] docs(field-kit): add plugin reference page Adds the Field Kit reference page covering installation, all four widgets (object-form, list, grid, tags), sub-field types, summary templates, and data durability guarantees. Adds the page to the Plugins sidebar after Block Kit. Follow-up to #702 per the maintainer's request on review. Signed-off-by: Filip Ilic --- docs/astro.config.mjs | 1 + docs/src/content/docs/plugins/field-kit.mdx | 243 ++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 docs/src/content/docs/plugins/field-kit.mdx diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 1e19ff4c9..5c9053610 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -88,6 +88,7 @@ export default defineConfig({ { label: "Plugin Settings", slug: "plugins/settings" }, { label: "Admin UI Extensions", slug: "plugins/admin-ui" }, { label: "Block Kit", slug: "plugins/block-kit" }, + { label: "Field Kit", slug: "plugins/field-kit" }, { label: "API Routes", slug: "plugins/api-routes" }, { label: "Sandbox & Security", slug: "plugins/sandbox" }, { label: "Publishing Plugins", slug: "plugins/publishing" }, diff --git a/docs/src/content/docs/plugins/field-kit.mdx b/docs/src/content/docs/plugins/field-kit.mdx new file mode 100644 index 000000000..cc5a744b5 --- /dev/null +++ b/docs/src/content/docs/plugins/field-kit.mdx @@ -0,0 +1,243 @@ +--- +title: Field Kit +description: Composable field widgets for json fields, configured through seed options. +--- + +import { Aside } from "@astrojs/starlight/components"; + +EmDash's `json` field type stores arbitrary structured data, but the default editor is a single-line text input where you have to type raw JSON by hand. **Field Kit** is a first-party plugin that ships four composable widgets for `json` fields, configured entirely through seed `options` — no React required from site builders. + + + +## Installation + +```bash +npm i @emdash-cms/plugin-field-kit +``` + +Register the plugin in `astro.config.mjs`: + +```typescript +import { defineConfig } from "astro/config"; +import emdash from "emdash"; +import { fieldKitPlugin } from "@emdash-cms/plugin-field-kit"; + +export default defineConfig({ + integrations: [ + emdash({ + plugins: [fieldKitPlugin()], + }), + ], +}); +``` + +Then attach a widget to any `json` field by setting `widget` to `field-kit:`: + +```json +{ + "slug": "ingredients", + "type": "json", + "widget": "field-kit:list", + "options": { "fields": [...] } +} +``` + +## Widgets + +| Widget | Use for | Stored value | +|--------|---------|--------------| +| `object-form` | Inline form for flat JSON objects | `{ key: value, ... }` | +| `list` | Ordered array editor with add / remove / reorder | `[{ ... }, ...]` | +| `grid` | Rows × columns matrix | `{ rowKey: { colKey: value } }` | +| `tags` | Free-form chip/tag input | `["tag1", "tag2"]` | + +If a widget is missing its required `options` (e.g. `fields` for `object-form`/`list`, or `rows`/`columns` for `grid`), the editor renders an inline "Widget misconfigured" warning instead of a broken input — useful while iterating on seed schemas. + +### object-form + +Renders a group of typed sub-fields that store as a single JSON object. Good for fixed-shape structured data like nutrition facts or contact info. + +```json +{ + "slug": "nutrition", + "type": "json", + "widget": "field-kit:object-form", + "options": { + "collapsed": false, + "fields": [ + { "key": "calories", "label": "Calories", "type": "number", "suffix": "kcal" }, + { "key": "protein", "label": "Protein", "type": "number", "suffix": "g" }, + { "key": "fat", "label": "Fat", "type": "number", "suffix": "g" }, + { "key": "carbs", "label": "Carbs", "type": "number", "suffix": "g" } + ] + } +} +``` + +Stored value: `{ "calories": 250, "protein": 12.5, "fat": 8, "carbs": 30 }`. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `fields` | `SubFieldDef[]` | _(required)_ | Sub-field definitions — see [Sub-fields](#sub-fields). | +| `collapsed` | `boolean` | `false` | Render the group collapsed by default. | +| `helpText` | `string` | — | Help text shown below the widget. | + +### list + +An ordered array editor with add, remove, and reorder controls. Each row is a JSON object whose shape is defined by `fields`. The row header shows a summary rendered from a Mustache-style template. + +```json +{ + "slug": "ingredients", + "type": "json", + "widget": "field-kit:list", + "options": { + "itemLabel": "Ingredient", + "min": 1, + "max": 50, + "sortable": true, + "summary": "{{name}} — {{amount}}", + "fields": [ + { "key": "name", "label": "Name", "type": "text", "required": true }, + { "key": "amount", "label": "Amount", "type": "text" }, + { "key": "optional", "label": "Optional", "type": "boolean" } + ] + } +} +``` + +Stored value: + +```json +[ + { "name": "Flour", "amount": "500g", "optional": false }, + { "name": "Butter", "amount": "200g", "optional": false } +] +``` + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `fields` | `SubFieldDef[]` | _(required)_ | Sub-field definitions for each row. | +| `itemLabel` | `string` | `"Item"` | Singular label for a row (used in the "Add" button and fallback row titles). | +| `min` | `number` | — | Minimum number of items. Below this, the remove button hides. | +| `max` | `number` | — | Maximum number of items. At this count, the add button hides. | +| `sortable` | `boolean` | `true` | Show up/down reorder buttons. | +| `summary` | `string` | — | Mustache template rendered as the collapsed-row title. See [Summary templates](#summary-templates). | +| `helpText` | `string` | — | Help text shown below the widget. | + +### grid + +A two-dimensional matrix of rows × columns. Each cell can be a toggle, text input, number input, or select. Useful for matrices like seasonal availability, price tables, or feature comparisons. + +```json +{ + "slug": "availability", + "type": "json", + "widget": "field-kit:grid", + "options": { + "cell": "toggle", + "rows": [ + { "key": "berries", "label": "Berries" }, + { "key": "stoneFruit", "label": "Stone fruit" }, + { "key": "citrus", "label": "Citrus" } + ], + "columns": [ + { "key": "spring", "label": "Spring" }, + { "key": "summer", "label": "Summer" }, + { "key": "autumn", "label": "Autumn" }, + { "key": "winter", "label": "Winter" } + ] + } +} +``` + +Stored value: + +```json +{ + "berries": { "spring": false, "summer": true, "autumn": false, "winter": false }, + "stoneFruit": { "spring": false, "summer": true, "autumn": true, "winter": false }, + "citrus": { "spring": false, "summer": false, "autumn": true, "winter": true } +} +``` + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `rows` | `GridAxisDef[]` | _(required)_ | Row definitions: `{ key, label, image? }`. | +| `columns` | `GridAxisDef[]` | _(required)_ | Column definitions: `{ key, label, image? }`. | +| `cell` | `"toggle"` \| `"text"` \| `"number"` \| `"select"` | `"toggle"` | Cell input type, applied uniformly to every cell. | +| `cellOptions` | `string[]` \| `Array<{ label, value }>` | `[]` | Required when `cell` is `"select"`. | +| `helpText` | `string` | — | Help text shown below the widget. | + +### tags + +A chip-style input for arrays of strings. Supports a fixed `suggestions` list, free-form custom values (toggleable), case transforms, and an optional `max`. + +```json +{ + "slug": "keywords", + "type": "json", + "widget": "field-kit:tags", + "options": { + "placeholder": "Add a keyword…", + "max": 10, + "transform": "lowercase", + "allowCustom": true, + "suggestions": ["vegan", "vegetarian", "gluten-free", "dairy-free", "nut-free"] + } +} +``` + +Stored value: `["vegan", "gluten-free"]`. + +Press Enter or `,` to commit a tag. Backspace on an empty input removes the last tag. Duplicate tags are silently ignored. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `placeholder` | `string` | `"Add..."` | Input placeholder shown when no tags are present. | +| `max` | `number` | — | Maximum number of tags. The input hides at the limit. | +| `suggestions` | `string[]` | `[]` | Autocomplete suggestions surfaced via a ``. | +| `allowCustom` | `boolean` | `true` | When `false`, only values from `suggestions` can be added. | +| `transform` | `"none"` \| `"lowercase"` \| `"uppercase"` \| `"trim"` | `"none"` | Normalize tags as they're added. | +| `helpText` | `string` | — | Help text shown below the widget. | + +## Sub-fields + +`object-form` and `list` accept an `options.fields` array of typed sub-field definitions. Each entry has a `key` (the JSON object key it writes to), a `label`, a `type`, and type-specific extras. + +| Sub-field type | Renders as | Notable extras | +|----------------|------------|----------------| +| `text` | Single-line input | `placeholder` | +| `textarea` | Multi-line input | `rows` (default `3`), `placeholder` | +| `number` | Numeric input | `min`, `max`, `step`, `prefix`, `suffix`, `placeholder` | +| `boolean` | Toggle switch | — | +| `select` | Dropdown | `options: string[] \| Array<{ label, value }>`, `placeholder` | +| `date` | Date input | — | +| `color` | Native color picker paired with a hex text input | — | +| `url` | URL input (HTML5 `type="url"`) | `placeholder` | + +Common props on every sub-field: `required`, `helpText`, `defaultValue`. + +## Summary templates + +The `list` widget renders each collapsed row using a Mustache-style template in `options.summary`. `{{key}}` is replaced with the row's value for that key (coerced to a string). Falsy values fall back to `"{itemLabel} {n}"`. + +``` +"summary": "{{name}} — {{amount}}" +``` + +Renders rows like `Flour — 500g`. The template is plain string substitution — no HTML, no nested expressions. + +## Data durability + +Field Kit widgets store plain JSON in the field's existing column. There are no plugin-specific tables, no foreign keys, no schema mutation. If you remove `@emdash-cms/plugin-field-kit` from your config, the data stays valid — only the editing UI changes back to the default `json` text input. + +This applies even when you change the widget shape: unknown keys on stored objects are preserved on the next write, so you can evolve a schema without losing data captured under an older field set. + +## See also + +- [Plugin Overview](/plugins/overview/) — how EmDash plugins work. +- [Creating Plugins](/plugins/creating-plugins/) — write your own field widgets if Field Kit doesn't fit. +- [Discussion #571](https://github.com/emdash-cms/emdash/discussions/571) — the proposal that led to this plugin.