mirror of
https://github.com/mmahdium/portfolio.git
synced 2026-08-16 21:14:31 +03:30
feat(blog): Enhance blog content styling and readability
- Add comprehensive blog content CSS with RTL support - Implement responsive typography and layout improvements - Create dedicated CSS for blog prose and content styling - Optimize code blocks, headings, and typography for better readability - Add scroll padding and responsive design considerations - Improve dark mode color contrast and styling - Enhance code and blockquote styling with better visual hierarchy
This commit is contained in:
@@ -10,6 +10,7 @@ const props = defineProps<{
|
||||
toc: {
|
||||
links: TocLink[]
|
||||
}
|
||||
mobile?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -25,33 +26,48 @@ const shouldShowToc = computed(() => {
|
||||
return countLinks(props.toc.links) >= 3
|
||||
})
|
||||
|
||||
// Smooth scroll to heading
|
||||
// Smooth scroll to heading with offset for sticky header
|
||||
const scrollToHeading = (id: string) => {
|
||||
const element = document.getElementById(id)
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
const offset = 100 // Offset for sticky header
|
||||
const elementPosition = element.getBoundingClientRect().top + window.pageYOffset
|
||||
const offsetPosition = elementPosition - offset
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
|
||||
activeId.value = id
|
||||
}
|
||||
}
|
||||
|
||||
// Track active section with IntersectionObserver
|
||||
onMounted(() => {
|
||||
const headings = document.querySelectorAll('article h2, article h3, article h4')
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
activeId.value = entry.target.id
|
||||
// Find the first intersecting heading
|
||||
const intersecting = entries.filter(entry => entry.isIntersecting)
|
||||
if (intersecting.length > 0) {
|
||||
// Sort by position and get the topmost one
|
||||
const topmost = intersecting.sort((a, b) =>
|
||||
a.boundingClientRect.top - b.boundingClientRect.top
|
||||
)[0]
|
||||
if (topmost && topmost.target.id) {
|
||||
activeId.value = topmost.target.id
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: '-80px 0px -80% 0px',
|
||||
threshold: 0
|
||||
rootMargin: '-100px 0px -66% 0px',
|
||||
threshold: [0, 0.25, 0.5, 0.75, 1]
|
||||
}
|
||||
)
|
||||
|
||||
// Observe all headings
|
||||
const headings = document.querySelectorAll('article h2, article h3, article h4')
|
||||
headings.forEach((heading) => observer.observe(heading))
|
||||
|
||||
// Cleanup
|
||||
@@ -59,51 +75,50 @@ onMounted(() => {
|
||||
headings.forEach((heading) => observer.unobserve(heading))
|
||||
})
|
||||
})
|
||||
|
||||
// Render TOC links recursively
|
||||
const renderTocLinks = (links: TocLink[]) => {
|
||||
return links
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside v-if="shouldShowToc" class="toc-container">
|
||||
<!-- Desktop: Sticky sidebar -->
|
||||
<nav class="hidden lg:block sticky top-24 max-h-[calc(100vh-8rem)] overflow-y-auto">
|
||||
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
||||
<div v-if="shouldShowToc">
|
||||
<!-- Desktop TOC -->
|
||||
<div v-if="!mobile"
|
||||
class="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 p-4 max-h-[calc(100vh-7rem)] overflow-y-auto">
|
||||
<h3 class="text-sm font-semibold mb-3 text-gray-900 dark:text-gray-100 uppercase tracking-wide">
|
||||
{{ t('blog.tableOfContents') }}
|
||||
</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<template v-for="link in toc.links" :key="link.id">
|
||||
<li>
|
||||
<a :href="`#${link.id}`" :class="[
|
||||
'block py-1 transition-colors',
|
||||
activeId === link.id
|
||||
? 'text-primary-600 dark:text-primary-400 font-medium'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
]" @click.prevent="scrollToHeading(link.id)">
|
||||
{{ link.text }}
|
||||
</a>
|
||||
<!-- Nested children (h3) -->
|
||||
<ul v-if="link.children && link.children.length > 0" class="ml-4 mt-1 space-y-1">
|
||||
<li v-for="child in link.children" :key="child.id">
|
||||
<a :href="`#${child.id}`" :class="[
|
||||
'block py-1 transition-colors',
|
||||
activeId === child.id
|
||||
? 'text-primary-600 dark:text-primary-400 font-medium'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
]" @click.prevent="scrollToHeading(child.id)">
|
||||
{{ child.text }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</nav>
|
||||
<nav>
|
||||
<ul class="space-y-1 text-sm">
|
||||
<template v-for="link in toc.links" :key="link.id">
|
||||
<li>
|
||||
<a :href="`#${link.id}`" :class="[
|
||||
'block py-1.5 px-2 rounded transition-all',
|
||||
activeId === link.id
|
||||
? 'text-primary-600 dark:text-primary-400 font-medium bg-primary-50 dark:bg-primary-900/20'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800/50'
|
||||
]" @click.prevent="scrollToHeading(link.id)">
|
||||
{{ link.text }}
|
||||
</a>
|
||||
<!-- Nested children (h3) -->
|
||||
<ul v-if="link.children && link.children.length > 0"
|
||||
class="ml-3 mt-0.5 space-y-0.5 border-l-2 border-gray-200 dark:border-gray-700 pl-3">
|
||||
<li v-for="child in link.children" :key="child.id">
|
||||
<a :href="`#${child.id}`" :class="[
|
||||
'block py-1 px-2 rounded text-xs transition-all',
|
||||
activeId === child.id
|
||||
? 'text-primary-600 dark:text-primary-400 font-medium bg-primary-50 dark:bg-primary-900/20'
|
||||
: 'text-gray-500 dark:text-gray-500 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800/50'
|
||||
]" @click.prevent="scrollToHeading(child.id)">
|
||||
{{ child.text }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Mobile: Collapsible accordion -->
|
||||
<UAccordion class="lg:hidden mb-6" :items="[
|
||||
<UAccordion v-if="mobile" :items="[
|
||||
{
|
||||
label: t('blog.tableOfContents'),
|
||||
icon: 'i-heroicons-list-bullet',
|
||||
@@ -141,11 +156,5 @@ const renderTocLinks = (links: TocLink[]) => {
|
||||
</ul>
|
||||
</template>
|
||||
</UAccordion>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toc-container {
|
||||
@apply w-full;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
<template>
|
||||
<div v-if="tags.length > 0" class="overflow-x-auto">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<UButton :color="!modelValue ? 'primary' : 'gray'" :variant="!modelValue ? 'solid' : 'soft'" size="sm"
|
||||
@click="selectTag(null)">
|
||||
<div v-if="tags.length > 0" class="overflow-x-auto scrollbar-hide -mx-1 px-1">
|
||||
<div class="flex flex-wrap gap-1.5 py-1">
|
||||
<!-- All Posts Badge -->
|
||||
<button :class="[
|
||||
'px-3 py-1 rounded-full text-xs font-medium transition-all duration-200',
|
||||
'ring-1 ring-inset',
|
||||
!modelValue
|
||||
? 'bg-primary-500 text-white ring-primary-500 shadow-sm'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 ring-gray-200 dark:ring-gray-700 hover:bg-gray-200 dark:hover:bg-gray-700 hover:ring-gray-300 dark:hover:ring-gray-600 hover:shadow-sm hover:scale-105'
|
||||
]" @click="selectTag(null)">
|
||||
{{ t('blog.allPosts') }}
|
||||
</UButton>
|
||||
</button>
|
||||
|
||||
<UButton v-for="tag in tags" :key="tag" :color="modelValue === tag ? 'primary' : 'gray'"
|
||||
:variant="modelValue === tag ? 'solid' : 'soft'" size="sm" @click="selectTag(tag)">
|
||||
<!-- Tag Badges -->
|
||||
<button v-for="tag in tags" :key="tag" :class="[
|
||||
'px-3 py-1 rounded-full text-xs font-medium transition-all duration-200',
|
||||
'ring-1 ring-inset',
|
||||
modelValue === tag
|
||||
? 'bg-primary-500 text-white ring-primary-500 shadow-sm'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 ring-gray-200 dark:ring-gray-700 hover:bg-gray-200 dark:hover:bg-gray-700 hover:ring-gray-300 dark:hover:ring-gray-600 hover:shadow-sm hover:scale-105'
|
||||
]" @click="selectTag(tag)">
|
||||
{{ tag }}
|
||||
</UButton>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -26,7 +38,6 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// Read query parameter on mount to restore filter state
|
||||
onMounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user