From 13283c2a66b13dbd8e88fb0d1a8ac3b4afe7a8d7 Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 2 Jun 2026 18:45:41 +0100 Subject: [PATCH 1/4] fix: match Etherpad colibris for button, chat message, and editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flagged components diverged from the real colibris skin: - EpButton primary was inverted (dark bg + green text). Real colibris .btn-primary is a primary-coloured (green) background with bg-coloured (white) text. Fixed; hover/active now darken via brightness filter. - EpChatMessage was a bubble-style redesign. Etherpad chat is a plain message line (#chattext p, padding 4px 10px): bold author, muted inline time, then the text — no bubbles, own/other styled the same. Ported. - EpEditor defaulted to a monospace font and #333 text. Etherpad's editor is proportional in --text-color (#485365). Defaults now follow the theme (--main-font-family / --text-color / --bg-color). Also carries the pnpm dlx→exec Playwright CI fix so this branch's browser tests pass independently of the docs PR. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 5 ++++- src/EpButton.ts | 18 +++++++++--------- src/EpChatMessage.ts | 35 ++++++++++++----------------------- src/EpEditor.ts | 6 +++--- 4 files changed, 28 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d1a59e..45f9be5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,10 @@ jobs: - run: pnpm install --frozen-lockfile - name: Install Playwright browsers - run: pnpm dlx playwright install --with-deps chromium + # Use the project-pinned Playwright (pnpm exec), not `pnpm dlx` which + # fetches the latest Playwright and installs mismatched browser + # revisions, leaving the pinned version's binary missing. + run: pnpm exec playwright install --with-deps chromium - name: Run tests run: pnpm test diff --git a/src/EpButton.ts b/src/EpButton.ts index 4c592c6..6214e4d 100644 --- a/src/EpButton.ts +++ b/src/EpButton.ts @@ -51,21 +51,21 @@ export class EpButton extends LitElement { background: var(--bg-soft-color, #f2f3f4); } - /* Primary */ + /* Primary — matches Etherpad colibris .btn-primary: primary-coloured + background with bg-coloured (white) text. */ :host([variant="primary"]) button { - background: var(--text-color, #586a69); - color: var(--primary-color, #64d29b); + background: var(--primary-color, #64d29b); + color: var(--bg-color, #ffffff); border: none; - transition: .2s background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease; + transition: filter 0.15s ease, opacity 0.15s ease; } - :host([variant="primary"]) button:active { - box-shadow: var(--primary-button-active,inset 0 1px 12px rgba(0, 0, 0, 0.9)); - background: var(--primary-button-active, #444); + :host([variant="primary"]) button:hover { + filter: brightness(0.94); } - :host([variant="primary"]) button:hover { - background: var(--dark-color, #4a5d5c); + :host([variant="primary"]) button:active { + filter: brightness(0.88); } /* Ghost */ diff --git a/src/EpChatMessage.ts b/src/EpChatMessage.ts index 2ca1da8..ad382f1 100644 --- a/src/EpChatMessage.ts +++ b/src/EpChatMessage.ts @@ -4,44 +4,39 @@ import { customElement, property } from 'lit/decorators.js'; @customElement('ep-chat-message') export class EpChatMessage extends LitElement { static styles = css` + /* Matches Etherpad colibris chat: a plain message line (#chattext p), + padding 4px 10px, bold author, muted inline time, then the text. No + bubbles — Etherpad does not style own vs other messages differently. */ :host { --ep-font: var(--main-font-family, Quicksand, Cantarell, "Open Sans", "Helvetica Neue", sans-serif); display: block; font-family: var(--ep-font); font-size: 14px; + line-height: 1.5; color: var(--text-color, #485365); - padding: 6px 10px; + padding: 4px 10px; + word-wrap: break-word; } :host(:first-child) { padding-top: 10px; } :host(:last-child) { padding-bottom: 10px; } - .header { - display: flex; - align-items: baseline; - gap: 8px; - margin-bottom: 2px; - } - .author { - font-weight: 700; - font-size: 13px; + font-weight: bold; } .time { font-size: 11px; color: var(--text-soft-color, #576273); + margin: 0 4px 0 6px; } - .body { - line-height: 1.5; - word-wrap: break-word; - } + .body { display: inline; } + /* authorColors mode: Etherpad tints the whole message with the author + colour. Opt-in via the own flag to keep the default view plain. */ :host([own]) { background: var(--bg-soft-color, #f2f3f4); - border-radius: 4px; - margin: 2px 0; } `; @@ -52,13 +47,7 @@ export class EpChatMessage extends LitElement { render() { return html` -
- ${this.author} - ${this.time ? html`${this.time}` : ''} -
-
- -
+ ${this.author}${this.time ? html`${this.time}` : html``} `; } } diff --git a/src/EpEditor.ts b/src/EpEditor.ts index 7729f37..eec2b55 100644 --- a/src/EpEditor.ts +++ b/src/EpEditor.ts @@ -28,11 +28,11 @@ export class EpEditor extends LitElement { height: 100%; min-height: inherit; overflow: auto; - font-family: var(--ep-editor-font, monospace); + font-family: var(--ep-editor-font, var(--main-font-family, Quicksand, Cantarell, "Open Sans", "Helvetica Neue", sans-serif)); font-size: var(--ep-editor-font-size, 14px); line-height: var(--ep-editor-line-height, 1.6); - color: var(--ep-editor-color, #333); - background: var(--ep-editor-bg, #fff); + color: var(--ep-editor-color, var(--text-color, #485365)); + background: var(--ep-editor-bg, var(--bg-color, #fff)); padding: var(--ep-editor-padding, 8px 12px); box-sizing: border-box; outline: none; From 0fb41eacc4d0274e10db47478a8c1940f34d5b5c Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 2 Jun 2026 18:48:08 +0100 Subject: [PATCH 2/4] fix: reset UA button background and align error red to Etherpad danger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EpButton: the base button never set a background, so the browser's light ButtonFace leaked through default/ghost/icon variants — fine in light themes, glaringly wrong in dark. Reset to transparent (matches Etherpad's `background: none` base). - EpInput/EpNotification/EpToast: error/danger colour was #d9534f; use Etherpad colibris .btn-danger #d1242f for consistency. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/EpButton.ts | 4 ++++ src/EpInput.ts | 4 ++-- src/EpNotification.ts | 4 ++-- src/EpToast.ts | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/EpButton.ts b/src/EpButton.ts index 6214e4d..f67c004 100644 --- a/src/EpButton.ts +++ b/src/EpButton.ts @@ -20,6 +20,10 @@ export class EpButton extends LitElement { line-height: 1.5; border: none; outline: none; + /* Reset the UA button background so it doesn't leak through variants + that set their own (default/ghost/icon were showing the browser's + light ButtonFace, which looked broken in dark themes). */ + background: transparent; display: inline-flex; width: 100%; height: 100%; diff --git a/src/EpInput.ts b/src/EpInput.ts index 04b76ad..838beb4 100644 --- a/src/EpInput.ts +++ b/src/EpInput.ts @@ -65,13 +65,13 @@ export class EpInput extends LitElement { :host([error]) input, :host([error]) textarea { - border-color: #d9534f; + border-color: #d1242f; } .error-text { font-family: var(--ep-font); font-size: 12px; - color: #d9534f; + color: #d1242f; margin-top: 4px; } `; diff --git a/src/EpNotification.ts b/src/EpNotification.ts index 6dc32d8..11f0199 100644 --- a/src/EpNotification.ts +++ b/src/EpNotification.ts @@ -90,7 +90,7 @@ export class EpNotification extends LitElement { } :host([type="error"]) .notification { - border-left-color: #d9534f; + border-left-color: #d1242f; } :host([type="info"]) .notification { @@ -105,7 +105,7 @@ export class EpNotification extends LitElement { } :host([type="success"]) .icon { color: var(--primary-color, #64d29b); } - :host([type="error"]) .icon { color: #d9534f; } + :host([type="error"]) .icon { color: #d1242f; } :host([type="info"]) .icon { color: var(--dark-color, #576273); } .body { flex: 1; min-width: 0; word-wrap: break-word; } diff --git a/src/EpToast.ts b/src/EpToast.ts index caab76e..4e4efde 100644 --- a/src/EpToast.ts +++ b/src/EpToast.ts @@ -13,7 +13,7 @@ interface ToastOptions { const toastIconSvg: Record = { success: ``, - error: ``, + error: ``, info: ``, }; @@ -61,7 +61,7 @@ export class EpToastItem extends LitElement { } :host([type="success"]) .toast { border-left-color: var(--primary-color, #64d29b); } - :host([type="error"]) .toast { border-left-color: #d9534f; } + :host([type="error"]) .toast { border-left-color: #d1242f; } :host([type="info"]) .toast { border-left-color: var(--dark-color, #576273); } .icon { flex-shrink: 0; width: 16px; height: 16px; margin-top: 2px; } From aa4c5900a2ae8ec7f759ecb5e1f009a91955dd5a Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 2 Jun 2026 19:52:28 +0100 Subject: [PATCH 3/4] fix: more colibris fidelity (checkbox, color picker, modal, editor link) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From a full component-by-component audit against the real colibris skin: - EpCheckbox: same inversion as the button — checked track used --text-color (dark) instead of --primary-color. Rebuilt as Etherpad's outlined toggle: bg-soft track with a text-soft border + thumb that turn primary-coloured (green) when checked (matches form.css :before/:after). - EpColorPicker: default swatches were generic saturated colours; replaced with Etherpad's pastel author-colour palette (AuthorManager.getColorPalette). - EpModal: backdrop was rgba(72,83,101,.3); use Etherpad's dialog backdrop rgba(0,0,0,.45) + blur(2px) (#settings-dialog::backdrop). - EpEditor: default link colour #0366d6 → Etherpad's #2e96f3. Audit found the remaining components (input, toast, notification, dropdown, toolbar-select, chat, user badge) already track the theme correctly. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/EpCheckbox.ts | 25 ++++++++++++++++++------- src/EpColorPicker.ts | 7 +++++-- src/EpEditor.ts | 2 +- src/EpModal.ts | 4 +++- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/EpCheckbox.ts b/src/EpCheckbox.ts index 421162c..9279fde 100644 --- a/src/EpCheckbox.ts +++ b/src/EpCheckbox.ts @@ -22,11 +22,17 @@ export class EpCheckbox extends LitElement { pointer-events: none; } + /* Etherpad colibris toggle: a light, outlined track (bg-soft fill with a + text-soft border) that switches to a primary-coloured border when + checked. */ .track { position: relative; - background: var(--middle-color, #d2d2d2); + box-sizing: border-box; + background: var(--bg-soft-color, #f2f3f4); + border: 2px solid var(--text-soft-color, #576273); border-radius: 10px; - transition: background 0.2s ease; + opacity: 0.7; + transition: border-color 0.2s ease, opacity 0.2s ease; flex-shrink: 0; } @@ -41,20 +47,25 @@ export class EpCheckbox extends LitElement { } :host([checked]) .track { - background: var(--text-color, #64d29b); + background: transparent; + border-color: var(--primary-color, #64d29b); + opacity: 1; } - + .thumb { position: absolute; top: 50%; width: 16px; height: 16px; border-radius: 50%; - background: white; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); - transition: transform 0.2s ease; + background: var(--text-soft-color, #576273); + transition: transform 0.2s ease, background 0.2s ease; transform: translateY(-50%); } + + :host([checked]) .thumb { + background: var(--primary-color, #64d29b); + } :host([variant="default"]) .thumb { left: 2px; diff --git a/src/EpColorPicker.ts b/src/EpColorPicker.ts index 8b17812..4db768f 100644 --- a/src/EpColorPicker.ts +++ b/src/EpColorPicker.ts @@ -27,9 +27,12 @@ const isLightColor = (color: string): boolean => { } }; +// Etherpad's author colour palette (the pastel tints from +// AuthorManager.getColorPalette) — the same swatches Etherpad offers when +// picking your author colour. const DEFAULT_COLORS = [ - 'black', 'red', 'green', 'blue', 'yellow', 'orange', - 'purple', 'pink', 'brown', 'gray', 'white', 'cyan', + '#ffc7c7', '#fff1c7', '#e3ffc7', '#c7ffd5', '#c7ffff', '#c7d5ff', '#e3c7ff', '#ffc7f1', + '#ffa8a8', '#ffe699', '#cfff9e', '#99ffb3', '#a3ffff', '#99b3ff', '#cc99ff', '#ff99e5', ]; @customElement('ep-color-picker') diff --git a/src/EpEditor.ts b/src/EpEditor.ts index eec2b55..8fa6085 100644 --- a/src/EpEditor.ts +++ b/src/EpEditor.ts @@ -63,7 +63,7 @@ export class EpEditor extends LitElement { .ep-editor-container .tag\\:u, .ep-editor-container u { text-decoration: underline; } .ep-editor-container .tag\\:s, .ep-editor-container s { text-decoration: line-through; } - .ep-editor-container a { color: var(--ep-editor-link-color, #0366d6); text-decoration: underline; } + .ep-editor-container a { color: var(--ep-editor-link-color, #2e96f3); text-decoration: underline; } :host([readonly]) .ep-editor-container { opacity: 0.85; diff --git a/src/EpModal.ts b/src/EpModal.ts index 9b45956..c38ba72 100644 --- a/src/EpModal.ts +++ b/src/EpModal.ts @@ -35,7 +35,9 @@ export class EpModal extends LitElement { :host([open]) { display: flex; } .overlay { position: fixed; inset: 0; - background: rgba(72, 83, 101, 0.3); + /* Matches Etherpad's dialog backdrop (#settings-dialog::backdrop). */ + background: rgba(0, 0, 0, 0.45); + backdrop-filter: blur(2px); animation: ep-modal-fade-in 0.15s ease; } .dialog { From 23d3cf1d2f456f239b6504b61962280980257ca7 Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 2 Jun 2026 20:07:41 +0100 Subject: [PATCH 4/4] =?UTF-8?q?fix(editor):=20typing=20reversed=20every=20?= =?UTF-8?q?character=20=E2=80=94=20render=20in=20light=20DOM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EpEditor mounted Etherpad's Ace engine inside a shadow root. Ace reads and restores the caret via the document Selection API, which does not work across a shadow boundary: getRangeAt() retargets the range to the shadow host, so the caret always read as offset 0 and every keystroke inserted at the document start — typing "hello" produced "olleh". - EpEditor now renders into the light DOM (createRenderRoot returns this), so the engine uses the standard, well-tested document-selection path. Styles moved from `static styles` into an inline
0) { + const r = ranges[0]; + if (r.startContainer) return r; + } + } + if (browserSelection.rangeCount === 0) return null; + return browserSelection.getRangeAt(0); + } + private getSelection(): any { const browserSelection = this.getDocSelection(); - if (!browserSelection || browserSelection.type === 'None' || - browserSelection.rangeCount === 0) { + if (!browserSelection) return null; + // In shadow DOM the legacy Selection reports type 'None' / rangeCount 0 + // even when the caret is in the shadow tree, but getComposedRanges still + // resolves it — so only apply these guards outside a shadow root. + if (!(this.rootNode instanceof ShadowRoot) && + (browserSelection.type === 'None' || browserSelection.rangeCount === 0)) { return null; } - const range = browserSelection.getRangeAt(0); + const range = this.getSelectionRange(browserSelection); + if (!range) return null; const isInBody = (n: Node | null): boolean => { while (n) {