Compare commits

..

11 Commits

15 changed files with 9016 additions and 152 deletions

1
.gitignore vendored
View File

@@ -40,3 +40,4 @@ yarn-error.log*
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
.opencode/** .opencode/**
deploy.zsh

View File

@@ -3,6 +3,10 @@
/* Dark mode: class-based (for @wrksz/themes) */ /* Dark mode: class-based (for @wrksz/themes) */
@custom-variant dark (&:is(.dark)); @custom-variant dark (&:is(.dark));
:root {
--scroll-offset: 7.5rem; /* ~120px, generous offset for sticky header */
}
/* === Design Tokens === */ /* === Design Tokens === */
@theme { @theme {
/* Colors — Light mode defaults */ /* Colors — Light mode defaults */
@@ -71,10 +75,10 @@
@layer base { @layer base {
html { html {
scroll-padding-top: 96px; scroll-behavior: smooth;
scroll-padding-top: var(--scroll-offset);
} }
body { body {
--header-height: 56px;
font-family: var(--font-serif); font-family: var(--font-serif);
font-weight: 400; font-weight: 400;
line-height: 1.6; line-height: 1.6;
@@ -185,7 +189,7 @@
.prose :where(h1, h2, h3, h4, h5, h6) { .prose :where(h1, h2, h3, h4, h5, h6) {
color: var(--color-ink); color: var(--color-ink);
scroll-margin-top: 96px; scroll-margin-top: var(--scroll-offset);
} }
.prose :where(h1) { margin: 2.75rem 0 1rem; } .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: 2px solid var(--color-accent);
outline-offset: 2px; 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;
}

View File

@@ -38,7 +38,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( 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}`}> <body className={`antialiased bg-canvas text-ink ${fraunces.className}`}>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"

View File

@@ -36,7 +36,7 @@ export default async function PostPage({ params }: { params: Promise<{ slug: str
<ScrollToTop /> <ScrollToTop />
<ReadingProgress /> <ReadingProgress />
<div className="mx-auto max-w-6xl px-6 py-16"> <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"> <article className="min-w-0">
<header className="mb-12"> <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> <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>

View File

@@ -21,7 +21,7 @@ export function Header() {
transition={{ duration: 0.4, ease: "easeOut" }} transition={{ duration: 0.4, ease: "easeOut" }}
className="sticky top-0 z-50 bg-canvas/80 backdrop-blur-sm border-b border-border" 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"> <Link href="/" className="heading-sm text-ink hover:text-accent transition-colors">
Krishna Krishna
</Link> </Link>

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef, type MouseEvent } from "react";
interface TOCItem { interface TOCItem {
id: string; id: string;
@@ -55,10 +55,26 @@ export function TableOfContents() {
return () => observerRef.current?.disconnect(); 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; if (headings.length === 0) return null;
return ( 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"> <h4 className="font-sans text-xs font-semibold uppercase tracking-wider text-ink-soft mb-3">
On this page On this page
</h4> </h4>
@@ -67,6 +83,7 @@ export function TableOfContents() {
<li key={heading.id}> <li key={heading.id}>
<a <a
href={`#${heading.id}`} href={`#${heading.id}`}
onClick={(e) => handleLinkClick(e, heading.id)}
className={`block text-sm transition-colors ${ className={`block text-sm transition-colors ${
heading.level === 3 ? "pl-3 text-ink-soft" : "text-ink" heading.level === 3 ? "pl-3 text-ink-soft" : "text-ink"
} ${activeId === heading.id ? "font-semibold text-accent" : ""}`} } ${activeId === heading.id ? "font-semibold text-accent" : ""}`}

39
components/ui/Callout.tsx Normal file
View 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>
);
}

View File

@@ -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.
![Simple local diagram for a starter design post](/starter-diagram.svg)
## 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.

View File

@@ -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
}
```

View File

@@ -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.
![Abstract gradient card used as a local starter cover](/starter-showcase.svg)
## 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
View 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: {} };
}
});
};
}

View File

@@ -1,22 +1,24 @@
import type { MDXComponents } from 'mdx/types' import type { MDXComponents } from 'mdx/types'
import { Callout } from '@/components/ui/Callout'
export function useMDXComponents(components: MDXComponents): MDXComponents { export function useMDXComponents(components: MDXComponents): MDXComponents {
return { return {
...components, ...components,
Callout,
// Typography // Typography
h1: (props) => ( 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} {props.children}
</h1> </h1>
), ),
h2: (props) => ( 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} {props.children}
</h2> </h2>
), ),
h3: (props) => ( 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} {props.children}
</h3> </h3>
), ),

View File

@@ -2,6 +2,7 @@ import type { NextConfig } from 'next'
import createMDX from '@next/mdx' import createMDX from '@next/mdx'
const mdxCodeBlockPipeline = new URL('./lib/mdx-hast-visitor.js', import.meta.url).pathname 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 = { const nextConfig: NextConfig = {
output: 'export', output: 'export',
@@ -11,7 +12,14 @@ const nextConfig: NextConfig = {
const withMDX = createMDX({ const withMDX = createMDX({
options: { options: {
remarkPlugins: ['remark-frontmatter', 'remark-smartypants', 'remark-math', 'remark-gfm'], remarkPlugins: [
'remark-frontmatter',
'remark-smartypants',
'remark-math',
'remark-gfm',
'remark-directive',
calloutDirectivePath,
],
rehypePlugins: [ rehypePlugins: [
'rehype-slug', 'rehype-slug',
['rehype-external-links', { target: '_blank', rel: ['nofollow', 'noopener', 'noreferrer'] }], ['rehype-external-links', { target: '_blank', rel: ['nofollow', 'noopener', 'noreferrer'] }],

8847
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -20,12 +20,13 @@
"next": "16.2.6", "next": "16.2.6",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"remark-frontmatter": "^5.0.0",
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-external-links": "^3.0.0", "rehype-external-links": "^3.0.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"rehype-pretty-code": "^0.14.3", "rehype-pretty-code": "^0.14.3",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark-directive": "^4.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-smartypants": "^3.0.2" "remark-smartypants": "^3.0.2"