snapshot
This commit is contained in:
@@ -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=="],
|
||||||
|
|||||||
@@ -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,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.
@@ -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}`}
|
||||||
|
|||||||
@@ -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, "&")
|
||||||
|
.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 `<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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user