Skip to content

feat(skin): add margin skin (11 themes; light/dark per theme; switchable from Settings)#7721

Draft
JohnMcLear wants to merge 5 commits into
developfrom
feat/margin-skin
Draft

feat(skin): add margin skin (11 themes; light/dark per theme; switchable from Settings)#7721
JohnMcLear wants to merge 5 commits into
developfrom
feat/margin-skin

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

@JohnMcLear JohnMcLear commented May 11, 2026

image

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:

Default Light Dark
colibris editorial editorial-dark
brutalist brutalist-dark
paper paper-dark
crt-light crt
industrial-light industrial

The skin uses the same CSS-variable contract colibris exposes (--primary-color, --bg-color, --main-font-family, --editor-horizontal-padding, …) and re-uses the colibris component partials, vendored into margin/src/ so the skin is fully self-contained.

A Theme dropdown is injected by margin/pad.js into 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 in localStorage under the marginTheme key and reflects across the pad and the lobby.

Why this is drop-in

  • No core template edits. The skin applies the saved theme (defaulting to colibris) on load — in the pad via pad.js, in the lobby via index.js. 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.
  • No new build-time dependencies. Only static assets under src/static/skins/margin/.
  • Skin selection unchanged. Enable with "skinName": "margin" in settings.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 with skinName=margin; lobby + pad render in the saved/default theme.
  • All eleven themes selectable from the Theme dropdown in User Settings and Pad-wide Settings; selection persists across reload and propagates to the editor iframes.
  • Built-in toolbar dropdowns (Font family / size / color, Heading style) and Settings popup dropdowns are vertically centered and option lists are width-capped.
  • Below 1000px viewport, --editor-horizontal-padding / --editor-vertical-padding zero out so the editor fills the container, matching colibris's responsive behavior.
  • Snap / CI build.

🤖 Generated with Claude Code

…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.
@qodo-code-review
Copy link
Copy Markdown

ⓘ 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.

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Add margin skin with 11 switchable themes and Settings integration

✨ Enhancement

Grey Divider

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"]
Loading

Grey Divider

File Changes

1. src/static/skins/margin/pad.js ✨ Enhancement +176/-0

Theme selector injection and iframe propagation

src/static/skins/margin/pad.js


2. src/static/skins/margin/index.js ✨ Enhancement +150/-0

Lobby theme bootstrap and recent pads UI

src/static/skins/margin/index.js


3. src/static/skins/margin/pad.css ✨ Enhancement +415/-0

Theme definitions and component styling

src/static/skins/margin/pad.css


View more (25)
4. src/static/skins/margin/index.css ✨ Enhancement +43/-0

Lobby page theme styling

src/static/skins/margin/index.css


5. src/static/skins/margin/timeslider.js ✨ Enhancement +4/-0

Timeslider custom start hook

src/static/skins/margin/timeslider.js


6. src/static/skins/margin/timeslider.css ✨ Enhancement +108/-0

Timeslider UI theme styling

src/static/skins/margin/timeslider.css


7. src/static/skins/margin/src/general.css ✨ Enhancement +11/-0

General body and heading styles

src/static/skins/margin/src/general.css


8. src/static/skins/margin/src/layout.css ✨ Enhancement +48/-0

Editor container and layout structure

src/static/skins/margin/src/layout.css


9. src/static/skins/margin/src/pad-editor.css ✨ Enhancement +48/-0

Editor iframe theme tokens and styling

src/static/skins/margin/src/pad-editor.css


10. src/static/skins/margin/src/pad-variants.css ✨ Enhancement +228/-0

Toolbar and editor background variants

src/static/skins/margin/src/pad-variants.css


11. src/static/skins/margin/src/components/buttons.css ✨ Enhancement +74/-0

Button styling and states

src/static/skins/margin/src/components/buttons.css


12. src/static/skins/margin/src/components/chat.css ✨ Enhancement +91/-0

Chat widget theme styling

src/static/skins/margin/src/components/chat.css


13. src/static/skins/margin/src/components/form.css ✨ Enhancement +122/-0

