etherpad-lite/src/static/css/pad.css
John McLear a65e3b75a4
fix(pad): show saved-revision markers in in-pad history mode (#7946) (#7948)
* fix(pad): show saved-revision markers in in-pad history mode (#7946)

When #7659 moved the timeslider into the pad as an embedded iframe, the
user-facing control became the outer #history-slider-input range input.
The saved-revision stars are still drawn by broadcast_slider.ts into the
iframe's #ui-slider-bar, but that DOM is hidden in embed mode, and
pad_mode.ts bridged rev/max/value/timer/authors to the outer slider but
never the saved revisions. Result: clicking "Save Revision" appeared to
work but no markers showed in the timeslider (regression in 3.3.x).

Bridge the embedded timeslider's clientVars.savedRevisions onto the outer
slider as percentage-positioned star markers, rendered on first sync and
re-rendered when the slider max changes. Markers are a purely visual,
aria-hidden overlay (keyboard/SR users already reach any revision via the
slider + step buttons) with a click-to-seek convenience for mouse users.

Adds a frontend-new Playwright spec exercising the real user flow (save a
revision in the pad, enter in-pad history mode, assert a visible marker on
the outer slider). The only prior coverage lived in the legacy mocha
suite (src/tests/frontend/specs/timeslider_revisions.js) which no CI
workflow runs, and the modern timeslider specs drive the ?embed=1 iframe
directly and never exercise the outer history UI — so this regression was
invisible to CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pad): live saved-revision markers + Qodo review fixes (#7946)

Address Qodo review on #7948:

- Live updates (bug 1): the server's SAVE_REVISION handler never broadcast
  NEW_SAVEDREV, so the client handler that adds a star was dead and no open
  timeslider ever updated live. Wire it up: Pad.addSavedRevision returns the
  new revision (undefined on duplicate); handleSaveRevisionMessage broadcasts
  NEW_SAVEDREV to the pad room. pad_mode.ts now sources outer markers from the
  embedded slider's live #ui-slider-bar .star DOM (labels from clientVars) and
  observes that bar so a revision saved by a collaborator appears on an
  already-open history slider. Live editors ignore the unknown message type.

- Single-revision pad (bug 2): allow max === 0 so a revision saved at rev 0
  still renders a marker instead of being cleared by the old max <= 0 guard.

- Test rigor (bug 3): assert the marker's inline left percentage directly
  instead of falling back to a layout coordinate, which let left:0% pass.

Adds a two-client Playwright test for the live path (verified it fails with
the server broadcast removed). Backend Pad + pad API specs (88) still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 12:12:36 +01:00

348 lines
10 KiB
CSS

@import url("pad/normalize.css");
@import url("pad/layout.css");
@import url("pad/fonts.css");
@import url("pad/toolbar.css");
@import url("pad/popup.css");
@import url("pad/popup_connectivity.css");
@import url("pad/popup_import_export.css");
@import url("pad/popup_users.css");
@import url("pad/icons.css");
@import url("pad/chat.css");
@import url("pad/gritter.css");
@import url("pad/loadingbox.css");
@import url("pad/form.css");
/* Screen reader only — visually hidden but announced by assistive technology */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/*
* Skip link — hidden until keyboard focus reveals it at the top-left of the
* viewport, giving screen-reader and keyboard-only users an immediate jump
* past the toolbar to the editor (WCAG 2.4.1 Bypass Blocks).
*/
.skip-link {
position: absolute;
top: -100px;
left: 8px;
z-index: 10000;
padding: 8px 12px;
background: #2c3e50;
color: #fff;
text-decoration: none;
border-radius: 0 0 4px 4px;
}
.skip-link:focus {
top: 0;
outline: 2px solid #fff;
outline-offset: 2px;
}
html {
font-size: 15px;
color: #3e3e3e;
}
html,
#sidedivinner > div:before {
font-family: Roboto;
}
.clear {
clear: both
}
a {
color: inherit;
overflow-x: auto;
white-space: nowrap;
}
a img {
border: 0
}
.thin-scrollbar::-webkit-scrollbar-track {
background-color: #f6f6f6;
border: 1px solid #f0f0f0;
}
.thin-scrollbar::-webkit-scrollbar {
width: 7px;
}
.thin-scrollbar::-webkit-scrollbar-thumb {
background-color: #C5C5C5;
}
.buttontext::-moz-focus-inner {
padding: 0;
border: 0;
}
.buttontext:focus{
/* Not sure why important is required here but it is */
border: 1px solid #666 !important;
}
.rtl {
direction: RTL
}
/* fix for misaligned checkboxes */
input[type=checkbox] {
vertical-align: -1px
}
input {
color: inherit;
}
.right {
float: right
}
@media (max-width: 800px) {
.hide-for-mobile { display: none; }
}
.etherpadBrand{
width:20%;
max-width:100px;
margin-left:auto;
margin-right:auto;
}
/* ----------------------------------------------------------------------- */
/* History mode (issue #7659): timeslider rendered in-place inside the */
/* pad page. The live editor stays mounted but hidden; a sibling iframe */
/* hosts the existing timeslider replay code. */
/* ----------------------------------------------------------------------- */
.history-banner {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
background: #fff8e1;
border-bottom: 1px solid #f0d27a;
color: #5b4b00;
font-size: 14px;
z-index: 5;
}
.history-banner[hidden] { display: none; }
.history-banner-label { font-weight: 600; }
.history-banner-rev,
.history-banner-date { opacity: 0.85; }
.history-banner #history-banner-return { margin-left: auto; }
/* The history iframe takes the place of the live ACE editor by occupying the
* same in-flow flex slot that #editorcontainer fills in live mode (flex: 1 1
* auto inside the row-flex #editorcontainerbox). This keeps the layout
* identical between modes — crucially including any in-flow side panels in
* #editorcontainerbox, such as ep_webrtc's video column, which now sit beside
* the history iframe exactly as they sit beside the live editor.
*
* This used to be an absolute, inset:0 overlay: it filled the editor area but
* took the iframe out of flow, so an in-flow side panel ended up hidden
* beneath it (see ether/ep_webrtc rendering nothing in the timeslider). */
.history-frame-mount {
display: none;
flex: 1 1 auto;
min-width: 0; /* allow the iframe to shrink to make room for a side panel */
border: 0;
background: var(--bg-color, #f2f3f4);
}
.history-frame-mount[hidden] { display: none; }
.history-frame-mount > iframe {
width: 100%;
height: 100%;
border: 0;
display: block;
}
/* While in history mode, hide the live ACE editor and show the history */
/* iframe in its place. The formatting menu on the left side of the */
/* toolbar (Bold/Italic/Lists/Indent/Undo/etc.) targets the hidden live */
/* editor, so we swap it for the history controls (slider + play/step */
/* buttons) that drive the iframe. The right-side menu (Settings / Share / */
/* Users / Chat / Home) stays fully interactive across modes. */
/* The two-id `#editorcontainerbox #editorcontainer { display: flex }` layout
* rule (specificity 0-2-0) outranks a plain `body.history-mode #editorcontainer`
* (0-1-1), so match the same container path here to actually win the cascade
* and remove the live editor from flow. This was latent before: the old
* absolute, inset:0 iframe overlay painted over the still-in-flow editor, so
* nobody noticed it wasn't hidden. Now that the iframe sits in the normal
* flex flow, the editor must genuinely be gone or the two would split the row. */
body.history-mode #editorcontainerbox #editorcontainer { display: none; }
body.history-mode .history-frame-mount { display: block; }
body.history-mode #editbar .menu_left { display: none; }
body.history-mode #editbar .show-more-icon-btn { display: none; }
body.history-mode #history-controls { display: flex; }
/* History toolbar controls (issue #7659): a slider + play/pause/step */
/* buttons + Follow/Speed controls that remote-control the embedded */
/* timeslider iframe. Take the place of the formatting menu while */
/* scrubbing. align-items + min-height keep the toolbar the same vertical */
/* size as in live mode so swapping modes doesn't reflow the layout. */
.history-controls {
display: none;
flex: 1 1 auto;
align-items: center;
gap: 8px;
padding: 0 12px;
min-width: 0;
min-height: 40px;
}
.history-controls[hidden] { display: none; }
.history-controls button.buttonicon {
flex: 0 0 auto;
/* Match the live-toolbar buttonicon visual weight (no <li><a> wrapper */
/* gives us a smaller default; bump padding so the icon hit area lines */
/* up vertically with menu_right's settings/share/users/etc. icons). */
padding: 6px 8px;
background: transparent;
border: 0;
cursor: pointer;
font-size: 15px;
color: inherit;
}
.history-controls button.buttonicon:hover { background: rgba(0,0,0,0.06); border-radius: 4px; }
.history-controls button.buttonicon.buttonicon-play.pause::before {
content: "\e829";
}
.history-slider-wrap {
position: relative;
flex: 1 1 auto;
min-width: 80px;
margin: 0 6px;
display: flex;
align-items: center;
}
.history-slider-input {
flex: 1 1 auto;
min-width: 0;
width: 100%;
cursor: pointer;
}
/* Saved-revision markers overlaid on the slider track (issue #7946). Inset
* left/right by ~half the native range thumb so a marker lines up with the
* thumb centre at the track extremes. pointer-events:none on the layer keeps
* slider dragging unobstructed; the stars themselves re-enable clicks to seek. */
.history-slider-stars {
position: absolute;
left: 8px;
right: 8px;
top: 0;
bottom: 0;
pointer-events: none;
}
.history-slider-stars .history-star {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
padding: 0;
margin: 0;
border: 0;
background: none;
line-height: 1;
cursor: pointer;
pointer-events: auto;
}
.history-slider-stars .history-star::before {
font-family: fontawesome-etherpad;
content: "\e856";
color: #da9700;
font-size: 14px;
}
.history-timer {
flex: 0 0 auto;
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.85;
white-space: nowrap;
}
.history-toggle {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
white-space: nowrap;
cursor: pointer;
}
.history-toggle input[type="checkbox"] { margin: 0; }
/* Follow toggle — eye icon, with a diagonal slash that appears only when
* the underlying checkbox is unchecked (auto-follow disabled). The hidden
* input still drives state (so pad_mode.ts's bridge code reads .checked
* and the existing label-for relationship handles click). */
.history-follow-toggle {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
cursor: pointer;
border-radius: 4px;
color: inherit;
}
.history-follow-toggle:hover { background: rgba(0,0,0,0.06); }
.history-follow-eye-slash { display: none; }
#history-options-followContents:not(:checked) + .history-follow-toggle .history-follow-eye-slash {
display: inline;
}
#history-options-followContents:not(:checked) + .history-follow-toggle {
opacity: 0.55;
}
/* Keep the checkbox in the DOM (label-for needs a present target) but
* hidden visually + accessibly redundant since the label conveys state. */
#history-options-followContents.sr-only {
position: absolute;
width: 1px; height: 1px;
margin: -1px; padding: 0; border: 0;
clip: rect(0 0 0 0); overflow: hidden;
}
.history-speed {
flex: 0 0 auto;
font-size: 13px;
padding: 2px 4px;
max-width: 130px;
}
/* Responsive — Follow + Speed inherit .hide-for-mobile (already collapses */
/* at <=800px). Pack the remaining play/slider/step buttons tighter so */
/* they always fit. At ultra-narrow widths the step buttons compact too. */
@media (max-width: 800px) {
.history-controls { padding: 0 6px; gap: 4px; min-height: 36px; }
.history-controls button.buttonicon { padding: 4px 6px; min-width: 32px; }
.history-slider-wrap { min-width: 60px; margin: 0 2px; }
}
@media (max-width: 480px) {
.history-controls #history-leftstep,
.history-controls #history-rightstep { min-width: 28px; padding: 2px; }
}
/* Chat replay header — appears above the chat log while scrubbing so the */
/* user knows the message list is filtered to a historical timestamp. */
.history-chat-header {
display: none;
padding: 6px 10px;
font-size: 12px;
font-weight: 600;
background: #fff8e1;
color: #5b4b00;
border-bottom: 1px solid #f0d27a;
}
body.history-mode .history-chat-header { display: block; }
body.history-mode .history-authors-row {
font-style: italic;
opacity: 0.85;
padding: 6px 8px;
}