Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@
## 2026-01-30 - [Improving Content Consumption with Scroll Enhancements]
**Learning:** For content-heavy blogs, a reading progress bar provides immediate visual feedback of remaining content, reducing cognitive load. Additionally, smooth scrolling enhances navigation between sections but MUST respect user preferences for reduced motion to ensure accessibility.
**Action:** Implement reading progress indicators as subtle, non-intrusive elements (e.g., at the viewport top). Always wrap `scroll-behavior: smooth` in a `(prefers-reduced-motion: no-preference)` media query.

## 2026-02-01 - [Cross-Layout Component Integration and Utility Consistency]
**Learning:** In projects where multiple layouts exist (e.g., `BaseLayout`, `PostLayout`, `CaseStudyLayout`) and do not share a common root, global UX enhancements must be explicitly registered in each layout to ensure a consistent experience. Additionally, using Tailwind's `!` (important) modifier is often necessary when dynamically adding/removing classes via JavaScript to override base styles or previously applied utility classes.
**Action:** Always audit all layout files when adding global UI components. Use Tailwind utility classes instead of custom CSS blocks whenever possible to maintain design system integrity.
48 changes: 48 additions & 0 deletions src/components/BackToTop.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
/**
* Back to Top button component.
* Appears after scrolling down and smoothly scrolls back to top.
*/
---

<button
id="back-to-top"
class="fixed bottom-8 right-8 p-3 rounded-full bg-[var(--background-secondary)] border border-[var(--border)] text-[var(--foreground-muted)] cursor-pointer transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] opacity-0 invisible translate-y-4 flex items-center justify-center z-40 shadow-2xl hover:text-[var(--accent)] hover:border-[var(--accent)] hover:bg-[var(--background-tertiary)] hover:-translate-y-0.5 active:translate-y-0"
aria-label="Back to top"
title="Back to top"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m18 15-6-6-6 6"></path>
</svg>
</button>

<script>
function initBackToTop() {
const backToTop = document.getElementById('back-to-top');

if (backToTop) {
const toggleBackToTop = () => {
if (window.scrollY > 300) {
backToTop.classList.add('!opacity-100', '!visible', '!translate-y-0');
} else {
backToTop.classList.remove('!opacity-100', '!visible', '!translate-y-0');
}
};

window.addEventListener('scroll', toggleBackToTop, { passive: true });

backToTop.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});

// Initial check
toggleBackToTop();
}
}

initBackToTop();
document.addEventListener('astro:page-load', initBackToTop);
</script>
74 changes: 74 additions & 0 deletions src/components/CodeCopy.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
/**
* Client-side script to add copy buttons to all code blocks.
* Injected into BaseLayout to work across all pages.
*/
---

<script>
function addCopyButtons() {
const codeBlocks = document.querySelectorAll('pre');

codeBlocks.forEach((codeBlock) => {
if (codeBlock.querySelector('.copy-code-button')) return;

const button = document.createElement('button');
// Using Tailwind classes for styling
button.className = 'copy-code-button absolute top-2 right-2 p-1.5 rounded-md bg-[var(--background-secondary)] border border-[var(--border)] text-[var(--foreground-muted)] cursor-pointer transition-all duration-200 opacity-0 flex items-center justify-center z-10 hover:text-[var(--foreground)] hover:border-[var(--border-hover)] hover:bg-[var(--background-tertiary)] hover:-translate-y-px active:translate-y-0';
button.type = 'button';
button.ariaLabel = 'Copy code to clipboard';
button.title = 'Copy code';
button.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
`;

codeBlock.classList.add('relative', 'group');
codeBlock.appendChild(button);

button.addEventListener('click', async () => {
const code = codeBlock.querySelector('code');
const text = code ? code.innerText : codeBlock.innerText;

try {
await navigator.clipboard.writeText(text);

const originalHTML = button.innerHTML;
button.classList.add('!text-[var(--accent)]', '!border-[var(--accent)]', '!opacity-100');
button.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
`;

setTimeout(() => {
button.classList.remove('!text-[var(--accent)]', '!border-[var(--accent)]', '!opacity-100');
button.innerHTML = originalHTML;
}, 2000);
} catch (err) {
console.error('Failed to copy: ', err);
}
});
});
}

