Skip to content

feat(nesting): column widths, draggable rows inside columns, and a collapsed state - #2248

Draft
bimsonz wants to merge 5 commits into
emdash-cms:mainfrom
bimsonz:feat/nesting-column-widths
Draft

feat(nesting): column widths, draggable rows inside columns, and a collapsed state#2248
bimsonz wants to merge 5 commits into
emdash-cms:mainfrom
bimsonz:feat/nesting-column-widths

Conversation

@bimsonz

@bimsonz bimsonz commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Builds on @louisescher's nesting block prototype from #2049 with the three things that stopped it being usable once I put it in front of an editor: columns could not be given different widths, the blocks inside a column could not be reordered, and a container could not be folded away.

This is opened as a draft, and is not trying to land ahead of @louisescher. His prototype commit is the base of this branch and stays attributed to him. It is opened against main rather than his branch because CI and a review pass only run on a PR to main. If he would rather these commits went onto his branch, or into a PR of his own, say so and I will move them and close this.

Related: #2049

Column widths

Equal columns cannot express a content and sidebar page, which is the most common two column layout there is. repeat(N, minmax(0, 1fr)) forces 50/50 at two columns, so a sidebar comes out the same width as the main content.

Adds a widths attribute with presets: equal, wide-first, wide-last, narrow-first, narrow-last. The weighted column takes 2fr against 1fr for the rest. Unrecognised values fall back to equal, and the existing collapse to a single column under 768px is unaffected. The control is disabled for a flex container, where columns size from their content and the site renderer ignores widths too.

Worth knowing before adding any further attributes: the admin carries its own Portable Text converters in PortableTextEditor.tsx, separate from packages/core/src/content/converters/. An attribute added to core alone passes core's round trip tests and is then dropped by every real save. That happened here, and the added test asserts a full PT to PM to PT cycle rather than a single direction.

Blocks inside a column

A block in a column had no drag handle, so it could be typed into but not reordered. TipTap only considers top level nodes unless nested targeting is on, and a block in a column is three levels deep.

The unit question is the part I am least sure about. With nested on, the default rules resolve the unit inside a structure: listItemFirstChild and listWrapperDeprioritize between them exclude a list's paragraph and its wrapper so the list item wins. That is right for a plain document. For a page built from containers a list is one row the page is made of, so this replaces them with a rule where a row is a child of the document or a child of a column.

That restates what the editor does today with nested off, and extends it one level down, so behaviour outside a container does not change. It also keeps a list movable as a block, which taking the defaults does not allow anywhere in the document once nested is on. Nothing the defaults guard is lost, because table internals and inline content are never children of the document or of a column; the tests assert that rather than assuming it, with a table cell resolving to the table and a quote's paragraph to the quote.

If you would rather keep your unit, I am happy to rework it. It is a choice about what a page is made of, not a claim that the defaults are wrong.

Two smaller pieces follow from the same decision:

  • The handle's gutter belongs to the row, not the column. Padding the column leaves the gutter outside every row's box, so a pointer there resolves to the column, which is not a drag target, and the container wins. In use that reads as the handle jumping away exactly as you reach for it. A row that indents its own content, a list or a quote, keeps that indent on top of the gutter, otherwise its markers or rule are drawn into the gutter under the handle.
  • Edge detection is off. It resolves ambiguity by pointer position, deducting by depth near a node's edge. With rows as the unit the only candidates are the row and its container, and the container has an explicit grab point in its header, so there is nothing left to resolve. Leaving it on excludes a candidate outright at the depth a row sits inside a column, and rows are often short enough that the 12px band covers half of one.

The container keeps a grab point of its own, since hovering it resolves to the deepest row inside it, but it is the header rather than a permanently visible grip.

Two duplicate grips are removed while here. Plugin and HTML blocks each drew their own at -start-8, in a gutter that does not exist inside a column, on top of the handle the editor already provides and the node wrapper's own drag target. Happy to split that out if you would rather keep this to nesting.

Folding a container away

A container is a large open box with no way to close it, so a page built from several cannot be scanned. Collapsed, it is one row reading its own name and what it holds, "2 columns, 9 blocks".

Collapsed is view state rather than a node attribute, so folding a container away is not a change to the document and does not land in a revision. The content is hidden rather than unmounted, because ProseMirror owns that element as the node's contentDOM.

The counts sit outside the translated words rather than in a plural message. A <Plural> renders its own ICU source until catalogues are extracted, which looks broken in the editor and in visual regression screenshots, and extracting catalogues is not something a feature PR should carry. The trade is that the summary is not plural agreed for richer locales, which is easy to change once the catalogue exists.

Not addressed here

  • A container inside a column is allowed by the schema and the renderer recurses, but I have not driven it in the admin.
  • Drag to reorder has no keyboard path, here or anywhere else in the editor. Not introduced by this change, but containers make it matter more.
  • The container cannot express a page grid with named regions, so a sidebar spanning specific rows is out, the way grid-row: 1 / span 3 would do it. That is a ceiling rather than a bug and is probably worth stating in the RFC.

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
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation. No messages.po changes are included.
  • I have added a changeset
  • New features link to an approved Discussion: [RFC]: Nesting Block #2049

AI-generated code disclosure

  • This PR includes AI-generated code -- model/tool: Claude Opus 5

Screenshots / test output

  • @emdash-cms/admin: 1224 passing across 100 files, 0 failing.
  • emdash: 4976 passing, 1 failing. The failure is tests/unit/astro/integration/virtual-modules.test.ts, which fails identically without this change, so it is not from it.
  • oxlint --type-aware --deny-warnings clean, oxfmt --check clean, tsgo --noEmit clean on both packages.

Worth flagging separately: packages/admin/tests/components/SeoPanel.test.tsx and tests/publish-button-locale.test.tsx each failed during full suite runs, a different test each time, and pass repeatedly in isolation. Both are untouched by this change and look non-deterministic under parallel browser load.

Behaviour was checked by driving the admin in a browser, not only through tests. Two columns measured 296/296 before the widths change and 394/197 after. With the gutter on the row, a pointer at x=801 beside a heading puts the handle at x=781, aligned to that heading and inside the row's own padding; before, the same pointer targeted the container.

@changeset-bot

changeset-bot Bot commented Jul 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: da43fca

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

This PR includes changesets to release 17 packages
Name Type
@emdash-cms/admin Minor
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Major
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@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 1,618 lines across 19 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


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


