Skip to content

Commit 867cf44

Browse files
committed
update title
AI-Session-Id: bb850d52-a517-4b4d-b173-e7ed9525bc2d AI-Tool: claude-code AI-Model: unknown
1 parent 3fdb34c commit 867cf44

2 files changed

Lines changed: 281 additions & 17 deletions

File tree

src/layouts/BaseLayout.astro

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,12 @@ const metaDescription = description ?? 'Writing and notes.';
130130
<source srcset="/mascot.png" type="image/png" />
131131
<img src="/mascot.svg" alt="Vikram mascot" class="h-[75px] w-[75px]" />
132132
</picture>
133-
<span class="font-hal truncate text-[30px] leading-none font-semibold tracking-tight text-black dark:text-zinc-50">
134-
{siteName}
133+
<span
134+
id="site-title"
135+
class="font-hal truncate text-[30px] leading-none font-semibold tracking-tight text-black dark:text-zinc-50"
136+
data-typing-text={siteName}
137+
>
138+
<span id="site-title-prefix"></span><span id="site-title-word"></span><span id="site-title-cursor" aria-hidden="true">_</span>
135139
</span>
136140
</a>
137141
<nav class="flex shrink-0 items-center gap-4 text-[16px]">
@@ -338,6 +342,104 @@ const metaDescription = description ?? 'Writing and notes.';
338342
})();
339343
</script>
340344

345+
<style is:inline>
346+
#site-title {
347+
white-space: nowrap;
348+
}
349+
350+
#site-title-cursor {
351+
display: inline-block;
352+
margin-left: 1px;
353+
opacity: 1;
354+
}
355+
356+
@media (prefers-reduced-motion: reduce) {
357+
#site-title-cursor {
358+
display: none;
359+
}
360+
}
361+
</style>
362+
363+
<script is:inline>
364+
(() => {
365+
const run = () => {
366+
if (window.__siteTitleLoopRunning) return;
367+
window.__siteTitleLoopRunning = true;
368+
369+
const host = document.getElementById('site-title');
370+
const prefixEl = document.getElementById('site-title-prefix');
371+
const wordEl = document.getElementById('site-title-word');
372+
const cursorEl = document.getElementById('site-title-cursor');
373+
374+
if (!host || !prefixEl || !wordEl || !cursorEl) return;
375+
376+
const prefersReduced =
377+
typeof window !== 'undefined' &&
378+
window.matchMedia &&
379+
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
380+
381+
const prefix = "Vikram's ";
382+
const words = ['Focushours', 'Blog', 'Tools', 'Experiments'];
383+
384+
if (prefersReduced) {
385+
prefixEl.textContent = prefix;
386+
wordEl.textContent = words[0];
387+
cursorEl.style.display = 'none';
388+
return;
389+
}
390+
391+
const sleep = (ms) => new Promise((r) => window.setTimeout(r, ms));
392+
393+
const typeInto = async (el, text, delay) => {
394+
el.textContent = '';
395+
for (let i = 0; i < text.length; i += 1) {
396+
el.textContent += text[i];
397+
await sleep(delay);
398+
}
399+
};
400+
401+
const backspace = async (el, delay) => {
402+
while (el.textContent && el.textContent.length > 0) {
403+
el.textContent = el.textContent.slice(0, -1);
404+
await sleep(delay);
405+
}
406+
};
407+
408+
const blinkCursor = async (times) => {
409+
for (let i = 0; i < times; i += 1) {
410+
cursorEl.style.opacity = '0';
411+
await sleep(500);
412+
cursorEl.style.opacity = '1';
413+
await sleep(500);
414+
}
415+
};
416+
417+
(async () => {
418+
prefixEl.textContent = '';
419+
wordEl.textContent = '';
420+
cursorEl.style.opacity = '1';
421+
422+
await typeInto(prefixEl, prefix, 55);
423+
await typeInto(wordEl, words[0], 70);
424+
425+
let idx = 0;
426+
while (true) {
427+
await blinkCursor(3);
428+
await backspace(wordEl, 35);
429+
idx = (idx + 1) % words.length;
430+
await typeInto(wordEl, words[idx], 70);
431+
}
432+
})();
433+
};
434+
435+
if (document.readyState === 'loading') {
436+
document.addEventListener('DOMContentLoaded', run);
437+
} else {
438+
run();
439+
}
440+
})();
441+
</script>
442+
341443
<div
342444
id="toast"
343445
class="pointer-events-none fixed bottom-5 left-1/2 z-[60] transition-all duration-200"

src/pages/blog/[...slug].astro

