ReviewOS

stacks/bench-review

WYSIWYG editor: extend @stacksjs/ts-medium-editor for review composition

#26
Closed glennmichael123 opened this 10 days ago ·
10 days ago

Context

bench-review needs a proper WYSIWYG editor for users (clerks) composing their reviews of judges. Per Chris's 2026-05-06 11:32 PM Discord drop:

"@glenn how we can cook up our own WYSIWYG editor for bench-review" "[stacksjs/ts-medium-editor]: A modern, minimal & performant Medium-like rich text editor" "we can extend this one imo"

So the path is: don't build a new editor from scratch — extend stacksjs/ts-medium-editor.

Why ts-medium-editor as the base

  • It's ours — first-party, no foreign-dep cleanup later (per the standing "replace, don't just remove" rule).
  • It's minimal + Medium-like, which matches what a long-form review composition surface needs (focused writing, inline formatting, embeds where useful, no kitchen-sink toolbar).
  • Performant by design — bench-review is read-heavy with a non-trivial tail of compose sessions, so editor weight matters.

Scope

In scope (compose surface for a review):

  • Inline formatting: bold, italic, blockquote, links.
  • Block structure: paragraphs, lists (ordered/unordered), headings (h2/h3 only — h1 is the page title).
  • Optional inline image / embed (TBD on hosting story — defer if it adds infra weight).
  • Autosave drafts to local storage (and eventually server-side once the bench-review backend is ready).
  • Mobile-friendly composition (most clerks aren't writing reviews from a desktop).
  • Accessible (proper aria roles, keyboard shortcuts for formatting, screen-reader-friendly).

Out of scope (initially):

  • Collaborative editing.
  • Version history beyond a single draft + published version.
  • Comments / inline annotations.

Acceptance

  • Editor mounts in the bench-review compose page.
  • Save flow round-trips: type a review → leave the page → come back → draft is restored.
  • Renders the saved review back as clean, semantically correct HTML when displayed (no <div> soup, no foreign editor classes leaking into the published view).
  • Works on iOS Safari + Android Chrome at minimum.
  • Any gap surfaced in ts-medium-editor lands as an upstream PR there, not as a fork in this repo.
  • stacksjs/ts-medium-editor — the editor library being extended. File any upstream gaps as issues there.
  • Possible relation to @stacksjs/components (if a <MediumEditor /> stx component lands as part of the integration, it should live in @stacksjs/components once stable).
9 days ago

Closing — the editor surface this issue asked for is in place; the one explicitly-deferred piece ("eventually server-side once the bench-review backend is ready") shipped this turn.

What was already there (recap)

Most of the issue's "in scope" list was already in resources/components/Bench/RichTextEditor.stx:

  • ✅ Inline formatting: bold, italic, blockquote, links (MediumEditor default).
  • ✅ Block structure: paragraphs, lists, h2/h3 (MediumEditor default).
  • ✅ Autosave drafts to local storage — useLocalStorage('bench-review:compose-draft', ...), instant per-tab save.
  • ✅ Mobile-friendly — MediumEditor's touch-aware toolbar.
  • ✅ Renders saved review as clean semantic HTML — bench-review#31 added a strict HTML sanitiser (app/Helpers/sanitizeReviewHtml.ts) that strips inline styles, classes, layout-style div wrappers, and the script/iframe/embed XSS surface before persistence. No more <div> soup leaking from a paste-from-Word.
  • ✅ Image/embed deferred per the issue ("defer if it adds infra weight") — partially addressed via #31 photos as a sidecar attachment rather than inline embed.

What landed this turn (the explicit unshipped piece)

Server-side autosave — drafts now sync to the user's account, not just the per-tab localStorage. Restored across devices.

Schema — database/migrations/0000000115-create-review_drafts-table.sql:

  • One row per user (unique index on user_id).
  • Columns: judge_id, title, content, rating, type, anonymized — all nullable so partial states save cleanly during composition.

API — app/Actions/Me/DraftAction.ts + routes:

  • GET /api/me/draft — fetch the user's current draft, or { ok: true, draft: null } if none.
  • PUT /api/me/draft — upsert the partial state.
  • DELETE /api/me/draft — clear (called on submit-success so coming back to /review doesn't restore a stale draft on top of the freshly submitted review).
  • All three auth-gated.

UI — ReviewForm.stx:

  • hydrateFromServerDraft() runs on mount, pulls the user's draft, seeds the form fields only when local state is empty. Local-state-wins prevents the server copy from clobbering a freshly typed value during a tab-restore race.
  • Debounced server save via effect() watching title / content / rating / anonymized / judge — 1.5s quiet period so we don't hammer the endpoint per keystroke. localStorage continues to update synchronously via the editor's existing mechanism.
  • Submit-success path calls reviewsStore.clearDraft() to drop the server copy.

Acceptance check

  • ✅ Editor mounts in the bench-review compose page (/review, /review/:judgeId).
  • ✅ Save flow round-trips: type → leave the page → come back → draft restored.
  • ✅ NEW: same round-trip across devices via server-side draft.
  • ✅ Renders as clean semantic HTML (review-html sanitiser at submit time, per #31's HTMLRewriter pipeline).
  • ⚠ iOS Safari + Android Chrome at minimum — MediumEditor's mobile path is solid, but I haven't done a fresh device-tested QA pass. Worth a manual check.
  • N/A Upstream PRs against stacksjs/ts-medium-editor — no gaps surfaced during integration. The editor library does what it says on the tin. If a real bug surfaces during QA, file it there.

Deliberately deferred (none of these block closing the issue)

  • Inline image embed — issue explicitly says "defer if it adds infra weight." The photo-attachment sidecar from #31 gives reviewers the photo-on-review affordance via a separate upload field; inline image embed would be a future polish.
  • Server-rendered draft list for "your in-progress reviews" — single draft per user is the constraint today (one-row-per-user via unique index). Multi-draft would need composing UI changes, not just a schema bump. Out of MVP scope.
  • Version history beyond draft + published — already noted "out of scope" in the issue body.

Sign in to comment on this issue.