Commit Graph

53 Commits

Author SHA1 Message Date
Matt Kane 582823328e fix: preserve Unicode in routable content slugs (#2505)
* fix: keep routable content addressable across languages

* fix: keep invalid legacy slugs out of sitemaps

* fix: close routable publish bypasses

* fix: support slugless non-routable seeds

* fix: protect published routable slugs on update

* fix: normalize unusable slugs in seed exports

* fix: reject slugless routable schedules

* chore: clarify slug generation contract

* chore: remove editor implementation narrative

* test: keep revisionless fixture routable
2026-08-16 23:23:34 +01:00
Matt Kane 9b998224a5 fix(editor): protect inline edits from file navigation (#2502)
* fix(editor): protect inline edits from file navigation

* test(editor): rely on cancellable transfer behavior

* fix(editor): own unsupported file drag lifecycle

* fix(editor): reannounce repeated file guidance
2026-08-16 18:43:06 +01:00
Matt Kane 1c4d4f04b2 feat(core): expose outer user middleware hook (#2499)
* feat(core): wrap EmDash with user middleware

* fix(core): validate outer middleware config
2026-08-16 17:39:47 +01:00
Daniel 2b3741e3e0 test(e2e): give fixture post pages a head so component styles ship (#2415)
* test(e2e): give fixture post pages a head so component styles ship

The fixture's post page opens `<html>` and goes straight to `<body>`. Astro
has no head to inject into, so the page ships no styles at all: every scoped
style of the components it renders — `Comments`, `CommentForm` and the whole
Portable Text component tree — is dropped. Measured on the rendered page:
zero `<style>` tags and zero stylesheet links, while the elements still carry
their classes and Astro's scoping attribute.

That leaves public rendering untestable end to end. Components ship as source
and are compiled by the consuming site, so "the styles reach the page" is a
guarantee worth a test; the specs that already load this page assert text and
attributes only, and pass either way.

Add the head, and a spec that reads a component's applied style from the
browser so a regression in style delivery fails here instead of shipping.

* test(e2e): retry the cold public route before asserting styles

The spec navigated straight to the post page and asserted a computed style.
The workerd dev runner's Vite dep optimizer can transiently 500 a cold route
even after the global setup's warm-up, which covers the admin and content API
routes but not this public page. Playwright's assertion retry does not help
there: once the navigation lands on an error page the element never appears,
so the page has to be reloaded.

Wrap the navigation in the same reload loop the sibling specs use.

---------

Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-08-13 10:15:12 +01:00
MA2153 a15a226189 test(e2e): wait for dashboard metrics before asserting layout (#2434)
The two dashboard layout tests read `dashboard-metric` cards immediately
after `waitForLoading()`, which only waits on the text "Loading" and
`.animate-spin`. The dashboard renders Kumo `SkeletonLine` placeholders
while the stats query is in flight, so neither selector matches and the
wait returns straight away. `expect(await count())` takes a single
snapshot with no retry, so a slow stats response fails the run --
observed on the Cloudflare E2E shard as "Expected: >= 3, Received: 0"
with the page snapshot still showing the skeleton state.

Use `expect.poll` for the card count and a web-first visibility
assertion before the tracking test's `evaluate`, both of which retry.

Verified by delaying the dashboard stats response 3s locally: both tests
fail without these changes and pass with them.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 12:26:31 +01:00
Daniel 5a5adb79d2 fix(core): make editor text alignment render on public pages (#2410)
* fix(core): make editor text alignment render on public pages

* test(core): prove alignment CSS reaches a rendered page

The repro test asserted Astro's `data-astro-cid-*` scoping attribute as a
stand-in for "the alignment rules ship and apply". That proxy cannot fail on
the regression it guards: the attribute is stamped whenever the component
carries any scoped style, so deleting or mistyping the rules keeps it green.
The container API cannot do better — `renderToString` emits markup only, and
the compiled style module resolves to an empty string under the repro config.

Move the CSS claim to the level that can see it. A fixture page renders a
Portable Text value through the public component tree, and an e2e spec reads
the computed alignment from the browser. The repro test keeps the part it can
prove on its own: the class the Block override emits per `textAlign`.

The fixture page carries its own markup instead of a seeded entry so the
suite's shared content stays untouched, which also keeps the admin
visual-regression baselines unchanged.

Drop the comments flagged in review and lower the changeset to the effect a
reader upgrading the package can observe.
2026-08-12 10:44:21 +01:00
Scott Buscemi 534f23884f fix(ui): move Comments/CommentForm to emdash/ui/comments subpath (#2039) (#2044)
* fix(ui): move Comments/CommentForm to emdash/ui/comments subpath (#2039)

Their <style> blocks were pulled into a shared, render-blocking CSS chunk
on every page that imported anything from the emdash/ui barrel (e.g.
PortableText), because Astro scans the whole barrel module graph. Splitting
them into a dedicated entry point keeps comment CSS off pages that don't
render comments, mirroring the existing emdash/ui/search entry point.

* fix(ui): keep barrel Comments exports as deprecated (#2039)

Address ascorbic review: add emdash/ui/comments without breaking existing
emdash/ui imports. Mark barrel re-exports @deprecated (remove in 1.0),
allowlist the subpath in the public-source guard, and sync template skills.
2026-08-08 11:39:42 +01:00
Noah (Nguyen Pham) 08c8f2b68c refine(admin): unify settings pages (#2341)
* refine(admin): establish settings layout and refine settings hub

* refine(admin): migrate general settings to shared layout

* refine(admin): migrate social settings to shared layout

* refine(admin): migrate seo settings to shared layout

* refine(admin): migrate security settings to shared layout

* refine(admin): migrate allowed domains settings to shared layout

* refine(admin): migrate api token settings to shared layout

* Revert "refine(admin): migrate api token settings to shared layout"

This reverts commit d7ca580183d6347f6588e3eac891079f853ec554.

* Revert "refine(admin): migrate allowed domains settings to shared layout"

This reverts commit ef6a153b0716028d5542b62c5943c6cad59a89b9.

* Revert "refine(admin): migrate security settings to shared layout"

This reverts commit 6319b92e5a63d378e2191f361475793f86a837d5.

* Revert "refine(admin): migrate seo settings to shared layout"

This reverts commit cf06366adda1d4f9418ebc97a4520bdbcba235b6.

* Revert "refine(admin): migrate social settings to shared layout"

This reverts commit 5d868f08b4a795a35fa64274a8a25fa7099ed9d3.

* Revert "refine(admin): migrate general settings to shared layout"

This reverts commit e3121dab81a2f23e51a00c86f87295ecfa52335c.

* Revert "refine(admin): establish settings layout and refine settings hub"

This reverts commit e496f212384ed40f90aa35f3e66faedcad79b497.

* refine(admin): establish settings layout and refine settings hub

* refine(admin): improve settings hub typography and copy

* refine(admin): migrate general settings to shared layout

* refine(admin): migrate social and seo settings to shared layout

* refine(admin): migrate security and signup domain settings to shared layout

* refine(admin): match security empty state border

* refine(admin): strengthen settings empty state borders

* refine(admin): clarify settings empty state borders

* refine(admin): migrate api token settings to shared layout

* refine(admin): migrate email settings to shared layout

* refine(admin): migrate backup settings to shared layout

* refine(admin): polish api token settings layout

* refine(admin): polish backup settings actions

* refine(admin): polish settings localization and controls

* fix(admin): harden settings page interactions

* refine(admin): align general settings media rows

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(admin): clarify settings language section

* fix(admin): restore settings accessibility and e2e coverage

* fix(admin): localize passkey activity timestamps

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-05 18:08:52 +01:00
Noah (Nguyen Pham) d2c259cb7f fix(admin): normalize content lifecycle status badges (#2340)
* refine(admin): normalize publish labels

* refine(admin): normalize lifecycle status badges

* fix(admin): normalize non-draft status badges

* chore(admin): satisfy type-aware status lint

* style: format

* test(admin): update normalized publish expectations

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
2026-08-05 12:01:19 +01:00
Noah (Nguyen Pham) 1d7c06395b fix(admin): refine the welcome dialog layout (#2263)
* fix(admin): refine welcome dialog hierarchy

The first-login dialog led with a 64px logo and a 24px heading while its
only action was a default secondary button, and the role sat in a tinted
card with the label rendered in brand blue -- reading as a link. Copy was
split across four alignment axes.

Left-align the content, move a 32px logo into the header row opposite the
close button, replace the tinted card with hairline dividers, show the
role as a Badge, and promote the action to a primary button. Badge
emphasis now tracks permission reach; `info` is avoided because it
measures 3.6:1 against the dialog surface, under WCAG AA for 12px text.

Type follows the scale established in #2252 (`leading-tight` headings,
`text-sm leading-5 text-pretty` body). The previous `leading-none` title
collided with the description once a long name wrapped.

* fix(admin): stretch the welcome dialog action to full width

The dialog has a single action, so a right-aligned button left the footer
lopsided against full-width content above it.

* fix(admin): even out the welcome dialog footer

The first divider led with 20px and the second with 16px, and the
full-width action sat at 400x36, reading thin against its own width.

* chore(admin): mention the full-width action in the changeset

* chore(admin): drop the role badge justification comment

* feat(admin): add a role icon to the welcome dialog badge

* refactor(admin): use the current UserCircleIcon export

UserCircle is deprecated in favour of UserCircleIcon.

* test(admin): cover wrapped welcome title spacing

* Update e2e/tests/welcome-modal.spec.ts

Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>

---------

Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>
2026-07-28 18:21:45 +01:00
Noah (Nguyen Pham) 6a0e93a692 fix(admin): adopt Kumo's brand colours (#2262)
* fix(admin): adopt Kumo's brand blue

The classic theme pinned --color-kumo-brand to the WordPress admin blue
(#2271b1), roughly half the chroma of Kumo's own brand, so primary
buttons rendered muted.

Drop the brand, brand-hover and link overrides so they inherit Kumo and
track future updates. --color-kumo-ring goes too: nothing in the admin or
Kumo ever read it.

Brand text keeps an override. Kumo maps --text-color-kumo-brand to the
Cloudflare orange wordmark (#f6821f), which would have recoloured 62
text-kumo-brand sites -- sign-in links, verified badges, and links inside
user content -- to orange at roughly 2.6:1 on white. Pointing it at the
link colour keeps those blue at 8.5:1 light and 7.7:1 dark, up from 5.2:1.

* fix(admin): drop the brand text override

Brand text resolves to Kumo's value alongside the fill, so no brand token
stays overridden in the classic theme. Accents using text-kumo-brand now
render Kumo's orange rather than a blue.

* fix(admin): colour links with Kumo's link token

Kumo maps --text-color-kumo-brand to the Cloudflare orange wordmark. The
admin used text-kumo-brand for links and accent icons, so inheriting
Kumo's value rendered them orange at 2.5:1 against the light surface,
under the 4.5:1 needed for text and the 3:1 needed for meaningful icons.

Point all 62 call sites at text-kumo-link, the token that already means
interactive text. Measures 8.8:1, and no theme token stays overridden.

Border and background brand utilities are untouched; they read
--color-kumo-brand, which is blue in both themes.

* test(e2e): follow the renamed locale marker class

The current-locale marker moved from text-kumo-brand to text-kumo-link,
so the selector no longer matched.
2026-07-28 17:36:09 +01:00
Noah (Nguyen Pham) f81f72000b fix(admin): refine typography hierarchy (#2252)
* feat(admin): refine typography hierarchy

* docs(admin): remove typography guide
2026-07-27 13:01:51 +01:00
Matt Kane 6379873dbb fix(e2e): make visual-regression editor captures deterministic (#2192)
Blur the active element before capturing so a hydration focus race on the
TipTap toolbar cannot leave a button highlighted in some runs and not
others, which produced spurious diffs on the content-editor and content-new
screens.

Also decide visual drift from git status of the snapshots directory rather
than from a non-zero Playwright exit. A flaky render can fail the first-pass
diff yet regenerate byte-identical baselines under --update-snapshots; the
old candidate-emptiness check always saw the committed baselines and flagged
drift regardless. Now a clean tree after regeneration is treated as flake or
infra failure and keeps the check green with a warning.
2026-07-22 12:07:54 +01:00
Matt Kane 73dadc2756 test: add initial visual regression baselines (#2165)
* test: add initial visual regression baselines

* docs: update visual test gating comment
2026-07-21 07:13:57 +00:00
Matt Kane 8ed0a9b1c5 test: add visual regression suite for the admin UI (#2147)
* test: add visual regression suite for the admin UI

Adds a Playwright visual-regression suite covering key admin screens in
LTR (English) and RTL (Arabic), gated behind EMDASH_VISUAL=1. Adds the CI
trio: an untrusted measure job, a trusted report job that comments diffs,
and a reaction-gated apply job that commits accepted Linux baselines on a
maintainer 👍. Adds inert data-testid hooks to mask drift-prone timestamps
and version strings.

* fix: address review on visual regression suite

- Widen visual.yml path filter to the whole e2e tree + playwright.config
  so fixture/seed/setup changes that alter screenshots trigger a measure run
- Await document.fonts.ready without returning the non-serializable
  FontFaceSet, which would throw in Playwright
- Import the canonical ServerInfo type from the fixtures instead of
  redeclaring a narrowed copy
- Report workflow: on a bootstrap run with no prior baselines, show the
  proposed candidate baselines instead of an empty "0 screens changed" table
- Add an empty changeset (only published-package change is inert testids)

* ci: run visual regression on lockfile/manifest changes

A Playwright or Chromium version bump alters pixel output, so a
pnpm-lock.yaml or package.json change can drift committed baselines
without touching any source path in the existing filter.
2026-07-21 05:24:28 +00:00
Noah (Nguyen Pham) aa7ef096c9 fix(admin): refine focused editor experience (#2137)
* fix(admin): refine sidebar collapse control

* fix(admin): synchronize sidebar brand animation

* fix(admin): sync boot loader theme

* fix(admin): unstick editor header

* fix(admin): unify editor surface colors

* fix(admin): refine editor publishing actions

* fix(admin): standardize content editor field labels and spacing

* fix(admin): refine content editor hierarchy

* fix(admin): align featured image help tooltip

* fix(admin): align configuration loading screen

* fix(admin): correct editor label presentation

* test(e2e): align publish action labels
2026-07-20 09:57:49 +01:00
Kevin Kyburz 6c96ac8b68 fix(visual-editing): flush unsaved inline edits on pagehide (#1945)
* fix(visual-editing): flush unsaved inline edits on pagehide (#1582)

* fix(visual-editing): address review — comment wording, robust e2e persistence check

Rename the flush comment's caveat marker and harden the e2e test: wait
for the PUT request (Playwright never delivers response events for a
page that navigated away, so waitForResponse deadlocks on the keepalive
request), then poll the rendered page until the edit is persisted
before asserting on a fresh load.
2026-07-11 15:53:28 +01:00
Kevin Kyburz fbb04abcff fix(admin): use the configured Site Icon as the admin favicon (#1505)
* fix(admin): use site favicon (Site Icon) for the admin shell

* fix(admin): prefer explicit admin.favicon over Site Icon

Address review: an explicitly configured build-time admin.favicon is
admin-specific and should not be overridden by the more general Site
Icon. Flip precedence to admin.favicon -> Site Icon -> bundled mark.
The Site Icon still brands the admin for the common case where no
admin.favicon is set.

* fix(admin): skip the settings read when admin.favicon is configured

* fix: emit the Site Icon MIME type on the admin favicon link

The media-file URL has no extension, so Chromium ignores SVG favicons
without type="image/svg+xml". Adds an e2e test that sets a Site Icon
via the API and asserts the admin shell links it with its MIME type.

---------

Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-07-11 07:36:34 +01:00
Noah (Nguyen Pham) 1c15097762 Refine admin editor settings panel actions (#1924)
* refactor(admin): allow routes to opt out of Shell main padding

Routes can now declare staticData.fullBleed to drop the Shell <main>'s
default p-6 padding and page scroll, managing their own scroll regions
instead. No route opts in yet; all existing routes render unchanged.

* refactor(admin): extract ContentSettingsPanel from ContentEditor

Moves the editor sidebar — all eight sections (Publish, Ownership,
Bylines, Translations, Taxonomies, SEO, Document Outline, Revisions)
plus the block-panel swap — into ContentSettingsPanel.tsx, along with
the sidebar-only BylineCreditsEditor and AuthorSelector components.

The panel is memoized: ContentEditor re-renders on every keystroke
(formData state), and the panel subtree is expensive. Handler props
are made useCallback-stable at both layers (ContentEditor internals
and the ContentEditPage/ContentNewPage inline arrows in router.tsx)
so the memo actually holds. Verified: zero panel re-renders while
typing and across a full autosave cycle.

No visual or behavioral change.

* feat(admin): structural 3-pane editor layout on desktop

Both editor routes (/content/$collection/$id and /new) opt into the
Shell's full-bleed mode. The editor form becomes a full-height flex
row: a centered max-w-3xl editor column with its own scroll, and a
320px settings panel built on Kumo Sidebar (collapsible="none",
contained) with independent scroll. The fields card chrome is gone —
fields sit directly on the page.

The panel aside stays inside the <form>, preserving native form
association for its inputs and submit buttons. Kumo Sidebar's side
prop is physical, so it's flipped explicitly for RTL — in Arabic the
nav sits right and the panel left. Below lg the panel stacks under
the fields (unchanged interim behavior; the sheet lands next).
Distraction-free mode is unchanged: the panel stays mounted but
hidden so panel-local state survives.

Verified: nested Sidebar.Provider does not interfere with the nav
sidebar's collapse; independent scroll regions; no layout jump from
/new to the edit route; RTL mirror; distraction-free enter/exit.

* feat(admin): move editor actions into a settings panel action bar

Adds a two-tier action bar pinned above the settings panel body:
autosave status and the primary action cluster (Preview, Save,
Publish/Unpublish/Publish changes) on top; publish-state badges with
Discard changes and Live View below. The status badges move here from
the panel's Publish section, giving publish state a single home.

The bar is deliberately not memoized — it absorbs the high-frequency
props (isDirty, isSaving, isAutosaving) so they never bust the
memoized panel body. The normal-mode header cluster and the
bottom-of-form duplicate Save button are removed; the editor strip
keeps only back/title/locale-badge and the distraction-free toggle.
The distraction-free hover overlay keeps its own save/publish/exit
controls since the panel is hidden in that mode.

Verified end to end: publish, pending changes via manual save,
discard from the bar's dialog, unpublish, autosave indicator,
distraction-free overlay, and native form association of panel
inputs.

* feat(admin): settings panel visual hierarchy pass

Flattens the settings panel into the structural aside: the rounded
card chrome is gone, sections are divided by full-width hairlines,
and every section heading drops to a shared muted label style (small,
uppercase, subtle) — including the headings rendered inside
TranslationsPanel (via a new optional headingClassName prop, since
it's shared with other pages), TaxonomySidebar, DocumentOutline, and
RevisionHistory, so the panel reads as one system. RevisionHistory
also loses its own nested card.

Publish keeps its prominence through position (first section, under
the action bar) and content weight. Move to Trash moves out of the
Publish section to an isolated section at the very bottom of the
panel.

Verified in light and dark themes and in Arabic (RTL mirror). No new
user-facing strings.

* feat(admin): settings sheet below lg breakpoint

The editor's Sidebar.Provider now wraps the whole editor layout
(collapsible="offcanvas", mobileBreakpoint=1024): at lg and above the
panel renders as the contained desktop pane exactly as before; below
lg Kumo renders it as an end-side slide-in sheet with a backdrop. A
Settings button in the editor strip toggles the sheet, and the strip
also keeps a Save button and autosave indicator below lg so saving
never requires opening the sheet. The contained prop flips with the
breakpoint — the desktop pane anchors inside the provider wrapper
while the mobile sheet needs viewport-fixed positioning.

Kumo's mobile sheet renders inline in the DOM (no portal), so panel
inputs and submit buttons keep native form association with zero
extra wiring — Save from inside the sheet verified end to end.
Opening a portable-text block detail panel below lg auto-opens the
sheet and restores its prior state on close (MobileBlockSidebarSync).
The panel is unmounted (not hidden) in distraction-free mode since a
mounted offcanvas pane would leave its layout gap behind.

The test browser viewport moves to 1280x800: the default 414px width
now renders the closed sheet, making panel controls unreachable for
the tests that exercise them.

Known deviation, needs manual QA in a visible browser: Kumo's sheet
dismisses on focusout, so opening a confirm dialog from the sheet may
close the sheet underneath (modal supersedes sheet); dialog state
transitions verified headless, but Escape ordering and focus return
depend on rAF-driven transitions that headless preview throttles.

* fix(admin): widen settings panel and stop byline credit overflow

The desktop settings pane grows from 20rem to 23rem; the mobile sheet
stays at 20rem so it still fits small phone viewports. Byline credit
cards no longer overflow the panel: the name block truncates, the
action buttons wrap when tight, and the panel body clamps horizontal
overflow so no section can poke past the panel edge.

* patch: spec

* test(admin): settings panel component tests + changeset

Adds component tests for ContentSettingsPanel (section visibility per
capability flags: role gating for Ownership/Bylines, hasSeo,
supportsRevisions, portableTextEditor presence, i18n, new-item vs
edit, the block detail panel swap, and trash-last ordering) and for
SettingsActionBar (publish cluster states across draft / live /
pending-changes / scheduled, new-item trimming, Live View link, and
autosave indicator states).

Adds the changeset for the content editor layout redesign.

* fix(admin): editor action bar usability nits

Three small intuitiveness fixes from review:

- The Preview button gets its text label back ("Preview" /
  "Preview draft") instead of being icon-only.
- Live View moves out of the action bar's context tier into the
  Publish section as a full-width row under the slug — the bar's
  second tier now holds just the status badges and Discard, so a
  busy post no longer crowds three controls into one strip.
- Discard changes is available again inside distraction-free mode.

The discard confirmation is extracted into a shared
DiscardDraftDialog (used by both the action bar and the
distraction-free overlay) so the copy and behavior cannot drift —
caught by the pre-commit review.

* fix(admin): clarify editor action ownership

* fix(admin): restore editor publish panel layout

* fix(admin): consistent editor panel headings and metadata footer

Aligns every settings panel section heading (Ownership, Bylines, Taxonomies, SEO, Outline, Revisions) with the Publish heading style, and moves the Created/Updated timestamps into a separated metadata footer so they read as read-only info instead of publish controls.

* fix(admin): stop SEO canonical URL field overflowing the settings panel

Wraps the Canonical URL input like the Meta Description field so its long description text wraps within the sidebar instead of forcing horizontal overflow.

* fix(admin): stop settings panel inheriting sidebar's nowrap

The Kumo Sidebar wrapper sets whitespace-nowrap for its collapse animation, which inherited into the settings panel and stopped long SEO field descriptions (e.g. Canonical URL) from wrapping, overflowing the panel. Resets white-space on the panel root. Reverts the ineffective SeoPanel wrapper div.

* fix(admin): drop empty taxonomy section in settings panel

TaxonomySidebar returns null when no taxonomies apply to the collection, but the panel always wrapped it in a bordered p-4 block, leaving an empty section. Moves the section chrome into TaxonomySidebar so it renders nothing when empty.

* fix(admin): put status label and badges on one line

Renders the Status label inline with its badges instead of stacking them, and uses a badge for the no-drafts status for visual consistency.

* fix(admin): add breathing room between status label and badges

* refactor(admin): merge autosave status into the Save control

The settings action bar, the below-lg editor header, and the distraction-free overlay each rendered a passive autosave "Saved" indicator next to a Save button that also read "Saved" when clean -- duplicating the status. Consolidate into the single SaveButton three-state control (Saved / Save / Saving...), fold autosave into its saving state, and remove AutosaveIndicator.

* fix(admin): make saved button feedback transient

Keep the merged Save control, but make Saved a brief completion confirmation instead of the clean resting state. The button now returns to disabled Save after the confirmation pulse, and new edits or saving progress interrupt the pulse immediately.

* fix(admin): smooth Save button state transitions

Keep the Kumo button as the control, but render icon and label as a stable overlaid content stack. Crossfade the Save, Saving, and Saved states with a short ease-out blur/scale transition, preserve reduced-motion behavior, and transition only button color properties instead of using transition-all.

* fix(admin): stabilize Save button loading feedback

Delay the visual Saving state to avoid spinner flashes on fast saves, keep it visible briefly once shown, and preserve the blur/scale transition between every visible SaveButton state.

* fix(admin): keep byline actions inline

Keep byline credit actions in the card header next to the byline identity by using a fixed two-column header layout and preventing the action group from wrapping below the name.

* chore: keep editor sidebar plan local

* feat(admin): redesign editor settings actions

* fix(admin): color saved feedback as success

* feat(admin): update RevisionHistory and DocumentOutline components

* fix(admin): align editor sidebar controls

* feat(admin): update components to use Text for headings and improve layout

* feat(admin): animate editor sidebar accordions

* fix(admin): stabilize editor settings interactions

* fix(admin): scope save feedback to editor writes, keep panel mounted in distraction-free

* fix(admin): keep block-panel sheet suspended in distraction-free, isolate auxiliary writes

* fix(admin): consistent corner radius hierarchy in editor settings panel

* fix(admin): align editor panel content to a consistent 16px gutter

* fix(admin): drop double gap above the trash section

* chore(admin): trim excessive comments in editor panel

* fix(admin): serialize editor content operations

* Revert "fix(admin): serialize editor content operations"

This reverts commit 52977b9a95fd7bbf82fdf80fd1f47d4acce98a43.

* fix(admin): preserve editor operation semantics

* fix(admin): preserve editor controls across layouts

* refactor(admin): simplify editor save feedback

* fix(admin): animate save button state changes

* chore(admin): format save button changes

* fix(e2e): update selectors for editor settings panel UI

Align E2E assertions with the merged SaveButton live region, Save/Saved labels, and updated byline/revision class names.

* chore(e2e): format content-types selector update
2026-07-10 21:08:35 +01:00
Noah (Nguyen Pham) 99b8a33b2e fix(media): refine media library and asset details UX (#1824)
* fix(media): refine media library and asset details UX

* fix(media): cancel stale detail dialog close timer

* fix(media): scope active query state to local filters

* fix(media): refine view toggle tests and provider deletes
2026-07-06 11:41:00 +01:00
Kevin Kyburz 1866fa346e fix(core): send Cache-Control private, no-store on the admin shell (#1741)
* fix(core): send Cache-Control private, no-store on the admin shell

* test(e2e): assert admin shell sends Cache-Control private, no-store
2026-07-06 10:26:02 +01:00
Mason James 7009f69a02 fix(e2e): dismiss the first-login welcome modal deterministically in devBypassAuth (#1812)
The admin shell opens the welcome modal whenever its currentUser query
resolves with isFirstLogin — on a cold workerd start that can be
seconds after the sidebar renders, past dismissOnboardingModal()'s 2s
visibility window, so the modal survives and swallows the test's
clicks (actions run 28678460523, E2E Cloudflare shard 3/8).

devBypassAuth now lands the bypass on the shell-free auth/me endpoint,
clears the welcomeDismissed flag over HTTP, and only then loads the
admin shell, so the first currentUser fetch already reports
isFirstLogin: false and the modal can never mount on the standard auth
path. The UI-side dismissal stays as a fallback for flows that
authenticate outside the bypass.
2026-07-04 07:57:17 +01:00
Matt Kane b4d7228d5e fix(core): scope admin stylesheet to its route in dev (#1593)
The admin shell imported the compiled Kumo/Tailwind theme as a top-level CSS
module. In astro dev, Astro injects every CSS module in the project graph into
every page's <head>, so the admin theme leaked onto public routes and overrode
host :root tokens. Import it as ?url and emit a route-scoped <link> instead;
make the Vite styles.css alias a regex so the ?url form still resolves to dist.

Closes #1281
2026-06-23 21:52:43 +01:00
Matt Kane a623c6b7db fix(media): optimize storage-backed images behind Cloudflare Access (#1549)
Wrap Astro's image endpoint so EmDash media bytes are read straight from
the storage adapter instead of being fetched over HTTP. The stock endpoint
fetches the (absolute) media URL to load the source; on Cloudflare that is a
self-subrequest that fails behind Access / global_fetch_strictly_public,
404ing from /_image. The wrapped endpoint matches the internal media route by
pathname, reads bytes from storage (no fetch), and transforms them with sharp
(Node) or the IMAGES binding (Cloudflare); everything else delegates to the
stock endpoint unchanged.

On by default where the image service is local; opt out with images: false.
2026-06-22 07:49:33 +01:00
Matt Kane fb31240d64 fix(admin): add avatar picker to byline editor (#1250) (#1486)
The byline avatarMediaId field was supported by the model and API but had no admin control, so avatars could only be set programmatically and every UI edit cleared a previously-set value. Adds a media picker to the byline editor and includes avatarMediaId in the create and update payloads.
2026-06-15 11:45:52 +01:00
Richard Bland d6269e7eb6 feat(i18n): add en-GB (British English) locale (#1411)
* feat(i18n): add en-GB (British English) locale

Adds an English (UK) locale catalogue: British spelling and morphology
only (analyse, authorise, licence-the-noun, Align Centre, Search Engine
Optimisation, etc.). ICU placeholders untouched. 19 strings differ from
en; all other msgstr values mirror the source so coverage is 100%.
Changeset included.

Follows the documented locales.ts workflow; lingui compile passes.

* test(e2e): match exact language names in language switcher test

The en-GB locale adds a second option containing "English", so the
substring match in the language switcher test now violates Playwright's
strict mode. Match the exact accessible name instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: MEM Digital <memdigital@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 09:33:15 +01:00
Matt Kane 850c1b7e23 feat: responsive srcset for media via astro:assets (#1438)
* feat: responsive srcset for media via astro:assets

Route locally/R2-stored media through Astro's configured image service
(astro:assets) so the Image and Portable Text image components emit a
responsive srcset and modern formats instead of a single full-size img.

Auto-registers image.remotePatterns for the storage publicUrl host, the
site origin (scoped to the media route) when siteUrl is set, and a
dev-only host-agnostic media pattern so it works in astro dev with no
config. Falls back to a plain img when optimization isn't possible
(no service, unauthorized host, unknown dimensions), so existing sites
are unaffected.

* fix: address review feedback on responsive images

- Update the visual-editing e2e assertion: optimized media srcs are now
  image-service URLs (/_image, /cdn-cgi/image) that encode the media path,
  so decode before matching MEDIA_FILE_PATTERN.
- buildImageRemotePatterns: only authorize http(s) public URLs, and scope
  the pattern to the path prefix when the public URL has one (avoids
  authorizing the whole CDN host).
- Source the components' breakpoint list from the shared
  RESPONSIVE_BREAKPOINTS constant instead of duplicating it.
- Add unit coverage for buildImageRemotePatterns.

* fix: ban cross-origin escapes in toAbsoluteMediaUrl (SSRF guard)

toAbsoluteMediaUrl resolved any root-relative path against the origin,
but protocol-relative URLs (//evil.com/x) and backslash tricks
(/\evil.com) also start with / and resolve to a different origin --
a classic SSRF vector once an image-service remotePattern authorizes
the media path. Only return the absolutized URL when it stays on the
same origin; otherwise leave it untouched (and buildResponsiveImage
then skips it, since it only accepts absolute http(s) URLs).

Adds a toAbsoluteMediaUrl describe block covering same-origin
resolution, already-absolute/empty/non-path passthrough, and the
protocol-relative + backslash SSRF guards.
2026-06-12 15:36:27 +01:00
emdashbot[bot] cedfcc527d [bot] Fix #1242: Admin content edit route should preserve locale for slug-based i18n entries (#1307)
* fix(admin/core): forward locale query param through content edit route and write handlers (#1242)

* fix(admin): carry locale through new-content save navigation

The create-new flow navigated to the edit route without a locale search
param, while edit links and translation navigation now append one. That
asymmetry made the i18n e2e test's full-URL equality assertion fail when
navigating back to the default-locale entry.

* fix(core): resolve locale-update overlap with #1302 after rebase

Main's #1302 already added locale reading to the PUT content-update
route. Drop this branch's duplicate declaration (kept main's
request.url form, which the existing route test exercises) so the
merged PUT handler has a single `locale`. The branch's remaining core
contribution is locale on DELETE and the publish/unpublish/schedule/
discard-draft sub-action routes.

* Fix e2e URL assertions for locale-preserving edit route

The edit route now carries a ?locale= search param (#1242), so the
CONTENT_EDIT_URL_PATTERN assertions in the i18n, content-crud,
field-widgets, and bylines specs (which anchored on the ULID) need to
tolerate an optional query string.

Also forward locale on the content list title link, matching the pencil
edit button, so clicking the title scopes the editor to the entry's
locale instead of falling back to the default.

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <m@mk.gg>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-06-12 10:43:05 +01:00
Matt Kane f4dd4b049c test(e2e): correct the CF-skip characterizations after investigating (#1327)
Matt's point — miniflare D1 is a local SQLite file, strongly consistent, so a
read-after-write 'lag' isn't possible; a missed read would be a real code bug.

Investigated both:
- i18n translations: reproduced against live D1 — the backend is CORRECT (the
  translations API returns both siblings, translation_group is right). The
  failure is front-end render timing on the slower workerd dev runtime, not a
  D1 bug. Comment corrected.
- invite-flow: couldn't reach the user-creation path without the passkey
  ceremony; re-characterized as a possible real SQLite-vs-D1-dialect bug needing
  a browser-driven repro, no longer hand-waved as a dev consistency artifact.
2026-06-04 16:33:32 +01:00
Matt Kane 3adc6031a9 test: harden silently-passing tests, prune framework-only and duplicate ones (#1339)
* test: harden silently-passing tests, prune framework-only and duplicate ones

Audit-driven cleanup of low-value and misleading tests:

- Fix silent-skip tests that passed when the asserted behavior was broken:
  device-flow Token Revoke (missing expect before the success guard),
  theme-unit-test gutenberg conversions (assertions gated on if(post)),
  and two e2e specs (publish status, plugins/wp-import "no error") that
  passed on a blank page.
- Rewrite plugins/hooks.test.ts sorting/beforeSave tests to actually
  execute the pipeline and assert ordering, dependency order, and content
  chaining instead of only checking getHookCount.
- Strengthen field structural validators (image/file/portableText) and the
  preview DO no-op transaction test with real reject/assert cases.
- Delete input-rules.test.ts (tested vanilla TipTap, not EmDash) and remove
  tautological/duplicate tests (auth-atproto descriptor invariants,
  do-config passthrough dupes, SignupPage/MediaDetailPanel/invite-flow/
  settings-pages duplicates).

* test: address review — close sqlite handle, tighten classic-editor selector, drop stray demos types

- hooks.test.ts: close the better-sqlite3 handle in afterEach (matches repo pattern)
- theme-unit-test: select a classic-category post with no wp: block markers so
  the classic-HTML fallback path is genuinely exercised
- revert an unrelated regenerated demos/cloudflare/emdash-env.d.ts that was
  swept in accidentally; merged latest main so the already-merged #1336
  vite-config/changeset no longer show in this PR's diff
2026-06-04 13:27:11 +01:00
Matt Kane 9ab1e7807e test(e2e): run the full suite against the Cloudflare/workerd target (#1322)
* test(e2e): wire marketplace + noop sandbox into the CF fixture

Mirrors the Node fixture: reads EMDASH_MARKETPLACE_URL and uses the runtime-
agnostic noop sandbox runner (createNoopSandboxRunner, no cloudflare:workers
import) so the marketplace browse/detail specs run against the workerd target.
Avoids the CF sandbox()/LOADER-binding setup, which is only needed at plugin
install time, not for the browse UI.

* test(e2e): run the full suite against Cloudflare, sharded

Expands the CF lane from the content-types smoke to the whole e2e suite (4 shards;
fewer than the Node lane's 8 because each shard boots its own workerd dev server).
235/237 specs pass on the workerd runtime.

Skips one CF-specific failure: 'invited user appears in the users list' — after a
passkey-invite registration the user isn't read back on workerd (passes on Node,
reproducible in isolation). Suspected D1 Sessions read-after-write under miniflare;
flagged for maintainers, skipped so the lane stays green.

* test(e2e): skip i18n translation-sibling check on Cloudflare

CI surfaced a second CF-specific failure with the same write-then-read signature
as the invite-flow skip: on the freshly created FR translation page the EN sibling
isn't read back in time, so its 'Edit' link doesn't render — though the translation
is created and navigable (the other i18n specs pass). Suspected D1 Sessions
read-after-write under miniflare dev; flagged for maintainers, skipped on CF so the
lane stays green.

* ci: shard the Cloudflare e2e lane 8 ways like the Node lane

Wall-clock breakdown showed per-shard setup is only ~30s (dev-server boot + seed
+ warm); the ~8min was test execution, not boot overhead. So 4 shards left it at
~9min vs the Node lane's ~5min. Tests parallelize near-linearly — 8 shards brings
it in line.
2026-06-04 06:35:50 +01:00
Matt Kane 85f2d9fbfd test(e2e): Cloudflare/workerd e2e harness — fixture + target parametrization (#1320)
* test(e2e): add minimal Cloudflare adapter fixture

Mirrors e2e/fixture but swaps the Node adapter + SQLite for the Cloudflare
adapter + D1/R2, so astro dev runs the workerd SSR module runner. Foundation
for a Cloudflare e2e lane that catches workerd-only regressions (#1292 class).

* test(e2e): parametrize harness for the Cloudflare target

EMDASH_E2E_TARGET=cloudflare boots e2e/fixture-cloudflare under astro dev
(workerd) instead of the Node fixture. Gate setup on dev-bypass actually
returning 200 rather than the server merely responding -- the Cloudflare dev
runner's Vite dep optimizer 500s on cold start until it finishes pre-bundling.
Clear miniflare D1/R2 state per run for isolation. Existing specs run
unmodified against both targets.

* test(e2e): address review — track CF seed, fix teardown/markers, seed content server-side

Review findings on #1320:
- Commit the CF fixture's .emdash/seed.json (it was gitignored, so the fixture
  silently fell back to the default seed in a fresh checkout). Un-ignore the dir.
- global-teardown cleaned only the Node fixture path; use info.workDir so the CF
  fixture's .astro/.wrangler/.emdash artifacts are cleaned too.
- ensureFixtureDepsBuilt gated on a single marker; check all required artifacts
  per target (cloudflare + plugin-color), else a half-built tree boots a broken
  fixture.

Root-cause fix for cold-start seeding: content seeding hand-rolled per-item API
POSTs, each the first hit to a route the workerd dev runner hadn't compiled —
racing the Vite optimizer and 500ing on a cold cache. Move the plain posts/pages
into the seed file's content block (applied server-side during dev-bypass) and
resolve their ids by slug (the list GET warms the content route before the one
remaining dynamic flow, the media upload + image post). Drop the CF fixture's
astro build script (it polluted the dev run) and give the slower workerd target
a 90s test timeout.

CF: content-types 11/11 warm and green cold; node: content-types 11/11, search
20/20.

* ci: add Cloudflare/workerd e2e smoke job

Runs the content-types spec against EMDASH_E2E_TARGET=cloudflare so the foundation
is actually exercised on every PR, not dormant. Single job (no sharding) scoped to
one spec that drives the admin SPA, schema CRUD, and seeded content end-to-end on
the workerd runtime. Broad sharded coverage + paths-gating is a follow-up.

* test(e2e): warm the admin's data routes before specs run

CI's slower 2-core runner exposed a cold-start gap: the content-types page's
client-side fetch to /_emdash/api/schema/collections hit that route cold, the
workerd optimizer served a 500, and the admin rendered an empty table — failing
the first specs (even on retry, since the route was still compiling). idsBySlug
warmed the content routes but not the schema/media routes. Warm those (authed)
to a 200 before tests start, so the browser's first fetches don't race the
optimizer.
2026-06-03 23:14:12 +01:00
Mohamed Hassan 28432b9b5a feat: extensible bylines (#1258)
* feat(bylines): types and storage interfaces for custom fields

* feat(bylines): migration 041 for custom field tables

* test(migrations): assert byline-field tables in dialect-compat fresh-run

* feat(bylines): BylineSchemaRegistry with version counter

* feat(bylines): per-isolate field-defs cache + request-cache invalidation helper

* feat(bylines): atomic version bumps around schema mutations for cache coherence

* feat(bylines): hydrate customFields in BylineRepository

* feat(bylines): batched customFields hydration for getBylinesForEntries

* feat(bylines): registry helpers, error codes, and reorder slug reservation for admin API

* feat(bylines): zod schemas for byline custom-field admin API

* feat(bylines): handler layer for byline-fields and update routes

* feat(bylines): admin API routes for byline custom fields

* test(bylines): admin API coverage for byline custom fields

* fix(admin): RTL-safe spacing, Lingui placeholder, error states for byline-schema

* test(admin): byline-schema permission + sidebar visibility coverage

* feat(admin): API client for byline custom-field schema

* feat(admin): register /byline-schema route

* fix(bylines): editors can read byline field defs via schema:read

* feat(admin): custom field inputs in byline edit form

* test(admin): byline edit form forwards customFields on save

* test(e2e): byline custom fields round-trip + changeset + query-counts snapshot

* feat(bylines): accept customFields on POST create route

* feat(admin): inline byline custom fields and surface schema link in page header

* refactor(admin): drop Byline Schema entry from sidebar

* fix(bylines): translatable hydration, url scheme, atomic create+update, D1 recovery

* fix(bylines): parity-aware dirty + always-advance clean for the field-defs cache

* fix(admin): unify byline-fields cache key and harden custom-field inputs

* ci: update query-count snapshots

* fix(bylines): qualify options.value in version SQL for postgres

* fix(bylines): lint

* fix(bylines): e2e test fix

* Update packages/core/src/database/repositories/byline.ts

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>

* fix(bylines): restore success return in coerceFieldValue url case

* feat(bylines): cache field-defs promise to coalesce concurrent reads

* style: format

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-06-03 19:35:42 +01:00
Scott Buscemi 886f2d1e49 fix(admin): add search to the byline picker and remove the 100-byline cap (#1225)
* fix(admin): add search to the byline picker and remove the 100-byline cap

The content-entity byline picker was a plain Select over the first 100
bylines with no search, so bylines past the first page were unreachable
and a credited byline outside that page failed to render at all. Replace
it with a debounced server-side search (fetchBylines already supports
search + cursor) and resolve credited bylines from the saved entry so
they always render. Closes #1217.

* fix(admin): repair byline picker test harness, e2e flow, and render-phase ref

- Add QueryClientProvider to the shared admin test render wrapper so
  components using useQuery (BylineCreditsEditor) mount without throwing
  'No QueryClient set'.
- Rewrite the bylines e2e spec to drive the search-input picker (debounced
  server search + clickable result list) instead of the removed Kumo Select,
  waiting for each credit to commit and reordering via the row-scoped Up button.
- Replace render-phase ref mutation in BylineCreditsEditor with a useMemo-built
  lookup map (concurrent-safe).
- Surface a localized inline error when the byline search query errors instead
  of falling through to the empty state.
- Add missing translationGroup to the makeByline test fixture.

---------

Co-authored-by: Matt Kane <m@mk.gg>
2026-06-02 10:11:59 +01:00
Matt Kane b9cc08e755 chore: bump @cloudflare/kumo to 2.3 (#1177)
* chore: bump @cloudflare/kumo to 2.3

Migrates two internal call sites to the Kumo 2 API:

- accordion block: Collapsible refactored to a compound component.
  Use Collapsible.Root / .DefaultTrigger / .DefaultPanel instead of
  <Collapsible label=...>.
- chart block: ChartPalette.color renamed to ChartPalette.categorical.

Admin tests that asserted on Button's native title attribute now read
aria-label instead, because Kumo 2 wraps <Button title> in a Tooltip
popup rather than setting the DOM title attribute. Updated the
@cloudflare/kumo mock in the blocks renderer tests to match the new
Collapsible compound shape.

No public API changes; consumers see identical behaviour.

* test(e2e): update title selectors for Kumo 2 Button tooltip

Kumo 2.x wraps <Button title> in a Tooltip popup rather than setting a
DOM title attribute. Switch redirect and revision history e2e selectors
from button[title=...] to button[aria-label=...] / [aria-label^=...].
The components already exposed accurate aria-labels (and in the
redirects case, more specific per-row labels via the source path).
2026-05-26 16:19:42 +01:00
Matt Kane d4377338fc chore: clean up oxlint warnings across packages (#1140)
* chore: clean up oxlint warnings across packages

Addresses 47 of 56 type-aware lint warnings, leaving only the
plugin-cli pipeline.ts unsafe assertions which warrant a separate
Zod-driven PR.

Changes by category:

- preserve-caught-error: add `cause` to thrown error in registry handler
- no-unused-vars: drop unused `Link` import, prefix unused e2e vars
- no-shadow: rename locals shadowing Lingui macros (`msg`, `plural`)
- no-unnecessary-type-assertion: remove redundant casts and `!` where
  prior `typeof` checks or tsconfig settings already narrow the type
- atproto-test-utils: add to test-override file glob (test factories
  legitimately produce branded types)
- contentful-to-portable-text: add runtime type guards in types.ts
  (`isContentfulLinkPayload`, `isContentfulSysEnvelope`,
  `parseAssetFile`, `getStringField`, `getRecordField`) and use them
  throughout, replacing scattered `as` casts with validated narrowing
- adapt-sandbox-entry: extract `normalizeRouteEntry` helper, simplify
  headers normalization (`ctx.request` is always a real Request in the
  in-process adapter), narrow remaining boundary casts to single
  disable comments with rationale
- admin/api/registry: use the shared `parseApiResponse` helper for
  install endpoint; localStorage cache parse uses a type guard

* test: drop unused byline IDs in bylines.spec.ts

The `_firstBylineId` / `_secondBylineId` names read as if the values
themselves are unused, but the test actually relies on the
`createByline` side effects (the bylines need to exist before the
combobox can find them by name). Drop the bindings and add a comment
explaining the intent.
2026-05-22 11:26:08 +00:00
Matt Kane 49b66d910c fix(admin): remove sticky editor header (#955)
* fix(admin): remove sticky editor header

The sticky save header had multiple compounding problems:
- Backdrop-blur transparency let scrolled content show through, looking
  visually broken when columns scrolled at different rates
- Fragile negative-margin trick (-mx-6 -mt-6) had to perfectly cancel
  parent padding; broke when ancestors changed
- z-index conflicts with the app bar header above
- ~85px of permanent vertical chrome on every editor page

The save action is already reachable via the form's bottom Save button
and the standard Cmd+S keyboard shortcut, so the sticky variant wasn't
earning its cost. Drop it across ContentEditor, ContentTypeEditor,
SectionEditor, and the settings sub-pages. Preserve the
distraction-free mode hover-revealed overlay in ContentEditor (different
use case: keep chrome out of the way while writing).

Drops the EditorHeader `sticky` prop entirely. Updates the test to
remove the now-irrelevant sticky-class assertions.

* fix(admin): add bottom-of-form Save buttons; correct doc claims

Address PR review feedback (#955): the previous commit claimed save was
'always reachable via in-form Save button + Cmd+S' but neither was true
for ContentEditor or SectionEditor (header was the only Save button, no
Cmd+S handler exists).

- ContentEditor: add a bottom-of-form SaveButton (mirrors the header
  state, last interactive control in DOM order)
- SectionEditor: add a bottom-of-form SaveButton (after the content
  editor, before the sidebar)
- Update doc comments and changeset to describe what's actually true
  (bottom save button per editor; no Cmd+S claim)
- Update ContentEditor.test.tsx queries to use .first() since there are
  now two Save buttons matching the same role/name (both submit the
  same form, so either works for the assertion)

* fix(admin): move bottom Save button into main editor column

The previous commit put the Save button at the absolute bottom of the
form -- after both the main editing column AND the entire sidebar
(Publish/Status/Taxonomies/SEO/Outline/Revisions). Visual testing
showed users had to scroll past the whole sidebar to reach it; way too
hidden to be discoverable.

Move the bottom SaveButton inside the main column (lg:col-span-2 in
ContentEditor, col-span-8 in SectionEditor) so it appears right after
the body field, where users naturally land when they finish editing.

SectionEditor was already correct -- just the ContentEditor placement
was wrong.

* test(e2e): scope Save button queries to .first() now that editors have two

Editor pages now render two SaveButtons (one in the header, one at the
bottom of the main column). Both submit the same form, so playwright's
strict-mode click can target either, but the unscoped queries
('button:has-text("Save")', 'getByRole("button", { name: "Save" })')
match both elements and fail.

- e2e/fixtures/admin.ts: clickSave() and waitForSaveComplete() now use
  .first() on the Save / Saved button locators.
- e2e/tests/form-data-loss.spec.ts: 3 inline Save queries updated to
  .first().

Other E2E save references are either dialog-scoped (redirects), text
'Save Changes' instead of 'Save' (content-types), or on bylines panel
which has only one save button -- those are unaffected.
2026-05-08 17:27:46 +01:00
Matt Kane a4968c1057 fix(admin): replace raw <select> and <input type="search"> with Kumo components (#950)
* fix(admin): replace raw <select> and <input type="search"> with Kumo Select / Input

Migrates 11 raw <select> elements (marketplace filters/sort, redirects,
sections filter, content editor byline picker, content picker modal,
field editor type, repeater sub-field, plugin block fields, WordPress
import user mapping, bylines routes) to Kumo Select. Where a paired
<label> existed, drop it and use Select's label prop. Where options come
from plugin/runtime data, use <Select.Option> children to preserve
flexibility; where they're a fixed translatable set, use the items
prop with t-wrapped labels.

Also migrates 2 marketplace search inputs (<input type="search">) to
Kumo Input with type="search". Drops redundant border/bg/padding
classes that Kumo provides automatically.

LocaleSwitcher.tsx is intentionally custom (size variants don't fit the
Select API) and stays as a raw <select>.

* fix(admin): use Select items prop for label resolution

Visual testing revealed that <Select> with only <Select.Option> children
displays the raw value in the trigger (not the label). Kumo's <Select>
needs the items prop (or a renderValue callback) to resolve trigger
labels.

Affected:
- routes/bylines.tsx: filter type select showed raw 'all', linked-user
  select was empty/zero-width
- WordPressImport.tsx: per-user mapping select would show raw user IDs

Both now use items={...} for the label map. Also wraps the bylines
filter Select in a flex-1 container so it grows to fill the row.

* fix(admin): address PR review feedback on Select migrations

- Convert CAPABILITY_LABELS in lib/api/marketplace.ts from plain strings
  to MessageDescriptor map; describeCapability resolves via i18n._(...).
  Fixes the i18n bug where t`${label}` couldn't be extracted by Lingui.
  Updates PluginManager consumers to call t(label) on the descriptor.
- Convert ThemeMarketplaceBrowse SORT_LABELS to MessageDescriptor too,
  matching MarketplaceBrowse pattern.
- Restore w-48 width class on WordPressImport user-mapping Select to
  prevent layout shift in the per-user mapping list.
- Restore empty placeholder option ('Select...') on RepeaterField,
  PortableTextEditor DynamicSelect, and BlockKitFieldWidget select
  renderers so users can clear the field.
- Update e2e/tests/bylines.spec.ts to use combobox + option click
  pattern instead of Playwright's native selectOption (Kumo Select is
  a custom button + popover, not a native select).
2026-05-08 13:44:03 +00:00
Fermin Jimenez 71f4e7d85b feat: i18n menus and taxonomies (#916)
* feat(i18n): add support for menus and taxonomies

* fix(migration 036): block rollback on multi-locale installs

* test(migration 036): cover data remap and rollback

* fix(menus): surface name in NOT_FOUND, detect CONFLICT without locale

* fix(seed): set translation_group on menu, item, and taxonomy_def inserts

* fixup! fix(seed): set translation_group on menu, item, and taxonomy_def inserts

* style: format

* ci: update query-count snapshots

* feat(seed): i18n round-trip for menus and taxonomies

* fix: update migration 036 to resolve locale via i18n config and add regression tests for non-default locales

* style: format

* chore(menus,taxonomies): tidy module conventions

* feat: implement i18n menu cloning and migration refinements for locale-aware references

* fix: update migration references to 036 in menus and add documentation for pending tasks

* feat: extend featured_image schema with metadata and update e2e test fixtures

* ci: update query-count snapshots

* test: mock useSearch hook in MenuEditor tests

* fix(menus): use defaultLocale in CONFLICT guard and document seed item caveat

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-05-06 10:58:06 +01:00
ask-bonk[bot] 491aeec5a6 Sticky Save buttons across editors (#854)
* fix(admin): consistently place sticky Save buttons across editor pages

The Content editor, Section editor, Content Type editor, and Settings
sub-pages now render their primary save action in a sticky top-right
header so users always see whether changes are unsaved while scrolling
long forms. The existing bottom-of-form save buttons are preserved so
keyboard and screen-reader users still encounter a save action as the
last interactive control on the page (DOM order is unchanged), per
@afercia's feedback on the issue thread.

Introduces a shared `EditorHeader` component for editor pages that want
the same sticky-header pattern.

Fixes #233

* fix(admin): scope settings e2e Save locators with .first() and correct EditorHeader RTL doc

Two checks were failing on PR #854:

1. `E2E tests (7/8)` — four tests in `e2e/tests/settings-pages.spec.ts`
   failed with Playwright strict-mode locator violations:

       strict mode violation: locator('button').filter({ hasText: 'Save Social Links' })
              resolved to 2 elements
       strict mode violation: locator('button').filter({ hasText: 'Save SEO Settings' })
              resolved to 2 elements

   The new sticky-header save buttons in `SocialSettings.tsx` and
   `SeoSettings.tsx` deliberately reuse the bottom-of-form button text so
   keyboard / screen-reader DOM order ends on a save action. Both buttons
   submit the same form via the `form` attribute association, so it's
   safe to scope the locator with `.first()`.

2. Copilot reviewer pointed out that `EditorHeader.tsx`'s RTL doc comment
   claimed the component uses logical `start-*`/`end-*`/`ms-*`/`me-*`
   utilities, but the implementation uses only symmetric `-mx-*` / `px-*`
   (which are direction-agnostic). Updated the comment to describe what
   the component actually guarantees, and call out that callers passing
   directional content into the slots should use logical classes.

---------

Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-04-30 19:51:00 +01:00
simnaut 31333dc593 feat: pluggable auth providers — add AT Protocol, refactor GitHub/Google (#398)
* feat: pluggable auth providers — add AT Protocol, refactor GitHub/Google

Introduces a pluggable auth provider system and uses it to add AT Protocol
authentication as the first plugin-based provider. GitHub and Google OAuth
are refactored from hardcoded buttons into the same provider interface.

- AuthProviderDescriptor interface with admin UI, routes, and public routes
- virtual:emdash/auth-providers Vite module for distributing provider components
- Shared findOrCreateOAuthUser() in @emdash-cms/auth for consistent signup gating
- AT Protocol auth via @atcute/oauth-node-client with PKCE (public client)
- allowedDIDs and allowedHandles config with independent handle verification
  via DNS-over-HTTPS + HTTP well-known (never trusts PDS handle claims)
- Default role for new signups changed to Subscriber
- First user becomes Admin during setup regardless of provider

* fix: address PR review — TOCTOU race, ensureTable memoization

- Replace countUsers() with setup_complete option flag check in both
  GitHub/Google and ATProto OAuth callbacks to prevent concurrent
  callbacks from both claiming first-user admin role
- Memoize ensureTable() in db-store.ts with a module-level boolean
  so CREATE TABLE IF NOT EXISTS only runs once per process

* style: format

* fix: address PR review — split auth-atproto package, add provider storage, update terminology

Addresses all 10 review comments on PR #398:

- Split auth provider into @emdash-cms/auth-atproto (npm-installable),
  keep syndication plugin in @emdash-cms/plugin-atproto (marketplace)
- Add `storage` field to AuthProviderDescriptor, reuse plugin storage
  infrastructure instead of manual SQL table creation
- Rename "AT Protocol" → "Atmosphere", remove "PDS" from user-facing strings
- Forbid self-signup when no allowlists configured (except first admin)
- Fix core callback.ts import to use #db alias
- Fix env.d.ts to reference emdash/locals package

* fix: CI failures — typecheck, lint, bundle, and i18n catalog

- Fix auth-atproto typecheck by aligning tsconfig with core's settings
  (noUncheckedIndexedAccess, lib targets) and adding explicit return type
  to storage.ts to avoid cross-package type portability issue
- Add @types/react to core devDependencies for auth/types.ts React refs
- Fix plugin bundle CLI: clean stale temp dirs before bundling, and
  expand TS_EXT_RE to strip .mjs/.js/.cjs extensions from dist entries
- Fix floating promise lint error in InlinePortableTextEditor
- Regenerate Lingui catalog to include interpolated "Sign in with {0}"

* fix: update tests and plugin config for pluggable auth changes

- Fix atproto plugin: add build step, tsdown dep, and point exports to
  dist files so plugin validation passes
- Update LoginPage browser test to mock fetchAuthMode instead of the
  removed fetchManifest
- Update SetupWizard browser test: step label changed from "Passkey"
  to "Sign In"
- Update E2E tests: passkey step text changed from "Set up your passkey"
  to "Choose how to sign in"

* fix(i18n): wrap missing "Back to login" string with Lingui t tag

* fix: address PR review — remove singleton, use getPublicOrigin

Remove module-scope singleton from OAuth client (unsafe on Workers
where module vars persist between requests). Construct per-request.

Replace url.origin with getPublicOrigin() in all atproto routes so
the correct origin is used behind reverse proxies. Export
getPublicOrigin from route-utils. Narrow the EmDashConfig import in
public-url.ts to an inline interface to avoid pulling the full core
type tree into auth-atproto's typecheck.

* fix(test): add missing virtual:emdash/config mock in mcp-discovery-post test

The test was missing a mock for the virtual:emdash/config module
imported by auth middleware, causing a module resolution error.

* fix(auth-atproto): suppress no-unsafe-type-assertion lint warnings

Add eslint-disable comments for type assertions required by the
@atcute SDK's type signatures and Astro's opaque locals types.

* style: format

* fix: address PR review — use Kumo components, rename label to Atmosphere

- Replace raw HTML form elements with Kumo Input, Button, and LinkButton
  in atproto admin components
- Update GitHub and Google LoginButtons to use Kumo LinkButton
- Rename auth provider label from "AT Protocol" to "Atmosphere"
- Improve oauth-client.ts loopback comment to cite RFC 8252 §8.3
- Remove broken client-side localhost→127.0.0.1 redirect that reset
  the setup wizard; server-side normalization handles this correctly

* fix(auth-atproto): use Button not LinkButton for LoginButton

Per ascorbic review: LinkButton is for actual links; LoginButton
expands the inline sign-in form on click, so it's a Button.

* fix(auth): restore findOrCreateOAuthUser export after upstream merge

Auto-merge with upstream/main reverted our pluggable-auth refactor —
findOrCreateOAuthUser and the CanSelfSignup type were silently removed
from packages/auth/src/index.ts, and consumer.ts was rolled back to the
private findOrCreateUser shape. auth-atproto's callback route depends on
these exports, so typecheck was failing.

Restored both files from branch tip 4faa43b. The User-Agent fix from
upstream commit 1a93d51 is preserved (it was already on the branch).

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-04-27 06:57:58 +01:00
CacheMeOwside 8221c2a3a3 fix(admin): unblock the saving of Content Types settings (#601)
* unblock the saving of Content Types settings and display errors if saving fails

* adds a test case for saving Content Types setting

* restore original state after test

* make the new Toast string translatable
2026-04-17 21:50:00 +01:00
Ophir Bucai 913cb62395 feat(admin): add full RTL support to admin UI (#565)
* chore: update .gitignore, remove stale backup file

Analysis of messages.po~
The backup file is outdated:
- messages.po: 5,519 lines (159KB) - current, fully extracted
- messages.po~: 749 lines (19KB) - old backup, incomplete

Key differences:
- The ~ file has only 749 lines vs 5,519 in the current file
- The ~ file is dated Apr 14 04:14 (same timestamp, so created during the same operation)

* feat(locales): add dir to LocaleDefinition type

* feat(locales): set html dir attribute with getLocaleDir + simple tests to prevent drift from expected output

* feat(locales): add LocaleDirectionProvider for RTL support

Introduces LocaleDirectionProvider component that manages text direction
and language attributes for RTL locales like Arabic.

Implementation:
- LocaleDirectionProvider wraps Kumo's DirectionProvider
- Syncs document.documentElement.dir and lang attributes with current locale
- Integrates with useLocale hook for locale state
- Properly ordered: I18nProvider → LocaleDirectionProvider

Features:
- Automatic direction switching (LTR ↔ RTL) on locale change
- Single source of truth for locale → direction mapping
- Updates both HTML lang and dir attributes (WCAG compliance)
- Comprehensive test coverage with cleanup hooks

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* refactor(admin): use direction-aware classes in content editor

Replace non-direction-aware Tailwind classes with direction-aware equivalents
in editor components to support RTL layouts:

- pl-* → ps-* (padding inline start)
- ml-* → ms-* (margin inline start)
- mr-* → me-* (margin inline end)
- left-* → start-* (positioning - CSS only, not FloatingUI placement)
- right-* → end-* (positioning - CSS only)
- text-left → text-start (text alignment)
- border-l → border-s (border inline start)

Note: FloatingUI placement values (e.g., "left-start") remain unchanged
as they are library-specific enums, not directional CSS classes.

Files updated:
- ContentEditor.tsx (5 changes)
- PortableTextEditor.tsx (3 changes)
- RepeaterField.tsx (1 change)
- editor/DocumentOutline.tsx (5 changes)
- editor/BlockMenu.tsx (6 changes)
- editor/ImageNode.tsx (2 changes)
- editor/ImageDetailPanel.tsx (1 change)
- editor/PluginBlockNode.tsx (1 change)
- editor/DragHandleWrapper.tsx (no CSS changes - FloatingUI placement kept as-is)

* refactor(admin): use direction-aware classes in media management

Replace non-direction-aware Tailwind classes with direction-aware equivalents
in media management components to support RTL layouts:

- left-* → start-* (positioning)
- right-* → end-* (positioning)
- pl-* → ps-* (padding inline start)
- pr-* → pe-* (padding inline end)
- ml-* → ms-* (margin inline start)
- text-left → text-start (text alignment)
- text-right → text-end (text alignment)
- border-l → border-s (border inline start)

Files updated:
- MediaLibrary.tsx (11 changes)
- MediaPickerModal.tsx (8 changes)
- MediaDetailPanel.tsx (2 changes)

* refactor(admin): use direction-aware classes in content list

Replace non-direction-aware Tailwind classes with direction-aware equivalents
in content list and picker components to support RTL layouts:

- left-* → start-* (positioning)
- right-* → end-* (positioning)
- pl-* → ps-* (padding inline start)
- mr-* → me-* (margin inline end)
- text-left → text-start (text alignment)
- text-right → text-end (text alignment)

Files updated:
- ContentList.tsx (13 changes)
- ContentPickerModal.tsx (4 changes)

* refactor(admin): use direction-aware classes in users & comments

Replace non-direction-aware Tailwind classes with direction-aware equivalents
in user and comment management components to support RTL layouts:

- left-* → start-* (positioning)
- right-* → end-* (positioning)
- pl-* → ps-* (padding inline start)
- ml-* → ms-* (margin inline start)
- text-left → text-start (text alignment)
- text-right → text-end (text alignment)
- border-l → border-s (border inline start)

Files updated:
- users/UserList.tsx (7 changes)
- users/UserDetail.tsx (2 changes)
- users/InviteUserModal.tsx (1 change)
- comments/CommentInbox.tsx (11 changes)
- comments/CommentDetail.tsx (1 change)

* refactor(admin): use direction-aware classes in plugins & marketplace

Replace non-direction-aware Tailwind classes with direction-aware equivalents
in plugin and marketplace components to support RTL layouts:

- left-* → start-* (positioning)
- right-* → end-* (positioning)
- pl-* → ps-* (padding inline start)

Files updated:
- MarketplaceBrowse.tsx (2 changes)
- ThemeMarketplaceBrowse.tsx (2 changes)
- MarketplacePluginDetail.tsx (4 changes)
- ThemeMarketplaceDetail.tsx (4 changes)
- SandboxedPluginPage.tsx (1 change)

* refactor(admin): use direction-aware classes (phases 6-8)

Complete refactoring to direction-aware Tailwind classes across remaining
admin components to support RTL layouts.

Replaced non-direction-aware classes with direction-aware equivalents:
- text-left/right → text-start/end
- pl-*/pr-* → ps-*/pe-*
- ml-*/mr-* → ms-*/me-*
- left-*/right-* → start-*/end-*
- border-l/r → border-s/e
- border-l-*/r-* → border-s-*/e-*

Phase 6 - Schema & Content Types (3 files):
- ContentTypeList.tsx
- ContentTypeEditor.tsx
- FieldEditor.tsx

Phase 7 - Taxonomy, Menus, Sections & Widgets (8 files):
- TaxonomyManager.tsx
- TaxonomySidebar.tsx
- MenuEditor.tsx
- MenuList.tsx
- Sections.tsx
- SectionPickerModal.tsx
- Widgets.tsx
- Redirects.tsx

Phase 8 - Import, Core UI & Miscellaneous (8 files):
- WordPressImport.tsx
- Header.tsx
- Sidebar.tsx
- RevisionHistory.tsx
- SeoImageField.tsx
- WelcomeModal.tsx
- settings/AllowedDomainsSettings.tsx
- routes/bylines.tsx

All 177 occurrences across 48 files refactored.

* refactor(admin): fix remaining icon spacing classes for RTL

Convert remaining ml-/mr- icon spacing classes to direction-aware ms-/me-:
- MarketplaceBrowse.tsx
- LoginPage.tsx
- ThemeMarketplaceDetail.tsx
- SetupWizard.tsx
- ThemeMarketplaceBrowse.tsx
- CapabilityConsentDialog.tsx
- PluginManager.tsx
- users/RoleBadge.tsx
- MarketplacePluginDetail.tsx

These were missed in the initial batch refactor as they were primarily
used for icon spacing within buttons and labels.

* build: upgrade Kumo to v1.18.0 via catalog

Update @cloudflare/kumo from 1.16.0/1.10.0 to 1.18.0 across admin
and blocks packages using pnpm catalog for consistent versioning.

Changes:
- Add @cloudflare/kumo: ^1.18.0 to pnpm-workspace catalog
- Update packages/admin to use catalog: (from ^1.16.0)
- Update packages/blocks to use catalog: (from ^1.10.0)

This update brings the latest Kumo UI components and potential
RTL improvements. Custom CSS overrides in admin/src/styles.css
remain in place for Kumo Sidebar RTL support.

* fix(admin): override Kumo GroupLabel text alignment for RTL

Add className="[&>span]:text-start" to all KumoSidebar.GroupLabel
components to override Kumo library's hard-coded text-left class.

Kumo v1.18.0 still uses non-RTL-aware classes in GroupLabel. This
scoped override using Tailwind's selector syntax ensures group labels
align correctly in both LTR and RTL layouts.

Affected labels: Content, Manage, Admin, Plugins

* fix(admin): add RTL support to Header component

Add RTL-aware classes to Header component:
- Sidebar.Trigger: Add rtl:rotate-180 to flip hamburger icon in RTL
- ArrowSquareOut icon: Use me-1 (margin-inline-end) instead of mr-1
- Logout button: Use text-start instead of text-left for alignment

All changes use Tailwind v4's logical properties and rtl: variant
for automatic RTL layout support.

* chore: add changeset for RTL support in admin UI

* test(admin): update Widgets test selectors for RTL classes

Update test selectors from `button.text-left` to `button.text-start`
to match the RTL refactoring. The widget expand buttons now use
direction-aware classes.

* Revert "build: upgrade Kumo to v1.18.0 via catalog"

This reverts commit 12eb2b982d15d513f67aef43ce43bb23a4f731d7.

* refactor(admin): remove unsupported animation classes

* feat(admin): replace hand-rolled dialog with @cloudflare/kumo primitive

* fix(tests): update button locator class for older revision in revisions spec

* test(admin): align UserDetail tests with Kumo Dialog primitive

Adapts tests for the Dialog.Portal rendering model (renders outside
screen.container) and the data-base-ui-inert overlay that blocks
Playwright pointer events.

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-15 07:14:37 +01:00
mohamedmostafa58 64f90d1957 fix: repair invite flow — URL prefix + registration UI (fixes #67) (#542)
* fix: repair invite flow — URL prefix + registration UI + fix failing test

Fixes the two compounding bugs in the user invite system (Issue #67):

- Bug 1: Invite URL drops /_emdash/ prefix. Fixed by pointing to admin UI page.
- Bug 2: No UI to complete registration. Added InviteAcceptPage with passkey registration.
- Bug 3 (from PR #81 review): Fixed failing test assertion to match new URL path.

Based on PR #81 by @SrikanthAlva with the requested test fix applied.

Closes #67

* fix: address review comments — remove unused code, use ULID, useSearch, fix duplicate button, simplify email store

* fix(auth): remove signup from public admin routes

Making /_emdash/admin/signup a public server-side route allowed
unauthenticated direct access. The signup page is only intended
to be reached via client-side navigation from the login page.

Made-with: Cursor

---------

Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-04-14 14:27:58 +01:00
Deepak Chauhan 10ebfe19b8 Fix autosave form reset bug (#302)
* Fix autosave form reset bug

Autosave was invalidating the query cache after completion, which
triggered a refetch of content data from the server. This refetch
updated the item prop, causing a useEffect in ContentEditor to
reset form state to the server values, overwriting unsaved user
changes.

The fix removes the queryClient.invalidateQueries() call from the
autosaveMutation onSuccess handler. The local form state remains
the source of truth during editing, and manual saves still properly
invalidate and refresh the cache.

Fixes #295

* Fix repeated autosave after successful autosave

* Stabilize autosave status assertions

* Patch autosave cache after autosave

* style: format

* Add autosave regression coverage

* Fix autosave CI follow-ups

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
2026-04-13 20:28:09 +01:00
Matt Kane e2f96aa74b fix(admin): use loadMessages in useLocale hook (#504)
* fix(admin): use loadMessages in useLocale hook

The client-side locale switcher had the same dynamic import issue —
raw `import(./${code}/messages.mjs)` fails when resolved through
Vite aliases. Reuse the glob-based loadMessages from index.ts.

* fix(admin): replace locale toggle buttons with kumo Select

Toggle buttons don't scale beyond a few locales. Use the kumo Select
dropdown instead. Add e2e tests for the language switcher.

* chore: add changeset
2026-04-12 22:17:38 +00:00
CacheMeOwside c70f66f7da fix(admin): detect redirect loops (#351)
* add selection rule for readable text selection contrast

* fix: detect and prevent redirect loops

* add changeset file

* fix typecheck error

* clean-up redundant e2e tests

---------

Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-04-12 16:28:00 +01:00
Cesar S Falcão 476cb3a585 fix(middleware): allow public access to search API at middleware layer (#424)
PR #107 removed the handler-level `requirePerm()` check from the search
endpoints, but the auth middleware still returned 401 before the handlers
ran because `/_emdash/api/search` was not in `PUBLIC_API_EXACT`. The
handler-level changes therefore never executed for anonymous callers,
and the shipped `LiveSearch` component (which fetches without credentials)
silently showed "No results found" on every query.

This change adds `/_emdash/api/search` to `PUBLIC_API_EXACT` so the
middleware lets anonymous GET requests reach the handler. The query
layer already hardcodes `status='published'`, so anonymous callers
still only see published content. Admin endpoints (`/enable`, `/rebuild`,
`/stats`, and `/suggest`) remain authenticated because they are not in
the set.

The existing E2E test `"search endpoint requires authentication"` asserted
the buggy behavior and is replaced with two new tests: one verifying
public access to `/_emdash/api/search`, and one verifying that `/stats`
and `/enable` remain gated.

Closes #104. Follows up on #107.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-04-11 13:19:10 +00:00
Pouf5 5eeab91882 i18n: Fix new content always being created in English (#377)
* Fix content always created in English

* formatting

* Remove i18n from simple config

* Improve router test to catch edge cases

* Fix E2E test failing due to an outdated regex

* Fix await-thenable lint error in router tests

Locators returned by screen.getByRole() are not Thenables — awaiting
them directly triggers the await-thenable lint rule. Remove the
unnecessary await when assigning the locator to a variable; the locator
is still usable for expect.element() and .element() calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Matt Kane <mkane@cloudflare.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 12:25:21 +01:00
seslly d2114523a5 fix: passkeys behind TLS reverse proxy (#225)
* fix: passkeys behind TLS reverse proxy

Add passkeyPublicOrigin and wire it through passkey routes so origin/rpId match
the browser when dev runs behind nginx. Expose dev-only /_emdash/api/dev/passkey-url,
add admin messaging for insecure WebAuthn contexts, nginx repro under demos/simple,
and direct kysely dependency for the simple demo Node adapter bundle.

Made-with: Cursor

* docs: add passkeyPublicOrigin to configuration reference

Adds the new passkeyPublicOrigin option and reverse proxy guidance
to the public-facing configuration docs as requested in PR review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update tests and more docs

* fix: add missing refresh-server-pat fixture and restore docs heading

---------

Co-authored-by: Joseph Eftekhari <jdeftekhari@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 07:41:07 +01:00