This commit is contained in:
Stefan ATZLESBERGER
2026-04-21 10:34:31 +02:00
parent 24b37821dc
commit a8cc169816
9 changed files with 163 additions and 24 deletions
+3
View File
@@ -12,6 +12,7 @@
"bun-plugin-tailwind": "^0.1.2", "bun-plugin-tailwind": "^0.1.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"highlight.js": "^11.11.1",
"lucide-react": "^0.545.0", "lucide-react": "^0.545.0",
"marked": "^18.0.0", "marked": "^18.0.0",
"react": "^19", "react": "^19",
@@ -144,6 +145,8 @@
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], "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=="], "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=="], "marked": ["marked@18.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-2e7Qiv/HJSXj8rDEpgTvGKsP8yYtI9xXHKDnrftrmnrJPaFNM7VRb2YCzWaX4BP1iCJ/XPduzDJZMFoqTCcIMA=="],
+1
View File
@@ -16,6 +16,7 @@
"bun-plugin-tailwind": "^0.1.2", "bun-plugin-tailwind": "^0.1.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"highlight.js": "^11.11.1",
"lucide-react": "^0.545.0", "lucide-react": "^0.545.0",
"marked": "^18.0.0", "marked": "^18.0.0",
"react": "^19", "react": "^19",
+1
View File
@@ -1,4 +1,5 @@
import { NotesApp } from "@/features/notes/NotesApp"; import { NotesApp } from "@/features/notes/NotesApp";
import "highlight.js/styles/github.css";
import "./index.css"; import "./index.css";
export function App() { export function App() {
Binary file not shown.
+1 -1
View File
@@ -157,7 +157,7 @@ export function SnippetCard({ snippet, treeColorByName, treeDisplayByName, onRep
</div> </div>
</CardContent> </CardContent>
<CardFooter className="flex flex-row flex-wrap items-center gap-2 border-t border-border/80 px-3 py-1"> <CardFooter className="flex flex-row flex-wrap items-center gap-2 border-t border-border/80 px-3 py-1">
<div className="flex min-h-7 min-w-0 flex-1 flex-wrap items-center gap-1"> <div className="snippet-tag-cloud flex min-h-7 min-w-0 flex-1 flex-wrap items-center gap-1.5">
{snippet.tags.length ? ( {snippet.tags.length ? (
snippet.tags.map((t, i) => ( snippet.tags.map((t, i) => (
<TagPill <TagPill
@@ -117,7 +117,7 @@ export function SnippetEditDialog({
onPick={name => applyTagSuggestion(name)} onPick={name => applyTagSuggestion(name)}
/> />
</div> </div>
<div className="flex min-h-[1.5rem] flex-wrap gap-1.5"> <div className="snippet-tag-cloud flex min-h-[1.5rem] flex-wrap gap-1.5">
{tagsFromString(tagsStr).map((t, i) => ( {tagsFromString(tagsStr).map((t, i) => (
<TagPill <TagPill
key={`${i}-${t}`} key={`${i}-${t}`}
+25 -9
View File
@@ -1,3 +1,7 @@
import { Hash } from "lucide-react";
import type { CSSProperties } from "react";
import { cn } from "@/lib/utils";
type Props = { type Props = {
tagKey: string; tagKey: string;
label: string; label: string;
@@ -8,22 +12,34 @@ export function TagPill({ tagKey, label, hex }: Props) {
const has = Boolean(hex && /^#[0-9A-Fa-f]{6}$/i.test(hex)); const has = Boolean(hex && /^#[0-9A-Fa-f]{6}$/i.test(hex));
return ( return (
<span <span
className={ className={cn(
"tag-pill inline-flex max-w-full items-center gap-1 rounded-lg border px-2 py-0.5 text-[11px] font-medium leading-tight shadow-sm ring-1 ring-black/[0.04] transition-[box-shadow,transform,background-color] duration-150 dark:ring-white/[0.06]",
has has
? "inline-flex max-w-full items-center rounded-full border-2 px-1.5 py-px text-[11px] font-medium leading-tight" ? "shadow-[0_1px_2px_rgba(0,0,0,0.06)] dark:shadow-[0_1px_2px_rgba(0,0,0,0.35)]"
: "inline-flex max-w-full items-center rounded-full border-2 border-border bg-muted/40 px-1.5 py-px text-[11px] leading-tight text-muted-foreground" : "border-border/70 bg-gradient-to-b from-muted/90 to-muted/45 text-muted-foreground dark:from-muted/50 dark:to-muted/25",
} )}
style={ style={
has has
? { ? ({
borderColor: hex!, borderColor: `${hex}cc`,
backgroundColor: `${hex}33`, background: `linear-gradient(165deg, color-mix(in oklch, ${hex} 28%, transparent) 0%, color-mix(in oklch, ${hex} 12%, transparent) 100%)`,
} boxShadow: `0 1px 2px color-mix(in oklch, ${hex} 35%, transparent), inset 0 1px 0 color-mix(in oklch, white 22%, transparent)`,
color: `color-mix(in oklch, ${hex} 12%, var(--foreground))`,
} as CSSProperties)
: undefined : undefined
} }
title={tagKey === label ? tagKey : `${label} (${tagKey})`} title={tagKey === label ? tagKey : `${label} (${tagKey})`}
> >
<span className="truncate">{label}</span> {has ? (
<span
className="size-1.5 shrink-0 rounded-full ring-1 ring-black/10 dark:ring-white/15"
style={{ backgroundColor: hex! }}
aria-hidden
/>
) : (
<Hash className="size-2.5 shrink-0 opacity-55" aria-hidden />
)}
<span className="min-w-0 truncate">{label}</span>
</span> </span>
); );
} }
@@ -1,7 +1,69 @@
import hljs from "highlight.js/lib/common";
import { marked } from "marked"; import { marked } from "marked";
marked.setOptions({ gfm: true }); marked.setOptions({ gfm: true });
/** Markdown-Sprachkennungen → highlight.js-Name */
const LANG_ALIASES: Record<string, string> = {
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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 `<pre class="snippet-pre"><code class="hljs language-${langClassSegment(use)}">${highlighted}</code></pre>\n`;
} catch {
return `<pre class="snippet-pre"><code class="hljs language-plaintext">${escapeHtml(text)}</code></pre>\n`;
}
},
},
});
}
ensureMarkedHighlighter();
export function parseSnippetMarkdown(md: string): string { export function parseSnippetMarkdown(md: string): string {
return marked.parse(md) as string; return marked.parse(md) as string;
} }
+69 -13
View File
@@ -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 😎 */ /* cool Bun background animation 😎 */
body::before { body::before {
content: ""; content: "";
@@ -53,8 +62,8 @@ body::before {
/* Markdown rendering (marked HTML) */ /* Markdown rendering (marked HTML) */
@layer components { @layer components {
.snippet-md { .snippet-md {
font-size: 0.875rem; font-size: 0.8rem;
line-height: 1.625; font-family: 'Inter';
} }
.snippet-md :where(p) { .snippet-md :where(p) {
@@ -79,14 +88,14 @@ body::before {
color: var(--muted-foreground); color: var(--muted-foreground);
} }
.snippet-md :where(code) { .snippet-md :where(code:not(.hljs)) {
border-radius: var(--radius-sm, 0.25rem); border-radius: var(--radius-sm, 0.25rem);
background-color: var(--muted); background-color: var(--muted);
padding: 0.125rem 0.25rem; 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; margin-block: 0.5rem;
overflow-x: auto; overflow-x: auto;
border-radius: var(--radius-md, 0.375rem); border-radius: var(--radius-md, 0.375rem);
@@ -94,27 +103,69 @@ body::before {
padding: 0.75rem; padding: 0.75rem;
} }
.snippet-md :where(pre code) { .snippet-md :where(pre:not(.snippet-pre) code) {
background-color: transparent; background-color: transparent;
padding: 0; 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) { .snippet-md :where(h1) {
margin-block: 0 0.5rem; margin-block: 0 0.5rem;
font-size: 1.25rem; font-size: 0.8rem;
font-weight: 600; font-weight: 600;
} }
.snippet-md :where(h2) { .snippet-md :where(h2) {
margin-block: 0.75rem 0.5rem; margin-block: 0.75rem 0.5rem;
font-size: 1.125rem; font-size: 0.8rem;
font-weight: 600; font-weight: 600;
} }
.snippet-md :where(h3) { .snippet-md :where(h3) {
margin-block: 0.5rem 0.25rem; margin-block: 0.5rem 0.25rem;
font-size: 1rem; font-size: 0.8rem;
font-weight: 600; font-weight: 600;
} }
@@ -140,7 +191,7 @@ body::before {
.snippet-md :where(table) { .snippet-md :where(table) {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-size: 0.8125rem; font-size: 0.8rem;
} }
.snippet-md :where(th, td) { .snippet-md :where(th, td) {
@@ -161,7 +212,7 @@ body::before {
/* Snippet-Karten: etwas dichter, Fokus auf Fließtext */ /* Snippet-Karten: etwas dichter, Fokus auf Fließtext */
.snippet-md-card-wrap .snippet-md { .snippet-md-card-wrap .snippet-md {
font-size: 0.8125rem; font-size: 0.8rem;
line-height: 1.55; line-height: 1.55;
} }
@@ -182,11 +233,16 @@ body::before {
margin-block-end: 0.25rem; 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; margin-block: 0.375rem;
padding: 0.5rem; 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) */ /* App dialogs: shared layout (Tailwind here, short class names in TSX) */
.dialog-snippet-header { .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; @apply shrink-0 space-y-1 border-b px-5 pb-3 pt-5 pr-12 text-start sm:px-6 sm:pr-14;