diff --git a/.changeset/align-editor-sidebar-handles.md b/.changeset/align-editor-sidebar-handles.md new file mode 100644 index 0000000000..bfd7e32821 --- /dev/null +++ b/.changeset/align-editor-sidebar-handles.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/admin": patch +--- + +Fixes vertical alignment of editor sidebar drag handles with standard and collapsible section headings. diff --git a/.changeset/stabilize-seo-description-layout.md b/.changeset/stabilize-seo-description-layout.md new file mode 100644 index 0000000000..95bd31b119 --- /dev/null +++ b/.changeset/stabilize-seo-description-layout.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/admin": patch +--- + +Fixes the meta description field shifting after the first character is entered. diff --git a/e2e/tests/content-crud.spec.ts b/e2e/tests/content-crud.spec.ts index 0ad987496e..de549e0a14 100644 --- a/e2e/tests/content-crud.spec.ts +++ b/e2e/tests/content-crud.spec.ts @@ -90,6 +90,56 @@ test.describe("Content CRUD", () => { }); test.describe("Edit Content", () => { + test("vertically aligns standard section drag handles with their headings", async ({ + admin, + }) => { + await admin.goToContent("posts"); + await admin.waitForLoading(); + await admin.page.getByRole("link", { name: "First Post", exact: true }).click(); + await admin.waitForLoading(); + + const dragHandle = admin.page.getByRole("button", { + name: "Drag to reorder Publish", + }); + const section = dragHandle.locator("xpath=ancestor::section"); + const heading = section.getByRole("heading", { name: "Publish" }); + const [handleBox, headingBox] = await Promise.all([ + dragHandle.boundingBox(), + heading.boundingBox(), + ]); + + expect(handleBox).not.toBeNull(); + expect(headingBox).not.toBeNull(); + expect( + Math.abs(handleBox!.y + handleBox!.height / 2 - (headingBox!.y + headingBox!.height / 2)), + ).toBeLessThanOrEqual(1); + }); + + test("vertically aligns disclosure section drag handles with their headers", async ({ + admin, + }) => { + await admin.goToContent("posts"); + await admin.waitForLoading(); + await admin.page.getByRole("link", { name: "First Post", exact: true }).click(); + await admin.waitForLoading(); + + const dragHandle = admin.page.getByRole("button", { + name: "Drag to reorder Revisions", + }); + const section = dragHandle.locator("xpath=ancestor::section"); + const disclosureTrigger = section.getByRole("button", { name: /^Revisions/ }); + const [handleBox, triggerBox] = await Promise.all([ + dragHandle.boundingBox(), + disclosureTrigger.boundingBox(), + ]); + + expect(handleBox).not.toBeNull(); + expect(triggerBox).not.toBeNull(); + expect( + Math.abs(handleBox!.y + handleBox!.height / 2 - (triggerBox!.y + triggerBox!.height / 2)), + ).toBeLessThanOrEqual(1); + }); + test("loads existing content for editing", async ({ admin }) => { // Go to content list await admin.goToContent("posts"); diff --git a/packages/admin/src/components/SeoPanel.tsx b/packages/admin/src/components/SeoPanel.tsx index 06715be7d0..1270b65178 100644 --- a/packages/admin/src/components/SeoPanel.tsx +++ b/packages/admin/src/components/SeoPanel.tsx @@ -201,10 +201,8 @@ export function SeoPanel({ { @@ -213,6 +211,9 @@ export function SeoPanel({ rows={3} dir="auto" /> +

+ {t`${draft.description.length}/160 characters`} +

diff --git a/packages/admin/src/components/SortableContentSettingsSections.tsx b/packages/admin/src/components/SortableContentSettingsSections.tsx index 090c6c1636..75b105cbc3 100644 --- a/packages/admin/src/components/SortableContentSettingsSections.tsx +++ b/packages/admin/src/components/SortableContentSettingsSections.tsx @@ -213,7 +213,7 @@ export function SortableContentSettingsSection({ {...listeners} className={cn( "absolute z-10 grid size-7 touch-none cursor-grab place-items-center rounded-md text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-kumo-accent active:cursor-grabbing", - isSorting ? "end-3 top-1/2 -translate-y-1/2" : "top-3", + isSorting ? "end-3 top-1/2 -translate-y-1/2" : disclosure ? "top-5" : "top-3", !isSorting && "end-3", )} aria-label={t`Drag to reorder ${label}`} diff --git a/packages/admin/tests/components/SeoPanel.test.tsx b/packages/admin/tests/components/SeoPanel.test.tsx index 5a93688e0c..329919c17a 100644 --- a/packages/admin/tests/components/SeoPanel.test.tsx +++ b/packages/admin/tests/components/SeoPanel.test.tsx @@ -60,6 +60,24 @@ describe("SeoPanel", () => { } }); + it("keeps the meta description field mounted while its character count updates", async () => { + const screen = await render( + + {}} + /> + , + ); + const initialField = screen.getByLabelText("Meta Description").element(); + + await userEvent.type(initialField, "f"); + + expect(screen.getByLabelText("Meta Description").element()).toBe(initialField); + await expect.element(screen.getByText("1/160 characters")).toBeVisible(); + }); + it("renders the existing OG image preview when set", async () => { const screen = await render(