mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-22 09:36:53 +00:00
* 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>
348 lines
10 KiB
CSS
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;
|
|
}
|