diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..3052c83
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,51 @@
+# 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.
+
+## Color (OKLCH)
+
+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.
diff --git a/PRODUCT.md b/PRODUCT.md
new file mode 100644
index 0000000..107680e
--- /dev/null
+++ b/PRODUCT.md
@@ -0,0 +1,35 @@
+# clip2md — Product Context
+
+**register: product**
+
+## Product Purpose
+
+Ein winziges, self-hosted Utility: langen oder formatierten Text (Reddit-Thread, Website,
+Artikel) reinpasten und in unter zwei Sekunden als sauberes `.md` speichern — ohne Umwege,
+ohne Login, ohne Server-Roundtrip. Ein einziger Screen, tastaturgetrieben.
+
+## Users
+
+Till (Heidrich Digital) und Team. Power-User, die schnell Web-Inhalte in ihr Markdown-Archiv
+(Obsidian, Outline, Repos) überführen. Nutzung am Desktop, oft nebenbei zwischen Lesen und
+Ablegen. Erwartung: reagiert sofort, kein Nachdenken, Keyboard-first (`⌘V`, `⌘S`).
+
+## Brand & Tone
+
+Heidrich Digital: ruhig, präzise, editorial-minimalistisch, anti-AI-slop. Deutsch, sachlich,
+auf Augenhöhe. Kein Marketing-Geschrei, keine Emojis im UI. Das Tool soll sich wie ein
+scharfes, wertiges Werkzeug anfühlen, nicht wie ein SaaS-Dashboard.
+
+## Strategic Principles
+
+- **Frictionless.** Der schnellste Weg von Zwischenablage zu `.md`. Jeder Klick, der wegfällt, ist ein Gewinn.
+- **Clientseitig & privat.** Nichts verlässt den Browser. Kein CDN, keine Third-Party-Requests (DSGVO-sauber, offline-fähig).
+- **Keyboard-first.** Maus optional. `⌘V` konvertiert, `⌘S` speichert.
+- **Ehrliches Feedback.** Der Nutzer sieht, was passiert ist (konvertiert vs. roh), ohne dass es aufdringlich wird.
+
+## Anti-References
+
+- Generische SaaS-Cards mit Icon + Heading + Text.
+- Hero-Metric-Kacheln, Gradient-Text, Glassmorphism.
+- Überladene Toolbars. Dieses Tool hat genau die Knöpfe, die es braucht.
+- Bunte „freundliche" Farbverläufe. Ruhe schlägt Effekt.
diff --git a/index.html b/index.html
index 0d2c2a1..6c41d7f 100644
--- a/index.html
+++ b/index.html
@@ -3,240 +3,419 @@
-
+
+
+
clip2md — Paste to Markdown
-
clip2md
- Reinpasten → ein Klick → als .md speichern.
+
m↓
+
clip2md
+ reinpasten → .md
- Speichern ⌘/Strg + S · Roh einfügen ⌘/Strg + ⇧ + V
+
-
-
-
+
+
+
+
+
+
+
+
Reinpasten
+
Langen Reddit-Thread, eine Website oder formatierten Text hier einfügen.
+ Formatiertes HTML wird automatisch zu sauberem Markdown, reiner Text bleibt roh.
+
⌘Veinfügen · ⌘Sspeichern
+
+
-
+
@@ -259,17 +438,20 @@ Dann ⌘/Strg+S oder „Speichern“.">
turndown.use(window.turndownPluginGfm.gfm);
}
} catch (e) { /* gfm optional */ }
- // Drop noise that clutters pasted web pages.
turndown.remove(["script", "style", "noscript", "iframe", "svg"]);
var editor = document.getElementById("editor");
+ var main = document.getElementById("main");
var filename = document.getElementById("filename");
+ var fnameBox = document.getElementById("fnameBox");
+ var fnameTag = document.getElementById("fnameTag");
var wordsEl = document.getElementById("words");
var charsEl = document.getElementById("chars");
- var srcInfo = document.getElementById("srcInfo");
+ var srcBadge = document.getElementById("srcBadge");
+ var srcText = document.getElementById("srcText");
var toastEl = document.getElementById("toast");
- var filenameEdited = false; // becomes true once the user types in the field
+ var filenameEdited = false;
var toastTimer = null;
// ---- Helpers --------------------------------------------------------
@@ -291,21 +473,17 @@ Dann ⌘/Strg+S oder „Speichern“.">
function slugify(s) {
if (!s) return "";
s = s.replace(/[äöüßÄÖÜ]/g, function (c) { return UMLAUT[c] || c; });
- s = s.normalize("NFKD").replace(/[̀-ͯ]/g, ""); // strip accents
- s = s.toLowerCase()
- .replace(/[^a-z0-9]+/g, "-")
- .replace(/^-+|-+$/g, "");
+ s = s.normalize("NFKD").replace(/[̀-ͯ]/g, "");
+ s = s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
return s.slice(0, 60).replace(/-+$/g, "");
}
function deriveTitle(md) {
var lines = md.split(/\r?\n/);
- // Prefer the first ATX heading.
for (var i = 0; i < lines.length; i++) {
var m = lines[i].match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/);
if (m && m[1].trim()) return m[1].trim();
}
- // Otherwise the first non-empty, non-decorative line.
for (var j = 0; j < lines.length; j++) {
var t = lines[j].trim();
if (!t) continue;
@@ -320,9 +498,22 @@ Dann ⌘/Strg+S oder „Speichern“.">
return today() + (slug ? "-" + slug : "-clip");
}
- function refreshFilename() {
+ function refreshFilename(flash) {
if (filenameEdited) return;
- filename.value = computeFilename(editor.value);
+ var next = computeFilename(editor.value);
+ if (next !== filename.value) {
+ filename.value = next;
+ if (flash) {
+ fnameBox.classList.remove("flash");
+ void fnameBox.offsetWidth; // reflow to restart animation
+ fnameBox.classList.add("flash");
+ }
+ }
+ }
+
+ function setFnameTag() {
+ if (filenameEdited) { fnameTag.textContent = "eigen"; fnameTag.classList.remove("auto"); }
+ else { fnameTag.textContent = "auto"; fnameTag.classList.add("auto"); }
}
function updateCounts() {
@@ -332,63 +523,81 @@ Dann ⌘/Strg+S oder „Speichern“.">
wordsEl.textContent = w.toLocaleString("de-DE");
}
- function refreshAll() { updateCounts(); refreshFilename(); }
+ function updateEmpty() {
+ main.classList.toggle("has-content", editor.value.length > 0);
+ }
+
+ function showSource(text, ok, pulse) {
+ srcText.textContent = text;
+ srcBadge.classList.toggle("ok", !!ok);
+ srcBadge.classList.add("show");
+ if (pulse) {
+ srcBadge.classList.remove("pulse");
+ void srcBadge.offsetWidth;
+ srcBadge.classList.add("pulse");
+ }
+ }
+ function hideSource() { srcBadge.classList.remove("show", "ok", "pulse"); }
+
+ function refreshAll(flash) { updateCounts(); refreshFilename(flash); updateEmpty(); }
// ---- Smart paste ----------------------------------------------------
- // Cmd/Ctrl+Shift+V forces a raw (plain-text) paste.
var forceRaw = false;
editor.addEventListener("paste", function (e) {
var cd = e.clipboardData || window.clipboardData;
- if (!cd) return; // let the browser handle it
+ if (!cd) return;
var html = cd.getData("text/html");
var text = cd.getData("text/plain");
- var out, source;
+ var out, badge, ok;
if (!forceRaw && html && html.trim()) {
try {
out = turndown.turndown(html).replace(/\n{3,}/g, "\n\n").trim();
- source = "HTML → Markdown konvertiert";
+ badge = "HTML → Markdown konvertiert"; ok = true;
} catch (err) {
- out = text; source = "Roh eingefügt (Konvertierung fehlgeschlagen)";
+ out = text; badge = "Roh eingefügt (Konvertierung fehlgeschlagen)"; ok = false;
}
} else {
out = text;
- source = forceRaw ? "Roh eingefügt (erzwungen)" : "Roh eingefügt (kein HTML)";
+ badge = forceRaw ? "Roh eingefügt (erzwungen)" : "Roh eingefügt";
+ ok = false;
}
forceRaw = false;
if (out == null) return;
e.preventDefault();
insertAtCursor(out);
- srcInfo.textContent = source;
- refreshAll();
- toast(source);
+ showSource(badge, ok, true);
+ refreshAll(true);
});
function insertAtCursor(str) {
var start = editor.selectionStart, end = editor.selectionEnd;
- var before = editor.value.slice(0, start);
- var after = editor.value.slice(end);
- editor.value = before + str + after;
+ editor.value = editor.value.slice(0, start) + str + editor.value.slice(end);
var pos = start + str.length;
editor.selectionStart = editor.selectionEnd = pos;
}
editor.addEventListener("input", function () {
- srcInfo.textContent = "";
- refreshAll();
+ hideSource();
+ refreshAll(false);
});
// ---- Filename field -------------------------------------------------
filename.addEventListener("input", function () {
filenameEdited = filename.value.trim().length > 0;
+ setFnameTag();
+ });
+ // Empty field → hand control back to auto.
+ filename.addEventListener("blur", function () {
+ if (!filename.value.trim()) { filenameEdited = false; setFnameTag(); refreshFilename(false); }
});
// ---- Save -----------------------------------------------------------
function save() {
var content = editor.value;
- if (!content.trim()) { toast("Nichts zu speichern.", true); return; }
+ if (!content.trim()) { toast("Nichts zu speichern.", true); flashEditor(); return; }
var name = (filename.value.trim() || computeFilename(content));
name = name.replace(/[\/\\:*?"<>|]+/g, "-").replace(/\s+/g, "-");
if (!/\.md$/i.test(name)) name += ".md";
@@ -396,15 +605,15 @@ Dann ⌘/Strg+S oder „Speichern“.">
var blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
- a.href = url;
- a.download = name;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
+ a.href = url; a.download = name;
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
- toast("Gespeichert: " + name);
+ toast("Gespeichert · " + name);
+ }
+ function flashEditor() {
+ editor.style.borderColor = "var(--danger)";
+ setTimeout(function () { editor.style.borderColor = ""; }, 500);
}
-
document.getElementById("save").addEventListener("click", save);
// ---- Copy -----------------------------------------------------------
@@ -430,8 +639,7 @@ Dann ⌘/Strg+S oder „Speichern“.">
if (editor.value && !confirm("Editor wirklich leeren?")) return;
editor.value = "";
filenameEdited = false;
- srcInfo.textContent = "";
- refreshAll();
+ hideSource(); setFnameTag(); refreshAll(false);
editor.focus();
});
@@ -441,14 +649,14 @@ Dann ⌘/Strg+S oder „Speichern“.">
if (mod && !e.shiftKey && !e.altKey && (e.key === "s" || e.key === "S")) {
e.preventDefault(); save(); return;
}
- // Arm a raw paste for the next paste event.
if (mod && e.shiftKey && (e.key === "v" || e.key === "V")) {
- forceRaw = true; // paste handler consumes and resets it
+ forceRaw = true;
}
});
// ---- Init -----------------------------------------------------------
- refreshAll();
+ setFnameTag();
+ refreshAll(false);
editor.focus();
})();