-
-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(skin): add margin skin (11 themes; light/dark per theme; switchable from Settings)#7721
feat(skin): add margin skin (11 themes; light/dark per theme; switchable from Settings) #7721JohnMcLear wants to merge 5 commits into
Conversation
...ettings) A standalone drop-in skin alongside colibris / no-skin, with eleven themes — one neutral default (colibris) and five named themes each in light and dark mode: editorial, brutalist, paper, crt, industrial. Theme is chosen via a Theme dropdown injected into the Settings popup (both User Settings and Pad-wide Settings columns) and persists in localStorage under `marginTheme`. The skin applies the saved theme on load — in the pad via pad.js, in the lobby via index.js — so no edits to src/templates/* are required. The skin uses the same CSS-variable contract colibris exposes (`--primary-color`, `--bg-color`, `--main-font-family`, `--editor-horizontal-padding`, ...) and reuses the colibris component partials, vendored into `margin/src/` so the skin is fully self-contained. Google Fonts powering the type stacks (Newsreader, Space Mono, Lora, IBM Plex Mono/Sans, VT323, Instrument Serif) are loaded via `@import` from `pad.css` / `index.css` so the skin does not require core template changes.
i You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.
Review Summary by Qodo
Add margin skin with 11 switchable themes and Settings integration
✨ Enhancement
Walkthroughs
Description
• Adds new "margin" skin with 11 themes (colibris default + 5 named themes in light/dark) • Injects Theme dropdown into Settings popup (User Settings + Pad-wide Settings) • Theme selection persists in localStorage and syncs across pad and lobby • Vendors colibris component partials for full self-contained skin • Supports Google Fonts for themed typography stacks (Newsreader, Space Mono, Lora, IBM Plex, VT323)
Diagram
flowchart LR
A["User selects theme<br/>in Settings dropdown"] --> B["Theme saved to<br/>localStorage"]
B --> C["data-theme attribute<br/>applied to html"]
C --> D["CSS variables<br/>override per theme"]
D --> E["Pad + lobby<br/>render in theme"]
F["Colibris component<br/>partials vendored"] --> G["margin/src/<br/>self-contained"]
G --> H["No core template<br/>edits needed"]
File Changes
1. src/static/skins/margin/pad.js
✨ Enhancement +176/-0
Theme selector injection and iframe propagation
2. src/static/skins/margin/index.js
✨ Enhancement +150/-0
Lobby theme bootstrap and recent pads UI
3. src/static/skins/margin/pad.css
✨ Enhancement +415/-0
Theme definitions and component styling
(追記) View more (25) (追記ここまで)
4. src/static/skins/margin/index.css
✨ Enhancement +43/-0
Lobby page theme styling
5. src/static/skins/margin/timeslider.js
✨ Enhancement +4/-0
Timeslider custom start hook
6. src/static/skins/margin/timeslider.css
✨ Enhancement +108/-0
Timeslider UI theme styling
7. src/static/skins/margin/src/general.css
✨ Enhancement +11/-0
General body and heading styles
8. src/static/skins/margin/src/layout.css
✨ Enhancement +48/-0
Editor container and layout structure
9. src/static/skins/margin/src/pad-editor.css
✨ Enhancement +48/-0
Editor iframe theme tokens and styling
10. src/static/skins/margin/src/pad-variants.css
✨ Enhancement +228/-0
Toolbar and editor background variants
11. src/static/skins/margin/src/components/buttons.css
✨ Enhancement +74/-0
Button styling and states
12. src/static/skins/margin/src/components/chat.css
✨ Enhancement +91/-0
Chat widget theme styling
13. src/static/skins/margin/src/components/form.css
✨ Enhancement +122/-0
Form inputs and select styling
14. src/static/skins/margin/src/components/gritter.css
✨ Enhancement +82/-0
Notification popup styling
15. src/static/skins/margin/src/components/import-export.css
✨ Enhancement +5/-0
Import/export UI styling
16. src/static/skins/margin/src/components/popup.css
✨ Enhancement +177/-0
Settings and modal popup styling
17. src/static/skins/margin/src/components/scrollbars.css
✨ Enhancement +41/-0
Scrollbar track and thumb styling
18. src/static/skins/margin/src/components/sidediv.css
✨ Enhancement +31/-0
Line number sidebar styling
19. src/static/skins/margin/src/components/table-of-content.css
✨ Enhancement +21/-0
Table of contents panel styling
20. src/static/skins/margin/src/components/toolbar.css
✨ Enhancement +154/-0
Toolbar buttons and menu styling
21. src/static/skins/margin/src/components/users.css
✨ Enhancement +65/-0
User list and color picker styling
22. src/static/skins/margin/src/plugins/author_hover.css
✨ Enhancement +10/-0
Author tooltip styling
23. src/static/skins/margin/src/plugins/brightcolorpicker.css
✨ Enhancement +14/-0
Color picker panel styling
24. src/static/skins/margin/src/plugins/comments.css
✨ Enhancement +112/-0
Comments sidebar and modal styling
25. src/static/skins/margin/src/plugins/font_color.css
✨ Enhancement +41/-0
Font color button and text colors
26. src/static/skins/margin/src/plugins/set_title_on_pad.css
✨ Enhancement +7/-0
Pad title bar styling
27. src/static/skins/margin/src/plugins/tables2.css
✨ Enhancement +239/-0
Table insertion and editing UI
28. src/static/skins/margin/README.md
📝 Documentation +53/-0
Installation and usage documentation
Code Review by Qodo
🐞 Bugs (3) 📘 Rule violations (1)
1. Hardcoded Google Fonts URL 📘 Rule violation ⛨ Security
Description
index.css and pad.css hardcode https:// Google Fonts @import URLs instead of using protocol-independent // URLs. This violates the guideline intended to avoid protocol-specific URL embedding so the resource works under both HTTP and HTTPS.
Code
src/static/skins/margin/index.css[5]+@import url("https://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");
Evidence
PR Compliance ID 7 requires protocol-independent URLs (//) where applicable, and the cited changes show @import statements in both src/static/skins/margin/index.css and src/static/skins/margin/pad.css using https://fonts.googleapis.com/... directly rather than a protocol-independent form.
src/static/skins/margin/index.css[5-5]
src/static/skins/margin/pad.css[25-25]
Best Practice: Repository guidelines
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description Update the Google Fonts `@import` statements in `index.css` and `pad.css` to avoid hardcoding the `https://` protocol and instead use protocol-independent `//` URLs as required by the compliance guideline. ## Issue Context PR Compliance ID 7 requires protocol-independent URLs (`//`) where applicable; these are fetchable external stylesheet URLs (`fonts.googleapis.com`) that can be expressed in protocol-independent form so they work under both HTTP and HTTPS. ## Fix Focus Areas - src/static/skins/margin/index.css[5-5] - src/static/skins/margin/pad.css[25-25]
i Copy this prompt and use it to remediate the issue with your preferred AI generation tools
2. Recent pad links broken 🐞 Bug ≡ Correctness
Description
margin/index.js builds recent-pad links via string concatenation with window.location.href, which can generate invalid URLs (missing trailing slash, query/hash present) and fails for pad names needing URL encoding. This can make the Recent Pads list navigate to a non-existent page.
Code
src/static/skins/margin/index.js[R84-90]+ li.className = 'recent-pad'; + const padPath = `${window.location.href}p/${pad.name}`; + const link = document.createElement('a'); + link.style.textDecoration = 'none'; + + link.href = padPath; + link.innerText = pad.name;
Evidence
The PR constructs pad URLs with ${window.location.href}p/${pad.name}; this is not robust URL joining and can yield malformed URLs depending on the lobby URL shape. Elsewhere in the codebase, navigation URLs are built with new URL(..., window.location.href) to avoid these edge cases.
src/static/skins/margin/index.js[84-90]
src/static/js/pad_editbar.ts[460-468]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ### Issue description `src/static/skins/margin/index.js` constructs recent-pad links by concatenating `window.location.href` with `p/${pad.name}`. This can produce broken URLs (no trailing slash, existing query/hash) and does not URL-encode the pad name. ### Issue Context Core code uses `new URL(rel, window.location.href)` to safely construct navigation URLs. ### Fix Focus Areas - src/static/skins/margin/index.js[84-90] ### Suggested change Use something like: - `const padPath = new URL(`p/${encodeURIComponent(pad.name)}`, window.location.href).href;` If Etherpad can be served from a sub-path, keep the base as `window.location.href` (not `origin`) but still use `new URL()` for correct joining.
i Copy this prompt and use it to remediate the issue with your preferred AI generation tools
3. RecentPads parse can throw 🐞 Bug ☼ Reliability
Description
margin/index.js and margin/pad.js call JSON.parse() on the recentPads localStorage value without error handling, so malformed data (or storage access failures) will throw and abort the skin’s customStart() execution. This breaks recent-pad rendering on the lobby and recent-pad history updates from the pad.
Code
src/static/skins/margin/index.js[R45-50]+ const recentPadListHeading = document.querySelector('[data-l10n-id="index.recentPads"]'); + const recentPadsFromLocalStorage = localStorage.getItem('recentPads'); + let recentPadListData = []; + if (recentPadsFromLocalStorage != null) { + recentPadListData = JSON.parse(recentPadsFromLocalStorage); + }
Evidence
Both the lobby and pad skin scripts parse recentPads from localStorage without a try/catch; JSON.parse() throws on invalid JSON, and localStorage.getItem() can also throw in some restricted storage modes. Because this is inside window.customStart(), an exception aborts the rest of that hook for the page.
src/static/skins/margin/index.js[45-50]
src/static/skins/margin/pad.js[151-158]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ### Issue description The margin skin reads and parses `recentPads` from `localStorage` without guarding against malformed JSON or storage access exceptions. A single bad value can throw and stop the skin’s `customStart()`. ### Issue Context This affects only the skin hook logic (recent pads list rendering on the lobby; recent pads list updates from the pad), but it’s still a user-visible breakage. ### Fix Focus Areas - src/static/skins/margin/index.js[45-55] - src/static/skins/margin/pad.js[151-175] ### Suggested change - Wrap `localStorage.getItem()` + `JSON.parse()` in `try/catch`. - On error, fall back to `[]`. - Validate that the parsed value is an array of objects with expected fields before using. - Wrap `localStorage.setItem()` calls similarly so quota/private-mode failures don’t throw.
i Copy this prompt and use it to remediate the issue with your preferred AI generation tools
4. Timeslider theme not applied 🐞 Bug ≡ Correctness
Description
margin/timeslider.js never applies the saved marginTheme to data-theme, but margin/pad.css defines theme tokens only under [data-theme="..."] selectors. As a result, the timeslider page (including history mode’s #history-frame iframe) won’t reflect the selected margin theme.
Evidence
The margin skin’s theming is driven by data-theme (see the per-theme blocks in pad.css), and the pad/lobby set it via their skin JS. The timeslider page includes the skin’s pad.css but does not set data-theme anywhere and the skin’s timeslider.js is empty, so theme selection is not applied when viewing history (including the in-place history iframe created by pad_mode.ts).
src/static/skins/margin/timeslider.js[1-4]
src/templates/timeslider.html[9-11]
src/templates/timeslider.html[44-50]
src/static/skins/margin/pad.css[49-52]
src/static/skins/margin/pad.css[65-79]
src/static/js/pad_mode.ts[234-244]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ### Issue description The margin theme is selected via `data-theme` and persisted in `localStorage` under `marginTheme`, but `src/static/skins/margin/timeslider.js` does not apply it. Therefore the timeslider page (including history mode’s in-place iframe) does not match the user’s chosen theme. ### Issue Context - `timeslider.html` loads the skin’s `pad.css` and `timeslider.css`. - Margin’s theme variables in `pad.css` are defined under `[data-theme="..."]`. - In history mode, the timeslider page is mounted as an iframe with id `history-frame`. ### Fix Focus Areas - src/static/skins/margin/timeslider.js[1-4] ### Suggested change Add the same early theme bootstrap used in `margin/index.js` (preferably with validation like `margin/pad.js`): - Read `localStorage.getItem('marginTheme')` in a try/catch - Validate against allowed theme values (or default to `colibris`) - Set `document.documentElement.setAttribute('data-theme', theme)` as early as possible Optionally: if `window.parent !== window`, you can also attempt to read the parent document’s `data-theme` as the first choice, falling back to localStorage.
i Copy this prompt and use it to remediate the issue with your preferred AI generation tools
The Theme dropdown now lists six themes (Colibris, Editorial, Brutalist, Paper, CRT, Industrial). Light/Dark is exposed as a separate `Dark mode` checkbox in the Settings popup — paired with the theme via a new `data-mode` attribute on <html> so palettes are selected via `[data-theme="X"][data-mode="light|dark"]` in pad.css / index.css. Defaults follow each theme's natural mode (CRT and Industrial start dark; the rest start light) when the user hasn't expressed a mode preference yet. Choices persist in `localStorage` under `marginTheme` and `marginMode`, and propagate to the editor iframes via pad.js. Colibris remains the default and has only a light palette.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. Hardcoded google fonts url 📘 Rule violation ⛨ Security
index.css and pad.css hardcode https:// Google Fonts @import URLs instead of using protocol-independent // URLs. This violates the guideline intended to avoid protocol-specific URL embedding so the resource works under both HTTP and HTTPS.
Agent Prompt
## Issue description
Update the Google Fonts `@import` statements in `index.css` and `pad.css` to avoid hardcoding the `https://` protocol and instead use protocol-independent `//` URLs as required by the compliance guideline.
## Issue Context
PR Compliance ID 7 requires protocol-independent URLs (`//`) where applicable; these are fetchable external stylesheet URLs (`fonts.googleapis.com`) that can be expressed in protocol-independent form so they work under both HTTP and HTTPS.
## Fix Focus Areas
- src/static/skins/margin/index.css[5-5]
- src/static/skins/margin/pad.css[25-25]
i Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 6a7242b — Google Fonts @import URLs in pad.css and index.css are now protocol-relative (//fonts.googleapis.com/...).
Addresses Qodo feedback on PR #7721: use //fonts.googleapis.com/... rather than hardcoded https:// so the resource works under both HTTP and HTTPS without protocol-specific embedding (PR Compliance ID 7).
...er theme) - Build recent-pad links with new URL() + encodeURIComponent so a trailing slash, query, hash, or special chars in pad names no longer produce a broken link. - Wrap recentPads read/parse and writes in try/catch on both the lobby and pad scripts; an exception used to abort customStart() and break recent pads / settings injection in private mode or when the entry was corrupted. - Bootstrap data-theme + data-mode on the timeslider page (and inherit from the parent doc when running inside #history-frame), since margin/pad.css scopes its theme tokens under [data-theme="..."]. History was unthemed.
JohnMcLear
commented
May 11, 2026
Addressed the three Qodo bugs in 08a25c9:
- Recent pad links broken —
src/static/skins/margin/index.jsnow builds the link withnew URL(\p/${encodeURIComponent(pad.name)}`, window.location.href).href, matching the pattern used inpad_editbar.ts`. recentPadsparse can throw — wrappedlocalStorage.getItem+JSON.parse(and the writes) intry/catchin bothmargin/index.jsandmargin/pad.js, with shape validation that the parsed value is an array of{name: string}objects before use. Falls back to[]on any failure.- Timeslider theme not applied —
margin/timeslider.jsnow readsmarginTheme/marginModefrom localStorage (with try/catch + allowlist validation), or inherits from the parent document when mounted as the#history-frameiframe, and setsdata-theme/data-modeon the documentElement beforecustomStart().
The Google Fonts rule violation (#1) was already fixed in 6a7242b.
Uh oh!
There was an error while loading. Please reload this page.
Potential PR
This PR rose from a conversation between me and Sam and an interest in Claude Designer. The PR adds additional Themes and a selector in Settings. Each Theme has been tested w/ plugins and has dark/light mode.
To Try the themes click Settings > Themes > Select the Theme you want then for Dark mode click the Dark mode toggle.
Summary
Adds a new third-party-friendly skin alongside
colibris/no-skin, with eleven themes — one neutral baseline (colibris) and five named themes each in light and dark mode:colibriseditorialeditorial-darkbrutalistbrutalist-darkpaperpaper-darkcrt-lightcrtindustrial-lightindustrialThe skin uses the same CSS-variable contract
colibrisexposes (--primary-color,--bg-color,--main-font-family,--editor-horizontal-padding, ...) and re-uses the colibris component partials, vendored intomargin/src/so the skin is fully self-contained.A Theme dropdown is injected by
margin/pad.jsinto both columns of the Settings popup (User Settings + Pad-wide Settings) and matches the markup/styling of the built-in Font type / Language rows. Selecting a theme persists the choice inlocalStorageunder themarginThemekey and reflects across the pad and the lobby.Why this is drop-in
colibris) on load — in the pad viapad.js, in the lobby viaindex.js. Google Fonts powering the type stacks (Newsreader / Space Mono / Lora / IBM Plex Mono/Sans / VT323 / Instrument Serif) are loaded via@importfrompad.css/index.css.src/static/skins/margin/."skinName": "margin"insettings.json, same mechanism as the other skins.Test plan
pnpm run plugins i ep_comments_page ep_webrtc ep_font_family ep_font_size ep_font_color ep_headings2 ep_set_title_on_pad ep_align ep_image_upload ep_subscript_and_superscript+ boot etherpad withskinName=margin; lobby + pad render in the saved/default theme.--editor-horizontal-padding/--editor-vertical-paddingzero out so the editor fills the container, matching colibris's responsive behavior.🤖 Generated with Claude Code