diff --git a/src/static/skins/margin/README.md b/src/static/skins/margin/README.md new file mode 100644 index 00000000000..ad5123fe32f --- /dev/null +++ b/src/static/skins/margin/README.md @@ -0,0 +1,61 @@ +# margin — Etherpad skin + +A standalone drop-in skin with six themes and an orthogonal Light/Dark toggle: + +| Theme | Light | Dark | Natural mode | +| --- | --- | --- | --- | +| `colibris` | ✓ | — | light (no dark palette) | +| `editorial` | ✓ | ✓ | light | +| `brutalist` | ✓ | ✓ | light | +| `paper` | ✓ | ✓ | light | +| `crt` | ✓ | ✓ | dark | +| `industrial` | ✓ | ✓ | dark | + +The current `data-theme` and `data-mode` attributes live on ``. Mode is paired with theme in CSS via `[data-theme="X"][data-mode="light|dark"]`. + +No external dependency on colibris — all component partials are vendored under `src/`. + +## Install + +1. Copy this `margin/` folder into `src/static/skins/`. +2. In `settings.json`, set: + ```json + "skinName": "margin" + ``` + +No template edits are required. The skin applies the user's saved theme + mode on load (defaulting to `colibris` + the theme's natural mode), the Google Fonts stylesheet is `@import`-ed from `pad.css` / `index.css`, and a **Theme** dropdown plus a **Dark mode** checkbox are injected into both the User Settings and Pad-wide Settings columns of the Settings popup. + +## Switch themes at runtime + +The Settings popup (gear icon in the toolbar) has: +- a **Theme** dropdown with the six themes, +- a **Dark mode** checkbox (orthogonal — flips light↔dark for any theme that has a dark palette). + +Choices persist in `localStorage` under `marginTheme` + `marginMode` and propagate across the pad and the lobby. + +Programmatically, from DevTools: + +```js +document.documentElement.dataset.theme = 'crt'; +document.documentElement.dataset.mode = 'dark'; +``` + +## Folder layout + +``` +margin/ +├─ index.css lobby / pad-list themes +├─ index.js lobby JS (early theme bootstrap) +├─ pad.css pad themes + component imports +├─ pad.js pad JS hooks (theme bootstrap, Settings dropdown, +│ iframe theme propagation) +├─ timeslider.css version timeline +├─ timeslider.js timeslider JS +├─ src/ +│ ├─ general.css, layout.css, pad-editor.css, pad-variants.css +│ ├─ components/ toolbar, chat, popups, users, gritter, scrollbars, … +│ └─ plugins/ comments, color picker, tables, … +└─ README.md +``` + +The `src/` partials are vendored from upstream colibris so this skin is fully self-contained — themes layer on top via `data-theme="…"` overrides in `pad.css` and `index.css`, and inherit the same CSS-variable contract (`--primary-color`, `--bg-color`, `--main-font-family`, `--editor-horizontal-padding`, …) that colibris exposes. diff --git a/src/static/skins/margin/index.css b/src/static/skins/margin/index.css new file mode 100644 index 00000000000..c67f4ea18f4 --- /dev/null +++ b/src/static/skins/margin/index.css @@ -0,0 +1,43 @@ +/* margin / index.css — lobby + pad-list page + * Styles the home / pad-list using the same data-theme system as pad.css. + * Drop alongside pad.css in src/static/skins/margin/. */ + +@import url("//fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300..700;1,6..72,300..700&family=Instrument+Serif:ital@0;1&family=Lora:ital,wght@0,400..700;1,400..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&family=VT323&family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700&display=swap"); + +/* Theme definitions are mirrored from pad.css so the lobby themes too. + * Both light and dark variants of each named theme are defined so the + * Settings popup's Theme dropdown can apply consistently across pages. */ +[data-theme="colibris"] { --m-bg:#f2f3f4; --m-fg:#485365; --m-soft:#576273; --m-accent:#64d29b; --m-panel:#fff; --m-rule:#dadada; --m-radius:3px; --m-shadow:0 2px 8px rgba(68,68,68,.08); --m-font:Quicksand, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; --m-ui-style:normal; --m-ui-case:none; } +[data-theme="editorial"][data-mode="light"] { --m-bg:#f5f0e8; --m-fg:#1c1916; --m-soft:#5a534a; --m-accent:#a8442b; --m-panel:#fbf8f2; --m-rule:rgba(28,25,22,.12); --m-radius:2px; --m-shadow:0 12px 36px -18px rgba(28,25,22,.20); --m-font:"Newsreader",Georgia,serif; --m-ui-style:italic; --m-ui-case:none; } +[data-theme="editorial"][data-mode="dark"] { color-scheme:dark; --m-bg:#16130f; --m-fg:#f0eadd; --m-soft:#a8a098; --m-accent:#d27047; --m-panel:#1c1916; --m-rule:rgba(240,234,221,.12); --m-radius:2px; --m-shadow:0 12px 36px -18px rgba(0,0,0,.6); --m-font:"Newsreader",Georgia,serif; --m-ui-style:italic; --m-ui-case:none; } +[data-theme="brutalist"][data-mode="light"] { --m-bg:#f3f3f0; --m-fg:#000; --m-soft:#222; --m-accent:#ff3b00; --m-panel:#fff; --m-rule:#000; --m-radius:0; --m-shadow:4px 4px 0 #000; --m-font:"Space Mono",ui-monospace,monospace; --m-ui-style:normal; --m-ui-case:uppercase; } +[data-theme="brutalist"][data-mode="dark"] { color-scheme:dark; --m-bg:#0c0c09; --m-fg:#fff; --m-soft:#ddd; --m-accent:#ff3b00; --m-panel:#000; --m-rule:#fff; --m-radius:0; --m-shadow:4px 4px 0 #fff; --m-font:"Space Mono",ui-monospace,monospace; --m-ui-style:normal; --m-ui-case:uppercase; } +[data-theme="paper"][data-mode="light"] { --m-bg:#f6f1e8; --m-fg:#2a2520; --m-soft:#6b6259; --m-accent:#b87333; --m-panel:#fbf6ec; --m-rule:rgba(42,37,32,.08); --m-radius:12px; --m-shadow:0 14px 40px -16px rgba(42,37,32,.18); --m-font:"Lora",Georgia,serif; --m-ui-style:normal; --m-ui-case:none; } +[data-theme="paper"][data-mode="dark"] { color-scheme:dark; --m-bg:#231e19; --m-fg:#efe7d4; --m-soft:#a89e8d; --m-accent:#d99560; --m-panel:#2a2520; --m-rule:rgba(239,231,212,.10); --m-radius:12px; --m-shadow:0 14px 40px -16px rgba(0,0,0,.6); --m-font:"Lora",Georgia,serif; --m-ui-style:normal; --m-ui-case:none; } +[data-theme="crt"][data-mode="light"] { --m-bg:#edf6ee; --m-fg:#04200d; --m-soft:#1c4a2b; --m-accent:#006b3f; --m-panel:#e9f5ed; --m-rule:rgba(0,107,63,.20); --m-radius:0; --m-shadow:0 0 0 1px rgba(0,107,63,.25); --m-font:"IBM Plex Mono",ui-monospace,monospace; --m-ui-style:normal; --m-ui-case:uppercase; } +[data-theme="crt"][data-mode="dark"] { color-scheme:dark; --m-bg:#04140a; --m-fg:#7fffae; --m-soft:#4ed188; --m-accent:#ffb84d; --m-panel:#08200f; --m-rule:rgba(127,255,174,.20); --m-radius:0; --m-shadow:0 0 24px rgba(127,255,174,.10); --m-font:"IBM Plex Mono",ui-monospace,monospace; --m-ui-style:normal; --m-ui-case:uppercase; } +[data-theme="industrial"][data-mode="light"] { --m-bg:#f0f1f4; --m-fg:#14171c; --m-soft:#525965; --m-accent:#cc9900; --m-panel:#f5f6f8; --m-rule:rgba(20,23,28,.08); --m-radius:3px; --m-shadow:0 12px 32px rgba(20,23,28,.10); --m-font:"IBM Plex Sans",ui-sans-serif,system-ui,sans-serif; --m-ui-style:normal; --m-ui-case:uppercase; } +[data-theme="industrial"][data-mode="dark"] { color-scheme:dark; --m-bg:#0d0f12; --m-fg:#e6e8eb; --m-soft:#9aa0a8; --m-accent:#ffcc00; --m-panel:#14171c; --m-rule:rgba(255,255,255,.08); --m-radius:3px; --m-shadow:0 12px 32px rgba(0,0,0,.4); --m-font:"IBM Plex Sans",ui-sans-serif,system-ui,sans-serif; --m-ui-style:normal; --m-ui-case:uppercase; } + +[data-theme] body, [data-theme] .body { background: var(--m-bg) !important; color: var(--m-fg); font-family: var(--m-font); } +[data-theme] body nav { border-bottom: 1px solid var(--m-rule); } +[data-theme] .logo-box { background: var(--m-accent); border-radius: var(--m-radius); } +[data-theme] #wrapper, [data-theme] .pad-datalist { + background: var(--m-panel); border: 1px solid var(--m-rule); + border-radius: var(--m-radius); box-shadow: var(--m-shadow); +} +[data-theme] .mission-statement h2 { color: var(--m-fg); text-transform: var(--m-ui-case); font-style: var(--m-ui-style); } +[data-theme] .mission-statement p { color: var(--m-soft); } +[data-theme] #padname { background: var(--m-panel); color: var(--m-fg); border-color: var(--m-rule); border-radius: var(--m-radius); } +[data-theme] #go2Name [type="submit"], [data-theme] #transferSessionButton { + background: var(--m-accent); border-radius: var(--m-radius); + text-transform: var(--m-ui-case); +} +[data-theme] #button { background: var(--m-panel); color: var(--m-fg); border: 1px solid var(--m-rule); border-radius: var(--m-radius); } +[data-theme] .pad-datalist h2 { border-bottom-color: var(--m-rule); color: var(--m-fg); } +[data-theme] .recent-pad:hover a { color: var(--m-accent); } + +[data-theme="brutalist"] #wrapper, [data-theme="brutalist"] .pad-datalist, +[data-theme="brutalist"] #button, [data-theme="brutalist"] #go2Name [type="submit"] { + border: 2px solid #000 !important; box-shadow: 4px 4px 0 #000; +} diff --git a/src/static/skins/margin/index.js b/src/static/skins/margin/index.js new file mode 100644 index 00000000000..6932571adab --- /dev/null +++ b/src/static/skins/margin/index.js @@ -0,0 +1,173 @@ +'use strict'; + +// Apply the user's saved theme + light/dark mode as early as possible so the +// lobby paints in the same theme as the last pad they visited. The controls +// that write these localStorage keys live in the pad's Settings popup +// (see pad.js). +const MARGIN_THEME_KEY = 'marginTheme'; +const MARGIN_MODE_KEY = 'marginMode'; +const MARGIN_THEME_DEFAULT = 'colibris'; +const MARGIN_MODE_DEFAULTS = { + colibris: 'light', editorial: 'light', brutalist: 'light', + paper: 'light', crt: 'dark', industrial: 'dark', +}; +try { + const theme = localStorage.getItem(MARGIN_THEME_KEY) || MARGIN_THEME_DEFAULT; + const mode = localStorage.getItem(MARGIN_MODE_KEY) || MARGIN_MODE_DEFAULTS[theme] || 'light'; + document.documentElement.setAttribute('data-theme', theme); + document.documentElement.setAttribute('data-mode', mode); +} catch (_) { + document.documentElement.setAttribute('data-theme', MARGIN_THEME_DEFAULT); + document.documentElement.setAttribute('data-mode', MARGIN_MODE_DEFAULTS[MARGIN_THEME_DEFAULT]); +} + +window.addEventListener('pageshow', (event) => { + if (event.persisted) { + if (document.readyState === 'complete' || document.readyState === 'interactive') { + window.customStart(); + } else { + window.addEventListener('DOMContentLoaded', window.customStart, {once: true}); + } + } +}); + +window.customStart = () => { + const recentPadList = document.getElementById('recent-pads'); + if (recentPadList) { + recentPadList.replaceChildren(); + } + // define your javascript here + // jquery is available - except index.js + // you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/ + const divHoldingPlaceHolderLabel = document + .querySelector('[data-l10n-id="index.placeholderPadEnter"]'); + + const observer = new MutationObserver(() => { + document.querySelector('#go2Name input') + .setAttribute('placeholder', divHoldingPlaceHolderLabel.textContent); + }); + + observer + .observe(divHoldingPlaceHolderLabel, {childList: true, subtree: true, characterData: true}); + + + const recentPadListHeading = document.querySelector('[data-l10n-id="index.recentPads"]'); + // localStorage may be unavailable (private mode, disabled cookies) and the + // stored value may be malformed if another tab corrupted it. Either case + // would throw out of customStart() and break the rest of the lobby init, + // so swallow both and fall back to an empty list. + let recentPadListData = []; + try { + const recentPadsFromLocalStorage = localStorage.getItem('recentPads'); + if (recentPadsFromLocalStorage != null) { + const parsed = JSON.parse(recentPadsFromLocalStorage); + if (Array.isArray(parsed)) { + recentPadListData = parsed.filter( + (p) => p && typeof p === 'object' && typeof p.name === 'string'); + } + } + } catch (_) { /* private mode / corrupted entry */ } + + // Remove duplicates based on pad name and sort by timestamp + recentPadListData = recentPadListData.filter( + (pad, index, self) => index === self.findIndex((p) => p.name === pad.name) + ).sort((a, b) => new Date(a.timestamp) > new Date(b.timestamp) ? -1 : 1); + + if (recentPadList && recentPadListData.length === 0) { + const parentStyle = recentPadList.parentElement.style; + recentPadListHeading.setAttribute('data-l10n-id', 'index.recentPadsEmpty'); + parentStyle.display = 'flex'; + parentStyle.justifyContent = 'center'; + parentStyle.alignItems = 'center'; + parentStyle.maxHeight = '100%'; + recentPadList.remove(); + } else if (recentPadList) { + /** + * @typedef {Object} Pad + * @property {string} name + */ + + /** + * @param {Pad} pad + */ + + const arrowIcon = ''; + const clockIcon = ''; + const personalIcon = ''; + recentPadListData.forEach((pad) => { + const li = document.createElement('li'); + + + li.style.cursor = 'pointer'; + + li.className = 'recent-pad'; + // Use new URL() so a trailing slash, query string, or hash on + // window.location.href doesn't produce a broken link, and so pad + // names with characters that need encoding still resolve. + const padPath = new URL(`p/${encodeURIComponent(pad.name)}`, + window.location.href).href; + const link = document.createElement('a'); + link.style.textDecoration = 'none'; + + link.href = padPath; + link.innerText = pad.name; + li.appendChild(link); + + + const arrowIconElement = document.createElement('span'); + arrowIconElement.className = 'recent-pad-arrow'; + arrowIconElement.innerHTML = arrowIcon; + li.appendChild(arrowIconElement); + + const nextRow = document.createElement('div'); + + nextRow.style.display = 'flex'; + nextRow.style.gap = '10px'; + nextRow.style.marginTop = '10px'; + + const clockIconElement = document.createElement('span'); + clockIconElement.className = 'recent-pad-clock'; + clockIconElement.innerHTML = clockIcon; + + nextRow.appendChild(clockIconElement); + + const time = new Date(pad.timestamp); + const userLocale = navigator.language || 'en-US'; + + const formattedTime = time.toLocaleDateString(userLocale, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + const timeElement = document.createElement('span'); + timeElement.className = 'recent-pad-time'; + timeElement.innerText = formattedTime; + + nextRow.appendChild(timeElement); + + const personalIconElement = document.createElement('span'); + personalIconElement.className = 'recent-pad-personal'; + personalIconElement.innerHTML = personalIcon; + + personalIconElement.style.marginLeft = '5px'; + + const members = document.createElement('span'); + members.className = 'recent-pad-members'; + members.innerText = pad.members; + + + nextRow.appendChild(personalIconElement); + nextRow.appendChild(members); + li.appendChild(nextRow); + + li.addEventListener('click', () => { + window.location.href = padPath; + }); + + // https://v0.dev/chat/etherpad-design-clone-qZnwOrVRXxH + recentPadList.appendChild(li); + }); + } +}; diff --git a/src/static/skins/margin/pad.css b/src/static/skins/margin/pad.css new file mode 100644 index 00000000000..307d8cd63b3 --- /dev/null +++ b/src/static/skins/margin/pad.css @@ -0,0 +1,416 @@ +/* ════════════════════════════════════════════════════════════════════════ + * margin / pad.css — a standalone Etherpad skin with 5 themes. + * + * Drop the entire `margin/` folder into src/static/skins/, then in + * settings.json set: + * + * "skinName": "margin" + * + * Choose a theme by setting data-theme on in src/templates/pad.html: + * + * + * + * Themes: editorial · brutalist · paper · crt · industrial + * + * Switch live in devtools: document.documentElement.dataset.theme='crt' + * + * This file inherits the same variable contract colibris uses + * (--primary-color, --text-color, --bg-color, --main-font-family, …) so + * Etherpad's built-in components pick up the theme automatically. + * ════════════════════════════════════════════════════════════════════════ */ + +/* Google Fonts powering the five themed type stacks (Newsreader/Editorial, + * Space Mono/Brutalist, Lora/Paper, IBM Plex Mono+VT323/CRT, IBM Plex + * Sans/Industrial, plus Instrument Serif for display H1/H2). */ +@import url("//fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300..700;1,6..72,300..700&family=Instrument+Serif:ital@0;1&family=Lora:ital,wght@0,400..700;1,400..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&family=VT323&family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700&display=swap"); + +/* Component partials — vendored from colibris so this skin is standalone. */ +@import url("src/general.css"); +@import url("src/layout.css"); +@import url("src/pad-editor.css"); +@import url("src/components/scrollbars.css"); +@import url("src/components/buttons.css"); +@import url("src/components/popup.css"); +@import url("src/components/chat.css"); +@import url("src/components/sidediv.css"); +@import url("src/components/gritter.css"); +@import url("src/components/table-of-content.css"); +@import url("src/components/toolbar.css"); +@import url("src/components/users.css"); +@import url("src/components/form.css"); +@import url("src/components/import-export.css"); +@import url("src/plugins/brightcolorpicker.css"); +@import url("src/plugins/font_color.css"); +@import url("src/plugins/tables2.css"); +@import url("src/plugins/set_title_on_pad.css"); +@import url("src/plugins/author_hover.css"); +@import url("src/plugins/comments.css"); + +/* Default theme if data-theme isn't set */ +:root { color-scheme: light; } +html:not([data-theme]) { /* fallback */ } + +/* Mirror colibris's narrow-viewport behavior: the per-theme blocks below + * each set --editor-horizontal-padding (40-64px) so the editor frame has + * room to breathe on desktop. Below 1000px (matching the layout.css media + * query that drops `max-width: 900px` on the iframe), zero them so the + * editor surface fills the available container width. */ +@media (max-width: 1000px) { + [data-theme] { + --editor-horizontal-padding: 0px !important; + --editor-vertical-padding: 0px !important; + } +} + +/* ─── 1. EDITORIAL ───────────────────────────────────────────────────── */ +[data-theme="editorial"][data-mode="light"] { + --super-dark-color:#1c1916; --dark-color:#5a534a; + --primary-color:#a8442b; --middle-color:rgba(28,25,22,.18); + --light-color:#f0eadd; --super-light-color:#fbf8f2; + --text-color:#1c1916; --text-soft-color:#5a534a; + --border-color:rgba(28,25,22,.18); + --bg-soft-color:#f0eadd; --bg-color:#fbf8f2; + --toolbar-border:1px solid rgba(28,25,22,.12); + --main-font-family:"Newsreader",Georgia,serif; + --editor-horizontal-padding:64px; --editor-vertical-padding:40px; + --m-bg:#f5f0e8; --m-rule:rgba(28,25,22,.12); + --m-radius:2px; --m-shadow:0 12px 36px -18px rgba(28,25,22,.20); + --m-ui-style:italic; --m-ui-case:none; --m-ui-track:.04em; +} + +/* ─── 2. BRUTALIST ──────────────────────────────────────────────────── */ +[data-theme="brutalist"][data-mode="light"] { + --super-dark-color:#000; --dark-color:#222; + --primary-color:#ff3b00; --middle-color:#000; + --light-color:#f3f3f0; --super-light-color:#fff; + --text-color:#000; --text-soft-color:#222; + --border-color:#000; + --bg-soft-color:#f3f3f0; --bg-color:#fff; + --toolbar-border:2px solid #000; + --main-font-family:"Space Mono",ui-monospace,monospace; + --editor-horizontal-padding:48px; --editor-vertical-padding:32px; + --m-bg:#f3f3f0; --m-rule:#000; + --m-radius:0; --m-shadow:4px 4px 0 #000; + --m-ui-style:normal; --m-ui-case:uppercase; --m-ui-track:.06em; +} + +/* ─── 3. PAPER ──────────────────────────────────────────────────────── */ +[data-theme="paper"][data-mode="light"] { + --super-dark-color:#2a2520; --dark-color:#6b6259; + --primary-color:#b87333; --middle-color:rgba(42,37,32,.18); + --light-color:#efe7d4; --super-light-color:#fbf6ec; + --text-color:#2a2520; --text-soft-color:#6b6259; + --border-color:rgba(42,37,32,.18); + --bg-soft-color:#efe7d4; --bg-color:#fbf6ec; + --toolbar-border:1px solid rgba(42,37,32,.08); + --main-font-family:"Lora",Georgia,serif; + --editor-horizontal-padding:56px; --editor-vertical-padding:40px; + --m-bg:#f6f1e8; --m-rule:rgba(42,37,32,.08); + --m-radius:10px; --m-shadow:0 14px 40px -16px rgba(42,37,32,.18); + --m-ui-style:normal; --m-ui-case:none; --m-ui-track:0; + --m-display-font:"Instrument Serif",Georgia,serif; +} + +/* ─── 4. CRT TERMINAL ───────────────────────────────────────────────── */ +[data-theme="crt"][data-mode="dark"] { + color-scheme: dark; + --super-dark-color:#7fffae; --dark-color:#4ed188; + --primary-color:#ffb84d; --middle-color:rgba(127,255,174,.45); + --light-color:#0c2a14; --super-light-color:#08200f; + --text-color:#7fffae; --text-soft-color:#4ed188; + --border-color:rgba(127,255,174,.45); + --bg-soft-color:#0c2a14; --bg-color:#08200f; + --toolbar-border:1px solid rgba(127,255,174,.45); + --main-font-family:"IBM Plex Mono",ui-monospace,monospace; + --editor-horizontal-padding:48px; --editor-vertical-padding:32px; + --m-bg:#04140a; --m-rule:rgba(127,255,174,.20); + --m-radius:0; --m-shadow:0 0 0 1px rgba(127,255,174,.25),0 0 24px rgba(127,255,174,.10); + --m-ui-style:normal; --m-ui-case:uppercase; --m-ui-track:.10em; + --m-display-font:"VT323",ui-monospace,monospace; +} + +/* ─── 5. INDUSTRIAL ─────────────────────────────────────────────────── */ +[data-theme="industrial"][data-mode="dark"] { + color-scheme: dark; + --super-dark-color:#e6e8eb; --dark-color:#9aa0a8; + --primary-color:#ffcc00; --middle-color:rgba(255,255,255,.18); + --light-color:#1c2027; --super-light-color:#14171c; + --text-color:#e6e8eb; --text-soft-color:#9aa0a8; + --border-color:rgba(255,255,255,.18); + --bg-soft-color:#1c2027; --bg-color:#14171c; + --toolbar-border:1px solid rgba(255,255,255,.08); + --main-font-family:"IBM Plex Sans",ui-sans-serif,system-ui,sans-serif; + --editor-horizontal-padding:56px; --editor-vertical-padding:36px; + --m-bg:#0d0f12; --m-rule:rgba(255,255,255,.08); + --m-radius:3px; --m-shadow:0 12px 32px rgba(0,0,0,.4); + --m-ui-style:normal; --m-ui-case:uppercase; --m-ui-track:.08em; +} + +/* ─── 0. COLIBRIS (default, mirrors colibris/pad.css) ──────────────── */ +[data-theme="colibris"] { + --super-dark-color:#485365; --dark-color:#576273; + --primary-color:#64d29b; --middle-color:#dadada; + --light-color:#f2f3f4; --super-light-color:#ffffff; + --text-color:#485365; --text-soft-color:#576273; + --border-color:#dadada; + --bg-soft-color:#f2f3f4; --bg-color:#ffffff; + --toolbar-border:none; + --main-font-family:Quicksand, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + --editor-horizontal-padding:40px; --editor-vertical-padding:25px; + --m-bg:#f2f3f4; --m-rule:#dadada; + --m-radius:3px; --m-shadow:0 2px 8px rgba(68,68,68,.08); + --m-ui-style:normal; --m-ui-case:none; --m-ui-track:0; +} + +/* ─── 1b. EDITORIAL — DARK ─────────────────────────────────────────── */ +[data-theme="editorial"][data-mode="dark"] { + color-scheme:dark; + --super-dark-color:#f0eadd; --dark-color:#a8a098; + --primary-color:#d27047; --middle-color:rgba(240,234,221,.18); + --light-color:#221e1a; --super-light-color:#1c1916; + --text-color:#f0eadd; --text-soft-color:#a8a098; + --border-color:rgba(240,234,221,.18); + --bg-soft-color:#221e1a; --bg-color:#1c1916; + --toolbar-border:1px solid rgba(240,234,221,.12); + --main-font-family:"Newsreader",Georgia,serif; + --editor-horizontal-padding:64px; --editor-vertical-padding:40px; + --m-bg:#16130f; --m-rule:rgba(240,234,221,.12); + --m-radius:2px; --m-shadow:0 12px 36px -18px rgba(0,0,0,.6); + --m-ui-style:italic; --m-ui-case:none; --m-ui-track:.04em; +} + +/* ─── 2b. BRUTALIST — DARK ─────────────────────────────────────────── */ +[data-theme="brutalist"][data-mode="dark"] { + color-scheme:dark; + --super-dark-color:#fff; --dark-color:#ddd; + --primary-color:#ff3b00; --middle-color:#fff; + --light-color:#111; --super-light-color:#000; + --text-color:#fff; --text-soft-color:#ddd; + --border-color:#fff; + --bg-soft-color:#111; --bg-color:#000; + --toolbar-border:2px solid #fff; + --main-font-family:"Space Mono",ui-monospace,monospace; + --editor-horizontal-padding:48px; --editor-vertical-padding:32px; + --m-bg:#0c0c09; --m-rule:#fff; + --m-radius:0; --m-shadow:4px 4px 0 #fff; + --m-ui-style:normal; --m-ui-case:uppercase; --m-ui-track:.06em; +} + +/* ─── 3b. PAPER — DARK ─────────────────────────────────────────────── */ +[data-theme="paper"][data-mode="dark"] { + color-scheme:dark; + --super-dark-color:#efe7d4; --dark-color:#a89e8d; + --primary-color:#d99560; --middle-color:rgba(239,231,212,.18); + --light-color:#332c25; --super-light-color:#2a2520; + --text-color:#efe7d4; --text-soft-color:#a89e8d; + --border-color:rgba(239,231,212,.18); + --bg-soft-color:#332c25; --bg-color:#2a2520; + --toolbar-border:1px solid rgba(239,231,212,.10); + --main-font-family:"Lora",Georgia,serif; + --editor-horizontal-padding:56px; --editor-vertical-padding:40px; + --m-bg:#231e19; --m-rule:rgba(239,231,212,.10); + --m-radius:10px; --m-shadow:0 14px 40px -16px rgba(0,0,0,.6); + --m-ui-style:normal; --m-ui-case:none; --m-ui-track:0; + --m-display-font:"Instrument Serif",Georgia,serif; +} + +/* ─── 4b. CRT — LIGHT (paper-terminal, less iconic but readable) ──── */ +[data-theme="crt"][data-mode="light"] { + --super-dark-color:#04200d; --dark-color:#1c4a2b; + --primary-color:#006b3f; --middle-color:rgba(0,107,63,.30); + --light-color:#dfeee2; --super-light-color:#e9f5ed; + --text-color:#04200d; --text-soft-color:#1c4a2b; + --border-color:rgba(0,107,63,.30); + --bg-soft-color:#dfeee2; --bg-color:#e9f5ed; + --toolbar-border:1px solid rgba(0,107,63,.30); + --main-font-family:"IBM Plex Mono",ui-monospace,monospace; + --editor-horizontal-padding:48px; --editor-vertical-padding:32px; + --m-bg:#edf6ee; --m-rule:rgba(0,107,63,.20); + --m-radius:0; --m-shadow:0 0 0 1px rgba(0,107,63,.25); + --m-ui-style:normal; --m-ui-case:uppercase; --m-ui-track:.10em; + --m-display-font:"VT323",ui-monospace,monospace; +} + +/* ─── 5b. INDUSTRIAL — LIGHT ──────────────────────────────────────── */ +[data-theme="industrial"][data-mode="light"] { + --super-dark-color:#14171c; --dark-color:#525965; + --primary-color:#cc9900; --middle-color:rgba(20,23,28,.18); + --light-color:#e6e8eb; --super-light-color:#f5f6f8; + --text-color:#14171c; --text-soft-color:#525965; + --border-color:rgba(20,23,28,.18); + --bg-soft-color:#e6e8eb; --bg-color:#f5f6f8; + --toolbar-border:1px solid rgba(20,23,28,.08); + --main-font-family:"IBM Plex Sans",ui-sans-serif,system-ui,sans-serif; + --editor-horizontal-padding:56px; --editor-vertical-padding:36px; + --m-bg:#f0f1f4; --m-rule:rgba(20,23,28,.08); + --m-radius:3px; --m-shadow:0 12px 32px rgba(20,23,28,.10); + --m-ui-style:normal; --m-ui-case:uppercase; --m-ui-track:.08em; +} + +/* ════════════════════════════════════════════════════════════════════ + * Cross-theme overrides (apply to anything under [data-theme]) + * ════════════════════════════════════════════════════════════════════ */ +[data-theme] body { background: var(--m-bg); color: var(--text-color); } +[data-theme] #editorcontainerbox, +[data-theme] #editorcontainer, +[data-theme] #padeditor, +[data-theme] #outerdocbody, +[data-theme] iframe[name="ace_outer"], +[data-theme] iframe[name="ace_inner"] { background: var(--m-bg) !important; } + +/* Editor frame outline — gives the pad surface a visible edge against + * the page background when both share dark/light tones. */ +[data-theme] iframe[name="ace_outer"] { + border: 1px solid var(--border-color) !important; + border-radius: 4px; + box-shadow: 0 1px 0 rgba(0,0,0,.04); +} +[data-theme="brutalist"] iframe[name="ace_outer"] { border-radius: 0; border-width: 2px !important; box-shadow: var(--m-shadow); border-color: var(--m-rule) !important; } +[data-theme="crt"][data-mode="dark"] iframe[name="ace_outer"] { box-shadow: 0 0 0 1px var(--border-color), 0 0 18px rgba(127,255,174,.18) inset; } +[data-theme="crt"][data-mode="light"] iframe[name="ace_outer"] { box-shadow: 0 0 0 1px var(--border-color); } + +/* Show-users / chat / share buttons in the menu_right have hard-coded + * background colors in upstream — force them to follow the theme. + * No left-borders: keep the right-side icons visually flush like the left. */ +[data-theme] .toolbar .menu_right li a, +[data-theme] #chaticon, +[data-theme] #chaticon a { + background: transparent !important; + color: var(--text-color) !important; + border-left: 0 !important; +} +[data-theme] #chaticon .chatlabel { color: var(--text-color) !important; } +[data-theme] .toolbar ul li[data-key=showusers] > a { + background: var(--primary-color) !important; + color: var(--bg-color) !important; +} + +/* Normalize and dispatches change via $().trigger('change'), + // which only fires jQuery-bound handlers — not native addEventListener. Bind + // through jQuery so the widget actually drives applyMarginTheme. + $(select).on('change', () => { + applyMarginTheme(select.value); + // Mirror to the sibling select (user-settings ↔ pad-settings). + $('.margin-theme-row select').each(function () { + if (this !== select) this.value = select.value; + }); + if ($.fn.niceSelect) $('.margin-theme-row select').niceSelect('update'); + }); + row.appendChild(label); + row.appendChild(select); + return row; +}; + +// Build the Dark mode toggle row. Same checkbox-on-label pattern as the +// existing "Disable Chat" / "Show line numbers" rows in pad.html so margin's +// form.css picks up identical chrome. +const buildDarkModeRow = (checkboxId) => { + const row = document.createElement('p'); + row.className = 'margin-mode-row'; + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.id = checkboxId; + checkbox.checked = document.documentElement.getAttribute('data-mode') === 'dark'; + const label = document.createElement('label'); + label.htmlFor = checkboxId; + label.textContent = 'Dark mode'; + $(checkbox).on('change', () => { + applyMarginMode(checkbox.checked ? 'dark' : 'light'); + $('.margin-mode-row input[type="checkbox"]').each(function () { + if (this !== checkbox) this.checked = checkbox.checked; + }); + }); + row.appendChild(checkbox); + row.appendChild(label); + return row; +}; + +const injectThemeSelector = () => { + // Two .dropdowns-container blocks exist: user settings + pad-wide settings. + // Mirror the Theme dropdown into both and the Dark mode toggle into the + // section the column lives in (so the row sits next to other checkboxes). + const sections = [ + { + sectionId: 'user-settings-section', + dropdownId: 'margin-theme-user', + checkboxId: 'margin-mode-user', + }, + { + sectionId: 'pad-settings-section', + dropdownId: 'margin-theme-pad', + checkboxId: 'margin-mode-pad', + }, + ]; + let injectedAny = false; + let allDone = true; + let injectedThisCall = false; + sections.forEach(({sectionId, dropdownId, checkboxId}) => { + const section = document.getElementById(sectionId); + if (!section) { allDone = false; return; } + const dropdownsContainer = section.querySelector('.dropdowns-container'); + if (!dropdownsContainer) { allDone = false; return; } + if (!document.getElementById(dropdownId)) { + dropdownsContainer.appendChild(buildThemeRow(dropdownId)); + injectedThisCall = true; + } + if (!document.getElementById(checkboxId)) { + // Place the Dark mode checkbox among the section's other checkboxes, + // before the dropdowns container so it reads in toggle-then-pickers + // order — matching the natural top-to-bottom flow of the section. + section.insertBefore(buildDarkModeRow(checkboxId), dropdownsContainer); + } + injectedAny = true; + }); + // Etherpad runs $('select').niceSelect() once at pad-init (pad_editbar.ts), + // so a freshly appended