diff --git a/content b/content index c105c2a..a859f93 160000 --- a/content +++ b/content @@ -1 +1 @@ -Subproject commit c105c2a14291e51933775ba0147a3ce0816658c8 +Subproject commit a859f93327991e27681e36728d4c492d5247d077 diff --git a/contentlayer.config.ts b/contentlayer.config.ts index 18552e0..9774af4 100644 --- a/contentlayer.config.ts +++ b/contentlayer.config.ts @@ -4,6 +4,7 @@ import rehypeSlug from 'rehype-slug'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import remarkGfm from 'remark-gfm'; import rehypePrettyCode from 'rehype-pretty-code'; +import { rehypeCallouts } from './lib/rehype-callouts'; export const Post = defineDocumentType(() => ({ name: 'Post', @@ -97,6 +98,7 @@ export default makeSource({ keepBackground: false, }, ], + rehypeCallouts, rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }], /** diff --git a/lib/rehype-callouts.ts b/lib/rehype-callouts.ts new file mode 100644 index 0000000..f3fd5c4 --- /dev/null +++ b/lib/rehype-callouts.ts @@ -0,0 +1,81 @@ +import { visit } from 'unist-util-visit'; + +/** + * Rehype plugin to transform GitHub-style blockquote alerts + * Transforms: > [!NOTE] into styled callout boxes + */ +export function rehypeCallouts() { + return (tree: any) => { + visit(tree, 'element', (node, index, parent) => { + // Only process blockquotes + if (node.tagName !== 'blockquote') return; + + // Check if first child is a paragraph + if (!node.children || node.children.length === 0) return; + const firstChild = node.children[0]; + if (firstChild.tagName !== 'p') return; + + // Check if paragraph starts with [!TYPE] + if (!firstChild.children || firstChild.children.length === 0) return; + const firstText = firstChild.children[0]; + if (firstText.type !== 'text') return; + + const match = firstText.value.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/i); + if (!match) return; + + const type = match[0].replace(/^\[!|\]\s*/g, '').toLowerCase(); + + // Remove the [!TYPE] marker from the text + firstText.value = firstText.value.replace(match[0], ''); + + // If the text node is now empty, remove it + if (!firstText.value.trim()) { + firstChild.children.shift(); + } + + // Add callout data attributes and classes + node.properties = node.properties || {}; + node.properties.className = ['callout', `callout-${type}`]; + node.properties['data-callout'] = type; + + // Add icon element at the beginning + const iconMap: Record = { + note: '📝', + tip: '💡', + important: '❗', + warning: '⚠️', + caution: '🚨', + }; + + const icon = { + type: 'element', + tagName: 'div', + properties: { className: ['callout-icon'] }, + children: [{ type: 'text', value: iconMap[type] || '📝' }], + }; + + const title = { + type: 'element', + tagName: 'div', + properties: { className: ['callout-title'] }, + children: [{ type: 'text', value: type.toUpperCase() }], + }; + + const header = { + type: 'element', + tagName: 'div', + properties: { className: ['callout-header'] }, + children: [icon, title], + }; + + const content = { + type: 'element', + tagName: 'div', + properties: { className: ['callout-content'] }, + children: node.children, + }; + + node.children = [header, content]; + }); + }; +} diff --git a/package-lock.json b/package-lock.json index 877543e..b1a749a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "rehype-autolink-headings": "^7.1.0", "rehype-pretty-code": "^0.14.1", "rehype-slug": "^6.0.0", + "remark-directive": "^4.0.0", "remark-gfm": "^4.0.1", "shiki": "^3.15.0", "tailwind-merge": "^3.4.0", @@ -7355,6 +7356,27 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -7804,6 +7826,25 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-extension-frontmatter": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", @@ -9712,6 +9753,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-4.0.0.tgz", + "integrity": "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-frontmatter": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", diff --git a/package.json b/package.json index 8029783..571dec9 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "rehype-autolink-headings": "^7.1.0", "rehype-pretty-code": "^0.14.1", "rehype-slug": "^6.0.0", + "remark-directive": "^4.0.0", "remark-gfm": "^4.0.1", "shiki": "^3.15.0", "tailwind-merge": "^3.4.0", diff --git a/styles/globals.css b/styles/globals.css index 2025e90..ab8b81d 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -425,4 +425,112 @@ body { .prose [data-rehype-pretty-code-title] + pre { @apply mt-0 rounded-t-none; +} + +/* GitHub-style Callouts/Alerts */ +.prose .callout { + @apply my-6 rounded-lg border-l-4 p-4 shadow-sm; + background: linear-gradient(135deg, var(--callout-bg-start), var(--callout-bg-end)); +} + +.prose .callout-header { + @apply mb-3 flex items-center gap-2; +} + +.prose .callout-icon { + @apply text-2xl; + line-height: 1; +} + +.prose .callout-title { + @apply text-sm font-bold uppercase tracking-wider; + color: var(--callout-title-color); + letter-spacing: 0.05em; +} + +.prose .callout-content { + @apply text-sm leading-relaxed; +} + +.prose .callout-content > *:first-child { + @apply mt-0; +} + +.prose .callout-content > *:last-child { + @apply mb-0; +} + +/* NOTE - Blue */ +.prose .callout-note { + --callout-bg-start: rgba(59, 130, 246, 0.08); + --callout-bg-end: rgba(59, 130, 246, 0.04); + --callout-title-color: #2563eb; + @apply border-blue-500; +} + +.dark .prose .callout-note { + --callout-bg-start: rgba(96, 165, 250, 0.12); + --callout-bg-end: rgba(96, 165, 250, 0.06); + --callout-title-color: #93c5fd; + @apply border-blue-400; +} + +/* TIP - Green */ +.prose .callout-tip { + --callout-bg-start: rgba(34, 197, 94, 0.08); + --callout-bg-end: rgba(34, 197, 94, 0.04); + --callout-title-color: #16a34a; + @apply border-green-500; +} + +.dark .prose .callout-tip { + --callout-bg-start: rgba(74, 222, 128, 0.12); + --callout-bg-end: rgba(74, 222, 128, 0.06); + --callout-title-color: #86efac; + @apply border-green-400; +} + +/* IMPORTANT - Purple */ +.prose .callout-important { + --callout-bg-start: rgba(168, 85, 247, 0.08); + --callout-bg-end: rgba(168, 85, 247, 0.04); + --callout-title-color: #9333ea; + @apply border-purple-500; +} + +.dark .prose .callout-important { + --callout-bg-start: rgba(192, 132, 252, 0.12); + --callout-bg-end: rgba(192, 132, 252, 0.06); + --callout-title-color: #c084fc; + @apply border-purple-400; +} + +/* WARNING - Orange/Yellow */ +.prose .callout-warning { + --callout-bg-start: rgba(251, 191, 36, 0.08); + --callout-bg-end: rgba(251, 191, 36, 0.04); + --callout-title-color: #d97706; + @apply border-yellow-500; +} + +.dark .prose .callout-warning { + --callout-bg-start: rgba(253, 224, 71, 0.12); + --callout-bg-end: rgba(253, 224, 71, 0.06); + --callout-title-color: #fde047; + @apply border-yellow-400; +} + +/* CAUTION - Red */ +.prose .callout-caution { + --callout-bg-start: rgba(239, 68, 68, 0.08); + --callout-bg-end: rgba(239, 68, 68, 0.04); + --callout-title-color: #dc2626; + @apply border-red-500; +} + +.dark .prose .callout-caution { + --callout-bg-start: rgba(248, 113, 113, 0.12); + --callout-bg-end: rgba(248, 113, 113, 0.06); + --callout-title-color: #fca5a5; + @apply border-red-400; } \ No newline at end of file