addCopyButtons();
document.addEventListener('astro:page-load', addCopyButtons);
</script>

<style is:global>
/* Show button on hover or focus */
pre:hover .copy-code-button,
.copy-code-button:focus-visible {
opacity: 1;
}

/* Always show on mobile */
@media (max-width: 768px) {
.copy-code-button {
opacity: 1;
}
}
</style>
36 changes: 18 additions & 18 deletions src/components/SearchDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,20 @@ export default function SearchDialog({ items }: SearchDialogProps) {
<div className="fixed inset-0 z-50">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm"
className="fixed inset-0 bg-black/40 backdrop-blur-sm"
onClick={() => setOpen(false)}
/>

{/* Dialog */}
<div className="fixed left-1/2 top-1/4 w-full max-w-lg -translate-x-1/2 -translate-y-1/4 p-4">
<Command
className="rounded-xl border border-[#27272a] bg-[#0a0a0a] shadow-2xl overflow-hidden"
className="rounded-xl border border-[var(--border)] bg-[var(--background)] shadow-2xl overflow-hidden"
loop
>
<div className="flex items-center border-b border-[#27272a] px-4">
<div className="flex items-center border-b border-[var(--border)] px-4">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 text-[#71717a] mr-3"
className="h-5 w-5 text-[var(--foreground-subtle)] mr-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
Expand All @@ -89,29 +89,29 @@ export default function SearchDialog({ items }: SearchDialogProps) {
value={search}
onValueChange={setSearch}
placeholder="Search..."
className="flex-1 bg-transparent py-4 text-[#fafafa] placeholder-[#71717a] outline-none"
className="flex-1 bg-transparent py-4 text-[var(--foreground)] placeholder-[var(--foreground-subtle)] outline-none"
/>
<kbd className="hidden sm:inline-flex items-center gap-1 rounded bg-[#141414] px-2 py-1 text-xs text-[#71717a]">
<kbd className="hidden sm:inline-flex items-center gap-1 rounded bg-[var(--background-secondary)] px-2 py-1 text-xs text-[var(--foreground-subtle)]">
ESC
</kbd>
</div>

<Command.List className="max-h-80 overflow-y-auto p-2">
<Command.Empty className="py-6 text-center text-sm text-[#71717a]">
<Command.Empty className="py-6 text-center text-sm text-[var(--foreground-subtle)]">
No results found.
</Command.Empty>

{pages.length > 0 && (
<Command.Group
heading="Pages"
className="text-xs font-medium text-[#71717a] px-2 py-1.5"
className="text-xs font-medium text-[var(--foreground-subtle)] px-2 py-1.5"
>
{pages.map((item) => (
<Command.Item
key={item.href}
value={item.title}
onSelect={() => handleSelect(item.href)}
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-[#a1a1aa] cursor-pointer data-[selected=true]:bg-[#141414] data-[selected=true]:text-[#fafafa]"
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-[var(--foreground-muted)] cursor-pointer data-[selected=true]:bg-[var(--background-secondary)] data-[selected=true]:text-[var(--foreground)]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -136,14 +136,14 @@ export default function SearchDialog({ items }: SearchDialogProps) {
{blogs.length > 0 && (
<Command.Group
heading="Blog Posts"
className="text-xs font-medium text-[#71717a] px-2 py-1.5 mt-2"
className="text-xs font-medium text-[var(--foreground-subtle)] px-2 py-1.5 mt-2"
>
{blogs.slice(0, 10).map((item) => (
<Command.Item
key={item.href}
value={item.title}
onSelect={() => handleSelect(item.href)}
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-[#a1a1aa] cursor-pointer data-[selected=true]:bg-[#141414] data-[selected=true]:text-[#fafafa]"
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-[var(--foreground-muted)] cursor-pointer data-[selected=true]:bg-[var(--background-secondary)] data-[selected=true]:text-[var(--foreground)]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -163,7 +163,7 @@ export default function SearchDialog({ items }: SearchDialogProps) {
</Command.Item>
))}
{blogs.length > 10 && (
<div className="px-3 py-2 text-xs text-[#71717a]">
<div className="px-3 py-2 text-xs text-[var(--foreground-subtle)]">
+ {blogs.length - 10} more posts
</div>
)}
Expand All @@ -173,14 +173,14 @@ export default function SearchDialog({ items }: SearchDialogProps) {
{notes.length > 0 && (
<Command.Group
heading="Notes"
className="text-xs font-medium text-[#71717a] px-2 py-1.5 mt-2"
className="text-xs font-medium text-[var(--foreground-subtle)] px-2 py-1.5 mt-2"
>
{notes.slice(0, 5).map((item) => (
<Command.Item
key={item.href}
value={item.title}
onSelect={() => handleSelect(item.href)}
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-[#a1a1aa] cursor-pointer data-[selected=true]:bg-[#141414] data-[selected=true]:text-[#fafafa]"
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-[var(--foreground-muted)] cursor-pointer data-[selected=true]:bg-[var(--background-secondary)] data-[selected=true]:text-[var(--foreground)]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -197,7 +197,7 @@ export default function SearchDialog({ items }: SearchDialogProps) {
<Command.Item
value="view-all-notes"
onSelect={() => handleSelect('/notes/')}
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-[#10b981] cursor-pointer data-[selected=true]:bg-[#141414]"
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-[var(--accent)] cursor-pointer data-[selected=true]:bg-[var(--background-secondary)]"
>
View all {notes.length} notes
</Command.Item>
Expand All @@ -206,11 +206,11 @@ export default function SearchDialog({ items }: SearchDialogProps) {
)}
</Command.List>

<div className="border-t border-[#27272a] px-4 py-2 text-xs text-[#71717a]">
<div className="border-t border-[var(--border)] px-4 py-2 text-xs text-[var(--foreground-subtle)]">
<span className="flex items-center gap-2">
<kbd className="rounded bg-[#141414] px-1.5 py-0.5">↑↓</kbd>
<kbd className="rounded bg-[var(--background-secondary)] px-1.5 py-0.5">↑↓</kbd>
<span>to navigate</span>
<kbd className="rounded bg-[#141414] px-1.5 py-0.5 ml-2">↵</kbd>
<kbd className="rounded bg-[var(--background-secondary)] px-1.5 py-0.5 ml-2">↵</kbd>
<span>to select</span>
</span>
</div>
Expand Down
8 changes: 8 additions & 0 deletions src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import BaseHead from '../components/BaseHead.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import SearchWrapper from '../components/SearchWrapper.astro';
import CodeCopy from '../components/CodeCopy.astro';
import BackToTop from '../components/BackToTop.astro';
import '../styles/global.css';

interface Props {
Expand Down Expand Up @@ -36,5 +38,11 @@ const { title, description, image, type, publishedTime } = Astro.props;

<!-- Command Palette -->
<SearchWrapper />

<!-- Code Block Copy Button -->
<CodeCopy />

<!-- Back to Top Button -->
<BackToTop />
</body>
</html>
12 changes: 12 additions & 0 deletions src/layouts/CaseStudyLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
import BaseHead from '../components/BaseHead.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import SearchWrapper from '../components/SearchWrapper.astro';
import CodeCopy from '../components/CodeCopy.astro';
import BackToTop from '../components/BackToTop.astro';
import '../styles/global.css';

interface Props {
Expand Down Expand Up @@ -85,5 +88,14 @@ const { title, company, role, period, technologies, summary, impact } = Astro.pr
</main>

<Footer />

<!-- Command Palette -->
<SearchWrapper />

<!-- Code Block Copy Button -->
<CodeCopy />

<!-- Back to Top Button -->
<BackToTop />
</body>
</html>
12 changes: 12 additions & 0 deletions src/layouts/PostLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import Footer from '../components/Footer.astro';
import ReadingProgress from '../components/ReadingProgress.astro';
import TableOfContents from '../components/TableOfContents.astro';
import TagCloud from '../components/TagCloud.astro';
import SearchWrapper from '../components/SearchWrapper.astro';
import CodeCopy from '../components/CodeCopy.astro';
import BackToTop from '../components/BackToTop.astro';
import '../styles/global.css';

interface Props {
Expand Down Expand Up @@ -142,5 +145,14 @@ const formattedDate = date.toLocaleDateString('en-US', {
</main>

<Footer />

<!-- Command Palette -->
<SearchWrapper />

<!-- Code Block Copy Button -->
<CodeCopy />

<!-- Back to Top Button -->
<BackToTop />
</body>
</html>