Skip to content

feat(plugins): add @emdash-cms/plugin-field-kit#702

Merged
ascorbic merged 1 commit intoemdash-cms:mainfrom
ilicfilip:feat/field-kit-plugin
Apr 28, 2026
Merged

feat(plugins): add @emdash-cms/plugin-field-kit#702
ascorbic merged 1 commit intoemdash-cms:mainfrom
ilicfilip:feat/field-kit-plugin

Conversation

@ilicfilip
Copy link
Copy Markdown
Contributor

What does this PR do?

Adds @emdash-cms/plugin-field-kit — a first-party plugin with four composable field widgets for json fields, configured entirely through seed options so site builders don't need to write React to get a usable editing UI.

  • object-form — inline form for flat JSON objects (nutrition facts, contact info, etc.)
  • list — ordered array editor with add / remove / reorder
  • grid — rows × columns matrix (availability hours, price matrix, spec sheet)
  • tags — free-form tag/chip input for string arrays

Widgets use Kumo components and semantic design tokens to match the admin's visual language. Stored data is clean JSON that survives removing the plugin — no shape mutation, no new columns, no migration.

Also widens FieldDescriptor.options from Array<{ value: string; label: string }> to Array<{ value: string; label: string }> | Record<string, unknown> so plugin widgets can accept arbitrary widget config (not only enum choices), per @ascorbic's note in the discussion. The array shape for select / multiSelect continues to work unchanged — ContentEditor narrows to Array at the usage site.

Follows up on discussion #571 — maintainer confirmed direction as first-party plugin.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes — no new errors introduced; the 22 warnings + 2 errors on this branch are pre-existing on main (in infra/cache-demo/tsconfig.json, fixtures/perf-site/tsconfig.json, and other unrelated files)
  • pnpm test passes — 771 admin tests + 30 new plugin tests
  • pnpm format has been run
  • I have added tests for my changes — 30 unit tests covering render, onChange shapes, grid legacy-array normalization, and tags dedupe / max / transform
  • User-visible strings in the admin UI are wrapped for translation (Lingui t`…` ) and pnpm locale:extract has been run
  • I have added a changeset — @emdash-cms/plugin-field-kit: minor, emdash: patch, @emdash-cms/admin: patch
  • New features link to an approved Discussion: feat: field-kit — composable field widgets for json fields #571

AI-generated code disclosure

  • This PR includes AI-generated code

Screenshots / test output

Smoke-tested end-to-end in a local demo (demos/field-kit-test/ — not committed) against a products collection exercising all four widgets. JSON round-trips cleanly and every widget renders without console warnings.

Test output
Test Files  53 passed (53)     # @emdash-cms/admin
Tests       771 passed (771)

Test Files  4 passed (4)       # @emdash-cms/plugin-field-kit
Tests       30 passed (30)