Lines changed: 177 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,10 @@ function renderInlineCode(input: string) {
117117
<h1 class="mb-0 text-[40px] leading-[1.1] font-semibold tracking-tight">
118118
<span set:html={renderInlineCode(post.data.title)} />
119119
</h1>
120-
<div class="text-base text-zinc-600 dark:text-zinc-400">{formattedDate}</div>
121-
{hasReadTime && (
122-
<div class="flex items-center gap-3 text-sm text-zinc-500 dark:text-zinc-400">
120+
121+
<div class="mt-5 flex flex-wrap items-center justify-between gap-4 text-sm text-zinc-600 dark:text-zinc-400">
122+
<div class="flex flex-wrap items-center gap-3">
123+
<span>{formattedDate}</span>
123124
{hasReadTime && (
124125
<span class="inline-flex items-center gap-1">
125126
<svg
@@ -135,16 +136,57 @@ function renderInlineCode(input: string) {
135136
<circle cx="12" cy="12" r="10" />
136137
<path d="M12 6v6l4 2" />
137138
</svg>
138-
<span>{readTime} min</span>
139+
<span>{readTime} minutes</span>
139140
</span>
140141
)}
141142
</div>
142-
)}
143-
{hasReadTime && (
144-
<div class="pt-3">
145-
<div class="h-px w-full bg-zinc-200 dark:bg-zinc-800"></div>
143+
144+
<div class="flex items-center gap-2">
145+
<span class="hidden sm:inline">Share:</span>
146+
<button
147+
type="button"
148+
data-share="x"
149+
class="grid h-8 w-8 place-items-center rounded-full border border-zinc-200 text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-900"
150+
aria-label="Share on X"
151+
>
152+
<svg viewBox="0 0 24 24" class="h-4 w-4" aria-hidden="true" fill="currentColor">
153+
<path
154+
d="M18.9 2H22l-6.8 7.8L23 22h-6.6l-5.2-6.7L5.3 22H2l7.4-8.5L1 2h6.7l4.7 6.1L18.9 2zm-1.1 18h1.7L6.8 4H5L17.8 20z"
155+
/>
156+
</svg>
157+
</button>
158+
159+
<button
160+
type="button"
161+
data-share="facebook"
162+
class="grid h-8 w-8 place-items-center rounded-full border border-zinc-200 text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-900"
163+
aria-label="Share on Facebook"
164+
>
165+
<svg viewBox="0 0 24 24" class="h-4 w-4" aria-hidden="true" fill="currentColor">
166+
<path
167+
d="M13.5 22v-8h2.7l.4-3h-3.1V9.1c0-.9.3-1.5 1.6-1.5h1.7V5c-.3 0-1.5-.1-2.8-.1-2.8 0-4.6 1.7-4.6 4.8V11H7v3h2.7v8h3.8z"
168+
/>
169+
</svg>
170+
</button>
171+
172+
<button
173+
type="button"
174+
data-share="linkedin"
175+
class="grid h-8 w-8 place-items-center rounded-full border border-zinc-200 text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-900"
176+
aria-label="Share on LinkedIn"
177+
>
178+
<svg viewBox="0 0 24 24" class="h-4 w-4" aria-hidden="true" fill="currentColor">
179+
<path
180+
d="M20.5 3h-17C2.7 3 2 3.7 2 4.5v17c0 .8.7 1.5 1.5 1.5h17c.8 0 1.5-.7 1.5-1.5v-17c0-.8-.7-1.5-1.5-1.5zM8 20H5v-9h3v9zM6.5 9.8C5.5 9.8 4.7 9 4.7 8s.8-1.8 1.8-1.8 1.8.8 1.8 1.8-.8 1.8-1.8 1.8zM20 20h-3v-4.9c0-1.2 0-2.7-1.7-2.7s-2 1.3-2 2.6V20h-3v-9h2.9v1.2h.1c.4-.8 1.4-1.7 2.9-1.7 3.1 0 3.7 2 3.7 4.7V20z"
181+
/>
182+
</svg>
183+
</button>
146184
</div>
147-
)}
185+
</div>
186+
187+
<div class="pt-3">
188+
<div class="h-px w-full bg-zinc-200 dark:bg-zinc-800"></div>
189+
</div>
148190
</div>
149191
<Content />
150192
</article>
@@ -177,7 +219,114 @@ function renderInlineCode(input: string) {
177219
}
178220
};
179221

222+
const setupShare = () => {
223+
const buttons = document.querySelectorAll('button[data-share]');
224+
if (!buttons.length) return;
225+
226+
const getShareUrl = (provider, pageUrl) => {
227+
const u = encodeURIComponent(pageUrl);
228+
switch (provider) {
229+
case 'x':
230+
return `https://twitter.com/intent/tweet?url=${u}`;
231+
case 'facebook':
232+
return `https://www.facebook.com/sharer/sharer.php?u=${u}`;
233+
case 'linkedin':
234+
return `https://www.linkedin.com/sharing/share-offsite/?url=${u}`;
235+
default:
236+
return '';
237+
}
238+
};
239+
240+
buttons.forEach((btn) => {
241+
btn.addEventListener('click', async () => {
242+
if (!(btn instanceof HTMLElement)) return;
243+
const provider = btn.getAttribute('data-share');
244+
const pageUrl = window.location.href;
245+
const shareUrl = getShareUrl(provider, pageUrl);
246+
if (!shareUrl) {
247+
const ok = await copyText(pageUrl);
248+
if (ok && typeof window.__toast === 'function') window.__toast('Copied link');
249+
return;
250+
}
251+
window.open(shareUrl, '_blank', 'noopener,noreferrer');
252+
});
253+
});
254+
};
255+
256+
const setupTocScrollSpy = () => {
257+
const aside = document.querySelector('aside[data-toc]');
258+
if (!aside) return;
259+
260+
const links = Array.from(aside.querySelectorAll('a[data-toc-link]'));
261+
if (!links.length) return;
262+
263+
const map = new Map();
264+
for (const a of links) {
265+
const id = a.getAttribute('data-toc-link');
266+
if (id) map.set(id, a);
267+
}
268+
269+
const headings = Array.from(
270+
document.querySelectorAll('article.prose h2[id], article.prose h3[id]')
271+
).filter((h) => map.has(h.getAttribute('id')));
272+
273+
if (!headings.length) return;
274+
275+
let activeId = '';
276+
277+
const setActive = (id) => {
278+
if (!id || id === activeId) return;
279+
const prev = activeId ? map.get(activeId) : null;
280+
if (prev) {
281+
prev.removeAttribute('data-active');
282+
prev.removeAttribute('aria-current');
283+
}
284+
const next = map.get(id);
285+
if (next) {
286+
next.setAttribute('data-active', 'true');
287+
next.setAttribute('aria-current', 'true');
288+
activeId = id;
289+
}
290+
};
291+
292+
const computeActive = () => {
293+
const offset = 140;
294+
let current = headings[0].getAttribute('id');
295+
for (const h of headings) {
296+
const top = h.getBoundingClientRect().top;
297+
if (top - offset <= 0) current = h.getAttribute('id');
298+
else break;
299+
}
300+
if (current) setActive(current);
301+
};
302+
303+
let ticking = false;
304+
const onScroll = () => {
305+
if (ticking) return;
306+
ticking = true;
307+
window.requestAnimationFrame(() => {
308+
computeActive();
309+
ticking = false;
310+
});
311+
};
312+
313+
window.addEventListener('scroll', onScroll, { passive: true });
314+
window.addEventListener('resize', onScroll);
315+
computeActive();
316+
317+
for (const a of links) {
318+
a.addEventListener('click', () => {
319+
const id = a.getAttribute('data-toc-link');
320+
if (!id) return;
321+
window.setTimeout(() => setActive(id), 0);
322+
});
323+
}
324+
};
325+
180326
const init = () => {
327+
setupShare();
328+
setupTocScrollSpy();
329+
181330
const root = document.querySelector('article.prose');
182331
if (!root) return;
183332

@@ -212,16 +361,18 @@ function renderInlineCode(input: string) {
212361
})();
213362
</script>
214363

215-
<aside class="hidden lg:block">
216-
<div class="sticky top-24 max-h-[calc(100vh-8rem)] overflow-auto border-l border-zinc-200 pl-6 dark:border-zinc-800">
217-
<div class="text-sm font-semibold text-zinc-700 dark:text-zinc-50">Table of content</div>
364+
<aside class="hidden lg:block" data-toc>
365+
<div class="sticky top-24 max-h-[calc(100vh-8rem)] overflow-auto">
366+
<div class="text-[16px] font-semibold text-zinc-700 dark:text-zinc-50">Table of Contents</div>
367+
<div class="mt-3 h-px w-full bg-zinc-200 dark:bg-zinc-800"></div>
218368
{toc.length > 0 ? (
219-
<nav class="mt-3">
220-
<ul class="space-y-2 text-sm">
369+
<nav class="mt-4">
370+
<ul class="space-y-1 text-sm">
221371
{toc.map((h) => (
222372
<li class={h.depth === 3 ? 'pl-4' : ''}>
223373
<a
224-
class="group flex items-start gap-2 no-underline text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white"
374+
data-toc-link={h.slug}
375+
class="toc-link block rounded-lg px-3 py-2 no-underline text-zinc-700 transition-colors dark:text-zinc-300"
225376
href={`#${h.slug}`}
226377
>
227378
<span class="leading-snug">{h.text}</span>
@@ -236,4 +387,15 @@ function renderInlineCode(input: string) {
236387
</div>
237388
</aside>
238389
</div>
390+
391+
<style is:inline>
392+
a.toc-link:hover {
393+
background: rgb(var(--brand-primary-rgb) / 0.09);
394+
color: rgb(var(--brand-link-rgb));
395+
}
396+
a.toc-link[data-active='true'] {
397+
background: rgb(var(--brand-primary-rgb) / 0.14);
398+
color: rgb(var(--brand-link-rgb));
399+
}
400+
</style>
239401
</BaseLayout>

0 commit comments

Comments
 (0)