From b00633af0fc76d091bdc8ea73a7a93b211c20134 Mon Sep 17 00:00:00 2001 From: Till Heidrich Date: Wed, 16 Sep 2026 12:39:26 +0200 Subject: [PATCH] =?UTF-8?q?Aufr=C3=A4umen,=20Reddit-Aufbereitung,=20Obsidi?= =?UTF-8?q?an,=20Verlauf,=20iPhone-Kurzbefehl;=20Spenden=20vorerst=20aus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - clean.js: Webseiten aufräumen (Mozilla Readability für Artikel, sonst Navigation/Cookie-Banner/Knöpfe entfernen), Knopf 'Ohne Aufräumen' - Reddit (neu und alt): Titel, Subreddit, Autor, keine Votes/Reply-Reste, Kommentare als verschachtelte Zitate mit (OP) - Optionaler Knopf 'In Obsidian öffnen' (obsidian://new, Vault/Ordner einstellbar, Text über Zwischenablage) - Optionaler Verlauf der letzten 10 Clips in IndexedDB, standardmäßig aus - Anleitung iPhone-Kurzbefehl in der App und docs/ios-shortcut.md, ?from=ios weist auf Einfügen hin - Spenden-Links ausgeblendet, bis ein funktionierender Link hinterlegt ist; PayPal-Abschnitt aus der Datenschutzerklärung entfernt - Datenschutz um Verlauf und Obsidian ergänzt, SW-Cache v6 --- Dockerfile | 2 +- README.md | 18 +- clean.js | 159 ++ datenschutz.html | 41 +- docs/ios-shortcut.md | 25 + impressum.html | 3 +- index.html | 261 ++- sw.js | 4 +- vendor/LICENSE-Readability.md | 13 + vendor/Readability.js | 2786 +++++++++++++++++++++++++++++++++ 10 files changed, 3260 insertions(+), 52 deletions(-) create mode 100644 clean.js create mode 100644 docs/ios-shortcut.md create mode 100644 vendor/LICENSE-Readability.md create mode 100644 vendor/Readability.js diff --git a/Dockerfile b/Dockerfile index bb59cb2..7cbe9f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM nginx:1.27-alpine # Static single-page app — no build step. COPY nginx.conf /etc/nginx/conf.d/default.conf COPY security-headers.conf /etc/nginx/security-headers.conf -COPY index.html impressum.html datenschutz.html legal.css icon-mono.svg manifest.webmanifest sw.js icon.svg icon-192.png icon-512.png icon-maskable-512.png apple-touch-icon.png /usr/share/nginx/html/ +COPY index.html clean.js impressum.html datenschutz.html legal.css icon-mono.svg manifest.webmanifest sw.js icon.svg icon-192.png icon-512.png icon-maskable-512.png apple-touch-icon.png /usr/share/nginx/html/ COPY vendor/ /usr/share/nginx/html/vendor/ EXPOSE 80 diff --git a/README.md b/README.md index 4333888..10a9cb8 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,16 @@ Obsidian vault, a wiki, or a repo. picks the HTML version when there is one. Browsers ask once for permission; `⌘V` always works too. - **Smart paste.** Formatted HTML (copy from a browser, e.g. Reddit `⌘A` → `⌘C`) is converted to clean Markdown. Plain text stays raw. Same `⌘V` for both, no mode switch. +- **Page clean-up.** Pasting a whole web page drops navigation, cookie banners, share buttons and + sidebars; articles are extracted with Mozilla Readability. *Undo clean-up* re-inserts the + unfiltered version with one click. +- **Reddit threads.** New and old Reddit: post title, subreddit and author on top, no vote counts or + Reply/Share leftovers, comments as nested quotes with `(OP)` marked. +- **Open in Obsidian.** Optional button that creates the note straight in your vault (vault and + folder configurable), via the `obsidian://` URI and the clipboard — no file detour. +- **History (opt-in).** Keeps the last 10 clips in this browser's IndexedDB. Off by default. +- **iPhone shortcut.** iOS keeps web apps out of the share sheet; a four-step Shortcut fills the gap + (see [docs/ios-shortcut.md](docs/ios-shortcut.md) or *iPhone shortcut* in the app footer). - **Force raw.** `⌘/Ctrl+Shift+V` pastes the next clip as plain text, skipping conversion. - **Live preview.** Toggle a side-by-side rendered Markdown view (marked + DOMPurify). - **Auto filename.** Derived from the first heading + date, e.g. `2026-08-11-my-thread.md`. @@ -79,11 +89,12 @@ Any static host works — point it at this repo. The reference deployment uses ## Update vendored libraries ```bash -npm pack turndown turndown-plugin-gfm marked dompurify @fontsource/space-grotesk @fontsource/space-mono +npm pack turndown turndown-plugin-gfm marked dompurify @mozilla/readability @fontsource/space-grotesk @fontsource/space-mono # turndown/dist/turndown.js -> vendor/turndown.js # turndown-plugin-gfm/dist/turndown-plugin-gfm.js -> vendor/turndown-plugin-gfm.js # marked/lib/marked.umd.js -> vendor/marked.umd.js # dompurify/dist/purify.min.js -> vendor/purify.min.js +# @mozilla/readability/Readability.js -> vendor/Readability.js # @fontsource/*/files/*-latin-{400,500,700}-normal.woff2 -> vendor/fonts/ ``` @@ -91,8 +102,5 @@ Bump the `CACHE` constant in `sw.js` whenever an asset changes so clients pick i ## License -[MIT](LICENSE). Turndown, marked, DOMPurify, Space Grotesk and Space Mono are under their respective MIT/Apache/OFL licenses. +[MIT](LICENSE). Turndown, marked, DOMPurify, Mozilla Readability, Space Grotesk and Space Mono are under their respective MIT/Apache/OFL licenses. -## Support - -clip2md is free. If it saves you time: [buy me a coffee via PayPal](https://www.paypal.com/donate/?business=info%40tillheidrich.de¤cy_code=EUR&item_name=clip2md). diff --git a/clean.js b/clean.js new file mode 100644 index 0000000..df2a477 --- /dev/null +++ b/clean.js @@ -0,0 +1,159 @@ +/* clip2md — page clean-up and Reddit formatting. Runs entirely in the browser. + Exposes window.Clip2mdClean = { prepare, postprocess }. */ +(function () { + 'use strict'; + + var BUTTON_WORDS = /^(reply|share|report|save|saved|follow|following|subscribe|unsubscribe|join|joined|award|give award|upvote|downvote|vote|like|likes|comment|comments|copy link|copy text|embed|print|tweet|pin it|email|more|more options|show more|show less|read more|see more|continue reading|load more|view more comments|back to top|skip to content|skip to main content|accept|accept all|reject|reject all|allow all|decline|manage cookies|cookie settings|close|dismiss|sign in|log in|sign up|antworten|teilen|melden|speichern|folgen|abonnieren|beitreten|gefällt mir|kommentieren|link kopieren|drucken|mehr|mehr anzeigen|weniger anzeigen|weiterlesen|mehr laden|nach oben|zum inhalt springen|akzeptieren|alle akzeptieren|ablehnen|alle ablehnen|einstellungen|cookie-einstellungen|schließen|anmelden|registrieren)$/i; + var JUNK_ATTR = /(^|[\s_-])(cookie|consent|gdpr|cmp|banner|newsletter|subscribe|share|sharing|social|advert|ad-slot|ads|sponsor|promo|popup|modal|overlay|sidebar|breadcrumb|related|recommend|paywall|toolbar|skip-link|comment-form|site-header|site-footer|masthead)([\s_-]|$)/i; + var JUNK_SELECTORS = 'nav, aside, footer, form, button, input, select, textarea, dialog, iframe, noscript, template, [hidden], [aria-hidden="true"], [role="navigation"], [role="banner"], [role="contentinfo"], [role="dialog"], [role="alertdialog"], [role="complementary"], [role="search"], [role="menu"], [role="menubar"], [role="toolbar"]'; + + function textLen(el) { return el ? (el.textContent || '').replace(/\s+/g, ' ').trim().length : 0; } + function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } + + // ---------- Generic page clean-up ---------- + function stripJunk(root) { + var total = textLen(root) || 1, removed = 0; + Array.prototype.slice.call(root.querySelectorAll(JUNK_SELECTORS)).forEach(function (el) { + // Never drop the bulk of the content just because it sits in an odd wrapper. + if (el.isConnected && textLen(el) < total * 0.5) { el.remove(); removed++; } + }); + Array.prototype.slice.call(root.querySelectorAll('[class], [id]')).forEach(function (el) { + if (!el.isConnected || /^(html|body|main|article)$/i.test(el.tagName)) return; + var sig = (el.getAttribute('class') || '') + ' ' + (el.id || ''); + if (JUNK_ATTR.test(sig) && textLen(el) < total * 0.25) { el.remove(); removed++; } + }); + Array.prototype.slice.call(root.querySelectorAll('a, span, div, li, p, strong, b')).forEach(function (el) { + if (!el.isConnected) return; + var t = (el.textContent || '').replace(/\s+/g, ' ').trim(); + if (t && t.length < 32 && BUTTON_WORDS.test(t) && !el.querySelector('img')) { el.remove(); removed++; } + }); + Array.prototype.slice.call(root.querySelectorAll('img')).forEach(function (img) { + var w = parseInt(img.getAttribute('width'), 10), h = parseInt(img.getAttribute('height'), 10); + var alt = (img.getAttribute('alt') || '') + ' ' + (img.getAttribute('src') || ''); + if ((w && w <= 48) || (h && h <= 48) || /avatar|icon|logo|emoji|spacer|pixel|tracking/i.test(alt)) { img.remove(); removed++; } + }); + return removed; + } + + function looksLikePage(doc) { + return !!doc.querySelector('article, main, [role="main"], nav, header, footer') || doc.querySelectorAll('a').length > 25; + } + + // ---------- Reddit ---------- + function isReddit(doc, html) { + if (doc.querySelector('shreddit-comment, shreddit-post, .thing.comment, .thing.link, [data-testid="comment"]')) return true; + return /reddit\.com\/(r|user)\//i.test(html) && /\b(reply|upvote|antworten)\b/i.test(doc.body ? doc.body.textContent : ''); + } + + function quote(lines, depth) { + var p = new Array(depth + 1).join('> '); + return lines.map(function (l) { return (p + l).replace(/\s+$/, ''); }); + } + + function nearest(el, sel) { var p = el.parentElement; return p ? p.closest(sel) : null; } + + function redditToMd(doc, toMd, labels) { + var out = [], post, title, author, sub, body, comments = []; + + // New Reddit (shreddit web components) + post = doc.querySelector('shreddit-post'); + var sc = doc.querySelectorAll('shreddit-comment'); + if (post || sc.length) { + title = post && post.getAttribute('post-title'); + author = post && post.getAttribute('author'); + sub = post && post.getAttribute('subreddit-prefixed-name'); + body = post && post.querySelector('[slot="text-body"]'); + var top = Array.prototype.filter.call(sc, function (c) { return !nearest(c, 'shreddit-comment'); }); + var renderNew = function (c, depth) { + var a = c.getAttribute('author') || '[deleted]'; + var bodyEl = Array.prototype.filter.call(c.querySelectorAll('[slot="comment"]'), function (b) { return b.closest('shreddit-comment') === c; })[0]; + var lines = ['**u/' + a + '**' + (author && a === author ? ' (OP)' : ''), '']; + lines = lines.concat((bodyEl ? toMd(bodyEl.innerHTML) : '').split('\n')); + var res = quote(lines, depth); + Array.prototype.filter.call(c.querySelectorAll('shreddit-comment'), function (k) { return nearest(k, 'shreddit-comment') === c; }) + .forEach(function (k) { res.push(quote([''], depth)[0]); res = res.concat(renderNew(k, depth + 1)); }); + return res; + }; + top.forEach(function (c) { comments.push(renderNew(c, 1).join('\n')); }); + } else { + // Old Reddit + var link = doc.querySelector('.thing.link'); + var oc = doc.querySelectorAll('.thing.comment'); + if (!link && !oc.length) return null; + if (link) { + var ta = link.querySelector('a.title'); title = ta && ta.textContent.trim(); + var au = link.querySelector('.tagline .author'); author = au && au.textContent.trim(); + var sr = link.querySelector('.tagline .subreddit'); sub = sr && sr.textContent.trim(); + body = link.querySelector('.usertext-body .md'); + } + var topOld = Array.prototype.filter.call(oc, function (c) { return !nearest(c, '.thing.comment'); }); + var renderOld = function (c, depth) { + var entry = c.querySelector(':scope > .entry') || c; + var au2 = entry.querySelector('.tagline .author'); var a = au2 ? au2.textContent.trim() : '[deleted]'; + var md = entry.querySelector('.usertext-body .md'); + var lines = ['**u/' + a + '**' + (author && a === author ? ' (OP)' : ''), ''].concat((md ? toMd(md.innerHTML) : '').split('\n')); + var res = quote(lines, depth); + Array.prototype.filter.call(c.querySelectorAll('.thing.comment'), function (k) { return nearest(k, '.thing.comment') === c; }) + .forEach(function (k) { res.push(quote([''], depth)[0]); res = res.concat(renderOld(k, depth + 1)); }); + return res; + }; + topOld.forEach(function (c) { comments.push(renderOld(c, 1).join('\n')); }); + } + + if (!title) { var h1 = doc.querySelector('h1'); title = h1 && h1.textContent.trim(); } + if (title) out.push('# ' + title, ''); + var meta = [sub, author ? 'u/' + author : ''].filter(Boolean).join(' · '); + if (meta) out.push('*' + meta + '*', ''); + if (body) { var b = toMd(body.innerHTML); if (b) out.push(b, ''); } + if (comments.length) { out.push('---', '', '## ' + labels.comments + ' (' + comments.length + ')', '', comments.join('\n\n')); } + var res = out.join('\n').replace(/\n{3,}/g, '\n\n').trim(); + return res.length > 20 ? res : null; + } + + var REDDIT_LINE = /^(reply|share|report|save|follow|award|upvote|downvote|vote|more replies|\d+ more repl(y|ies)|level \d+|op|edited|•|·|u\/[\w-]+ avatar|r\/[\w-]+ icon|\d+(\.\d+)?\s*[kKmM]?|\d+\s*(s|m|h|d|w|mo|y|yr|min|hr)\.?\s*ago|go to comments|sort by:?|best|top|new|controversial|old|q&a|comments|antworten|teilen|melden)$/i; + + function postprocess(md, opts) { + var lines = md.split('\n'), out = [], inFence = false; + for (var i = 0; i < lines.length; i++) { + var l = lines[i], t = l.trim(); + if (/^(```|~~~)/.test(t)) { inFence = !inFence; out.push(l); continue; } + if (inFence) { out.push(l); continue; } + var bare = t.replace(/^[-*+]\s+/, '').replace(/^\[([^\]]*)\]\([^)]*\)$/, '$1').trim(); + if (bare && BUTTON_WORDS.test(bare)) continue; + if (opts.reddit && bare && REDDIT_LINE.test(bare)) continue; + if (/^\[\s*\]\([^)]*\)$/.test(t) || /^[|•·\-–—]+$/.test(t) && !/^-{3,}$/.test(t)) continue; + out.push(l.replace(/\[\s*\]\([^)]*\)/g, '')); + } + return out.join('\n').replace(/\n{3,}/g, '\n\n').trim(); + } + + /* prepare(html, opts) → { md?, html?, kind, title? } + opts: { cleanup, reddit, toMd(htmlString)→md, labels:{comments} } */ + function prepare(html, opts) { + var doc; + try { doc = new DOMParser().parseFromString(html, 'text/html'); } catch (e) { return { html: html, kind: 'plain' }; } + if (!doc || !doc.body) return { html: html, kind: 'plain' }; + + var reddit = isReddit(doc, html); + if (opts.reddit && reddit) { + try { var rmd = redditToMd(doc, opts.toMd, opts.labels); if (rmd) return { md: rmd, kind: 'reddit', reddit: true }; } catch (e) {} + } + if (!opts.cleanup) return { html: html, kind: 'plain', reddit: reddit }; + + var before = textLen(doc.body); + if (before > 1200 && typeof window.Readability === 'function' && looksLikePage(doc) && !reddit) { + try { + var art = new window.Readability(doc.cloneNode(true), { charThreshold: 250, keepClasses: false }).parse(); + if (art && art.content && (art.textContent || '').trim().length > before * 0.3) { + var content = art.content; + if (art.title && content.indexOf(art.title) < 0) content = '

' + esc(art.title) + '

' + content; + return { html: content, kind: 'article', title: art.title, reddit: reddit }; + } + } catch (e) {} + } + var n = stripJunk(doc.body); + return { html: doc.body.innerHTML, kind: n ? 'cleaned' : 'plain', reddit: reddit }; + } + + window.Clip2mdClean = { prepare: prepare, postprocess: postprocess }; +})(); diff --git a/datenschutz.html b/datenschutz.html index 0773c53..314f73f 100644 --- a/datenschutz.html +++ b/datenschutz.html @@ -64,6 +64,12 @@ Auf Smartphones übergibt „Speichern" die Datei an das Teilen-Menü Ihres Betriebssystems; wohin Sie sie von dort weitergeben, entscheiden Sie selbst.

+

+ Das Aufräumen eingefügter Webseiten und die Aufbereitung von Reddit-Threads laufen ebenfalls + vollständig in Ihrem Browser. Der optionale Knopf „In Obsidian öffnen" legt den Text in die + Zwischenablage und übergibt ihn an die Obsidian-App auf Ihrem Gerät; an uns oder Dritte wird + dabei nichts übertragen. +

3. Lokale Speicherung Ihrer Einstellungen

@@ -74,6 +80,13 @@ eingefügten Inhalte. Es handelt sich nicht um Cookies. Sie können die Werte jederzeit über die Einstellungen Ihres Browsers löschen.

+

+ Nur wenn Sie in den Einstellungen „Letzte 10 Clips behalten" einschalten, speichert die Seite + die zuletzt bearbeiteten Texte in der lokalen Datenbank Ihres Browsers (IndexedDB). + Die Funktion ist standardmäßig ausgeschaltet. Die Einträge verlassen Ihr Gerät nicht, sind nur in + diesem Browser sichtbar und lassen sich im Verlauf einzeln oder vollständig löschen; beim + Ausschalten werden Sie gefragt, ob der Verlauf gelöscht werden soll. +

Rechtsgrundlage für diese ausschließlich lokale Speicherung ist Ihr Wunsch nach einer funktionsfähigen Bedienoberfläche; ein Zugriff unsererseits findet nicht statt. @@ -132,28 +145,7 @@ vermeiden möchten, lassen Sie die Vorschau ausgeschaltet.

-

8. Spenden über PayPal

-

- Auf der Seite finden Sie einen Link „Spenden" bzw. „Buy me a coffee". Es handelt sich um einen - einfachen Link; beim Aufruf von clip2md werden keine Inhalte, Skripte oder Cookies von PayPal - geladen und keine Daten an PayPal übertragen. Erst wenn Sie den Link anklicken, verlassen Sie - diese Seite und werden zu PayPal weitergeleitet. -

-

- Anbieter ist die PayPal (Europe) S.à r.l. et Cie, S.C.A., 22–24 Boulevard Royal, L-2449 - Luxemburg. Für die Verarbeitung auf den Seiten von PayPal ist PayPal selbst verantwortlich; es - gilt dessen Datenschutzerklärung: - paypal.com/de/legalhub/privacy-full. -

-

- Wenn Sie spenden, erhalten wir von PayPal die zur Abwicklung nötigen Angaben: Ihren Namen, Ihre - E-Mail-Adresse, den Betrag, das Datum und gegebenenfalls eine Nachricht, die Sie hinzufügen. - Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Abwicklung der Zuwendung) sowie Art. 6 Abs. 1 - lit. c DSGVO in Verbindung mit den steuer- und handelsrechtlichen Aufbewahrungspflichten - (in der Regel zehn Jahre). Eine weitere Nutzung, etwa für Werbung, findet nicht statt. -

