Compare commits
53 Commits
975c1bec9b
...
a294153599
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a294153599 | ||
|
|
c6475cce54 | ||
|
|
0bdd4861f0 | ||
|
|
189680f772 | ||
|
|
974f60ffac | ||
|
|
fb1d6bf69a | ||
|
|
76676ddfed | ||
|
|
ca4885301a | ||
|
|
764600e6fe | ||
|
|
40d5c75775 | ||
|
|
2c4e7a6649 | ||
|
|
99619c4e9f | ||
|
|
b525ddcd04 | ||
|
|
41cfccda2a | ||
|
|
709278e649 | ||
|
|
13a82263e3 | ||
|
|
18fe7a1613 | ||
|
|
6c1c751313 | ||
|
|
e58f2c7c7c | ||
|
|
14b9274c47 | ||
|
|
57431f91c2 | ||
|
|
3ee150280f | ||
|
|
bc65f83c1e | ||
|
|
4ea90f90d7 | ||
|
|
c0376df402 | ||
|
|
ff4b556381 | ||
|
|
fd2102ed21 | ||
|
|
48af740469 | ||
|
|
00d1859f41 | ||
|
|
d4a6819093 | ||
|
|
d332691e7e | ||
|
|
d8631dfc1b | ||
|
|
ecfe0ce9cc | ||
|
|
ed94a782a3 | ||
|
|
c1a5d2ea0d | ||
|
|
efe0b5b394 | ||
|
|
f4264886f9 | ||
|
|
7199eb458a | ||
|
|
3e2a5a391c | ||
|
|
5b3e24ca78 | ||
|
|
ff58dd000c | ||
|
|
7c8a4d8c52 | ||
|
|
b21a83c548 | ||
|
|
637cb7cdaa | ||
|
|
7adc47d65f | ||
|
|
4dd609179a | ||
|
|
903ff3d0d6 | ||
|
|
e005040109 | ||
|
|
ebefb3496a | ||
|
|
b8e0a180d1 | ||
|
|
9f6b22891f | ||
|
|
eaef10c824 | ||
|
|
9ae127eb77 |
82
app/blog/[slug]/page.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { getPosts, getPost } from '@/lib/posts'
|
||||
import { tagToSlug } from '@/lib/tags'
|
||||
import { TableOfContents } from '@/components/posts/TableOfContents'
|
||||
import { ScrollToTop } from '@/components/ui/ScrollToTop'
|
||||
import { ReadingProgress } from '@/components/ui/ReadingProgress'
|
||||
|
||||
export const dynamicParams = false
|
||||
export const dynamic = 'force-static'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await getPosts()
|
||||
if (posts.length === 0) {
|
||||
return [{ slug: '__no-posts__' }]
|
||||
}
|
||||
return posts.map((post) => ({ slug: post.slug }))
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const slug = (await params).slug
|
||||
const post = await getPost(slug)
|
||||
if (!post) return { title: 'Not Found' }
|
||||
return { title: post.title }
|
||||
}
|
||||
|
||||
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const slug = (await params).slug
|
||||
|
||||
if (slug === '__no-posts__') notFound()
|
||||
|
||||
const post = await getPost(slug)
|
||||
|
||||
if (!post) notFound()
|
||||
|
||||
const { default: PostContent } = await import(
|
||||
/* turbopackOptional: true */
|
||||
`@/content/posts/${slug}.mdx`
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollToTop />
|
||||
<ReadingProgress />
|
||||
<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="section-label" dateTime={post.date}>{new Date(post.date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })} · {post.author} · {post.readingTime} min</time>
|
||||
<h1 className="heading-xl font-display text-ink mt-3 mb-2">
|
||||
{post.title}
|
||||
</h1>
|
||||
{post.coverImage && (
|
||||
<img
|
||||
src={post.coverImage}
|
||||
alt={`Featured image for article: ${post.title}`}
|
||||
className="my-8 aspect-[16/9] w-full rounded-lg border border-border bg-surface object-cover shadow-card"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
fetchPriority="high"
|
||||
/>
|
||||
)}
|
||||
{post.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{post.tags.map((tag) => (
|
||||
<Link key={tag} href={`/blog/tags/${tagToSlug(tag)}/`} className="rounded-sm bg-surface px-3 py-1 font-mono text-[11px] uppercase tracking-wider text-ink-soft border border-border transition-colors hover:border-accent/50 hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent">
|
||||
{tag}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<div className="prose prose-lg article-prose max-w-none min-w-0">
|
||||
<PostContent />
|
||||
</div>
|
||||
</article>
|
||||
<aside className="hidden lg:block">
|
||||
<TableOfContents />
|
||||
</aside>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
16
app/blog/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Header } from '@/components/layout/Header'
|
||||
import { Footer } from '@/components/layout/Footer'
|
||||
import { ScrollProgress } from '@/components/ui/ScrollProgress'
|
||||
|
||||
export default function BlogLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<ScrollProgress />
|
||||
<Header />
|
||||
<main className="mx-auto w-full max-w-6xl px-6 py-16 sm:py-24">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import { getPosts } from '@/lib/posts'
|
||||
import { PostSearch } from '@/components/posts/PostSearch'
|
||||
|
||||
export const metadata = { title: 'Posts' }
|
||||
export const metadata = { title: 'Field Notes' }
|
||||
|
||||
export default async function PostsPage() {
|
||||
const posts = await getPosts()
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-5xl px-6 py-12 sm:py-16">
|
||||
<>
|
||||
<header className="mb-10 border-b border-border pb-8">
|
||||
<p className="section-label mb-4">FIELD NOTES /.06</p>
|
||||
<h1 className="heading-xl m-0 text-ink">
|
||||
Posts
|
||||
Field Notes
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
@@ -18,13 +19,9 @@ export default async function PostsPage() {
|
||||
<PostSearch posts={posts} />
|
||||
) : (
|
||||
<section className="empty-state" aria-label="No posts published">
|
||||
<p className="font-mono text-xs uppercase tracking-[0.22em] text-ink-soft">No posts yet</p>
|
||||
<h2 className="heading-sm m-0 text-ink">The archive is empty.</h2>
|
||||
<p className="m-0 max-w-xl text-sm leading-6 text-ink-soft">
|
||||
Publish an MDX post to populate this list and enable archive search.
|
||||
</p>
|
||||
<p className="m-0 text-sm leading-6 text-ink-soft">Archive empty.</p>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,9 @@ export const dynamic = 'force-static'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const tags = await getAllTags()
|
||||
if (tags.length === 0) {
|
||||
return [{ tag: '__no-tags__' }]
|
||||
}
|
||||
return tags.map((tag) => ({ tag: tag.slug }))
|
||||
}
|
||||
|
||||
@@ -27,6 +30,9 @@ export async function generateMetadata({ params }: TagPageProps): Promise<Metada
|
||||
|
||||
export default async function TagPage({ params }: TagPageProps) {
|
||||
const slug = (await params).tag
|
||||
|
||||
if (slug === '__no-tags__') notFound()
|
||||
|
||||
const tags = await getAllTags()
|
||||
const tag = tags.find((item) => item.slug === slug)
|
||||
|
||||
@@ -35,9 +41,9 @@ export default async function TagPage({ params }: TagPageProps) {
|
||||
const posts = await getPostsByTag(slug)
|
||||
|
||||
return (
|
||||
<main className="max-w-4xl mx-auto px-6 py-12">
|
||||
<div className="max-w-4xl mx-auto px-6 py-12">
|
||||
<header className="mb-12">
|
||||
<p className="font-mono text-sm text-ink-soft mb-3">Tag</p>
|
||||
<p className="section-label mb-3">Tag</p>
|
||||
<h1 className="heading-xl text-ink mb-3">{tag.label}</h1>
|
||||
<p className="text-ink-soft">
|
||||
{tag.count} {tag.count === 1 ? 'post' : 'posts'}
|
||||
@@ -50,6 +56,6 @@ export default async function TagPage({ params }: TagPageProps) {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
BIN
app/favicon.ico
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 31 KiB |
112
app/globals.css
@@ -38,6 +38,7 @@
|
||||
--font-sans: "Fraunces", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-serif: "IBM Plex Mono", ui-monospace, monospace;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, monospace;
|
||||
--font-display: "Space Grotesk";
|
||||
|
||||
/* Typography scale */
|
||||
--text-xs: 0.75rem; --text-xs--line-height: 1.2;
|
||||
@@ -71,6 +72,22 @@
|
||||
from { opacity: 0; transform: translateY(24px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Signal Blacksite */
|
||||
--color-blacksite-bg: #030506;
|
||||
--color-blacksite-bg2: #07090a;
|
||||
--color-blacksite-surface: #101517;
|
||||
--color-blacksite-surface2: #171d21;
|
||||
--color-blacksite-surface3: #20282d;
|
||||
--color-blacksite-text: #f4f7f8;
|
||||
--color-blacksite-muted: #8b949a;
|
||||
--color-blacksite-faint: #5f696f;
|
||||
--color-blacksite-line: #2c353a;
|
||||
--color-signal-cyan: #00f0ff;
|
||||
--color-signal-orange: #ff5a1f;
|
||||
--color-signal-green: #b6ff00;
|
||||
--color-signal-red: #ff3b30;
|
||||
--color-signal-violet: #805dff;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -86,7 +103,7 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-sans);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.015em;
|
||||
@@ -114,25 +131,25 @@
|
||||
|
||||
/* === Dark mode token overrides === */
|
||||
.dark {
|
||||
--color-canvas: oklch(0 0 0);
|
||||
--color-surface: oklch(0.07 0 0);
|
||||
--color-ink: oklch(1 0 0);
|
||||
--color-ink-soft: oklch(0.72 0 0);
|
||||
--color-border: oklch(0.17 0 0);
|
||||
--color-accent: oklch(0.68 0.20 250);
|
||||
--color-canvas: #030506;
|
||||
--color-surface: #101517;
|
||||
--color-ink: #f4f7f8;
|
||||
--color-ink-soft: #8b949a;
|
||||
--color-border: #2c353a;
|
||||
--color-accent: #00f0ff;
|
||||
|
||||
/* Code blocks — Dark mode */
|
||||
--color-code-block: oklch(0.04 0 0);
|
||||
--color-code-block-hover: oklch(0.06 0 0);
|
||||
--color-code-gutter: oklch(0.30 0 0);
|
||||
--color-code-gutter-border: oklch(0.14 0 0);
|
||||
--color-code-title-bg: oklch(0.03 0 0);
|
||||
--color-code-title-text: oklch(0.55 0 0);
|
||||
--color-code-block: #07090a;
|
||||
--color-code-block-hover: #101517;
|
||||
--color-code-gutter: #8b949a;
|
||||
--color-code-gutter-border: #2c353a;
|
||||
--color-code-title-bg: #07090a;
|
||||
--color-code-title-text: #8b949a;
|
||||
--color-code-line-highlight: oklch(0.12 0.02 250 / 0.3);
|
||||
--color-code-line-highlight-border: oklch(0.68 0.20 250);
|
||||
--color-code-line-highlight-border: #00f0ff;
|
||||
--color-code-mark-bg: oklch(0.10 0.03 250 / 0.4);
|
||||
--color-code-copy: oklch(0.60 0 0);
|
||||
--color-code-copy-hover: oklch(0.85 0 0);
|
||||
--color-code-copy: #8b949a;
|
||||
--color-code-copy-hover: #f4f7f8;
|
||||
|
||||
--shadow-card: 0 1px 4px oklch(1 0 0 / 0.06);
|
||||
--shadow-card-hover: 0 22px 50px -34px oklch(1 0 0 / 0.22), 0 1px 4px oklch(1 0 0 / 0.08);
|
||||
@@ -170,11 +187,19 @@
|
||||
opacity: 0.22;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-blacksite-muted);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
border: 1px dashed color-mix(in oklch, var(--color-border) 78%, var(--color-ink) 22%);
|
||||
border-radius: 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
background: color-mix(in oklch, var(--color-surface) 55%, transparent);
|
||||
padding: 2rem;
|
||||
}
|
||||
@@ -204,7 +229,7 @@
|
||||
}
|
||||
|
||||
.article-prose :where(p, ul, ol, blockquote, details, .callout, [data-callout]) {
|
||||
max-width: 72ch;
|
||||
max-width: 68ch;
|
||||
}
|
||||
|
||||
.prose :where(a) {
|
||||
@@ -212,6 +237,30 @@
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
|
||||
.prose [data-footnote-ref] {
|
||||
color: var(--color-signal-cyan);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.prose .footnotes {
|
||||
margin-top: 3rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: 1rem;
|
||||
color: var(--color-ink-soft);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.prose .footnotes ol {
|
||||
margin: 0.75rem 0 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.prose .footnotes li {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.prose :where(ul, ol) {
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
@@ -545,7 +594,7 @@ html.dark code[data-theme*=" "] span {
|
||||
left: 0;
|
||||
height: 2px;
|
||||
width: 0%;
|
||||
background: var(--color-ink);
|
||||
background: var(--color-signal-cyan);
|
||||
z-index: 9999;
|
||||
will-change: width;
|
||||
}
|
||||
@@ -586,7 +635,8 @@ html.dark code[data-theme*=" "] span {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.scroll-to-top:hover {
|
||||
border-color: var(--color-ink);
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* === Reading progress vertical bar === */
|
||||
@@ -602,7 +652,7 @@ html.dark code[data-theme*=" "] span {
|
||||
.reading-progress-fill {
|
||||
width: 100%;
|
||||
height: 0%;
|
||||
background: var(--color-ink);
|
||||
background: var(--color-signal-cyan);
|
||||
border-radius: 2px;
|
||||
}
|
||||
@keyframes reading-progress { from { height: 0%; } to { height: 100%; } }
|
||||
@@ -653,29 +703,33 @@ body, body *, [class*="bg-"], [class*="border-"], [class*="text-"] {
|
||||
|
||||
/* Type variants */
|
||||
.callout-note {
|
||||
@apply border-accent/30 bg-accent/[0.06];
|
||||
border-color: color-mix(in oklch, var(--color-signal-cyan) 30%, var(--color-border));
|
||||
background: color-mix(in oklch, var(--color-signal-cyan) 8%, var(--color-surface));
|
||||
}
|
||||
.callout-note .callout-title {
|
||||
@apply text-accent;
|
||||
color: var(--color-signal-cyan);
|
||||
}
|
||||
|
||||
.callout-tip {
|
||||
@apply border-emerald-500/30 bg-emerald-500/[0.06];
|
||||
border-color: color-mix(in oklch, var(--color-signal-green) 30%, var(--color-border));
|
||||
background: color-mix(in oklch, var(--color-signal-green) 8%, var(--color-surface));
|
||||
}
|
||||
.callout-tip .callout-title {
|
||||
@apply text-emerald-600 dark:text-emerald-400;
|
||||
color: var(--color-signal-green);
|
||||
}
|
||||
|
||||
.callout-warning {
|
||||
@apply border-amber-500/30 bg-amber-500/[0.06];
|
||||
border-color: color-mix(in oklch, var(--color-signal-orange) 30%, var(--color-border));
|
||||
background: color-mix(in oklch, var(--color-signal-orange) 8%, var(--color-surface));
|
||||
}
|
||||
.callout-warning .callout-title {
|
||||
@apply text-amber-600 dark:text-amber-400;
|
||||
color: var(--color-signal-orange);
|
||||
}
|
||||
|
||||
.callout-danger {
|
||||
@apply border-red-500/30 bg-red-500/[0.06];
|
||||
border-color: color-mix(in oklch, var(--color-signal-red) 30%, var(--color-border));
|
||||
background: color-mix(in oklch, var(--color-signal-red) 8%, var(--color-surface));
|
||||
}
|
||||
.callout-danger .callout-title {
|
||||
@apply text-red-600 dark:text-red-400;
|
||||
color: var(--color-signal-red);
|
||||
}
|
||||
|
||||
103
app/icon.svg
Normal file
@@ -0,0 +1,103 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500" height="500">
|
||||
<rect width="500" height="500" fill="#fff"/>
|
||||
<g transform="translate(250,250)">
|
||||
|
||||
<!-- Outer circle -->
|
||||
<circle cx="0" cy="0" r="220" fill="none" stroke="#000" stroke-width="2"/>
|
||||
|
||||
<!-- Triangle inscribed in outer circle (pointing up) -->
|
||||
<polygon points="0,-220 190.5,110 -190.5,110" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Triangle inscribed (pointing down) -->
|
||||
<polygon points="0,220 -190.5,-110 190.5,-110" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Circle inscribed in the star (radius ~110) -->
|
||||
<circle cx="0" cy="0" r="110" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Hexagon from the star intersections -->
|
||||
<polygon points="95,-165 190.5,-55 190.5,55 95,165 -95,165 -190.5,55 -190.5,-55 -95,-165"
|
||||
fill="none" stroke="#000" stroke-width="1"/>
|
||||
|
||||
<!-- Inner circle -->
|
||||
<circle cx="0" cy="0" r="55" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Triangle inscribed in inner circle (up) -->
|
||||
<polygon points="0,-55 47.6,27.5 -47.6,27.5" fill="none" stroke="#000" stroke-width="1"/>
|
||||
|
||||
<!-- Triangle inscribed in inner circle (down) -->
|
||||
<polygon points="0,55 -47.6,-27.5 47.6,-27.5" fill="none" stroke="#000" stroke-width="1"/>
|
||||
|
||||
<!-- Small circle at center -->
|
||||
<circle cx="0" cy="0" r="27" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Tiny triangle up -->
|
||||
<polygon points="0,-27 23.4,13.5 -23.4,13.5" fill="none" stroke="#000" stroke-width="0.8"/>
|
||||
|
||||
<!-- Tiny triangle down -->
|
||||
<polygon points="0,27 -23.4,-13.5 23.4,-13.5" fill="none" stroke="#000" stroke-width="0.8"/>
|
||||
|
||||
<!-- Center dot -->
|
||||
<circle cx="0" cy="0" r="4" fill="#000"/>
|
||||
|
||||
<!-- Lines from center through all vertices -->
|
||||
<g stroke="#000" stroke-width="0.5" opacity="0.4">
|
||||
<!-- Through outer triangle vertices -->
|
||||
<line x1="0" y1="-4" x2="0" y2="-220"/>
|
||||
<line x1="0" y1="4" x2="0" y2="220"/>
|
||||
<line x1="0" y1="0" x2="190.5" y2="110"/>
|
||||
<line x1="0" y1="0" x2="-190.5" y2="110"/>
|
||||
<line x1="0" y1="0" x2="190.5" y2="-110"/>
|
||||
<line x1="0" y1="0" x2="-190.5" y2="-110"/>
|
||||
</g>
|
||||
|
||||
<!-- Small circles at outer triangle vertices -->
|
||||
<g fill="#fff" stroke="#000" stroke-width="1.2">
|
||||
<circle cx="0" cy="-220" r="5"/>
|
||||
<circle cx="190.5" cy="110" r="5"/>
|
||||
<circle cx="-190.5" cy="110" r="5"/>
|
||||
</g>
|
||||
<g fill="#000">
|
||||
<circle cx="0" cy="-220" r="2"/>
|
||||
<circle cx="190.5" cy="110" r="2"/>
|
||||
<circle cx="-190.5" cy="110" r="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Small circles at inner triangle vertices -->
|
||||
<g fill="#fff" stroke="#000" stroke-width="1">
|
||||
<circle cx="0" cy="-55" r="3.5"/>
|
||||
<circle cx="47.6" cy="27.5" r="3.5"/>
|
||||
<circle cx="-47.6" cy="27.5" r="3.5"/>
|
||||
<circle cx="0" cy="55" r="3.5"/>
|
||||
<circle cx="47.6" cy="-27.5" r="3.5"/>
|
||||
<circle cx="-47.6" cy="-27.5" r="3.5"/>
|
||||
</g>
|
||||
|
||||
<!-- Dashed outermost ring -->
|
||||
<circle cx="0" cy="0" r="232" fill="none" stroke="#000" stroke-width="0.6" stroke-dasharray="2 6"/>
|
||||
|
||||
<!-- Arc marks at cardinal points on outer ring -->
|
||||
<g stroke="#000" stroke-width="1.2" stroke-linecap="round" fill="none">
|
||||
<path d="M -6,-226 A 6,6 0 0,1 6,-226"/>
|
||||
<path d="M -6,226 A 6,6 0 0,0 6,226"/>
|
||||
<path d="M -226,-6 A 6,6 0 0,0 -226,6"/>
|
||||
<path d="M 226,-6 A 6,6 0 0,1 226,6"/>
|
||||
</g>
|
||||
|
||||
<!-- Tiny dots at cardinal points -->
|
||||
<g fill="#000">
|
||||
<circle cx="0" cy="-232" r="1.5"/>
|
||||
<circle cx="0" cy="232" r="1.5"/>
|
||||
<circle cx="-232" cy="0" r="1.5"/>
|
||||
<circle cx="232" cy="0" r="1.5"/>
|
||||
</g>
|
||||
|
||||
<!-- Corner marks -->
|
||||
<g stroke="#000" stroke-width="1" stroke-linecap="round" opacity="0.5">
|
||||
<line x1="-164" y1="-164" x2="-152" y2="-152"/>
|
||||
<line x1="164" y1="-164" x2="152" y2="-152"/>
|
||||
<line x1="-164" y1="164" x2="-152" y2="152"/>
|
||||
<line x1="164" y1="164" x2="152" y2="152"/>
|
||||
</g>
|
||||
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -1,11 +1,8 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Fraunces, IBM_Plex_Mono, JetBrains_Mono } from 'next/font/google'
|
||||
import { Fraunces, IBM_Plex_Mono, JetBrains_Mono, Space_Grotesk } from 'next/font/google'
|
||||
import { ThemeProvider } from '@wrksz/themes/next'
|
||||
import './globals.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { Header } from '@/components/layout/Header'
|
||||
import { Footer } from '@/components/layout/Footer'
|
||||
import { ScrollProgress } from '@/components/ui/ScrollProgress'
|
||||
import { Providers } from './providers'
|
||||
|
||||
const fraunces = Fraunces({
|
||||
@@ -17,6 +14,12 @@ const fraunces = Fraunces({
|
||||
variable: '--font-fraunces',
|
||||
})
|
||||
|
||||
const spaceGrotesk = Space_Grotesk({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
variable: '--font-space-grotesk',
|
||||
})
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
@@ -31,27 +34,33 @@ const plexMono = IBM_Plex_Mono({
|
||||
variable: '--font-plex-mono',
|
||||
})
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://localhost:3000'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(siteUrl),
|
||||
title: { template: '%s | Krishna', default: 'Krishna' },
|
||||
description: 'A sleek static journal with code and math.',
|
||||
description: 'GPU pipelines, hardened systems, real-time infrastructure.',
|
||||
icons: {
|
||||
icon: '/ouroboros.svg',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Krishna',
|
||||
description: 'GPU pipelines, hardened systems, real-time infrastructure.',
|
||||
images: ['/og.png'],
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className={`${fraunces.variable} ${jetbrainsMono.variable} ${plexMono.variable}`} data-scroll-behavior="smooth" suppressHydrationWarning>
|
||||
<html lang="en" className={`${fraunces.variable} ${spaceGrotesk.variable} ${jetbrainsMono.variable} ${plexMono.variable} dark`} data-scroll-behavior="smooth" suppressHydrationWarning>
|
||||
<body className={`antialiased bg-canvas text-ink ${fraunces.className}`}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
defaultTheme="dark"
|
||||
forcedTheme="dark"
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<Providers>
|
||||
<ScrollProgress />
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
</Providers>
|
||||
<Providers>{children}</Providers>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,14 +4,29 @@ export const metadata = { title: "Not Found" }
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="min-h-screen flex items-center justify-center px-6">
|
||||
<div className="text-center">
|
||||
<p className="font-mono text-sm text-ink-soft mb-4">404</p>
|
||||
<h1 className="heading-xl text-ink mt-0 mb-4">Page not found</h1>
|
||||
<p className="text-ink-soft mb-8">The page you{`'`}re looking for doesn{`'`}t exist.</p>
|
||||
<Link href="/" className="inline-block rounded-lg bg-ink px-6 py-3 text-sm font-medium text-canvas hover:opacity-80 transition-opacity">
|
||||
Go home
|
||||
</Link>
|
||||
<main className="flex min-h-screen items-center justify-center bg-blacksite-bg px-6 text-blacksite-text">
|
||||
<div className="w-full max-w-2xl border border-blacksite-line bg-blacksite-surface/60 p-8 shadow-2xl shadow-black/40 sm:p-12">
|
||||
<p className="section-label mb-5">SIGNAL LOST /.404</p>
|
||||
<h1 className="mb-5 font-display text-5xl font-semibold tracking-tight text-blacksite-text sm:text-7xl">
|
||||
Route dissolved into static.
|
||||
</h1>
|
||||
<p className="mb-8 max-w-lg text-sm leading-6 text-blacksite-muted sm:text-base">
|
||||
This coordinate is dark; return to base or scan the field notes.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
className="border border-signal-orange px-4 py-3 font-mono text-xs uppercase tracking-[0.16em] text-signal-orange transition-colors hover:bg-signal-orange hover:text-blacksite-bg focus-visible:bg-signal-orange focus-visible:text-blacksite-bg"
|
||||
>
|
||||
Return home
|
||||
</Link>
|
||||
<Link
|
||||
href="/blog/"
|
||||
className="border border-blacksite-line px-4 py-3 font-mono text-xs uppercase tracking-[0.16em] text-blacksite-text transition-colors hover:border-signal-cyan hover:text-signal-cyan focus-visible:border-signal-cyan focus-visible:text-signal-cyan"
|
||||
>
|
||||
Field notes
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
||||
61
app/page.tsx
@@ -1,30 +1,45 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import { CapabilityRows } from '@/components/portfolio/CapabilityRows'
|
||||
import { ClosingCta } from '@/components/portfolio/ClosingCta'
|
||||
import { CommandPalette } from '@/components/portfolio/CommandPalette'
|
||||
import { ContactTerminal } from '@/components/portfolio/ContactTerminal'
|
||||
import { FeaturedDossier } from '@/components/portfolio/FeaturedDossier'
|
||||
import { FieldNotes } from '@/components/portfolio/FieldNotes'
|
||||
import { Hero } from '@/components/portfolio/Hero'
|
||||
import { Manifesto } from '@/components/portfolio/Manifesto'
|
||||
import { OperatorProfile } from '@/components/portfolio/OperatorProfile'
|
||||
import { PortfolioNav } from '@/components/portfolio/PortfolioNav'
|
||||
import { StatusBar } from '@/components/portfolio/StatusBar'
|
||||
import { WorkIndex } from '@/components/portfolio/WorkIndex'
|
||||
import { getPosts } from '@/lib/posts'
|
||||
import { PostList } from '@/components/posts/PostList'
|
||||
import { hero } from '@/lib/portfolio'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Krishna Ayyalasomayajula — Systems Engineer',
|
||||
description: hero.subline,
|
||||
openGraph: {
|
||||
images: ['/og.png'],
|
||||
},
|
||||
}
|
||||
|
||||
export default async function HomePage() {
|
||||
const posts = await getPosts()
|
||||
const posts = (await getPosts()).slice(0, 4)
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-5xl px-6 py-10 sm:py-14">
|
||||
<section aria-labelledby="recent-heading" className="space-y-7">
|
||||
<div className="border-b border-border pb-5">
|
||||
<h1 id="recent-heading" className="heading-xl m-0 text-ink">
|
||||
Recent
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{posts.length > 0 ? (
|
||||
<PostList posts={posts} />
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<p className="font-mono text-xs uppercase tracking-[0.22em] text-ink-soft">No posts yet</p>
|
||||
<h3 className="heading-sm m-0 text-ink">The notebook is ready.</h3>
|
||||
<p className="m-0 max-w-xl text-sm leading-6 text-ink-soft">
|
||||
Add your first MDX post and it will show up here with the same polished card treatment.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
<div className="min-h-screen bg-blacksite-bg text-blacksite-text">
|
||||
<StatusBar />
|
||||
<PortfolioNav />
|
||||
<Hero />
|
||||
<FeaturedDossier />
|
||||
<WorkIndex />
|
||||
<Manifesto />
|
||||
<CapabilityRows />
|
||||
<OperatorProfile />
|
||||
<FieldNotes posts={posts} />
|
||||
<ContactTerminal />
|
||||
<ClosingCta />
|
||||
<CommandPalette />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { getPosts, getPost, tagToSlug } from '@/lib/posts'
|
||||
import { TableOfContents } from '@/components/posts/TableOfContents'
|
||||
import { ScrollToTop } from '@/components/ui/ScrollToTop'
|
||||
import { ReadingProgress } from '@/components/ui/ReadingProgress'
|
||||
|
||||
export const dynamicParams = false
|
||||
export const dynamic = 'force-static'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await getPosts()
|
||||
return posts.map((post) => ({ slug: post.slug }))
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const slug = (await params).slug
|
||||
const post = await getPost(slug)
|
||||
if (!post) return { title: 'Not Found' }
|
||||
return { title: post.title }
|
||||
}
|
||||
|
||||
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const slug = (await params).slug
|
||||
const post = await getPost(slug)
|
||||
|
||||
if (!post) notFound()
|
||||
|
||||
const { default: PostContent } = await import(
|
||||
/* turbopackOptional: true */
|
||||
`@/content/posts/${slug}.mdx`
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<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)_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>
|
||||
<h1 className="heading-xl text-ink mt-3 mb-2">
|
||||
{post.title}
|
||||
</h1>
|
||||
<div className="flex items-center gap-4 font-mono text-xs text-ink-soft">
|
||||
{post.author && <span>by {post.author}</span>}
|
||||
<span>{post.readingTime} min read</span>
|
||||
</div>
|
||||
{post.coverImage && (
|
||||
<img
|
||||
src={post.coverImage}
|
||||
alt={`Featured image for article: ${post.title}`}
|
||||
className="my-8 aspect-[16/9] w-full rounded-2xl border border-border bg-surface object-cover shadow-card"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
fetchPriority="high"
|
||||
/>
|
||||
)}
|
||||
{post.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{post.tags.map((tag) => (
|
||||
<Link key={tag} href={`/tags/${tagToSlug(tag)}/`} className="rounded-full bg-surface px-3 py-1 text-xs font-medium text-ink-soft border border-border transition-colors hover:border-accent/50 hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent">
|
||||
{tag}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<div className="prose prose-lg article-prose max-w-none min-w-0">
|
||||
<PostContent />
|
||||
</div>
|
||||
</article>
|
||||
<aside className="hidden lg:block">
|
||||
<TableOfContents />
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
20
app/sitemap.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { MetadataRoute } from 'next'
|
||||
import { getAllTags, getPosts } from '@/lib/posts'
|
||||
import { tagToSlug } from '@/lib/tags'
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://localhost:3000'
|
||||
|
||||
export const dynamic = 'force-static'
|
||||
|
||||
const absoluteUrl = (pathname: string): string => new URL(pathname, siteUrl).toString()
|
||||
|
||||
const [posts, tags] = await Promise.all([getPosts(), getAllTags()])
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [
|
||||
{ url: absoluteUrl('/') },
|
||||
{ url: absoluteUrl('/blog/') },
|
||||
...posts.map((post) => ({ url: absoluteUrl(`/blog/${post.slug}/`) })),
|
||||
...tags.map((tag) => ({ url: absoluteUrl(`/blog/tags/${tagToSlug(tag.label)}/`) })),
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-border mt-16 py-8 text-center">
|
||||
<p className="font-mono text-sm text-ink-soft">
|
||||
© {new Date().getFullYear()} Krishna A. All rights reserved.
|
||||
<p className="font-mono text-xs text-ink-soft">
|
||||
© {new Date().getFullYear()} Krishna Ayyalasomayajula
|
||||
</p>
|
||||
</footer>
|
||||
)
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { m } from "motion/react";
|
||||
import { ThemeToggle } from "@/components/ui/ThemeToggle";
|
||||
|
||||
const navLinks = [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "Posts", href: "/posts/" },
|
||||
{ label: "PORTFOLIO", href: "/" },
|
||||
{ label: "FIELD NOTES", href: "/blog/" },
|
||||
];
|
||||
|
||||
export function Header() {
|
||||
@@ -22,8 +21,11 @@ export function Header() {
|
||||
className="sticky top-0 z-50 bg-canvas/80 backdrop-blur-sm border-b border-border"
|
||||
>
|
||||
<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
|
||||
href="/"
|
||||
className="font-mono text-xs uppercase tracking-[0.16em] text-ink transition-colors hover:text-ink-soft"
|
||||
>
|
||||
KRISHNA / SIGNAL
|
||||
</Link>
|
||||
<div className="flex items-center gap-8">
|
||||
{navLinks.map((link) => {
|
||||
@@ -37,7 +39,7 @@ export function Header() {
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={`font-medium text-sm transition-colors ${
|
||||
className={`font-mono text-xs uppercase tracking-[0.16em] transition-colors ${
|
||||
isActive ? "text-ink" : "text-ink-soft hover:text-ink"
|
||||
}`}
|
||||
>
|
||||
@@ -45,7 +47,6 @@ export function Header() {
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</m.nav>
|
||||
|
||||
53
components/portfolio/CapabilityRows.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { m, useReducedMotion } from "motion/react";
|
||||
|
||||
import { capabilities } from "@/lib/portfolio";
|
||||
|
||||
export function CapabilityRows() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section
|
||||
id="capabilities"
|
||||
aria-labelledby="capabilities-heading"
|
||||
className="mx-auto max-w-7xl px-6 py-28 text-blacksite-text sm:px-8 sm:py-36 lg:px-12"
|
||||
>
|
||||
<div className="mb-12">
|
||||
<div>
|
||||
<p className="section-label mb-3">CAPABILITIES /.04</p>
|
||||
<h2
|
||||
id="capabilities-heading"
|
||||
className="font-display text-3xl font-semibold tracking-[-0.05em] text-blacksite-text sm:text-5xl"
|
||||
>
|
||||
Built for hostile constraints.
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-blacksite-line">
|
||||
{capabilities.map((capability, index) => (
|
||||
<m.div
|
||||
key={capability.index}
|
||||
className="grid gap-5 border-b border-blacksite-line py-6 sm:grid-cols-[1fr_auto] sm:items-center sm:py-8"
|
||||
initial={shouldReduceMotion ? false : { opacity: 0, y: 14 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-12%" }}
|
||||
transition={{
|
||||
duration: shouldReduceMotion ? 0 : 0.45,
|
||||
delay: shouldReduceMotion ? 0 : index * 0.07,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
<p className="font-display text-[clamp(28px,4.5vw,64px)] font-semibold leading-[0.95] tracking-[-0.06em] text-blacksite-text">
|
||||
{capability.text}
|
||||
</p>
|
||||
<p className="section-label text-left text-signal-orange sm:text-right">
|
||||
{capability.index}
|
||||
</p>
|
||||
</m.div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
84
components/portfolio/ClosingCta.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import Link from "next/link";
|
||||
import { contact } from "@/lib/portfolio";
|
||||
|
||||
const sectionLinks = [
|
||||
{ label: "Work", href: "#work" },
|
||||
{ label: "Capabilities", href: "#capabilities" },
|
||||
{ label: "Operator", href: "#operator" },
|
||||
{ label: "Contact", href: "#contact" },
|
||||
];
|
||||
|
||||
const elsewhereLinks = [
|
||||
{ label: "Git", href: contact.git },
|
||||
{ label: "LinkedIn", href: contact.linkedIn },
|
||||
];
|
||||
|
||||
export function ClosingCta() {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="border-t border-blacksite-line bg-blacksite-bg px-6 py-20 text-blacksite-text sm:px-8 sm:py-24 lg:px-12">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<a
|
||||
href="#work"
|
||||
className="group min-h-52 border border-blacksite-line bg-blacksite-surface p-6 transition-colors hover:border-signal-orange/70 focus-visible:outline-signal-orange sm:p-8"
|
||||
>
|
||||
<span className="font-display text-4xl font-semibold tracking-[-0.04em] text-blacksite-text transition-colors group-hover:text-signal-orange sm:text-6xl">
|
||||
View Work →
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={`mailto:${contact.email}`}
|
||||
className="group min-h-52 border border-blacksite-line bg-blacksite-surface p-6 transition-colors hover:border-signal-orange/70 focus-visible:outline-signal-orange sm:p-8"
|
||||
>
|
||||
<span className="font-display text-4xl font-semibold tracking-[-0.04em] text-blacksite-text transition-colors group-hover:text-signal-orange sm:text-6xl">
|
||||
Send Signal →
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-8 border border-blacksite-line bg-blacksite-surface2/35 p-6 sm:grid-cols-2 lg:grid-cols-3 lg:p-8">
|
||||
<nav aria-label="Footer sections">
|
||||
<p className="section-label mb-4">Sections</p>
|
||||
<ul className="grid gap-3 font-mono text-xs uppercase tracking-[0.14em] text-blacksite-muted">
|
||||
{sectionLinks.map((link) => (
|
||||
<li key={link.href}>
|
||||
<a href={link.href} className="transition-colors hover:text-signal-orange focus-visible:outline-signal-orange">
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<nav aria-label="Footer field notes">
|
||||
<p className="section-label mb-4">Field Notes</p>
|
||||
<Link href="/blog/" className="font-mono text-xs uppercase tracking-[0.14em] text-blacksite-muted transition-colors hover:text-signal-orange focus-visible:outline-signal-orange">
|
||||
/blog/
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<nav aria-label="Footer elsewhere">
|
||||
<p className="section-label mb-4">Elsewhere</p>
|
||||
<ul className="grid gap-3 font-mono text-xs uppercase tracking-[0.14em] text-blacksite-muted">
|
||||
{elsewhereLinks.map((link) => (
|
||||
<li key={link.href}>
|
||||
<a href={link.href} target="_blank" rel="noreferrer" className="transition-colors hover:text-signal-orange focus-visible:outline-signal-orange">
|
||||
{link.label} ↗
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-wrap items-center justify-between gap-4 font-mono text-xs uppercase tracking-[0.14em] text-blacksite-muted">
|
||||
<p>© {year}</p>
|
||||
<p>⌘K</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
207
components/portfolio/CommandPalette.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { contact } from "@/lib/portfolio";
|
||||
|
||||
type PaletteAction =
|
||||
| { kind: "scroll"; targetId: "work" | "capabilities" | "operator" | "contact" }
|
||||
| { kind: "route"; href: "/blog/" }
|
||||
| { kind: "copy-email" }
|
||||
| { kind: "external"; href: string };
|
||||
|
||||
const commands: {
|
||||
label: string;
|
||||
hint: string;
|
||||
keywords: string[];
|
||||
action: PaletteAction;
|
||||
}[] = [
|
||||
{
|
||||
label: "Go to Work",
|
||||
hint: "#work",
|
||||
keywords: ["projects", "dossiers", "selected work"],
|
||||
action: { kind: "scroll", targetId: "work" },
|
||||
},
|
||||
{
|
||||
label: "Go to Capabilities",
|
||||
hint: "#capabilities",
|
||||
keywords: ["skills", "systems", "kernel"],
|
||||
action: { kind: "scroll", targetId: "capabilities" },
|
||||
},
|
||||
{
|
||||
label: "Go to Operator",
|
||||
hint: "#operator",
|
||||
keywords: ["profile", "identity", "honors"],
|
||||
action: { kind: "scroll", targetId: "operator" },
|
||||
},
|
||||
{
|
||||
label: "Go to Contact",
|
||||
hint: "#contact",
|
||||
keywords: ["email", "signal"],
|
||||
action: { kind: "scroll", targetId: "contact" },
|
||||
},
|
||||
{
|
||||
label: "Open Field Notes",
|
||||
hint: "/blog/",
|
||||
keywords: ["blog", "notes", "archive"],
|
||||
action: { kind: "route", href: "/blog/" },
|
||||
},
|
||||
{
|
||||
label: "Copy Email",
|
||||
hint: "copy",
|
||||
keywords: [contact.email, "mail", "contact"],
|
||||
action: { kind: "copy-email" },
|
||||
},
|
||||
{
|
||||
label: "Open Git profile",
|
||||
hint: "git",
|
||||
keywords: ["source", "forge", "code"],
|
||||
action: { kind: "external", href: contact.git },
|
||||
},
|
||||
{
|
||||
label: "Open LinkedIn",
|
||||
hint: "in",
|
||||
keywords: ["profile", "social", "linkedin"],
|
||||
action: { kind: "external", href: contact.linkedIn },
|
||||
},
|
||||
];
|
||||
|
||||
export function CommandPalette() {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState<PaletteAction | null>(null);
|
||||
const lastFocusedElement = useRef<HTMLElement | null>(null);
|
||||
const wasOpen = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
function rememberFocus() {
|
||||
lastFocusedElement.current =
|
||||
document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||
}
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key.toLowerCase() !== "k" || (!event.metaKey && !event.ctrlKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
rememberFocus();
|
||||
setOpen((current) => !current);
|
||||
}
|
||||
|
||||
function onPaletteEvent() {
|
||||
rememberFocus();
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("blacksite:palette", onPaletteEvent);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("blacksite:palette", onPaletteEvent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasOpen.current && !open) {
|
||||
lastFocusedElement.current?.focus();
|
||||
}
|
||||
|
||||
wasOpen.current = open;
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function runAction(action: PaletteAction) {
|
||||
if (action.kind === "scroll") {
|
||||
document.getElementById(action.targetId)?.scrollIntoView({ block: "start" });
|
||||
}
|
||||
|
||||
if (action.kind === "route") {
|
||||
router.push(action.href);
|
||||
}
|
||||
|
||||
if (action.kind === "copy-email") {
|
||||
try {
|
||||
await navigator.clipboard.writeText(contact.email);
|
||||
} catch {
|
||||
// Clipboard failures should not strand the command dialog open.
|
||||
}
|
||||
}
|
||||
|
||||
if (action.kind === "external") {
|
||||
window.open(action.href, "_blank", "noopener");
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
setOpen(false);
|
||||
setPendingAction(null);
|
||||
}
|
||||
}
|
||||
|
||||
void runAction(pendingAction);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [pendingAction, router]);
|
||||
|
||||
return (
|
||||
<Command.Dialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
label="Signal Blacksite command palette"
|
||||
loop
|
||||
className="border border-blacksite-line bg-blacksite-surface text-blacksite-text shadow-[0_32px_120px_rgba(0,0,0,0.62)]"
|
||||
overlayClassName="fixed inset-0 z-[90] bg-black/72 backdrop-blur-sm"
|
||||
contentClassName="fixed left-1/2 top-[12vh] z-[100] w-[calc(100vw-2rem)] max-w-2xl -translate-x-1/2 overflow-hidden rounded-lg border border-blacksite-line bg-blacksite-surface shadow-[0_32px_120px_rgba(0,0,0,0.62)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-signal-cyan"
|
||||
>
|
||||
<div className="border-b border-blacksite-line px-4 py-3 sm:px-5">
|
||||
<p className="section-label">COMMAND PALETTE</p>
|
||||
<Command.Input
|
||||
autoFocus
|
||||
placeholder="Command…"
|
||||
className="mt-3 w-full bg-transparent font-mono text-base text-blacksite-text outline-none placeholder:text-blacksite-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-signal-cyan sm:text-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Command.List className="max-h-[min(24rem,60vh)] overflow-y-auto p-2 [scroll-padding-block:0.5rem]">
|
||||
<Command.Empty className="px-3 py-8 text-center font-mono text-xs uppercase tracking-[0.16em] text-blacksite-muted">
|
||||
No matching command.
|
||||
</Command.Empty>
|
||||
|
||||
<Command.Group
|
||||
heading="Navigation"
|
||||
className="[&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:pb-2 [&_[cmdk-group-heading]]:pt-3 [&_[cmdk-group-heading]]:font-mono [&_[cmdk-group-heading]]:text-[11px] [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-[0.16em] [&_[cmdk-group-heading]]:text-blacksite-muted"
|
||||
>
|
||||
{commands.map((command) => (
|
||||
<Command.Item
|
||||
key={command.label}
|
||||
value={command.label}
|
||||
keywords={command.keywords}
|
||||
onSelect={() => setPendingAction(command.action)}
|
||||
className="group flex cursor-pointer select-none items-center justify-between gap-4 rounded-md border border-transparent px-3 py-3 font-mono text-sm text-blacksite-text outline-none transition-colors data-[selected=true]:border-signal-cyan/60 data-[selected=true]:bg-signal-cyan/12 data-[selected=true]:text-signal-cyan data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50"
|
||||
>
|
||||
<span>{command.label}</span>
|
||||
<kbd className="shrink-0 rounded border border-blacksite-line bg-blacksite-bg2 px-2 py-1 text-[10px] uppercase tracking-[0.16em] text-blacksite-muted transition-colors group-data-[selected=true]:border-signal-cyan/50 group-data-[selected=true]:text-signal-cyan">
|
||||
{command.hint}
|
||||
</kbd>
|
||||
</Command.Item>
|
||||
))}
|
||||
</Command.Group>
|
||||
</Command.List>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-blacksite-line px-4 py-3 font-mono text-[11px] uppercase tracking-[0.16em] text-blacksite-muted sm:px-5">
|
||||
<span>↑↓ select</span>
|
||||
<span>Enter run · Esc close</span>
|
||||
</div>
|
||||
</Command.Dialog>
|
||||
);
|
||||
}
|
||||
102
components/portfolio/ContactTerminal.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { contact } from "@/lib/portfolio";
|
||||
|
||||
type CopyTarget = "email";
|
||||
type CopyStatus = "idle" | "copied" | "failed";
|
||||
|
||||
const rows = [
|
||||
{ label: "EMAIL", value: contact.email, kind: "copy" as const, target: "email" as CopyTarget },
|
||||
{ label: "GIT", value: contact.git, kind: "link" as const },
|
||||
{ label: "LINKEDIN", value: contact.linkedIn, kind: "link" as const },
|
||||
];
|
||||
|
||||
export function ContactTerminal() {
|
||||
const [copyState, setCopyState] = useState<Record<CopyTarget, CopyStatus>>({
|
||||
email: "idle",
|
||||
});
|
||||
const resetTimers = useRef<Partial<Record<CopyTarget, ReturnType<typeof setTimeout>>>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const timers = resetTimers.current;
|
||||
|
||||
return () => {
|
||||
Object.values(timers).forEach((timer) => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function copyValue(target: CopyTarget, value: string) {
|
||||
if (resetTimers.current[target]) {
|
||||
clearTimeout(resetTimers.current[target]);
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
setCopyState((current) => ({ ...current, [target]: "copied" }));
|
||||
} catch {
|
||||
setCopyState((current) => ({ ...current, [target]: "failed" }));
|
||||
}
|
||||
|
||||
resetTimers.current[target] = setTimeout(() => {
|
||||
setCopyState((current) => ({ ...current, [target]: "idle" }));
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="contact" aria-labelledby="contact-title" className="mx-auto max-w-7xl px-6 py-28 text-blacksite-text sm:px-8 sm:py-36 lg:px-12">
|
||||
<p className="section-label mb-12">CONTACT /.07</p>
|
||||
<div className="border border-blacksite-line bg-blacksite-surface shadow-[0_32px_120px_rgba(0,0,0,0.42)]">
|
||||
<div className="flex items-center gap-3 border-b border-blacksite-line px-5 py-4 sm:px-7">
|
||||
<span aria-hidden="true" className="size-2 rounded-full bg-signal-green shadow-[0_0_18px_rgba(182,255,0,0.5)]" />
|
||||
<p className="font-mono text-xs text-blacksite-muted">> contact.channel</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-0 lg:grid-cols-[0.8fr_1.2fr]">
|
||||
<div className="border-b border-blacksite-line px-5 py-8 sm:px-7 lg:border-b-0 lg:border-r lg:px-10">
|
||||
<h2 id="contact-title" className="font-display text-3xl font-semibold uppercase tracking-[-0.04em] text-blacksite-text sm:text-5xl">
|
||||
Send signal.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-blacksite-line">
|
||||
{rows.map((row) => (
|
||||
<div key={row.label} className="grid gap-3 px-5 py-6 sm:px-7 md:grid-cols-[7rem_minmax(0,1fr)] lg:px-8">
|
||||
<p className="section-label pt-1">{row.label}</p>
|
||||
{row.kind === "copy" ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyValue(row.target, row.value)}
|
||||
className="group flex min-w-0 items-center justify-between gap-4 text-left font-mono text-sm text-blacksite-text transition-colors hover:underline focus-visible:outline-blacksite-text"
|
||||
>
|
||||
<span className="break-all">{row.value}</span>
|
||||
<span className="shrink-0 text-[11px] uppercase tracking-[0.16em] text-blacksite-muted group-hover:text-blacksite-text">
|
||||
{copyState[row.target] === "copied" ? "copied" : copyState[row.target] === "failed" ? "copy failed" : "copy"}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href={row.value}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="group flex min-w-0 items-center justify-between gap-4 font-mono text-sm text-blacksite-text transition-colors hover:underline focus-visible:outline-blacksite-text"
|
||||
>
|
||||
<span className="break-all">{row.value}</span>
|
||||
<span aria-hidden="true" className="shrink-0 text-blacksite-muted transition-transform group-hover:translate-x-0.5 group-hover:text-blacksite-text">
|
||||
↗
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
92
components/portfolio/FeaturedDossier.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import { m, useReducedMotion } from "motion/react";
|
||||
import { featuredDossier } from "@/lib/portfolio";
|
||||
|
||||
const fieldRows = [
|
||||
{ label: "MISSION", value: featuredDossier.mission },
|
||||
{ label: "CONSTRAINT", value: featuredDossier.constraint ?? "—" },
|
||||
{ label: "SYSTEM", value: featuredDossier.system },
|
||||
{ label: "RESULT", value: featuredDossier.result },
|
||||
];
|
||||
|
||||
export function FeaturedDossier() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<m.section
|
||||
aria-labelledby="featured-dossier-title"
|
||||
initial={shouldReduceMotion ? false : { opacity: 0, y: 18 }}
|
||||
whileInView={shouldReduceMotion ? undefined : { opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
||||
viewport={{ once: true, amount: 0.18 }}
|
||||
className="mx-auto max-w-7xl px-6 py-28 text-blacksite-text sm:px-8 sm:py-36 lg:px-12"
|
||||
>
|
||||
<div className="mb-12 flex items-center justify-between gap-4">
|
||||
<p className="section-label">FEATURED /.01</p>
|
||||
<span aria-hidden="true" className="size-2 rounded-full bg-signal-cyan shadow-[0_0_22px_rgba(0,240,255,0.62)]" />
|
||||
</div>
|
||||
|
||||
<article className="overflow-hidden border border-blacksite-line bg-blacksite-surface shadow-[0_32px_120px_rgba(0,0,0,0.42)]">
|
||||
<div className="relative overflow-hidden border-b border-blacksite-line p-6 pb-0 sm:p-8 sm:pb-0">
|
||||
<div aria-hidden="true" className="absolute inset-x-0 top-0 h-px bg-signal-cyan/50" />
|
||||
<h2
|
||||
id="featured-dossier-title"
|
||||
className="font-display translate-y-4 text-[clamp(4.5rem,18vw,15rem)] font-semibold uppercase leading-[0.72] tracking-[-0.08em] text-blacksite-text"
|
||||
>
|
||||
{featuredDossier.title.toUpperCase()}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-0 lg:grid-cols-[minmax(0,1fr)_22rem]">
|
||||
<div className="divide-y divide-blacksite-line border-b border-blacksite-line lg:border-b-0 lg:border-r">
|
||||
{fieldRows.map((row) => (
|
||||
<div key={row.label} className="grid gap-3 p-6 sm:p-8 md:grid-cols-[9rem_minmax(0,1fr)]">
|
||||
<p className="section-label text-signal-cyan">{row.label}</p>
|
||||
<p className="text-sm leading-7 text-blacksite-muted sm:text-base">{row.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<aside className="bg-blacksite-surface2/45 p-6 sm:p-8" aria-label="Featured dossier details and links">
|
||||
<div className="border-b border-blacksite-line pb-6">
|
||||
<p className="font-mono text-xs uppercase tracking-[0.14em] text-blacksite-text">
|
||||
{featuredDossier.role} <span className="text-signal-cyan">·</span> {featuredDossier.year}
|
||||
</p>
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{featuredDossier.stack.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
className="border border-blacksite-line bg-blacksite-surface px-3 py-1 font-mono text-[11px] uppercase tracking-[0.14em] text-blacksite-muted"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6">
|
||||
<p className="section-label mb-3">LINKS</p>
|
||||
<div className="grid gap-3">
|
||||
{featuredDossier.links.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center justify-between gap-4 border border-blacksite-line px-4 py-3 font-mono text-xs uppercase tracking-[0.14em] text-blacksite-text transition-colors hover:border-signal-cyan/70 hover:text-signal-cyan focus-visible:outline-signal-cyan"
|
||||
>
|
||||
<span>{link.label}</span>
|
||||
<span aria-hidden="true" className="text-signal-cyan transition-transform group-hover:translate-x-0.5">
|
||||
↗
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</article>
|
||||
</m.section>
|
||||
);
|
||||
}
|
||||
75
components/portfolio/FieldNotes.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import Link from "next/link";
|
||||
import type { PostMeta } from "@/lib/posts";
|
||||
|
||||
type FieldNotesProps = {
|
||||
posts: PostMeta[];
|
||||
};
|
||||
|
||||
function formatPostDate(date: string) {
|
||||
const parsed = new Date(date);
|
||||
|
||||
if (Number.isNaN(parsed.getTime())) return date;
|
||||
|
||||
return parsed.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
export function FieldNotes({ posts }: FieldNotesProps) {
|
||||
const recentPosts = posts.slice(0, 4);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="notes"
|
||||
className="border-y border-blacksite-line bg-blacksite-bg2 px-6 py-28 text-blacksite-text sm:px-8 sm:py-36 lg:px-12"
|
||||
>
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-12">
|
||||
<p className="section-label">FIELD NOTES /.06</p>
|
||||
</div>
|
||||
|
||||
{recentPosts.length > 0 ? (
|
||||
<>
|
||||
<div className="divide-y divide-blacksite-line border-y border-blacksite-line">
|
||||
{recentPosts.map((post) => (
|
||||
<Link
|
||||
key={post.slug}
|
||||
href={`/blog/${post.slug}/`}
|
||||
className="group grid gap-3 px-0 py-6 transition-colors hover:bg-blacksite-surface/55 focus-visible:outline-signal-cyan sm:grid-cols-[12rem_minmax(0,1fr)] sm:px-4"
|
||||
>
|
||||
<time className="section-label pt-1" dateTime={post.date}>
|
||||
{formatPostDate(post.date)}
|
||||
</time>
|
||||
<span className="min-w-0">
|
||||
<span className="block font-display text-2xl font-medium tracking-[-0.03em] text-blacksite-text transition-colors group-hover:text-signal-cyan group-focus-visible:text-signal-cyan">
|
||||
{post.title}
|
||||
</span>
|
||||
{post.excerpt && (
|
||||
<span className="mt-2 block max-w-3xl text-sm leading-6 text-blacksite-muted">
|
||||
{post.excerpt}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 flex justify-end">
|
||||
<Link
|
||||
href="/blog/"
|
||||
className="section-label rounded-md border border-blacksite-line px-4 py-3 !text-blacksite-text transition-colors hover:border-signal-cyan/70 hover:!text-signal-cyan focus-visible:outline-signal-cyan"
|
||||
>
|
||||
All field notes →
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="border border-dashed border-blacksite-line bg-blacksite-surface/45 p-8 sm:p-10">
|
||||
<p className="section-label !text-blacksite-text">ARCHIVE EMPTY</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
85
components/portfolio/Hero.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { m, useReducedMotion } from "motion/react";
|
||||
import { hero } from "@/lib/portfolio";
|
||||
import { HeroCanvas } from "./HeroCanvas";
|
||||
|
||||
const paletteEventName = "blacksite:palette";
|
||||
|
||||
function openCommandPalette() {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent(paletteEventName));
|
||||
}
|
||||
|
||||
export function Hero() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const headingFragments = hero.heading
|
||||
.split(". ")
|
||||
.map((fragment, index, fragments) => {
|
||||
if (index < fragments.length - 1 && !fragment.endsWith(".")) {
|
||||
return `${fragment}.`;
|
||||
}
|
||||
|
||||
return fragment;
|
||||
});
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="hero-heading"
|
||||
className="relative isolate flex min-h-screen overflow-hidden bg-blacksite-bg px-5 py-28 text-blacksite-text sm:px-8 lg:px-12"
|
||||
>
|
||||
<HeroCanvas />
|
||||
<div className="absolute inset-0 z-0 bg-[linear-gradient(90deg,rgba(3,5,6,0.98),rgba(3,5,6,0.78)_52%,rgba(3,5,6,0.96))]" />
|
||||
|
||||
<m.div
|
||||
initial={shouldReduceMotion ? false : { opacity: 0, y: 18 }}
|
||||
animate={shouldReduceMotion ? undefined : { opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="relative z-10 mx-auto flex w-full max-w-7xl flex-col justify-center"
|
||||
>
|
||||
<div className="mb-8 inline-flex w-fit items-center gap-3 border border-blacksite-line bg-blacksite-surface/80 px-4 py-2 font-mono text-[11px] uppercase tracking-[0.16em] text-blacksite-muted shadow-[0_0_40px_rgba(0,0,0,0.35)] backdrop-blur-sm">
|
||||
<span className="h-2 w-2 rounded-full bg-signal-green shadow-[0_0_14px_rgba(182,255,0,0.75)]" />
|
||||
[STATUS: ONLINE]
|
||||
</div>
|
||||
|
||||
<h1
|
||||
id="hero-heading"
|
||||
className="max-w-6xl font-display text-[clamp(3rem,9vw,7.5rem)] font-medium leading-[0.92] tracking-[-0.05em] text-balance text-blacksite-text"
|
||||
>
|
||||
{headingFragments.map((fragment) => (
|
||||
<span key={fragment} className="block">
|
||||
{fragment}
|
||||
</span>
|
||||
))}
|
||||
</h1>
|
||||
|
||||
<p className="mt-6 max-w-2xl font-mono text-sm leading-7 text-blacksite-muted sm:text-base">
|
||||
{hero.subline}
|
||||
</p>
|
||||
|
||||
<div className="mt-10 flex flex-col gap-3 sm:flex-row">
|
||||
<a
|
||||
href="#work"
|
||||
className="group inline-flex items-center justify-center border border-blacksite-line border-l-signal-orange bg-blacksite-surface px-6 py-3 font-mono text-xs uppercase tracking-[0.16em] text-blacksite-text transition-colors hover:border-signal-orange hover:bg-blacksite-surface2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-signal-orange"
|
||||
>
|
||||
View Work
|
||||
<span className="ml-3 text-signal-orange" aria-hidden="true">
|
||||
/.
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={openCommandPalette}
|
||||
className="inline-flex items-center justify-center border border-blacksite-line bg-blacksite-bg2/85 px-6 py-3 font-mono text-xs uppercase tracking-[0.16em] text-blacksite-muted transition-colors hover:border-signal-orange hover:text-blacksite-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-signal-orange"
|
||||
>
|
||||
⌘K
|
||||
</button>
|
||||
</div>
|
||||
</m.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
206
components/portfolio/HeroCanvas.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useReducedMotion } from "motion/react";
|
||||
|
||||
const COLORS = {
|
||||
bg: "#030506",
|
||||
line: "rgba(44, 53, 58, 0.46)",
|
||||
dot: "rgba(244, 247, 248, 0.38)",
|
||||
cyan: "rgba(0, 240, 255, 0.82)",
|
||||
};
|
||||
|
||||
const GRID_COLUMNS = 14;
|
||||
const GRID_ROWS = 13;
|
||||
const PULSE_PERIOD_MS = 5200;
|
||||
|
||||
function projectX(width: number, xIndex: number, yIndex: number, time: number) {
|
||||
const normalizedX = (xIndex / (GRID_COLUMNS - 1) - 0.5) * 2;
|
||||
const normalizedY = yIndex / (GRID_ROWS - 1);
|
||||
const depth = normalizedY * normalizedY;
|
||||
|
||||
return (
|
||||
width * 0.5 +
|
||||
normalizedX * width * (0.13 + depth * 0.5) +
|
||||
Math.sin(time * 0.00008 + yIndex * 0.45) * 7
|
||||
);
|
||||
}
|
||||
|
||||
function projectY(height: number, yIndex: number, time: number) {
|
||||
const normalizedY = yIndex / (GRID_ROWS - 1);
|
||||
const drift = Math.sin(time * 0.00012 + yIndex * 0.7) * 5;
|
||||
|
||||
return height * 0.24 + height * 0.82 * normalizedY * normalizedY + drift;
|
||||
}
|
||||
|
||||
export function HeroCanvas() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = canvas.getContext("2d", { alpha: true });
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let dpr = 1;
|
||||
let frameId = 0;
|
||||
let isMounted = true;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const resize = () => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
width = Math.max(1, rect.width);
|
||||
height = Math.max(1, rect.height);
|
||||
canvas.width = Math.floor(width * dpr);
|
||||
canvas.height = Math.floor(height * dpr);
|
||||
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
if (shouldReduceMotion) {
|
||||
draw(0);
|
||||
}
|
||||
};
|
||||
|
||||
const drawGridLines = (time: number) => {
|
||||
context.lineWidth = 1;
|
||||
context.strokeStyle = COLORS.line;
|
||||
|
||||
for (let column = 0; column < GRID_COLUMNS; column += 1) {
|
||||
context.beginPath();
|
||||
for (let row = 0; row < GRID_ROWS; row += 1) {
|
||||
const x = projectX(width, column, row, time);
|
||||
const y = projectY(height, row, time);
|
||||
if (row === 0) {
|
||||
context.moveTo(x, y);
|
||||
} else {
|
||||
context.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
for (let row = 0; row < GRID_ROWS; row += 1) {
|
||||
context.beginPath();
|
||||
for (let column = 0; column < GRID_COLUMNS; column += 1) {
|
||||
const x = projectX(width, column, row, time);
|
||||
const y = projectY(height, row, time);
|
||||
if (column === 0) {
|
||||
context.moveTo(x, y);
|
||||
} else {
|
||||
context.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
context.stroke();
|
||||
}
|
||||
};
|
||||
|
||||
const drawDots = (time: number) => {
|
||||
context.fillStyle = COLORS.dot;
|
||||
|
||||
for (let row = 0; row < GRID_ROWS; row += 1) {
|
||||
const normalizedY = row / (GRID_ROWS - 1);
|
||||
const radius = 0.9 + normalizedY * 1.15;
|
||||
|
||||
for (let column = 0; column < GRID_COLUMNS; column += 1) {
|
||||
context.beginPath();
|
||||
context.arc(
|
||||
projectX(width, column, row, time),
|
||||
projectY(height, row, time),
|
||||
radius,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
);
|
||||
context.fill();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const drawPulse = (time: number) => {
|
||||
if (shouldReduceMotion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = (time % PULSE_PERIOD_MS) / PULSE_PERIOD_MS;
|
||||
const row = 8;
|
||||
const maxColumn = Math.min(
|
||||
GRID_COLUMNS - 1,
|
||||
Math.max(1, Math.floor(progress * (GRID_COLUMNS + 2))),
|
||||
);
|
||||
|
||||
context.save();
|
||||
context.globalAlpha = progress > 0.84 ? (1 - progress) / 0.16 : 1;
|
||||
context.lineWidth = 2;
|
||||
context.strokeStyle = COLORS.cyan;
|
||||
context.shadowBlur = 14;
|
||||
context.shadowColor = COLORS.cyan;
|
||||
context.beginPath();
|
||||
|
||||
for (let column = 0; column <= maxColumn; column += 1) {
|
||||
const x = projectX(width, column, row, time);
|
||||
const y = projectY(height, row, time);
|
||||
if (column === 0) {
|
||||
context.moveTo(x, y);
|
||||
} else {
|
||||
context.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
context.stroke();
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const draw = (time: number) => {
|
||||
context.clearRect(0, 0, width, height);
|
||||
context.fillStyle = COLORS.bg;
|
||||
context.fillRect(0, 0, width, height);
|
||||
drawGridLines(time);
|
||||
drawDots(time);
|
||||
drawPulse(time);
|
||||
};
|
||||
|
||||
const tick = (time: number) => {
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
draw(time);
|
||||
frameId = window.requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
resize();
|
||||
if ("ResizeObserver" in window) {
|
||||
resizeObserver = new ResizeObserver(resize);
|
||||
resizeObserver.observe(canvas);
|
||||
}
|
||||
window.addEventListener("resize", resize, { passive: true });
|
||||
|
||||
if (!shouldReduceMotion) {
|
||||
frameId = window.requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
resizeObserver?.disconnect();
|
||||
window.removeEventListener("resize", resize);
|
||||
if (frameId) {
|
||||
window.cancelAnimationFrame(frameId);
|
||||
}
|
||||
};
|
||||
}, [shouldReduceMotion]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 h-full w-full opacity-[0.4]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
52
components/portfolio/Manifesto.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { m, useReducedMotion } from "motion/react";
|
||||
|
||||
import { manifesto } from "@/lib/portfolio";
|
||||
|
||||
const SURVIVE_PHRASE = "survive contact";
|
||||
|
||||
function splitAccentPhrase(statement: string) {
|
||||
const phraseIndex = statement.indexOf(SURVIVE_PHRASE);
|
||||
|
||||
if (phraseIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
before: statement.slice(0, phraseIndex),
|
||||
after: statement.slice(phraseIndex + SURVIVE_PHRASE.length),
|
||||
};
|
||||
}
|
||||
|
||||
export function Manifesto() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const accentSplit = splitAccentPhrase(manifesto);
|
||||
|
||||
return (
|
||||
<m.section
|
||||
aria-labelledby="manifesto-heading"
|
||||
className="mx-auto max-w-7xl px-6 py-28 text-blacksite-muted sm:px-8 sm:py-36 lg:px-12"
|
||||
initial={shouldReduceMotion ? false : { opacity: 0, y: 18 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: shouldReduceMotion ? 0 : 0.6, ease: "easeOut" }}
|
||||
>
|
||||
<p className="section-label mb-12">PRINCIPLE /.03</p>
|
||||
<h2
|
||||
id="manifesto-heading"
|
||||
className="max-w-5xl font-display text-[clamp(2.25rem,6vw,4.5rem)] font-semibold leading-[1.05] tracking-[-0.06em] text-blacksite-muted"
|
||||
>
|
||||
{accentSplit ? (
|
||||
<>
|
||||
{accentSplit.before}
|
||||
<span className="text-signal-cyan">{SURVIVE_PHRASE}</span>
|
||||
{accentSplit.after}
|
||||
</>
|
||||
) : (
|
||||
manifesto
|
||||
)}
|
||||
</h2>
|
||||
</m.section>
|
||||
);
|
||||
}
|
||||
135
components/portfolio/OperatorProfile.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { honors, identity, publication, skillsMatrix } from "@/lib/portfolio";
|
||||
|
||||
export function OperatorProfile() {
|
||||
return (
|
||||
<section
|
||||
id="operator"
|
||||
className="bg-blacksite-bg px-6 py-28 text-blacksite-text sm:px-8 sm:py-36 lg:px-12"
|
||||
>
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="section-label mb-12">OPERATOR /.05</div>
|
||||
|
||||
<div className="overflow-hidden rounded-2xl border border-blacksite-line bg-blacksite-surface shadow-[0_24px_90px_rgba(0,0,0,0.35)]">
|
||||
<div className="relative h-72 overflow-hidden border-b border-blacksite-line bg-blacksite-bg2 sm:h-80">
|
||||
<img
|
||||
src="/banner.jpg"
|
||||
alt=""
|
||||
className="h-full w-full object-cover object-center grayscale contrast-125 opacity-70"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-t from-blacksite-bg via-blacksite-bg/55 to-transparent"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h2 className="absolute bottom-6 left-6 font-display text-3xl leading-none tracking-tight text-blacksite-text sm:bottom-8 sm:left-8 sm:text-5xl">
|
||||
{identity.name}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-px bg-blacksite-line lg:grid-cols-[1fr_1.15fr]">
|
||||
<div className="bg-blacksite-surface p-8 sm:p-10">
|
||||
<dl className="grid gap-5 text-sm text-blacksite-muted sm:text-base">
|
||||
<div>
|
||||
<dt className="section-label mb-2">ROLE</dt>
|
||||
<dd className="text-blacksite-text">{identity.roleLine}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="section-label mb-2">EDUCATION</dt>
|
||||
<dd>{identity.education}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="section-label mb-2">SITE</dt>
|
||||
<dd>{identity.site}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-px bg-blacksite-line">
|
||||
<article className="bg-blacksite-surface2 p-8 sm:p-10">
|
||||
<p className="section-label mb-4">PUBLICATION</p>
|
||||
<h3 className="font-display text-2xl leading-tight text-blacksite-text">
|
||||
{publication.title}
|
||||
</h3>
|
||||
<p className="mt-4 text-sm text-blacksite-muted sm:text-base">
|
||||
{publication.venue} · {publication.year} · {publication.authorship}
|
||||
</p>
|
||||
<a
|
||||
href={publication.href}
|
||||
className="section-label mt-6 inline-flex text-signal-cyan underline decoration-signal-cyan/35 underline-offset-4 transition-colors hover:text-blacksite-text focus-visible:outline-signal-cyan"
|
||||
>
|
||||
DOI: {publication.doi}
|
||||
</a>
|
||||
</article>
|
||||
|
||||
<div className="bg-blacksite-surface p-8 sm:p-10">
|
||||
<p className="section-label mb-5">HONORS</p>
|
||||
<ul className="grid gap-4">
|
||||
{honors.map((honor) => (
|
||||
<li
|
||||
key={`${honor.title}-${honor.year}`}
|
||||
className="grid gap-2 border-l border-signal-cyan/50 pl-4 sm:grid-cols-[auto_1fr] sm:items-baseline sm:gap-4"
|
||||
>
|
||||
<span className="section-label text-signal-cyan">
|
||||
{honor.year || "RECOGNITION"}
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-blacksite-text">{honor.title}</span>
|
||||
{honor.detail ? (
|
||||
<span className="text-blacksite-muted"> — {honor.detail}</span>
|
||||
) : null}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-blacksite-line bg-blacksite-bg2 p-8 sm:p-10">
|
||||
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<p className="section-label">SKILLS</p>
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden rounded-xl border border-blacksite-line">
|
||||
<table className="w-full border-collapse text-left text-sm sm:text-base">
|
||||
<thead className="bg-blacksite-surface2">
|
||||
<tr>
|
||||
<th scope="col" className="section-label px-4 py-3 sm:px-5">
|
||||
Domain
|
||||
</th>
|
||||
<th scope="col" className="section-label px-4 py-3 sm:px-5">
|
||||
Capabilities
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-blacksite-line bg-blacksite-surface">
|
||||
{skillsMatrix.map((row) => (
|
||||
<tr key={row.category}>
|
||||
<th
|
||||
scope="row"
|
||||
className="w-56 px-4 py-4 align-top font-mono text-xs uppercase tracking-[0.16em] text-blacksite-text sm:px-5"
|
||||
>
|
||||
{row.category}
|
||||
</th>
|
||||
<td className="px-4 py-4 text-blacksite-muted sm:px-5">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{row.skills.map((skill) => (
|
||||
<span
|
||||
key={skill}
|
||||
className="rounded-full border border-blacksite-line bg-blacksite-surface2 px-3 py-1 text-xs text-blacksite-text sm:text-sm"
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
78
components/portfolio/PortfolioNav.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const navLinks = [
|
||||
{ label: "WORK", href: "#work" },
|
||||
{ label: "CAPABILITIES", href: "#capabilities" },
|
||||
{ label: "FIELD NOTES", href: "/blog/" },
|
||||
{ label: "CONTACT", href: "#contact" },
|
||||
];
|
||||
|
||||
function openCommandPalette() {
|
||||
window.dispatchEvent(new CustomEvent("blacksite:palette"));
|
||||
}
|
||||
|
||||
export function PortfolioNav() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label="Portfolio navigation"
|
||||
className="sticky top-3 z-50 mx-auto mt-3 w-[calc(100%-2rem)] max-w-5xl rounded-xl border border-blacksite-line bg-blacksite-surface/82 px-6 py-2 text-blacksite-muted shadow-[0_18px_70px_rgba(0,0,0,0.38)] backdrop-blur-md sm:px-8 lg:px-12"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
aria-current={pathname === "/" ? "page" : undefined}
|
||||
className="section-label whitespace-nowrap !text-blacksite-text transition-colors hover:!text-signal-cyan focus-visible:outline-signal-cyan aria-[current=page]:!text-signal-cyan"
|
||||
>
|
||||
KRISHNA / SIGNAL
|
||||
</Link>
|
||||
|
||||
<div className="flex min-w-0 flex-1 items-center justify-end gap-1 sm:gap-2">
|
||||
<div className="flex min-w-0 items-center gap-1 overflow-x-auto">
|
||||
{navLinks.map((link) => {
|
||||
const isBlogActive =
|
||||
link.href === "/blog/" &&
|
||||
(pathname === "/blog" || pathname.startsWith("/blog/"));
|
||||
|
||||
if (link.href.startsWith("/")) {
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
aria-current={isBlogActive ? "page" : undefined}
|
||||
className="section-label rounded-md px-2 py-2 transition-colors hover:!text-signal-cyan focus-visible:outline-signal-cyan aria-[current=page]:!text-signal-cyan sm:px-3"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="section-label rounded-md px-2 py-2 transition-colors hover:!text-signal-cyan focus-visible:outline-signal-cyan active:!text-signal-cyan sm:px-3"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={openCommandPalette}
|
||||
className="section-label rounded-lg border border-blacksite-line bg-blacksite-surface2 px-3 py-2 !text-blacksite-text transition-colors hover:border-signal-cyan/70 hover:!text-signal-cyan focus-visible:outline-signal-cyan active:!text-signal-cyan"
|
||||
aria-label="Open command palette"
|
||||
>
|
||||
⌘K
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
23
components/portfolio/StatusBar.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { statusLine } from "@/lib/portfolio";
|
||||
|
||||
export function StatusBar() {
|
||||
return (
|
||||
<div className="border-b border-blacksite-line bg-blacksite-bg text-blacksite-muted">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-6 py-2 sm:px-8 lg:px-12">
|
||||
<p className="section-label flex min-w-0 items-center gap-2">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="size-1.5 shrink-0 rounded-full bg-signal-green shadow-[0_0_14px_rgba(182,255,0,0.45)]"
|
||||
/>
|
||||
<span className="truncate">{statusLine}</span>
|
||||
</p>
|
||||
<a
|
||||
href="#contact"
|
||||
className="section-label shrink-0 !text-blacksite-text transition-colors hover:!text-signal-cyan focus-visible:outline-signal-cyan"
|
||||
>
|
||||
Contact ↗
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
220
components/portfolio/WorkIndex.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { m, useReducedMotion } from "motion/react";
|
||||
|
||||
import { dossiers, type Dossier } from "@/lib/portfolio";
|
||||
|
||||
const activeText = "text-signal-green";
|
||||
const activeBorder = "border-l-signal-green";
|
||||
const activeGlow = "shadow-[0_0_32px_rgba(182,255,0,0.12)]";
|
||||
|
||||
function isLiveStatus(status: string) {
|
||||
return /\b(live|deployed)\b/i.test(status);
|
||||
}
|
||||
|
||||
function headlineProof(dossier: Dossier) {
|
||||
return dossier.proof.find((proof) => proof.metric) ?? dossier.proof[0];
|
||||
}
|
||||
|
||||
export function WorkIndex() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="work"
|
||||
aria-labelledby="work-title"
|
||||
className="mx-auto max-w-7xl px-6 py-28 text-blacksite-text sm:px-8 sm:py-36 lg:px-12"
|
||||
>
|
||||
<p id="work-title" className="section-label mb-12 text-blacksite-muted">
|
||||
WORK /.02
|
||||
</p>
|
||||
|
||||
<m.div
|
||||
initial={shouldReduceMotion ? false : { opacity: 0, y: 18 }}
|
||||
whileInView={shouldReduceMotion ? undefined : { opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-80px" }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
className="overflow-hidden border-y border-blacksite-line bg-blacksite-bg2/70"
|
||||
>
|
||||
<div className="section-label grid grid-cols-[minmax(0,1.35fr)_minmax(0,1fr)_minmax(0,0.8fr)_5rem] gap-4 border-b border-blacksite-line px-4 py-3 text-blacksite-muted max-md:hidden">
|
||||
<span>PROJECT</span>
|
||||
<span>PROOF</span>
|
||||
<span>TYPE</span>
|
||||
<span className="text-right">YEAR</span>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-blacksite-line">
|
||||
{dossiers.map((dossier, index) => {
|
||||
const isActive = activeId === dossier.id;
|
||||
const shouldDim = activeId !== null && !isActive;
|
||||
const proof = headlineProof(dossier);
|
||||
const detailsExpanded = shouldReduceMotion || isActive;
|
||||
const liveStatus = isLiveStatus(dossier.status);
|
||||
|
||||
return (
|
||||
<article
|
||||
key={dossier.id}
|
||||
tabIndex={0}
|
||||
onMouseEnter={() => setActiveId(dossier.id)}
|
||||
onMouseLeave={() => setActiveId(null)}
|
||||
onFocus={() => setActiveId(dossier.id)}
|
||||
onBlur={(event) => {
|
||||
if (
|
||||
!event.currentTarget.contains(
|
||||
event.relatedTarget as Node | null,
|
||||
)
|
||||
) {
|
||||
setActiveId(null);
|
||||
}
|
||||
}}
|
||||
className={`group border-l-2 border-l-transparent bg-blacksite-bg2/20 px-4 py-6 outline-none transition duration-200 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-signal-green ${
|
||||
isActive ? `${activeBorder} bg-blacksite-surface/70 ${activeGlow}` : ""
|
||||
} ${shouldDim ? "opacity-45" : "opacity-100"}`}
|
||||
>
|
||||
<div className="grid gap-3 md:grid-cols-[minmax(0,1.35fr)_minmax(0,1fr)_minmax(0,0.8fr)_5rem] md:items-start md:gap-4">
|
||||
<div className="min-w-0">
|
||||
<p className="section-label mb-1 text-blacksite-muted md:hidden">
|
||||
PROJECT
|
||||
</p>
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="font-mono text-xs text-blacksite-muted">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<h3
|
||||
className={`font-display text-2xl leading-none tracking-[-0.03em] transition-colors duration-200 ${
|
||||
isActive ? activeText : "text-blacksite-text"
|
||||
}`}
|
||||
>
|
||||
{dossier.title}
|
||||
</h3>
|
||||
{dossier.accent === "green" ? (
|
||||
<span aria-hidden="true" className="size-2 rounded-full bg-signal-green shadow-[0_0_14px_rgba(182,255,0,0.45)]" />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="section-label mb-1 text-blacksite-muted md:hidden">
|
||||
PROOF
|
||||
</p>
|
||||
<p className="font-mono text-xs uppercase tracking-[0.16em] text-signal-green">
|
||||
{dossier.proofHeadline}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<p className="section-label mb-1 text-blacksite-muted md:hidden">
|
||||
TYPE
|
||||
</p>
|
||||
<p className="text-sm leading-5 text-blacksite-muted">
|
||||
{dossier.type}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="md:text-right">
|
||||
<p className="section-label mb-1 text-blacksite-muted md:hidden">
|
||||
YEAR
|
||||
</p>
|
||||
<p className="font-mono text-xs text-blacksite-muted">
|
||||
{dossier.year}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<m.div
|
||||
initial={false}
|
||||
animate={
|
||||
detailsExpanded
|
||||
? { opacity: 1, height: "auto" }
|
||||
: { opacity: 0, height: 0 }
|
||||
}
|
||||
transition={
|
||||
shouldReduceMotion
|
||||
? { duration: 0 }
|
||||
: { duration: 0.22, ease: "easeOut" }
|
||||
}
|
||||
className="overflow-hidden [@media_(hover:none)]:!h-auto [@media_(hover:none)]:!opacity-100"
|
||||
>
|
||||
<div className="grid gap-5 pt-5 md:grid-cols-[minmax(0,1.35fr)_minmax(0,1fr)_minmax(0,0.8fr)_5rem] md:gap-4">
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<p className="section-label">MISSION</p>
|
||||
<p className="text-sm leading-6 text-blacksite-text">
|
||||
{dossier.mission}
|
||||
</p>
|
||||
{dossier.constraint ? (
|
||||
<p className="text-xs leading-5 text-blacksite-muted">
|
||||
Constraint: {dossier.constraint}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="section-label">STATUS</p>
|
||||
<p className="flex items-center gap-2 font-mono text-xs uppercase tracking-[0.16em] text-blacksite-muted">
|
||||
{liveStatus ? (
|
||||
<span aria-hidden="true" className="size-2 rounded-full bg-signal-green shadow-[0_0_14px_rgba(182,255,0,0.45)]" />
|
||||
) : null}
|
||||
{dossier.status}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="section-label">STACK</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{dossier.stack.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
className="border border-blacksite-line bg-blacksite-surface px-2 py-1 font-mono text-[11px] uppercase tracking-[0.14em] text-blacksite-muted"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 md:text-right">
|
||||
<p className="section-label">PROOF</p>
|
||||
<p
|
||||
className={`font-display text-xl leading-none ${activeText}`}
|
||||
>
|
||||
{proof?.metric ?? proof?.label ?? dossier.result}
|
||||
</p>
|
||||
<p className="text-xs leading-5 text-blacksite-muted">
|
||||
{proof?.metric ? proof.label : dossier.result}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 md:col-span-4">
|
||||
<p className="section-label">LINKS</p>
|
||||
{dossier.links.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{dossier.links.map((link) => (
|
||||
<a
|
||||
key={`${dossier.id}-${link.href}-${link.label}`}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="border border-blacksite-line bg-blacksite-surface2 px-3 py-2 font-mono text-xs uppercase tracking-[0.14em] text-blacksite-text transition-colors hover:border-signal-green hover:text-signal-green focus-visible:outline-signal-green"
|
||||
>
|
||||
{link.label} ↗
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="font-mono text-xs uppercase tracking-[0.14em] text-blacksite-muted">
|
||||
Closed source
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</m.div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</m.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -3,18 +3,7 @@
|
||||
import Link from 'next/link';
|
||||
import { m, useReducedMotion } from 'motion/react';
|
||||
import type { PostMeta } from '@/lib/posts';
|
||||
|
||||
const tagToClientSlug = (tag: string): string =>
|
||||
tag
|
||||
.normalize('NFKD')
|
||||
.toLowerCase()
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/\+/g, ' plus ')
|
||||
.replace(/#/g, ' sharp ')
|
||||
.replace(/[\\/]+/g, ' ')
|
||||
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
||||
.replace(/-{2,}/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
import { tagToSlug } from '@/lib/tags';
|
||||
|
||||
export function PostCard({ slug, title, date, excerpt, tags = [], author, readingTime, index = 0, coverImage }: PostMeta & { index?: number }) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
@@ -26,11 +15,11 @@ export function PostCard({ slug, title, date, excerpt, tags = [], author, readin
|
||||
whileHover={shouldReduceMotion ? undefined : { y: -2 }}
|
||||
transition={{ duration: 0.35, delay: shouldReduceMotion ? 0 : Math.min(index * 0.04, 0.18), ease: [0.22, 1, 0.36, 1] }}
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
className="group relative scroll-mt-20 rounded-2xl border border-border/90 bg-canvas p-5 shadow-card transition-[background-color,border-color,box-shadow] duration-200 ease-out hover:border-ink/25 hover:bg-surface/40 hover:shadow-card-hover dark:hover:border-ink/35 sm:p-7"
|
||||
className="group relative scroll-mt-20 rounded-lg border border-border/90 bg-canvas p-5 shadow-card transition-[background-color,border-color,box-shadow] duration-200 ease-out hover:border-ink/25 hover:bg-surface/40 hover:shadow-card-hover dark:hover:border-ink/35 sm:p-7"
|
||||
>
|
||||
<Link href={`/posts/${slug}/`} className="block">
|
||||
<Link href={`/blog/${slug}/`} className="block">
|
||||
{coverImage && (
|
||||
<div className="mb-5 overflow-hidden rounded-xl border border-border/80 bg-surface">
|
||||
<div className="mb-5 overflow-hidden rounded-md border border-border/80 bg-surface">
|
||||
<img
|
||||
src={coverImage}
|
||||
alt={`Cover image for ${title}`}
|
||||
@@ -56,7 +45,7 @@ export function PostCard({ slug, title, date, excerpt, tags = [], author, readin
|
||||
{tags && tags.length > 0 && (
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{tags.slice(0, 3).map((tag) => (
|
||||
<Link key={tag} href={`/tags/${tagToClientSlug(tag)}/`} className="rounded-full border border-border bg-surface/70 px-3 py-1 text-xs text-ink-soft transition-colors duration-200 hover:border-accent/60 hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent">
|
||||
<Link key={tag} href={`/blog/tags/${tagToSlug(tag)}/`} className="rounded-sm border border-border bg-surface/70 px-3 py-1 font-mono text-[11px] uppercase tracking-wider text-ink-soft transition-colors duration-200 hover:border-accent/60 hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent">
|
||||
{tag}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import { tagToSlug } from '@/lib/tags'
|
||||
import { PostCard } from './PostCard'
|
||||
|
||||
type SearchPost = {
|
||||
@@ -37,15 +38,6 @@ const normalizeSearchText = (value: string): string =>
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
|
||||
const tagToClientSlug = (tag: string): string =>
|
||||
normalizeSearchText(tag)
|
||||
.replace(/\+/g, ' plus ')
|
||||
.replace(/#/g, ' sharp ')
|
||||
.replace(/[\\/]+/g, ' ')
|
||||
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
||||
.replace(/-{2,}/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
|
||||
const getOrderedCharacterScore = (field: string, query: string): number => {
|
||||
if (!query) return 0
|
||||
|
||||
@@ -100,7 +92,7 @@ const getTagFilters = (posts: SearchPost[]): TagFilter[] => {
|
||||
const postTags = new Set<string>()
|
||||
|
||||
post.tags.forEach((tag) => {
|
||||
const slug = tagToClientSlug(tag)
|
||||
const slug = tagToSlug(tag)
|
||||
if (!slug || postTags.has(slug)) return
|
||||
|
||||
postTags.add(slug)
|
||||
@@ -130,7 +122,7 @@ export function PostSearch({ posts }: PostSearchProps) {
|
||||
|
||||
const results = useMemo(() => {
|
||||
const scoredPosts = posts.reduce<ScoredPost[]>((matches, post, index) => {
|
||||
if (selectedTag && !post.tags.some((tag) => tagToClientSlug(tag) === selectedTag)) {
|
||||
if (selectedTag && !post.tags.some((tag) => tagToSlug(tag) === selectedTag)) {
|
||||
return matches
|
||||
}
|
||||
|
||||
@@ -155,7 +147,7 @@ export function PostSearch({ posts }: PostSearchProps) {
|
||||
|
||||
return (
|
||||
<section aria-labelledby="posts-search-heading" className="space-y-8">
|
||||
<div className="space-y-5 rounded-xl border border-border bg-surface/30 p-4 sm:p-5">
|
||||
<div className="space-y-5 rounded-lg border border-border bg-surface/30 p-4 sm:p-5">
|
||||
<div className="space-y-2">
|
||||
<label id="posts-search-heading" htmlFor="post-search" className="block text-sm font-medium text-ink">
|
||||
Search posts
|
||||
@@ -166,7 +158,7 @@ export function PostSearch({ posts }: PostSearchProps) {
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search title, excerpt, tag, author, or slug"
|
||||
placeholder="Search…"
|
||||
className="min-w-0 flex-1 rounded-lg border border-border bg-canvas px-3 py-2 text-sm text-ink outline-none transition-colors placeholder:text-ink-soft/70 focus:border-accent focus:ring-2 focus:ring-accent/20"
|
||||
/>
|
||||
<button
|
||||
@@ -175,7 +167,7 @@ export function PostSearch({ posts }: PostSearchProps) {
|
||||
disabled={!query}
|
||||
className="rounded-lg border border-border px-3 py-2 text-sm text-ink-soft transition-colors hover:border-accent/50 hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
Clear search
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,7 +185,7 @@ export function PostSearch({ posts }: PostSearchProps) {
|
||||
type="button"
|
||||
aria-pressed={isSelected}
|
||||
onClick={() => setSelectedTag(isSelected ? null : tag.slug)}
|
||||
className="rounded-full border border-border bg-canvas px-3 py-1 text-xs text-ink-soft transition-colors hover:border-accent/50 hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent aria-pressed:border-accent aria-pressed:bg-accent/10 aria-pressed:text-accent"
|
||||
className="rounded-sm border border-border bg-canvas px-3 py-1 font-mono text-[11px] uppercase tracking-wider text-ink-soft transition-colors hover:border-accent/50 hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent aria-pressed:border-accent aria-pressed:bg-accent/10 aria-pressed:text-accent"
|
||||
>
|
||||
{tag.label} <span className="font-mono text-ink-soft/70">{tag.count}</span>
|
||||
</button>
|
||||
@@ -217,7 +209,7 @@ export function PostSearch({ posts }: PostSearchProps) {
|
||||
}}
|
||||
className="self-start rounded-lg border border-border px-3 py-1.5 text-sm text-ink-soft transition-colors hover:border-accent/50 hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent sm:self-auto"
|
||||
>
|
||||
Clear filters
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -230,7 +222,7 @@ export function PostSearch({ posts }: PostSearchProps) {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-ink-soft">No posts match your search.</p>
|
||||
<p className="text-ink-soft">No matches.</p>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -75,8 +75,8 @@ export function TableOfContents() {
|
||||
|
||||
return (
|
||||
<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 className="section-label mb-3">
|
||||
INDEX
|
||||
</h4>
|
||||
<ul className="space-y-1">
|
||||
{headings.map((heading) => (
|
||||
|
||||
@@ -8,11 +8,11 @@ interface CalloutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const iconMap: Record<string, string> = {
|
||||
note: 'ℹ️',
|
||||
tip: '💡',
|
||||
warning: '⚠️',
|
||||
danger: '🚫',
|
||||
const tagMap: Record<string, string> = {
|
||||
note: 'NOTE',
|
||||
tip: 'TIP',
|
||||
warning: 'WARNING',
|
||||
danger: 'DANGER',
|
||||
};
|
||||
|
||||
const typeClasses: Record<string, string> = {
|
||||
@@ -27,12 +27,14 @@ export function Callout({ type = 'note', title, children }: CalloutProps) {
|
||||
|
||||
return (
|
||||
<div className={className} role="note">
|
||||
{title && (
|
||||
<div className="callout-title">
|
||||
<span className="callout-icon">{iconMap[type] || iconMap.note}</span>
|
||||
<div className="callout-title">
|
||||
<span className="text-[11px] font-mono tracking-[0.16em]">
|
||||
{tagMap[type] || tagMap.note}
|
||||
</span>
|
||||
{title && (
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
<div className="callout-content">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "@wrksz/themes/client";
|
||||
import { m } from "motion/react";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<m.button
|
||||
type="button"
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="p-2 rounded-lg border border-border bg-canvas text-ink hover:border-ink transition-colors"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
)}
|
||||
</m.button>
|
||||
);
|
||||
}
|
||||
228
content/rendering-test-card.mdx
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
title: "The Rendering Test Card"
|
||||
date: "2026-07-06"
|
||||
excerpt: "Every element this pipeline renders, on one page."
|
||||
tags: [meta, mdx, typography]
|
||||
author: "Krishna"
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Signal grammar
|
||||
|
||||
This card is a field check, not a showpiece. It asks the renderer to carry ordinary prose, sharp metadata, and the odd piece of tactical noise without losing the thread. If the page looks calm under load, the pipeline can be trusted. If it flinches here, it will flinch in production.
|
||||
|
||||
Read the text like a watch log. **Strong claims are reserved for verified contact.** *Emphasis marks drift, not drama.* ~~Dead assertions stay visible until the after-action report is signed.~~ Smart punctuation should land cleanly — “quotes” should curl, and a hard break should not feel like a crash. The internal archive lives at [the field notes index](/blog/). The outside manual can sit at [rehype-pretty-code](https://rehype-pretty.pages.dev/). A bare link should wake up by itself: https://example.com/rendering-test-card.
|
||||
|
||||
The voice here stays terse. Each paragraph names a capability, applies pressure, and moves. That is the house rule: fewer ornaments, more bearing. A page can be severe without becoming sterile. The interesting test is whether small UI affordances remain legible when the copy refuses to decorate them.
|
||||
|
||||
---
|
||||
|
||||
## Lists
|
||||
|
||||
### Inventory
|
||||
|
||||
Unordered checks keep the scan loose:
|
||||
|
||||
- Confirm the route exports as a static post.
|
||||
- Confirm the prose measure stays narrow enough for a long read.
|
||||
- Confirm links, code, math, and admonitions agree on the same blacksite language.
|
||||
- Nested notes should indent, not collapse.
|
||||
- Secondary bullets should feel like subordinate evidence.
|
||||
- Confirm the page still works when there is no cover image.
|
||||
|
||||
Ordered checks keep the operator honest:
|
||||
|
||||
1. Build the archive.
|
||||
2. Open the rendered HTML.
|
||||
3. Search for the artifacts the pipeline promises.
|
||||
1. Highlighted code lines.
|
||||
2. KaTeX nodes.
|
||||
3. Footnote anchors.
|
||||
4. Callout wrappers.
|
||||
4. Record only what happened.
|
||||
|
||||
Task lists expose the GFM path:
|
||||
|
||||
- [x] Write the proving document.
|
||||
- [x] Include nested structure.
|
||||
- [ ] Re-run after any renderer change.
|
||||
- [ ] Delete nothing until the replacement has better evidence.
|
||||
|
||||
---
|
||||
|
||||
## Code
|
||||
|
||||
### Highlight pressure
|
||||
|
||||
The first fence checks title, caption, line highlights, range highlights, and character highlights in one pass. The word `signal` should glow wherever the highlighter is told to track it.
|
||||
|
||||
```ts title="lib/signal.ts" caption="Line + char highlights" {2,4-5} /signal/
|
||||
type Packet = { channel: string; signal: number; sealed: boolean };
|
||||
|
||||
export function normalizeSignal(packet: Packet) {
|
||||
const signal = Math.max(0, Math.min(packet.signal, 1));
|
||||
const channel = packet.channel.trim().toLowerCase();
|
||||
return { ...packet, channel, signal, sealed: true };
|
||||
}
|
||||
```
|
||||
|
||||
The second fence checks a custom starting line number. It should begin at ten and still keep the gutter stable.
|
||||
|
||||
```tsx showLineNumbers{10}
|
||||
export function StatusGlyph({ hot }: { hot: boolean }) {
|
||||
return (
|
||||
<span data-state={hot ? "hot" : "cold"}>
|
||||
{hot ? "ONLINE" : "QUIET"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Shell output should remain compact and copyable.
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
grep -l "data-highlighted-line" out/blog/rendering-test-card/index.html
|
||||
```
|
||||
|
||||
Diff blocks stay plain. No transformer-only notation is allowed here.
|
||||
|
||||
```diff
|
||||
- status: ornamental
|
||||
+ status: verified
|
||||
- proof: implied
|
||||
+ proof: linked
|
||||
```
|
||||
|
||||
ANSI logs are ugly by design; the renderer should still box them cleanly.
|
||||
|
||||
```ansi
|
||||
\x1b[36m[signal]\x1b[0m route compiled
|
||||
\x1b[32m[ok]\x1b[0m artifacts exported
|
||||
\x1b[33m[warn]\x1b[0m rerun after CSS changes
|
||||
```
|
||||
|
||||
Inline code should also carry language metadata: `[1,2,3].join("-"){:js}`. If that tiny token renders with the same discipline as the larger fences, the small print is doing its job.
|
||||
|
||||
---
|
||||
|
||||
## Math
|
||||
|
||||
### Compact proof
|
||||
|
||||
Inline math should sit inside the sentence without shaking the baseline: $e^{i\pi}+1=0$. The sentence remains prose; the equation is just a hard stop.
|
||||
|
||||
Display math gets its own clearing. The matrix below is simple, but it is enough to prove that block rendering, centering, and KaTeX styles all survived the export.
|
||||
|
||||
$$
|
||||
\begin{bmatrix}
|
||||
1 & 0 & 0 \\
|
||||
0 & \cos\theta & -\sin\theta \\
|
||||
0 & \sin\theta & \cos\theta
|
||||
\end{bmatrix}
|
||||
\begin{bmatrix}
|
||||
x \\
|
||||
y \\
|
||||
z
|
||||
\end{bmatrix}
|
||||
=
|
||||
\begin{bmatrix}
|
||||
x \\
|
||||
y\cos\theta - z\sin\theta \\
|
||||
y\sin\theta + z\cos\theta
|
||||
\end{bmatrix}
|
||||
$$
|
||||
|
||||
The goal is not mathematical density. The goal is verifying that a technical note can carry the notation it needs without importing a new surface.
|
||||
|
||||
---
|
||||
|
||||
## Callouts
|
||||
|
||||
### Directive sweep
|
||||
|
||||
:::note{title="Renderer note"}
|
||||
The note block marks neutral intelligence. It should accept a title, preserve block spacing, and render like part of the system rather than a pasted warning label.
|
||||
|
||||
This second paragraph proves container directives can carry more than one child. The gap between paragraphs matters because real notes often contain both the condition and the reason.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Use the smallest artifact that proves the path. A single post can test more of the stack than a dozen screenshots if it names the contracts directly.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Syntax that depends on an unconfigured transformer is a trap. Plain language fences are boring. Boring is stable.
|
||||
:::
|
||||
|
||||
:::danger
|
||||
Do not fix a content failure by changing the pipeline from this post. The test card is a witness. It is not a mechanic.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Table
|
||||
|
||||
### Alignment grid
|
||||
|
||||
| Element | Contract | Evidence |
|
||||
| :--- | :---: | ---: |
|
||||
| Code | highlighted line + copy chrome | built HTML |
|
||||
| Math | inline + display KaTeX | `katex` |
|
||||
| Footnotes | backrefs and labels | `data-footnote` |
|
||||
| Callouts | four directive variants | `callout` |
|
||||
| Media | body banner, no cover crop | `/banner.jpg` |
|
||||
|
||||
Tables are where spacing failures show up first. Left labels, centered contracts, and right-weighted evidence should all remain readable without becoming a spreadsheet costume.
|
||||
|
||||
---
|
||||
|
||||
## Quotes & Footnotes
|
||||
|
||||
### Chain of custody
|
||||
|
||||
> The first rule is to preserve the signal.
|
||||
>
|
||||
> > The second rule is to mark every repeater that touched it.
|
||||
>
|
||||
> A quote with a nested quote should feel like a transcript, not a decoration.
|
||||
|
||||
Footnotes carry side-channel evidence without derailing the main line.[^1] A second footnote proves numbering, backlinks, and spacing after multiple references.[^2] The prose should not pretend the notes are invisible. They are part of the record, just filed below the fold.
|
||||
|
||||
[^1]: The first note verifies the GFM footnote path and the styled reference marker in the paragraph.
|
||||
[^2]: The second note verifies ordered footnote output and return links after export.
|
||||
|
||||
---
|
||||
|
||||
## Details
|
||||
|
||||
### Collapsible packet
|
||||
|
||||
<details>
|
||||
<summary>Open the sealed appendix</summary>
|
||||
|
||||
Inside the disclosure sits the low-priority material: rerun instructions, caveats, and the kind of context that should be available without owning the first read. The summary line must stay keyboard reachable. The body must keep normal prose spacing. The border should match the rest of the blacksite shell.
|
||||
|
||||
</details>
|
||||
|
||||
This section is deliberately small. A details block is not a drawer for hiding bad structure. It is a pressure valve for material that belongs nearby but not inline.
|
||||
|
||||
---
|
||||
|
||||
## Media
|
||||
|
||||
### Banner and controls
|
||||
|
||||
<figure>
|
||||
|
||||

|
||||
|
||||
<figcaption>Signal banner: wide body media, uncropped by the post header.</figcaption>
|
||||
</figure>
|
||||
|
||||
The banner belongs in the body because the card needs to prove ordinary image rendering, not cover-image cropping. The ratio is long and low. Let it stay that way. If the image fills the measure, rounds its corners, and keeps a readable caption, the media path is aligned with the rest of the archive.
|
||||
|
||||
Keyboard hints should remain tactile: <kbd>⌘</kbd> <kbd>K</kbd>. They are tiny controls, but tiny controls are where aesthetic systems usually leak. This one should look like hardware, not a pill from another site.
|
||||
|
||||
End of card. The route has now touched typography, lists, code, math, callouts, tables, quotes, footnotes, details, media, and keyboard chrome. Any future renderer change can come back here and ask the same blunt question: did the signal survive contact?
|
||||
@@ -3,8 +3,12 @@ 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') {
|
||||
// Handle textDirective, leafDirective, and containerDirective callouts
|
||||
if (
|
||||
node.type === 'textDirective' ||
|
||||
node.type === 'leafDirective' ||
|
||||
node.type === 'containerDirective'
|
||||
) {
|
||||
const name = node.name;
|
||||
|
||||
// Only handle known callout types
|
||||
@@ -12,18 +16,17 @@ export default function calloutDirective() {
|
||||
if (!validTypes.includes(name)) return;
|
||||
|
||||
// Extract attributes (e.g., title="...")
|
||||
const attributes = node.attributes || [];
|
||||
const attrs = attributes.map((attr) => ({
|
||||
const attrs = Object.entries(node.attributes ?? {}).map(([name, value]) => ({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: attr.name,
|
||||
value: attr.value,
|
||||
name,
|
||||
value,
|
||||
}));
|
||||
|
||||
// Add type attribute
|
||||
attrs.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'type',
|
||||
value: { type: 'mdxFlowExpression', value: `"${name}"` },
|
||||
value: name,
|
||||
});
|
||||
|
||||
// Build children from node's children
|
||||
|
||||
384
lib/portfolio.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
export type Accent = "cyan" | "orange" | "green" | "violet";
|
||||
|
||||
export type Proof = {
|
||||
label: string;
|
||||
href?: string;
|
||||
metric?: string;
|
||||
};
|
||||
|
||||
export type DossierLink = {
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type Dossier = {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
status: string;
|
||||
year: string;
|
||||
role?: string;
|
||||
stack: string[];
|
||||
mission: string;
|
||||
constraint?: string;
|
||||
system: string;
|
||||
result: string;
|
||||
proofHeadline: string;
|
||||
proof: Proof[];
|
||||
links: DossierLink[];
|
||||
accent: Accent;
|
||||
};
|
||||
|
||||
export type Capability = {
|
||||
index: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type Honor = {
|
||||
title: string;
|
||||
detail: string;
|
||||
year: string;
|
||||
};
|
||||
|
||||
export type Identity = {
|
||||
name: string;
|
||||
roleLine: string;
|
||||
education: string;
|
||||
site: string;
|
||||
};
|
||||
|
||||
export type Hero = {
|
||||
heading: string;
|
||||
subline: string;
|
||||
};
|
||||
|
||||
export type Publication = {
|
||||
title: string;
|
||||
venue: string;
|
||||
year: string;
|
||||
authorship: string;
|
||||
doi: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type SkillsMatrixRow = {
|
||||
category: string;
|
||||
skills: string[];
|
||||
};
|
||||
|
||||
export type Contact = {
|
||||
email: string;
|
||||
git: string;
|
||||
linkedIn: string;
|
||||
};
|
||||
|
||||
export const identity: Identity = {
|
||||
name: "Krishna Ayyalasomayajula",
|
||||
roleLine: "Founding Engineer @ CoGuide",
|
||||
education: "Computational Engineering, UT Austin",
|
||||
site: "krishna.ayyalasomayajula.net",
|
||||
};
|
||||
|
||||
export const statusLine = "BUILDING AT COGUIDE";
|
||||
|
||||
export const hero: Hero = {
|
||||
heading: "GPU pipelines. Hardened systems. Real-time infrastructure.",
|
||||
subline: "Founding engineer at CoGuide.",
|
||||
};
|
||||
|
||||
export const manifesto = "Systems that survive contact.";
|
||||
|
||||
export const capabilities: Capability[] = [
|
||||
{ index: "01", text: "GPU pipelines, down to the kernel" },
|
||||
{ index: "02", text: "Production backends from zero" },
|
||||
{ index: "03", text: "Systems that survive the open internet" },
|
||||
{ index: "04", text: "Data at market speed" },
|
||||
{ index: "05", text: "Infrastructure, end to end" },
|
||||
];
|
||||
|
||||
export const featuredDossier: Dossier = {
|
||||
id: "coguide",
|
||||
title: "CoGuide",
|
||||
type: "AI instructional-insight platform",
|
||||
status: "deployed",
|
||||
year: "2026—",
|
||||
role: "Founding Engineer / MTS",
|
||||
stack: ["Rust", "AWS", "CUDA", "YOLOv8"],
|
||||
mission: "Classroom audio → private coaching feedback.",
|
||||
constraint: "Legacy T4 GPUs, zero to production",
|
||||
system:
|
||||
"Rust backend (~30k lines) on full AWS infra. Custom CUDA fusion kernels; two-stage CV pipeline at ~0.96× real-time.",
|
||||
result: "10+ schools. 2× market cost efficiency; 10× vs AWS Transcribe.",
|
||||
proofHeadline: "10+ schools · 10× AWS",
|
||||
proof: [
|
||||
{ label: "deployed in schools", metric: "10+ schools" },
|
||||
{ label: "cost efficiency vs market alternatives", metric: "2×" },
|
||||
{ label: "cost efficiency vs AWS Transcribe", metric: "10×" },
|
||||
{ label: "hand-raise detection", metric: "~0.96× real-time" },
|
||||
],
|
||||
links: [
|
||||
{ label: "coguide.ai", href: "https://coguide.ai" },
|
||||
{
|
||||
label: "CoGuide-Miniproj",
|
||||
href: "https://git.cyber.ayyalasomayajula.net/marsultor/CoGuide-Miniproj",
|
||||
},
|
||||
],
|
||||
accent: "cyan",
|
||||
};
|
||||
|
||||
export const dossiers: Dossier[] = [
|
||||
{
|
||||
id: "exalock",
|
||||
title: "exalock",
|
||||
type: "GPU/Wayland lockscreen",
|
||||
status: "WIP",
|
||||
year: "2026",
|
||||
stack: ["C", "C++23", "GLSL"],
|
||||
mission: "GPU/Wayland lockscreen",
|
||||
constraint: "raw ext-session-lock-v1",
|
||||
system: "RK4 stream-function particle sim",
|
||||
result: "WIP 2026 GPU/Wayland lockscreen",
|
||||
proofHeadline: "RK4 particle sim",
|
||||
proof: [{ label: "raw ext-session-lock-v1" }],
|
||||
links: [
|
||||
{
|
||||
label: "marsultor/exalock",
|
||||
href: "https://git.cyber.ayyalasomayajula.net/marsultor/exalock",
|
||||
},
|
||||
],
|
||||
accent: "cyan",
|
||||
},
|
||||
{
|
||||
id: "quant-streamer",
|
||||
title: "Quant Streamer",
|
||||
type: "real-time market data",
|
||||
status: "closed source",
|
||||
year: "2025",
|
||||
stack: ["Kafka", "MQTT", "DXLink", "ClickHouse", "C++"],
|
||||
mission: "real-time market data",
|
||||
system: "100K datapoints/s via Kafka/MQTT/DXLink; ClickHouse; C++ LSTM + Heston/Black–Scholes",
|
||||
result: "100K datapoints/s via Kafka/MQTT/DXLink",
|
||||
proofHeadline: "100K pts/s",
|
||||
proof: [{ label: "market data throughput", metric: "100K datapoints/s" }],
|
||||
links: [],
|
||||
accent: "orange",
|
||||
},
|
||||
{
|
||||
id: "mqtt-push-pipeline",
|
||||
title: "MQTT Push Pipeline",
|
||||
type: "infra",
|
||||
status: "live since Aug 2025",
|
||||
year: "2025",
|
||||
stack: ["Rust", "MQTT", "RSS", "C++"],
|
||||
mission: "infra pipeline for push alerts",
|
||||
system: "Rust consumer sustaining 10M notifications/s benchmark",
|
||||
result: "10k+ real alerts relayed",
|
||||
proofHeadline: "10M notif/s",
|
||||
proof: [
|
||||
{ label: "notification benchmark", metric: "10M notifications/s" },
|
||||
{ label: "real alerts relayed", metric: "10k+" },
|
||||
],
|
||||
links: [
|
||||
{
|
||||
label: "C++ repo",
|
||||
href: "https://git.cyber.ayyalasomayajula.net/marsultor",
|
||||
},
|
||||
{
|
||||
label: "Rust repo",
|
||||
href: "https://git.cyber.ayyalasomayajula.net/marsultor",
|
||||
},
|
||||
{
|
||||
label: "RSS repo",
|
||||
href: "https://git.cyber.ayyalasomayajula.net/marsultor",
|
||||
},
|
||||
],
|
||||
accent: "green",
|
||||
},
|
||||
{
|
||||
id: "matrix-fs",
|
||||
title: "Matrix FS",
|
||||
type: "encrypted L2 storage over IPFS",
|
||||
status: "closed source",
|
||||
year: "—",
|
||||
stack: [
|
||||
"Rust",
|
||||
"Tauri",
|
||||
"Svelte",
|
||||
"IPFS",
|
||||
"AES-256-CBC",
|
||||
"ChaCha20-Poly1305",
|
||||
"Tor",
|
||||
],
|
||||
mission: "encrypted L2 storage over IPFS",
|
||||
system: "Rust + Tauri/Svelte; AES-256-CBC + ChaCha20-Poly1305; Tor hidden-service ticketing",
|
||||
result: "closed source encrypted L2 storage over IPFS",
|
||||
proofHeadline: "dual-cipher, over Tor",
|
||||
proof: [{ label: "AES-256-CBC + ChaCha20-Poly1305" }],
|
||||
links: [],
|
||||
accent: "violet",
|
||||
},
|
||||
{
|
||||
id: "home-infrastructure",
|
||||
title: "Home Infrastructure",
|
||||
type: "infrastructure",
|
||||
status: "live",
|
||||
year: "—",
|
||||
stack: ["Linux", "Docker/Podman", "ZFS"],
|
||||
mission: "home infrastructure",
|
||||
system: "3 HA servers, 40+ containers, 150TB ZFS, ~50 users",
|
||||
result: "live infrastructure serving ~50 users",
|
||||
proofHeadline: "150TB · 50 users",
|
||||
proof: [
|
||||
{ label: "HA servers", metric: "3" },
|
||||
{ label: "containers", metric: "40+" },
|
||||
{ label: "ZFS storage", metric: "150TB" },
|
||||
{ label: "users", metric: "~50" },
|
||||
],
|
||||
links: [],
|
||||
accent: "green",
|
||||
},
|
||||
{
|
||||
id: "cyberpatriot-automation",
|
||||
title: "CyberPatriot Automation",
|
||||
type: "hardening suite",
|
||||
status: "licensed",
|
||||
year: "—",
|
||||
stack: ["Rust", "PowerShell"],
|
||||
mission: "Rust/PowerShell hardening suite",
|
||||
system: "CyberPatriot Automation hardening suite",
|
||||
result: "30+ pts/round; licensed to another team",
|
||||
proofHeadline: "30+ pts/round",
|
||||
proof: [
|
||||
{ label: "points per round", metric: "30+" },
|
||||
{ label: "licensed to another team" },
|
||||
],
|
||||
links: [],
|
||||
accent: "orange",
|
||||
},
|
||||
{
|
||||
id: "browser-security-red-team-poc",
|
||||
title: "Browser-Security Red-Team PoC",
|
||||
type: "educational",
|
||||
status: "educational",
|
||||
year: "—",
|
||||
stack: ["TypeScript", "Bun", "Playwright"],
|
||||
mission: "educational browser-security red-team PoC",
|
||||
system: "TS/Bun + Playwright stealth defeating bot detection",
|
||||
result: "educational PoC defeating bot detection",
|
||||
proofHeadline: "detection defeated",
|
||||
proof: [{ label: "Playwright stealth defeating bot detection" }],
|
||||
links: [
|
||||
{
|
||||
label: "marsultor/linkedin-bot",
|
||||
href: "https://git.cyber.ayyalasomayajula.net/marsultor/linkedin-bot",
|
||||
},
|
||||
],
|
||||
accent: "violet",
|
||||
},
|
||||
{
|
||||
id: "hardened-ssh-portfolio-server",
|
||||
title: "Hardened SSH Portfolio Server",
|
||||
type: "live security PoC",
|
||||
status: "live",
|
||||
year: "—",
|
||||
stack: ["C++", "FTXUI", "SSH"],
|
||||
mission: "live security PoC",
|
||||
system: "C++ FTXUI over SSH",
|
||||
result: "survived 1M+ attacks from 100 countries over 18 months, zero compromise",
|
||||
proofHeadline: "1M+ attacks · 0 compromise",
|
||||
proof: [
|
||||
{ label: "attacks survived", metric: "1M+" },
|
||||
{ label: "countries", metric: "100" },
|
||||
{ label: "duration", metric: "18 months" },
|
||||
{ label: "compromise count", metric: "zero" },
|
||||
],
|
||||
links: [],
|
||||
accent: "cyan",
|
||||
},
|
||||
];
|
||||
|
||||
export const publication: Publication = {
|
||||
title:
|
||||
"Rule-based Tensor Mutations Embedded within LLMs for Low-Cost Mathematical Computation",
|
||||
venue: "TechRxiv preprint (IEEE)",
|
||||
year: "2025",
|
||||
authorship: "sole author",
|
||||
doi: "10.36227/techrxiv.175393256.68603080/v1",
|
||||
href: "https://doi.org/10.36227/techrxiv.175393256.68603080/v1",
|
||||
};
|
||||
|
||||
export const honors: Honor[] = [
|
||||
{
|
||||
title: "Lockheed Martin Cyber Quest 2nd place",
|
||||
detail: "team captain, ~30% of team score",
|
||||
year: "2025",
|
||||
},
|
||||
{ title: "CyberPatriot State Gold", detail: "", year: "2025" },
|
||||
{ title: "NSA Codebreaker", detail: "5 tasks", year: "2024" },
|
||||
{ title: "Hack The Box", detail: "peak global rank 564", year: "" },
|
||||
{ title: "National Merit Commendation", detail: "", year: "" },
|
||||
];
|
||||
|
||||
export const skillsMatrix: SkillsMatrixRow[] = [
|
||||
{
|
||||
category: "Languages",
|
||||
skills: [
|
||||
"Rust async/unsafe/FFI",
|
||||
"C",
|
||||
"C++23",
|
||||
"Python",
|
||||
"TypeScript",
|
||||
"GLSL",
|
||||
"SQL",
|
||||
"Bash",
|
||||
"Java",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Systems & Infra",
|
||||
skills: [
|
||||
"Linux",
|
||||
"Docker/Podman",
|
||||
"WireGuard",
|
||||
"Nginx/Traefik",
|
||||
"PostgreSQL",
|
||||
"ClickHouse",
|
||||
"Redis",
|
||||
"Kafka",
|
||||
"MQTT",
|
||||
"gRPC",
|
||||
"ZFS",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Scientific Computing",
|
||||
skills: [
|
||||
"NumPy/SciPy/JAX",
|
||||
"Eigen",
|
||||
"BLAS/LAPACK",
|
||||
"CUDA/cuBLAS",
|
||||
"ODE solvers",
|
||||
"PyTorch",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Security",
|
||||
skills: [
|
||||
"reverse engineering",
|
||||
"binary exploitation",
|
||||
"network forensics",
|
||||
"eBPF/seccomp/AppArmor",
|
||||
"red/blue tooling",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Frontend & Embedded",
|
||||
skills: ["Next.js/React", "Svelte", "Tauri", "FTXUI", "STM32/ESP32"],
|
||||
},
|
||||
];
|
||||
|
||||
export const contact: Contact = {
|
||||
email: "krishna@ayyalasomayajula.net",
|
||||
git: "https://git.cyber.ayyalasomayajula.net/marsultor",
|
||||
linkedIn: "https://www.linkedin.com/in/krishna-ayy/",
|
||||
};
|
||||
15
lib/posts.ts
@@ -2,6 +2,9 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
import { cache } from 'react'
|
||||
import { tagToSlug } from '@/lib/tags'
|
||||
|
||||
export { tagToSlug } from '@/lib/tags'
|
||||
|
||||
const postsDirectory = path.join(process.cwd(), 'content/posts')
|
||||
|
||||
@@ -61,18 +64,6 @@ export const normalizePostData = (
|
||||
readingTime,
|
||||
})
|
||||
|
||||
export const tagToSlug = (tag: string): string =>
|
||||
tag
|
||||
.normalize('NFKD')
|
||||
.toLowerCase()
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/\+/g, ' plus ')
|
||||
.replace(/#/g, ' sharp ')
|
||||
.replace(/[\\/]+/g, ' ')
|
||||
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
||||
.replace(/-{2,}/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
|
||||
const getMdxFiles = cache(async () => {
|
||||
try {
|
||||
const files = await fs.promises.readdir(postsDirectory)
|
||||
|
||||
11
lib/tags.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const tagToSlug = (tag: string): string =>
|
||||
tag
|
||||
.normalize('NFKD')
|
||||
.toLowerCase()
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/\+/g, ' plus ')
|
||||
.replace(/#/g, ' sharp ')
|
||||
.replace(/[\\/]+/g, ' ')
|
||||
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
||||
.replace(/-{2,}/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
@@ -37,11 +37,11 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
</p>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="my-6 border-l-4 border-accent/40 bg-surface/60 py-1 pl-5 pr-4 italic text-ink-soft">
|
||||
<blockquote className="my-6 border-l-4 border-accent/60 bg-surface/60 py-1 pl-5 pr-4 text-ink-soft">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
hr: () => <hr className="my-8 border-border" />,
|
||||
hr: () => <hr className="my-12 border-border" />,
|
||||
a: ({ href, children, ...props }) => {
|
||||
const isExternal = typeof href === 'string' && (href.startsWith('http://') || href.startsWith('https://'))
|
||||
return (
|
||||
@@ -80,14 +80,14 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
<img
|
||||
src={src}
|
||||
alt={alt ?? ''}
|
||||
className="mx-auto my-8 h-auto max-h-[80vh] w-auto max-w-full rounded-2xl border border-border bg-surface object-contain shadow-card"
|
||||
className="mx-auto my-8 h-auto max-h-[80vh] w-auto max-w-full rounded-lg border border-border bg-surface object-contain shadow-card"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
{...rest}
|
||||
/>
|
||||
),
|
||||
table: ({ children }) => (
|
||||
<div className="my-8 overflow-x-auto rounded-xl border border-border bg-canvas">
|
||||
<div className="my-8 overflow-x-auto rounded-lg border border-border bg-canvas">
|
||||
<table className="w-full min-w-[40rem] border-collapse text-sm">
|
||||
{children}
|
||||
</table>
|
||||
@@ -113,7 +113,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
),
|
||||
|
||||
// Collapsible sections
|
||||
details: (props) => <details className="my-6 rounded-xl border border-border bg-surface/70 p-4 shadow-card" {...props} />,
|
||||
details: (props) => <details className="my-6 rounded-lg border border-border bg-surface/70 p-4 shadow-card" {...props} />,
|
||||
summary: (props) => <summary className="cursor-pointer font-semibold text-ink" {...props} />,
|
||||
}
|
||||
}
|
||||
|
||||
474
package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@next/mdx": "^16.2.6",
|
||||
"@rehype-pretty/transformers": "npm:@jsr/rehype-pretty__transformers@^0.13.4",
|
||||
"@wrksz/themes": "^0.9.3",
|
||||
"cmdk": "^1.1.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"katex": "^0.17.0",
|
||||
"motion": "12.18.1",
|
||||
@@ -1346,6 +1347,318 @@
|
||||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.5.tgz",
|
||||
"integrity": "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz",
|
||||
"integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.0.tgz",
|
||||
"integrity": "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.19.tgz",
|
||||
"integrity": "sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.5",
|
||||
"@radix-ui/react-compose-refs": "1.1.3",
|
||||
"@radix-ui/react-context": "1.2.0",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.15",
|
||||
"@radix-ui/react-focus-guards": "1.1.4",
|
||||
"@radix-ui/react-focus-scope": "1.1.12",
|
||||
"@radix-ui/react-id": "1.1.2",
|
||||
"@radix-ui/react-portal": "1.1.13",
|
||||
"@radix-ui/react-presence": "1.1.7",
|
||||
"@radix-ui/react-primitive": "2.1.7",
|
||||
"@radix-ui/react-slot": "1.3.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.3",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.15.tgz",
|
||||
"integrity": "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.5",
|
||||
"@radix-ui/react-compose-refs": "1.1.3",
|
||||
"@radix-ui/react-primitive": "2.1.7",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.2",
|
||||
"@radix-ui/react-use-effect-event": "0.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz",
|
||||
"integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.12.tgz",
|
||||
"integrity": "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.3",
|
||||
"@radix-ui/react-primitive": "2.1.7",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz",
|
||||
"integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.13.tgz",
|
||||
"integrity": "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.7",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.7.tgz",
|
||||
"integrity": "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.7.tgz",
|
||||
"integrity": "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz",
|
||||
"integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz",
|
||||
"integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz",
|
||||
"integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.3",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz",
|
||||
"integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz",
|
||||
"integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rehype-pretty/transformers": {
|
||||
"name": "@jsr/rehype-pretty__transformers",
|
||||
"version": "0.13.4",
|
||||
@@ -1870,7 +2183,7 @@
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.15",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
@@ -1878,7 +2191,7 @@
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.2.3",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
@@ -2562,6 +2875,18 @@
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.2",
|
||||
"dev": true,
|
||||
@@ -2976,6 +3301,22 @@
|
||||
"version": "0.0.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cmdk": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
|
||||
"integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-id": "^1.1.0",
|
||||
"@radix-ui/react-primitive": "^2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/collapse-white-space": {
|
||||
"version": "2.1.0",
|
||||
"license": "MIT",
|
||||
@@ -3040,7 +3381,7 @@
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
@@ -3174,6 +3515,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node-es": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
@@ -4159,6 +4506,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-nonce": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
@@ -7225,6 +7581,75 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
|
||||
"integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.7",
|
||||
"react-style-singleton": "^2.2.3",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.3",
|
||||
"use-sidecar": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll-bar": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
|
||||
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-style-singleton": "^2.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-nonce": "^1.0.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/recma-build-jsx": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
@@ -8653,6 +9078,49 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-callback-ref": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sidecar": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
|
||||
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-node-es": "^1.1.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vfile": {
|
||||
"version": "6.0.3",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@next/mdx": "^16.2.6",
|
||||
"@rehype-pretty/transformers": "npm:@jsr/rehype-pretty__transformers@^0.13.4",
|
||||
"@wrksz/themes": "^0.9.3",
|
||||
"cmdk": "^1.1.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"katex": "^0.17.0",
|
||||
"motion": "12.18.1",
|
||||
|
||||
1034
portfolio-site-research-report-v2.md
Normal file
BIN
public/banner.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
@@ -1 +0,0 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 391 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
95
public/og-image.svg
Normal file
@@ -0,0 +1,95 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
|
||||
<rect width="1200" height="630" fill="#fff"/>
|
||||
|
||||
<g transform="translate(400,315)">
|
||||
<!-- Outer circle -->
|
||||
<circle cx="0" cy="0" r="220" fill="none" stroke="#000" stroke-width="2"/>
|
||||
|
||||
<!-- Triangle up -->
|
||||
<polygon points="0,-220 190.5,110 -190.5,110" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Triangle down -->
|
||||
<polygon points="0,220 -190.5,-110 190.5,-110" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Circle inscribed -->
|
||||
<circle cx="0" cy="0" r="110" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Hexagon -->
|
||||
<polygon points="95,-165 190.5,-55 190.5,55 95,165 -95,165 -190.5,55 -190.5,-55 -95,-165"
|
||||
fill="none" stroke="#000" stroke-width="1"/>
|
||||
|
||||
<!-- Inner circle -->
|
||||
<circle cx="0" cy="0" r="55" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Inner triangle up -->
|
||||
<polygon points="0,-55 47.6,27.5 -47.6,27.5" fill="none" stroke="#000" stroke-width="1"/>
|
||||
|
||||
<!-- Inner triangle down -->
|
||||
<polygon points="0,55 -47.6,-27.5 47.6,-27.5" fill="none" stroke="#000" stroke-width="1"/>
|
||||
|
||||
<!-- Small circle -->
|
||||
<circle cx="0" cy="0" r="27" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Tiny triangles -->
|
||||
<polygon points="0,-27 23.4,13.5 -23.4,13.5" fill="none" stroke="#000" stroke-width="0.8"/>
|
||||
<polygon points="0,27 -23.4,-13.5 23.4,-13.5" fill="none" stroke="#000" stroke-width="0.8"/>
|
||||
|
||||
<!-- Center dot -->
|
||||
<circle cx="0" cy="0" r="4" fill="#000"/>
|
||||
|
||||
<!-- Radial lines -->
|
||||
<g stroke="#000" stroke-width="0.5" opacity="0.4">
|
||||
<line x1="0" y1="-4" x2="0" y2="-220"/>
|
||||
<line x1="0" y1="4" x2="0" y2="220"/>
|
||||
<line x1="0" y1="0" x2="190.5" y2="110"/>
|
||||
<line x1="0" y1="0" x2="-190.5" y2="110"/>
|
||||
<line x1="0" y1="0" x2="190.5" y2="-110"/>
|
||||
<line x1="0" y1="0" x2="-190.5" y2="-110"/>
|
||||
</g>
|
||||
|
||||
<!-- Vertex dots -->
|
||||
<g fill="#fff" stroke="#000" stroke-width="1.2">
|
||||
<circle cx="0" cy="-220" r="5"/>
|
||||
<circle cx="190.5" cy="110" r="5"/>
|
||||
<circle cx="-190.5" cy="110" r="5"/>
|
||||
</g>
|
||||
<g fill="#000">
|
||||
<circle cx="0" cy="-220" r="2"/>
|
||||
<circle cx="190.5" cy="110" r="2"/>
|
||||
<circle cx="-190.5" cy="110" r="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Dashed outer ring -->
|
||||
<circle cx="0" cy="0" r="232" fill="none" stroke="#000" stroke-width="0.6" stroke-dasharray="2 6"/>
|
||||
|
||||
<!-- Arc marks -->
|
||||
<g stroke="#000" stroke-width="1.2" stroke-linecap="round" fill="none">
|
||||
<path d="M -6,-226 A 6,6 0 0,1 6,-226"/>
|
||||
<path d="M -6,226 A 6,6 0 0,0 6,226"/>
|
||||
<path d="M -226,-6 A 6,6 0 0,0 -226,6"/>
|
||||
<path d="M 226,-6 A 6,6 0 0,1 226,6"/>
|
||||
</g>
|
||||
|
||||
<!-- Cardinal dots -->
|
||||
<g fill="#000">
|
||||
<circle cx="0" cy="-232" r="1.5"/>
|
||||
<circle cx="0" cy="232" r="1.5"/>
|
||||
<circle cx="-232" cy="0" r="1.5"/>
|
||||
<circle cx="232" cy="0" r="1.5"/>
|
||||
</g>
|
||||
|
||||
<!-- Corner marks -->
|
||||
<g stroke="#000" stroke-width="1" stroke-linecap="round" opacity="0.5">
|
||||
<line x1="-164" y1="-164" x2="-152" y2="-152"/>
|
||||
<line x1="164" y1="-164" x2="152" y2="-152"/>
|
||||
<line x1="-164" y1="164" x2="-152" y2="152"/>
|
||||
<line x1="164" y1="164" x2="152" y2="152"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Text on the right -->
|
||||
<text x="750" y="300" font-family="Georgia, serif" font-size="72" fill="#000" letter-spacing="-1">Krishna</text>
|
||||
<text x="750" y="360" font-family="monospace" font-size="22" fill="#000" opacity="0.6" letter-spacing="2">JOURNAL</text>
|
||||
<line x1="750" y1="390" x2="850" y2="390" stroke="#000" stroke-width="1" opacity="0.4"/>
|
||||
<text x="750" y="425" font-family="monospace" font-size="14" fill="#000" opacity="0.35" letter-spacing="1">CODE & MATH</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
BIN
public/og.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
103
public/ouroboros.svg
Normal file
@@ -0,0 +1,103 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500" height="500">
|
||||
<rect width="500" height="500" fill="#fff"/>
|
||||
<g transform="translate(250,250)">
|
||||
|
||||
<!-- Outer circle -->
|
||||
<circle cx="0" cy="0" r="220" fill="none" stroke="#000" stroke-width="2"/>
|
||||
|
||||
<!-- Triangle inscribed in outer circle (pointing up) -->
|
||||
<polygon points="0,-220 190.5,110 -190.5,110" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Triangle inscribed (pointing down) -->
|
||||
<polygon points="0,220 -190.5,-110 190.5,-110" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Circle inscribed in the star (radius ~110) -->
|
||||
<circle cx="0" cy="0" r="110" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Hexagon from the star intersections -->
|
||||
<polygon points="95,-165 190.5,-55 190.5,55 95,165 -95,165 -190.5,55 -190.5,-55 -95,-165"
|
||||
fill="none" stroke="#000" stroke-width="1"/>
|
||||
|
||||
<!-- Inner circle -->
|
||||
<circle cx="0" cy="0" r="55" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Triangle inscribed in inner circle (up) -->
|
||||
<polygon points="0,-55 47.6,27.5 -47.6,27.5" fill="none" stroke="#000" stroke-width="1"/>
|
||||
|
||||
<!-- Triangle inscribed in inner circle (down) -->
|
||||
<polygon points="0,55 -47.6,-27.5 47.6,-27.5" fill="none" stroke="#000" stroke-width="1"/>
|
||||
|
||||
<!-- Small circle at center -->
|
||||
<circle cx="0" cy="0" r="27" fill="none" stroke="#000" stroke-width="1.5"/>
|
||||
|
||||
<!-- Tiny triangle up -->
|
||||
<polygon points="0,-27 23.4,13.5 -23.4,13.5" fill="none" stroke="#000" stroke-width="0.8"/>
|
||||
|
||||
<!-- Tiny triangle down -->
|
||||
<polygon points="0,27 -23.4,-13.5 23.4,-13.5" fill="none" stroke="#000" stroke-width="0.8"/>
|
||||
|
||||
<!-- Center dot -->
|
||||
<circle cx="0" cy="0" r="4" fill="#000"/>
|
||||
|
||||
<!-- Lines from center through all vertices -->
|
||||
<g stroke="#000" stroke-width="0.5" opacity="0.4">
|
||||
<!-- Through outer triangle vertices -->
|
||||
<line x1="0" y1="-4" x2="0" y2="-220"/>
|
||||
<line x1="0" y1="4" x2="0" y2="220"/>
|
||||
<line x1="0" y1="0" x2="190.5" y2="110"/>
|
||||
<line x1="0" y1="0" x2="-190.5" y2="110"/>
|
||||
<line x1="0" y1="0" x2="190.5" y2="-110"/>
|
||||
<line x1="0" y1="0" x2="-190.5" y2="-110"/>
|
||||
</g>
|
||||
|
||||
<!-- Small circles at outer triangle vertices -->
|
||||
<g fill="#fff" stroke="#000" stroke-width="1.2">
|
||||
<circle cx="0" cy="-220" r="5"/>
|
||||
<circle cx="190.5" cy="110" r="5"/>
|
||||
<circle cx="-190.5" cy="110" r="5"/>
|
||||
</g>
|
||||
<g fill="#000">
|
||||
<circle cx="0" cy="-220" r="2"/>
|
||||
<circle cx="190.5" cy="110" r="2"/>
|
||||
<circle cx="-190.5" cy="110" r="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Small circles at inner triangle vertices -->
|
||||
<g fill="#fff" stroke="#000" stroke-width="1">
|
||||
<circle cx="0" cy="-55" r="3.5"/>
|
||||
<circle cx="47.6" cy="27.5" r="3.5"/>
|
||||
<circle cx="-47.6" cy="27.5" r="3.5"/>
|
||||
<circle cx="0" cy="55" r="3.5"/>
|
||||
<circle cx="47.6" cy="-27.5" r="3.5"/>
|
||||
<circle cx="-47.6" cy="-27.5" r="3.5"/>
|
||||
</g>
|
||||
|
||||
<!-- Dashed outermost ring -->
|
||||
<circle cx="0" cy="0" r="232" fill="none" stroke="#000" stroke-width="0.6" stroke-dasharray="2 6"/>
|
||||
|
||||
<!-- Arc marks at cardinal points on outer ring -->
|
||||
<g stroke="#000" stroke-width="1.2" stroke-linecap="round" fill="none">
|
||||
<path d="M -6,-226 A 6,6 0 0,1 6,-226"/>
|
||||
<path d="M -6,226 A 6,6 0 0,0 6,226"/>
|
||||
<path d="M -226,-6 A 6,6 0 0,0 -226,6"/>
|
||||
<path d="M 226,-6 A 6,6 0 0,1 226,6"/>
|
||||
</g>
|
||||
|
||||
<!-- Tiny dots at cardinal points -->
|
||||
<g fill="#000">
|
||||
<circle cx="0" cy="-232" r="1.5"/>
|
||||
<circle cx="0" cy="232" r="1.5"/>
|
||||
<circle cx="-232" cy="0" r="1.5"/>
|
||||
<circle cx="232" cy="0" r="1.5"/>
|
||||
</g>
|
||||
|
||||
<!-- Corner marks -->
|
||||
<g stroke="#000" stroke-width="1" stroke-linecap="round" opacity="0.5">
|
||||
<line x1="-164" y1="-164" x2="-152" y2="-152"/>
|
||||
<line x1="164" y1="-164" x2="152" y2="-152"/>
|
||||
<line x1="-164" y1="164" x2="-152" y2="152"/>
|
||||
<line x1="164" y1="164" x2="152" y2="152"/>
|
||||
</g>
|
||||
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -1,17 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 540" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Starter design diagram</title>
|
||||
<desc id="desc">Three connected nodes labeled design, content, and build.</desc>
|
||||
<rect width="900" height="540" rx="36" fill="#f8fafc"/>
|
||||
<path d="M250 270h400" stroke="#94a3b8" stroke-width="12" stroke-linecap="round"/>
|
||||
<path d="M450 185v170" stroke="#94a3b8" stroke-width="12" stroke-linecap="round"/>
|
||||
<g font-family="ui-sans-serif, system-ui, sans-serif" font-weight="700" text-anchor="middle">
|
||||
<circle cx="250" cy="270" r="98" fill="#dbeafe" stroke="#2563eb" stroke-width="8"/>
|
||||
<text x="250" y="282" fill="#1e3a8a" font-size="34">Design</text>
|
||||
<circle cx="650" cy="270" r="98" fill="#dcfce7" stroke="#16a34a" stroke-width="8"/>
|
||||
<text x="650" y="282" fill="#14532d" font-size="34">Content</text>
|
||||
<circle cx="450" cy="150" r="88" fill="#fef3c7" stroke="#d97706" stroke-width="8"/>
|
||||
<text x="450" y="162" fill="#78350f" font-size="32">Build</text>
|
||||
<circle cx="450" cy="390" r="88" fill="#fce7f3" stroke="#db2777" stroke-width="8"/>
|
||||
<text x="450" y="402" fill="#831843" font-size="32">Ship</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,26 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Starter MDX showcase cover</title>
|
||||
<desc id="desc">A small abstract gradient cover with cards representing math, code, and design.</desc>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#7c3aed"/>
|
||||
<stop offset="0.55" stop-color="#2563eb"/>
|
||||
<stop offset="1" stop-color="#14b8a6"/>
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="18" stdDeviation="20" flood-color="#0f172a" flood-opacity="0.25"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="1200" height="630" rx="42" fill="url(#bg)"/>
|
||||
<circle cx="1030" cy="120" r="150" fill="#ffffff" opacity="0.16"/>
|
||||
<circle cx="130" cy="560" r="210" fill="#ffffff" opacity="0.12"/>
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="155" y="145" width="890" height="340" rx="32" fill="#ffffff" opacity="0.92"/>
|
||||
<rect x="210" y="205" width="260" height="42" rx="12" fill="#111827" opacity="0.9"/>
|
||||
<rect x="210" y="275" width="330" height="28" rx="10" fill="#6366f1" opacity="0.85"/>
|
||||
<rect x="210" y="330" width="260" height="28" rx="10" fill="#0f766e" opacity="0.75"/>
|
||||
<text x="615" y="255" fill="#111827" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="46" font-weight="700">MDX</text>
|
||||
<text x="615" y="325" fill="#334155" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="34">$E=mc²</text>
|
||||
<text x="615" y="390" fill="#334155" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="30">code · links · tables</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
Before Width: | Height: | Size: 128 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
Before Width: | Height: | Size: 385 B |