Form inputs and select styling

src/static/skins/margin/src/components/form.css


14. src/static/skins/margin/src/components/gritter.css ✨ Enhancement +82/-0

Notification popup styling

src/static/skins/margin/src/components/gritter.css


15. src/static/skins/margin/src/components/import-export.css ✨ Enhancement +5/-0

Import/export UI styling

src/static/skins/margin/src/components/import-export.css


16. src/static/skins/margin/src/components/popup.css ✨ Enhancement +177/-0

Settings and modal popup styling

src/static/skins/margin/src/components/popup.css


17. src/static/skins/margin/src/components/scrollbars.css ✨ Enhancement +41/-0

Scrollbar track and thumb styling

src/static/skins/margin/src/components/scrollbars.css


18. src/static/skins/margin/src/components/sidediv.css ✨ Enhancement +31/-0

Line number sidebar styling

src/static/skins/margin/src/components/sidediv.css


19. src/static/skins/margin/src/components/table-of-content.css ✨ Enhancement +21/-0

Table of contents panel styling

src/static/skins/margin/src/components/table-of-content.css


20. src/static/skins/margin/src/components/toolbar.css ✨ Enhancement +154/-0

Toolbar buttons and menu styling

src/static/skins/margin/src/components/toolbar.css


21. src/static/skins/margin/src/components/users.css ✨ Enhancement +65/-0

User list and color picker styling

src/static/skins/margin/src/components/users.css


22. src/static/skins/margin/src/plugins/author_hover.css ✨ Enhancement +10/-0

Author tooltip styling

src/static/skins/margin/src/plugins/author_hover.css


23. src/static/skins/margin/src/plugins/brightcolorpicker.css ✨ Enhancement +14/-0

Color picker panel styling

src/static/skins/margin/src/plugins/brightcolorpicker.css


24. src/static/skins/margin/src/plugins/comments.css ✨ Enhancement +112/-0

Comments sidebar and modal styling

src/static/skins/margin/src/plugins/comments.css


25. src/static/skins/margin/src/plugins/font_color.css ✨ Enhancement +41/-0

Font color button and text colors

src/static/skins/margin/src/plugins/font_color.css


26. src/static/skins/margin/src/plugins/set_title_on_pad.css ✨ Enhancement +7/-0

Pad title bar styling

src/static/skins/margin/src/plugins/set_title_on_pad.css


27. src/static/skins/margin/src/plugins/tables2.css ✨ Enhancement +239/-0

Table insertion and editing UI

src/static/skins/margin/src/plugins/tables2.css


28. src/static/skins/margin/README.md 📝 Documentation +53/-0

Installation and usage documentation

src/static/skins/margin/README.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 11, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1)

Grey Divider


Action required

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]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

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.

ⓘ 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.

ⓘ 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.
Code

src/static/skins/margin/timeslider.js[R1-4]

+'use strict';
+
+window.customStart = () => {
+};
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.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

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.
Comment thread src/static/skins/margin/index.css Outdated
* 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("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");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

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/…).

@JohnMcLear JohnMcLear marked this pull request as draft May 11, 2026 10:40
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).
@JohnMcLear JohnMcLear requested a review from SamTV12345 May 11, 2026 13:31
…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
Copy link
Copy Markdown
Member Author

Addressed the three Qodo bugs in 08a25c9:

  • Recent pad links brokensrc/static/skins/margin/index.js now builds the link with new URL(\p/${encodeURIComponent(pad.name)}`, window.location.href).href, matching the pattern used in pad_editbar.ts`.
  • recentPads parse can throw — wrapped localStorage.getItem + JSON.parse (and the writes) in try/catch in both margin/index.js and margin/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 appliedmargin/timeslider.js now reads marginTheme / marginMode from localStorage (with try/catch + allowlist validation), or inherits from the parent document when mounted as the #history-frame iframe, and sets data-theme / data-mode on the documentElement before customStart().

The Google Fonts rule violation (#1) was already fixed in 6a7242b.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant