mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
feat: raw markdown URLs, author display, GitHub Stars, and frontmatter docs
v1.18.1 - CopyPageDropdown raw markdown URLs
- AI services (ChatGPT, Claude, Perplexity) now receive /raw/{slug}.md URLs
- Direct access to clean markdown content for better AI parsing
- No HTML parsing required by AI services
- Renamed buildUrlFromPageUrl to buildUrlFromRawMarkdown
v1.19.0 - Author display for posts and pages
- New optional authorName and authorImage frontmatter fields
- Round avatar image displayed next to date and read time
- Works on individual post and page views
- Write page updated with new field reference
v1.19.1 - GitHub Stars on Stats page
- Live star count from waynesutton/markdown-site repository
- Fetches from GitHub public API (no token required)
- Stats page now displays 6 cards with responsive grid
Documentation updates
- Frontmatter Flow section added to docs.md, setup-guide.md, files.md
- How frontmatter works with step-by-step processing flow
- Instructions for adding new frontmatter fields
Updated files:
- src/components/CopyPageDropdown.tsx
- src/pages/Stats.tsx
- src/pages/Post.tsx
- src/pages/Write.tsx
- src/styles/global.css
- convex/schema.ts
- convex/posts.ts
- convex/pages.ts
- scripts/sync-posts.ts
- content/blog/setup-guide.md
- content/pages/docs.md
- content/pages/changelog-page.md
- files.md
- README.md
- TASK.md
- changelog.md
- AGENTS.md
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { Copy, MessageSquare, Sparkles, Search, Check, AlertCircle, FileText, Download } from "lucide-react";
|
||||
import {
|
||||
Copy,
|
||||
MessageSquare,
|
||||
Sparkles,
|
||||
Search,
|
||||
Check,
|
||||
AlertCircle,
|
||||
FileText,
|
||||
Download,
|
||||
} from "lucide-react";
|
||||
|
||||
// Maximum URL length for query parameters (conservative limit)
|
||||
const MAX_URL_LENGTH = 6000;
|
||||
@@ -14,9 +23,11 @@ interface AIService {
|
||||
supportsUrlPrefill: boolean;
|
||||
// Custom URL builder for services with special formats
|
||||
buildUrl?: (prompt: string) => string;
|
||||
// URL-based builder - takes raw markdown file URL for better AI parsing
|
||||
buildUrlFromRawMarkdown?: (rawMarkdownUrl: string) => string;
|
||||
}
|
||||
|
||||
// All services send the full markdown content directly
|
||||
// AI services configuration - uses raw markdown URLs for better AI parsing
|
||||
const AI_SERVICES: AIService[] = [
|
||||
{
|
||||
id: "chatgpt",
|
||||
@@ -25,17 +36,27 @@ const AI_SERVICES: AIService[] = [
|
||||
baseUrl: "https://chatgpt.com/",
|
||||
description: "Analyze with ChatGPT",
|
||||
supportsUrlPrefill: true,
|
||||
// ChatGPT accepts ?q= with full text content
|
||||
buildUrl: (prompt) => `https://chatgpt.com/?q=${encodeURIComponent(prompt)}`,
|
||||
// Uses raw markdown file URL for direct content access
|
||||
buildUrlFromRawMarkdown: (rawMarkdownUrl) => {
|
||||
const prompt =
|
||||
`Summarize the page and then ask what the user needs help with. Be concise and to the point.\n\n` +
|
||||
`Here is the raw markdown file URL:\n${rawMarkdownUrl}`;
|
||||
return `https://chatgpt.com/?q=${encodeURIComponent(prompt)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "claude",
|
||||
name: "Claude",
|
||||
icon: Sparkles,
|
||||
baseUrl: "https://claude.ai/new",
|
||||
baseUrl: "https://claude.ai/",
|
||||
description: "Analyze with Claude",
|
||||
supportsUrlPrefill: true,
|
||||
buildUrl: (prompt) => `https://claude.ai/new?q=${encodeURIComponent(prompt)}`,
|
||||
buildUrlFromRawMarkdown: (rawMarkdownUrl) => {
|
||||
const prompt =
|
||||
`Summarize the page and then ask what the user needs help with. Be concise and to the point.\n\n` +
|
||||
`Here is the raw markdown file URL:\n${rawMarkdownUrl}`;
|
||||
return `https://claude.ai/new?q=${encodeURIComponent(prompt)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "perplexity",
|
||||
@@ -44,7 +65,12 @@ const AI_SERVICES: AIService[] = [
|
||||
baseUrl: "https://www.perplexity.ai/search",
|
||||
description: "Research with Perplexity",
|
||||
supportsUrlPrefill: true,
|
||||
buildUrl: (prompt) => `https://www.perplexity.ai/search?q=${encodeURIComponent(prompt)}`,
|
||||
buildUrlFromRawMarkdown: (rawMarkdownUrl) => {
|
||||
const prompt =
|
||||
`Summarize the page and then ask what the user needs help with. Be concise and to the point.\n\n` +
|
||||
`Here is the raw markdown file URL:\n${rawMarkdownUrl}`;
|
||||
return `https://www.perplexity.ai/search?q=${encodeURIComponent(prompt)}`;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -63,28 +89,28 @@ interface CopyPageDropdownProps {
|
||||
// Enhanced markdown format for better LLM parsing
|
||||
function formatAsMarkdown(props: CopyPageDropdownProps): string {
|
||||
const { title, content, url, description, date, tags, readTime } = props;
|
||||
|
||||
|
||||
// Build metadata section
|
||||
const metadataLines: string[] = [];
|
||||
metadataLines.push(`Source: ${url}`);
|
||||
if (date) metadataLines.push(`Date: ${date}`);
|
||||
if (readTime) metadataLines.push(`Reading time: ${readTime}`);
|
||||
if (tags && tags.length > 0) metadataLines.push(`Tags: ${tags.join(", ")}`);
|
||||
|
||||
|
||||
// Build the full markdown document
|
||||
let markdown = `# ${title}\n\n`;
|
||||
|
||||
|
||||
// Add description if available
|
||||
if (description) {
|
||||
markdown += `> ${description}\n\n`;
|
||||
}
|
||||
|
||||
|
||||
// Add metadata block
|
||||
markdown += `---\n${metadataLines.join("\n")}\n---\n\n`;
|
||||
|
||||
|
||||
// Add main content
|
||||
markdown += content;
|
||||
|
||||
|
||||
return markdown;
|
||||
}
|
||||
|
||||
@@ -102,45 +128,45 @@ function generateSkillName(slug: string): string {
|
||||
// Follows: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview
|
||||
function formatAsSkill(props: CopyPageDropdownProps): string {
|
||||
const { title, content, slug, description, tags } = props;
|
||||
|
||||
|
||||
// Generate compliant skill name
|
||||
const skillName = generateSkillName(slug);
|
||||
|
||||
|
||||
// Build description with "when to use" triggers (max 1024 chars)
|
||||
const tagList = tags && tags.length > 0 ? tags.join(", ") : "";
|
||||
let skillDescription = description || `Guide about ${title.toLowerCase()}.`;
|
||||
|
||||
|
||||
// Add usage triggers to description
|
||||
if (tagList) {
|
||||
skillDescription += ` Use when working with ${tagList.toLowerCase()} or when asked about ${title.toLowerCase()}.`;
|
||||
} else {
|
||||
skillDescription += ` Use when asked about ${title.toLowerCase()}.`;
|
||||
}
|
||||
|
||||
|
||||
// Truncate description if needed (max 1024 chars)
|
||||
if (skillDescription.length > 1024) {
|
||||
skillDescription = skillDescription.slice(0, 1021) + "...";
|
||||
}
|
||||
|
||||
|
||||
// Build YAML frontmatter (required by Agent Skills spec)
|
||||
let skill = `---\n`;
|
||||
skill += `name: ${skillName}\n`;
|
||||
skill += `description: ${skillDescription}\n`;
|
||||
skill += `---\n\n`;
|
||||
|
||||
|
||||
// Add title
|
||||
skill += `# ${title}\n\n`;
|
||||
|
||||
|
||||
// Add instructions section
|
||||
skill += `## Instructions\n\n`;
|
||||
skill += content;
|
||||
|
||||
|
||||
// Add examples section placeholder if content doesn't include examples
|
||||
if (!content.toLowerCase().includes("## example")) {
|
||||
skill += `\n\n## Examples\n\n`;
|
||||
skill += `Use this skill when the user asks about topics covered in this guide.\n`;
|
||||
}
|
||||
|
||||
|
||||
return skill;
|
||||
}
|
||||
|
||||
@@ -154,7 +180,7 @@ type FeedbackState = "idle" | "copied" | "error" | "url-too-long";
|
||||
|
||||
export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
const { title } = props;
|
||||
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [feedback, setFeedback] = useState<FeedbackState>("idle");
|
||||
const [feedbackMessage, setFeedbackMessage] = useState("");
|
||||
@@ -195,7 +221,7 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
|
||||
const items = menu.querySelectorAll<HTMLButtonElement>(".copy-page-item");
|
||||
const currentIndex = Array.from(items).findIndex(
|
||||
(item) => item === document.activeElement
|
||||
(item) => item === document.activeElement,
|
||||
);
|
||||
|
||||
switch (event.key) {
|
||||
@@ -276,7 +302,7 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
const handleCopyPage = async () => {
|
||||
const markdown = formatAsMarkdown(props);
|
||||
const success = await writeToClipboard(markdown);
|
||||
|
||||
|
||||
if (success) {
|
||||
setFeedback("copied");
|
||||
setFeedbackMessage("Copied!");
|
||||
@@ -284,18 +310,30 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
setFeedback("error");
|
||||
setFeedbackMessage("Failed to copy");
|
||||
}
|
||||
|
||||
|
||||
clearFeedback();
|
||||
setTimeout(() => setIsOpen(false), 1500);
|
||||
};
|
||||
|
||||
// Generic handler for opening AI services
|
||||
// All services receive the full markdown content directly
|
||||
// Uses raw markdown URL for better AI parsing
|
||||
// IMPORTANT: window.open must happen BEFORE any await to avoid popup blockers
|
||||
const handleOpenInAI = async (service: AIService) => {
|
||||
// Use raw markdown URL for better AI parsing
|
||||
if (service.buildUrlFromRawMarkdown) {
|
||||
// Build raw markdown URL from page URL and slug
|
||||
const origin = new URL(props.url).origin;
|
||||
const rawMarkdownUrl = `${origin}/raw/${props.slug}.md`;
|
||||
const targetUrl = service.buildUrlFromRawMarkdown(rawMarkdownUrl);
|
||||
window.open(targetUrl, "_blank");
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Other services: send full markdown content
|
||||
const markdown = formatAsMarkdown(props);
|
||||
const prompt = `Please analyze this article:\n\n${markdown}`;
|
||||
|
||||
|
||||
// Build the target URL using the service's buildUrl function
|
||||
if (!service.buildUrl) {
|
||||
// Fallback: open base URL FIRST (sync), then copy to clipboard
|
||||
@@ -311,9 +349,9 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
clearFeedback();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const targetUrl = service.buildUrl(prompt);
|
||||
|
||||
|
||||
// Check URL length - if too long, open base URL then copy to clipboard
|
||||
if (isUrlTooLong(targetUrl)) {
|
||||
// Open window FIRST (must be sync to avoid popup blocker)
|
||||
@@ -337,9 +375,11 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
// Handle download skill file (Anthropic Agent Skills format)
|
||||
const handleDownloadSkill = () => {
|
||||
const skillContent = formatAsSkill(props);
|
||||
const blob = new Blob([skillContent], { type: "text/markdown;charset=utf-8" });
|
||||
const blob = new Blob([skillContent], {
|
||||
type: "text/markdown;charset=utf-8",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
|
||||
// Create temporary link and trigger download as SKILL.md
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
@@ -347,10 +387,10 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
|
||||
// Clean up object URL
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
|
||||
setFeedback("copied");
|
||||
setFeedbackMessage("Downloaded!");
|
||||
clearFeedback();
|
||||
@@ -363,7 +403,9 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
case "copied":
|
||||
return <Check size={16} className="copy-page-icon feedback-success" />;
|
||||
case "error":
|
||||
return <AlertCircle size={16} className="copy-page-icon feedback-error" />;
|
||||
return (
|
||||
<AlertCircle size={16} className="copy-page-icon feedback-error" />
|
||||
);
|
||||
case "url-too-long":
|
||||
return <Check size={16} className="copy-page-icon feedback-warning" />;
|
||||
default:
|
||||
@@ -447,7 +489,9 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
<div className="copy-page-item-content">
|
||||
<span className="copy-page-item-title">
|
||||
Open in {service.name}
|
||||
<span className="external-arrow" aria-hidden="true">↗</span>
|
||||
<span className="external-arrow" aria-hidden="true">
|
||||
↗
|
||||
</span>
|
||||
</span>
|
||||
<span className="copy-page-item-desc">
|
||||
{service.description}
|
||||
@@ -471,11 +515,11 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
<div className="copy-page-item-content">
|
||||
<span className="copy-page-item-title">
|
||||
View as Markdown
|
||||
<span className="external-arrow" aria-hidden="true">↗</span>
|
||||
</span>
|
||||
<span className="copy-page-item-desc">
|
||||
Open raw .md file
|
||||
<span className="external-arrow" aria-hidden="true">
|
||||
↗
|
||||
</span>
|
||||
</span>
|
||||
<span className="copy-page-item-desc">Open raw .md file</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -488,9 +532,7 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
>
|
||||
<Download size={16} className="copy-page-icon" aria-hidden="true" />
|
||||
<div className="copy-page-item-content">
|
||||
<span className="copy-page-item-title">
|
||||
Download as SKILL.md
|
||||
</span>
|
||||
<span className="copy-page-item-title">Download as SKILL.md</span>
|
||||
<span className="copy-page-item-desc">
|
||||
Anthropic Agent Skills format
|
||||
</span>
|
||||
|
||||
@@ -72,7 +72,7 @@ export const siteConfig: SiteConfig = {
|
||||
// Optional logo/header image (place in public/images/, set to null to hide)
|
||||
logo: "/images/logo.svg",
|
||||
intro: null, // Set in Home.tsx to allow JSX with links
|
||||
bio: `Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents.`,
|
||||
bio: `Your content is instantly available to browsers, LLMs, and AI agents.`,
|
||||
|
||||
// Featured section configuration
|
||||
// viewMode: 'list' shows bullet list, 'cards' shows card grid with excerpts
|
||||
|
||||
@@ -97,16 +97,8 @@ export default function Home() {
|
||||
<strong>
|
||||
An open-source publishing framework for AI agents and developers.
|
||||
</strong>{" "}
|
||||
Write markdown, sync from the terminal.{" "}
|
||||
<a
|
||||
href="https://github.com/waynesutton/markdown-site"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="home-text-link"
|
||||
>
|
||||
Fork it
|
||||
</a>
|
||||
, customize it, ship it.
|
||||
<br />
|
||||
Write markdown, sync from the terminal.
|
||||
</p>
|
||||
|
||||
<p className="home-bio">{siteConfig.bio}</p>
|
||||
|
||||
@@ -163,6 +163,23 @@ export default function Post() {
|
||||
<article className="post-article">
|
||||
<header className="post-header">
|
||||
<h1 className="post-title">{page.title}</h1>
|
||||
{/* Author avatar and name for pages (optional) */}
|
||||
{(page.authorImage || page.authorName) && (
|
||||
<div className="post-meta-header">
|
||||
<div className="post-author">
|
||||
{page.authorImage && (
|
||||
<img
|
||||
src={page.authorImage}
|
||||
alt={page.authorName || "Author"}
|
||||
className="post-author-image"
|
||||
/>
|
||||
)}
|
||||
{page.authorName && (
|
||||
<span className="post-author-name">{page.authorName}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<BlogPost content={page.content} />
|
||||
@@ -228,6 +245,22 @@ export default function Post() {
|
||||
<header className="post-header">
|
||||
<h1 className="post-title">{post.title}</h1>
|
||||
<div className="post-meta-header">
|
||||
{/* Author avatar and name (optional) */}
|
||||
{(post.authorImage || post.authorName) && (
|
||||
<div className="post-author">
|
||||
{post.authorImage && (
|
||||
<img
|
||||
src={post.authorImage}
|
||||
alt={post.authorName || "Author"}
|
||||
className="post-author-image"
|
||||
/>
|
||||
)}
|
||||
{post.authorName && (
|
||||
<span className="post-author-name">{post.authorName}</span>
|
||||
)}
|
||||
<span className="post-meta-separator">·</span>
|
||||
</div>
|
||||
)}
|
||||
<time className="post-date">
|
||||
{format(parseISO(post.date), "MMMM yyyy")}
|
||||
</time>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery } from "convex/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
@@ -8,9 +9,8 @@ import {
|
||||
FileText,
|
||||
BookOpen,
|
||||
Activity,
|
||||
TrendingUp,
|
||||
Code,
|
||||
} from "lucide-react";
|
||||
import { GithubLogo } from "@phosphor-icons/react";
|
||||
|
||||
// Site launched Dec 14, 2025 at 1:00 PM (v1.0.0), stats added same day (v1.2.0)
|
||||
const SITE_LAUNCH_DATE = "Dec 14, 2025 at 1:00 PM";
|
||||
@@ -35,6 +35,17 @@ export default function Stats() {
|
||||
const navigate = useNavigate();
|
||||
const stats = useQuery(api.stats.getStats);
|
||||
|
||||
// GitHub stars state
|
||||
const [githubStars, setGithubStars] = useState<number | null>(null);
|
||||
|
||||
// Fetch GitHub stars on mount
|
||||
useEffect(() => {
|
||||
fetch("https://api.github.com/repos/waynesutton/markdown-site")
|
||||
.then((res) => res.json())
|
||||
.then((data) => setGithubStars(data.stargazers_count))
|
||||
.catch(() => setGithubStars(null));
|
||||
}, []);
|
||||
|
||||
// Don't render until stats load
|
||||
if (stats === undefined) {
|
||||
return null;
|
||||
@@ -78,6 +89,13 @@ export default function Stats() {
|
||||
value: stats.publishedPages,
|
||||
description: "Static pages",
|
||||
},
|
||||
{
|
||||
number: "06",
|
||||
icon: GithubLogo,
|
||||
title: "GitHub Stars",
|
||||
value: githubStars ?? "...",
|
||||
description: "waynesutton/markdown-site",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -35,6 +35,8 @@ const POST_FIELDS = [
|
||||
},
|
||||
{ name: "featured", required: false, example: "true" },
|
||||
{ name: "featuredOrder", required: false, example: "1" },
|
||||
{ name: "authorName", required: false, example: '"Jane Doe"' },
|
||||
{ name: "authorImage", required: false, example: '"/images/authors/jane.png"' },
|
||||
];
|
||||
|
||||
// Frontmatter field definitions for pages
|
||||
@@ -47,6 +49,8 @@ const PAGE_FIELDS = [
|
||||
{ name: "image", required: false, example: '"/images/thumbnail.png"' },
|
||||
{ name: "featured", required: false, example: "true" },
|
||||
{ name: "featuredOrder", required: false, example: "1" },
|
||||
{ name: "authorName", required: false, example: '"Jane Doe"' },
|
||||
{ name: "authorImage", required: false, example: '"/images/authors/jane.png"' },
|
||||
];
|
||||
|
||||
// Generate frontmatter template based on content type
|
||||
|
||||
@@ -750,6 +750,27 @@ body {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Author display in post header */
|
||||
.post-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.post-author-image {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.post-author-name {
|
||||
font-size: var(--font-size-post-meta-header);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.post-description {
|
||||
font-size: var(--font-size-post-description);
|
||||
color: var(--text-secondary);
|
||||
@@ -1462,7 +1483,7 @@ body {
|
||||
/* Modern horizontal cards container - 5 equal columns on large screens */
|
||||
.stats-cards-modern {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 0;
|
||||
margin-bottom: 64px;
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -1471,10 +1492,10 @@ body {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Ensure 5-column layout on large screens (explicit rule) */
|
||||
/* Ensure 6-column layout on large screens (explicit rule) */
|
||||
@media (min-width: 1101px) {
|
||||
.stats-cards-modern {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1707,18 +1728,26 @@ body {
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.stat-card-modern:nth-child(3) {
|
||||
/* Remove right border on last card of each row (3rd, 6th) */
|
||||
.stat-card-modern:nth-child(3),
|
||||
.stat-card-modern:nth-child(6) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Add top border on second row (4th, 5th, 6th) */
|
||||
.stat-card-modern:nth-child(4),
|
||||
.stat-card-modern:nth-child(5) {
|
||||
.stat-card-modern:nth-child(5),
|
||||
.stat-card-modern:nth-child(6) {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stat-card-modern:nth-child(4) {
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stat-card-modern:nth-child(5) {
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -1735,19 +1764,23 @@ body {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
/* Remove right border on even cards (end of each row) */
|
||||
.stat-card-modern:nth-child(2n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Add top border on rows 2 and 3 (cards 3-6) */
|
||||
.stat-card-modern:nth-child(3),
|
||||
.stat-card-modern:nth-child(4),
|
||||
.stat-card-modern:nth-child(5) {
|
||||
.stat-card-modern:nth-child(5),
|
||||
.stat-card-modern:nth-child(6) {
|
||||
border-top: 1px solid var(--border-color);
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stat-card-modern:nth-child(4) {
|
||||
border-right: none;
|
||||
/* Odd cards in rows 2+ need right border */
|
||||
.stat-card-modern:nth-child(3),
|
||||
.stat-card-modern:nth-child(5) {
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stat-card-modern-value {
|
||||
|
||||
Reference in New Issue
Block a user