CSS-only fix restoring horizontal scrolling for readable/wide tables in the WebKit renderer (regression from the T-1542 cutover), plus a stylesheet guard test and a Makefile -destination-timeout so test/build targets don't hang on a wireless device.
table { max-width: 100% } was never overridden per display mode, and an auto-width table shrinks columns to fit rather than overflowing — so readable/scroll tables squashed.width: max-content + max-width: none on the inner <table> for the scroll and readable modes; fitted is untouched.max-width: none) still squashed on-device; caught by manual verification.-destination-timeout 20 so make test/build stop hanging while CoreDevice probes an unreachable wireless iPhone (install target untouched).Ready to push
The behaviour change is CSS-only, verified scrolling on an iOS device, with targeted macOS tests (DocumentCSSTableRulesTests, BlockHTMLEmitterTests, WideTableTests) passing ** TEST SUCCEEDED ** and SwiftLint clean across 470 files. One review finding (a Makefile loop indentation slip) was fixed and amended. No emitter/model/bridge code changed.
b61284a T-1559: Add readable-tables-restoration smolspec 653116d T-1559: Restore horizontal scrolling for readable/wide tables (WebKit renderer) 041f224 T-1559: Cap xcodebuild destination resolution so tests do not hang on a wireless device working-tree Review fix amended into 041f224: test-locales loop indentation Wide tables were squashed to fit the screen instead of scrolling sideways. Now a table wider than the page scrolls horizontally within itself, and the page stays put.
Cramming many columns into the available width made tables hard to read. They now keep their natural width and you scroll the table to see the rest.
A table left at width: auto shrinks its columns to fit and never overflows. Telling it width: max-content makes it take its real content width, so it spills past the container and the container's existing scrollbar appears.
The renderer already wrapped each table in .prism-table-wrap (overflow-x: auto) tagged with data-prism-table-mode. The squash was a pure CSS bug: table { width: auto; max-width: 100% } was never overridden per mode, so scroll mode's width: max-content was clamped by max-width: 100% and readable mode's 300px cell caps couldn't make a width: auto table overflow.
Both scrolling modes set width: max-content; max-width: none on the inner table. readable keeps per-cell max-width: 300px + word-break (content width = sum of capped columns). fitted keeps max-width: 100% (shares width, no scroll). html/body keep overflow-x: hidden so only the table scrolls.
CSS layout isn't unit-testable here, so verification = a stylesheet rule-pin test + required on-device check (decision_log Decision 2).
width: max-content is the fix; max-width: none is necessary but not sufficient. An auto-layout table with width: auto settles at the available (wrapper) width when content prefers more (≈clamp(MIN, available, MAX)), distributing/shrinking columns; releasing the max-width ceiling alone doesn't change that. Only pinning width: max-content makes the used width exceed the wrapper and trigger overflow-x: auto. The first attempt set only max-width: none and squashed on-device — caught by the manual-verification task.
Both use width: max-content; they differ only in the per-cell cap. max-width on table cells is 'undefined' in CSS 2.1 but honored by modern WebKit with word-break — confirmed on device.
Emitter, data-prism-table-mode tagging, TableDisplayMode.initialMode (auto-selects fitted/readable, never scroll), and the setTableModes bridge were already correct and tested. No Swift change needed.
document.css
Why it matters. User-visible behaviour: restores horizontal scrolling for wide/readable tables; without width:max-content the table never overflows its wrapper.
What to look at. document.css scroll-mode and readable-mode table rules
DocumentCSSTableRulesTests.swift
Why it matters. The regression was a silent CSS edit; this test fails if either scrolling-mode rule loses width:max-content or max-width:none.
What to look at. DocumentCSSTableRulesTests.swift:95-145 (scrollModeScrolls / readableModeScrolls)
Makefile
Why it matters. Test/build targets hung indefinitely while CoreDevice probed an unreachable wirelessly-paired iPhone — even for the macOS destination.
What to look at. Makefile DEST_TIMEOUT applied to test-quick/test/test-ui/test-locales/build-ios/build-macos
The emitter wrapper, mode tagging, and bridge were already correct and tested; the defect was one un-overridden CSS property. Smallest, lowest-risk change.
An auto-width table shrinks to fit and never overflows; releasing the max-width cap is insufficient. On-device testing of the max-width:none-only attempt confirmed the squash persisted.
The live WebPage harness never applies the emitted stylesheet and WebPage has no sized viewport, so scrollWidth/clientWidth geometry is unreliable. See decision_log Decision 2.
Bundled into the branch at the author's request after the wireless-device hang blocked local test runs; install target left able to reach the device.
| Severity | Area | Finding | Resolution |
|---|---|---|---|
| minor | Makefile test-locales loop | The replace_all that inserted $(DEST_TIMEOUT) left the loop's -destination line at 2 tabs while its siblings use 3 (functionally harmless — Make only needs a leading tab — but inconsistent). | Re-indented the line to 3 tabs; verified make -n parses all targets; amended into the Makefile commit. |
| nit | DocumentCSSTableRulesTests helpers | releasesMaxWidth/takesContentWidth re-run normalized() on an already-normalised body (double normalisation). | Left as-is — harmless and keeps the helpers robust to un-normalised input; not worth a test-file change. |
Click to expand.
diff --git a/prism/Resources/WebRenderer/document.css b/prism/Resources/WebRenderer/document.cssindex d06aa9e..8171967 100644--- a/prism/Resources/WebRenderer/document.css+++ b/prism/Resources/WebRenderer/document.css@@ -478,12 +478,24 @@ th { } /* Horizontal-scroll mode: the table keeps its natural content width and scrolls- * sideways within the wrapper. */-.prism-table-wrap[data-prism-table-mode="scroll"] table { width: max-content; }+ * sideways within the wrapper. max-width: none releases the base 100% cap so the table+ * can exceed the wrapper and the wrapper's overflow-x: auto engages. */+.prism-table-wrap[data-prism-table-mode="scroll"] table {+ width: max-content;+ max-width: none;+} /* Readable mode: columns cap at a max width so the table fits the column (300pt rule); * a capped table still wider than the viewport scrolls within the wrapper (inherited- * overflow-x: auto) rather than overflowing the page. */+ * overflow-x: auto) rather than overflowing the page. width: max-content makes the table+ * take its content width (the sum of the 300px-capped columns) instead of shrinking to+ * fit the wrapper — an auto-width table shrinks columns to fit and never overflows, so+ * max-width: none alone does not scroll. max-width: none releases the base 100% cap so the+ * content-width table can exceed the wrapper and the wrapper's overflow-x: auto engages. */+.prism-table-wrap[data-prism-table-mode="readable"] table {+ width: max-content;+ max-width: none;+} .prism-table-wrap[data-prism-table-mode="readable"] th, .prism-table-wrap[data-prism-table-mode="readable"] td { max-width: 300px;
diff --git a/prismTests/WebRendering/DocumentCSSTableRulesTests.swift b/prismTests/WebRendering/DocumentCSSTableRulesTests.swiftnew file mode 100644index 0000000..cdbebc2--- /dev/null+++ b/prismTests/WebRendering/DocumentCSSTableRulesTests.swift@@ -0,0 +1,146 @@+//+// DocumentCSSTableRulesTests.swift+// prismTests+//+// Stylesheet guard for the readable-tables-restoration fix (T-1559).+//+// The T-1542 WebKit cutover left the base rule `table { max-width: 100% }`+// un-overridden per mode, so a `scroll`-mode table's `width: max-content` and a+// `readable`-mode table's 300px cell caps were both clamped to the+// `.prism-table-wrap` width — the table could never overflow the wrapper, so the+// wrapper's `overflow-x: auto` never engaged and the table squashed. The fix+// releases the cap (`max-width: none`) on the inner `<table>` in BOTH scrolling+// modes; `fitted` keeps the base cap.+//+// Both scrolling modes need TWO properties on the inner `<table>`: `width: max-content`+// (so the table takes its content width instead of shrinking columns to fit the wrapper —+// an auto-width table never overflows) and `max-width: none` (so the base 100% cap does+// not clamp it back to the wrapper). `width: max-content` is the load-bearing one: device+// testing showed `max-width: none` alone leaves a `readable` table squashed (T-1559).+//+// This is a rule-pin, NOT a behavioural proof. CSS layout cannot be asserted by the+// test infrastructure (the live WebPage harness never applies the emitted stylesheet+// and WebPage has no sized viewport in tests — see specs/readable-tables-restoration/+// decision_log.md Decision 2). The guard loads the bundled `document.css` from the+// same `Bundle.main` path the scheme handler uses and asserts both properties are present,+// so a future edit that re-adds the cap or drops the content width fails here. Rendered+// behaviour is verified manually on macOS and iOS.+//++import Foundation+import Testing+@testable import prism++@Suite("document.css table-mode max-width override guard (T-1559)")+struct DocumentCSSTableRulesTests {++ /// Loads the bundled `document.css` — the only stylesheet served through+ /// `prism-doc://`, fetched here from the same `Bundle.main` location the scheme+ /// handler and other WebRendering tests use.+ private static func loadDocumentCSS() throws -> String {+ let url = try #require(+ Bundle.main.url(forResource: "document", withExtension: "css"),+ "bundled document.css must be present in the test host"+ )+ return try String(contentsOf: url, encoding: .utf8)+ }++ /// Collapses runs of whitespace to single spaces so matching tolerates+ /// reformatting (newlines, indentation) without depending on exact layout.+ private static func normalized(_ css: String) -> String {+ css.replacingOccurrences(+ of: "\\s+",+ with: " ",+ options: .regularExpression+ )+ }++ /// Extracts the body (`{ … }`) of the first rule whose selector text contains+ /// `selectorFragment`. Returns nil if no such rule exists. Matching is done on the+ /// whitespace-normalized stylesheet so the fragment can be written naturally.+ private static func ruleBody(+ containingSelector selectorFragment: String,+ in normalizedCSS: String+ ) -> String? {+ var searchStart = normalizedCSS.startIndex+ while let selectorRange = normalizedCSS.range(+ of: selectorFragment,+ range: searchStart..<normalizedCSS.endIndex+ ) {+ // A rule body opens at the first `{` after the selector and before the next+ // `}` — guard against matching a fragment that sits inside a prior body.+ guard let openBrace = normalizedCSS.range(+ of: "{",+ range: selectorRange.upperBound..<normalizedCSS.endIndex+ ) else {+ return nil+ }+ let priorClose = normalizedCSS.range(+ of: "}",+ range: selectorRange.upperBound..<openBrace.lowerBound+ )+ if priorClose == nil {+ guard let closeBrace = normalizedCSS.range(+ of: "}",+ range: openBrace.upperBound..<normalizedCSS.endIndex+ ) else {+ return nil+ }+ return String(normalizedCSS[openBrace.upperBound..<closeBrace.lowerBound])+ }+ searchStart = selectorRange.upperBound+ }+ return nil+ }++ /// Whether a rule body releases the max-width cap (`max-width: none`).+ private static func releasesMaxWidth(_ body: String) -> Bool {+ normalized(body).contains("max-width: none")+ }++ /// Whether a rule body sizes the table to its content (`width: max-content`) — the+ /// property that actually makes the table overflow the wrapper and scroll.+ private static func takesContentWidth(_ body: String) -> Bool {+ normalized(body).contains("width: max-content")+ }++ @Test("scroll-mode table rule scrolls: content width + released cap")+ func scrollModeScrolls() throws {+ let css = Self.normalized(try Self.loadDocumentCSS())+ let body = try #require(+ Self.ruleBody(+ containingSelector: #"[data-prism-table-mode="scroll"] table"#,+ in: css+ ),+ "expected a scroll-mode table rule in document.css"+ )+ #expect(+ Self.takesContentWidth(body),+ "scroll-mode table rule must contain `width: max-content` so the table overflows .prism-table-wrap and scrolls"+ )+ #expect(+ Self.releasesMaxWidth(body),+ "scroll-mode table rule must contain `max-width: none` so the base 100% cap does not clamp it"+ )+ }++ @Test("readable-mode table rule scrolls: content width + released cap")+ func readableModeScrolls() throws {+ let css = Self.normalized(try Self.loadDocumentCSS())+ let body = try #require(+ Self.ruleBody(+ containingSelector: #"[data-prism-table-mode="readable"] table"#,+ in: css+ ),+ "expected a readable-mode table rule in document.css"+ )+ #expect(+ Self.takesContentWidth(body),+ "readable-mode table rule needs `width: max-content` — an auto-width table shrinks to fit, never scrolls (T-1559)"+ )+ #expect(+ Self.releasesMaxWidth(body),+ "readable-mode table rule must contain `max-width: none` so the content-width table is not clamped back to the wrapper"+ )+ }+}
diff --git a/Makefile b/Makefileindex b87e8e6..c1e897c 100644--- a/Makefile+++ b/Makefile@@ -8,6 +8,12 @@ PROJECT = prism.xcodeproj BUNDLE_ID = me.nore.ig.prism CONFIG ?= Debug +# Cap how long xcodebuild waits for a -destination to resolve. A wirelessly+# paired iPhone that is unreachable otherwise makes the test/build actions hang+# indefinitely while CoreDevice probes it (even for the macOS destination). This+# does NOT affect the `id=$(DEVICE_ID)` install target, which must reach the device.+DEST_TIMEOUT = -destination-timeout 20+ # Pipe through xcbeautify if available, otherwise raw output XCBEAUTIFY := $(shell command -v xcbeautify 2>/dev/null) ifdef XCBEAUTIFY@@ -72,6 +78,7 @@ build-ios: xcodebuild build \ -project $(PROJECT) \ -scheme $(SCHEME) \+ $(DEST_TIMEOUT) \ -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \ -configuration $(CONFIG) \ -derivedDataPath $(DERIVED_DATA) \@@ -82,6 +89,7 @@ build-macos: clean xcodebuild build \ -project $(PROJECT) \ -scheme $(SCHEME) \+ $(DEST_TIMEOUT) \ -destination 'platform=macOS' \ -configuration $(CONFIG) \ -derivedDataPath $(DERIVED_DATA) \@@ -100,6 +108,7 @@ test-quick: xcodebuild test \ -project $(PROJECT) \ -scheme $(SCHEME) \+ $(DEST_TIMEOUT) \ -destination 'platform=macOS' \ -configuration Debug \ -derivedDataPath $(DERIVED_DATA) \@@ -114,6 +123,7 @@ test: xcodebuild test \ -project $(PROJECT) \ -scheme $(SCHEME) \+ $(DEST_TIMEOUT) \ -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \ -configuration Debug \ -derivedDataPath $(DERIVED_DATA) \@@ -126,6 +136,7 @@ test-ui: xcodebuild test \ -project $(PROJECT) \ -scheme $(SCHEME) \+ $(DEST_TIMEOUT) \ -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \ -configuration Debug \ -derivedDataPath $(DERIVED_DATA) \@@ -139,6 +150,7 @@ test-locales: xcodebuild build-for-testing \ -project $(PROJECT) \ -scheme $(SCHEME) \+ $(DEST_TIMEOUT) \ -destination 'platform=macOS' \ -configuration Debug \ -derivedDataPath $(DERIVED_DATA) \@@ -149,6 +161,7 @@ test-locales: xcodebuild test-without-building \ -project $(PROJECT) \ -scheme $(SCHEME) \+ $(DEST_TIMEOUT) \ -destination 'platform=macOS' \ -configuration Debug \ -derivedDataPath $(DERIVED_DATA) \@@ -161,6 +174,7 @@ test-locales: xcodebuild test-without-building \ -project $(PROJECT) \ -scheme $(SCHEME) \+ $(DEST_TIMEOUT) \ -destination 'platform=macOS' \ -configuration Debug \ -derivedDataPath $(DERIVED_DATA) \
diff --git a/CHANGELOG.md b/CHANGELOG.mdindex 1f22008..40f0d18 100644--- a/CHANGELOG.md+++ b/CHANGELOG.md@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Readable/wide tables in the WebKit renderer squashed their columns to fit the available width instead of scrolling horizontally (T-1559, a regression from the T-1542 cutover). Root cause: the inner `<table>` used `width: auto` (which shrinks columns to fit and never overflows) and the base `table { max-width: 100% }` cap was never overridden per display mode, so a too-wide table could never exceed its `.prism-table-wrap` container and the wrapper's `overflow-x: auto` never engaged. The `scroll` and `readable` table rules in `document.css` now set `width: max-content` + `max-width: none`, so the table takes its content width (in `readable` mode, the sum of the 300px-capped columns), overflows the wrapper, and scrolls within itself — the page never scrolls sideways (`html`/`body` keep `overflow-x: hidden`). The emitter, `data-prism-table-mode` tagging, and bridge were already correct, so this is CSS-only. Verified on an iOS device against `samples/tables-and-gfm.md`; a `DocumentCSSTableRulesTests` guard pins both properties on each scrolling-mode rule against silent re-breakage. - Web renderer Markdown fidelity — three CommonMark structures the WebKit renderer was losing now render faithfully (T-1558), guarded by a new `samples/`-driven compliance suite: - **Rich blocks inside list items** (tables, images, mermaid diagrams) render as their full elements in source order instead of flattening to a paragraph of text. A recursive section-less `renderInnerBlock` replaces the old `renderNestedBlock` (which flattened everything except code/blockquote/paragraph); nested code blocks now also carry their `language-*` class. The parser already produced the right `.block(.table/.image/.mermaid)` children — the loss was emitter-only. - **Ordered-list start numbers** are preserved: `3.` renders `<ol start="3">` (verbatim, including 0), threaded through nested lists and lists inside `<details>`. Blank-line-separated same-delimiter groups still merge into one renumbered list, matching CommonMark/GitHub. `MarkdownBlock.list` and `ListItem.NestedList` gained a `start` field captured from `OrderedList.startIndex`.@@ -27,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `readable-tables-restoration` smolspec (T-1559) diagnosing and planning the fix for the WebKit-renderer regression where readable/wide tables squash to fit instead of scrolling horizontally (lost in the T-1542 cutover). Root cause: `document.css`'s base `table { max-width: 100% }` is never overridden per mode, so a `scroll`-mode table's `width: max-content` and a `readable`-mode table's 300px cell caps are clamped to the `.prism-table-wrap` width and the wrapper's `overflow-x: auto` never engages. Planned fix is CSS-only — override `max-width: none` on the inner table in `scroll`/`readable` modes; the emitter, `data-prism-table-mode` tagging, and bridge plumbing are already correct. Verification is a `document.css` stylesheet guard test plus a required manual macOS/iOS check (a behavioural live-harness scroll test was rejected: the harness serves one fixed HTML for every `prism-doc://` request so `document.css` never applies, and `WebPage` has no sized viewport for stable geometry). smolspec, 4 tasks, and decision log under `specs/readable-tables-restoration/`. - WebView rendering verification suites (T-1542, phase 7) — the automated evidence the single-cutover release gate depends on. A live-`WebPage` security regression suite (`WebSecurityRegressionTests`) drives the full pipeline (emitter → sanitizer → `prism-doc://` handler under the verbatim CSP) for every Req 8.5 attack class — raw-HTML/SVG `<script>`, `<foreignObject>`, `<base>`, path traversal, `srcset` and CSS `url()` exfiltration, `javascript:` URLs, event-handler attributes, meta-refresh, and bridge forgery — asserting an execution sentinel never fires, a scheme-handler spy sees only mediated `prism-doc://` requests, forged/stale/unknown bridge messages are dropped, and the non-persistent store is empty after the session (Req 8.4); a defence-in-depth pass injects the script vectors raw (bypassing the sanitizer) to prove `allowsContentJavaScript = false` + CSP are independently sufficient. A parity suite (`WebParityFixtureTests`) captures the legacy SwiftUI renderer's model-level visible text as a golden baseline — committed markdown fixtures per block variant + combination documents under `prismTests/WebRendering/Fixtures/`, asserted equal to the emitter's DOM-extracted text (Req 1.1) — recorded now, before the cutover deletes the comparison baseline; raw-HTML cases live in a separate deliberate-change group asserting the new sanitized-render behaviour (Req 1.8), and search-parity corpora check `SearchService` counts against rendered highlight counts (Req 6.1). `prism-perf.js` adds an in-page rAF + Long-Animation-Frames probe reporting max frame gap and LoAF entries via the `perfSample` bridge message; `ConvergenceProbe` gains a web-scroll driver (bridge `scrollByPage`/`scrollToEdge` instead of the SwiftUI ScrollView) and `Tools/convergence-probe.sh` folds the in-page frame gap (DEGRADED > 250 ms, Req 9.2) and combined app+WebContent+GPU RSS (DEGRADED > 400 MB, Req 9.4) into its verdict, with a committed `Tools/fixtures/few-huge-rows.md` giant-nested-list fixture (the T-1513 stall shape); `diagFailure`/`perfSample` log via the existing facility with category/numbers only, no document content (Req 11.5). - WebView rendering search highlighting and footnote popovers (T-1542, phase 6). Search counts and navigation order stay in `SearchService`/`SearchCoordinator` unchanged; a new `SearchStateFeeder` translates that native truth into a per-block `{textMatchCount, matchedFootnoteIds, current}` payload pushed via `setSearchState`, where `textMatchCount` excludes appended footnote-content matches so every counted text match has a visible rendered equivalent (Req 6.1) and footnote-content matches surface as badge indication addressed by id, never a text ordinal (Req 7.2). `prism-search.js` (isolated bridge world) re-finds the query in each block's rendered text — crossing bold/inline-code formatting boundaries via concatenated text-node walking — and registers ranges on two named CSS Custom Highlights (`prism-search`, `prism-search-current`) windowed to the viewport ± margin and re-windowed on scroll, the deliberate satisfaction of "all matches highlighted" (Req 6.2); clearing the query empties both highlights (Req 6.3). The web view's built-in find navigator stays disabled so Cmd+F routes to Prism's search. One native change: `MarkdownBlock.searchableText` for raw-HTML blocks now returns `HTMLSanitizer.plainText` so counted matches are visible (Req 6.1, Decision 4). Footnote popovers are re-implemented over a per-window shared, non-persistent `WebPage` (`FootnotePopoverWebPage`) whose `prism-doc://` handler serves footnote-fragment HTML emitted by `BlockHTMLEmitter` (Decision 10); it renders fresh content per presentation and `reset()` clears served content so nothing is retained between footnotes or documents (Req 8.4), keeping the popover (iPad/macOS) / sheet (iPhone) presentation and the existing `prism://footnote/{id}` → coordinator routing. JS query matching is case-insensitive but intentionally not diacritic-folded (folding would shift the UTF-16 offsets the range mapping depends on; native still owns count parity). Tests: search feeder count-parity/current-match-addressing/raw-HTML-sanitized-text plus live CSS-Custom-Highlight registration/clear/badge/format-straddling, and footnote emitter rendering/supported-type filtering/badge-tap routing plus live present-replace-reset isolation. - WebView rendering notes integration (T-1542, phase 5) — notes work end-to-end in the web renderer, with a backup safeguarding the one irreversible failure mode. `prism-notes.js` (isolated bridge world) renders note indicators, inline note bubbles, and the document notes banner from `setNoteIndicators`/`setInlineNotes` payloads (native renders the bubble/banner HTML; JS injects it as `data-prism-chrome`, `user-select:none`), and posts `noteIndicatorTapped`/`inlineNoteTapped`/`blockContextRequested` (with the listItem/tableRow sub-target) on interaction; `WebDocumentMessageRouter` routes those onto the existing native note popover / `AddNoteSheet` / `onTapInlineNote` flows so native stays the source of truth (Req 5.2/5.3/5.6). Selection-anchored note creation — a new capability (Decision 4; previously `NoteTextRange` was import-only) — lets the user select text and add a note (Req 12): the JS resolves each selection endpoint to its `data-prism-run` ancestor + UTF-16 offset, maps through the source-map run, clamps to run boundaries and takes the covering range across runs in one block, and posts `selectionNote{blockID, range, rect}`; the controller slices the block's source text by that UTF-16 range and converts to `NoteTextRange`'s Swift-String character offsets (identical to inline-comment import) before calling `NotesManager.createNote`, declining a range that splits a surrogate pair or exceeds bounds. A cross-block selection is declined in-page with a catalog-supplied feedback label and posts nothing — no corrupt multi-block anchor (Req 12.4). `NotesBackupStore` (actor, `prism/Services/`) writes a one-per-document backup — byte-identical to `NotesStore`'s encoding — before `NotesManager`'s first relocation write under the new renderer; a backup-write failure blocks the relocation write and surfaces the existing notes-error path, and a user-confirmed Settings "Notes Backup" restore row does an atomic file replace that leaves the live store intact on failure (Req 5.4/5.7, Decision 6). Tests: live-`WebPage` notes rendering/interaction (`WebNotesBehaviourTests`, 10 cases), the native selection→`NoteTextRange` conversion incl. astral-char and surrogate-split/out-of-bounds decline (`WebSelectionNoteTests`), note-message routing (`WebDocumentMessageRouterTests`, 13 cases), and `NotesBackupStore` write-once/round-trip-byte-parity/atomic-restore plus relocation-parity corpus asserting relocation outcomes and JSON bytes are unchanged across the renderer change.
diff --git a/specs/readable-tables-restoration/smolspec.md b/specs/readable-tables-restoration/smolspec.mdnew file mode 100644index 0000000..dd621c7--- /dev/null+++ b/specs/readable-tables-restoration/smolspec.md@@ -0,0 +1,105 @@+# Readable Tables Restoration++## Overview++After the WebKit renderer cutover (T-1542), tables that should scroll horizontally+(readable and wide modes) instead squash their columns to fit the available width.+The HTML emitter and native bridge already compute and tag the correct display mode+per table; the regression is a CSS box-model bug in `document.css` that prevents a+wide table from ever overflowing its scroll wrapper. This restores the intended+behaviour: only the table scrolls horizontally, never the page.++## Requirements++- The system MUST render a table in `scroll` mode (`data-prism-table-mode="scroll"`)+ at its natural content width and scroll it horizontally within the+ `.prism-table-wrap` container when it exceeds the viewport.+- The system MUST render a table in `readable` mode (`data-prism-table-mode="readable"`)+ with columns capped at 300px (cells wrapping at that width), and scroll it+ horizontally within the `.prism-table-wrap` container when the capped table still+ exceeds the viewport.+- The system MUST leave `fitted` mode (`data-prism-table-mode="fitted"`) behaviour+ unchanged: a wide fitted table shares the available width / squashes to fit and does+ NOT scroll horizontally.+- The system MUST NOT introduce horizontal scrolling of the page (`html`/`body`) for any+ table; only the `.prism-table-wrap` container scrolls. (This is already guaranteed by+ the existing `html`/`body` `overflow-x: hidden`; the fix must not break it.)+- The system SHOULD preserve per-column text alignment, header styling, and the+ auto-selected initial mode already emitted by `BlockHTMLEmitter`.++## Implementation Approach++- **File to modify:** `prism/Resources/WebRenderer/document.css`, the table section+ (lines ~452–494).+- **Root cause:** The base rule `table { width: auto; max-width: 100% }` (line ~457)+ is never overridden per mode. In `scroll` mode `width: max-content` (line ~482) is+ capped by the inherited `max-width: 100%`, so the table cannot exceed+ `.prism-table-wrap` (whose width is the content column width). `overflow-x: auto` on+ the wrapper (line ~476) therefore never engages and the table squashes. The same cap+ defeats `readable` mode's 300px cell caps. `html`/`body` already set+ `overflow-x: hidden` (lines ~215/221), so the page is correctly pinned — only the+ inner table needs to be allowed to overflow the wrapper.+- **Fix:** Override `max-width: none` on the inner `<table>` in `scroll` and `readable`+ modes so the table grows to its natural (or 300px-capped) width and the wrapper's+ existing `overflow-x: auto` scrolls it.+ - `scroll`: keep `width: max-content`, add `max-width: none`.+ - `readable`: add a `table { max-width: none }` rule alongside the existing cell caps.+- **No emitter/Swift behaviour changes.** The emitter (`BlockHTMLEmitter.tableHTML`), the+ `data-prism-table-mode` attribute, `TableDisplayMode.initialMode`, and the+ `setTableModes`/`tableModeToggled` bridge plumbing are already correct and tested+ (`prismTests/WebRendering/BlockHTMLEmitterTests.swift`).+- **Pattern reference:** The intended scroll-within-wrapper model is documented in the+ existing CSS comment block (lines ~469–494) and `specs/readable-tables/requirements.md`+ (Req 1.4, 1.7, 5.1) and `specs/wide-tables/smolspec.md`.+- **Out of Scope:**+ - 80pt minimum column width (readable-tables Req 1.2/5.1) — deferred; not required to+ stop the squashing.+ - The per-table mode-toggle button UI — intentionally hidden in favour of auto-defaults+ (per `specs/readable-tables/requirements.md`); the bridge listener already exists.+ - Any change to table parsing, `MarkdownBlock.table`, or `TableDisplayMode`.+ - The retired SwiftUI `AccessibleTableView` Grid path and its `known_bugs.md` items.++### Verification++CSS layout cannot be asserted by the existing test infrastructure: the live `WebPage`+harness (`prismTests/WebRendering/WebDocumentLiveHarness.swift` /+`SpikeWebPageHarness`) serves one fixed HTML document for every `prism-doc://` request,+so the emitted `document.css` stylesheet link is never applied, and `WebPage` has no+sized viewport in tests, making `scrollWidth`/`clientWidth` geometry unreliable. A+behavioural scroll test is therefore rejected (see `decision_log.md`). Verification is+two-pronged:++- **Stylesheet guard (automated regression pin):** A unit test loads the bundled+ `document.css` (via `Bundle.main.url(forResource: "document", withExtension: "css")`,+ the same path the scheme handler and other tests use) and asserts that the `scroll`+ and `readable` table rules release the `max-width` cap (contain `max-width: none`).+ This pins the specific rule against silent re-breakage; it does NOT prove rendered+ scrolling.+- **Manual device/simulator check (primary behavioural verification):** Open a document+ containing (a) a complex table that auto-selects `readable` (>3 columns or any cell+ >40 chars) and (b) a wide table, on macOS and iOS, and confirm: the table scrolls+ horizontally WITHIN itself; readable columns cap at ~300px and wrap; the page/body does+ NOT gain a horizontal scrollbar; and a simple `fitted` table still shares width without+ scrolling.++## Risks and Assumptions++- **Risk:** `max-width: none` could let a `fitted` table overflow / start scrolling. |+ Mitigation: the override is scoped to `[data-prism-table-mode="scroll"]` and+ `="readable"` only; `fitted` keeps the base `max-width: 100%`. The manual check+ explicitly confirms a wide fitted table still squashes and does not scroll.+- **Risk:** `max-width: 300px` on `<td>`/`<th>` under `table-layout: auto` may not+ constrain columns in WebKit, leaving `readable` identical to `scroll`. | Mitigation:+ the existing rule pairs the cap with `overflow-wrap`/`word-break`, which WebKit honours;+ the manual check confirms readable columns cap at ~300px and wrap. If the cap proves+ ineffective during implementation, that is a finding to surface, not silently accept.+- **Risk:** CSS layout cannot be asserted automatically, so the stylesheet guard pins+ the rule text but not behaviour. | Mitigation: pair the guard with the manual+ device/simulator check (see Verification); behavioural live-harness testing was+ evaluated and rejected (`decision_log.md`).+- **Assumption:** `html`/`body` `overflow-x: hidden` keeps the page from scrolling+ sideways once the inner table is allowed to overflow its wrapper (verified in+ `document.css` lines ~215/221).+- **Assumption:** The emitter continues to auto-select `readable` for complex tables+ (>3 columns or any cell >40 chars) and `fitted` otherwise — verified by+ `BlockHTMLEmitterTests.tableInitialMode`.
diff --git a/specs/readable-tables-restoration/decision_log.md b/specs/readable-tables-restoration/decision_log.mdnew file mode 100644index 0000000..2fda44c--- /dev/null+++ b/specs/readable-tables-restoration/decision_log.md@@ -0,0 +1,106 @@+# Decision Log: Readable Tables Restoration++## Decision 1: Fix the regression in CSS only, not the emitter++**Date**: 2026-06-17+**Status**: accepted++### Context++After the T-1542 WebKit cutover, readable/wide tables squash to fit the available width+instead of scrolling horizontally (T-1559). Investigation showed `BlockHTMLEmitter`+already wraps every table in `.prism-table-wrap` and tags it with the correct+`data-prism-table-mode` (`fitted`/`readable`/`scroll`), and the `setTableModes` bridge+plumbing is intact and unit-tested. The defect is entirely in `document.css`: the base+rule `table { max-width: 100% }` is never overridden per mode, so a `scroll`-mode table's+`width: max-content` and a `readable`-mode table's 300px cell caps are both clamped to the+wrapper width. The table can never exceed the wrapper, so the wrapper's `overflow-x: auto`+never engages.++### Decision++Fix the regression by overriding `max-width: none` on the inner `<table>` in `scroll` and+`readable` modes in `document.css`. Make no changes to the emitter, the model, or the+bridge.++### Rationale++The emitter and bridge already produce the correct DOM and are covered by passing tests.+The single root cause is one un-overridden CSS property. A CSS-only fix is the smallest+change that restores the behaviour and carries the least regression risk.++### Alternatives Considered++- **Change the base `table` rule to drop `max-width: 100%`**: Rejected — a `fitted` table+ would then be free to overflow and scroll, breaking fitted mode. The cap must stay for+ fitted; only scroll/readable should release it.+- **Re-introduce a SwiftUI table view**: Rejected — the SwiftUI/Textual renderer was+ retired in T-1542; reviving it contradicts the architecture.++### Consequences++**Positive:**+- Minimal, low-risk change isolated to one stylesheet.+- Restores readable/wide scrolling without touching parsing, the emitter, or the bridge.++**Negative:**+- CSS behaviour is not unit-testable here (see Decision 2), so part of the verification is+ manual.++---++## Decision 2: Verify via a stylesheet guard plus manual check, not a behavioural live test++**Date**: 2026-06-17+**Status**: accepted++### Context++A reviewer asked for a behavioural test asserting the table actually scrolls+(`tableEl.scrollWidth > wrap.clientWidth`) and the page does not. Two infrastructure facts+make that infeasible without disproportionate work and flakiness:++1. The live `WebPage` harness (`SpikeWebPageHarness`) serves one fixed HTML document for+ every `prism-doc://` request, so the emitted `<link rel="stylesheet"+ href="prism-doc://…/document.css">` resolves to the HTML itself — `document.css` is+ never applied. A behavioural test would first require teaching the harness to serve the+ real bundled CSS.+2. `WebPage` in tests has no sized window/viewport, so `clientWidth` and `scrollWidth`+ geometry is undefined/unstable, making any layout-geometry assertion unreliable.++### Decision++Verify the fix with (a) an automated stylesheet guard test that loads the bundled+`document.css` and asserts the `scroll`/`readable` table rules contain `max-width: none`,+and (b) a required manual device/simulator check on macOS and iOS. Do not add a+behavioural live-harness scroll test.++### Rationale++The guard pins the exact rule that regressed, so a future edit that re-adds the cap fails a+test. The manual check is the honest behavioural verification for a visual CSS change and+also covers the two things the guard cannot: that readable columns actually cap at ~300px+and that fitted tables still do not scroll. Building a reliable behavioural harness test+would cost more than the fix itself and risks a flaky test in a suite already prone to+WebKit-test flakiness.++### Alternatives Considered++- **Behavioural live-harness test**: Rejected for this change — requires harness changes to+ serve real CSS and a sized viewport for stable geometry; high effort and flakiness risk+ for a one-property fix. Revisit if the harness gains CSS serving + viewport sizing.+- **No automated test (manual only)**: Rejected — leaves the specific rule unguarded+ against silent regression, which is exactly how this defect slipped in.++### Consequences++**Positive:**+- Cheap, deterministic automated pin on the regressed rule.+- Behavioural correctness is still verified, on real platforms.++**Negative:**+- The guard asserts rule text, not rendered behaviour; it must be paired with the manual+ check to be meaningful.+- Manual verification is a human step that cannot run in CI.++---
diff --git a/specs/readable-tables-restoration/tasks.md b/specs/readable-tables-restoration/tasks.mdnew file mode 100644index 0000000..e95afd8--- /dev/null+++ b/specs/readable-tables-restoration/tasks.md@@ -0,0 +1,30 @@+---+references:+ - smolspec.md+---+# Readable Tables Restoration++- [x] 1. Readable and wide tables scroll horizontally within their wrapper instead of squashing <!-- id:de2btfq -->+ - In prism/Resources/WebRenderer/document.css, release the inner table max-width cap for scroll and readable modes so a too-wide table overflows .prism-table-wrap and uses its existing overflow-x: auto.+ - scroll mode: keep width: max-content and add max-width: none.+ - readable mode: add a table { max-width: none } rule alongside the existing 300px cell caps.+ - Leave fitted mode (base max-width: 100%) untouched. See smolspec.md Implementation Approach.+ - Verify: make build-macos and make lint succeed; spot-check that a complex/auto-readable table now scrolls within itself.++- [x] 2. Stylesheet guard test pins the scroll/readable max-width override <!-- id:de2btfr -->+ - Add a unit test that loads the bundled document.css via Bundle.main.url(forResource: document, withExtension: css) and asserts the scroll and readable table rules release the cap (contain max-width: none), failing if the cap is reintroduced.+ - Honestly a rule-pin, not a behavioural proof (see decision_log.md Decision 2).+ - Verify: test passes; temporarily removing the override makes it fail.+ - Blocked-by: de2btfq (Readable and wide tables scroll horizontally within their wrapper instead of squashing)++- [x] 3. Existing table rendering tests and lint pass with no regression <!-- id:de2btfs -->+ - Run the table/emitter suites (prismTests/WebRendering/BlockHTMLEmitterTests, WideTableTests) plus the new guard test, and make lint.+ - Confirm fitted-mode emission and all existing table behaviour are unaffected.+ - Verify: targeted tests green, lint clean.+ - Blocked-by: de2btfr (Stylesheet guard test pins the scroll/readable max-width override)++- [x] 4. Manual macOS and iOS verification of rendered table scrolling <!-- id:de2btft -->+ - Open a document containing a complex auto-readable table (>3 columns or any cell >40 chars) and a wide table in the running app on macOS and iOS.+ - Confirm per smolspec.md Verification: each table scrolls horizontally WITHIN itself; readable columns cap at ~300px and wrap; the page/body gains NO horizontal scrollbar; a simple fitted table still shares width without scrolling.+ - Verify: observed behaviour matches the criteria.+ - Blocked-by: de2btfq (Readable and wide tables scroll horizontally within their wrapper instead of squashing)
max-width on table cells is 'undefined' in CSS 2.1; modern WebKit honors it with word-break. Confirmed on-device that readable columns cap and wrap while the table scrolls.
Targeted relevant classes pass and the change is CSS + Makefile only; the full suite has unrelated pre-existing flakiness and was not re-run here. Run make test-quick / make test for the complete matrix.