{post.tags.map((tag: string) => (
{tag}
diff --git a/src/app/posts/[...slug]/page.tsx b/src/app/posts/[...slug]/page.tsx
index dcf1107..82c5b10 100644
--- a/src/app/posts/[...slug]/page.tsx
+++ b/src/app/posts/[...slug]/page.tsx
@@ -31,68 +31,465 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
return () => clearInterval(interval);
}, [slugPath]);
- // On post load or update, scroll to anchor in hash if present
+ // Enhanced anchor scrolling logic
useEffect(() => {
- // Scroll to anchor if hash is present
- const scrollToHash = () => {
- if (window.location.hash) {
- const id = window.location.hash.substring(1);
- const el = document.getElementById(id);
- if (el) {
- el.scrollIntoView({ behavior: 'smooth', block: 'start' });
- }
- }
- };
- // On initial load
- scrollToHash();
- // Listen for hash changes
- window.addEventListener('hashchange', scrollToHash);
- return () => {
- window.removeEventListener('hashchange', scrollToHash);
- };
- }, [post]);
+ if (!post) return;
- // Intercept anchor clicks in rendered markdown to ensure smooth scrolling to headings
- useEffect(() => {
- // Find the rendered markdown container
- const prose = document.querySelector('.prose');
- if (!prose) return;
- /**
- * Handles clicks on anchor links (e.g. Table of Contents links) inside the markdown.
- * - If the link is an in-page anchor (starts with #), prevent default navigation.
- * - Try to find an element with the corresponding id and scroll to it.
- * - If not found, search all headings for one whose text matches the anchor (case-insensitive, ignoring spaces/punctuation).
- * - If a match is found, scroll to that heading.
- * - Update the URL hash without reloading the page.
- */
- const handleClick = (e: Event) => {
- if (!(e instanceof MouseEvent)) return;
- let target = e.target as HTMLElement | null;
- // Traverse up to find the closest anchor tag
- while (target && target.tagName !== 'A') {
- target = target.parentElement;
+ // Function to scroll to element using scrollIntoView
+ const scrollToElement = (element: HTMLElement) => {
+ // Get comprehensive element information
+ const documentHeight = document.documentElement.scrollHeight;
+ const windowHeight = window.innerHeight;
+
+ // Detect if we're on desktop or mobile layout
+ const isDesktop = window.innerWidth >= 640; // sm breakpoint
+ const proseContainer = document.querySelector('.prose');
+
+ console.log('Layout detection:', {
+ isDesktop,
+ windowWidth: window.innerWidth,
+ proseContainer: proseContainer ? 'found' : 'not found'
+ });
+
+ // Get the absolute position of the element
+ const currentScrollY = window.scrollY;
+ let elementTop = 0;
+
+ if (isDesktop) {
+ // For desktop, we need to account for the nested container structure
+ // The content is inside a container with padding and margins
+ const rect = element.getBoundingClientRect();
+ elementTop = rect.top + currentScrollY;
+
+ // If we're at the top and getting 0, try a different approach
+ if (elementTop === 0 && currentScrollY === 0) {
+ // Walk up the DOM tree to calculate position
+ let currentElement = element;
+ while (currentElement && currentElement !== document.body) {
+ elementTop += currentElement.offsetTop;
+ currentElement = currentElement.offsetParent as HTMLElement;
+ }
+ }
+ } else {
+ // For mobile, use the simpler approach
+ const rect = element.getBoundingClientRect();
+ elementTop = rect.top + currentScrollY;
}
- if (target && target.tagName === 'A' && target.getAttribute('href')?.startsWith('#')) {
- e.preventDefault();
- const id = target.getAttribute('href')!.slice(1);
- let el = document.getElementById(id);
- if (!el) {
- // Try to find a heading whose text matches the id (case-insensitive, ignoring spaces/punctuation)
- const headings = prose.querySelectorAll('h1, h2, h3, h4, h5, h6');
- const normalize = (str: string) => str.toLowerCase().replace(/[^a-z0-9]+/g, '');
- const normId = normalize(id);
- const found = Array.from(headings).find(h => normalize(h.textContent || '') === normId);
- el = (found as HTMLElement) || null;
+
+ // If we're not at the top, temporarily scroll to top to get accurate positions
+ if (currentScrollY > 0 && elementTop === currentScrollY) {
+ // Temporarily scroll to top to get accurate element positions
+ window.scrollTo(0, 0);
+
+ // Wait a moment for the scroll to complete, then measure
+ setTimeout(() => {
+ const rect = element.getBoundingClientRect();
+ elementTop = rect.top;
+
+ // Restore original scroll position
+ window.scrollTo(0, currentScrollY);
+
+ // Now perform the actual scroll to the target
+ performActualScroll(elementTop);
+ }, 50);
+ return;
+ } else {
+ // We're already at the top or have a valid position, get position directly
+ if (elementTop === 0 && currentScrollY === 0) {
+ const rect = element.getBoundingClientRect();
+ elementTop = rect.top;
}
- if (el) {
- el.scrollIntoView({ behavior: 'smooth', block: 'start' });
- history.replaceState(null, '', `#${id}`);
+ performActualScroll(elementTop);
+ }
+
+ function performActualScroll(elementTop: number) {
+ console.log('Element details:', {
+ elementText: element.textContent?.substring(0, 50),
+ elementId: element.id,
+ elementTop,
+ currentScrollY: window.scrollY,
+ documentHeight,
+ windowHeight,
+ canScroll: documentHeight > windowHeight,
+ isDesktop
+ });
+
+ // Check if page is scrollable
+ if (documentHeight <= windowHeight) {
+ console.warn('Page is not tall enough to scroll');
+ return;
}
+
+ // Calculate the target scroll position with different offsets for desktop/mobile
+ const offset = isDesktop ? 120 : 100; // Slightly more offset for desktop due to header
+ const targetScrollY = Math.max(0, elementTop - offset);
+
+ console.log('Scroll calculation:', {
+ elementTop,
+ targetScrollY,
+ offset,
+ currentScrollY: window.scrollY,
+ scrollDifference: targetScrollY - window.scrollY,
+ isDesktop
+ });
+
+ // Check if we need to scroll at all
+ if (Math.abs(window.scrollY - targetScrollY) < 10) {
+ console.log('Element already at target position, no scroll needed');
+ return;
+ }
+
+ // Use a simple, reliable scroll method
+ console.log(`Scrolling from ${window.scrollY} to ${targetScrollY}`);
+
+ // Use requestAnimationFrame for smooth scrolling
+ const startScrollY = window.scrollY;
+ const scrollDistance = targetScrollY - startScrollY;
+ const duration = 500; // 500ms
+ const startTime = performance.now();
+
+ const animateScroll = (currentTime: number) => {
+ const elapsed = currentTime - startTime;
+ const progress = Math.min(elapsed / duration, 1);
+
+ // Easing function (ease-out)
+ const easeOut = 1 - Math.pow(1 - progress, 3);
+
+ const currentScrollY = startScrollY + (scrollDistance * easeOut);
+ window.scrollTo(0, currentScrollY);
+
+ if (progress < 1) {
+ requestAnimationFrame(animateScroll);
+ } else {
+ console.log('Scroll animation completed');
+ }
+ };
+
+ requestAnimationFrame(animateScroll);
+
+ // Log the scroll after a delay to verify it worked
+ setTimeout(() => {
+ console.log('Scroll verification - new scrollY:', window.scrollY);
+ console.log('Scroll difference:', Math.abs(window.scrollY - targetScrollY));
+ }, 1000);
}
};
- prose.addEventListener('click', handleClick);
+
+ // Function to find and scroll to element with retry
+ const findAndScrollToElement = (id: string, retryCount: number = 0) => {
+ // First check if the content is rendered
+ const proseContent = document.querySelector('.prose');
+ if (!proseContent || !proseContent.innerHTML.trim()) {
+ if (retryCount < 10) {
+ console.log(`Content not yet rendered, retrying... (${retryCount + 1}/10)`);
+ setTimeout(() => {
+ findAndScrollToElement(id, retryCount + 1);
+ }, 100);
+ return;
+ } else {
+ console.warn('Content not rendered after retries');
+ return;
+ }
+ }
+
+ // Find the element, but only consider visible ones
+ const allElements = document.querySelectorAll(`#${id}`);
+ let element: HTMLElement | null = null;
+
+ // Check if we're on desktop or mobile
+ const isDesktop = window.innerWidth >= 640;
+
+ for (const el of Array.from(allElements)) {
+ const htmlEl = el as HTMLElement;
+ // Check if the element is visible (not hidden by CSS)
+ const rect = htmlEl.getBoundingClientRect();
+ const isVisible = rect.width > 0 && rect.height > 0;
+
+ if (isVisible) {
+ element = htmlEl;
+ break;
+ }
+ }
+
+ if (element) {
+ console.log('Found target element:', element.textContent?.substring(0, 50));
+ scrollToElement(element);
+ } else if (retryCount < 5) {
+ console.log(`Element not found for anchor: ${id}, retrying... (${retryCount + 1}/5)`);
+ setTimeout(() => {
+ findAndScrollToElement(id, retryCount + 1);
+ }, 100);
+ } else {
+ console.warn(`Element with id "${id}" not found after retries`);
+ }
+ };
+
+ // Function to handle anchor link clicks
+ const handleAnchorClick = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+ const link = target.closest('a');
+
+ if (!link || !link.getAttribute('href')?.startsWith('#')) return;
+
+ const href = link.getAttribute('href');
+ const id = href?.substring(1);
+
+ if (!id) return;
+
+ console.log('Anchor click detected:', href);
+
+ // Prevent default behavior first
+ event.preventDefault();
+
+ // Find the target element and scroll to it
+ findAndScrollToElement(id);
+ };
+
+ // Function to handle hash-based scrolling on page load
+ const handleHashScroll = () => {
+ if (!window.location.hash) return;
+
+ const id = window.location.hash.substring(1);
+ console.log('Handling hash scroll for:', id);
+
+ // Use a longer delay to ensure DOM is fully rendered
+ setTimeout(() => {
+ findAndScrollToElement(id);
+ }, 300);
+ };
+
+ // Handle initial hash scroll
+ handleHashScroll();
+
+ // Add event listener for anchor clicks
+ document.addEventListener('click', handleAnchorClick);
+
+ // Add a test function to the window object for debugging
+ (window as any).testScroll = (id: string) => {
+ console.log('Testing scroll to:', id);
+ findAndScrollToElement(id);
+ };
+
+ // Add a function to test basic scrolling
+ (window as any).testBasicScroll = () => {
+ console.log('Testing basic scroll functionality');
+ const currentScrollY = window.scrollY;
+ const testScrollY = currentScrollY + 500;
+
+ console.log('Current scrollY:', currentScrollY);
+ console.log('Target scrollY:', testScrollY);
+
+ window.scrollTo({
+ top: testScrollY,
+ behavior: 'smooth'
+ });
+
+ setTimeout(() => {
+ console.log('Basic scroll test completed, new scrollY:', window.scrollY);
+ }, 1000);
+ };
+
+ // Add a function to test scrolling to a specific element
+ (window as any).testElementScroll = (id: string) => {
+ console.log('Testing scroll to element:', id);
+ const element = document.getElementById(id);
+ if (element) {
+ console.log('Element found, testing scroll...');
+ scrollToElement(element);
+ } else {
+ console.log('Element not found:', id);
+ (window as any).listIds();
+ }
+ };
+
+ // Add a simple test function
+ (window as any).runAnchorTest = () => {
+ console.log('Running anchor link test...');
+
+ // Test 1: Check if we can find the "overview" heading
+ const overviewElement = document.getElementById('overview');
+ if (overviewElement) {
+ console.log('✅ Found overview element, testing scroll...');
+ scrollToElement(overviewElement);
+ } else {
+ console.log('❌ Overview element not found');
+ }
+
+ // Test 2: Check if we can find the "test-heading" element
+ setTimeout(() => {
+ const testHeadingElement = document.getElementById('test-heading');
+ if (testHeadingElement) {
+ console.log('✅ Found test-heading element, testing scroll...');
+ scrollToElement(testHeadingElement);
+ } else {
+ console.log('❌ Test-heading element not found');
+ }
+ }, 2000);
+ };
+
+ // Add a desktop-specific test function
+ (window as any).testDesktopScroll = () => {
+ console.log('=== Desktop Scroll Test ===');
+
+ const isDesktop = window.innerWidth >= 640;
+ console.log('Layout detection:', {
+ isDesktop,
+ windowWidth: window.innerWidth,
+ windowHeight: window.innerHeight
+ });
+
+ // Test scrolling to a known element
+ const overviewElement = document.getElementById('overview');
+ if (overviewElement) {
+ console.log('Testing desktop scroll to overview...');
+
+ // Get element position using desktop method
+ const rect = overviewElement.getBoundingClientRect();
+ const elementTop = rect.top + window.scrollY;
+
+ console.log('Desktop position calculation:', {
+ rectTop: rect.top,
+ currentScrollY: window.scrollY,
+ calculatedElementTop: elementTop
+ });
+
+ scrollToElement(overviewElement);
+ } else {
+ console.log('Overview element not found');
+ }
+
+ console.log('=== End Desktop Test ===');
+ };
+
+ // Add a function to list all available IDs
+ (window as any).listIds = () => {
+ const allIds = Array.from(document.querySelectorAll('[id]')).map(el => ({
+ id: el.id,
+ text: el.textContent?.substring(0, 50),
+ tag: el.tagName
+ }));
+ console.log('Available IDs on page:', allIds);
+ return allIds;
+ };
+
+ // Add a function to debug anchor links
+ (window as any).debugAnchors = () => {
+ console.log('=== Anchor Link Debug ===');
+
+ // Get all anchor links
+ const anchorLinks = Array.from(document.querySelectorAll('a[href^="#"]')).map(el => ({
+ href: el.getAttribute('href'),
+ text: el.textContent,
+ targetId: el.getAttribute('href')?.substring(1)
+ }));
+
+ console.log('Anchor links found:', anchorLinks);
+
+ // Get all headings with IDs
+ const headings = Array.from(document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]')).map(el => ({
+ id: el.id,
+ text: el.textContent?.substring(0, 50),
+ tag: el.tagName,
+ offsetTop: (el as HTMLElement).offsetTop,
+ getBoundingClientRect: (el as HTMLElement).getBoundingClientRect()
+ }));
+
+ console.log('Headings with IDs:', headings);
+
+ // Check which anchor links have matching headings
+ anchorLinks.forEach(link => {
+ const hasMatch = headings.some(h => h.id === link.targetId);
+ const status = hasMatch ? '✅' : '❌';
+ console.log(`${status} [${link.text}](#${link.targetId}) -> ${hasMatch ? 'FOUND' : 'NOT FOUND'}`);
+ });
+
+ console.log('=== End Debug ===');
+ };
+
+ // Add a function to show element positions
+ (window as any).showPositions = () => {
+ console.log('=== Element Positions ===');
+
+ const headings = Array.from(document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]'));
+
+ // Filter to only visible elements
+ const visibleHeadings = headings.filter(el => {
+ const rect = (el as HTMLElement).getBoundingClientRect();
+ return rect.width > 0 && rect.height > 0;
+ });
+
+ visibleHeadings.forEach((el, index) => {
+ const element = el as HTMLElement;
+
+ // Calculate absolute position
+ let elementTop = 0;
+ let currentElement = element;
+ while (currentElement && currentElement !== document.body) {
+ elementTop += currentElement.offsetTop;
+ currentElement = currentElement.offsetParent as HTMLElement;
+ }
+
+ console.log(`${index + 1}. ${element.textContent?.substring(0, 30)}:`);
+ console.log(` ID: ${element.id}`);
+ console.log(` Calculated elementTop: ${elementTop}`);
+ console.log(` element.offsetTop: ${element.offsetTop}`);
+ console.log(` Current scrollY: ${window.scrollY}`);
+ console.log(` Would scroll to: ${Math.max(0, elementTop - 100)}`);
+ console.log('---');
+ });
+
+ console.log(`=== End Positions (${visibleHeadings.length} visible elements) ===`);
+ };
+
+ // Add a simple test function
+ (window as any).testScrollToElement = (id: string) => {
+ // Find visible element with this ID
+ const allElements = document.querySelectorAll(`#${id}`);
+ let element: HTMLElement | null = null;
+
+ for (const el of Array.from(allElements)) {
+ const htmlEl = el as HTMLElement;
+ const rect = htmlEl.getBoundingClientRect();
+ const isVisible = rect.width > 0 && rect.height > 0;
+
+ if (isVisible) {
+ element = htmlEl;
+ break;
+ }
+ }
+
+ if (element) {
+ console.log(`Testing scroll to ${id}...`);
+
+ // Calculate position the same way as scrollToElement
+ let elementTop = 0;
+ let currentElement = element;
+ while (currentElement && currentElement !== document.body) {
+ elementTop += currentElement.offsetTop;
+ currentElement = currentElement.offsetParent as HTMLElement;
+ }
+
+ const targetScrollY = Math.max(0, elementTop - 100);
+ console.log(`Element ${id} is at position ${elementTop}, would scroll to ${targetScrollY}`);
+ console.log(`Current scroll position: ${window.scrollY}`);
+
+ // Perform the scroll
+ scrollToElement(element);
+ } else {
+ console.log(`Element with id "${id}" not found`);
+ (window as any).listIds();
+ }
+ };
+
return () => {
- prose.removeEventListener('click', handleClick);
+ document.removeEventListener('click', handleAnchorClick);
+ delete (window as any).testScroll;
+ delete (window as any).testBasicScroll;
+ delete (window as any).testElementScroll;
+ delete (window as any).listIds;
+ delete (window as any).debugAnchors;
+ delete (window as any).showPositions;
+ delete (window as any).testScrollToElement;
};
}, [post]);
@@ -111,39 +508,106 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
}
return (
-
-
- ← Zurück zu den Beiträgen
-
- {post.title}
-
- {post.date ? (
-
Veröffentlicht: {format(new Date(post.date), 'd. MMMM yyyy')}
- ) : (
-
-
- ⚙️
- ⚙️
-
-
In Bearbeitung
-
- )}
-
Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}
-
-
- {post.tags.map((tag) => (
-
+ {/* Mobile: Full width, no borders */}
+
+ {/* Mobile back button */}
+
+
- {tag}
-
- ))}
+
+ Zurück
+
+
+
+ {/* Mobile content - full width, optimized for reading */}
+
+
{post.title}
+
+
+ {post.date ? (
+
Veröffentlicht: {format(new Date(post.date), 'd. MMMM yyyy')}
+ ) : (
+
+ ⚙️
+ In Bearbeitung
+
+ )}
+
Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}
+
+
+
+ {post.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {/* Mobile-optimized prose content */}
+
+
+
+
+ {/* Desktop: Wider content area with minimal borders */}
+
+
+ {/* Desktop back button */}
+
+
+ Zurück zu den Beiträgen
+
+
+ {/* Desktop content with minimal border */}
+
+
{post.title}
+
+
+ {post.date ? (
+
Veröffentlicht: {format(new Date(post.date), 'd. MMMM yyyy')}
+ ) : (
+
+ ⚙️
+ In Bearbeitung
+
+ )}
+
Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}
+
+
+
+ {post.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {/* Desktop-optimized prose content */}
+
+
+
-
);
}
\ No newline at end of file
diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts
index 1a8d169..18db88d 100644
--- a/src/lib/markdown.ts
+++ b/src/lib/markdown.ts
@@ -28,7 +28,100 @@ function getFileCreationDate(filePath: string): Date {
return stats.birthtime;
}
+// Function to generate ID from text (matches frontend logic)
+function generateId(text: string): string {
+ return text
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '');
+}
+
+// Enhanced slugification function that matches GitHub-style anchor links
+function slugify(text: string): string {
+ return text
+ .toLowerCase()
+ .trim()
+ .replace(/[^\w\s-]/g, '') // Remove special characters except spaces and hyphens
+ .replace(/[\s_-]+/g, '-') // Replace spaces, underscores, and multiple hyphens with single hyphen
+ .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
+}
+
+// Function to process anchor links in markdown content
+function processAnchorLinks(content: string): string {
+ // Find all markdown links that point to anchors (e.g., [text](#anchor))
+ return content.replace(/\[([^\]]+)\]\(#([^)]+)\)/g, (match, linkText, anchor) => {
+ // Only slugify if the anchor doesn't already look like a slug
+ // This prevents double-processing of already-correct anchor links
+ const isAlreadySlugified = /^[a-z0-9-]+$/.test(anchor);
+ const slugifiedAnchor = isAlreadySlugified ? anchor : slugify(anchor);
+ return `[${linkText}](#${slugifiedAnchor})`;
+ });
+}
+
+// Utility function to debug anchor links (for development)
+export function debugAnchorLinks(content: string): void {
+ if (process.env.NODE_ENV !== 'development') return;
+
+ console.log('=== Anchor Link Debug Info ===');
+
+ // Extract all headings and their IDs
+ const headingRegex = /^(#{1,6})\s+(.+)$/gm;
+ const headings: Array<{ level: number; text: string; id: string }> = [];
+
+ let match;
+ while ((match = headingRegex.exec(content)) !== null) {
+ const level = match[1].length;
+ const text = match[2].trim();
+ const id = slugify(text);
+ headings.push({ level, text, id });
+ }
+
+ console.log('Generated heading IDs:');
+ headings.forEach(({ level, text, id }) => {
+ console.log(` H${level}: "${text}" -> id="${id}"`);
+ });
+
+ // Extract all anchor links
+ const anchorLinkRegex = /\[([^\]]+)\]\(#([^)]+)\)/g;
+ const anchorLinks: Array<{ linkText: string; originalAnchor: string; slugifiedAnchor: string }> = [];
+
+ while ((match = anchorLinkRegex.exec(content)) !== null) {
+ const linkText = match[1];
+ const originalAnchor = match[2];
+ const slugifiedAnchor = slugify(originalAnchor);
+ anchorLinks.push({ linkText, originalAnchor, slugifiedAnchor });
+ }
+
+ console.log('Anchor links found:');
+ anchorLinks.forEach(({ linkText, originalAnchor, slugifiedAnchor }) => {
+ const headingExists = headings.some(h => h.id === slugifiedAnchor);
+ const status = headingExists ? '✅' : '❌';
+ console.log(` ${status} [${linkText}](#${originalAnchor}) -> [${linkText}](#${slugifiedAnchor})`);
+ });
+
+ // Show missing headings
+ const missingAnchors = anchorLinks.filter(({ slugifiedAnchor }) =>
+ !headings.some(h => h.id === slugifiedAnchor)
+ );
+
+ if (missingAnchors.length > 0) {
+ console.warn('Missing headings for these anchor links:');
+ missingAnchors.forEach(({ linkText, originalAnchor, slugifiedAnchor }) => {
+ console.warn(` - [${linkText}](#${originalAnchor}) -> id="${slugifiedAnchor}"`);
+ });
+ }
+
+ console.log('=== End Debug Info ===');
+}
+
const renderer = new marked.Renderer();
+
+// Custom heading renderer to add IDs
+renderer.heading = (text, level) => {
+ const id = slugify(text);
+ return `${text}`;
+};
+
renderer.code = (code, infostring, escaped) => {
const lang = (infostring || '').match(/\S*/)?.[0];
const highlighted = lang && hljs.getLanguage(lang)
@@ -53,7 +146,12 @@ export async function getPostBySlug(slug: string): Promise {
let processedContent = '';
try {
- const rawHtml = marked.parse(content);
+ // Debug anchor links in development
+ debugAnchorLinks(content);
+
+ // Process anchor links before parsing markdown
+ const processedMarkdown = processAnchorLinks(content);
+ const rawHtml = marked.parse(processedMarkdown);
const window = new JSDOM('').window;
const purify = DOMPurify(window);
processedContent = purify.sanitize(rawHtml as string, {
diff --git a/tailwind.config.js b/tailwind.config.js
index d2078d7..a202e62 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -6,7 +6,45 @@ module.exports = {
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
- extend: {},
+ extend: {
+ screens: {
+ 'xs': '475px',
+ '3xl': '1600px',
+ },
+ spacing: {
+ '18': '4.5rem',
+ '88': '22rem',
+ },
+ fontSize: {
+ 'xs': ['0.75rem', { lineHeight: '1rem' }],
+ 'sm': ['0.875rem', { lineHeight: '1.25rem' }],
+ 'base': ['1rem', { lineHeight: '1.5rem' }],
+ 'lg': ['1.125rem', { lineHeight: '1.75rem' }],
+ 'xl': ['1.25rem', { lineHeight: '1.75rem' }],
+ '2xl': ['1.5rem', { lineHeight: '2rem' }],
+ '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
+ '4xl': ['2.25rem', { lineHeight: '2.5rem' }],
+ '5xl': ['3rem', { lineHeight: '1' }],
+ '6xl': ['3.75rem', { lineHeight: '1' }],
+ },
+ colors: {
+ gray: {
+ 50: '#f9fafb',
+ 100: '#f3f4f6',
+ 200: '#e5e7eb',
+ 300: '#d1d5db',
+ 400: '#9ca3af',
+ 500: '#6b7280',
+ 600: '#4b5563',
+ 700: '#374151',
+ 800: '#1f2937',
+ 900: '#111827',
+ },
+ },
+ animation: {
+ 'spin-reverse': 'spin 1s linear infinite reverse',
+ },
+ },
},
plugins: [
require('@tailwindcss/typography'),