Compare commits
11 Commits
72443140f9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
975c1bec9b | ||
|
|
6a35cc25d4 | ||
|
|
75fea5e49c | ||
|
|
48ad4512c3 | ||
|
|
3aa101812e | ||
|
|
f8383fb471 | ||
|
|
0f516bd23b | ||
|
|
64d9eb2474 | ||
|
|
29c89cf891 | ||
|
|
7c653f3852 | ||
|
|
c617ee4438 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -40,3 +40,4 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.opencode/**
|
||||
deploy.zsh
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
/* Dark mode: class-based (for @wrksz/themes) */
|
||||
@custom-variant dark (&:is(.dark));
|
||||
|
||||
:root {
|
||||
--scroll-offset: 7.5rem; /* ~120px, generous offset for sticky header */
|
||||
}
|
||||
|
||||
/* === Design Tokens === */
|
||||
@theme {
|
||||
/* Colors — Light mode defaults */
|
||||
@@ -71,10 +75,10 @@
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-padding-top: 96px;
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: var(--scroll-offset);
|
||||
}
|
||||
body {
|
||||
--header-height: 56px;
|
||||
font-family: var(--font-serif);
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
@@ -185,7 +189,7 @@
|
||||
|
||||
.prose :where(h1, h2, h3, h4, h5, h6) {
|
||||
color: var(--color-ink);
|
||||
scroll-margin-top: 96px;
|
||||
scroll-margin-top: var(--scroll-offset);
|
||||
}
|
||||
|
||||
.prose :where(h1) { margin: 2.75rem 0 1rem; }
|
||||
@@ -632,3 +636,46 @@ body, body *, [class*="bg-"], [class*="border-"], [class*="text-"] {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Callout/Admonition styles */
|
||||
.callout {
|
||||
@apply my-6 rounded-lg border p-4 text-sm leading-relaxed;
|
||||
}
|
||||
.callout-title {
|
||||
@apply flex items-center gap-2 font-semibold mb-2;
|
||||
}
|
||||
.callout-icon {
|
||||
@apply text-base;
|
||||
}
|
||||
.callout-content {
|
||||
@apply pl-6;
|
||||
}
|
||||
|
||||
/* Type variants */
|
||||
.callout-note {
|
||||
@apply border-accent/30 bg-accent/[0.06];
|
||||
}
|
||||
.callout-note .callout-title {
|
||||
@apply text-accent;
|
||||
}
|
||||
|
||||
.callout-tip {
|
||||
@apply border-emerald-500/30 bg-emerald-500/[0.06];
|
||||
}
|
||||
.callout-tip .callout-title {
|
||||
@apply text-emerald-600 dark:text-emerald-400;
|
||||
}
|
||||
|
||||
.callout-warning {
|
||||
@apply border-amber-500/30 bg-amber-500/[0.06];
|
||||
}
|
||||
.callout-warning .callout-title {
|
||||
@apply text-amber-600 dark:text-amber-400;
|
||||
}
|
||||
|
||||
.callout-danger {
|
||||
@apply border-red-500/30 bg-red-500/[0.06];
|
||||
}
|
||||
.callout-danger .callout-title {
|
||||
@apply text-red-600 dark:text-red-400;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className={`${fraunces.variable} ${jetbrainsMono.variable} ${plexMono.variable}`} suppressHydrationWarning>
|
||||
<html lang="en" className={`${fraunces.variable} ${jetbrainsMono.variable} ${plexMono.variable}`} data-scroll-behavior="smooth" suppressHydrationWarning>
|
||||
<body className={`antialiased bg-canvas text-ink ${fraunces.className}`}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
|
||||
@@ -36,7 +36,7 @@ export default async function PostPage({ params }: { params: Promise<{ slug: str
|
||||
<ScrollToTop />
|
||||
<ReadingProgress />
|
||||
<div className="mx-auto max-w-6xl px-6 py-16">
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_220px] lg:gap-12">
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_14rem] lg:gap-12">
|
||||
<article className="min-w-0">
|
||||
<header className="mb-12">
|
||||
<time className="font-mono text-sm text-ink-soft" dateTime={post.date}>{new Date(post.date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}</time>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function Header() {
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
className="sticky top-0 z-50 bg-canvas/80 backdrop-blur-sm border-b border-border"
|
||||
>
|
||||
<div className="max-w-4xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<Link href="/" className="heading-sm text-ink hover:text-accent transition-colors">
|
||||
Krishna
|
||||
</Link>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useState, useRef, type MouseEvent } from "react";
|
||||
|
||||
interface TOCItem {
|
||||
id: string;
|
||||
@@ -55,10 +55,26 @@ export function TableOfContents() {
|
||||
return () => observerRef.current?.disconnect();
|
||||
}, []);
|
||||
|
||||
const handleLinkClick = (e: MouseEvent<HTMLAnchorElement>, id: string) => {
|
||||
e.preventDefault();
|
||||
const target = document.getElementById(id);
|
||||
if (!target) return;
|
||||
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
if (prefersReducedMotion) {
|
||||
window.history.pushState(null, '', `#${id}`);
|
||||
target.scrollIntoView({ behavior: 'auto' });
|
||||
} else {
|
||||
target.scrollIntoView({ behavior: 'smooth' });
|
||||
window.history.pushState(null, '', `#${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (headings.length === 0) return null;
|
||||
|
||||
return (
|
||||
<nav aria-label="Table of contents" className="lg:sticky lg:top-[var(--header-height)]">
|
||||
<nav aria-label="Table of contents" className="lg:sticky lg:top-20">
|
||||
<h4 className="font-sans text-xs font-semibold uppercase tracking-wider text-ink-soft mb-3">
|
||||
On this page
|
||||
</h4>
|
||||
@@ -67,6 +83,7 @@ export function TableOfContents() {
|
||||
<li key={heading.id}>
|
||||
<a
|
||||
href={`#${heading.id}`}
|
||||
onClick={(e) => handleLinkClick(e, heading.id)}
|
||||
className={`block text-sm transition-colors ${
|
||||
heading.level === 3 ? "pl-3 text-ink-soft" : "text-ink"
|
||||
} ${activeId === heading.id ? "font-semibold text-accent" : ""}`}
|
||||
|
||||
39
components/ui/Callout.tsx
Normal file
39
components/ui/Callout.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface CalloutProps {
|
||||
type?: 'note' | 'tip' | 'warning' | 'danger';
|
||||
title?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const iconMap: Record<string, string> = {
|
||||
note: 'ℹ️',
|
||||
tip: '💡',
|
||||
warning: '⚠️',
|
||||
danger: '🚫',
|
||||
};
|
||||
|
||||
const typeClasses: Record<string, string> = {
|
||||
note: 'callout-note',
|
||||
tip: 'callout-tip',
|
||||
warning: 'callout-warning',
|
||||
danger: 'callout-danger',
|
||||
};
|
||||
|
||||
export function Callout({ type = 'note', title, children }: CalloutProps) {
|
||||
const className = `callout ${typeClasses[type] || typeClasses.note}`;
|
||||
|
||||
return (
|
||||
<div className={className} role="note">
|
||||
{title && (
|
||||
<div className="callout-title">
|
||||
<span className="callout-icon">{iconMap[type] || iconMap.note}</span>
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="callout-content">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
title: "Designing With Next.js"
|
||||
date: "2026-05-22"
|
||||
excerpt: "A short sample note about pairing interface design decisions with static Next.js content."
|
||||
tags:
|
||||
- Design
|
||||
- Next.js
|
||||
- Web Dev
|
||||
author: "Starter Content"
|
||||
coverImage: "/starter-diagram.svg"
|
||||
---
|
||||
|
||||
This sample note gives tag pages another realistic post to list. It is intentionally brief and ready to replace.
|
||||
|
||||

|
||||
|
||||
## A tiny design checklist
|
||||
|
||||
- Keep headings scannable.
|
||||
- Pair every visual with helpful alt text.
|
||||
- Use consistent tags so archive pages feel populated.
|
||||
|
||||
The same content can support readers and tooling: humans get structure, while static route generation gets predictable params.
|
||||
|
||||
```ts title="content-tags.ts" {3}
|
||||
export const starterTags = [
|
||||
'Design',
|
||||
'Next.js',
|
||||
'Web Dev',
|
||||
]
|
||||
```
|
||||
|
||||
> Replace this with a real design journal, launch note, or tutorial when you are ready.
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
title: "Math Notes for Builders"
|
||||
date: "2026-05-08"
|
||||
excerpt: "A compact starter post that keeps Math and Code tags populated while checking equation rendering."
|
||||
tags:
|
||||
- Math
|
||||
- Code
|
||||
- Web Dev
|
||||
author: "Starter Content"
|
||||
---
|
||||
|
||||
This replaceable note exists to keep the **Math**, **Code**, and **Web Dev** tag pages meaningful during early validation.
|
||||
|
||||
## Tiny model
|
||||
|
||||
Inline math can express a quick estimate like $n \log n$, while display math can show the full relationship:
|
||||
|
||||
$$
|
||||
T(n) = O(n \log n) + O(k)
|
||||
$$
|
||||
|
||||
## Practical reminder
|
||||
|
||||
- Prefer simple explanations before formulas.
|
||||
- Add runnable code when it clarifies an idea.
|
||||
- Link back to related posts, such as the [starter showcase](/posts/starter-mdx-showcase/).
|
||||
|
||||
```js title="estimate.js" {1,4}
|
||||
export function estimate(items, constant = 1) {
|
||||
const n = Math.max(items.length, 1)
|
||||
return n * Math.log2(n) + constant
|
||||
}
|
||||
```
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
title: "Starter MDX Showcase"
|
||||
date: "2026-06-03"
|
||||
excerpt: "Sample starter content demonstrating math, local images, links, tables, tasks, and code blocks for validating site routes."
|
||||
tags:
|
||||
- Design
|
||||
- Next.js
|
||||
- Math
|
||||
- Code
|
||||
- Web Dev
|
||||
author: "Starter Content"
|
||||
coverImage: "/starter-showcase.svg"
|
||||
---
|
||||
|
||||
This is deliberately sample content for Krishna's journal. Replace it with your own writing once the routes, tags, and search experience are validated.
|
||||
|
||||

|
||||
|
||||
## What this post covers
|
||||
|
||||
- A local image from `public/` referenced with an absolute path.
|
||||
- Links to [Next.js](https://nextjs.org/) and an internal [tag page](/tags/web-dev/).
|
||||
- GFM tables, task lists, blockquotes, code fences, and math.
|
||||
|
||||
> Starter posts should be easy to delete, but rich enough to prove the publishing pipeline works end to end.
|
||||
|
||||
## Markdown and GFM examples
|
||||
|
||||
| Feature | Purpose | Status |
|
||||
| --- | --- | --- |
|
||||
| Tags | Builds static tag pages | Ready |
|
||||
| Math | Checks KaTeX rendering | Ready |
|
||||
| Code | Checks syntax highlighting | Ready |
|
||||
|
||||
- [x] Confirm route generation has at least one post.
|
||||
- [x] Confirm spaced tags such as **Web Dev** create usable params.
|
||||
- [ ] Replace this demo with a real article.
|
||||
|
||||
Inline code like `generateStaticParams` should be readable inside a sentence, and inline math such as $E = mc^2$ should render without extra setup.
|
||||
|
||||
Display math is useful for longer equations:
|
||||
|
||||
$$
|
||||
\operatorname{score}(post) = \frac{links + images + code}{reading\ time}
|
||||
$$
|
||||
|
||||
## Code with title and highlighted lines
|
||||
|
||||
```tsx title="components/example-card.tsx" {2,6-8}
|
||||
type ExampleCardProps = {
|
||||
title: string
|
||||
href: string
|
||||
}
|
||||
|
||||
export function ExampleCard({ title, href }: ExampleCardProps) {
|
||||
return <a href={href}>{title}</a>
|
||||
}
|
||||
```
|
||||
|
||||
## Lists, links, and details
|
||||
|
||||
1. Draft the article in MDX.
|
||||
2. Add concrete examples and screenshots.
|
||||
3. Link to useful references, such as the [MDX docs](https://mdxjs.com/).
|
||||
|
||||
<details>
|
||||
<summary>Why keep demo content obvious?</summary>
|
||||
<p>Because starter posts are fixtures for validation, not permanent editorial content.</p>
|
||||
</details>
|
||||
|
||||
## Closing note
|
||||
|
||||
This post intentionally exercises common authoring features so the owner can validate static post routes, tag pages, and future search indexing with realistic but replaceable content.
|
||||
41
lib/callout-directive.js
Normal file
41
lib/callout-directive.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
export default function calloutDirective() {
|
||||
return (tree) => {
|
||||
visit(tree, (node) => {
|
||||
// Handle textDirective (has content) and leafDirective (no content)
|
||||
if (node.type === 'textDirective' || node.type === 'leafDirective') {
|
||||
const name = node.name;
|
||||
|
||||
// Only handle known callout types
|
||||
const validTypes = ['note', 'tip', 'warning', 'danger'];
|
||||
if (!validTypes.includes(name)) return;
|
||||
|
||||
// Extract attributes (e.g., title="...")
|
||||
const attributes = node.attributes || [];
|
||||
const attrs = attributes.map((attr) => ({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: attr.name,
|
||||
value: attr.value,
|
||||
}));
|
||||
|
||||
// Add type attribute
|
||||
attrs.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'type',
|
||||
value: { type: 'mdxFlowExpression', value: `"${name}"` },
|
||||
});
|
||||
|
||||
// Build children from node's children
|
||||
const children = node.children || [];
|
||||
|
||||
// Transform to mdxJsxFlowElement
|
||||
node.type = 'mdxJsxFlowElement';
|
||||
node.name = 'Callout';
|
||||
node.attributes = attrs;
|
||||
node.children = children;
|
||||
node.data = { hName: 'Callout', hProperties: {} };
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
import type { MDXComponents } from 'mdx/types'
|
||||
import { Callout } from '@/components/ui/Callout'
|
||||
|
||||
|
||||
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
return {
|
||||
...components,
|
||||
Callout,
|
||||
// Typography
|
||||
h1: (props) => (
|
||||
<h1 {...props} className="scroll-m-20 heading-xl text-ink mt-10 mb-4">
|
||||
<h1 {...props} className="heading-xl text-ink mt-10 mb-4">
|
||||
{props.children}
|
||||
</h1>
|
||||
),
|
||||
h2: (props) => (
|
||||
<h2 {...props} className="scroll-m-20 border-b border-border pb-2 heading-lg text-ink mt-12 mb-4 first:mt-0">
|
||||
<h2 {...props} className="border-b border-border pb-2 heading-lg text-ink mt-12 mb-4 first:mt-0">
|
||||
{props.children}
|
||||
</h2>
|
||||
),
|
||||
h3: (props) => (
|
||||
<h3 {...props} className="scroll-m-20 heading-md text-ink mt-8 mb-3">
|
||||
<h3 {...props} className="heading-md text-ink mt-8 mb-3">
|
||||
{props.children}
|
||||
</h3>
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NextConfig } from 'next'
|
||||
import createMDX from '@next/mdx'
|
||||
|
||||
const mdxCodeBlockPipeline = new URL('./lib/mdx-hast-visitor.js', import.meta.url).pathname
|
||||
const calloutDirectivePath = new URL('./lib/callout-directive.js', import.meta.url).pathname
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'export',
|
||||
@@ -11,7 +12,14 @@ const nextConfig: NextConfig = {
|
||||
|
||||
const withMDX = createMDX({
|
||||
options: {
|
||||
remarkPlugins: ['remark-frontmatter', 'remark-smartypants', 'remark-math', 'remark-gfm'],
|
||||
remarkPlugins: [
|
||||
'remark-frontmatter',
|
||||
'remark-smartypants',
|
||||
'remark-math',
|
||||
'remark-gfm',
|
||||
'remark-directive',
|
||||
calloutDirectivePath,
|
||||
],
|
||||
rehypePlugins: [
|
||||
'rehype-slug',
|
||||
['rehype-external-links', { target: '_blank', rel: ['nofollow', 'noopener', 'noreferrer'] }],
|
||||
|
||||
8847
package-lock.json
generated
Normal file
8847
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,12 +20,13 @@
|
||||
"next": "16.2.6",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-pretty-code": "^0.14.3",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-directive": "^4.0.0",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-smartypants": "^3.0.2"
|
||||
|
||||
Reference in New Issue
Block a user