Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/static/skins/margin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# margin — Etherpad skin

A standalone drop-in skin with eleven themes — one neutral default and five named themes each available in light and dark mode:

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

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 on load (defaulting to `colibris`), the Google Fonts stylesheet is `@import`-ed from `pad.css` / `index.css`, and a **Theme** dropdown is injected into the User Settings and Pad-wide Settings popups.

## Switch themes at runtime

The Settings popup (the gear icon in the toolbar) has a **Theme** dropdown in both User Settings and Pad-wide Settings columns. Selecting a theme persists the choice in `localStorage` under the `marginTheme` key and reflects across the pad and the lobby.

Programmatically, from DevTools:

```js
document.documentElement.dataset.theme = 'crt'
```

## 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.
43 changes: 43 additions & 0 deletions src/static/skins/margin/index.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

150 changes: 150 additions & 0 deletions src/static/skins/margin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use strict';

// Apply the user's saved theme as early as possible so the lobby paints in
// the same theme as the last pad they visited. The dropdown that writes
// this localStorage key lives in the pad's Settings popup (see pad.js).
const MARGIN_THEME_KEY = 'marginTheme';
const MARGIN_THEME_DEFAULT = 'colibris';
try {
const saved = localStorage.getItem(MARGIN_THEME_KEY);
document.documentElement.setAttribute('data-theme', saved || MARGIN_THEME_DEFAULT);
} catch (_) {
document.documentElement.setAttribute('data-theme', 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"]');
const recentPadsFromLocalStorage = localStorage.getItem('recentPads');
let recentPadListData = [];
if (recentPadsFromLocalStorage != null) {
recentPadListData = JSON.parse(recentPadsFromLocalStorage);
}

// 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 = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right w-4 h-4 text-gray-400"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg>';
const clockIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock w-3 h-3"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>';
const personalIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users w-3 h-3"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>';
recentPadListData.forEach((pad) => {
const li = document.createElement('li');


li.style.cursor = 'pointer';

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;
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);
});
}
};
Loading
Loading