- -

9. Ihre Rechte

+

8. Ihre Rechte

Sie haben das Recht auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18), Datenübertragbarkeit (Art. 20) sowie @@ -161,20 +153,19 @@ (Art. 21). Wenden Sie sich dafür an die oben genannte Adresse.

-

10. Beschwerderecht

+

9. Beschwerderecht

Sie können sich bei einer Datenschutz-Aufsichtsbehörde beschweren. Zuständig ist die Landesbeauftragte für den Datenschutz Niedersachsen, Prinzenstraße 5, 30159 Hannover.

-

11. Stand

+

10. Stand

Diese Datenschutzerklärung hat den Stand September 2026.

diff --git a/docs/ios-shortcut.md b/docs/ios-shortcut.md new file mode 100644 index 0000000..0a2188d --- /dev/null +++ b/docs/ios-shortcut.md @@ -0,0 +1,25 @@ +# iPhone shortcut for clip2md + +iOS does not let web apps into the share sheet. This Shortcut copies the current Safari page and opens clip2md. + +1. Open **Shortcuts**, tap **+**, name it **clip2md**. +2. Tap **ⓘ**, enable **Show in Share Sheet**, allow **Safari web pages**, **Rich text**, **Text**. +3. Add **Get Details of Safari Web Page** → **Page Contents** (input: Shortcut Input). +4. Add **Copy to Clipboard**. +5. Add **Open URLs** → `https://clip2md.heidrich-digital.de/?from=ios` (use your own domain when self-hosting). + +In Safari: Share → **clip2md** → tap **Paste**. iOS asks once whether clip2md may paste. + +For a selection only, no Shortcut is needed: copy it and use the Paste button. + +--- + +## Deutsch + +1. App **Kurzbefehle** öffnen, **+** tippen, Name **clip2md**. +2. **ⓘ** tippen, **Im Share-Sheet anzeigen** einschalten, **Safari-Webseiten**, **Rich Text**, **Text** zulassen. +3. Aktion **Details von Safari-Webseite abrufen** → **Seiteninhalt**. +4. Aktion **In Zwischenablage kopieren**. +5. Aktion **URLs öffnen** → `https://clip2md.heidrich-digital.de/?from=ios` + +In Safari: Teilen → **clip2md** → **Einfügen** tippen. diff --git a/impressum.html b/impressum.html index 60169de..afcf7e0 100644 --- a/impressum.html +++ b/impressum.html @@ -65,7 +65,7 @@

