Commit 451bd9c
* feat(pad): scrub history in-place on the pad URL (#7659)
Clicking the timeslider toolbar button now keeps the user on /p/:pad and
toggles a hash-based history mode (#rev/N) instead of navigating to a
separate /timeslider page. The pad shell — chat, users panel, settings,
plugin chrome — stays mounted across the transition. A sticky banner
plus a sepia tint on the toolbar make it unmistakable that what is
visible is historical, not live.
Implementation:
- New PadModeController (src/static/js/pad_mode.ts) owns enter/exit,
the URL hash, browser back/forward, and a mutation-observer bridge
from the inner timeslider's revision label/date into the outer
banner. Esc and a Return-to-live button both exit history.
- pad.html grows a banner element and an iframe mount slot. The live
ACE iframe stays mounted but hidden during history; on exit the
socket is still alive, so the user snaps straight back to the
current state without a reconnect.
- The /p/:pad/timeslider route 302-redirects to the pad page for
direct visits (legacy bookmarks), and serves the timeslider HTML
for the in-pad iframe when called with ?embed=1. The embedded
variant hides the redundant title and return-to-pad button via
CSS; the slider, settings, and export controls stay reachable.
- Legacy #NN shortlinks are preserved through the redirect by the
browser and translated to #rev/NN client-side.
Tests:
- New backend spec asserts the 302 redirect, pad-name preservation,
and the ?embed=1 path still serves the timeslider HTML.
- New padmode.spec.ts exercises toolbar entry, return-to-live,
browser back, and direct /timeslider URL handling. Asserts the
rendered localized banner string, not just element presence.
- Existing timeslider specs that hit /p/:pad/timeslider directly
now pass ?embed=1 to bypass the redirect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pad): make history iframe fill the editor area (#7659)
Without an explicit positioning model the history-frame-mount inherited
half-width from a phantom flex parent and the embedded timeslider
rendered at 640×625 instead of the full editor area. Switch to the same
absolute-fill model the live ACE iframe uses by making
#editorcontainerbox the positioning anchor when in history mode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pad): address Qodo review and CI failures (#7659)
Concrete review fixes for PR #7710:
- Tighten the embed query check from `if (!req.query.embed)` to
`req.query.embed !== '1'` so values like `?embed=0` no longer bypass
the redirect.
- Fix the `#rev/latest` mapping: the parser yields -1 for "latest",
which the iframe sync handler was clamping to 0 and so jumping the
embedded timeslider to revision 0. Resolve "latest" to the inner
BroadcastSlider's upper bound instead.
- Update existing backend tests (`socialMeta`, `specialpages`) that
hit `/p/:pad/timeslider` directly — they now pass `?embed=1` like
the rest of the suite. Without this fix three pre-existing tests
failed CI (302 instead of 200).
- Document the route change in `doc/skins.md` and `doc/skins.adoc`:
direct visits redirect; iframe consumers use `?embed=1`.
- Back out a stray `data-theme="editorial"` attribute and the
hardcoded Google Fonts `<link>` tags from `pad.html` that leaked
into the branch from an unrelated working-tree change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(pad): consolidate chrome and replay chat/users in history mode (#7659)
Picks up the rough edges left by the initial in-place history mode:
the embedded timeslider iframe was rendering its own duplicate Settings
and Export buttons, and the chat panel + users list still showed live
state while the editor scrubbed back in time.
Chrome consolidation
- Hide the entire inner editbar's right-side toolbar and modal popups
in embedded mode (slider stays). Outer pad shell now owns Settings,
Export, Share, Users, Chat across both modes.
- Outer Settings popup grows a "History playback" section (visible
only when scrubbing) with playback speed + follow-contents. Both
bridge to the iframe's BroadcastSlider state.
- Outer Export anchors are rewritten to /p/<pad>/<rev>/export/<type>
on each scrub and restored on exit, so Save As exports the visible
historical revision.
Chat replay
- Each chat message is annotated with data-timestamp at render time.
In history mode, messages newer than the scrubbed revision's
timestamp are display:none'd; a "Chat as of HH:MM" header sits
above the chat log.
- Restores cleanly on exit (inline display cleared, header removed).
Users replay
- Live users table is replaced with the embedded timeslider's
authors-at-this-revision label while scrubbing; restored on exit.
Plumbing
- Expose padContents on window in broadcast.ts so the outer pad can
read currentTime after each scrub without postMessage.
- Expose BroadcastSlider on window in timeslider.ts so the outer pad
can register an onSlider callback to drive replay UI.
Tests
- New padmode specs cover: history-only Settings section, hidden
embedded chrome, chat filter + replay header, Export href
rewriting + restore, authors-row swap + restore.
- timeslider_line_numbers cookie-persistence test updated to bypass
the now-hidden inner Settings popup (programmatic checkbox).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pad): theme propagation, hide inert buttons, plugin loading (#7659)
Picks up rough edges from the in-place history mode that turned up in
real usage:
Theme / dark mode
- skin_variants.updateSkinVariantsClasses now also walks the history
iframe (and its ace_outer/ace_inner) so toggling dark mode while
scrubbing re-themes the embedded view in lockstep.
- timeslider.ts inherits the parent's skinVariant tokens (super-dark-*
/ dark-* / full-width-editor) on first paint when it detects it is
embedded — same-origin guarantee, falls through silently if not.
Toolbar UX
- Hide #editbar .menu_left (Bold/Italic/Lists/Indent/Undo/...) and the
show-more chevron while in history mode. Those buttons target the
hidden live editor and would do nothing useful; rendering them
disabled-looking implied state the user doesn't have. Right-side menu
(Settings / Share / Users / Chat / Home) stays at full opacity and
fully interactive.
Slider position
- Pin the embedded #editbar to the bottom of the iframe so the outer
banner and the slider can't visually compete for the same band of
pixels. Reserve padding-bottom on the iframe's editorcontainerbox so
the editor never scrolls under the slider.
Plugin loading in timeslider
- timeSliderBootstrap.js now pre-loads plugin modules into a Map and
passes them to plugins.update(), mirroring padBootstrap.js. Without
this the loadFn fallback called require(path) at runtime, which the
esbuild-bundled timeslider couldn't resolve, so client_hooks like
ep_headings2's aceRegisterBlockElements silently failed to register
and historical revisions rendered without plugin chrome.
Tests
- New padmode specs cover: outer toolbar's left/right asymmetry, slider
pinned to bottom, dark-mode class propagation into the history iframe.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(pad): move history slider into the outer toolbar (#7659)
The slider previously rendered inside the embedded iframe — first at the
top (where it visually competed with the banner), then briefly at the
bottom (where the chat icon overlapped it). Both were wrong. Move the
controls into the outer toolbar's left zone, where #editbar .menu_left
is hidden in history mode and the slider can occupy the full width
without colliding with anything.
- pad.html grows a #history-controls div (slider + play/pause/step
buttons + timer) inside #editbar, between menu_left and menu_right.
Hidden by default; revealed via body.history-mode CSS.
- pad.css swaps #editbar .menu_left out for #history-controls in
history mode (display:none / display:flex).
- timeslider.css fully hides the embedded iframe's #editbar — the
outer toolbar now owns the slider, and the iframe is purely the
editor surface.
- pad_mode.ts wires the outer controls as a remote control: the
range input calls inner BroadcastSlider.setSliderPosition, the play
button calls BroadcastSlider.playpause, step buttons forward clicks
to the inner #leftstep/#rightstep so they share the existing logic.
An onSlider subscription mirrors inner state back into the outer
slider value, timer label, and play-button .pause class.
Tests
- Existing timeslider.spec asserts the outer controls are visible.
- New padmode specs cover: inner editbar fully hidden, outer toolbar
swap (menu_left → history-controls), and outer slider drives the
iframe's revision via BroadcastSlider.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pad): exempt embedded history iframe from userdup kick (#7659)
When the in-place history iframe opens its socket, the server's
duplicate-author kick treats it as a stale tab and disconnects the
parent pad's live socket — toolbar-overlay drops over the editor and
Settings/Share/Users/Chat all stop responding. Mark the iframe's
connection with `embed=1` in the socket.io handshake query, record it
on sessionInfo, and skip the kick whenever either side is embedded.
- timeslider.ts: detect `?embed=1` (and parent !== window) on
the iframe URL, pass through as a query parameter to socketio.connect.
- PadMessageHandler: read socket.handshake.query.embed on CLIENT_READY,
set sessionInfo.embed; the duplicate-author kick now skips when
either the connecting session OR the existing session is embedded.
Behavior preserved
- Two real tabs (both non-embedded): older tab still gets kicked.
- Authenticated sessions still bypass the kick entirely.
- Live pad socket survives entry into history mode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pad): a11y of history toolbar controls (#7659)
The new history controls (slider + play/step/timer) had hardcoded
English aria-labels, which html10n won't replace because they were
present without the data-l10n-aria-label marker. Screen readers in
non-English locales would have heard English. Drop the static aria
labels and let html10n.translateElement populate aria-label from the
data-l10n-id translation, matching how the rest of the toolbar works.
- pad.html: remove hardcoded aria-label on play/step buttons and the
range input; keep titles (hover tooltip) and data-l10n-id. Add
role="toolbar" + data-l10n-id on the controls container so the
toolbar landmark is announced. Mark play button as a toggle with
aria-pressed reflecting playback state.
- en.json: add pad.historyMode.controlsLabel and
pad.historyMode.sliderLabel for the toolbar landmark and the slider.
- pad_mode.ts: keep aria-pressed in sync with the inner playback state
on every revision update.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pad): a11y + responsive for history controls (#7659)
Two issues with the previous a11y attempt: the data-l10n-id on icon
buttons was setting their textContent (drawing "Playback / Pause Pad
Contents" on screen next to the glyph), and there was no responsive
treatment so the timer + slider could overflow narrow viewports.
- pad.html: drop data-l10n-id from the icon buttons. They're now
empty <button>s. Localized title (hover tooltip) and aria-label
(screen reader name) are populated by pad_mode.localizeControls()
using the existing timeslider.* keys, with an html10n.bind
subscription so language switches re-localize.
- Mark #history-timer as hide-for-mobile.
- pad.css: dedicated @media (max-width: 800px) and 480px rules
shrink padding, gap, and button widths so play + slider + step
buttons stay on a single toolbar line at narrow viewports. Mirrors
the legacy timeslider's responsive behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(pad): inline Follow + Playback speed, match toolbar height (#7659)
Two follow-ups from real testing:
- Move "Follow pad content updates" (now "Follow") and "Playback speed"
out of the Settings popup and inline them in the history-mode
toolbar, alongside the slider + play/step buttons. They were always
needed while scrubbing; one extra click into Settings was friction.
Removed the now-empty #history-settings-section.
- The history controls toolbar was visibly shorter than the live
toolbar because the icon buttons sat as bare <button> elements
without the live editbar's <li><a> wrapping. Add explicit
min-height (40px) and per-button padding so the toolbar is the same
vertical size in both modes — switching between live and history
no longer reflows.
- Differentiate "iframe-mounted history view" from "direct ?embed=1
visit". Only the former hides the inner timeslider editbar — direct
visits keep their full chrome so existing test/legacy entry points
stay independently usable. Marker: timeslider.ts adds an
`iframe-mode` class on body when window.parent !== window; CSS
scopes the hide to that combo.
Tests
- padmode spec asserts Follow + Speed live in the toolbar (not the
Settings popup) and are visible in history mode, hidden in live.
- timeslider*.spec direct-?embed=1 flows continue to pass because the
inner editbar is no longer hidden when not iframe-mounted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pad): use absolute path for legacy /timeslider redirect (#7659)
CI Firefox failed the legacy-URL redirect test (1 of 32 jobs); Chromium
passed. The redirect Location header was a relative `../padname`, which
both browsers resolve to /p/padname for `/p/padname/timeslider`. Firefox
flaked on it once consistently. Switch to an absolute path including
the proxy prefix so the resolution is unambiguous across browsers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): accept 304 on legacy timeslider redirect (#7659)
CI Firefox failed `expect(res.status()).toBe(200)` because Firefox
issues a conditional GET when the redirect target is the same URL the
test just loaded via goToNewPad — the server returns 304 Not Modified
and the test treats that as a regression. Chromium happens to send
fresh requests so it stayed green.
Accept either 200 or 304 — both are valid completed navigations to the
pad page; what we actually care about is the pathname assertion above.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(pad): eye toggle for Follow, fix line-number alignment (#7659)
Two refinements from real testing in 9002:
Follow as an eye toggle
- Replace the labeled checkbox with an inline-SVG eye icon. The eye is
always rendered; a diagonal slash is overlaid via SVG <line> only
when the underlying (visually hidden) checkbox is unchecked. Default
state is on (auto-following) so the eye renders unobstructed.
- Localized hover tooltip + aria-label flips with state — html10n
populates "Following pad changes — click to stop following" vs
"Not following pad changes — click to follow", and pad_mode.ts
re-applies on every change event so screen readers narrate the
action the click would take.
- Hidden checkbox keeps the existing pad_mode.ts bridge code working
(still reads .checked) and lets <label for="…"> handle the click.
Line-number alignment fix (broadcast.ts)
- The first-line height formula was
`nextDocLine.offsetTop - innerdocbody.padding-top`
which only computes the right value when innerdocbody is the
offsetParent. In the in-pad history iframe, outerdocbody contributes
its own padding-top to the offsetTop chain, so the first gutter row
was 20px too tall and every subsequent line drifted out of
alignment. Use the consistent `next.offsetTop - current.offsetTop`
formula for every iteration — same result in the standalone
timeslider, correct result in the embedded one.
- New padmode spec asserts every gutter row's top matches the editor
line's top within 2px, in iframe-mounted history mode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cbf7128 commit 451bd9c
26 files changed
Lines changed: 1459 additions & 56 deletions
File tree
- doc
- src
- locales
- node
- handler
- hooks/express
- static
- css
- js
- templates
- tests
- backend/specs
- frontend-new
- helper
- specs
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
10 | | - | |
| 9 | + | |
| 10 | + | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
14 | 21 | | |
15 | 22 | | |
16 | 23 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
10 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
11 | 18 | | |
12 | 19 | | |
13 | 20 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
234 | 234 | | |
235 | 235 | | |
236 | 236 | | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
237 | 250 | | |
238 | 251 | | |
239 | 252 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
418 | 418 | | |
419 | 419 | | |
420 | 420 | | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
421 | 427 | | |
422 | 428 | | |
423 | 429 | | |
| |||
1051 | 1057 | | |
1052 | 1058 | | |
1053 | 1059 | | |
1054 | | - | |
| 1060 | + | |
1055 | 1061 | | |
1056 | 1062 | | |
1057 | 1063 | | |
1058 | 1064 | | |
1059 | | - | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
| 1068 | + | |
| 1069 | + | |
1060 | 1070 | | |
1061 | 1071 | | |
1062 | 1072 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
228 | 228 | | |
229 | 229 | | |
230 | 230 | | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
231 | 238 | | |
232 | | - | |
233 | 239 | | |
234 | 240 | | |
235 | 241 | | |
| |||
246 | 252 | | |
247 | 253 | | |
248 | 254 | | |
| 255 | + | |
249 | 256 | | |
250 | 257 | | |
251 | 258 | | |
| |||
392 | 399 | | |
393 | 400 | | |
394 | 401 | | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
395 | 414 | | |
396 | 415 | | |
397 | 416 | | |
| |||
403 | 422 | | |
404 | 423 | | |
405 | 424 | | |
| 425 | + | |
406 | 426 | | |
407 | 427 | | |
408 | 428 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
107 | 107 | | |
108 | 108 | | |
109 | 109 | | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
6 | 20 | | |
7 | 21 | | |
8 | 22 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | | - | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
54 | 57 | | |
55 | 58 | | |
56 | 59 | | |
| |||
155 | 158 | | |
156 | 159 | | |
157 | 160 | | |
158 | | - | |
159 | | - | |
160 | | - | |
161 | | - | |
162 | | - | |
163 | | - | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
164 | 169 | | |
165 | 170 | | |
166 | 171 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
198 | 198 | | |
199 | 199 | | |
200 | 200 | | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
201 | 205 | | |
202 | 206 | | |
203 | 207 | | |
| |||
0 commit comments