Follow-ups (separate PRs)

  • visibleWhen — conditional sub-field visibility (already prototyped on ilicfilip/field-kit#2; leaving to a follow-up per discussion feedback)
  • Tier 2 widgets if there's demand

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 21, 2026

🦋 Changeset detected

Latest commit: a534ab4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@emdash-cms/plugin-field-kit Minor
emdash Patch
@emdash-cms/admin Patch
@emdash-cms/cloudflare Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

Scope check

This PR changes 3,986 lines across 33 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 21, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
packages/admin/src/locales/ar/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/de/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/en/messages.po Source changed, localizations will be marked as outdated.
packages/admin/src/locales/es-419/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/eu/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/fr/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/ja/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/ko/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/pseudo/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/pt-BR/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/zh-CN/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/zh-TW/messages.po Localization changed, will be marked as complete. 🔄️
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 21, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@702

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@702

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@702

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@702

emdash

npm i https://pkg.pr.new/emdash@702

create-emdash

npm i https://pkg.pr.new/create-emdash@702

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@702

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@702

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@702

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@702

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@702

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@702

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@702

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@702

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@702

commit: a534ab4

@ilicfilip ilicfilip force-pushed the feat/field-kit-plugin branch from a29c6b0 to 623d2e6 Compare April 21, 2026 12:29
@github-actions
Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new first‑party plugin package, @emdash-cms/plugin-field-kit, that provides configurable (seed‑driven) React field widgets for editing json fields in the admin UI, and updates the manifest/admin types to allow FieldDescriptor.options to be either enum choices or arbitrary widget config.

Changes:

  • Introduces @emdash-cms/plugin-field-kit with 4 JSON widgets (object-form, list, grid, tags) plus shared helpers and a test suite.
  • Widens FieldDescriptor.options to Array<{value,label}> | Record<string, unknown> and narrows at select/multiSelect call sites in ContentEditor.
  • Updates Lingui locale catalogs to match the shifted line numbers in ContentEditor.

Reviewed changes

Copilot reviewed 32 out of 33 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds workspace entries/dependency resolutions for the new plugin (and other workspace changes).
packages/plugins/field-kit/package.json Declares the new plugin package, peer deps, and scripts.
packages/plugins/field-kit/CHANGELOG.md Initializes plugin changelog.
packages/plugins/field-kit/tsconfig.json Adds TypeScript config for the new plugin package.
packages/plugins/field-kit/vitest.config.ts Adds Vitest config for plugin unit tests (jsdom + React).
packages/plugins/field-kit/src/index.ts Defines the plugin via definePlugin() and exports a config descriptor helper.
packages/plugins/field-kit/src/admin.tsx Exposes the plugin’s field widget components to the admin plugin loader.
packages/plugins/field-kit/src/shared/types.ts Adds shared widget/sub-field type definitions.
packages/plugins/field-kit/src/shared/utils.ts Adds normalization + template rendering utilities used by widgets.
packages/plugins/field-kit/src/shared/sub-field.tsx Implements a reusable sub-field renderer used by object-form and list.
packages/plugins/field-kit/src/widgets/object-form.tsx Implements the inline JSON object editor widget.
packages/plugins/field-kit/src/widgets/list.tsx Implements the ordered JSON array editor widget (add/remove/reorder).
packages/plugins/field-kit/src/widgets/grid.tsx Implements the rows×cols JSON matrix editor widget.
packages/plugins/field-kit/src/widgets/tags.tsx Implements the string-array tags/chips widget.
packages/plugins/field-kit/tests/object-form.test.tsx Unit tests for object-form widget behavior.
packages/plugins/field-kit/tests/list.test.tsx Unit tests for list widget behavior.
packages/plugins/field-kit/tests/grid.test.tsx Unit tests for grid widget behavior (incl. legacy array normalization).
packages/plugins/field-kit/tests/tags.test.tsx Unit tests for tags widget behavior (dedupe/max/transform, etc.).
packages/core/src/api/types.ts Widens API FieldDescriptor.options type to support plugin widget configs.
packages/admin/src/components/ContentEditor.tsx Mirrors widened FieldDescriptor.options type and narrows for select/multiSelect rendering.
packages/admin/src/locales/en/messages.po Regenerates message catalog references after ContentEditor line shifts.
packages/admin/src/locales/zh-CN/messages.po Regenerates message catalog references after ContentEditor line shifts.
packages/admin/src/locales/pt-BR/messages.po Regenerates message catalog references after ContentEditor line shifts.
packages/admin/src/locales/pseudo/messages.po Regenerates message catalog references after ContentEditor line shifts.
packages/admin/src/locales/ko/messages.po Regenerates message catalog references after ContentEditor line shifts.
packages/admin/src/locales/eu/messages.po Regenerates message catalog references after ContentEditor line shifts.
packages/admin/src/locales/es-419/messages.po Regenerates message catalog references after ContentEditor line shifts.
packages/admin/src/locales/de/messages.po Regenerates message catalog references after ContentEditor line shifts.
.changeset/field-kit-plugin-initial.md Adds changeset entries for the new plugin and related patches.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +4 to +16
* Normalize a value into a plain object keyed by sub-field definitions.
* Missing keys get their defaultValue (or undefined). Unknown keys are stripped.
*/
export function normalizeObject(value: unknown, fields: SubFieldDef[]): Record<string, unknown> {
const obj: Record<string, unknown> = {};
const source =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
for (const field of fields) {
obj[field.key] =
source[field.key] !== undefined ? source[field.key] : (field.defaultValue ?? undefined);
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeObject() explicitly strips unknown keys (only keys present in options.fields are preserved). This is data-lossy for existing JSON values that may contain extra keys (e.g. schema evolution or partially-managed objects) and contradicts the PR description claim of "no shape mutation". Consider preserving unknown keys by merging the original object into the normalized output (or add an explicit stripUnknownKeys option defaulting to false).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. normalizeObject() now starts with a shallow clone of the input (when it's a plain object) and then overlays declared-field defaults, so keys not listed in options.fields survive a round-trip. Added a test in object-form.test.tsx that asserts an unknown key stray is preserved through onChange, and flipped the previous test that had codified the strip-on-write behavior. This makes the "no shape mutation" claim in the PR body actually true.

Comment on lines +38 to +40
export function SubField({ def, value, onChange }: SubFieldProps) {
const fieldId = `field-kit-${def.key}`;

Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SubField builds its input id as field-kit-${def.key}. In the list widget, the same sub-field key repeats across multiple list items, producing duplicate IDs in the DOM and breaking label/input associations (and potentially automation/a11y tooling). Pass a stable per-instance prefix (e.g. parent field id + item index) into SubField, or accept an id prop and compose it in the parent.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. SubField now takes an explicit id prop instead of deriving field-kit-${def.key} internally. The two callers compose per-instance ids:

Added a test in object-form.test.tsx asserting sub-field ids are unique and scoped under the parent id, plus a test in list.test.tsx asserting different list items produce different sub-field ids.

Comment on lines +97 to +124
{!minimal && (
<label className="mb-1.5 block text-sm font-medium text-kumo-default">
{label}
{required && <span className="ml-0.5 text-kumo-danger">*</span>}
</label>
)}

<div className="flex min-h-9 flex-wrap items-center gap-1.5 rounded-md bg-kumo-base p-1.5 ring ring-kumo-hairline focus-within:ring-kumo-hairline">
{tags.map((tag, i) => (
<span key={`${tag}-${i}`} className="inline-flex items-center gap-1">
<Badge variant="secondary">
<span className="mr-1">{tag}</span>
<Button
variant="ghost"
shape="circle"
size="xs"
aria-label={`Remove ${tag}`}
onClick={() => removeTag(i)}
icon={<X />}
/>
</Badge>
</span>
))}

{!atLimit && (
<input
id={id}
type="text"
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rendered <label> isn’t associated with the <input> (no htmlFor, and the input has no aria-label). In minimal mode the top label is hidden entirely, leaving the input without an accessible name. Associate the label via htmlFor={id} and/or add aria-label/aria-labelledby on the input so screen readers can announce what the field is.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added htmlFor={id} to the visible label and aria-label={label} on the <input>. The aria-label is the load-bearing one for minimal mode — when the visible label is hidden, the input still announces with the field's label as its accessible name.

Comment on lines +174 to +178
<button
type="button"
className="flex flex-1 cursor-pointer items-center gap-1.5 border-none bg-transparent p-0 text-left text-sm text-kumo-default"
onClick={() => setExpandedIndex(isExpanded ? null : index)}
>
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<button> is used for the expandable row header even though the repo’s admin UI conventions require using Kumo components for standard UI controls (see AGENTS.md “Admin UI: Use Kumo Components”). Consider replacing this with a Kumo Button (e.g. variant="ghost", full-width) to keep consistent styling and built-in accessibility behaviors.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding off on swapping to Kumo Button — happy to revisit if you'd prefer. The row header is a disclosure-style trigger (chevron + summary text, where the summary itself is a user-configured mustache template), and the closest existing pattern in the admin is RepeaterField.tsx which uses a plain <div onClick> for its collapsible header — our <button type="button"> is strictly more accessible than that. Raw <button type="button"> styled with Kumo design tokens is also used routinely elsewhere in the admin (TaxonomySidebar.tsx, SetupWizard.tsx, LoginPage.tsx, etc.) alongside Kumo <Button>. If you'd prefer we swap to Kumo Button anyway for consistency, just flag it and I'll change both this and the object-form header.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside from existing patterns, is there a reason to not use the Kumo components? Does the style not work here? I'm fine with diverging if that's the case, but in general I'd say use Kumo unless there's a specific reason not too. There are definitely quite a few places in the codebase that should be using Kumo but aren't – whether because they slipped in, or because they weren't caught when we first migrated, so I wouldn't place much weight on that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point — you're right. I checked Kumo Button's actual API: variant="ghost" with the chevron passed as icon handles this cleanly, no technical blocker. I'd been fighting Kumo's defaults with border-none bg-transparent p-0 which is exactly the kind of thing that belongs on a migration follow-up list.

Switched both the list.tsx and object-form.tsx disclosure headers to Kumo Button.

Tests + visual smoke check confirm both disclosure patterns render + interact correctly.

Comment on lines +74 to +79
{!minimal && (
<button
type="button"
className="mb-2 flex cursor-pointer items-center gap-1.5 border-none bg-transparent p-0 text-sm font-medium text-kumo-default"
onClick={() => setIsOpen((o) => !o)}
>
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The collapsible header uses a raw <button> instead of a Kumo Button, which diverges from the admin UI convention of using Kumo components for standard controls (AGENTS.md “Admin UI: Use Kumo Components”). Swapping to Kumo Button helps ensure consistent focus styles, accessibility, and theming.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reasoning as the list.tsx header above — mirroring that decision here for consistency. Will switch both (or neither) based on your call.

Comment on lines +59 to +66
} else if (rowVal && typeof rowVal === "object") {
// Object format: copy known column values
const rowObj = rowVal as Record<string, unknown>;
for (const col of columns) {
if (rowObj[col.key] !== undefined) {
rowOut[col.key] = rowObj[col.key];
}
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeGrid() drops any unknown column keys when the stored value is in object form (it only copies keys listed in columns). If a site stored additional per-cell metadata or the column set changed over time, those values will be silently lost on the next write. If the goal is to keep JSON durable/removable, consider preserving unknown keys (copy the entire row object and then ensure required row/col keys exist), or gate pruning behind an explicit option.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. In the object-format branch the row is now seeded with Object.assign(rowOut, rowObj) before declared columns are overlaid, so cells added to the schema later (or managed outside the widget) aren't silently dropped on save. Added a grid test asserting an unknown cell key legacy: "keep-me" survives a toggle write.

Comment on lines +164 to +170
{items.map((item, index) => {
const isExpanded = expandedIndex === index;
return (
<div
key={index}
className={`border-b border-kumo-hairline last:border-b-0 ${
isExpanded ? "bg-kumo-tint" : ""
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This list is explicitly reorderable (sortable + move up/down), but each row uses key={index}. Using the array index as a React key for reorderable items can cause incorrect state retention and focus/DOM mismatches when items are moved. Use a stable per-item key instead (e.g. include an internal id per item in state, or maintain a parallel stable key list that isn’t persisted to the stored JSON).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The widget now maintains a parallel itemIds: string[] state array, generated with crypto.randomUUID() when items are added. It's kept in lockstep with the stored items across add/remove/reorder and used as the React key — so form state, focus, and sub-field DOM nodes now follow the moved item rather than the slot. The stable ids are never persisted to the stored JSON; the value shape stays a plain array of objects.

Considered the alternative of persisting a _key per item (as RepeaterField.tsx does), but chose ephemeral-id-in-state to keep the stored JSON shape a clean array of plain objects, matching the PR's "no shape mutation" promise. Happy to change course if you'd prefer the _key approach for consistency.

Comment on lines +85 to +89
/**
* For `select` / `multiSelect`: the list of enum choices.
* For `json` fields driven by a plugin `widget`: arbitrary widget config.
*/
options?: Array<{ value: string; label: string }> | Record<string, unknown>;
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FieldDescriptor.options is widened here to allow plugin widgets to receive arbitrary config objects, but the admin-side manifest type (packages/admin/src/lib/api/client.tsAdminManifest.collections[].fields[].options) is still typed as an enum array only. This creates a type mismatch and makes it easy for future code to accidentally treat plugin widget options as Array everywhere. Consider updating the shared manifest/client types to match this union so the admin consumes plugin widget config safely and consistently.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Widened AdminManifest.collections[].fields[].options in packages/admin/src/lib/api/client.ts to match the union I landed in FieldDescriptor.optionsArray<{ value: string; label: string }> | Record<string, unknown>. Good catch; I widened two of the three sites originally and this was the one I missed.

Comment thread pnpm-lock.yaml Outdated
Comment on lines +241 to +271
demos/field-kit-test:
dependencies:
'@astrojs/node':
specifier: 'catalog:'
version: 10.0.0(astro@6.0.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.55.2)(tsx@4.21.0)(typescript@6.0.0-beta)(yaml@2.8.2))
'@astrojs/react':
specifier: 'catalog:'
version: 5.0.0(@types/node@24.10.13)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(jiti@2.6.1)(lightningcss@1.31.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tsx@4.21.0)(yaml@2.8.2)
'@emdash-cms/plugin-field-kit':
specifier: workspace:*
version: link:../../packages/plugins/field-kit
astro:
specifier: 'catalog:'
version: 6.0.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.55.2)(tsx@4.21.0)(typescript@6.0.0-beta)(yaml@2.8.2)
better-sqlite3:
specifier: 'catalog:'
version: 12.8.0
emdash:
specifier: workspace:*
version: link:../../packages/core
react:
specifier: 'catalog:'
version: 19.2.4
react-dom:
specifier: 'catalog:'
version: 19.2.4(react@19.2.4)
devDependencies:
'@astrojs/check':
specifier: 'catalog:'
version: 0.9.7(prettier-plugin-astro@0.14.1)(prettier@3.8.1)(typescript@6.0.0-beta)

Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pnpm-lock.yaml now includes an importers.demos/field-kit-test workspace entry, but there is no demos/field-kit-test/ directory in the repo and it isn't listed in pnpm-workspace.yaml. This makes the lockfile inconsistent with the workspace and can cause confusing lockfile churn (and may break frozen installs depending on pnpm version). Regenerate the lockfile without this stray importer entry (or add the missing workspace package if it was intended to be committed).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The demos/field-kit-test/ directory was local-only smoke-test scaffolding I'd created during development; the demos/* workspace glob captured it when I ran pnpm install. I moved that directory out of the workspace and regenerated the lockfile — grep -n "demos/field-kit-test" pnpm-lock.yaml now returns nothing. Good catch, this would have broken --frozen-lockfile on CI.

@ilicfilip
Copy link
Copy Markdown
Contributor Author

Pushed an update addressing Copilot's review — single clean commit rebased on latest main (2862582):

Fixed (7): duplicate sub-field DOM ids in the list widget, missing accessible name on tags input in minimal mode, key={index} on the reorderable list (now uses an ephemeral crypto.randomUUID()-backed parallel array so keys follow items across reorders without polluting the stored JSON), unknown-key preservation in normalizeObject / normalizeGrid (and flipped the test that had codified the buggy strip behavior), admin manifest options type — I widened two of the three sites originally, this catches the third — and the stray demos/field-kit-test/ lockfile importer.

Left as-is (2): raw <button type="button"> for the disclosure headers in list/object-form. Reasoning in the inline replies — happy to swap to Kumo Button if you'd prefer.

Verification: typecheck + 784 admin tests + 33 plugin tests (30 + 3 new) pass. pnpm format and pnpm run locale:extract applied.

@ascorbic
Copy link
Copy Markdown
Collaborator

@ilicfilip this needs conflicts to be resolved. the easiest way is to remove the messages.po changes from the PR: the locales will be updated automatically when we merge this

@ilicfilip
Copy link
Copy Markdown
Contributor Author

@ascorbic done — force-pushed 1e2b2bb rebased onto latest main.

  • Dropped all 13 messages.po changes from this PR per your suggestion; the auto-extract job will regenerate them on merge.
  • One small fix on top: oxlint's prefer-array-fill rule has tightened on main since this branch was cut and was now flagging the Array.from(..., () => makeItemId()) call in list.tsx (false positive — makeItemId() returns a fresh crypto.randomUUID() per call, not a constant). Rewrote as a tiny for loop to keep lint clean without an inline disable.

Verified locally: 33 plugin tests + 796 admin tests pass, pnpm format applied, no new lint errors.

Comment thread packages/plugins/field-kit/package.json Outdated
@@ -0,0 +1,48 @@
{
"name": "@emdash-cms/plugin-field-kit",
"version": "0.1.0",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"version": "0.1.0",
"version": "0.0.0",

New packages need to start at 0.0.0 – I have to manually publish it the first time, before it's bumped when published in CI

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a534ab4 - set version to 0.0.0 in package.json. Also updated the matching PLUGIN_VERSION constant in src/index.ts so the runtime-reported version stays in sync with the package version. The minor bump in the changeset is unchanged, so the next CI release after your manual publish will move it to 0.1.0.

Composable field widgets for `json` fields. Four widgets configured
entirely through seed `options` — no React required from site builders:

  - object-form — inline form for flat JSON objects
  - list        — ordered array editor with add/remove/reorder
  - grid        — rows × columns matrix (toggle / text / number / select cells)
  - tags        — free-form tag/chip input for string arrays

Widgets use Kumo components and semantic design tokens to match the
admin's visual language. Stored data is clean JSON that survives
removing the plugin — no shape mutation, no new columns, no migration.

Ships with 30 unit tests covering render, onChange shapes, grid
legacy-array format normalization, and tags dedupe/max/transform.

Also widens `FieldDescriptor.options` from
`Array<{ value: string; label: string }>` to
`Array<{ value: string; label: string }> | Record<string, unknown>`
so plugin widgets can accept arbitrary widget config (not only enum
choices). The array shape for `select` / `multiSelect` continues to
work unchanged — `ContentEditor` narrows at the usage site.

Signed-off-by: Filip Ilic <ilic.filip@gmail.com>
@ilicfilip ilicfilip force-pushed the feat/field-kit-plugin branch from 1e2b2bb to a534ab4 Compare April 28, 2026 12:49
Copy link
Copy Markdown
Collaborator

@ascorbic ascorbic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Could you do a follow-up PR with docs for this

@ascorbic ascorbic enabled auto-merge (squash) April 28, 2026 12:56
@ascorbic ascorbic merged commit 0ee372a into emdash-cms:main Apr 28, 2026
36 checks passed
@emdashbot emdashbot Bot mentioned this pull request Apr 28, 2026
@ilicfilip ilicfilip deleted the feat/field-kit-plugin branch April 28, 2026 13:03
ascorbic pushed a commit that referenced this pull request Apr 28, 2026
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 <ilic.filip@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants