Skip to content

fix(admin): move admin dialogs to Kumo's LayerDialog - #4495

Open
khoinguyenpham04 wants to merge 50 commits into
noah/kumo-version-update-12d776from
noah/dialog-layer-dialog-migration-1ba0e6
Open

khoinguyenpham04 wants to merge 50 commits into
noah/kumo-version-update-12d776from
noah/dialog-layer-dialog-migration-1ba0e6

Conversation

@khoinguyenpham04

@khoinguyenpham04 khoinguyenpham04 commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Moves the admin's dialogs to Kumo's LayerDialog. Each dialog keeps its title in a fixed header, scrolls its content in a card, and puts Cancel and the main action in a tray along the bottom. On phones, dialogs open as bottom sheets.

Inside the dialogs, groups of fields share one section heading style, selects fill their column, and paired fields stack on phones. Explanations that sat under fields are behind an info button beside the label. Errors, warnings about what an action will do, and format rules the dialog does not check stay visible. Every info button in the admin, inside dialogs or not, now opens on a tap as well as on hover and keyboard focus.

Stacked on #4308: LayerDialog first shipped in Kumo 2.14, so this lands after the Kumo 2.15 upgrade.

Converted to LayerDialog

  • ConfirmDialog (35 confirmations), and the confirmations written inline in the content list, the entry settings panel (discard, publish, move to trash), the entry lock notice and the plugin draft preview.
  • The widget area, menu, menu item, section, redirect, taxonomy, term, byline, byline field, media folder, change domain, invite user, allowed domain, plugin block, publication date and welcome dialogs.
  • The field dialog and the relation dialog.
  • The plugin permissions and uninstall dialogs, which were hand-built overlays without a focus trap or Escape handling.
  • Block Kit button confirmations in @emdash-cms/blocks.

Kept on Dialog: the media picker, media details, media upload, bulk tagging, content picker and section picker. They need more than LayerDialog's 768px, several footer buttons, or a search field pinned above a scrolling list. The calendar and user side panels are unchanged.

Behavior that comes with the design

  • Dialogs with Cancel and an action no longer show a separate × button. Cancel and Escape behave as before.
  • A confirmation's message sits under its title as the dialog's description. Without a LayerDialog.Description, Kumo describes a dialog by its whole body, so screen readers would read checkbox labels and error text as part of the message. Form dialogs get a one-sentence description for the same reason, so a screen reader doesn't read out every field type or calendar day each time one opens.
  • LayerDialog holds one action, so Delete in the byline and media folder dialogs moves to a row at the end of the dialog that says what deleting does. The invite link view offers Done and Copy invite link.
  • Creating a relationship from the field dialog opens it in a second dialog on top. Its Back button returns to the field.
  • Phone sheets can be swiped down to dismiss. The byline form and the plugin draft preview refuse the swipe, as they already refused a click outside, and so do alerts and dialogs with a save in progress.
  • A dialog takes focus itself when it opens (Base UI Drawer's behavior), unless one of its fields takes focus.
  • Info buttons open on hover, keyboard focus or a tap. A click or tap pins the tooltip open until the next press on it, a press outside (also inside a popover) or Escape. Screen readers read the note as the button's description, without opening it.
  • The byline avatar shows as a small circle with its actions beside it. The welcome dialog shows the logo above its title, with Get Started in the tray.
  • The relation form no longer says it cannot generate a slug before both side names are filled in.

Error states and interactions

Every dialog now handles mistakes, slow requests and stray presses the same way:

  • The main action stays enabled. Submitting with missing or invalid input marks each field that needs attention (aria-invalid plus its message), focuses the first, and sends nothing. Messages appear on the first submit and then follow the input. The checks mirror the server's schemas, so most mistakes never reach it, and a rule stored data may predate applies to new items only. A new select field also needs an option, and a new blocks field a block type, since editors can't fill in either without one. Length, item and relation limits must be whole numbers (2.5 used to save as 2), and a field limit whose text isn't a number, such as 1e, is pointed at instead of being dropped.
  • A server error that belongs to a field shows on that field in the admin's words: a taken slug, name or source path (CONFLICT), a reserved field slug, a domain the check rejects, an existing user. When that field was edited while the request ran, the refusal shows in the error box instead, since the field no longer holds the refused value. Other failures show in the dialog's error box and are no longer also toasted, apart from a failed save of unsaved edits before scheduling or changing the publication date.
  • Enter submits through a real form, a double press sends one request, and a dialog can't be dismissed while its request runs.
  • A press outside or a swipe down no longer discards typed input. Cancel and Escape still close the dialog, and an untouched dialog still closes on a press outside.
  • Confirmations that send a request (deleting a content type, field, comment or entry, deleting permanently, discarding draft changes, moving to trash from the editor, replacing an image with its crop, taking over a locked entry) stay open with progress and show their own failure instead of closing first. A refusal the entry lock explains closes the confirmation and hands over to the lock notice. An error left from an earlier request isn't shown when a confirmation reopens.
  • Smaller fixes: a term name that repeats under another parent suggests a free slug such as accessories-1 (the slug sent) instead of the server silently saving a different one; a MIME type typed but not added is saved with the field; menu links follow the API's own href rule, so mailto:, tel: and #top work; the plugin block URL takes any absolute URI except javascript:, data: and vbscript: (Bluesky at:// posts work); a plugin install asking for more permissions no longer reads as a failure; deleting a redirect that is already gone counts as deleted.

Shared pieces live in lib/dialog-form.ts: revealErrors and focusFirstInvalid, guardUnsavedInput for LayerDialog.Root, useConfirmedRequest for confirmations that should stay open until their request lands, useSingleFlight for submits, useConfirmPress for a confirm action a double press should run once (ConfirmDialog and the plugin permissions and uninstall dialogs share it), and useTakenValue and useValueAtFailure for putting a refusal on its field only while the field holds the refused value.
ConfirmDialog always locks while pending (its preventCloseWhilePending prop is gone), ignores a second press within half a second, and hides an error that predates its opening. DialogError puts focus back in the dialog when the control that started the request was disabled.

Supporting changes

  • DialogSection gives grouped fields one heading style, with an optional description, help button and section action.
  • HelpTooltip no longer has a hover-only mode (openOnPress is gone), so FieldHelpLabel, HelpLabel and DialogSection all open on a tap. The content editor's content language and byline help and the media details filename, alt text and focal point help, which were hover-only, now use it; the media details buttons are the standard info button, slightly smaller than their old bare icons.
  • HelpTooltip and HelpLabel put explanatory text behind an info button. Base UI doesn't tie a tooltip to its trigger, so the note is repeated in a hidden element that describes the button. FieldHelpLabel now uses HelpTooltip. Its tap-to-open mode used to open and close on the same tap, because the tooltip closed again when the hover that a tap emulates ended.
  • KumoTranslationsProvider gives LayerDialog's built-in Close and Cancel labels the admin's translations.
  • The phone-width max-width rule for centred dialogs skips LayerDialog's full-width sheets.
  • DialogError scrolls itself into view, because the actions that raise an error stay pinned below a body that scrolls on its own.

New regression tests cover these behaviors, and each fails without its fix:

  • The relation dialog. It never finished closing when its popup was replaced mid-transition, it froze defaults such as collection labels at mount, and it reported the slug error early.
  • Swipe protection on phone sheets, tested with real touch input through CDP.
  • The field dialog's save action and errors staying reachable in a long form, and the relation step's Back.
  • The invite link view.
  • The help tooltip on tap (real touch input), hover and keyboard focus, on a click that hover opened first, and on a press elsewhere in a popover, and the note as its button's description.
  • Confirmation descriptions: a dialog and an alertdialog with a checkbox and an error, and the entry lock alert after a failed take-over, are described by their message alone, and each form dialog by its one-sentence description.
  • Error states: each dialog's submit-time errors, focus and nothing-sent checks, server conflicts on their fields (and in the error box when the field was edited during the save, or as a second save starts), whole-number and unreadable limits, double presses (including the plugin dialogs), press-outside and swipe guards, and confirmations that keep their failure (ConfirmDialog.test.tsx, confirmation-failures.test.tsx on the real router, and the per-dialog files). For the review fixes, I reverted each fix and watched its test fail.

Test updates replace removed × buttons with Cancel, open tooltips to check moved text and find them by the tooltip role (their text also matches the button's hidden description), use exact label matches where an info button's name also contains the label, and use DOM clicks where Base UI's inert overlay intercepts Playwright clicks on unstyled test pages.

Type of change

  • Bug fix
  • Feature (requires an accepted feature plan or RFC)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Your connection to this change

  • I use EmDash and ran into the problem this PR addresses
  • I maintain a tool, dependency, or service that EmDash uses, and this change concerns that integration
  • A Maintainer approved this specific change before I opened the PR

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 (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to their merged design PR: not applicable, bug fix
  • I have included screenshots below if this PR changes the UI

AI assistance disclosure

  • This PR includes AI-generated code or text — model/tool: Claude Opus 5.5

Screenshots / test output

Before is #4308 (Kumo 2.15 with the old dialogs); after is this branch. Same viewport and content for each pair, light theme.

Before After
Delete redirect confirmation before the change, with the title, text and buttons in one white panel Delete redirect confirmation after the change, with the message under the title in a card and Cancel and Delete in a tray below
New redirect form before the change, with a close button and the buttons at the bottom right New redirect form after the change, with the description in the header, a full-width status code select, and Cancel and Create in the tray
Configure field dialog before the change, with a Reference heading above a narrow relationship select and a close button Configure field dialog after the change, with "Name the field and choose what it accepts." under the title, a full-width relationship select, and Cancel and Add Field in the tray
New relation step before the change, replacing the field dialog's content, with uppercase section labels and a slug error shown before any input New relation dialog after the change, opened on top of the field dialog, with sentence-case sections, info buttons beside Roles and How many, and no early slug error
Edit byline dialog before the change, scrolled to the end, with a large empty avatar box and Delete, Cancel and Save in the footer Edit byline dialog after the change, scrolled to the end, with a circular avatar, an Attribution section with an info button, and a Delete row above Cancel and Save
Edit byline field dialog before the change, with notes under Slug, Type and Translatable and a narrow Type select Edit byline field dialog after the change, with "Change how bylines fill in this detail." under the title, info buttons beside Slug, Type and Translatable, and a full-width Type select
Welcome dialog before the change, with the logo above the title and a full-width Get Started button Welcome dialog after the change, with the logo above the title and Close and Get Started in the tray

The other redesigned dialogs.

Before After
Plugin permissions dialog before the change, a hand-built overlay with dividers above and below the permission list and an outlined Accept & Update button Plugin permissions dialog after the change, with the plugin and version under the title, the permissions in a card, and Cancel and a primary Accept & Update in the tray
Uninstall plugin dialog before the change, a hand-built overlay with the message and the storage checkbox above a divider Uninstall plugin dialog after the change, with the message under the title, the storage checkbox in a card, and Cancel and Uninstall in the tray
Invite link dialog before the change, with a close button, the link cut off by an ellipsis beside a copy icon, and an outlined Done button Invite link dialog after the change, with the whole link wrapped in its box, and Done and a primary Copy invite link in the tray
Create taxonomy dialog before the change, with a close button and notes under Name and Collections Create taxonomy dialog after the change, with info buttons beside Name and Collections, Collections under a section divider, and Cancel and Create Taxonomy in the tray
Edit menu item dialog before the change, with narrow Target and Parent selects, a note under CSS classes, and outlined Cancel and Save buttons Edit menu item dialog after the change, with "Change this item's label, link and place in the menu." under the title, full-width Target and Parent selects, an info button beside CSS classes, and Cancel and a primary Save in the tray
Create widget area dialog before the change, with a close button and outlined Cancel and Create buttons Create widget area dialog after the change, with a sentence under the title saying what a widget area is, and Cancel and a primary Create in the tray
Create section dialog before the change, with a close button and a note under Slug Create section dialog after the change, with an info button beside Slug and Cancel and Create section in the tray

Error states. Before is this branch before the error-state commits (67e762fb0), so each pair shows only the error handling.

Before After
New relation dialog before, submitted with nothing chosen: Create Relation is disabled and nothing says why New relation dialog after, submitted with nothing chosen: Links to and Linked side are outlined in red with "Choose a content type." and "Enter a name.", and Create Relation stays enabled
Configure field dialog before, with an empty label: Add Field is disabled Configure field dialog after submitting an empty label: the Label field is outlined in red with "Enter a label."
New redirect dialog before, with a taken source path: the server's English message "A redirect from "/dup-source" already exists" in a banner New redirect dialog after, with a taken source path: the Source path field says "Another redirect already uses this source path. Enter a different one."
Posts list before a failed move to trash: the confirmation has closed and a "Failed to delete" toast shows in the corner Posts list after a failed move to trash: the Move to Trash? confirmation stays open with the failure inside and the action ready to retry
Add custom link dialog before, with //partner.example: the server's message "customUrl: URL must use http, https, mailto, tel, a relative path, or a fragment identifier" in a banner Add custom link dialog after, with //partner.example: the URL field says "Enter a path that starts with a single /."
Invite user dialog before, with an invalid address: only the browser's tooltip, "Please include an '@' in the email address", in English Invite user dialog after, with an invalid address: the field says "Enter a valid email address, such as name@example.com."

Help on a phone. Before is this branch without the tap fix; after is after a tap, at 390px.

Before After
Featured Image label in the content editor on a phone before the change, after a tap on its info button: no help shows Featured Image label on a phone after a tap on its info button, with "Used as the main visual for this post on listing pages and at the top of the post" in a tooltip
SEO Title field in the phone settings sheet before the change, after a tap on its info button: no help shows SEO Title field in the phone settings sheet after a tap, with "Overrides the page title in search engine results" in a tooltip
Bylines heading in the phone settings sheet before the change, after a tap on its info button: no help shows Bylines heading in the phone settings sheet after a tap, with "Shown to readers in this order." in a tooltip
Byline filter popover on a phone before the change, after a tap on the Include inferred bylines info button: no help shows Byline filter popover on a phone after a tap, with "Also match the byline linked to an entry's author when it has none assigned." in a tooltip
Alt Text field in media details on a phone before the change, with a larger bare info icon that shows nothing on a tap Alt Text field in media details on a phone after a tap on the standard info button, with "Used by screen readers and when image fails to load" in a tooltip
Help on a phone in Arabic
Featured image SEO title
Arabic content editor on a phone after a tap on the Featured Image info button, with the help in a tooltip and the layout mirrored Arabic settings sheet on a phone after a tap on the SEO Title info button, with the help in a tooltip and the label aligned to the right
Error states on a phone (390px) and in Arabic
Before After
New redirect sheet on a phone before, submitted empty, with no message in the page New redirect sheet on a phone after, submitted empty, with "Enter a source path." and "Enter a destination path." under the two fields
Arabic new redirect dialog before, submitted empty: only the browser's English tooltip "Please fill out this field." Arabic new redirect dialog after, submitted empty: both path fields are outlined in red with their messages aligned to the right; the new messages show in English until they are translated
Phone (390px)
Before After
Delete redirect confirmation on a phone before the change, a centred dialog Delete redirect confirmation on a phone after the change, a bottom sheet with a drag handle
New redirect form on a phone before the change, a centred dialog with the group field running past its edge New redirect form on a phone after the change, a bottom sheet with status code and group stacked, and Cancel and Create at the bottom
Arabic
Before After
Arabic delete redirect confirmation before the change Arabic delete redirect confirmation after the change, with Cancel on the right and Delete on the left
Arabic new redirect form before the change Arabic new redirect form after the change, mirrored with Cancel on the right and Create on the left
Arabic edit byline field dialog before the change, with notes under Slug, Type and Translatable Arabic edit byline field dialog after the change, mirrored, with info buttons beside Slug, Type and Translatable and the Slug note open in a tooltip; the new description under the title shows in English until it is translated

Test output:

  • Admin suite: 4,088 tests in 280 files pass on chore(deps): update @cloudflare/kumo to 2.15.0 #4308's latest head, and the blocks suite passes (167 tests). The new description tests fail with their fix reverted, and the SeoPanel and BylineFilter tap tests fail with the old hover-only FieldHelpLabel.
  • pnpm build, pnpm typecheck and pnpm lint:quick pass, and pnpm lint:json reports 0 diagnostics.
  • Playwright: the 26 specs that open these dialogs (184 tests, 1 skipped) pass. In the first run, 39 tests in the later specs failed when the e2e server's state file disappeared mid-run; all 39 pass on a rerun. The welcome title test needed a longer name to keep the title wrapping now that the header has no × button. These ran before the description and tap commits; CI runs them on this head.

@changeset-bot

changeset-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8528211

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

This PR includes changesets to release 12 packages
Name Type
@emdash-cms/admin Patch
@emdash-cms/blocks Patch
emdash Patch
@emdash-cms/plugin-test Patch
@emdash-cms/plugin-embeds Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/auth Patch
create-emdash Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
@emdash-cms/auth-atproto 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

@pkg-pr-new

pkg-pr-new Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-test

npm i https://pkg.pr.new/@emdash-cms/plugin-test@4495

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-loader

npm i https://pkg.pr.new/@emdash-cms/registry-loader@4495

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/@emdash-cms/registry-moderation@4495

@emdash-cms/registry-verification

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

upgrade-emdash

npm i https://pkg.pr.new/upgrade-emdash@4495

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-ai-search

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-search@4495

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 8528211

@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.

Moving the admin dialogs onto Kumo's LayerDialog is the right approach. It replaces hand-built dialog chrome with the design system's dialog and gives CapabilityConsentDialog and UninstallConfirmDialog a focus trap and Escape handling. One accessibility regression needs fixing: confirmation messages lose their association with the dialog's accessible description. Only one reviewer raised it; the other found nothing needing a fix, but its review did not mention description wiring. The other reviewer also could not check Kumo 2.15's LayerDialog props against type declarations, so it relied on the PR's passing typecheck.


Reviewed by anthropic/claude-opus-5-5 and openai/gpt-6.1-sol.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://noah-dialog-layer-dialog-migration-1ba0e6.try.emdashcms.com, https://noah-dialog-layer-dialog-migration-1ba0e6-emdash-playground.emdash-cms.workers.dev (commit 8528211)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://3d706445.try.emdashcms.com, https://3d706445-emdash-playground.emdash-cms.workers.dev 8528211 2026-11-01T01:00:35.823Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://7e404999.try.emdashcms.com, https://7e404999-emdash-playground.emdash-cms.workers.dev fadc06e 2026-11-01T00:30:09.090Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://ebd9e4ae.try.emdashcms.com, https://ebd9e4ae-emdash-playground.emdash-cms.workers.dev 66aed39 2026-10-31T22:48:16.336Z Visit the dashboard ↗
  • Build: In progress 🔵

View logs ↗
19b8637 2026-10-31T22:44:57.645Z View logs ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://50db83c8.try.emdashcms.com, https://50db83c8-emdash-playground.emdash-cms.workers.dev df4f5b8 2026-10-31T10:12:31.106Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://84877a62.try.emdashcms.com, https://84877a62-emdash-playground.emdash-cms.workers.dev 1430270 2026-10-31T00:30:56.641Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://f0cd0619.try.emdashcms.com, https://f0cd0619-emdash-playground.emdash-cms.workers.dev 42979e8 2026-10-30T17:46:52.156Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://57fc0c5f.try.emdashcms.com, https://57fc0c5f-emdash-playground.emdash-cms.workers.dev 7bce330 2026-10-30T17:17:41.875Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://d0f5c7bc.try.emdashcms.com, https://d0f5c7bc-emdash-playground.emdash-cms.workers.dev 82cc54e 2026-10-30T16:55:55.960Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://ed1890c2.try.emdashcms.com, https://ed1890c2-emdash-playground.emdash-cms.workers.dev 590303b 2026-10-30T16:10:59.543Z Visit the dashboard ↗

View all previews: View all previews ↗

@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.

Moving the admin dialogs onto Kumo's LayerDialog is the right approach. It replaces hand-built dialog chrome with the design system's dialog and adds focus trapping and Escape handling to the plugin permissions and uninstall dialogs. One accessibility regression from the previous review is still open at this head. The confirmation message in ConfirmDialog and in the inline confirmations (ContentList, ContentSettingsPanel, EntryLockNotice) is now a plain Text paragraph. It is no longer the dialog's accessible description, and no accessible-description assertions were added. Other dialogs in this PR already use LayerDialog.Description, so the fix is available. Neither reviewer found other issues. Both reviews were static and did not run tests, builds or lint, and Kumo's LayerDialog prop types could not be checked directly, so the passing typecheck is the only evidence there.


Reviewed by anthropic/claude-opus-5-5 and openai/gpt-6.1-sol. The change since the last review (1376 lines) was too large for a scoped follow-up, so this is a full review.

@danielmlr danielmlr 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.

Hi Noah, 5 items that are worth to look at:

  1. Just a heads-up that it's conflicting with #4308's branch, so the last six commits haven't had a CI run yet.
  2. The notes behind the new info buttons don't reach screen readers yet, since the tooltip isn't linked to its button. Some used to be visible text, like "Slugs cannot be changed after the field is created." A hidden span with the note and aria-describedby, like the name span next to it, should fix that. A Popover with openOnHover would work too.
  3. The later commits go beyond the move and change what's inside the dialogs: section headings, notes moved behind info buttons, the avatar and the Delete row. The screenshots cover seven dialogs, so most of the redesigned ones haven't been seen yet. Before and after shots of the plugin permissions, uninstall, invite link, taxonomy, menu, widget and section dialogs would help, plus one form with info buttons in Arabic. Moving those commits into a follow-up PR would work just as well, and this one would stay the move.
  4. On phones, the form sheets (field, relation, redirect and others) can be swiped away, and what was typed is lost. The byline form's swipe guard on those dialogs feels like the safer default.
  5. On the emdashbot threads: ConfirmDialog's message is still the description, but without a Description slot the whole Body is, error text included. LayerDialog.Description would keep it to the message.

@emdashbot emdashbot Bot added review/needs-review No maintainer or bot review yet and removed review/needs-review No maintainer or bot review yet labels Oct 30, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Oct 30, 2026
LayerDialog renders its own Close and Cancel labels, so the admin now
supplies translated ones through KumoLocaleProvider.

On phones LayerDialog is a full-width bottom sheet, which the max-width
rule for centred dialogs would squeeze, so that rule now skips popups
carrying data-swipe-direction.

Its actions stay pinned below a body that scrolls on its own, so an error
they raise can land out of sight at the end of a long form. DialogError
now scrolls itself into view when a message appears.
ConfirmDialog and the confirmations written inline in the content list,
the entry settings panel, the entry lock notice and the plugin draft
preview now use LayerDialog: the title in a fixed header, the copy in a
card, and Cancel and the action in a tray. Destructive confirmations that
used role="alertdialog" use LayerDialog.Alert, and preventCloseWhilePending
maps to dismissDisabled.

ConfirmDialog drops titleClassName, descriptionClassName and compact,
because LayerDialog fixes its own typography and spacing.

Confirmations that closed through Dialog.Close now track their open state
and close themselves when confirmed. The plugin draft preview refuses a
swipe on a phone, as it already refused a press outside, so its changes
can't be thrown away by accident.
Converts the widget area, menu, menu item, section, redirect, taxonomy,
term, byline, byline field, media folder, domain, invite, allowed domain,
plugin block, publication date and welcome dialogs. Each form lives in the
body and submits through a form= button in the actions, so Enter and
native validation still work. Dismissing while a save is pending is
blocked where Cancel was disabled before.

LayerDialog has room for one action, so Delete in the byline and media
folder dialogs moves to the end of the content, and the invite link view
offers Done and Copy invite link. The byline form refuses a phone swipe,
as it already refused a press outside, so typed details survive.

The 320px scheduling test now waits for the sheet to finish sliding up
before it measures it.
…rDialog

LayerDialog's dismiss button always closes its dialog, so the relation
step's Back can no longer be a second button in the field dialog. The
relation form now opens in its own dialog on top, whose dismiss button
says Back, and returns to the field when it closes.

RelationFormPanel now renders LayerDialog content and reports saves in
flight, so RelationDialog can refuse to close under one. RelationDialog
replaces the panel as it opens rather than as it closes: replacing the
popup during its exit transition left it mounted, and building the form
at mount froze defaults such as collection labels that load later.
Both were hand-built overlays without focus trapping or Escape. They now
use LayerDialog, so focus stays inside, Escape cancels, and each is named
by its visible title instead of "Capability consent" and "Uninstall
confirmation".
Plugin admin pages ask for confirmation in the same dialog as the rest of
the admin, and the confirmation now has a real dialog title.
… dialogs

DialogSection gives grouped fields in a dialog one heading style, with a
divider above every group but the first. HelpTooltip and HelpLabel put
explanatory text behind an info button that stays out of the name of the
field whose label holds it. A tap now pins the tooltip open: the hover a
tap emulates used to close it straight away, so help buttons that open on
press never worked on touch screens.
The relation form said a slug could not be generated from the role names
as soon as the linking side was filled in, before the linked side had a
name to generate one from.
The avatar picker filled the byline dialog with a full-width drop box. It
is now a 64px circle with its Select, Replace, Edit asset and Remove
buttons beside it.
Section headings use one style instead of uppercase eyebrows and small
labels, selects fill their column, the relation slug follows the role
names it is built from, and notes that only explain move behind info
buttons. Errors, the reference conversion warning and the new relation
slug's format hint stay visible.
Delete in the byline and media folder dialogs becomes a row at the end
that says what deleting does. The byline field dialog's notes move behind
info buttons, the redirect form's fields stack on phones, and the welcome
dialog shows the logo above its title again, with Get Started in the tray.
The menu, section, taxonomy, invite, domain and widget dialogs, the plugin
block dialog and the publication date dialog use the same field spacing,
full-width selects and help buttons.

@danielmlr danielmlr 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.

Thank you!

@emdashbot emdashbot Bot added review/needs-rereview New commits since the last review and removed review/needs-rereview New commits since the last review labels Oct 31, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview New commits since the last review labels Oct 31, 2026
… into noah/dialog-layer-dialog-migration-1ba0e6

# Conflicts:
#	packages/admin/src/components/ContentList.tsx
#	packages/admin/src/components/TaxonomyManager.tsx
#	packages/admin/src/components/comments/CommentInbox.tsx
#	packages/admin/src/router.tsx
#	packages/admin/tests/components/TaxonomyManager.test.tsx
#	packages/admin/tests/routes/byline-schema.test.tsx
The bulk "Delete Permanently?" confirmation shows its failure, so the
comments page no longer also raises the bulk-action toast for a delete.
Approving, marking as spam and trashing still report a failure in the
toast, since no dialog is open for them.
@github-actions github-actions Bot added review/needs-rereview New commits since the last review and removed review/approved Approved; no new commits since labels Oct 31, 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.

Moving the admin's confirmation and form dialogs to Kumo's LayerDialog is the right approach. The earlier accessible-description findings stay resolved: ConfirmDialog, the inline confirmations, and the Block Kit confirmation all use LayerDialog.Description, and regression tests cover them. One new issue remains. Relation limits can silently save a different number than the user typed. openai/gpt-6.1-sol raised it. anthropic/claude-opus-5-5 found nothing to fix and did not examine this validation path. This was a static review only; no tests, builds, or lint were run.


Reviewed by anthropic/claude-opus-5-5 and openai/gpt-6.1-sol. The change since the last review (2629 lines) was too large for a scoped follow-up, so this is a full review.

…g the save

A dialog showed the server's "already used" refusal on its field and left
it out of the banner. When the field was edited while the request ran, it
no longer held the refused value, so the refusal showed nowhere and the
save failed without a message.

The menu, widget area, section, term, taxonomy, redirect, relation,
allowed domain, field, byline field and invite dialogs now show the
refusal on its field only if the field still held the sent value when the
answer came back, and in the banner otherwise. The field and byline field
dialogs also keep any failure in the banner when the form changed while
the save ran; a change after the failure still clears it.
The relation dialog's "At most…" limit and the field dialog's length and
item limits were read with parseInt, so 2.5 was saved as 2 and 1e3 as 1,
without a message. The form's noValidate had also turned off the browser's
step check that used to refuse 2.5 in the relation dialog.

These limits must now be whole numbers, the value sent is the value the
form checked, and the messages say "Enter a whole number of N or more."
A field limit whose text isn't a number at all, such as "1e", reads as
empty and was dropped; the field dialog now points at it with "Enter a
number." instead.
…e form

Kumo's LayerDialog makes its body the dialog's accessible description
when there is no Description, so screen readers read out the whole form,
such as every field type or every day of the calendar, each time one of
these dialogs opens.

The field, schedule, publication date, rename menu, custom link, menu
item, widget area, folder, byline field and plugin block dialogs now say
what they are for in one sentence under the title, as the other dialogs
in the admin do.
The plugin permissions and uninstall dialogs only disabled their action
while the request was pending, and both clicks of a double click can land
before that renders, so a double click sent the install, update or
uninstall twice. They now ignore a second press within the same moment,
as ConfirmDialog does; that guard moves to `useConfirmPress` so the three
dialogs share it. A double click also no longer accepts the extra
permissions or public routes an install asks for before they are shown.
…t dialog

While the delete runs, its confirmation disables its button, focus falls
to the page, and Escape reaches the edit dialog behind it as well. The
page already ignores the edit dialog's close while a delete is open; this
test fails without that guard, which no test covered.
`useSingleFlight` marked a submit as running before calling it, and a
submit that threw before returning a promise left that mark set, so every
later press of the dialog's action was ignored. It now clears the mark
and rethrows. No current caller throws this way; the helper's other
caller pattern, `useConfirmedRequest`, already guards against it.
"Couldn't take over this entry" stayed up when the lock passed from the
person the take-over was tried against to someone else, so it read as a
failed attempt against the new holder. It now clears when the holder
changes as well as when the lock's state does.
…nges dialogs

Both dialogs wrote out the same swipe refusal and pointer guard that
`guardUnsavedInput(true, …)` gives, so a later change to the guard would
have missed them. Behavior is unchanged; the swipe tests cover both.
@emdashbot emdashbot Bot added review/needs-rereview New commits since the last review and removed review/needs-rereview New commits since the last review labels Nov 1, 2026
The field, byline field and invite dialogs moved focus to the first
invalid field whenever the server refused the slug or address, even when
that field had been edited during the save and the refusal went to the
banner instead. Focus then jumped away from the field being typed in,
such as to a Label that had been cleared. They now move focus only when
the field shows the refusal.

@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.

Moving the admin's confirmation and form dialogs to Kumo's LayerDialog is the right approach, and keeping Dialog for the pickers and wide dialogs is a reasonable split. Both reviewers approved with no new findings. The earlier items are resolved at this head. Relation and field limits now reject fractional values instead of truncating them, and the value sent is the value that was checked. The confirmations, inline confirmations and Block Kit confirmation render their message through LayerDialog.Description, with tests covering the accessible description. This was a static review only; no tests, builds or lint were run.


Reviewed by anthropic/claude-opus-5-5 and openai/gpt-6.1-sol. The change since the last review (1083 lines) was too large for a scoped follow-up, so this is a full review.

The byline field and invite dialogs compared the value kept from a
failed save with what was sent last. Pressing the action again replaces
that a moment before the old failure clears, so for one render a value
just sent was marked as already used and focus moved onto it, though the
server had refused the earlier value. The dialogs now keep what was sent
together with the failure, so a new submit can't change the comparison.
The field dialog gets the same change, although its page clears the
failure in time.

@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.

The follow-up is the right change: snapshotting both the submitted value and the form at failure prevents an old refusal from being reassigned when another save starts, without changing the dialog architecture.

I reviewed every hunk since the previous review, traced the shared failure-snapshot helper and parent save handlers, and checked the updated focus and retry regression tests. The earlier integer-validation and accessible-description findings remain resolved; the author's responses are addressed. I found no new issues to fix in this follow-up.

Static review only; no tests, builds, or lint were run.


Follow-up review by openai/gpt-6.1-sol, scoped to the changes since the last review.

@emdashbot emdashbot Bot added review/needs-rereview New commits since the last review and removed review/needs-rereview New commits since the last review labels Nov 1, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview New commits since the last review labels Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants