diff --git a/bun.lock b/bun.lock index 5993929..222f77f 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "bun-plugin-tailwind": "^0.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "highlight.js": "^11.11.1", "lucide-react": "^0.545.0", "marked": "^18.0.0", "react": "^19", @@ -144,6 +145,8 @@ "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], + "lucide-react": ["lucide-react@0.545.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw=="], "marked": ["marked@18.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-2e7Qiv/HJSXj8rDEpgTvGKsP8yYtI9xXHKDnrftrmnrJPaFNM7VRb2YCzWaX4BP1iCJ/XPduzDJZMFoqTCcIMA=="], diff --git a/package.json b/package.json index cdc57bf..6adab37 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "bun-plugin-tailwind": "^0.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "highlight.js": "^11.11.1", "lucide-react": "^0.545.0", "marked": "^18.0.0", "react": "^19", diff --git a/src/App.tsx b/src/App.tsx index d073d32..677808a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { NotesApp } from "@/features/notes/NotesApp"; +import "highlight.js/styles/github.css"; import "./index.css"; export function App() { diff --git a/src/assets/fonts/Inter-Variable.ttf b/src/assets/fonts/Inter-Variable.ttf new file mode 100644 index 0000000..e31b51e Binary files /dev/null and b/src/assets/fonts/Inter-Variable.ttf differ diff --git a/src/features/snippets/SnippetCard.tsx b/src/features/snippets/SnippetCard.tsx index 5e5c11f..17ea97c 100644 --- a/src/features/snippets/SnippetCard.tsx +++ b/src/features/snippets/SnippetCard.tsx @@ -157,7 +157,7 @@ export function SnippetCard({ snippet, treeColorByName, treeDisplayByName, onRep -
+
{snippet.tags.length ? ( snippet.tags.map((t, i) => ( applyTagSuggestion(name)} />
-
+
{tagsFromString(tagsStr).map((t, i) => ( - {label} + {has ? ( + + ) : ( + + )} + {label} ); } diff --git a/src/features/snippets/lib/parseSnippetMarkdown.ts b/src/features/snippets/lib/parseSnippetMarkdown.ts index ced6b5f..fcc09fd 100644 --- a/src/features/snippets/lib/parseSnippetMarkdown.ts +++ b/src/features/snippets/lib/parseSnippetMarkdown.ts @@ -1,7 +1,69 @@ +import hljs from "highlight.js/lib/common"; import { marked } from "marked"; marked.setOptions({ gfm: true }); +/** Markdown-Sprachkennungen → highlight.js-Name */ +const LANG_ALIASES: Record = { + ts: "typescript", + tsx: "typescript", + js: "javascript", + mjs: "javascript", + cjs: "javascript", + sh: "bash", + zsh: "bash", + yml: "yaml", + md: "markdown", + rs: "rust", + py: "python", + rb: "ruby", + kt: "kotlin", + kts: "kotlin", + text: "plaintext", + txt: "plaintext", +}; + +function resolveLang(raw: string | undefined): string { + const t = (raw ?? "").trim().toLowerCase(); + if (!t) return "plaintext"; + return LANG_ALIASES[t] ?? t; +} + +function langClassSegment(lang: string): string { + return lang.replace(/[^a-z0-9_-]/gi, "-") || "plaintext"; +} + +function escapeHtml(s: string): string { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +let hljsConfigured = false; + +function ensureMarkedHighlighter(): void { + if (hljsConfigured) return; + hljsConfigured = true; + marked.use({ + renderer: { + code({ text, lang }) { + const key = resolveLang(lang); + const use = hljs.getLanguage(key) ? key : "plaintext"; + try { + const highlighted = hljs.highlight(text, { language: use, ignoreIllegals: true }).value; + return `
${highlighted}
\n`; + } catch { + return `
${escapeHtml(text)}
\n`; + } + }, + }, + }); +} + +ensureMarkedHighlighter(); + export function parseSnippetMarkdown(md: string): string { return marked.parse(md) as string; } diff --git a/src/index.css b/src/index.css index 61d2ade..3fd9796 100644 --- a/src/index.css +++ b/src/index.css @@ -10,6 +10,15 @@ } } +@font-face { + font-family: 'Inter'; + src: url('./assets/fonts/Inter-Variable.ttf') format('truetype'); + font-style: normal; + font-weight: normal; + font-display: swap; +} + + /* cool Bun background animation 😎 */ body::before { content: ""; @@ -53,8 +62,8 @@ body::before { /* Markdown rendering (marked HTML) */ @layer components { .snippet-md { - font-size: 0.875rem; - line-height: 1.625; + font-size: 0.8rem; + font-family: 'Inter'; } .snippet-md :where(p) { @@ -79,14 +88,14 @@ body::before { color: var(--muted-foreground); } - .snippet-md :where(code) { + .snippet-md :where(code:not(.hljs)) { border-radius: var(--radius-sm, 0.25rem); background-color: var(--muted); padding: 0.125rem 0.25rem; - font-size: 0.875em; + font-size: 0.8rem; } - .snippet-md :where(pre) { + .snippet-md :where(pre:not(.snippet-pre)) { margin-block: 0.5rem; overflow-x: auto; border-radius: var(--radius-md, 0.375rem); @@ -94,27 +103,69 @@ body::before { padding: 0.75rem; } - .snippet-md :where(pre code) { + .snippet-md :where(pre:not(.snippet-pre) code) { background-color: transparent; padding: 0; - font-size: 0.8125rem; + font-size: 0.8rem; + } + + /* Highlight.js Codeblöcke */ + .snippet-md :where(pre.snippet-pre) { + margin-block: 0.5rem; + overflow-x: auto; + border-radius: var(--radius-md, 0.375rem); + border: 1px solid var(--border); + background-color: color-mix(in oklch, var(--muted) 88%, var(--background)); + padding: 0.65rem 0.75rem; + box-shadow: inset 0 1px 0 color-mix(in oklch, white 55%, transparent); + } + + .dark .snippet-md :where(pre.snippet-pre) { + box-shadow: inset 0 1px 0 color-mix(in oklch, white 8%, transparent); + } + + .snippet-md :where(pre.snippet-pre code.hljs) { + display: block; + width: max-content; + min-width: 100%; + padding: 0; + font-size: 0.8rem; + line-height: 1.55; + tab-size: 2; + background: transparent !important; + } + + .snippet-tag-cloud { + border-radius: 0.75rem; + border: 1px solid color-mix(in oklch, var(--border) 85%, transparent); + background: linear-gradient( + 165deg, + color-mix(in oklch, var(--muted) 55%, transparent) 0%, + color-mix(in oklch, var(--muted) 22%, transparent) 100% + ); + padding: 0.4rem 0.5rem; + box-shadow: inset 0 1px 0 color-mix(in oklch, white 40%, transparent); + } + + .dark .snippet-tag-cloud { + box-shadow: inset 0 1px 0 color-mix(in oklch, white 6%, transparent); } .snippet-md :where(h1) { margin-block: 0 0.5rem; - font-size: 1.25rem; + font-size: 0.8rem; font-weight: 600; } .snippet-md :where(h2) { margin-block: 0.75rem 0.5rem; - font-size: 1.125rem; + font-size: 0.8rem; font-weight: 600; } .snippet-md :where(h3) { margin-block: 0.5rem 0.25rem; - font-size: 1rem; + font-size: 0.8rem; font-weight: 600; } @@ -140,7 +191,7 @@ body::before { .snippet-md :where(table) { width: 100%; border-collapse: collapse; - font-size: 0.8125rem; + font-size: 0.8rem; } .snippet-md :where(th, td) { @@ -161,7 +212,7 @@ body::before { /* Snippet-Karten: etwas dichter, Fokus auf Fließtext */ .snippet-md-card-wrap .snippet-md { - font-size: 0.8125rem; + font-size: 0.8rem; line-height: 1.55; } @@ -182,11 +233,16 @@ body::before { margin-block-end: 0.25rem; } - .snippet-md-card-wrap .snippet-md :where(pre) { + .snippet-md-card-wrap .snippet-md :where(pre:not(.snippet-pre)) { margin-block: 0.375rem; padding: 0.5rem; } + .snippet-md-card-wrap .snippet-md :where(pre.snippet-pre) { + margin-block: 0.375rem; + padding: 0.45rem 0.55rem; + } + /* App dialogs: shared layout (Tailwind here, short class names in TSX) */ .dialog-snippet-header { @apply shrink-0 space-y-1 border-b px-5 pb-3 pt-5 pr-12 text-start sm:px-6 sm:pr-14;