mirror of
https://github.com/mmahdium/portfolio.git
synced 2025-12-20 09:23:54 +01:00
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:
342
app/assets/css/blog-content.css
Normal file
342
app/assets/css/blog-content.css
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
/* Blog Content Styles - Optimized for Persian/RTL */
|
||||||
|
|
||||||
|
/* Base typography */
|
||||||
|
.blog-content {
|
||||||
|
font-size: 1.125rem; /* 18px */
|
||||||
|
line-height: 2; /* خطفاصله بیشتر */
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
color: rgb(55, 65, 81);
|
||||||
|
max-width: 75ch;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content {
|
||||||
|
color: rgb(229, 231, 235);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RTL-specific improvements */
|
||||||
|
.blog-content-rtl {
|
||||||
|
line-height: 2.2 !important; /* فارسی نیاز به فاصله بیشتری داره */
|
||||||
|
letter-spacing: 0.02em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force LTR for code blocks in RTL */
|
||||||
|
.blog-content-rtl pre,
|
||||||
|
.blog-content-rtl code {
|
||||||
|
direction: ltr;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Headings */
|
||||||
|
.blog-content h1,
|
||||||
|
.blog-content h2,
|
||||||
|
.blog-content h3,
|
||||||
|
.blog-content h4 {
|
||||||
|
scroll-margin-top: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content h1 {
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 2.25em;
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content h1 {
|
||||||
|
color: rgb(243, 244, 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content h2 {
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 1.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.875em;
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content h2 {
|
||||||
|
color: rgb(243, 244, 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content h3 {
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.5em;
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content h3 {
|
||||||
|
color: rgb(243, 244, 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content h4 {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.25em;
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content h4 {
|
||||||
|
color: rgb(243, 244, 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Paragraphs - فاصله زیاد برای خوانایی بهتر */
|
||||||
|
.blog-content p {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* First paragraph after heading */
|
||||||
|
.blog-content h1 + p,
|
||||||
|
.blog-content h2 + p,
|
||||||
|
.blog-content h3 + p,
|
||||||
|
.blog-content h4 + p {
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lists */
|
||||||
|
.blog-content ul,
|
||||||
|
.blog-content ol {
|
||||||
|
margin-top: 2.25rem;
|
||||||
|
margin-bottom: 2.25rem;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content-rtl ul,
|
||||||
|
.blog-content-rtl ol {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content li {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content-rtl li {
|
||||||
|
line-height: 2.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content li p {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nested lists */
|
||||||
|
.blog-content ul ul,
|
||||||
|
.blog-content ol ol,
|
||||||
|
.blog-content ul ol,
|
||||||
|
.blog-content ol ul {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Strong text */
|
||||||
|
.blog-content strong {
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content strong {
|
||||||
|
color: rgb(243, 244, 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
.blog-content pre {
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.blog-content pre {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
.blog-content code:not(pre code) {
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
background-color: rgba(99, 102, 241, 0.1);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgb(79, 70, 229);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content code:not(pre code) {
|
||||||
|
background-color: rgba(99, 102, 241, 0.2);
|
||||||
|
color: rgb(165, 180, 252);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blockquotes */
|
||||||
|
.blog-content blockquote {
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
padding: 1.75rem;
|
||||||
|
border-left: 4px solid rgb(99, 102, 241);
|
||||||
|
background-color: rgba(99, 102, 241, 0.05);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content-rtl blockquote {
|
||||||
|
border-left: none;
|
||||||
|
border-right: 4px solid rgb(99, 102, 241);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content blockquote {
|
||||||
|
background-color: rgba(99, 102, 241, 0.1);
|
||||||
|
border-left-color: rgb(129, 140, 248);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content-rtl blockquote {
|
||||||
|
border-right-color: rgb(129, 140, 248);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content blockquote p {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
.blog-content a {
|
||||||
|
color: rgb(99, 102, 241);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: rgba(99, 102, 241, 0.3);
|
||||||
|
text-underline-offset: 0.25em;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content a:hover {
|
||||||
|
color: rgb(79, 70, 229);
|
||||||
|
text-decoration-color: rgba(99, 102, 241, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content a {
|
||||||
|
color: rgb(129, 140, 248);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content a:hover {
|
||||||
|
color: rgb(165, 180, 252);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal rule */
|
||||||
|
.blog-content hr {
|
||||||
|
margin-top: 3.5rem;
|
||||||
|
margin-bottom: 3.5rem;
|
||||||
|
border-color: rgba(148, 163, 184, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content hr {
|
||||||
|
border-color: rgba(71, 85, 105, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Images */
|
||||||
|
.blog-content img {
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
.blog-content table {
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.blog-content table {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content th,
|
||||||
|
.blog-content td {
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.3);
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content th {
|
||||||
|
background-color: rgba(99, 102, 241, 0.1);
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content th {
|
||||||
|
background-color: rgba(99, 102, 241, 0.15);
|
||||||
|
color: rgb(243, 244, 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .blog-content td {
|
||||||
|
border-color: rgba(71, 85, 105, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Images - prevent overflow */
|
||||||
|
.blog-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent horizontal scroll on all elements */
|
||||||
|
.blog-content * {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content pre,
|
||||||
|
.blog-content code,
|
||||||
|
.blog-content table {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.blog-content {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content h2 {
|
||||||
|
font-size: 1.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content h3 {
|
||||||
|
font-size: 1.375em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content pre {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
margin-left: -1rem;
|
||||||
|
margin-right: -1rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "@nuxt/ui";
|
@import "@nuxt/ui";
|
||||||
@import "./transitions.css";
|
@import "./transitions.css";
|
||||||
|
@import "./prose.css";
|
||||||
|
@import "./blog-content.css";
|
||||||
|
|
||||||
@source "../../components/**/*.{vue,js,ts}";
|
@source "../../components/**/*.{vue,js,ts}";
|
||||||
@source "../../layouts/**/*.vue";
|
@source "../../layouts/**/*.vue";
|
||||||
@@ -62,6 +64,7 @@
|
|||||||
html {
|
html {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
/* Avoid width variation */
|
/* Avoid width variation */
|
||||||
|
scroll-padding-top: 2rem; /* Offset for fixed navbar when using anchor links */
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@@ -159,14 +162,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Hide scrollbars for overflow containers */
|
/* Hide scrollbars for overflow containers */
|
||||||
.no-scrollbar {
|
.no-scrollbar,
|
||||||
|
.scrollbar-hide {
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
/* IE and Edge */
|
/* IE and Edge */
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
/* Firefox */
|
/* Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-scrollbar::-webkit-scrollbar {
|
.no-scrollbar::-webkit-scrollbar,
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
/* Chrome, Safari, Opera */
|
/* Chrome, Safari, Opera */
|
||||||
}
|
}
|
||||||
|
|||||||
388
app/assets/css/prose.css
Normal file
388
app/assets/css/prose.css
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
/*
|
||||||
|
* Enhanced Prose Styles for Blog Content
|
||||||
|
* Optimized for both LTR and RTL (Persian) content
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Base prose container */
|
||||||
|
.prose {
|
||||||
|
color: rgb(55, 65, 81);
|
||||||
|
max-width: 65ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose {
|
||||||
|
color: rgb(229, 231, 235);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography scale */
|
||||||
|
.prose {
|
||||||
|
font-size: 1.125rem; /* 18px */
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RTL-specific adjustments */
|
||||||
|
.prose[dir="rtl"] {
|
||||||
|
line-height: 2.1;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Headings */
|
||||||
|
.prose :where(h1):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 2.25em;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.8888889em;
|
||||||
|
line-height: 1.1111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(h1):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(243, 244, 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(h2):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.875em;
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
line-height: 1.3333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(h2):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(243, 244, 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(h3):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-top: 1.6em;
|
||||||
|
margin-bottom: 0.6em;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(h3):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(243, 244, 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(h4):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.25em;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(h4):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(243, 244, 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Paragraphs */
|
||||||
|
.prose :where(p):not(:where([class~="not-prose"] *)) {
|
||||||
|
margin-top: 1.75em;
|
||||||
|
margin-bottom: 1.75em;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lead paragraph */
|
||||||
|
.prose :where([class~="lead"]):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(75, 85, 99);
|
||||||
|
font-size: 1.25em;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin-top: 1.2em;
|
||||||
|
margin-bottom: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where([class~="lead"]):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(209, 213, 219);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
.prose :where(a):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(99, 102, 241);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: rgba(99, 102, 241, 0.3);
|
||||||
|
text-underline-offset: 0.25em;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(a):not(:where([class~="not-prose"] *)):hover {
|
||||||
|
color: rgb(79, 70, 229);
|
||||||
|
text-decoration-color: rgba(99, 102, 241, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(a):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(129, 140, 248);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(a):not(:where([class~="not-prose"] *)):hover {
|
||||||
|
color: rgb(165, 180, 252);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Strong */
|
||||||
|
.prose :where(strong):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(strong):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(243, 244, 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lists */
|
||||||
|
.prose :where(ul):not(:where([class~="not-prose"] *)) {
|
||||||
|
list-style-type: disc;
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
padding-left: 1.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose[dir="rtl"] :where(ul):not(:where([class~="not-prose"] *)) {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 1.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(ol):not(:where([class~="not-prose"] *)) {
|
||||||
|
list-style-type: decimal;
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
padding-left: 1.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose[dir="rtl"] :where(ol):not(:where([class~="not-prose"] *)) {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 1.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(li):not(:where([class~="not-prose"] *)) {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(li > p):not(:where([class~="not-prose"] *)) {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nested lists */
|
||||||
|
.prose :where(ul > li > *:first-child):not(:where([class~="not-prose"] *)) {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(ul > li > *:last-child):not(:where([class~="not-prose"] *)) {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blockquotes */
|
||||||
|
.prose :where(blockquote):not(:where([class~="not-prose"] *)) {
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: italic;
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
border-left-width: 0.25rem;
|
||||||
|
border-left-color: rgb(99, 102, 241);
|
||||||
|
quotes: "\201C""\201D""\2018""\2019";
|
||||||
|
margin-top: 2.5em;
|
||||||
|
margin-bottom: 2.5em;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
padding-top: 1em;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
background-color: rgba(99, 102, 241, 0.05);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose[dir="rtl"] :where(blockquote):not(:where([class~="not-prose"] *)) {
|
||||||
|
border-left: none;
|
||||||
|
border-right-width: 0.25rem;
|
||||||
|
border-right-color: rgb(99, 102, 241);
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-right: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(blockquote):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(229, 231, 235);
|
||||||
|
border-left-color: rgb(129, 140, 248);
|
||||||
|
background-color: rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose[dir="rtl"] :where(blockquote):not(:where([class~="not-prose"] *)) {
|
||||||
|
border-right-color: rgb(129, 140, 248);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(blockquote p:first-of-type):not(:where([class~="not-prose"] *))::before {
|
||||||
|
content: open-quote;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(blockquote p:last-of-type):not(:where([class~="not-prose"] *))::after {
|
||||||
|
content: close-quote;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code */
|
||||||
|
.prose :where(code):not(:where([class~="not-prose"], pre *)) {
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875em;
|
||||||
|
background-color: rgba(99, 102, 241, 0.1);
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(code):not(:where([class~="not-prose"], pre *)) {
|
||||||
|
color: rgb(243, 244, 246);
|
||||||
|
background-color: rgba(99, 102, 241, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(code):not(:where([class~="not-prose"], pre *))::before,
|
||||||
|
.prose :where(code):not(:where([class~="not-prose"], pre *))::after {
|
||||||
|
content: "`";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pre/Code blocks */
|
||||||
|
.prose :where(pre):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(229, 231, 235);
|
||||||
|
background-color: rgb(31, 41, 55);
|
||||||
|
overflow-x: auto;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.875em;
|
||||||
|
line-height: 1.7142857;
|
||||||
|
margin-top: 2.5em;
|
||||||
|
margin-bottom: 2.5em;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(pre code):not(:where([class~="not-prose"] *)) {
|
||||||
|
background-color: transparent;
|
||||||
|
border-width: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: inherit;
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(pre code):not(:where([class~="not-prose"] *))::before,
|
||||||
|
.prose :where(pre code):not(:where([class~="not-prose"] *))::after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal rules */
|
||||||
|
.prose :where(hr):not(:where([class~="not-prose"] *)) {
|
||||||
|
border-color: rgb(229, 231, 235);
|
||||||
|
border-top-width: 1px;
|
||||||
|
margin-top: 3em;
|
||||||
|
margin-bottom: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(hr):not(:where([class~="not-prose"] *)) {
|
||||||
|
border-color: rgb(55, 65, 81);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Images */
|
||||||
|
.prose :where(img):not(:where([class~="not-prose"] *)) {
|
||||||
|
margin-top: 2.5em;
|
||||||
|
margin-bottom: 2.5em;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(figure):not(:where([class~="not-prose"] *)) {
|
||||||
|
margin-top: 2.5em;
|
||||||
|
margin-bottom: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(figure > *):not(:where([class~="not-prose"] *)) {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(figcaption):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(107, 114, 128);
|
||||||
|
font-size: 0.875em;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-top: 1em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(figcaption):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(156, 163, 175);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
.prose :where(table):not(:where([class~="not-prose"] *)) {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: auto;
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 2.5em;
|
||||||
|
margin-bottom: 2.5em;
|
||||||
|
font-size: 0.875em;
|
||||||
|
line-height: 1.7142857;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(thead):not(:where([class~="not-prose"] *)) {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-color: rgb(209, 213, 219);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(thead):not(:where([class~="not-prose"] *)) {
|
||||||
|
border-bottom-color: rgb(75, 85, 99);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(thead th):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(17, 24, 39);
|
||||||
|
font-weight: 600;
|
||||||
|
vertical-align: bottom;
|
||||||
|
padding-right: 0.75em;
|
||||||
|
padding-bottom: 0.75em;
|
||||||
|
padding-left: 0.75em;
|
||||||
|
background-color: rgba(99, 102, 241, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(thead th):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: rgb(243, 244, 246);
|
||||||
|
background-color: rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(tbody tr):not(:where([class~="not-prose"] *)) {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-color: rgb(229, 231, 235);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose :where(tbody tr):not(:where([class~="not-prose"] *)) {
|
||||||
|
border-bottom-color: rgb(55, 65, 81);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(tbody tr:last-child):not(:where([class~="not-prose"] *)) {
|
||||||
|
border-bottom-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(tbody td):not(:where([class~="not-prose"] *)) {
|
||||||
|
vertical-align: baseline;
|
||||||
|
padding: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.prose {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(h1):not(:where([class~="not-prose"] *)) {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(h2):not(:where([class~="not-prose"] *)) {
|
||||||
|
font-size: 1.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(h3):not(:where([class~="not-prose"] *)) {
|
||||||
|
font-size: 1.375em;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,7 +54,7 @@ const selectedIcon = computed<string>(() => items.value.find(i => i.value === mo
|
|||||||
const { startLocaleSwitching } = useLocaleSwitching()
|
const { startLocaleSwitching } = useLocaleSwitching()
|
||||||
const loading = useLoadingIndicator()
|
const loading = useLoadingIndicator()
|
||||||
|
|
||||||
// On selection change, update locale and navigate
|
// On selection change, navigate first then update locale
|
||||||
watch(model, async (val, oldVal) => {
|
watch(model, async (val, oldVal) => {
|
||||||
if (val === oldVal) return
|
if (val === oldVal) return
|
||||||
|
|
||||||
@@ -66,25 +66,39 @@ watch(model, async (val, oldVal) => {
|
|||||||
loading.start()
|
loading.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update locale
|
|
||||||
await setLocale(val)
|
|
||||||
|
|
||||||
// Get the current route path without locale prefix
|
// Get the current route path without locale prefix
|
||||||
const currentPath = router.currentRoute.value.path
|
const currentPath = router.currentRoute.value.path
|
||||||
const pathWithoutLocale = currentPath.replace(/^\/(en|fa)/, '')
|
const pathWithoutLocale = currentPath.replace(/^\/(en|fa)/, '')
|
||||||
|
|
||||||
// Build new path with new locale
|
// Check if we're on a blog post page
|
||||||
const newLocalePrefix = val === 'en' ? '' : `/${val}`
|
const isBlogPost = pathWithoutLocale.startsWith('/blog/') && pathWithoutLocale !== '/blog' && pathWithoutLocale !== '/blog/'
|
||||||
const newPath = `${newLocalePrefix}${pathWithoutLocale || '/'}`
|
|
||||||
|
|
||||||
// Navigate to new path
|
let newPath: string
|
||||||
|
|
||||||
|
if (isBlogPost) {
|
||||||
|
// If on a blog post, redirect to blog listing page in the new locale
|
||||||
|
newPath = val === 'en' ? '/blog' : `/${val}/blog`
|
||||||
|
} else {
|
||||||
|
// For other pages, try to navigate to the equivalent page
|
||||||
|
const newLocalePrefix = val === 'en' ? '' : `/${val}`
|
||||||
|
newPath = `${newLocalePrefix}${pathWithoutLocale || '/'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to new path FIRST (before setLocale to avoid RTL/LTR flash)
|
||||||
if (newPath !== currentPath) {
|
if (newPath !== currentPath) {
|
||||||
await router.push(newPath)
|
await router.push(newPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore scroll position after navigation
|
// Update locale AFTER navigation
|
||||||
|
await setLocale(val)
|
||||||
|
|
||||||
|
// Restore scroll position after navigation (only if not redirecting from blog post)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
window.scrollTo(0, scrollY)
|
if (!isBlogPost) {
|
||||||
|
window.scrollTo(0, scrollY)
|
||||||
|
} else {
|
||||||
|
window.scrollTo(0, 0) // Scroll to top when redirecting to blog listing
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
setTimeout(() => loading.finish(), 600)
|
setTimeout(() => loading.finish(), 600)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const props = defineProps<{
|
|||||||
toc: {
|
toc: {
|
||||||
links: TocLink[]
|
links: TocLink[]
|
||||||
}
|
}
|
||||||
|
mobile?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -25,33 +26,48 @@ const shouldShowToc = computed(() => {
|
|||||||
return countLinks(props.toc.links) >= 3
|
return countLinks(props.toc.links) >= 3
|
||||||
})
|
})
|
||||||
|
|
||||||
// Smooth scroll to heading
|
// Smooth scroll to heading with offset for sticky header
|
||||||
const scrollToHeading = (id: string) => {
|
const scrollToHeading = (id: string) => {
|
||||||
const element = document.getElementById(id)
|
const element = document.getElementById(id)
|
||||||
if (element) {
|
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
|
activeId.value = id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track active section with IntersectionObserver
|
// Track active section with IntersectionObserver
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
const headings = document.querySelectorAll('article h2, article h3, article h4')
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
entries.forEach((entry) => {
|
// Find the first intersecting heading
|
||||||
if (entry.isIntersecting) {
|
const intersecting = entries.filter(entry => entry.isIntersecting)
|
||||||
activeId.value = entry.target.id
|
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',
|
rootMargin: '-100px 0px -66% 0px',
|
||||||
threshold: 0
|
threshold: [0, 0.25, 0.5, 0.75, 1]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Observe all headings
|
// Observe all headings
|
||||||
const headings = document.querySelectorAll('article h2, article h3, article h4')
|
|
||||||
headings.forEach((heading) => observer.observe(heading))
|
headings.forEach((heading) => observer.observe(heading))
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
@@ -59,51 +75,50 @@ onMounted(() => {
|
|||||||
headings.forEach((heading) => observer.unobserve(heading))
|
headings.forEach((heading) => observer.unobserve(heading))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Render TOC links recursively
|
|
||||||
const renderTocLinks = (links: TocLink[]) => {
|
|
||||||
return links
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside v-if="shouldShowToc" class="toc-container">
|
<div v-if="shouldShowToc">
|
||||||
<!-- Desktop: Sticky sidebar -->
|
<!-- Desktop TOC -->
|
||||||
<nav class="hidden lg:block sticky top-24 max-h-[calc(100vh-8rem)] overflow-y-auto">
|
<div v-if="!mobile"
|
||||||
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
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') }}
|
{{ t('blog.tableOfContents') }}
|
||||||
</h3>
|
</h3>
|
||||||
<ul class="space-y-2 text-sm">
|
<nav>
|
||||||
<template v-for="link in toc.links" :key="link.id">
|
<ul class="space-y-1 text-sm">
|
||||||
<li>
|
<template v-for="link in toc.links" :key="link.id">
|
||||||
<a :href="`#${link.id}`" :class="[
|
<li>
|
||||||
'block py-1 transition-colors',
|
<a :href="`#${link.id}`" :class="[
|
||||||
activeId === link.id
|
'block py-1.5 px-2 rounded transition-all',
|
||||||
? 'text-primary-600 dark:text-primary-400 font-medium'
|
activeId === link.id
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
? 'text-primary-600 dark:text-primary-400 font-medium bg-primary-50 dark:bg-primary-900/20'
|
||||||
]" @click.prevent="scrollToHeading(link.id)">
|
: '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'
|
||||||
{{ link.text }}
|
]" @click.prevent="scrollToHeading(link.id)">
|
||||||
</a>
|
{{ link.text }}
|
||||||
<!-- Nested children (h3) -->
|
</a>
|
||||||
<ul v-if="link.children && link.children.length > 0" class="ml-4 mt-1 space-y-1">
|
<!-- Nested children (h3) -->
|
||||||
<li v-for="child in link.children" :key="child.id">
|
<ul v-if="link.children && link.children.length > 0"
|
||||||
<a :href="`#${child.id}`" :class="[
|
class="ml-3 mt-0.5 space-y-0.5 border-l-2 border-gray-200 dark:border-gray-700 pl-3">
|
||||||
'block py-1 transition-colors',
|
<li v-for="child in link.children" :key="child.id">
|
||||||
activeId === child.id
|
<a :href="`#${child.id}`" :class="[
|
||||||
? 'text-primary-600 dark:text-primary-400 font-medium'
|
'block py-1 px-2 rounded text-xs transition-all',
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
activeId === child.id
|
||||||
]" @click.prevent="scrollToHeading(child.id)">
|
? 'text-primary-600 dark:text-primary-400 font-medium bg-primary-50 dark:bg-primary-900/20'
|
||||||
{{ child.text }}
|
: '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'
|
||||||
</a>
|
]" @click.prevent="scrollToHeading(child.id)">
|
||||||
</li>
|
{{ child.text }}
|
||||||
</ul>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</ul>
|
||||||
</ul>
|
</li>
|
||||||
</nav>
|
</template>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mobile: Collapsible accordion -->
|
<!-- Mobile: Collapsible accordion -->
|
||||||
<UAccordion class="lg:hidden mb-6" :items="[
|
<UAccordion v-if="mobile" :items="[
|
||||||
{
|
{
|
||||||
label: t('blog.tableOfContents'),
|
label: t('blog.tableOfContents'),
|
||||||
icon: 'i-heroicons-list-bullet',
|
icon: 'i-heroicons-list-bullet',
|
||||||
@@ -141,11 +156,5 @@ const renderTocLinks = (links: TocLink[]) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
</UAccordion>
|
</UAccordion>
|
||||||
</aside>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.toc-container {
|
|
||||||
@apply w-full;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="tags.length > 0" class="overflow-x-auto">
|
<div v-if="tags.length > 0" class="overflow-x-auto scrollbar-hide -mx-1 px-1">
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-1.5 py-1">
|
||||||
<UButton :color="!modelValue ? 'primary' : 'gray'" :variant="!modelValue ? 'solid' : 'soft'" size="sm"
|
<!-- All Posts Badge -->
|
||||||
@click="selectTag(null)">
|
<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') }}
|
{{ t('blog.allPosts') }}
|
||||||
</UButton>
|
</button>
|
||||||
|
|
||||||
<UButton v-for="tag in tags" :key="tag" :color="modelValue === tag ? 'primary' : 'gray'"
|
<!-- Tag Badges -->
|
||||||
:variant="modelValue === tag ? 'solid' : 'soft'" size="sm" @click="selectTag(tag)">
|
<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 }}
|
{{ tag }}
|
||||||
</UButton>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -26,7 +38,6 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
// Read query parameter on mount to restore filter state
|
// Read query parameter on mount to restore filter state
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="alertClass" class="my-4 flex items-center gap-2 rounded-lg px-4 py-3">
|
<div :class="alertClass"
|
||||||
|
class="my-6 flex items-center gap-3 rounded-xl px-4 py-3.5 ring-1 ring-inset backdrop-blur-sm">
|
||||||
<UIcon :name="icon" class="h-5 w-5 flex-shrink-0" />
|
<UIcon :name="icon" class="h-5 w-5 flex-shrink-0" />
|
||||||
<div class="flex-1 text-sm">
|
<div class="flex-1 text-sm leading-relaxed">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -69,31 +69,31 @@ const languageLabel = computed(() => {
|
|||||||
.code-block-header {
|
.code-block-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
padding: 1rem 1.5rem;
|
padding: 0.875rem 1.5rem;
|
||||||
background: rgba(30, 41, 59, 0.8);
|
background: rgba(30, 41, 59, 0.8);
|
||||||
border-bottom: 1px solid rgba(71, 85, 105, 0.3);
|
border-bottom: 1px solid rgba(71, 85, 105, 0.3);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
min-height: 3rem;
|
min-height: 2.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-filename {
|
.code-filename {
|
||||||
font-size: 0.9375rem;
|
font-size: 0.875rem;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-language {
|
.code-language {
|
||||||
font-size: 0.8125rem;
|
font-size: 0.75rem;
|
||||||
color: #94a3b8;
|
color: #cbd5e1;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.25rem 0.625rem;
|
||||||
background: rgba(99, 102, 241, 0.15);
|
background: rgba(99, 102, 241, 0.2);
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
border: 1px solid rgba(99, 102, 241, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-block-content {
|
.code-block-content {
|
||||||
@@ -177,14 +177,12 @@ const languageLabel = computed(() => {
|
|||||||
<div class="code-block-wrapper" @mouseenter="isHovered = true" @mouseleave="isHovered = false">
|
<div class="code-block-wrapper" @mouseenter="isHovered = true" @mouseleave="isHovered = false">
|
||||||
<!-- Header with filename and language -->
|
<!-- Header with filename and language -->
|
||||||
<div v-if="filename || language" class="code-block-header">
|
<div v-if="filename || language" class="code-block-header">
|
||||||
<div class="flex items-center gap-3">
|
<span v-if="filename" class="code-filename">
|
||||||
<span v-if="filename" class="code-filename">
|
{{ filename }}
|
||||||
{{ filename }}
|
</span>
|
||||||
</span>
|
<span v-if="language && !filename" class="code-language">
|
||||||
<span v-if="language && !filename" class="code-language">
|
{{ languageLabel }}
|
||||||
{{ languageLabel }}
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Code block -->
|
<!-- Code block -->
|
||||||
|
|||||||
195
app/pages/blog/[...slug].example.vue
Normal file
195
app/pages/blog/[...slug].example.vue
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const route = useRoute()
|
||||||
|
const { data: page, error } = await useAsyncData(route.path, () => queryContent(route.path).findOne())
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: 'Page not found',
|
||||||
|
fatal: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: page.value?.title,
|
||||||
|
description: page.value?.description,
|
||||||
|
ogTitle: page.value?.title,
|
||||||
|
ogDescription: page.value?.description,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: relatedArticles } = await useAsyncData(`content:related-articles:${page.value?.title}`, () => queryContent('/articles').where({ categories: { $in: page.value?.categories }, _extension: 'md' }).only(['_path', 'title', 'categories', 'description', 'publishedAt', 'image', 'authors']).sort({ publishedAt: -1 }).limit(3).find())
|
||||||
|
|
||||||
|
const appConfig = useAppConfig()
|
||||||
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UContainer
|
||||||
|
v-if="page"
|
||||||
|
>
|
||||||
|
<UPage>
|
||||||
|
<UPageHeader
|
||||||
|
class="max-w-5xl mx-auto"
|
||||||
|
:title="page.title"
|
||||||
|
:description="page.description"
|
||||||
|
>
|
||||||
|
<template #headline>
|
||||||
|
<!-- Waiting for https://github.com/nuxt/ui-pro/issues/114 -->
|
||||||
|
<!-- <dl>
|
||||||
|
<dt class="sr-only">
|
||||||
|
Categories
|
||||||
|
</dt>
|
||||||
|
<dd> -->
|
||||||
|
<template
|
||||||
|
v-for="(category, index) in page.categories"
|
||||||
|
:key="category"
|
||||||
|
>
|
||||||
|
<ULink
|
||||||
|
:to="`/categories/${category}`"
|
||||||
|
>
|
||||||
|
{{ formatCategory(category) }}
|
||||||
|
</ULink>
|
||||||
|
|
||||||
|
<span v-if="index < page.categories.length - 1">
|
||||||
|
-
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<!-- </dd>
|
||||||
|
</dl> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<NuxtImg
|
||||||
|
v-if="page.image"
|
||||||
|
:src="page.image.src"
|
||||||
|
:alt="page.image.alt"
|
||||||
|
class="mt-8 w-full object-cover rounded-lg aspect-[16/9]"
|
||||||
|
>
|
||||||
|
<dl class="mt-8 flex justify-between text-stone-700 text-sm">
|
||||||
|
<dt class="sr-only">
|
||||||
|
Author
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
<ol class="space-x-4">
|
||||||
|
<li
|
||||||
|
v-for="author in page.authors"
|
||||||
|
:key="author.name"
|
||||||
|
>
|
||||||
|
<ULink
|
||||||
|
:to="author.social"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<UAvatar
|
||||||
|
:src="author.avatar"
|
||||||
|
:alt="author.name"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ author.name }}
|
||||||
|
</span>
|
||||||
|
</ULink>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</dd>
|
||||||
|
<dt class="sr-only">
|
||||||
|
Published at
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
<time :datetime="page.publishedAt">
|
||||||
|
{{ formatDate(page.publishedAt) }}
|
||||||
|
</time>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</nuxtimg>
|
||||||
|
</UPageHeader>
|
||||||
|
|
||||||
|
<div class="mt-8 max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-[96px_768px_1fr]">
|
||||||
|
<div class="lg:px-8 flex lg:flex-col lg:items-end gap-2">
|
||||||
|
<UTooltip text="Share on X">
|
||||||
|
<UButton
|
||||||
|
:to="`https://twitter.com/share?text=${page.title}&url=${runtimeConfig.app.name}${page._path}`"
|
||||||
|
target="_blank"
|
||||||
|
icon="i-simple-icons-x"
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
square
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip text="Share on Facebook">
|
||||||
|
<UButton
|
||||||
|
:to="`https://www.facebook.com/sharer/sharer.php?u=${runtimeConfig.app.name}${page._path}&t=${page.title}`"
|
||||||
|
target="_blank"
|
||||||
|
icon="i-simple-icons-facebook"
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
square
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip text="Share on LinkedIn">
|
||||||
|
<UButton
|
||||||
|
:to="`https://www.linkedin.com/shareArticle?url=${runtimeConfig.app.name}${page._path}&title=${page.title}`"
|
||||||
|
target="_blank"
|
||||||
|
icon="i-simple-icons-linkedin"
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
square
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 lg:mt-0 w-full">
|
||||||
|
<UPageBody
|
||||||
|
prose
|
||||||
|
:ui="{ wrapper: 'mt-0' }"
|
||||||
|
>
|
||||||
|
<ContentRenderer :value="page" />
|
||||||
|
</UPageBody>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row-start-2 lg:row-start-1 lg:col-start-3 lg:px-8 space-y-8">
|
||||||
|
<UButton
|
||||||
|
v-bind="appConfig.page.article.cta"
|
||||||
|
color="primary"
|
||||||
|
size="lg"
|
||||||
|
:ui="{ base: 'w-full justify-center' }"
|
||||||
|
class="hidden lg:inline-flex"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UDivider />
|
||||||
|
|
||||||
|
<UContentToc
|
||||||
|
:links="page.body?.toc?.links"
|
||||||
|
:ui="{ wrapper: 'top-4', container: { base: 'py-0 pb-3 lg:py-0 lg:pb-8' } }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UDivider class="lg:hidden" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section v-if="relatedArticles">
|
||||||
|
<h2 class="text-2xl text-stone-900 font-bold">
|
||||||
|
Related Articles
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="mt-3 border-b border-stone-200" />
|
||||||
|
|
||||||
|
<UPageGrid class="mt-8">
|
||||||
|
<ArticleCard
|
||||||
|
v-for="article in relatedArticles"
|
||||||
|
:key="article._path"
|
||||||
|
:to="article._path!"
|
||||||
|
:title="article.title!"
|
||||||
|
:description="article.description"
|
||||||
|
:date="article.publishedAt"
|
||||||
|
:image="article.image"
|
||||||
|
:authors="article.authors"
|
||||||
|
/>
|
||||||
|
</UPageGrid>
|
||||||
|
</section>
|
||||||
|
</UPage>
|
||||||
|
</UContainer>
|
||||||
|
</template>
|
||||||
@@ -120,15 +120,21 @@ if (post.value) {
|
|||||||
{{ t('blog.backToBlog') }}
|
{{ t('blog.backToBlog') }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- Mobile TOC -->
|
||||||
|
<div v-if="(post as any).body?.toc?.links?.length" class="lg:hidden mb-8">
|
||||||
|
<BlogTableOfContents :toc="(post as any).body.toc" :mobile="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Main Content Layout -->
|
<!-- Main Content Layout -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-8 lg:gap-12">
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="lg:col-span-8">
|
<div class="min-w-0 overflow-x-hidden">
|
||||||
<!-- Blog Post Metadata -->
|
<!-- Blog Post Metadata -->
|
||||||
<BlogPost :post="post" />
|
<BlogPost :post="post" />
|
||||||
|
|
||||||
<!-- Content Renderer -->
|
<!-- Content Renderer -->
|
||||||
<article :dir="locale === 'fa' ? 'rtl' : 'ltr'" class="prose prose-lg dark:prose-invert max-w-none mt-8"
|
<article :dir="locale === 'fa' ? 'rtl' : 'ltr'"
|
||||||
|
:class="['blog-content', locale === 'fa' ? 'blog-content-rtl' : 'blog-content-ltr']"
|
||||||
suppressHydrationWarning>
|
suppressHydrationWarning>
|
||||||
<ContentRenderer v-if="(post as any).body" :value="(post as any).body" />
|
<ContentRenderer v-if="(post as any).body" :value="(post as any).body" />
|
||||||
</article>
|
</article>
|
||||||
@@ -137,65 +143,15 @@ if (post.value) {
|
|||||||
<BlogNavigation :prev="prevPost" :next="nextPost" />
|
<BlogNavigation :prev="prevPost" :next="nextPost" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sidebar: Table of Contents -->
|
<!-- Sidebar: Table of Contents (Desktop) -->
|
||||||
<aside class="lg:col-span-4">
|
<aside v-if="(post as any).body?.toc?.links?.length" class="hidden lg:block">
|
||||||
<BlogTableOfContents v-if="(post as any).body?.toc" :toc="(post as any).body.toc" />
|
<UContentToc :links="(post as any).body.toc.links" :title="t('blog.tableOfContents')" color="primary"
|
||||||
|
highlight :ui="{
|
||||||
|
root: 'sticky top-24',
|
||||||
|
container: 'bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 p-4'
|
||||||
|
}" />
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UContainer>
|
</UContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Force LTR for code blocks in RTL context */
|
|
||||||
article[dir="rtl"] :deep(pre),
|
|
||||||
article[dir="rtl"] :deep(code) {
|
|
||||||
direction: ltr;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Better spacing for prose elements */
|
|
||||||
article :deep(h1) {
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
article :deep(h2) {
|
|
||||||
margin-top: 2.25rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
article :deep(h3) {
|
|
||||||
margin-top: 2rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
article :deep(p) {
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
article :deep(ul),
|
|
||||||
article :deep(ol) {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
padding-left: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
article :deep(li) {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
article :deep(pre) {
|
|
||||||
margin-top: 2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
article :deep(blockquote) {
|
|
||||||
margin-top: 2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
border-left: 4px solid rgba(99, 102, 241, 0.5);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ draft: false
|
|||||||
|
|
||||||
Nuxt Content یک سیستم مدیریت محتوای فایلمحور قدرتمند است که به شما امکان میدهد محتوای خود را به صورت Markdown، YAML، CSV یا JSON بنویسید و با یک API شبیه MongoDB آن را جستجو کنید.
|
Nuxt Content یک سیستم مدیریت محتوای فایلمحور قدرتمند است که به شما امکان میدهد محتوای خود را به صورت Markdown، YAML، CSV یا JSON بنویسید و با یک API شبیه MongoDB آن را جستجو کنید.
|
||||||
|
|
||||||
|
::blog-callout{type="info"}
|
||||||
|
این مقاله یک راهنمای جامع برای شروع کار با Nuxt Content است. اگر تازه شروع کردهاید، این مقاله برای شما مناسب است!
|
||||||
|
::
|
||||||
|
|
||||||
## چرا Nuxt Content؟
|
## چرا Nuxt Content؟
|
||||||
|
|
||||||
Nuxt Content مزایای متعددی برای برنامههای محتوا-محور ارائه میدهد:
|
Nuxt Content مزایای متعددی برای برنامههای محتوا-محور ارائه میدهد:
|
||||||
|
|||||||
Reference in New Issue
Block a user