mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-18 00:57:55 +00:00
* docs(updater): add four-tier auto-update design spec
Four-tier opt-in self-update subsystem (off / notify / manual / auto / autonomous).
GitHub Releases as source of truth; install-method auto-detection with admin
override; in-process execution with supervisor restart; 60s drain + announce;
auto-rollback on health-check failure with crash-loop guard. Pad-side severe/
vulnerable badge that does not leak the running version. Top-level adminEmail
with escalating cadence (weekly while vulnerable, monthly while severe).
Refs: docs/superpowers/specs/2026-04-25-auto-update-design.md
* docs(updater): add PR 1 (Tier 1 notify) implementation plan
Bite-sized TDD task breakdown for shipping Tier 1 notify only:
- VersionChecker, InstallMethodDetector, UpdatePolicy, Notifier, state modules
- /admin/update/status (admin-auth) and /api/version-status (public, no version leak)
- Admin UI banner + read-only update page + nav link
- Pad-side severe/vulnerable footer badge
- Settings: updates.* block + top-level adminEmail
- Tests: vitest unit + mocha integration + Playwright admin/pad
- CHANGELOG + doc/admin/updates.md
PRs 2-4 (manual/auto/autonomous) get their own plans after PR 1 lands.
* feat(updater): add shared types for auto-update subsystem
* feat(updater): clarify OutdatedLevel and EMPTY_STATE doc, drop path header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): add semver helpers and vulnerable-below parser
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(updater): tighten semver regex to reject four-part versions
* feat(updater): add state persistence with schema validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(updater): reject null email and array latest in state validation
typeof null === 'object' meant {email:null} passed the old isValid check,
which would crash downstream Notifier code reading email.severeAt. Likewise,
an array would pass the typeof latest === 'object' branch. Introduce
isPlainObject helper (null-safe, Array.isArray guard) and use it for both
fields. Adds two regression tests covering the exact broken inputs.
* feat(updater): add install-method detector with override
* feat(updater): add policy evaluator
* feat(updater): add GitHub Releases checker with ETag support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(updater): validate release fields and preserve ETag on prerelease
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): add email cadence decider
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(updater): tagChanged email fires regardless of cadence; drop unused field
* feat(settings): add updates.* and adminEmail settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): wire boot hook and periodic checker
Register expressCreateServer/shutdown hooks in ep.json and implement
the boot-wiring module that detects install method, starts the polling
interval and runs the notifier dedupe pass each tick.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): add /admin/update/status and /api/version-status endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* i18n(updater): add english strings for update banner, page, and pad badge
* feat(updater): add pad footer badge for severe/vulnerable status
* feat(admin-ui): add update banner, page, and nav link
Add UpdateStatusPayload to the zustand store, a persistent UpdateBanner
rendered in the App layout, a /update page showing version details and
changelog, and a Bell nav link — all wired to the /admin/update/status
endpoint added in Task 10.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(updater): add Playwright specs for admin banner/page and pad badge
* docs(updater): document tier 1 settings, badge, email cadence
* refactor(updater): dedupe helpers, fix misleading log, add banner styling
- Export stateFilePath from index.ts and import it in updateStatus.ts (removes local duplicate)
- Import getEpVersion from Settings.ts in both index.ts and updateStatus.ts (removes two local definitions)
- Fix misleading 'backing off' log message — no backoff is implemented, just retries at next interval
- Remove EMPTY_STATE_FOR_TESTS re-export from state.ts; state.test.ts now imports EMPTY_STATE directly from types.ts
- Add .update-banner and .update-page CSS rules to admin/src/index.css
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(updater): address review feedback — async wrap, tier=off skip, poll race, opt-in admin gate
- Wrap /api/version-status and /admin/update/status with a small async helper
so a rejected promise becomes next(err) instead of an unhandled rejection.
- Short-circuit route registration when updates.tier === 'off' so the heavier
opt-out also removes the HTTP surface (matches pre-PR behavior for that case).
- Add an in-flight guard around performCheck() so overlapping interval ticks
can't race on update-state.json writes or duplicate email decisions; track
the initial setTimeout handle and clear it in shutdown().
- Add updates.requireAdminForStatus (default false) so admins can lock
/admin/update/status to authenticated admin sessions without disabling the
updater. Default false preserves current behavior (the running version is
already exposed publicly via /health). Backend specs cover unauth → 401,
non-admin → 403, admin → 200.
- Bump admin troubleshooting menu count test 5 → 6 to account for the new
Update nav link.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(updater): address Qodo round-2 review feedback
Round 2 of Qodo review on #7601. Addressing the action-required items:
#1 Badge bypassed pad baseURL — derive basePath the same way
padBootstrap.js does (`new URL('..', window.location.href).pathname`)
and prefix the fetch with it. Subpath deployments now reach
/<prefix>/api/version-status instead of 404ing.
#2 Updater poller could get stuck — `getCurrentState()` is now inside
the try/finally so a one-time loadState() rejection can't leave
`checkInFlight=true` and permanently silence polling.
#3 Updates off hung admin page — UpdatePage now self-fetches and
renders explicit `disabled` (404), `unauthorized` (401/403), and
`error` states instead of staying on "Loading...". Banner-driven
prefetch is still honoured if it landed first.
#11 NaN polling interval — coerce `checkIntervalHours` to a number,
clamp to [1h, 168h], log a warning and fall back to 6h on
non-finite input. Math.max(1, NaN) === NaN previously meant a
malformed settings.json could turn the poller into a tight loop.
#13 State validation accepted broken subfields — `isValid()` now
inspects `latest.{version,tag,body,publishedAt,htmlUrl,prerelease}`,
`vulnerableBelow[].{announcedBy,threshold}`, and
`email.{severeAt,vulnerableAt,vulnerableNewReleaseTag}`. A
hand-edited file with a number where a string is expected is now
treated as corrupt and reset to EMPTY_STATE rather than crashing
later in semver parsing or email rendering.
#14 Badge cache stampede — wrap `computeOutdated()` in a single-flight
promise so concurrent requests at cache expiry await one shared
computation instead of fanning out into N redundant disk reads.
Plus six new state.test.ts cases covering each new validation guard.
Pushing back on the remaining items:
#4 `updates.tier` defaults to `notify` — intentional. The whole point
of tier 1 is to surface the "you are behind" signal to admins by
default. Opt-in defeats the purpose; the existing failure mode
(admin never hears about a security-relevant release) is exactly
what this PR is fixing.
#5/#8 Admin status endpoint admin-auth — `currentVersion` is already
public via `/health`, so wrapping the route in admin-auth doesn't
reduce the disclosure surface meaningfully. Operators who want it
gated set `updates.requireAdminForStatus=true` (already wired and
covered by the comment on the route handler).
#10 Plain `https://` URLs in planning doc — planning markdown is
viewed in editors and on GitHub where protocol-relative URLs would
either render literally or break entirely. Keeping `https://`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
924 lines
15 KiB
CSS
924 lines
15 KiB
CSS
:root {
|
|
--etherpad-color: #0f775b;
|
|
--etherpad-comp: #9C8840;
|
|
--etherpad-light: #99FF99;
|
|
--sidebar-width: 20em;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: Karla;
|
|
src: url(/Karla-Regular.ttf);
|
|
}
|
|
|
|
html, body, #root {
|
|
box-sizing: border-box;
|
|
height: 100%;
|
|
font-family: "Karla", sans-serif;
|
|
}
|
|
|
|
*, *:before, *:after {
|
|
box-sizing: inherit;
|
|
font-size: 16px;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
color: #333;
|
|
font: 14px helvetica, sans-serif;
|
|
background: #eee;
|
|
}
|
|
|
|
div.menu {
|
|
left: 0;
|
|
transition: left .3s;
|
|
height: 100vh;
|
|
font-size: 16px;
|
|
font-weight: bolder;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: var(--sidebar-width);
|
|
z-index: 99;
|
|
position: fixed;
|
|
}
|
|
|
|
[role="dialog"] h2 {
|
|
color: var(--etherpad-color);
|
|
}
|
|
|
|
.icon-button {
|
|
display: flex;
|
|
gap: 10px;
|
|
background-color: var(--etherpad-color);
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.icon-button:hover {
|
|
background-color: #13a37c;
|
|
}
|
|
|
|
|
|
.dialog-close-button {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
color: var(--etherpad-color);
|
|
}
|
|
|
|
.icon-button:active {
|
|
background-color: #13a37c;
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.icon-button svg {
|
|
align-self: center;
|
|
}
|
|
|
|
.icon-button span {
|
|
align-self: center;
|
|
}
|
|
|
|
|
|
div.menu span:first-child {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
div.menu span:first-child svg {
|
|
margin-right: 10px;
|
|
align-self: center;
|
|
}
|
|
|
|
|
|
div.menu h1 {
|
|
font-size: 50px;
|
|
text-align: center;
|
|
}
|
|
|
|
.inner-menu {
|
|
border-radius: 0 20px 20px 0;
|
|
padding: 10px;
|
|
flex-grow: 100;
|
|
background-color: var(--etherpad-comp);
|
|
color: white;
|
|
height: 100vh;
|
|
}
|
|
|
|
div.menu ul {
|
|
color: white;
|
|
padding: 0;
|
|
}
|
|
|
|
div.menu li a {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
div.menu svg {
|
|
align-self: center;
|
|
}
|
|
|
|
div.menu li {
|
|
padding: 10px;
|
|
color: white;
|
|
list-style: none;
|
|
margin-left: 3px;
|
|
line-height: 3;
|
|
}
|
|
|
|
|
|
div.menu li:has(.active) {
|
|
background-color: #9C885C;
|
|
}
|
|
|
|
div.menu li a {
|
|
color: lightgray;
|
|
}
|
|
|
|
|
|
div.innerwrapper {
|
|
transition: margin-left .3s;
|
|
isolation: isolate;
|
|
background-color: #F0F0F0;
|
|
overflow: auto;
|
|
height: 100vh;
|
|
flex-grow: 100;
|
|
margin-left: var(--sidebar-width);
|
|
padding: 20px 20px 20px;
|
|
}
|
|
|
|
div.innerwrapper-err {
|
|
display: none;
|
|
}
|
|
|
|
#wrapper {
|
|
background: none repeat scroll 0px 0px #FFFFFF;
|
|
box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.2);
|
|
min-height: 100%; /*always display a scrollbar*/
|
|
}
|
|
|
|
h1 {
|
|
font-size: 29px;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.separator {
|
|
margin: 10px 0;
|
|
height: 1px;
|
|
background: #aaa;
|
|
background: -webkit-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
|
background: -moz-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
|
background: -ms-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
|
background: -o-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
|
}
|
|
|
|
form {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
#inner {
|
|
width: 300px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
input {
|
|
font-weight: bold;
|
|
font-size: 15px;
|
|
}
|
|
|
|
|
|
.sort {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.sort:after {
|
|
content: '▲▼'
|
|
}
|
|
|
|
.sort.up:after {
|
|
content: '▲'
|
|
}
|
|
|
|
.sort.down:after {
|
|
content: '▼'
|
|
}
|
|
|
|
|
|
#installed-plugins thead tr th:nth-child(3) {
|
|
width: 15%;
|
|
}
|
|
|
|
table {
|
|
border: 1px solid #ddd;
|
|
border-radius: 3px;
|
|
border-spacing: 0;
|
|
width: 100%;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.table-container {
|
|
width: 100%;
|
|
overflow: auto;
|
|
max-height: 90vh;
|
|
}
|
|
|
|
|
|
#available-plugins th:first-child, #available-plugins th:nth-child(2) {
|
|
text-align: center;
|
|
}
|
|
|
|
td, th {
|
|
padding: 5px;
|
|
}
|
|
|
|
.template {
|
|
display: none;
|
|
}
|
|
|
|
#installed-plugins td > div {
|
|
position: relative; /* Allows us to position the loading indicator relative to this row */
|
|
display: inline-block; /*make this fill the whole cell*/
|
|
width: 100%;
|
|
}
|
|
|
|
.messages {
|
|
height: 5em;
|
|
}
|
|
|
|
.messages * {
|
|
display: none;
|
|
text-align: center;
|
|
}
|
|
|
|
.messages .fetching {
|
|
display: block;
|
|
}
|
|
|
|
.progress {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
right: 0;
|
|
padding: auto;
|
|
|
|
background: rgb(255, 255, 255);
|
|
display: none;
|
|
}
|
|
|
|
#search-progress.progress {
|
|
padding-top: 20%;
|
|
background: rgba(255, 255, 255, 0.3);
|
|
}
|
|
|
|
.progress * {
|
|
display: block;
|
|
margin: 0 auto;
|
|
text-align: center;
|
|
color: #666;
|
|
}
|
|
|
|
|
|
.settings-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
height: 100%;
|
|
}
|
|
|
|
.settings {
|
|
flex-grow: max(1, 1);
|
|
outline: none;
|
|
width: 100%;
|
|
resize: none;
|
|
font-family: monospace;
|
|
}
|
|
|
|
#response {
|
|
display: inline;
|
|
}
|
|
|
|
a:link, a:visited, a:hover, a:focus {
|
|
color: #333333;
|
|
text-decoration: none;
|
|
}
|
|
|
|
a:focus, a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.installed-results a:link,
|
|
.search-results a:link,
|
|
.installed-results a:visited,
|
|
.search-results a:visited,
|
|
.installed-results a:hover,
|
|
.search-results a:hover,
|
|
.installed-results a:focus,
|
|
.search-results a:focus {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.installed-results a:focus,
|
|
.search-results a:focus,
|
|
.installed-results a:hover,
|
|
.search-results a:hover {
|
|
text-decoration: none;
|
|
}
|
|
|
|
pre {
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
|
|
#icon-button {
|
|
color: var(--etherpad-color);
|
|
top: 10px;
|
|
background-color: transparent;
|
|
border: none;
|
|
z-index: 99;
|
|
position: absolute;
|
|
left: 10px;
|
|
}
|
|
|
|
|
|
.inner-menu span:nth-child(2) {
|
|
display: flex;
|
|
margin-top: 30px;
|
|
}
|
|
|
|
#wrapper.closed .menu {
|
|
left: calc(-1 * var(--sidebar-width));
|
|
}
|
|
|
|
#wrapper.closed .innerwrapper {
|
|
margin-left: 0;
|
|
}
|
|
|
|
@media (max-width: 800px) {
|
|
|
|
div.innerwrapper {
|
|
margin-left: 0;
|
|
}
|
|
|
|
.inner-menu {
|
|
border-radius: 0;
|
|
}
|
|
|
|
div.menu {
|
|
height: auto;
|
|
border-right: none;
|
|
--sidebar-width: 100%;
|
|
float: left;
|
|
}
|
|
|
|
table {
|
|
border: none;
|
|
}
|
|
|
|
table, thead, tbody, td, tr {
|
|
display: block;
|
|
}
|
|
|
|
thead tr {
|
|
display: none;
|
|
}
|
|
|
|
tr {
|
|
border: 1px solid #ccc;
|
|
margin-bottom: 5px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
td {
|
|
border: none;
|
|
border-bottom: 1px solid #eee;
|
|
position: relative;
|
|
padding-left: 50%;
|
|
white-space: normal;
|
|
text-align: left;
|
|
}
|
|
|
|
td.name {
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
td:before {
|
|
position: absolute;
|
|
top: 6px;
|
|
left: 6px;
|
|
text-align: left;
|
|
padding-right: 10px;
|
|
white-space: nowrap;
|
|
font-weight: bold;
|
|
content: attr(data-label);
|
|
}
|
|
|
|
td:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
table input[type="button"] {
|
|
float: none;
|
|
}
|
|
}
|
|
|
|
|
|
.settings-button-bar {
|
|
margin-top: 10px;
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.login-background {
|
|
background-image: url("/fond.jpg");
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
background-size: cover;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100vh;
|
|
background-color: #f0f0f0;
|
|
}
|
|
|
|
.login-inner-box div {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.login-inner-box [type=submit] {
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
|
|
.login-textinput {
|
|
width: 100%;
|
|
padding: 10px;
|
|
background-color: #fffacc;
|
|
border-radius: 5px;
|
|
border: 1px solid #ccc;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.login-box {
|
|
padding: 20px;
|
|
border-radius: 40px;
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
background-color: #fff;
|
|
}
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
.login-box {
|
|
width: 90%
|
|
}
|
|
}
|
|
|
|
.login-inner-box {
|
|
position: relative;
|
|
padding: 20px;
|
|
}
|
|
|
|
.login-title {
|
|
padding: 0;
|
|
margin: 0;
|
|
text-align: center;
|
|
color: var(--etherpad-color);
|
|
font-size: 4rem;
|
|
font-weight: 1000;
|
|
}
|
|
|
|
.login-button {
|
|
padding: 10px;
|
|
background-color: var(--etherpad-color);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
width: 100%;
|
|
height: 40px;
|
|
}
|
|
|
|
.dialog-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background-color: white;
|
|
z-index: 100;
|
|
}
|
|
|
|
|
|
.dialog-confirm-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
z-index: 100;
|
|
}
|
|
|
|
|
|
.dialog-confirm-content {
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
background-color: white;
|
|
transform: translate(-50%, -50%);
|
|
padding: 20px;
|
|
z-index: 101;
|
|
}
|
|
|
|
|
|
.dialog-content {
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
padding: 20px;
|
|
z-index: 101;
|
|
}
|
|
|
|
.dialog-title {
|
|
color: var(--etherpad-color);
|
|
font-size: 2em;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
|
|
.ToastViewport {
|
|
position: fixed;
|
|
top: 10px;
|
|
right: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
width: 390px;
|
|
max-width: 100vw;
|
|
margin: 0;
|
|
list-style: none;
|
|
z-index: 2147483647;
|
|
outline: none;
|
|
}
|
|
|
|
.ToastRootSuccess {
|
|
background-color: lawngreen;
|
|
}
|
|
|
|
.ToastRootFailure {
|
|
background-color: red;
|
|
}
|
|
|
|
.ToastRootFailure > .ToastTitle {
|
|
color: white;
|
|
}
|
|
|
|
.ToastRoot {
|
|
border-radius: 20px;
|
|
box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
|
|
padding: 15px;
|
|
display: grid;
|
|
grid-template-areas: 'title action' 'description action';
|
|
grid-template-columns: auto max-content;
|
|
column-gap: 15px;
|
|
align-items: center;
|
|
}
|
|
|
|
.ToastRoot[data-state='open'] {
|
|
animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
.ToastRoot[data-state='closed'] {
|
|
animation: hide 100ms ease-in;
|
|
}
|
|
|
|
.ToastRoot[data-swipe='move'] {
|
|
transform: translateX(var(--radix-toast-swipe-move-x));
|
|
}
|
|
|
|
.ToastRoot[data-swipe='cancel'] {
|
|
transform: translateX(0);
|
|
transition: transform 200ms ease-out;
|
|
}
|
|
|
|
.ToastRoot[data-swipe='end'] {
|
|
animation: swipeOut 100ms ease-out;
|
|
}
|
|
|
|
@keyframes hide {
|
|
from {
|
|
opacity: 1;
|
|
}
|
|
to {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
transform: translateX(calc(100% + var(--viewport-padding)));
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
@keyframes swipeOut {
|
|
from {
|
|
transform: translateX(var(--radix-toast-swipe-end-x));
|
|
}
|
|
to {
|
|
transform: translateX(calc(100% + var(--viewport-padding)));
|
|
}
|
|
}
|
|
|
|
.ToastTitle {
|
|
grid-area: title;
|
|
margin-bottom: 5px;
|
|
font-weight: 500;
|
|
color: var(--slate-12);
|
|
padding: 10px;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.ToastDescription {
|
|
grid-area: description;
|
|
margin: 0;
|
|
color: var(--slate-11);
|
|
font-size: 13px;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.ToastAction {
|
|
grid-area: action;
|
|
}
|
|
|
|
.help-block {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 20px
|
|
}
|
|
|
|
.search-field {
|
|
position: relative;
|
|
}
|
|
|
|
.search-field input {
|
|
border-color: transparent;
|
|
border-radius: 20px;
|
|
height: 2.5rem;
|
|
width: 100%;
|
|
padding: 5px 5px 5px 30px;
|
|
}
|
|
|
|
.search-field input:focus {
|
|
outline: none;
|
|
}
|
|
|
|
|
|
.send-message {
|
|
position: relative;
|
|
}
|
|
|
|
.send-message input {
|
|
width: auto;
|
|
}
|
|
|
|
.send-message {
|
|
}
|
|
|
|
.send-message svg {
|
|
position: absolute;
|
|
right: 3px;
|
|
bottom: -3px;
|
|
left: auto !important;
|
|
}
|
|
|
|
.search-field svg {
|
|
position: absolute;
|
|
left: 3px;
|
|
bottom: -3px;
|
|
}
|
|
|
|
|
|
.search-field svg {
|
|
color: gray
|
|
}
|
|
|
|
table {
|
|
margin: 25px 0;
|
|
font-size: 0.9em;
|
|
font-family: sans-serif;
|
|
min-width: 400px;
|
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
th:first-child {
|
|
border-top-left-radius: 10px;
|
|
}
|
|
|
|
th:last-child {
|
|
border-top-right-radius: 10px;
|
|
}
|
|
|
|
table thead tr {
|
|
font-size: 25px;
|
|
background-color: var(--etherpad-color);
|
|
color: #ffffff;
|
|
text-align: left;
|
|
}
|
|
|
|
table tbody tr {
|
|
border-bottom: 1px solid #dddddd;
|
|
}
|
|
|
|
table tr:nth-child(even) td {
|
|
background-color: lightgray;
|
|
}
|
|
|
|
table tr td {
|
|
padding: 12px 15px;
|
|
}
|
|
|
|
table tbody tr:nth-of-type(even) {
|
|
background-color: #f3f3f3;
|
|
}
|
|
|
|
table tbody tr:last-of-type {
|
|
border-bottom: 2px solid #009879;
|
|
}
|
|
|
|
table tbody tr.active-row {
|
|
font-weight: bold;
|
|
color: #009879;
|
|
}
|
|
|
|
|
|
.pad-pagination {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.pad-pagination button {
|
|
display: flex;
|
|
padding: 10px 20px;
|
|
border-radius: 5px;
|
|
border: none;
|
|
color: black;
|
|
cursor: pointer;
|
|
}
|
|
|
|
|
|
.pad-pagination button:disabled {
|
|
background: transparent;
|
|
color: lightgrey;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.pad-pagination span {
|
|
align-self: center;
|
|
}
|
|
|
|
.pad-pagination > span {
|
|
font-size: 20px;
|
|
}
|
|
|
|
|
|
.login-page .login-form .input-control input[type=text], .login-page .login-form .input-control input[type=email], .login-page .login-form .input-control input[type=password], .login-page .signup-form .input-control input[type=text], .login-page .signup-form .input-control input[type=email], .login-page .signup-form .input-control input[type=password], .login-page .forgot-form .input-control input[type=text], .login-page .forgot-form .input-control input[type=email], .login-page .forgot-form .input-control input[type=password] {
|
|
width: 100%;
|
|
padding: 12px 20px;
|
|
margin: 8px 0;
|
|
display: inline-block;
|
|
border-bottom: 2px solid #ccc;
|
|
border-top: 0;
|
|
border-left: 0;
|
|
border-right: 0;
|
|
-webkit-box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
border-radius: 5px;
|
|
font-size: 14px;
|
|
color: #666;
|
|
background-color: #f8f8f8;
|
|
-webkit-transition: all 0.3s ease-in-out;
|
|
transition: all 0.3s ease-in-out;
|
|
}
|
|
|
|
input, button, select, optgroup, textarea {
|
|
margin: 0;
|
|
font-family: inherit;
|
|
font-size: inherit;
|
|
line-height: inherit;
|
|
}
|
|
|
|
.icon-input {
|
|
position: relative;
|
|
}
|
|
|
|
.icon-input svg {
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
right: 10px;
|
|
color: #666;
|
|
}
|
|
|
|
|
|
.SwitchRoot {
|
|
align-self: center;
|
|
width: 60px;
|
|
height: 30px;
|
|
background-color: black;
|
|
border-radius: 9999px;
|
|
position: relative;
|
|
box-shadow: 0 2px 10px var(--black-a7);
|
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
|
}
|
|
|
|
.SwitchRoot:focus {
|
|
box-shadow: 0 0 0 2px black;
|
|
}
|
|
|
|
.SwitchRoot[data-state='checked'] {
|
|
background-color: var(--etherpad-color);
|
|
}
|
|
|
|
.SwitchThumb {
|
|
display: block;
|
|
width: 20px;
|
|
height: 20px;
|
|
background-color: white;
|
|
border-radius: 9999px;
|
|
box-shadow: 0 2px 2px var(--black-a7);
|
|
transition: transform 100ms;
|
|
transform: translateX(2px);
|
|
will-change: transform;
|
|
}
|
|
|
|
.SwitchThumb[data-state='checked'] {
|
|
transform: translateX(25px);
|
|
}
|
|
|
|
.Label {
|
|
color: white;
|
|
font-size: 15px;
|
|
line-height: 1;
|
|
}
|
|
|
|
.message {
|
|
position: relative;
|
|
padding: 10px;
|
|
border: 1px solid #e0e0e0;
|
|
margin: 10px 20px 10px 10px;
|
|
border-radius: 10px 0 10px 10px;
|
|
background-color: var(--etherpad-color);
|
|
color: white
|
|
}
|
|
|
|
.search-pads {
|
|
text-align: center;
|
|
}
|
|
|
|
.search-pads-body tr td:last-child {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.manage-pads-header {
|
|
display: flex;
|
|
}
|
|
|
|
/* Update banner — shown on every admin page when a new version is available. */
|
|
.update-banner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 14px;
|
|
margin: 0 0 12px 0;
|
|
background: #fff3cd;
|
|
color: #664d03;
|
|
border: 1px solid #ffe69c;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
}
|
|
.update-banner a {
|
|
color: inherit;
|
|
text-decoration: underline;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Update page layout. */
|
|
.update-page { padding: 16px 0; }
|
|
.update-page h1 { margin-bottom: 16px; }
|
|
.update-page dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0 0 24px; }
|
|
.update-page dt { font-weight: 600; color: #555; }
|
|
.update-page dd { margin: 0; }
|
|
.update-page pre { background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 4px; padding: 12px; font-size: 13px; max-height: 400px; overflow: auto; }
|