Add on-page editor for the go-graph-cms

Injectable React editor that is side-loaded onto the statically rendered
page and edits through the CMS HTTP API (POST /api/edit).

- Shadow-root UI over server HTML; patches morphed in, undo/redo from
  server inverses (reuses @k6n/cms-editor EditClient/morph/markdown editor)
- Sign-in via go-auth device grant as the CMS client (aud matches), token
  refresh + 401 retry; live preview swapped in on load and on tab focus
- Schema-driven property form (arrays, maps, variants, $ref, validation)
- Every string property editable where it is rendered (value-bound inline
  fields and attribute chips), markdown WYSIWYG for marked fields
- Block palette + drag & drop with ghost drop targets, insertion slots,
  sort snapping between siblings (Alt to nest)
- x-style annotations: on-page padding/margin/gap handles, Style panel,
  wrap-in-layout; x-graph label/group for titles and grouping
- Dev workflow: Vite proxy to the live site (token injected server-side)
  or an offline in-memory mock CMS; jsdom test suite

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
matsandClaude Sonnet 5.5 committed 2026-10-31 22:34:48 +01:00
commit 1af9ec3172
67 files changed
+12322

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
import type { IncomingHttpHeaders, IncomingMessage } from 'node:http'
/** Paths Vite itself serves (HMR client, source modules, deps). */
export const isViteOwned = (url: string) =>
/^\/(@|src\/|node_modules\/|__|mock\/|test\/)/.test(url)
/** A navigation to a page we should fetch + inject, as opposed to an asset/API call. */
export function isPageRequest(req: IncomingMessage): boolean {
const url = req.url ?? '/'
if (req.method !== 'GET' || isViteOwned(url)) return false
if (/^\/(api|editor|assets|js|css|fonts|static)(\/|$)/.test(url)) return false
if (/\.[a-z0-9]{2,5}($|\?)/i.test(url.split('?')[0])) return false // has a file extension
return String(req.headers.accept ?? '').includes('text/html')
}
export interface Auth {
token?: string
apiKey?: string
cookie?: string
}
const hasPreviewCookie = (cookie?: string) => /(?:^|;\s*)cms_preview=1/.test(cookie ?? '')
/** Headers for an upstream request: browser cookies + configured credentials + preview flag. */
export function upstreamHeaders(
incoming: IncomingHttpHeaders,
auth: Auth,
opts: { identity?: boolean } = {},
): Record<string, string> {
const h: Record<string, string> = {}
if (incoming.accept) h.accept = String(incoming.accept)
if (incoming['accept-language']) h['accept-language'] = String(incoming['accept-language'])
if (opts.identity) h['accept-encoding'] = 'identity'
const cookie = [incoming.cookie, auth.cookie].filter(Boolean).join('; ')
if (cookie) h.cookie = cookie
// The edge routes on the cookie OR this header; send both so a proxy hop can't lose it.
if (hasPreviewCookie(cookie)) h['x-cms-preview'] = '1'
if (auth.token) h.authorization = `Bearer ${auth.token}`
else if (incoming.authorization) h.authorization = String(incoming.authorization)
if (auth.apiKey) h['x-api-key'] = auth.apiKey
return h
}
/** Put the dev entry in front of </body> (or at the end). */
export function injectDevEntry(html: string): string {
const tag = '<script type="module" src="/src/dev.ts"></script>'
return /<\/body>/i.test(html) ? html.replace(/<\/body>/i, `${tag}</body>`) : html + tag
}