diff --git a/DESIGN.md b/DESIGN.md
index 3052c83..fcba850 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -1,51 +1,13 @@
# clip2md — Design System
-Register: **product**. Color strategy: **Restrained** (tinted neutrals + one accent ≤10%).
-Theme: **dark**. Scene: Nutzer legt spätabends oder tagsüber am Monitor schnell einen langen
-Thread ab; eine ruhige, dunkle, blendfreie Fläche hält den Fokus auf dem Text, nicht auf dem UI.
+Seit September 2026 Teil der **Klarbild-Familie** (Tokens aus `klarbild-oss/src/styles/tokens.css`).
-## Color (OKLCH)
+- **Farbe:** warmes Papier `#EAEAE3`, Karte `#FBFBF7`, Linie `#D7D6CC`, Tinte `#16150F`, ein kräftiges Blau `#1B3BE0` (OKLCH 45% 0.23 267) für Primäraktion, Fokus, Logo. Dunkelmodus: blau getönte Neutrals, Blau aufgehellt.
+- **Schrift:** Space Grotesk (UI, Überschriften), Space Mono (Editor, Dateiname, Mono-Labels in Versalien mit 0.18em Sperrung). Self-hosted, keine Google-Fonts.
+- **Form:** Karten mit 4px Radius, Bedienelemente als Pills (999px). Kein Schatten außer 1px-Hauch und Toast/Sheet.
+- **Logo:** Klemmbrett mit Pfeil nach unten (Zwischenablage → Datei) in Blau, Wortmarke „clip**2**md" mit blauer 2. App-Icon: cremefarbenes Klemmbrett auf blauem Quadrat.
+- **Mobil:** Touch-Ziele ≥44px, Eingaben 16px (kein iOS-Zoom), `env(safe-area-inset-*)`, Einstellungen als Bottom-Sheet, Aktionsleiste als 5er-Raster mit Icon über Label, Speichern über Teilen-Menü.
+- **Motion:** ease-out-quint, 120–240ms, nur Opacity/Transform; `prefers-reduced-motion` respektiert.
-Neutrals sind zur Marken-Hue (kühles Blau-Grau, ~265) getönt — nie reines `#000`/`#fff`.
-
-| Token | OKLCH | Rolle |
-| --- | --- | --- |
-| `--bg` | `0.17 0.014 265` | App-Hintergrund (tief) |
-| `--surface` | `0.215 0.016 265` | Editor-Fläche |
-| `--raised` | `0.255 0.018 265` | Bars, Buttons |
-| `--line` | `0.32 0.02 265` | Ruhige Rahmen |
-| `--line-strong`| `0.42 0.024 265` | Aktive Rahmen |
-| `--text` | `0.95 0.006 265` | Primärtext |
-| `--muted` | `0.68 0.012 265` | Sekundärtext, Labels |
-| `--faint` | `0.5 0.012 265` | Tertiär, Legende |
-| `--accent` | `0.74 0.15 152` | Grün — Primäraktion, Fokus, „konvertiert" |
-| `--accent-ink` | `0.22 0.05 152` | Text auf Accent |
-| `--danger` | `0.66 0.16 25` | Fehler-Toast, destruktiv |
-
-Accent trägt <10% der Fläche: Save-Button, Fokus-Ring, „konvertiert"-Badge. Sonst Neutrals.
-
-## Typography
-
-- **Sans (UI-Chrome):** Inter / system-ui. Labels, Buttons, Legende.
-- **Mono (Daten):** ui-monospace / JetBrains Mono. Editor, Dateiname, Zähler — alles, was „Inhalt" ist.
-- Scale mit ≥1.25-Kontrast: 11 (legende) · 12.5 (meta) · 14 (body/editor) · 15 (buttons) · 18 (wordmark).
-- Editor-Zeilenhöhe 1.65, `tab-size: 2`, Body-Zeilen bleiben durch die Mono-Fläche natürlich lesbar.
-
-## Elevation & Layout
-
-- Drei Ebenen: `bg` (App) → `surface` (Editor, das Herzstück, bekommt den Raum) → `raised` (Bars/Buttons).
-- Keine Cards, keine Schatten außer einem weichen Toast-Drop. Trennung über 1px-Linien und Ton, nicht Boxen.
-- Spacing-Rhythmus variiert bewusst: großzügig um den Editor, kompakt in der Toolbar.
-- Radius 10–12px, konsistent.
-
-## Motion
-
-- Ease-out-quint (`cubic-bezier(0.22, 1, 0.36, 1)`), 120–240ms. Kein Bounce.
-- Nur Opacity/Transform animieren. Toast slidet ein, Button-Press senkt 1px, „konvertiert"-Badge pulst einmal kurz, auto-aktualisierter Dateiname flasht dezent.
-- `prefers-reduced-motion`: alle nicht-essenziellen Transitions aus.
-
-## Accessibility
-
-- `:focus-visible` Ring in Accent, überall. Touch-Ziele ≥40px.
-- Toasts über `aria-live="polite"`. Editor & Felder korrekt gelabelt.
-- Kontrast: Text/`bg` und Accent-Ink/Accent über WCAG AA.
+## Historie
+Vorher: dunkles Theme mit Grün (v1), dann helles Editorial mit Geist und Zinnober/Blau (v2). Beides abgelöst.
diff --git a/README.md b/README.md
index 7b7c4c3..43910d4 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,15 @@
# clip2md — Paste to Markdown
Paste a long Reddit thread, an article, or any formatted web content, and save it as clean
-Markdown in a single keystroke. Runs entirely in your browser: nothing is uploaded, no account,
+Markdown in one click. Works on desktop and phone. Runs entirely in your browser: nothing is uploaded, no account,
no tracking, works offline.
**Live demo:** https://clip2md.heidrich-digital.de

+
+
## Why
Copying from the web and getting usable Markdown is usually two or three tools and a lot of
@@ -19,6 +21,8 @@ Obsidian vault, a wiki, or a repo.
## Features
+- **One-click paste.** The *Paste* button reads the clipboard directly (Async Clipboard API) and
+ 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.
- **Force raw.** `⌘/Ctrl+Shift+V` pastes the next clip as plain text, skipping conversion.
@@ -29,9 +33,11 @@ Obsidian vault, a wiki, or a repo.
knowledge bases like Obsidian.
- **Conversion options.** Links as plain text, strip images, bullet marker (`-` / `*` / `+`),
heading style (ATX `#` / Setext).
-- **Drag & drop.** Drop an `.html`, `.md` or `.txt` file to load or convert it.
+- **Open & drop files.** Pick or drop an `.html`, `.md` or `.txt` file to load or convert it.
+- **Mobile-first.** 44 px touch targets, 16 px inputs (no iOS zoom), safe-area aware, settings as a
+ bottom sheet; *Save* hands the file to the share sheet (Files, Obsidian, mail) on phones.
- **Bilingual.** English and German, switchable in the header.
-- **Light & dark.** Warm, flat, editorial light theme by default; dark mode one click away.
+- **Light & dark.** Warm paper, one strong blue, clipboard logo; dark mode one click away.
- **Save / copy / clear**, word + character count and reading time.
- **Installable PWA**, fully offline after first load.
@@ -49,9 +55,10 @@ construction.
## Tech
-- Single self-contained `index.html` (inline CSS/JS, OKLCH tokens, Geist + Geist Mono).
+- Single self-contained `index.html` (inline CSS/JS, OKLCH tokens, Space Grotesk + Space Mono).
- Vendored, no CDN: Turndown + GFM plugin, marked, DOMPurify, fonts — all under `vendor/`.
-- Served by `nginx:alpine` (`Dockerfile` + `nginx.conf`) with a service worker for offline use.
+- Served by `nginx:alpine` (`Dockerfile` + `nginx.conf`) with a service worker for offline use
+ (network-first for pages, cache-first for static assets).
- No build step, no backend, no database.
## Run locally
@@ -71,16 +78,20 @@ 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 geist
+npm pack turndown turndown-plugin-gfm marked dompurify @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
-# geist/dist/fonts/**/*-Variable.woff2 -> vendor/fonts/
+# @fontsource/*/files/*-latin-{400,500,700}-normal.woff2 -> vendor/fonts/
```
Bump the `CACHE` constant in `sw.js` whenever an asset changes so clients pick it up.
## License
-[MIT](LICENSE). Turndown, marked, DOMPurify and Geist are under their respective MIT/OFL licenses.
+[MIT](LICENSE). Turndown, marked, DOMPurify, 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/datenschutz.html b/datenschutz.html
index a261cec..0773c53 100644
--- a/datenschutz.html
+++ b/datenschutz.html
@@ -2,29 +2,29 @@
-
+
-
+
Datenschutzerklärung — clip2md
-
-
+
+
-
-
+
+ Rechtliches
Datenschutzerklärung
+
Der Text, den Sie auf dieser Seite einfügen, wird ausschließlich in Ihrem Browser
@@ -57,6 +57,13 @@
Zwischenablage erfolgen ebenfalls lokal über die entsprechenden Browser-Schnittstellen.
Auch dabei werden keine Daten an uns übermittelt.
+
+ Über die Schaltfläche „Einfügen" liest die Seite die Zwischenablage Ihres Geräts aus. Das
+ geschieht nur, wenn Sie die Schaltfläche betätigen, und je nach Browser erst nach Ihrer
+ Zustimmung. Der Inhalt wird wie beim Einfügen per Tastatur ausschließlich lokal umgewandelt.
+ Auf Smartphones übergibt „Speichern" die Datei an das Teilen-Menü Ihres Betriebssystems; wohin
+ Sie sie von dort weitergeben, entscheiden Sie selbst.
+
3. Lokale Speicherung Ihrer Einstellungen
@@ -117,7 +124,7 @@
Einwilligungsabfrage ist deshalb nicht erforderlich.
- Eine Ausnahme betrifft die optionale Vorschau: Enthält der von Ihnen eingefuegte Text
+ Eine Ausnahme betrifft die optionale Vorschau: Enthält der von Ihnen eingefügte Text
Bilder, die auf fremde Server verweisen, lädt Ihr Browser diese Bilder beim Anzeigen der
Vorschau von dort. Dabei erfährt der jeweilige fremde Server Ihre IP-Adresse. Übertragen
wird dabei nichts von Ihrem Text; die Herkunft wird durch die Einstellung
@@ -125,7 +132,28 @@
vermeiden möchten, lassen Sie die Vorschau ausgeschaltet.
-
8. Ihre Rechte
+
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
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
@@ -133,17 +161,20 @@
(Art. 21). Wenden Sie sich dafür an die oben genannte Adresse.
-
9. Beschwerderecht
+
10. 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.
-
10. Stand
+
11. Stand
Diese Datenschutzerklärung hat den Stand September 2026.
+
diff --git a/docs/screenshot-light.png b/docs/screenshot-light.png
index b89ac54..b53631d 100644
Binary files a/docs/screenshot-light.png and b/docs/screenshot-light.png differ
diff --git a/docs/screenshot-mobile.png b/docs/screenshot-mobile.png
new file mode 100644
index 0000000..fa4a4c9
Binary files /dev/null and b/docs/screenshot-mobile.png differ
diff --git a/icon-mono.svg b/icon-mono.svg
index b1f1129..a0f4a76 100644
--- a/icon-mono.svg
+++ b/icon-mono.svg
@@ -1,7 +1,7 @@
-
-
-
-
-
+AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo5NjkzMWJiNy1jYTg3LTRjNWEtOGE4Ni0yNjQxYzJjODg3Y2IAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaEd1ybodD3L6bmfkOq9bA40AAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDpiYmUwY2ZjNi00NGNhLTRkMWItYjRiNi04MmRjY2U1ZWQyYzJscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNoD8B6kTZGbHgLyzg08rdPaAAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggefl5EZp24D6m6pO00dQE2grxyV6xfE4gO+musKFljSakZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaJenzbrkdVtDScW6NB1RE6oAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCD6NBeqJ7Ht0mHPVl42j6FWIZFbAbXQRXPffHTHE/OE8GRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBi2Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjk2OTMxYmI3LWNhODctNGM1YS04YTg2LTI2NDFjMmM4ODdjYi9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjgyMDQyNDg5LWEwZmQtNDFkMS1iNzJiLWNiN2I3ODQ5ZGM3ZHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCB5+XkRmnbgPqbqk7TR1ATaCvHJXrF8TiA76a6woWWNJqJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggaNNTgOw83IrurlIppyF4vuc6nRxPx9W196cCasAjeCSiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggjA55f0u5dgptrQQn9+ArLOZjPV71ZLqDOc0AfCcWtx50Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQNpGwGISx+yrcl0tZKCRSLni7rR99HqcAoxgjp8fGU3zZgfliCJJFreG1gw80RGYZX0xeyJFtzqX6eiCm3mHLBc=
+
+
+
+
diff --git a/icon.svg b/icon.svg
index 29fc33d..7c4d055 100644
--- a/icon.svg
+++ b/icon.svg
@@ -1,8 +1,8 @@
-
-
-
-
-
-
+AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo3NjZmY2E1NS0xNWZhLTRjMjktOWMwMi0yZWQ2YTRlMTE5N2IAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaIxdjdUDEhE5YIY3eHT5SCoAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDo1NGNhN2NlYy0wYWE0LTQ5YzMtOGI5Ny00YzUxNDdlMmJjNjJscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNo9xq2bExTiscFiT0oWzKpFgAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggsVAEfTnGrUgMMt4oWhSoE9/0KzYLZORmwRH2LXb/+VKkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaIs0FFpWK28j1nfpDaf+6ewAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCC9PsI0VhKdqWl+KMmXDr0D88GymzjDSxOZnAykNujo+mRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBi2Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjc2NmZjYTU1LTE1ZmEtNGMyOS05YzAyLTJlZDZhNGUxMTk3Yi9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjMyYTlhZWM1LWI3NjgtNDM1Zi1hYmQwLWI3YWI0ZjM1ZDliMXJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCCxUAR9OcatSAwy3ihaFKgT3/QrNgtk5GbBEfYtdv/5UqJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggC2jWKBQAXG2Nl28/mppC+AVk5pvIfJUH9IZaGTTCDMaiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFgglSQJIGyHAJjeNhWxWBLoo/KtOZ1TONf6O1V9ENt4vf10Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQKiyQz4Wq6BjxhYqiHPXV1AKlAfphe04zwZxyluWcGmPZuTfY0Geg4cTqT8u0RJ3NvUJpQuMktnYl7Du+gnwbhI=
+
+
+
+
+
diff --git a/impressum.html b/impressum.html
index 0d5edda..60169de 100644
--- a/impressum.html
+++ b/impressum.html
@@ -2,29 +2,29 @@
-
+
-
+
Impressum — clip2md
-
-
+
+
-
-
+
+ Rechtliches
Impressum
+
Angaben gemäß § 5 DDG
@@ -65,12 +65,15 @@
Urheberrecht und Lizenz
Der Quellcode von clip2md steht unter der MIT-Lizenz. Die eingebundenen Bibliotheken
- (Turndown, Marked, DOMPurify) und die Schriftfamilie Geist stehen unter ihren jeweiligen
- freien Lizenzen. Texte und Gestaltung dieser Seite unterliegen dem deutschen Urheberrecht.
+ (Turndown, Marked, DOMPurify, 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.
+
diff --git a/index.html b/index.html
index 9cac78f..d4767c1 100644
--- a/index.html
+++ b/index.html
@@ -2,378 +2,395 @@
-
-
-
+
+
+
+
+
-
+
+
clip2md — Paste to Markdown
+
-
-
-
m↓
-
clip2 md
-
paste → .md
-
+
+
+
+
+
- File
-
+ File
+
.md
auto
-
Save
-
Copy
-
Clear
+
+
+
+ Paste
+
+
+
+ Save
+
+
+
+ Copy
+
+
+
+ Open
+
+
+
+ Clear
+
+
+
-
-
-
-
-
Paste to start
-
Drop in a long Reddit thread, an article or any formatted text. Rich HTML becomes clean Markdown, plain text stays as-is. You can also drag in an .html or .md file.
-
⌘V paste · ⌘S save
+
-
+
+
+
+
Paste to start
+
A long Reddit thread, an article, any formatted text. Rich HTML becomes clean Markdown, plain text stays as it is.
+
+
+ Paste from clipboard
+
+
⌘V paste · ⌘S save · or drop a file
+
+
+
-
-
-
Output
+
+
+
Output
YAML front matter Prepend title, date, source, tags
-
+
-
Conversion
+
Conversion
Links as plain text
@@ -394,7 +411,7 @@
-
Filename
+
Filename
Title + date
@@ -413,245 +430,282 @@
(function () {
"use strict";
- // ---------- i18n ----------
var T = {
en: {
- tagline: 'paste → .md', preview: 'Preview', file: 'File', save: 'Save', copy: 'Copy', clear: 'Clear',
+ tagline: 'Paste → Markdown', preview: 'Preview', file: 'File', save: 'Save', copy: 'Copy', clear: 'Clear', open: 'Open', paste: 'Paste',
+ donate: 'Donate', coffee: 'Buy me a coffee', t_donate: 'Support clip2md via PayPal',
empty_h: 'Paste to start',
- empty_p: 'Drop in a long Reddit thread, an article or any formatted text. Rich HTML becomes clean Markdown, plain text stays as-is. You can also drag in an .html or .md file.',
- k_paste: 'paste', k_save: 'save', words: 'words', chars: 'chars', minread: 'min read',
- privacy: 'client-side · nothing is uploaded',
+ empty_p: 'A long Reddit thread, an article, any formatted text. Rich HTML becomes clean Markdown, plain text stays as it is.',
+ paste_big: 'Paste from clipboard', k_paste: 'paste', k_save: 'save', k_drop: 'or drop a file',
+ words: 'words', chars: 'chars', minread: 'min read',
+ privacy: 'Runs in your browser. Nothing is uploaded.',
s_output: 'Output', s_fm: 'YAML front matter', s_fm_hint: 'Prepend title, date, source, tags',
s_conv: 'Conversion', s_links: 'Links as plain text', s_images: 'Remove images', s_file: 'Filename',
s_p_title: 'Title + date', s_p_dt: 'Date + time',
- ph_source: 'Source URL (optional)', ph_tags: 'tags, comma-separated',
+ ph_source: 'Source URL', ph_tags: 'tags, comma-separated',
t_preview: 'Toggle preview', t_settings: 'Settings', t_theme: 'Toggle theme',
- src_conv: 'Converted HTML → Markdown', src_raw: 'Pasted as text', src_raw_forced: 'Pasted raw (forced)', src_fail: 'Pasted as text (conversion failed)',
+ src_conv: 'HTML → Markdown', src_raw: 'Plain text', src_raw_forced: 'Plain text (forced)', src_fail: 'Plain text (conversion failed)',
saved: 'Saved', copied: 'Markdown copied', nosave: 'Nothing to save', nocopy: 'Nothing to copy',
- loaded: 'File loaded', clear_confirm: 'Clear the editor?', copyfail: 'Copy failed'
+ loaded: 'File loaded', clear_confirm: 'Clear the editor?', copyfail: 'Copy failed',
+ paste_empty: 'The clipboard is empty', paste_denied: 'No clipboard access — press ⌘V / Ctrl+V or long-press the editor and choose Paste',
+ replace_confirm: 'Replace the editor content?', toobig: 'File too large (max 10 MB)', readfail: 'Could not read file'
},
de: {
- tagline: 'reinpasten → .md', preview: 'Vorschau', file: 'Datei', save: 'Speichern', copy: 'Kopieren', clear: 'Leeren',
- empty_h: 'Reinpasten',
- empty_p: 'Langen Reddit-Thread, einen Artikel oder formatierten Text einfügen. HTML wird zu sauberem Markdown, reiner Text bleibt roh. Du kannst auch eine .html- oder .md-Datei hineinziehen.',
- k_paste: 'einfügen', k_save: 'speichern', words: 'Wörter', chars: 'Zeichen', minread: 'Min. Lesezeit',
- privacy: 'clientseitig · nichts wird gesendet',
+ tagline: 'Einfügen → Markdown', preview: 'Vorschau', file: 'Datei', save: 'Speichern', copy: 'Kopieren', clear: 'Leeren', open: 'Öffnen', paste: 'Einfügen',
+ donate: 'Spenden', coffee: 'Spendier mir einen Kaffee', t_donate: 'clip2md per PayPal unterstützen',
+ empty_h: 'Einfügen und los',
+ empty_p: 'Ein langer Reddit-Thread, ein Artikel, beliebig formatierter Text. HTML wird zu sauberem Markdown, reiner Text bleibt, wie er ist.',
+ paste_big: 'Aus Zwischenablage einfügen', k_paste: 'einfügen', k_save: 'speichern', k_drop: 'oder Datei hineinziehen',
+ words: 'Wörter', chars: 'Zeichen', minread: 'Min. Lesezeit',
+ privacy: 'Läuft in deinem Browser. Nichts wird hochgeladen.',
s_output: 'Ausgabe', s_fm: 'YAML-Front-Matter', s_fm_hint: 'Titel, Datum, Quelle, Tags voranstellen',
s_conv: 'Konvertierung', s_links: 'Links als Klartext', s_images: 'Bilder entfernen', s_file: 'Dateiname',
s_p_title: 'Titel + Datum', s_p_dt: 'Datum + Uhrzeit',
- ph_source: 'Quell-URL (optional)', ph_tags: 'Tags, kommagetrennt',
- t_preview: 'Vorschau umschalten', t_settings: 'Einstellungen', t_theme: 'Thema wechseln',
- src_conv: 'HTML → Markdown konvertiert', src_raw: 'Roh eingefügt', src_raw_forced: 'Roh eingefügt (erzwungen)', src_fail: 'Roh eingefügt (Konvertierung fehlgeschlagen)',
+ ph_source: 'Quell-URL', ph_tags: 'Tags, kommagetrennt',
+ t_preview: 'Vorschau umschalten', t_settings: 'Einstellungen', t_theme: 'Hell/Dunkel',
+ src_conv: 'HTML → Markdown', src_raw: 'Reiner Text', src_raw_forced: 'Reiner Text (erzwungen)', src_fail: 'Reiner Text (Konvertierung fehlgeschlagen)',
saved: 'Gespeichert', copied: 'Markdown kopiert', nosave: 'Nichts zu speichern', nocopy: 'Nichts zu kopieren',
- loaded: 'Datei geladen', clear_confirm: 'Editor wirklich leeren?', copyfail: 'Kopieren fehlgeschlagen'
+ loaded: 'Datei geladen', clear_confirm: 'Editor wirklich leeren?', copyfail: 'Kopieren fehlgeschlagen',
+ paste_empty: 'Die Zwischenablage ist leer', paste_denied: 'Kein Zugriff auf die Zwischenablage — ⌘V / Strg+V drücken oder lange in den Editor tippen und „Einfügen" wählen',
+ replace_confirm: 'Editor-Inhalt ersetzen?', toobig: 'Datei zu groß (max. 10 MB)', readfail: 'Datei nicht lesbar'
}
};
var lang = 'en';
function t(k) { return (T[lang] && T[lang][k]) || T.en[k] || k; }
- // ---------- Storage ----------
var LS = { get: function(k,d){ try { var v = localStorage.getItem('clip2md:'+k); return v===null?d:JSON.parse(v); } catch(e){ return d; } },
set: function(k,v){ try { localStorage.setItem('clip2md:'+k, JSON.stringify(v)); } catch(e){} } };
- // ---------- Elements ----------
var $ = function(id){ return document.getElementById(id); };
var editor=$('editor'), main=$('main'), preview=$('preview'),
filename=$('filename'), fnameBox=$('fnameBox'), fnameTag=$('fnameTag'),
wordsEl=$('words'), charsEl=$('chars'), rtEl=$('rt'),
srcBadge=$('srcBadge'), srcText=$('srcText'), toastEl=$('toast'),
- settingsPop=$('settingsPop'), settingsBtn=$('settingsBtn'), previewBtn=$('previewBtn'), themeBtn=$('themeBtn');
+ settingsPop=$('settingsPop'), settingsBtn=$('settingsBtn'), previewBtn=$('previewBtn'), themeBtn=$('themeBtn'), scrim=$('scrim');
+ var coarse = window.matchMedia && matchMedia('(pointer: coarse)').matches;
var settings = {
- frontMatter: LS.get('frontMatter', false),
- source: LS.get('source', ''),
- tags: LS.get('tags', ''),
- stripLinks: LS.get('stripLinks', false),
- dropImages: LS.get('dropImages', false),
- bullet: LS.get('bullet', '-'),
- heading: LS.get('heading', 'atx'),
- pattern: LS.get('pattern', 'title')
+ frontMatter: LS.get('frontMatter', false), source: LS.get('source', ''), tags: LS.get('tags', ''),
+ stripLinks: LS.get('stripLinks', false), dropImages: LS.get('dropImages', false),
+ bullet: LS.get('bullet', '-'), heading: LS.get('heading', 'atx'), pattern: LS.get('pattern', 'title')
};
-
var filenameEdited = false, forceRaw = false, forceRawTimer = null, toastTimer = null, previewOn = LS.get('preview', false);
- // ---------- Turndown (rebuilt on settings change) ----------
+ // ---------- Turndown ----------
var td;
function buildTurndown() {
- td = new TurndownService({
- headingStyle: settings.heading, hr: '---', bulletListMarker: settings.bullet,
- codeBlockStyle: 'fenced', emDelimiter: '*', strongDelimiter: '**', linkStyle: 'inlined'
- });
+ td = new TurndownService({ headingStyle: settings.heading, hr: '---', bulletListMarker: settings.bullet,
+ codeBlockStyle: 'fenced', emDelimiter: '*', strongDelimiter: '**', linkStyle: 'inlined' });
try { if (window.turndownPluginGfm && window.turndownPluginGfm.gfm) td.use(window.turndownPluginGfm.gfm); } catch(e){}
- td.remove(['script','style','noscript','iframe','svg']);
+ td.remove(['script','style','noscript','iframe','svg','meta','link']);
+ // One space after the list marker instead of Turndown's three.
+ td.addRule('tightListItem', { filter: 'li', replacement: function(content, node, opt){
+ content = content.replace(/^\n+/, '').replace(/\n+$/, '\n').replace(/\n/gm, '\n ');
+ var prefix = opt.bulletListMarker + ' ', parent = node.parentNode;
+ if (parent && parent.nodeName === 'OL') { var start = parent.getAttribute('start'), idx = Array.prototype.indexOf.call(parent.children, node);
+ prefix = (start ? Number(start) + idx : idx + 1) + '. '; }
+ return prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : ''); } });
if (settings.dropImages) td.addRule('noimg', { filter: 'img', replacement: function(){ return ''; } });
if (settings.stripLinks) td.addRule('flatlink', { filter: 'a', replacement: function(c){ return c; } });
}
buildTurndown();
+ function htmlToMd(html){ return td.turndown(html).replace(/\n{3,}/g,'\n\n').trim(); }
// ---------- Helpers ----------
- function toast(msg, isErr){ toastEl.textContent = msg; toastEl.classList.toggle('err', !!isErr); toastEl.classList.add('show');
- clearTimeout(toastTimer); toastTimer = setTimeout(function(){ toastEl.classList.remove('show'); }, 2200); }
+ function toast(msg, isErr, ms){ toastEl.textContent = msg; toastEl.classList.toggle('err', !!isErr); toastEl.classList.add('show');
+ clearTimeout(toastTimer); toastTimer = setTimeout(function(){ toastEl.classList.remove('show'); }, ms || 2400); }
function pad(n){ return n<10?'0'+n:''+n; }
function today(){ var d=new Date(); return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate()); }
function nowTime(){ var d=new Date(); return pad(d.getHours())+pad(d.getMinutes()); }
-
var UML={ 'ä':'ae','ö':'oe','ü':'ue','ß':'ss','Ä':'ae','Ö':'oe','Ü':'ue' };
function slugify(s){ if(!s) return '';
- s=s.replace(/[äöüßÄÖÜ]/g,function(c){return UML[c]||c;}).normalize('NFKD').replace(/[\u0300-\u036f]/g,'');
+ s=s.replace(/[äöüßÄÖÜ]/g,function(c){return UML[c]||c;}).normalize('NFKD').replace(/[̀-ͯ]/g,'');
return s.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,60).replace(/-+$/g,''); }
- function deriveTitle(md){ var L=md.split(/\r?\n/), i, m, t;
+ function deriveTitle(md){ var L=md.split(/\r?\n/), i, m, s;
for(i=0;i`|]{1,}|\[.*\])$/.test(t)) continue; return t.replace(/^[#>*\-\s]+/,'').trim(); }
+ for(i=0;i`|]{1,}|\[.*\])$/.test(s)) continue; return s.replace(/^[#>*\-\s]+/,'').trim(); }
return ''; }
- function computeFilename(md){
- if (settings.pattern === 'datetime') return today()+'-'+nowTime();
+ function computeFilename(md){ if (settings.pattern === 'datetime') return today()+'-'+nowTime();
var slug = slugify(deriveTitle(md)); return today()+(slug?'-'+slug:'-clip'); }
-
function refreshFilename(flash){ if(filenameEdited) return; var next=computeFilename(editor.value);
if(next!==filename.value){ filename.value=next; if(flash){ fnameBox.classList.remove('flash'); void fnameBox.offsetWidth; fnameBox.classList.add('flash'); } } }
function setFnameTag(){ if(filenameEdited){ fnameTag.textContent = lang==='de'?'eigen':'custom'; fnameTag.classList.remove('auto'); }
else { fnameTag.textContent='auto'; fnameTag.classList.add('auto'); } }
-
function countWords(v){ return v.trim()?v.trim().split(/\s+/).length:0; }
- function updateCounts(){ var v=editor.value, w=countWords(v);
- charsEl.textContent=v.length.toLocaleString(lang==='de'?'de-DE':'en-US');
- wordsEl.textContent=w.toLocaleString(lang==='de'?'de-DE':'en-US');
- rtEl.textContent=Math.max(1, Math.round(w/200)); }
+ function updateCounts(){ var v=editor.value, w=countWords(v), loc=lang==='de'?'de-DE':'en-US';
+ charsEl.textContent=v.length.toLocaleString(loc); wordsEl.textContent=w.toLocaleString(loc); rtEl.textContent=Math.max(1, Math.round(w/200)); }
function updateEmpty(){ main.classList.toggle('has-content', editor.value.length>0); }
- function buildFrontMatter(){
- if(!settings.frontMatter) return '';
- var title=deriveTitle(editor.value)||'Untitled';
- var lines=['---','title: '+JSON.stringify(title),'date: '+today()];
+ function buildFrontMatter(){ if(!settings.frontMatter) return '';
+ var title=deriveTitle(editor.value)||'Untitled', lines=['---','title: '+JSON.stringify(title),'date: '+today()];
if(settings.source.trim()) lines.push('source: '+JSON.stringify(settings.source.trim()));
if(settings.tags.trim()){ var tg=settings.tags.split(',').map(function(s){return s.trim();}).filter(Boolean); if(tg.length) lines.push('tags: ['+tg.join(', ')+']'); }
- lines.push('---',''); return lines.join('\n');
- }
+ lines.push('---',''); return lines.join('\n'); }
function fullContent(){ return buildFrontMatter()+editor.value; }
// ---------- Preview ----------
var pvTimer=null;
function escapeHtml(s){ return s.replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); }
- function renderPreview(){ if(!previewOn) return;
- var md=editor.value, fmHtml='';
- // Show YAML front matter as a compact metadata block, not as body text.
- if(settings.frontMatter){
- var fm=buildFrontMatter();
- var m=fm.match(/^---\n([\s\S]*?)\n---/);
- if(m){ var rows=m[1].split('\n').map(function(l){ var i=l.indexOf(':'); if(i<0) return escapeHtml(l);
- return ''+escapeHtml(l.slice(0,i))+' '+escapeHtml(l.slice(i))+' '; }).join('');
- fmHtml=''+rows+'
'; }
- }
+ function renderPreview(){ if(!previewOn) return; var md=editor.value, fmHtml='';
+ if(settings.frontMatter){ var m=buildFrontMatter().match(/^---\n([\s\S]*?)\n---/);
+ if(m){ fmHtml=''+m[1].split('\n').map(function(l){ var i=l.indexOf(':'); if(i<0) return escapeHtml(l);
+ return ''+escapeHtml(l.slice(0,i))+' '+escapeHtml(l.slice(i))+' '; }).join('')+'
'; } }
var html=window.marked?window.marked.parse(md,{gfm:true,breaks:false}):escapeHtml(md);
- var clean=window.DOMPurify?window.DOMPurify.sanitize(html):html;
- preview.innerHTML=fmHtml+clean; }
+ preview.innerHTML=fmHtml+(window.DOMPurify?window.DOMPurify.sanitize(html):escapeHtml(md)); }
function schedulePreview(){ if(!previewOn) return; clearTimeout(pvTimer); pvTimer=setTimeout(renderPreview,120); }
- function showSource(key, ok, pulse){ srcText.textContent=t(key); srcBadge.dataset.key=key; srcBadge.classList.toggle('ok',!!ok); srcBadge.classList.add('show');
- if(pulse){ srcBadge.classList.remove('pulse'); void srcBadge.offsetWidth; srcBadge.classList.add('pulse'); } }
+ function showSource(key, ok){ srcText.textContent=t(key); srcBadge.dataset.key=key; srcBadge.classList.toggle('ok',!!ok); srcBadge.classList.add('show');
+ srcBadge.classList.remove('pulse'); void srcBadge.offsetWidth; srcBadge.classList.add('pulse'); }
function hideSource(){ srcBadge.classList.remove('show','ok','pulse'); srcBadge.dataset.key=''; }
-
function refreshAll(flash){ updateCounts(); refreshFilename(flash); updateEmpty(); schedulePreview(); }
- // ---------- Paste ----------
- editor.addEventListener('paste', function(e){
- var cd=e.clipboardData||window.clipboardData; if(!cd) return;
- var html=cd.getData('text/html'), text=cd.getData('text/plain'), out, key, ok;
- if(!forceRaw && html && html.trim()){
- try { out=td.turndown(html).replace(/\n{3,}/g,'\n\n').trim(); key='src_conv'; ok=true; }
- catch(err){ out=text; key='src_fail'; ok=false; }
- } else { out=text; key=forceRaw?'src_raw_forced':'src_raw'; ok=false; }
- forceRaw=false; if(out==null) return;
- e.preventDefault(); insertAtCursor(out); showSource(key,ok,true); refreshAll(true);
- });
+ // ---------- Insert ----------
function insertAtCursor(str){
- // execCommand haelt den nativen Undo-Stack der Textarea intakt (Cmd+Z nach dem Einfuegen).
- editor.focus();
- var ok=false;
+ // execCommand keeps the textarea's native undo stack intact.
+ editor.focus({ preventScroll: true }); var ok=false;
try { ok = document.execCommand('insertText', false, str); } catch(e){ ok=false; }
if(ok) return;
- var s=editor.selectionStart,en=editor.selectionEnd;
+ var s=editor.selectionStart, en=editor.selectionEnd;
editor.value=editor.value.slice(0,s)+str+editor.value.slice(en);
- var p=s+str.length; editor.selectionStart=editor.selectionEnd=p; }
+ editor.selectionStart=editor.selectionEnd=s+str.length; }
+ function ingest(html, text, raw){
+ var out, key, ok=false;
+ if(!raw && html && html.trim()){
+ try { out=htmlToMd(html); key='src_conv'; ok=true; if(!out && text) { out=text; key='src_raw'; ok=false; } }
+ catch(err){ out=text||''; key='src_fail'; }
+ } else { out=text||''; key=raw?'src_raw_forced':'src_raw'; }
+ if(!out) return false;
+ insertAtCursor(out); showSource(key, ok); refreshAll(true); return true; }
+ editor.addEventListener('paste', function(e){
+ var cd=e.clipboardData||window.clipboardData; if(!cd) return;
+ var html=cd.getData('text/html'), text=cd.getData('text/plain'), raw=forceRaw; forceRaw=false;
+ if(!html && !text) return;
+ e.preventDefault(); ingest(html, text, raw);
+ });
editor.addEventListener('input', function(){ hideSource(); refreshAll(false); });
- // ---------- Drag & drop ----------
- ['dragenter','dragover'].forEach(function(ev){ editor.addEventListener(ev, function(e){ e.preventDefault(); main.classList.add('drag'); }); });
- ['dragleave','dragend'].forEach(function(ev){ editor.addEventListener(ev, function(){ main.classList.remove('drag'); }); });
- editor.addEventListener('drop', function(e){
- var f=e.dataTransfer&&e.dataTransfer.files&&e.dataTransfer.files[0]; main.classList.remove('drag'); if(!f) return;
- e.preventDefault(); var r=new FileReader();
- if(f.size > 10*1024*1024){ toast(lang==='de'?'Datei zu gross (max. 10 MB)':'File too large (max 10 MB)', true); return; }
- if(editor.value.trim() && !confirm(lang==='de'?'Editor-Inhalt durch die Datei ersetzen?':'Replace the editor content with this file?')) return;
- r.onerror=function(){ toast(lang==='de'?'Datei nicht lesbar':'Could not read file', true); };
- r.onload=function(){ var txt=String(r.result||''); var name=f.name.toLowerCase();
- if(/\.html?$/.test(name)){ try{ txt=td.turndown(txt).replace(/\n{3,}/g,'\n\n').trim(); showSource('src_conv',true,true); }catch(_){ showSource('src_fail',false,true);} }
- else { showSource('src_raw',false,true); }
+ // ---------- One-click paste (Async Clipboard API) ----------
+ function pasteFromClipboard(){
+ var c = navigator.clipboard;
+ function fail(err){
+ if(err && err.name==='NotAllowedError' || !c){ toast(t('paste_denied'), true, 4200); }
+ else { toast(t('paste_empty'), true); }
+ editor.focus();
+ }
+ function done(html, text){ if(!ingest(html, text, false)) fail({ name: 'Empty' }); }
+ if(c && c.read){
+ // Must be the first call inside the click handler, otherwise Safari drops the user gesture.
+ c.read().then(function(items){
+ var htmlP=null, textP=null;
+ for(var i=0;i-1) htmlP=items[i].getType('text/html').then(function(b){ return b.text(); });
+ if(!textP && ty.indexOf('text/plain')>-1) textP=items[i].getType('text/plain').then(function(b){ return b.text(); }); }
+ return Promise.all([htmlP||'', textP||'']);
+ }).then(function(r){ done(r[0], r[1]); }, function(err){
+ if(c.readText && err && err.name!=='NotAllowedError'){ c.readText().then(function(tx){ done('', tx); }, fail); } else fail(err);
+ });
+ } else if(c && c.readText){ c.readText().then(function(tx){ done('', tx); }, fail); }
+ else fail({ name: 'NotAllowedError' });
+ }
+ $('pasteBtn').addEventListener('click', pasteFromClipboard);
+ $('pasteBig').addEventListener('click', pasteFromClipboard);
+
+ // ---------- Files (drop + picker) ----------
+ function loadFile(f){
+ if(!f) return;
+ if(f.size > 10*1024*1024){ toast(t('toobig'), true); return; }
+ if(editor.value.trim() && !confirm(t('replace_confirm'))) return;
+ var r=new FileReader();
+ r.onerror=function(){ toast(t('readfail'), true); };
+ r.onload=function(){ var txt=String(r.result||''), name=f.name.toLowerCase();
+ if(/\.html?$/.test(name) || f.type==='text/html'){ try{ txt=htmlToMd(txt); showSource('src_conv',true); }catch(_){ showSource('src_fail',false); } }
+ else showSource('src_raw',false);
editor.value=txt;
- if(!filenameEdited){ var base=f.name.replace(/\.[^.]+$/,''); if(base){ filename.value=slugify(base)||computeFilename(txt); } }
+ if(!filenameEdited){ var base=slugify(f.name.replace(/\.[^.]+$/,'')); if(base) filename.value=base; }
refreshAll(true); toast(t('loaded')); };
r.readAsText(f);
- });
+ }
+ ['dragenter','dragover'].forEach(function(ev){ editor.addEventListener(ev, function(e){ e.preventDefault(); main.classList.add('drag'); }); });
+ ['dragleave','dragend'].forEach(function(ev){ editor.addEventListener(ev, function(){ main.classList.remove('drag'); }); });
+ editor.addEventListener('drop', function(e){ main.classList.remove('drag');
+ var f=e.dataTransfer&&e.dataTransfer.files&&e.dataTransfer.files[0]; if(!f) return; e.preventDefault(); loadFile(f); });
+ var fileInput=$('fileInput');
+ $('openBtn').addEventListener('click', function(){ fileInput.value=''; fileInput.click(); });
+ fileInput.addEventListener('change', function(){ loadFile(fileInput.files && fileInput.files[0]); });
// ---------- Filename ----------
filename.addEventListener('input', function(){ filenameEdited=filename.value.trim().length>0; setFnameTag(); });
filename.addEventListener('blur', function(){ if(!filename.value.trim()){ filenameEdited=false; setFnameTag(); refreshFilename(false); } });
+ filename.addEventListener('keydown', function(e){ if(e.key==='Enter'){ e.preventDefault(); filename.blur(); } });
// ---------- Save / Copy / Clear ----------
- function save(){ var content=fullContent(); if(!editor.value.trim()){ toast(t('nosave'),true); flashEditor(); return; }
+ function flashEditor(){ var p=editor.parentNode; p.style.borderColor='var(--err)'; setTimeout(function(){ p.style.borderColor=''; },600); }
+ function save(){
+ if(!editor.value.trim()){ toast(t('nosave'),true); flashEditor(); return; }
var name=(filename.value.trim()||computeFilename(editor.value)).replace(/[\/\\:*?"<>|]+/g,'-').replace(/\s+/g,'-');
if(!/\.md$/i.test(name)) name+='.md';
+ var content=fullContent();
+ // Phones: hand the file to the share sheet (Files, Obsidian, mail …). Desktop: download.
+ if(coarse && navigator.canShare && typeof File==='function'){
+ try {
+ var file=new File([content], name, { type: 'text/markdown' });
+ if(navigator.canShare({ files: [file] })){
+ navigator.share({ files: [file], title: name }).then(function(){ toast(t('saved')+' · '+name); }, function(err){ if(err && err.name!=='AbortError') download(content, name); });
+ return;
+ }
+ } catch(e){}
+ }
+ download(content, name);
+ }
+ function download(content, name){
var blob=new Blob([content],{type:'text/markdown;charset=utf-8'}), url=URL.createObjectURL(blob), a=document.createElement('a');
a.href=url; a.download=name; document.body.appendChild(a); a.click(); document.body.removeChild(a);
- setTimeout(function(){ URL.revokeObjectURL(url); },1000); toast(t('saved')+' · '+name); }
- function flashEditor(){ editor.style.borderColor='var(--danger)'; setTimeout(function(){ editor.style.borderColor=''; },500); }
+ setTimeout(function(){ URL.revokeObjectURL(url); },1500); toast(t('saved')+' · '+name); }
$('save').addEventListener('click', save);
- $('copy').addEventListener('click', function(){ var v=fullContent(); if(!editor.value.trim()){ toast(t('nocopy'),true); return; }
+ $('copy').addEventListener('click', function(){ if(!editor.value.trim()){ toast(t('nocopy'),true); return; } var v=fullContent();
if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(v).then(function(){ toast(t('copied')); }, function(){ legacyCopy(v); }); }
else legacyCopy(v); });
- function legacyCopy(v){ var ta=document.createElement('textarea'); ta.value=v; ta.style.position='fixed'; ta.style.opacity='0'; document.body.appendChild(ta); ta.select();
+ function legacyCopy(v){ var ta=document.createElement('textarea'); ta.value=v; ta.setAttribute('readonly',''); ta.style.position='fixed'; ta.style.opacity='0'; document.body.appendChild(ta); ta.select();
try{ document.execCommand('copy'); toast(t('copied')); }catch(e){ toast(t('copyfail'),true); } document.body.removeChild(ta); }
$('clear').addEventListener('click', function(){ if(editor.value && !confirm(t('clear_confirm'))) return;
- editor.value=''; filenameEdited=false; hideSource(); setFnameTag(); refreshAll(false); editor.focus(); });
+ editor.value=''; filenameEdited=false; hideSource(); setFnameTag(); refreshAll(false); if(!coarse) editor.focus(); });
// ---------- Keyboard ----------
document.addEventListener('keydown', function(e){ var mod=e.metaKey||e.ctrlKey;
if(mod && !e.shiftKey && !e.altKey && (e.key==='s'||e.key==='S')){ e.preventDefault(); save(); return; }
if(mod && e.shiftKey && (e.key==='v'||e.key==='V')){ forceRaw=true; clearTimeout(forceRawTimer); forceRawTimer=setTimeout(function(){ forceRaw=false; },3000); }
- if(e.key==='Escape'){ closeSettings(); } });
+ if(e.key==='Escape') closeSettings(); });
- // ---------- Preview toggle ----------
+ // ---------- Preview ----------
function setPreview(on){ previewOn=on; main.classList.toggle('split',on); previewBtn.setAttribute('aria-pressed',on?'true':'false'); LS.set('preview',on); if(on) renderPreview(); }
previewBtn.addEventListener('click', function(){ setPreview(!previewOn); });
// ---------- Theme ----------
- function setTheme(theme){ document.documentElement.setAttribute('data-theme', theme); LS.set('theme', theme);
- document.querySelector('meta[name=theme-color]').setAttribute('content', theme==='dark'?'#231f1b':'#faf7f2'); }
- themeBtn.addEventListener('click', function(){ var cur=document.documentElement.getAttribute('data-theme')==='dark'?'dark':'light'; setTheme(cur==='dark'?'light':'dark'); });
+ function setTheme(theme, persist){ document.documentElement.setAttribute('data-theme', theme); if(persist) LS.set('theme', theme);
+ document.querySelector('meta[name=theme-color]').setAttribute('content', theme==='dark'?'#15171c':'#EAEAE3'); }
+ themeBtn.addEventListener('click', function(){ setTheme(document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark', true); });
// ---------- Language ----------
function applyLang(){
document.documentElement.setAttribute('lang', lang);
- document.querySelectorAll('[data-i18n]').forEach(function(el){ el.innerHTML=t(el.getAttribute('data-i18n')); });
+ document.querySelectorAll('[data-i18n]').forEach(function(el){ el.textContent=t(el.getAttribute('data-i18n')); });
document.querySelectorAll('[data-i18n-ph]').forEach(function(el){ el.setAttribute('placeholder', t(el.getAttribute('data-i18n-ph'))); });
- document.querySelectorAll('[data-i18n-title]').forEach(function(el){ el.setAttribute('title', t(el.getAttribute('data-i18n-title'))); });
- $('langEn').setAttribute('aria-pressed', lang==='en'?'true':'false');
- $('langDe').setAttribute('aria-pressed', lang==='de'?'true':'false');
+ document.querySelectorAll('[data-i18n-title]').forEach(function(el){ el.setAttribute('title', t(el.getAttribute('data-i18n-title'))); el.setAttribute('aria-label', t(el.getAttribute('data-i18n-title'))); });
+ var lb=$('langBtn'); lb.textContent = lang==='de'?'EN':'DE'; lb.title = lang==='de'?'English':'Deutsch';
+ if(coarse){ $('empty').querySelector('p').textContent=t('empty_p'); }
if(srcBadge.dataset.key) srcText.textContent=t(srcBadge.dataset.key);
setFnameTag(); updateCounts(); }
function setLang(l){ lang=l; LS.set('lang',l); applyLang(); }
- $('langEn').addEventListener('click', function(){ setLang('en'); });
- $('langDe').addEventListener('click', function(){ setLang('de'); });
+ $('langBtn').addEventListener('click', function(){ setLang(lang==='de'?'en':'de'); });
- // ---------- Settings popover ----------
- function openSettings(){ settingsPop.classList.add('open'); settingsBtn.setAttribute('aria-expanded','true'); }
- function closeSettings(){ settingsPop.classList.remove('open'); settingsBtn.setAttribute('aria-expanded','false'); }
- settingsBtn.addEventListener('click', function(e){ e.stopPropagation(); settingsPop.classList.contains('open')?closeSettings():openSettings(); });
- document.addEventListener('click', function(e){ if(settingsPop.classList.contains('open') && !settingsPop.contains(e.target) && e.target!==settingsBtn) closeSettings(); });
- settingsPop.addEventListener('click', function(e){ e.stopPropagation(); });
+ // ---------- Settings ----------
+ function openSettings(){ settingsPop.classList.add('open'); scrim.classList.add('open'); settingsBtn.setAttribute('aria-expanded','true'); }
+ function closeSettings(){ settingsPop.classList.remove('open'); scrim.classList.remove('open'); settingsBtn.setAttribute('aria-expanded','false'); }
+ settingsBtn.addEventListener('click', function(){ settingsPop.classList.contains('open')?closeSettings():openSettings(); });
+ scrim.addEventListener('click', closeSettings);
function bindToggle(id, key, after){ var el=$(id); el.checked=!!settings[key];
el.addEventListener('change', function(){ settings[key]=el.checked; LS.set(key,el.checked); if(after) after(); }); }
@@ -659,26 +713,26 @@
el.addEventListener('change', function(){ settings[key]=el.value; LS.set(key,el.value); if(after) after(); }); }
function bindText(id, key){ var el=$(id); el.value=settings[key];
el.addEventListener('input', function(){ settings[key]=el.value; LS.set(key,el.value); schedulePreview(); }); }
-
var fmFields=$('fmFields');
function syncFmFields(){ fmFields.hidden=!settings.frontMatter; }
bindToggle('fmToggle','frontMatter', function(){ syncFmFields(); refreshAll(false); });
bindText('fmSource','source'); bindText('fmTags','tags');
- bindToggle('stripLinks','stripLinks', function(){ buildTurndown(); });
- bindToggle('dropImages','dropImages', function(){ buildTurndown(); });
- bindSelect('bulletSel','bullet', function(){ buildTurndown(); });
- bindSelect('headingSel','heading', function(){ buildTurndown(); });
+ bindToggle('stripLinks','stripLinks', buildTurndown);
+ bindToggle('dropImages','dropImages', buildTurndown);
+ bindSelect('bulletSel','bullet', buildTurndown);
+ bindSelect('headingSel','heading', buildTurndown);
bindSelect('namePattern','pattern', function(){ refreshFilename(true); });
syncFmFields();
// ---------- Init ----------
- setTheme(LS.get('theme', (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light'));
- setLang(LS.get('lang','en'));
+ setTheme(document.documentElement.getAttribute('data-theme')==='dark'?'dark':'light', false);
+ var navLang=(navigator.language||'en').slice(0,2).toLowerCase();
+ setLang(LS.get('lang', navLang==='de'?'de':'en'));
setPreview(previewOn);
- setFnameTag(); refreshAll(false); editor.focus();
+ setFnameTag(); refreshAll(false);
+ if(!coarse) editor.focus();
- // ---------- PWA ----------
- if('serviceWorker' in navigator){ window.addEventListener('load', function(){ navigator.serviceWorker.register('sw.js').catch(function(){}); }); }
+ if('serviceWorker' in navigator){ window.addEventListener('load', function(){ navigator.serviceWorker.register('/sw.js').catch(function(){}); }); }
})();
diff --git a/legal.css b/legal.css
index 04dfe55..7f1b18b 100644
--- a/legal.css
+++ b/legal.css
@@ -1,66 +1,64 @@
-@font-face { font-family: "Geist"; src: url("vendor/fonts/Geist-Variable.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; }
-@font-face { font-family: "Geist Mono"; src: url("vendor/fonts/GeistMono-Variable.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; }
+@font-face { font-family: "Space Grotesk"; src: url("/vendor/fonts/space-grotesk-latin-400-normal.woff2") format("woff2"); font-weight: 400; font-display: swap; }
+@font-face { font-family: "Space Grotesk"; src: url("/vendor/fonts/space-grotesk-latin-700-normal.woff2") format("woff2"); font-weight: 700; font-display: swap; }
+@font-face { font-family: "Space Mono"; src: url("/vendor/fonts/space-mono-latin-400-normal.woff2") format("woff2"); font-weight: 400; font-display: swap; }
:root {
- --paper: oklch(98.4% 0.006 80);
- --surface: oklch(99.6% 0.003 80);
- --line: oklch(89% 0.008 75);
- --ink: oklch(26% 0.02 55);
- --ink-2: oklch(44% 0.018 55);
- --ink-3: oklch(58% 0.014 55);
- --accent: oklch(56% 0.168 34);
- --accent-hi: oklch(52% 0.18 33);
- --r: 10px;
- --font: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
- --mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+ --paper: oklch(92.8% 0.005 95); --card: oklch(98.4% 0.004 95); --tint: oklch(95.6% 0.005 95);
+ --line: oklch(86.8% 0.007 95); --mark: oklch(70% 0.006 95);
+ --ink: oklch(21% 0.01 95); --ink-soft: oklch(49% 0.008 95);
+ --accent: oklch(45% 0.23 267); --accent-2: oklch(38% 0.21 267); --accent-bg: oklch(45% 0.23 267 / 0.07); --on-accent: #fff;
+ --radius: 4px; --shadow: 0 1px 2px oklch(21% 0.01 95 / 0.05);
+ --sans: "Space Grotesk", system-ui, -apple-system, sans-serif;
+ --mono: "Space Mono", ui-monospace, Menlo, monospace;
}
[data-theme="dark"] {
- --paper: oklch(19% 0.008 60);
- --surface: oklch(22% 0.01 60);
- --line: oklch(30% 0.012 60);
- --ink: oklch(93% 0.006 75);
- --ink-2: oklch(72% 0.012 70);
- --ink-3: oklch(60% 0.012 65);
- --accent: oklch(70% 0.15 40);
- --accent-hi: oklch(76% 0.14 42);
+ --paper: oklch(17.5% 0.01 267); --card: oklch(21% 0.012 267); --tint: oklch(24% 0.014 267);
+ --line: oklch(31% 0.015 267); --mark: oklch(52% 0.012 267);
+ --ink: oklch(94% 0.005 95); --ink-soft: oklch(72% 0.01 95);
+ --accent: oklch(70% 0.15 267); --accent-2: oklch(79% 0.12 267); --accent-bg: oklch(70% 0.15 267 / 0.14); --on-accent: oklch(17% 0.03 267);
+ --shadow: 0 1px 2px oklch(0% 0 0 / 0.3);
}
-
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
-body {
- margin: 0; background: var(--paper); color: var(--ink);
- font-family: var(--font); font-size: 1rem; line-height: 1.62;
- -webkit-font-smoothing: antialiased;
-}
-
-.legal-head { border-bottom: 1px solid var(--line); background: var(--surface); }
-.legal-head .inner, main.legal { width: min(100% - 2.5rem, 46rem); margin-inline: auto; }
-.legal-head .inner { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.9rem 0; }
-.legal-head a.back { display: inline-flex; align-items: center; gap: 0.45rem; text-decoration: none; color: var(--ink); font-weight: 500; }
-.legal-head .wordmark { font-family: var(--mono); font-size: 1.02rem; letter-spacing: -0.03em; }
-.legal-head .wordmark b { color: var(--accent); }
-.legal-head nav { display: flex; gap: 1.25rem; font-size: 0.875rem; }
-.legal-head nav a { color: var(--ink-3); text-decoration: none; }
-.legal-head nav a:hover, .legal-head nav a[aria-current="page"] { color: var(--accent); }
-
-main.legal { padding: 3rem 0 5rem; }
-h1 { font-size: clamp(1.9rem, 1.5rem + 1.6vw, 2.5rem); letter-spacing: -0.03em; line-height: 1.12; margin: 0 0 1.5rem; }
-h2 { font-size: 1.0625rem; letter-spacing: -0.015em; margin: 2.4rem 0 0.6rem; }
-p { margin: 0 0 1em; color: var(--ink-2); }
-p.intro { color: var(--ink); }
-ul { padding-left: 1.15rem; margin: 0 0 1em; color: var(--ink-2); }
-li { margin-bottom: 0.32em; }
+body { margin: 0; background: var(--paper); color: var(--ink); font-family: var(--sans); font-size: 16px; line-height: 1.6;
+ -webkit-font-smoothing: antialiased; padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); }
a { color: var(--accent); text-underline-offset: 3px; }
-a:hover { color: var(--accent-hi); }
-:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 3px; }
-code, .mono { font-family: var(--mono); font-size: 0.92em; }
+a:hover { color: var(--accent-2); }
+:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
-table.legal-table { border-collapse: collapse; margin: 0 0 1em; }
-table.legal-table th { text-align: left; font-weight: 500; color: var(--ink-3); padding: 0.22rem 1.6rem 0.22rem 0; vertical-align: top; white-space: nowrap; font-size: 0.9375rem; }
-table.legal-table td { padding: 0.22rem 0; vertical-align: top; color: var(--ink-2); }
+.topbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; gap: 12px;
+ padding: calc(10px + env(safe-area-inset-top)) 16px 10px; border-bottom: 1px solid var(--line);
+ background: color-mix(in oklab, var(--card) 88%, transparent); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
+.brand { display: inline-flex; align-items: center; gap: 9px; color: var(--ink); text-decoration: none; font-weight: 700; font-size: 1.2rem; letter-spacing: -0.02em; }
+.brand:hover { color: var(--ink); }
+.brand svg { width: 22px; height: 22px; color: var(--accent); }
+.brand .two { color: var(--accent); }
+.topbar nav { margin-left: auto; display: flex; gap: 4px; flex-wrap: wrap; }
+.topbar nav a { min-height: 40px; display: inline-flex; align-items: center; padding: 0 12px; border-radius: 999px; color: var(--ink-soft); text-decoration: none; font-size: 0.9rem; }
+.topbar nav a:hover { color: var(--ink); background: var(--tint); }
+.topbar nav a[aria-current="page"] { color: var(--accent); background: var(--accent-bg); font-weight: 700; }
-.legal-foot { border-top: 1px solid var(--line); margin-top: 3rem; padding-top: 1.25rem; font-size: 0.875rem; color: var(--ink-3); }
-.legal-foot a { color: var(--ink-3); }
+main.legal { max-width: 760px; margin: 0 auto; padding: 32px 16px calc(64px + env(safe-area-inset-bottom)); }
+.mono-label { font-family: var(--mono); font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-soft); }
+h1 { font-size: clamp(1.9rem, 1.5rem + 1.6vw, 2.5rem); letter-spacing: -0.02em; line-height: 1.12; margin: 6px 0 20px; }
+.card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 8px 24px 20px; }
+h2 { font-size: 1.08rem; letter-spacing: -0.01em; margin: 1.8rem 0 0.5rem; }
+p { margin: 0 0 1em; color: var(--ink-soft); }
+p.intro { color: var(--ink); font-size: 1.05rem; margin-bottom: 20px; }
+ul { padding-left: 1.15rem; margin: 0 0 1em; color: var(--ink-soft); }
+li { margin-bottom: 0.3em; }
+code, .mono { font-family: var(--mono); font-size: 0.88em; }
+table.legal-table { border-collapse: collapse; margin: 0 0 1em; width: 100%; }
+table.legal-table th { text-align: left; font-weight: 400; color: var(--ink-soft); padding: 0.3rem 1.4rem 0.3rem 0; vertical-align: top; white-space: nowrap; font-family: var(--mono); font-size: 0.78rem; letter-spacing: .04em; }
+table.legal-table td { padding: 0.3rem 0; vertical-align: top; overflow-wrap: anywhere; }
+.legal-foot { margin-top: 24px; display: flex; gap: 8px 18px; flex-wrap: wrap; font-size: 0.9rem; }
+.legal-foot a { color: var(--ink-soft); text-decoration: none; padding: 6px 0; }
.legal-foot a:hover { color: var(--accent); }
-
-@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }
+@media (max-width: 560px) {
+ .card { padding: 4px 16px 16px; }
+ .topbar nav a { padding: 0 10px; font-size: 0.85rem; }
+ table.legal-table th, table.legal-table td { display: block; padding: 0; }
+ table.legal-table td { margin-bottom: 10px; }
+}
+@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: .001ms !important; animation-duration: .001ms !important; } }
+.card > :first-child { margin-top: 18px; }
diff --git a/manifest.webmanifest b/manifest.webmanifest
index a443a11..441edf8 100644
--- a/manifest.webmanifest
+++ b/manifest.webmanifest
@@ -1,13 +1,14 @@
{
"name": "clip2md — Paste to Markdown",
"short_name": "clip2md",
- "description": "Paste rich text or a web page and save it as clean Markdown in one keystroke. Client-side, private, offline-capable.",
- "start_url": ".",
- "scope": ".",
+ "description": "Paste rich text or a web page and save it as clean Markdown in one click. Client-side, private, offline-capable.",
+ "start_url": "/",
+ "scope": "/",
"display": "standalone",
- "background_color": "#faf7f2",
- "theme_color": "#faf7f2",
+ "background_color": "#EAEAE3",
+ "theme_color": "#EAEAE3",
"icons": [
- { "src": "icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" }
+ { "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" },
+ { "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "maskable" }
]
}
diff --git a/nginx.conf b/nginx.conf
index 0518722..2596cdb 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -38,7 +38,10 @@ server {
add_header Cache-Control "no-cache";
}
+ # Pages and icons revalidate on every load, so a deploy reaches users immediately.
location / {
+ include /etc/nginx/security-headers.conf;
+ add_header Cache-Control "no-cache";
try_files $uri $uri.html $uri/ /index.html;
}
}
diff --git a/sw.js b/sw.js
index d8183db..a10391e 100644
--- a/sw.js
+++ b/sw.js
@@ -1,19 +1,25 @@
-/* clip2md service worker — offline shell cache. Bump CACHE on any asset change. */
-var CACHE = 'clip2md-v3';
+/* clip2md service worker.
+ Pages (navigations, .html): network first, cache only as offline fallback —
+ so new or changed pages are never frozen for existing users.
+ Static assets (vendor, fonts, icons): cache first. Bump CACHE on asset changes. */
+var CACHE = 'clip2md-v4';
var ASSETS = [
- './',
- './index.html',
- './impressum.html',
- './datenschutz.html',
- './legal.css',
- './icon.svg',
- './manifest.webmanifest',
- './vendor/turndown.js',
- './vendor/turndown-plugin-gfm.js',
- './vendor/marked.umd.js',
- './vendor/purify.min.js',
- './vendor/fonts/Geist-Variable.woff2',
- './vendor/fonts/GeistMono-Variable.woff2'
+ '/',
+ '/index.html',
+ '/impressum.html',
+ '/datenschutz.html',
+ '/legal.css',
+ '/icon.svg',
+ '/manifest.webmanifest',
+ '/vendor/turndown.js',
+ '/vendor/turndown-plugin-gfm.js',
+ '/vendor/marked.umd.js',
+ '/vendor/purify.min.js',
+ '/vendor/fonts/space-grotesk-latin-400-normal.woff2',
+ '/vendor/fonts/space-grotesk-latin-500-normal.woff2',
+ '/vendor/fonts/space-grotesk-latin-700-normal.woff2',
+ '/vendor/fonts/space-mono-latin-400-normal.woff2',
+ '/vendor/fonts/space-mono-latin-700-normal.woff2'
];
self.addEventListener('install', function (e) {
@@ -29,18 +35,36 @@ self.addEventListener('activate', function (e) {
);
});
+function isPage(req, url) {
+ return req.mode === 'navigate' || req.destination === 'document' || /\.html$/.test(url.pathname) || url.pathname === '/';
+}
+
self.addEventListener('fetch', function (e) {
- if (e.request.method !== 'GET') return;
- e.respondWith(
- caches.match(e.request).then(function (hit) {
- return hit || fetch(e.request).then(function (res) {
- // Runtime-cache same-origin successful responses.
- if (res && res.ok && e.request.url.indexOf(self.location.origin) === 0) {
- var copy = res.clone();
- caches.open(CACHE).then(function (c) { c.put(e.request, copy); });
- }
+ var req = e.request;
+ if (req.method !== 'GET') return;
+ var url = new URL(req.url);
+ if (url.origin !== self.location.origin) return; // external links (PayPal, GitHub) untouched
+
+ if (isPage(req, url)) {
+ e.respondWith(
+ fetch(req).then(function (res) {
+ if (res && res.ok) { var copy = res.clone(); caches.open(CACHE).then(function (c) { c.put(req, copy); }); }
return res;
- }).catch(function () { return caches.match('./index.html'); });
+ }).catch(function () {
+ return caches.match(req).then(function (hit) {
+ return hit || caches.match(url.pathname.replace(/\/$/, '/index.html')) || caches.match('/index.html');
+ });
+ })
+ );
+ return;
+ }
+
+ e.respondWith(
+ caches.match(req).then(function (hit) {
+ return hit || fetch(req).then(function (res) {
+ if (res && res.ok) { var copy = res.clone(); caches.open(CACHE).then(function (c) { c.put(req, copy); }); }
+ return res;
+ });
})
);
});
diff --git a/vendor/fonts/Geist-Variable.woff2 b/vendor/fonts/Geist-Variable.woff2
deleted file mode 100644
index 5c5999d..0000000
Binary files a/vendor/fonts/Geist-Variable.woff2 and /dev/null differ
diff --git a/vendor/fonts/GeistMono-Variable.woff2 b/vendor/fonts/GeistMono-Variable.woff2
deleted file mode 100644
index efaf95d..0000000
Binary files a/vendor/fonts/GeistMono-Variable.woff2 and /dev/null differ
diff --git a/vendor/fonts/LICENSE-SpaceGrotesk.txt b/vendor/fonts/LICENSE-SpaceGrotesk.txt
new file mode 100644
index 0000000..d5666d7
--- /dev/null
+++ b/vendor/fonts/LICENSE-SpaceGrotesk.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/vendor/fonts/LICENSE-SpaceMono.txt b/vendor/fonts/LICENSE-SpaceMono.txt
new file mode 100644
index 0000000..50478a0
--- /dev/null
+++ b/vendor/fonts/LICENSE-SpaceMono.txt
@@ -0,0 +1,93 @@
+Copyright 2016 The Space Mono Project Authors (https://github.com/googlefonts/spacemono) SpaceMono-Italic.ttf: Copyright 2016 The Space Mono Project Authors (https://github.com/googlefonts/spacemono) SpaceMono-Bold.ttf: Copyright 2016 The Space Mono Project Authors (https://github.com/googlefonts/spacemono) SpaceMono-BoldItalic.ttf: Copyright 2016 The Space Mono Project Authors (https://github.com/googlefonts/spacemono)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/vendor/fonts/space-grotesk-latin-400-normal.woff2 b/vendor/fonts/space-grotesk-latin-400-normal.woff2
new file mode 100644
index 0000000..0e63471
Binary files /dev/null and b/vendor/fonts/space-grotesk-latin-400-normal.woff2 differ
diff --git a/vendor/fonts/space-grotesk-latin-500-normal.woff2 b/vendor/fonts/space-grotesk-latin-500-normal.woff2
new file mode 100644
index 0000000..0db251f
Binary files /dev/null and b/vendor/fonts/space-grotesk-latin-500-normal.woff2 differ
diff --git a/vendor/fonts/space-grotesk-latin-700-normal.woff2 b/vendor/fonts/space-grotesk-latin-700-normal.woff2
new file mode 100644
index 0000000..44604a0
Binary files /dev/null and b/vendor/fonts/space-grotesk-latin-700-normal.woff2 differ
diff --git a/vendor/fonts/space-mono-latin-400-normal.woff2 b/vendor/fonts/space-mono-latin-400-normal.woff2
new file mode 100644
index 0000000..2752f60
Binary files /dev/null and b/vendor/fonts/space-mono-latin-400-normal.woff2 differ
diff --git a/vendor/fonts/space-mono-latin-700-normal.woff2 b/vendor/fonts/space-mono-latin-700-normal.woff2
new file mode 100644
index 0000000..6563bad
Binary files /dev/null and b/vendor/fonts/space-mono-latin-700-normal.woff2 differ