Urheberrecht und Lizenz

Der Quellcode von clip2md steht unter der MIT-Lizenz. Die eingebundenen Bibliotheken - (Turndown, Marked, DOMPurify, MIT- bzw. Apache-Lizenz) und die Schriftfamilien Space Grotesk und Space Mono stehen unter ihren jeweiligen + (Turndown, Marked, DOMPurify, Mozilla Readability; MIT- bzw. Apache-Lizenz) und die Schriftfamilien Space Grotesk und Space Mono stehen unter ihren jeweiligen freien Lizenzen (Schriften: SIL Open Font License). Texte und Gestaltung dieser Seite unterliegen dem deutschen Urheberrecht.

@@ -73,7 +73,6 @@ diff --git a/index.html b/index.html index 4688b96..f1ae513 100644 --- a/index.html +++ b/index.html @@ -111,6 +111,14 @@ .pill.donate { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 45%, var(--line)); } .pill.donate:hover { background: var(--accent); color: var(--on-accent); border-color: var(--accent); } .pill.donate svg { width: 16px; height: 16px; } + .mini { min-height: 30px; padding: 0 11px; display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line); border-radius: 999px; + background: transparent; color: var(--ink-soft); font-size: 0.8rem; font-weight: 500; cursor: pointer; white-space: nowrap; transition: color .15s, background .15s, border-color .15s; } + .mini:hover { color: var(--ink); background: var(--tint); } + .mini.accent { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 40%, var(--line)); } + .mini svg { width: 14px; height: 14px; } + .mini[hidden] { display: none; } + .btn.nudge { animation: nudge 1.4s var(--ease) 3; } + @keyframes nudge { 0%,100% { box-shadow: 0 0 0 0 var(--accent-bg); } 50% { box-shadow: 0 0 0 8px color-mix(in oklab, var(--accent) 22%, transparent); } } /* ---------- Action bar ---------- */ .bar { display: flex; align-items: stretch; gap: 8px; padding: 16px var(--gut) 12px; flex-wrap: wrap; } @@ -156,6 +164,7 @@ .pane-head .regmark::before { left: 50%; top: 0; width: 1.5px; height: 100%; transform: translateX(-50%); } .pane-head .regmark::after { top: 50%; left: 0; height: 1.5px; width: 100%; transform: translateY(-50%); } .pane-head .spacer { flex: 1; } + .pane-head .badge { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } main:not(.split) .preview-pane { display: none; } .editor-pane:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); } main.drag .editor-pane { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); } @@ -206,6 +215,29 @@ footer nav a { color: var(--ink-soft); text-decoration: none; padding: 6px 0; } footer nav a:hover { color: var(--accent); } footer nav a.coffee { color: var(--accent); font-weight: 500; } + footer nav button.linkish { background: none; border: 0; padding: 6px 0; color: var(--ink-soft); cursor: pointer; font-size: inherit; } + footer nav button.linkish:hover { color: var(--accent); } + [data-donate][hidden] { display: none !important; } + + /* History + help sheets reuse .pop */ + .pop.wide { width: 420px; } + .hist { list-style: none; margin: 0; padding: 0 6px 6px; } + .hist li { display: flex; align-items: center; gap: 8px; border-radius: var(--radius-sm); } + .hist li:hover { background: var(--tint); } + .hist .load { flex: 1; min-width: 0; text-align: left; background: none; border: 0; padding: 10px 8px; cursor: pointer; min-height: 48px; } + .hist .load b { display: block; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .hist .load span { font-family: var(--mono); font-size: 11px; color: var(--ink-soft); } + .hist .del { width: 40px; height: 40px; border: 0; background: none; color: var(--ink-soft); cursor: pointer; border-radius: 999px; } + .hist .del:hover { color: var(--err); } + .pop .empty-note { padding: 6px 12px 14px; color: var(--ink-soft); font-size: 0.9rem; margin: 0; } + .pop .foot { display: flex; justify-content: space-between; gap: 8px; padding: 6px 12px 10px; } + .help { padding: 0 12px 12px; font-size: 0.93rem; } + .help ol { padding-left: 1.2rem; margin: 6px 0 10px; } + .help li { margin-bottom: 6px; } + .help code { font-family: var(--mono); font-size: 0.82em; background: var(--tint); padding: 1px 4px; border-radius: 3px; overflow-wrap: anywhere; } + .help p { margin: 6px 0; color: var(--ink-soft); } + .sheet-head { display: flex; align-items: center; justify-content: space-between; padding: 6px 6px 0 12px; } + .sheet-head h2 { font-size: 1.05rem; margin: 6px 0; } /* ---------- Settings sheet ---------- */ .scrim { position: fixed; inset: 0; z-index: 45; background: oklch(21% 0.01 95 / 0.25); opacity: 0; pointer-events: none; transition: opacity .2s var(--ease); } @@ -270,6 +302,11 @@ .pop.open { animation: sheet .24s var(--ease); } @keyframes sheet { from { transform: translateY(40px); opacity: 0; } to { transform: none; opacity: 1; } } footer { font-size: 0.8rem; } + .pop.wide { width: auto; } + .pane-head { flex-wrap: wrap; row-gap: 6px; } + #histBtn span, #obsBtn span { display: none; } + #histBtn, #obsBtn { width: 34px; padding: 0; justify-content: center; } + .mini { min-height: 34px; } } @media (max-width: 380px) { .brand { font-size: 1.05rem; } @@ -302,7 +339,7 @@ - @@ -347,6 +384,15 @@ Markdown + + +
@@ -371,7 +417,8 @@ Runs in your browser. Nothing is uploaded.
+

Clean-up

+
+ + +
+
+ + +
+
+

Obsidian

+
+ + +
+ +
+

History

+
+ + +
+

Filename