Wiki reader and editor parity contract #52

Closed
opened 2026-07-28 08:10:37 +00:00 by archvillainette · 1 comment
Owner

Part of #47

Question

What must hold true in both the wiki reader and the wiki editor, so a frame change cannot break one by fixing the other?

This is not hypothetical. A previous pass justified text in the reader and broke the editor. The wiki editor is a TipTap surface in sow-nodebb-plugin-wiki (tiptap/, wiki-compose.tpl); the reader is wiki-page.tpl styled by the 1700-line scss/westgate/_wiki-prose.scss in this theme.

Resolve:

  • Which properties must be identical in both, because a difference is visible as content jumping when you hit edit: content column width, line length, font stack, size, line height, paragraph spacing, list indentation, heading scale.
  • Which properties are legitimately allowed to differ, and why: editor chrome, gutters, toolbars, selection and caret styling, placeholder text.
  • Where the shared styling should physically live so parity is structural rather than remembered. Today the reader styles sit in the theme and the editor sits in the plugin, which is exactly why they drift.
  • Which properties are known editor hazards — text justification is one confirmed case; identify the rest before they cost another pass.
  • How parity is verified: the concrete check a session runs before claiming a wiki frame change works, on both surfaces, in the live dev stack.

Output is a short contract that later wiki tickets are held to.

Part of #47 ## Question What must hold true in both the wiki reader and the wiki editor, so a frame change cannot break one by fixing the other? This is not hypothetical. A previous pass justified text in the reader and broke the editor. The wiki editor is a TipTap surface in `sow-nodebb-plugin-wiki` (`tiptap/`, `wiki-compose.tpl`); the reader is `wiki-page.tpl` styled by the 1700-line `scss/westgate/_wiki-prose.scss` in this theme. Resolve: - Which properties must be identical in both, because a difference is visible as content jumping when you hit edit: content column width, line length, font stack, size, line height, paragraph spacing, list indentation, heading scale. - Which properties are legitimately allowed to differ, and why: editor chrome, gutters, toolbars, selection and caret styling, placeholder text. - Where the shared styling should physically live so parity is structural rather than remembered. Today the reader styles sit in the theme and the editor sits in the plugin, which is exactly why they drift. - Which properties are known editor hazards — text justification is one confirmed case; identify the rest before they cost another pass. - How parity is verified: the concrete check a session runs before claiming a wiki frame change works, on both surfaces, in the live dev stack. Output is a short contract that later wiki tickets are held to.
archvillainette added the wayfinder:grilling label 2026-07-28 08:10:37 +00:00
archvillainette added a new dependency 2026-07-28 08:10:55 +00:00
archvillainette self-assigned this 2026-07-31 14:11:01 +00:00
Author
Owner

Answer: parity is a DOM contract, not a rule anyone has to remember

Ownership first, because it decides everything else.

The wiki plugin owns its own chrome. Its endgame is publishable open source, not locked to our infra: it must look good standing alone on Harmony defaults, with no theme tweaks. Everything it renders is then customizable from outside with plain CSS, the way Harmony itself is. Our theme is just one consumer of that surface.

That rules out enforcing parity by forbidding the theme to write CSS — the customization we want and the prohibition cannot both hold. So parity comes from structure instead:

The plugin guarantees that reader and editor expose the same prose DOM: same root hook, same prose container hook, same element names inside. Editor chrome lives under separate, editor-only hooks. Any prose rule written against the shared hooks therefore hits both surfaces or neither — a reader-only fix becomes unwriteable, not merely discouraged.

That is the whole contract. The rest is what it implies.

Shared hooks (prose — a rule here MUST hit both)

Already true today, and now load-bearing rather than incidental:

  • Root: .westgate-wiki on both. Reader templates/wiki-page.tpl:1, compose templates/wiki-compose.tpl:1 (class="westgate-wiki westgate-wiki-compose" — the compose root carries both).
  • Prose container: .wiki-article-prose on both. Reader wiki-page.tpl:63, editable wiki-compose.tpl:47.
  • Same stylesheet on both surfaces: public/wiki-article-body.css, served as westgate-wiki/compose/article-body.css (reader wiki-page.tpl:268, compose wiki-compose.tpl:87, mapped in lib/core/compose-assets.js).

Must be identical in both, because a difference shows as content jumping when you hit edit: content column width and reading measure, font stack, font size, line height, paragraph and heading spacing, list indentation, heading scale, link colour, blockquote/code/table/callout/infobox rendering, text alignment and hyphenation.

Editor-only hooks (chrome — differing here is correct)

.westgate-wiki-compose and .wiki-editor__*, plus .ProseMirror-* state classes. Legitimately different: toolbar, source panel, gutters, sticky offsets, caret and selection styling, placeholder text, node-selected outlines, resize handles, slash-command menu.

The boundary test: if turning the property off would change how the finished page reads, it is prose and belongs on the shared hooks. If it only exists while editing, it is chrome.

Where the styling physically lives

  • Plugin owns every prose selector and every structural decision — which element gets the measure, what opts out of it, how a callout is built. Defaults must be Harmony-compatible and good with no theme present.
  • Theme sets --wiki-* token values only. It does not restate plugin structure.
  • Escape hatch, named: page-frame furniture around the prose (card chrome, ToC drawer layout, fab positioning, the surrounding grid) stays a theme concern under theme selectors. That is the frame, not the prose.

Prose alignment

Justified reading prose is plugin-owned behaviour, not a theme selector. The plugin owns the implementation and the full opt-out set (table cells, blockquote, pre, namespace/section listings). Site preference is expressed through an enumerated token the plugin honours — --wiki-prose-text-align: start | justify, default start — so a consumer flips one value and gets justification applied identically to both surfaces, opt-outs travelling with it. What a consumer cannot do is re-scope what gets justified. Westgate sets justify; it is our taste, not the plugin default.

Known editor hazards

  1. text-align: justify — confirmed, cost a pass. Now plugin-owned per above.
  2. hyphens: auto — travels with justification, same fix.
  3. Reading measure written as a raw value in the theme. scss/westgate/_wiki-prose.scss:228 caps .wiki-content-layout at 58rem for the reader; :236 repeats 58rem for .westgate-wiki-compose .wiki-compose-editor.wiki-article-prose. One meaning, two constants, two selectors — drifts the moment one is edited. Collapses to --wiki-prose-measure (plugin default 70ch, wiki-article-body.css:79).
  4. Dead editor-only prose CSS. ~50 lines in _wiki-prose.scss (roughly :341:540) target .ck.ck-editor__editable_inline.ck-content — CKEditor classes the TipTap editor never emits. They are editor-only copies of heading rules: the exact shape that produced hazard 1, still sitting in the file, now also dead. Delete.
  5. Reader-only ancestors used to reach prose. .wiki-page-content, .wiki-with-fab, .wiki-content-layout, .wiki-namespace-index exist only on the reader. Any prose property reached through them is by construction missing in the editor.
  6. Bootstrap/Harmony leakage through .card. The reader wraps prose in a card, the editor does not; card padding and background rules therefore land on one surface only.

How parity is verified

Cheap, mechanical, runs before any wiki frame change is called done:

  1. Static (no forum): a contract test in this repo greps the compiled wiki SCSS and fails on any prose-affecting property reached through a reader-only or editor-only ancestor. Whitelist is the chrome hook list above. This catches every hazard-5 regression without booting anything.
  2. Live (required, never skipped): in ../sow-nodebb (nix develop --command make dev), open the same wiki page as reader and as editor, and compare computed styles on the .wiki-article-prose element and a paragraph inside it: font-family, font-size, line-height, max-width, text-align, hyphens, margin-*. Any difference is a bug in the change, not an editor quirk. Then read a paragraph on both, side by side — a computed-style match still misses a broken caret or a shifted first line.

Reader-only verification does not count as verification. That law stands.

Follow-up

Cleanup implied by this contract (delete the dead CKEditor block, collapse the duplicated 58rem into --wiki-prose-measure, add the static parity test) is build work, not map work. Filed separately; this map only holds later wiki tickets to the contract above.

## Answer: parity is a DOM contract, not a rule anyone has to remember Ownership first, because it decides everything else. **The wiki plugin owns its own chrome.** Its endgame is publishable open source, not locked to our infra: it must look good standing alone on Harmony defaults, with no theme tweaks. Everything it renders is then customizable from outside with plain CSS, the way Harmony itself is. Our theme is just one consumer of that surface. That rules out enforcing parity by forbidding the theme to write CSS — the customization we want and the prohibition cannot both hold. So parity comes from structure instead: > **The plugin guarantees that reader and editor expose the same prose DOM: same root hook, same prose container hook, same element names inside. Editor chrome lives under separate, editor-only hooks. Any prose rule written against the shared hooks therefore hits both surfaces or neither — a reader-only fix becomes unwriteable, not merely discouraged.** That is the whole contract. The rest is what it implies. ### Shared hooks (prose — a rule here MUST hit both) Already true today, and now load-bearing rather than incidental: - Root: `.westgate-wiki` on both. Reader `templates/wiki-page.tpl:1`, compose `templates/wiki-compose.tpl:1` (`class="westgate-wiki westgate-wiki-compose"` — the compose root carries *both*). - Prose container: `.wiki-article-prose` on both. Reader `wiki-page.tpl:63`, editable `wiki-compose.tpl:47`. - Same stylesheet on both surfaces: `public/wiki-article-body.css`, served as `westgate-wiki/compose/article-body.css` (reader `wiki-page.tpl:268`, compose `wiki-compose.tpl:87`, mapped in `lib/core/compose-assets.js`). **Must be identical in both**, because a difference shows as content jumping when you hit edit: content column width and reading measure, font stack, font size, line height, paragraph and heading spacing, list indentation, heading scale, link colour, blockquote/code/table/callout/infobox rendering, text alignment and hyphenation. ### Editor-only hooks (chrome — differing here is correct) `.westgate-wiki-compose` and `.wiki-editor__*`, plus `.ProseMirror-*` state classes. Legitimately different: toolbar, source panel, gutters, sticky offsets, caret and selection styling, placeholder text, node-selected outlines, resize handles, slash-command menu. The boundary test: **if turning the property off would change how the finished page reads, it is prose and belongs on the shared hooks. If it only exists while editing, it is chrome.** ### Where the styling physically lives - **Plugin** owns every prose *selector* and every structural decision — which element gets the measure, what opts out of it, how a callout is built. Defaults must be Harmony-compatible and good with no theme present. - **Theme** sets `--wiki-*` token *values* only. It does not restate plugin structure. - **Escape hatch, named:** page-frame furniture *around* the prose (card chrome, ToC drawer layout, fab positioning, the surrounding grid) stays a theme concern under theme selectors. That is the frame, not the prose. ### Prose alignment Justified reading prose is **plugin-owned behaviour**, not a theme selector. The plugin owns the implementation *and* the full opt-out set (table cells, blockquote, `pre`, namespace/section listings). Site preference is expressed through an enumerated token the plugin honours — `--wiki-prose-text-align: start | justify`, default `start` — so a consumer flips one value and gets justification applied identically to both surfaces, opt-outs travelling with it. What a consumer cannot do is re-scope *what* gets justified. Westgate sets `justify`; it is our taste, not the plugin default. ### Known editor hazards 1. **`text-align: justify`** — confirmed, cost a pass. Now plugin-owned per above. 2. **`hyphens: auto`** — travels with justification, same fix. 3. **Reading measure written as a raw value in the theme.** `scss/westgate/_wiki-prose.scss:228` caps `.wiki-content-layout` at `58rem` for the reader; `:236` repeats `58rem` for `.westgate-wiki-compose .wiki-compose-editor.wiki-article-prose`. One meaning, two constants, two selectors — drifts the moment one is edited. Collapses to `--wiki-prose-measure` (plugin default `70ch`, `wiki-article-body.css:79`). 4. **Dead editor-only prose CSS.** ~50 lines in `_wiki-prose.scss` (roughly `:341`–`:540`) target `.ck.ck-editor__editable_inline.ck-content` — CKEditor classes the TipTap editor never emits. They are editor-only *copies* of heading rules: the exact shape that produced hazard 1, still sitting in the file, now also dead. Delete. 5. **Reader-only ancestors used to reach prose.** `.wiki-page-content`, `.wiki-with-fab`, `.wiki-content-layout`, `.wiki-namespace-index` exist only on the reader. Any prose property reached through them is by construction missing in the editor. 6. **Bootstrap/Harmony leakage through `.card`.** The reader wraps prose in a `card`, the editor does not; card padding and background rules therefore land on one surface only. ### How parity is verified Cheap, mechanical, runs before any wiki frame change is called done: 1. **Static (no forum):** a contract test in this repo greps the compiled wiki SCSS and fails on any prose-affecting property reached through a reader-only or editor-only ancestor. Whitelist is the chrome hook list above. This catches every hazard-5 regression without booting anything. 2. **Live (required, never skipped):** in `../sow-nodebb` (`nix develop --command make dev`), open the *same* wiki page as reader and as editor, and compare computed styles on the `.wiki-article-prose` element and a paragraph inside it: `font-family`, `font-size`, `line-height`, `max-width`, `text-align`, `hyphens`, `margin-*`. Any difference is a bug in the change, not an editor quirk. Then read a paragraph on both, side by side — a computed-style match still misses a broken caret or a shifted first line. Reader-only verification does not count as verification. That law stands. ### Follow-up Cleanup implied by this contract (delete the dead CKEditor block, collapse the duplicated `58rem` into `--wiki-prose-measure`, add the static parity test) is build work, not map work. Filed separately; this map only holds later wiki tickets to the contract above.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Blocks
#58 Wiki page frame, reader and editor
ShadowsOverWestgate/sow-nodebb-theme
Reference: ShadowsOverWestgate/sow-nodebb-theme#52