1 out of 2 committers have signed the CLA.
✅ (bimsonz)[https://github.com/bimsonz]
@louisescher
You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@github-actions github-actions Bot added query-count changed PR diff modifies query-count snapshot files cla: needed labels Jul 26, 2026
@MA2153 MA2153 added the bot:review Trigger an emdashbot code review on this PR label Jul 26, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a well-scoped follow-on to the approved nesting-block discussion: equal columns, non-reorderable nested rows, and the lack of a collapsed state are all real blockers the prototype had, and the approach here (view-state folding, row-level drag rules, duplicated admin converters so attributes survive the admin round-trip) fits EmDash’s architecture. The tests cover the core PT↔PM seam, the admin seam, and drag-unit behavior.

I checked the changed admin components, core converters/components, and tests. There are no new server routes, no DB queries, and no auth changes, so the logged-out hot path is unaffected. I did find an i18n issue and several AGENTS.md comment-discipline violations that should be cleaned up before this leaves draft.

Headline: approach is right, code is careful, but the collapsed-container summary and several new reviewer-facing/narrative comments need fixing.

Comment on lines +337 to +344
A `<Plural>` here renders its own ICU source until then, which is worse
to look at than an unagreed plural. */}
{collapsed && (
<span className="text-kumo-subtle/70 text-sm">
{columnCount} {t`columns`}
{", "}
{blockCount} {t`blocks`}
</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[needs fixing] The collapsed summary concatenates {columnCount} {t\columns`}with a literal comma and{blockCount} {t`blocks`}` (lines 341-343). That hard-codes English punctuation and word order and won’t agree for plural rules in any locale. The JSX comment above it (lines 337-340) is also framed for this review (‘until catalogs are extracted’) rather than for future readers, which violates the AGENTS.md comment rules.

Use Lingui’s plural (import from @lingui/core/macro) and drop the comment block.

Suggested change
A `<Plural>` here renders its own ICU source until then, which is worse
to look at than an unagreed plural. */}
{collapsed && (
<span className="text-kumo-subtle/70 text-sm">
{columnCount} {t`columns`}
{", "}
{blockCount} {t`blocks`}
</span>
{collapsed && (
<span className="text-kumo-subtle/70 text-sm">
{t`${columnCount} ${plural(columnCount, { one: "column", other: "columns" })}, ${blockCount} ${plural(blockCount, { one: "block", other: "blocks" })}`}
</span>
)}

Run pnpm locale:extract so the source locale gets the plural forms; that removes the ICU-source rendering issue the comment describes.

Comment on lines +67 to +105
// A stale position between transactions -- treat as top level.
return false;
}
}

/**
* The draggable unit is a row: a child of the document, or a child of a nesting
* column, which is the same thing one level down.
*
* This is what the editor already did. With nested targeting off, TipTap targets
* top level blocks, so a list drags as one block and its items do not drag at all.
* The rule restates that and extends it into columns, which is why behaviour
* outside a container is unchanged by enabling nesting.
*
* It deliberately replaces TipTap's default rules rather than joining them, and
* that is a choice about units, not a claim that they are wrong. Their defaults
* resolve the unit *inside* a structure: for a list, `listItemFirstChild` and
* `listWrapperDeprioritize` between them exclude the paragraph and the wrapper so
* the list item wins. That is right for a plain document and wrong for a page
* built from containers, where a list is one row a page is composed of and its
* items are the row's internals. The two cannot both hold, and picking theirs
* means a list can no longer be moved as a block anywhere in the document.
*
* Nothing the defaults guard is lost. Table internals and inline content are
* never children of the document or of a column, so they are excluded here by
* construction; the tests assert that rather than assuming it.
*
* Columns themselves are never a target either: `selectable: false`, and they are
* added and removed from the container's toolbar.
*/
export const _rowsOnlyRule: DragHandleRule = {
id: "emdashRowsOnly",
evaluate: ({ node, depth, $pos }) => {
const EXCLUDE = 1000;
if (node.type.name === "nestingColumn") return EXCLUDE;
if (depth <= 1) return 0;
return $pos.node(depth - 1).type.name === "nestingColumn" ? 0 : EXCLUDE;
},
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[needs fixing] This oversized doc block for _rowsOnlyRule is addressed to the reviewer (‘that is a choice about units, not a claim that they are wrong’, ‘If you would rather keep your unit…’, ‘Nothing the defaults guard is lost…’). Per AGENTS.md, comments should not justify decisions, narrate rejected alternatives, or address whoever is reviewing. The behavior is already asserted by the unit tests.

Replace it with a short rationale for future readers:

Suggested change
// A stale position between transactions -- treat as top level.
return false;
}
}
/**
* The draggable unit is a row: a child of the document, or a child of a nesting
* column, which is the same thing one level down.
*
* This is what the editor already did. With nested targeting off, TipTap targets
* top level blocks, so a list drags as one block and its items do not drag at all.
* The rule restates that and extends it into columns, which is why behaviour
* outside a container is unchanged by enabling nesting.
*
* It deliberately replaces TipTap's default rules rather than joining them, and
* that is a choice about units, not a claim that they are wrong. Their defaults
* resolve the unit *inside* a structure: for a list, `listItemFirstChild` and
* `listWrapperDeprioritize` between them exclude the paragraph and the wrapper so
* the list item wins. That is right for a plain document and wrong for a page
* built from containers, where a list is one row a page is composed of and its
* items are the row's internals. The two cannot both hold, and picking theirs
* means a list can no longer be moved as a block anywhere in the document.
*
* Nothing the defaults guard is lost. Table internals and inline content are
* never children of the document or of a column, so they are excluded here by
* construction; the tests assert that rather than assuming it.
*
* Columns themselves are never a target either: `selectable: false`, and they are
* added and removed from the container's toolbar.
*/
export const _rowsOnlyRule: DragHandleRule = {
id: "emdashRowsOnly",
evaluate: ({ node, depth, $pos }) => {
const EXCLUDE = 1000;
if (node.type.name === "nestingColumn") return EXCLUDE;
if (depth <= 1) return 0;
return $pos.node(depth - 1).type.name === "nestingColumn" ? 0 : EXCLUDE;
},
};
/**
* Drag unit: direct children of the document or of a nesting column.
* Table internals and inline content are excluded by the schema.
*/
export const _rowsOnlyRule: DragHandleRule = {

Comment on lines +107 to +131
/**
* Nested targeting, so a block inside a nesting column can be reordered.
*
* Edge detection is off, and follows from the same choice. It resolves ambiguity by
* pointer position, deducting by depth near a node's edge so the parent wins there.
* With rows as the unit there is no ambiguity left to resolve: a column is never a
* target, so the only candidates are the row and its container, and the container
* has an explicit grab point of its own in its header. Leaving it on only breaks
* things, because the deduction excludes a candidate outright at the depth a row
* sits inside a column, and rows are often short enough that the 12px band covers
* half of one.
*
* The handle's placement depends on this too. The gutter it sits in belongs to the
* row, so a target deeper than a row is measured from the wrong box and the handle
* lands over the content instead of beside it.
*
* Module level so the reference is stable: DragHandle's effect depends on it, and a
* fresh object each render re-registers the plugin.
*/
export const _nestedDragOptions = {
rules: [_rowsOnlyRule],
defaultRules: false,
edgeDetection: "none" as const,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[needs fixing] The comment block for _nestedDragOptions repeats the PR narrative about why edge detection is off and why the handle placement follows from that choice. Like the _rowsOnlyRule block, this is reviewer-facing justification rather than code documentation.

The option object is self-describing; remove the narrative:

Suggested change
/**
* Nested targeting, so a block inside a nesting column can be reordered.
*
* Edge detection is off, and follows from the same choice. It resolves ambiguity by
* pointer position, deducting by depth near a node's edge so the parent wins there.
* With rows as the unit there is no ambiguity left to resolve: a column is never a
* target, so the only candidates are the row and its container, and the container
* has an explicit grab point of its own in its header. Leaving it on only breaks
* things, because the deduction excludes a candidate outright at the depth a row
* sits inside a column, and rows are often short enough that the 12px band covers
* half of one.
*
* The handle's placement depends on this too. The gutter it sits in belongs to the
* row, so a target deeper than a row is measured from the wrong box and the handle
* lands over the content instead of beside it.
*
* Module level so the reference is stable: DragHandle's effect depends on it, and a
* fresh object each render re-registers the plugin.
*/
export const _nestedDragOptions = {
rules: [_rowsOnlyRule],
defaultRules: false,
edgeDetection: "none" as const,
};
export const _nestedDragOptions = {
rules: [_rowsOnlyRule],
defaultRules: false,
edgeDetection: "none" as const,
};

Comment on lines 87 to 91
>
{/* No grip of its own -- see PluginBlockNode. The editor's drag handle covers
every block, and -start-8 puts this one in a gutter a nesting column does
not have. */}
<div className="relative group">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[needs fixing] This JSX comment explains why a duplicate grip was removed. That context belongs in the commit message/PR description, not in a code comment. Delete the block.

Comment on lines +266 to +272
>
{/* No grip of its own: the editor's drag handle already offers one for every
block, and the wrapper above carries data-drag-handle, so a third one only
ever sat underneath the other two. It was positioned at -start-8, in the
editor's left gutter, which does not exist inside a nesting column -- there
the grip hung outside the column, over its neighbour or outside the
container entirely. */}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[needs fixing] This JSX comment justifies why the duplicate grip was removed. Per AGENTS.md, comments should not narrate rejected alternatives or address the reviewer. Delete the block.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 26, 2026
louisescher and others added 5 commits July 27, 2026 01:52
Builds on the nesting block prototype with the two things that stopped it being
usable on a real page: columns could not be given different widths, and the
blocks inside a column could not be reordered.

Column widths

Equal columns cannot express a content-plus-sidebar page, which is the most
common two-column layout there is. `repeat(N, minmax(0, 1fr))` forces 50/50 at
two columns, so a sidebar comes out the same width as the main content.

Adds a `widths` attribute with presets: equal, wide-first, wide-last,
narrow-first, narrow-last. The weighted column takes 2fr against 1fr for the
rest, which gives the familiar two-thirds/one-third split at two columns and
stays sensible above that. Unrecognised values fall back to equal, and the
existing collapse to a single column under 768px is unaffected. The control is
disabled for a flex container, where columns size from their content and neither
the editor nor the site renderer applies a ratio.

Note the admin carries its own Portable Text converters, separate from the ones
in core. An attribute added to core alone passes core's round-trip tests and is
then dropped by every real save, which is what happened here: the preset applied
on screen, saved without complaint, and came back as "Equal" on reload. Both
sides now carry it, and the new test asserts a full PT to PM to PT cycle rather
than a single direction.

Rows inside columns

A block in a column had no drag handle, so it could be typed into but not
reordered. TipTap only considers top level nodes unless nested targeting is
switched on, and a block in a column is three levels deep.

Enabling it needs very little. A block inside a column already scores full marks
under the default rules, and ties resolve to the deepest candidate, so it beats
its column and its container unaided. The one thing the defaults cannot know
about is that `nestingColumn` is `selectable: false` and must never be a drag
target, so that is the only rule added. The defaults are left on: they are what
keeps list and table internals from becoming targets, and a list item behaves
identically inside a column and outside one.

Edge detection drops the top edge and keeps the left. It deducts by depth within
12px of an edge, enough to exclude a candidate outright at the depth a row sits
inside a column. On the left that is wanted, and it is how a blockquote is
grabbed by its border. On the top it is not: rows are often 28px, so the band
covers half the row, and because a column is not a drag target the promotion
lands two levels up on the container rather than on the immediate parent the
rule is meant to prefer. This is the one shared default the PR changes.

The handle also needs somewhere to go. The editor reserves a 64px gutter down
its own edge; a column has none, so a nested handle landed on the column beside
it or outside the container. The gutter is padded onto each row rather than onto
the column, because padding the column leaves it outside every row's box and a
pointer there resolves to the column, which is not a target, so the handle jumps
away as the editor reaches for it.

The container keeps a grab point of its own, since hovering it resolves to the
deepest row inside it, but it is the header rather than a permanently visible
grip. Nothing else in the editor shows a handle when it is not hovered.

Two duplicate grips are removed while here. Plugin and HTML blocks each drew
their own at `-start-8`, in a gutter that does not exist inside a column, on top
of the handle the editor already provides and the wrapper's own drag target.

Also lets a plugin block's header wrap. Its action buttons hold their width
while hidden, so in a sidebar column they left the label a few pixels and it
broke one character per line.
A container is a large open box with no way to close it, so a page built from
several of them cannot be scanned. Adds a disclosure in the header: collapsed,
the container is one row reading its own name and what it holds, "2 columns, 9
blocks".

Collapsed is view state rather than a node attribute. Folding a container away
is not a change to the document, so it should not mark the page dirty or land in
a revision, and it resets on reload the way a disclosure is expected to.

The content is hidden rather than unmounted. ProseMirror owns that element as
the node's contentDOM, and removing it would detach the container's content from
the document.

The layout controls fold away with it, since they describe content that is not
on screen. Delete stays, so an unwanted container does not have to be opened
first.

The counts sit outside the translated words rather than inside a plural message.
A `<Plural>` renders its own ICU source until catalogues are extracted, which
looks broken in the editor and in screenshots, and extracting them here is not
something a feature PR should carry. The trade is that the summary is not plural
agreed for locales with richer rules, which is easy to change once the catalogue
exists.
The collapsed container summary was built by concatenating counts with
translated words and a literal comma, which fixes English word order and
punctuation. It is now a single message with plural forms for both counts.

The comments added across these changes carried decision rationale and
narrative about what was tried. Cut back to the invariants a reader would
otherwise get wrong: the handle gutter and its offset have to agree, the
admin's grid template mirrors core's, and the node's contentDOM stays
mounted while collapsed.
@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.

@bimsonz
bimsonz force-pushed the feat/nesting-column-widths branch from f48485c to da43fca Compare July 27, 2026 01:12
@github-actions github-actions Bot removed query-count changed PR diff modifies query-count snapshot files needs-rebase labels Jul 27, 2026
@bimsonz

bimsonz commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, this was a useful review. All five fixed, plus the comments you did not flag.

i18n. Taken, with # placeholders to match the other plural calls in the admin; byline-schema.tsx already nests one inside t the same way. I have not committed messages.po. AGENTS.md says to keep catalogue changes out of non-translation PRs since auto-extract.yml runs on merge, so I ran the extract locally to confirm the message comes out right and then reverted it. Happy to include it if you would rather.

Comments. Fixed the four, then went through every comment this branch adds and cut the rest to the same standard, about 120 lines.

One deviation worth flagging. I kept a single line on _nestedDragOptions:

/** Module level: DragHandle re-registers its plugin if this identity changes. */

The same file already carries that note on computePositionConfig, so it read as an invariant the file documents rather than narrative. Say the word and it goes.

Also rebased onto main, which resolves the conflict in the two converters (gallery and nesting each added a case, both kept) and clears the query-count diff, which was this branch sitting behind f81aa684.

@pkg-pr-new

pkg-pr-new Bot commented Jul 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2248

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2248

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2248

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2248

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2248

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2248

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2248

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: da43fca

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