mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
fix: fork configuration now updates 14 files, logoGallery uses relative URLs and Search result highlighting and scroll-to-match
This commit is contained in:
@@ -22,7 +22,7 @@ Your content is instantly available to browsers, LLMs, and AI agents.. Write mar
|
|||||||
- **Total Posts**: 17
|
- **Total Posts**: 17
|
||||||
- **Total Pages**: 4
|
- **Total Pages**: 4
|
||||||
- **Latest Post**: 2025-12-29
|
- **Latest Post**: 2025-12-29
|
||||||
- **Last Updated**: 2026-01-04T04:52:17.079Z
|
- **Last Updated**: 2026-01-04T05:50:22.819Z
|
||||||
|
|
||||||
## Tech stack
|
## Tech stack
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Project instructions for Claude Code.
|
|||||||
## Project context
|
## Project context
|
||||||
|
|
||||||
<!-- Auto-updated by sync:discovery -->
|
<!-- Auto-updated by sync:discovery -->
|
||||||
<!-- Site: markdown | Posts: 17 | Pages: 4 | Updated: 2026-01-04T04:52:17.080Z -->
|
<!-- Site: markdown | Posts: 17 | Pages: 4 | Updated: 2026-01-04T05:50:22.820Z -->
|
||||||
|
|
||||||
Markdown sync framework. Write markdown in `content/`, run sync commands, content appears instantly via Convex real-time database. Built for developers and AI agents.
|
Markdown sync framework. Write markdown in `content/`, run sync commands, content appears instantly via Convex real-time database. Built for developers and AI agents.
|
||||||
|
|
||||||
|
|||||||
@@ -49,20 +49,21 @@ The file `fork-config.json` is gitignored, so your configuration stays local and
|
|||||||
npm run configure
|
npm run configure
|
||||||
```
|
```
|
||||||
|
|
||||||
This updates all 11 configuration files automatically:
|
This updates all 14 configuration files automatically:
|
||||||
|
|
||||||
- `src/config/siteConfig.ts`
|
- `src/config/siteConfig.ts` (site name, bio, GitHub username, gitHubRepo config, default theme)
|
||||||
- `src/pages/Home.tsx`
|
- `src/pages/Home.tsx` (intro paragraph, footer links)
|
||||||
- `src/pages/Post.tsx`
|
- `src/pages/Post.tsx` (SITE_URL, SITE_NAME constants)
|
||||||
- `convex/http.ts`
|
- `src/pages/DocsPage.tsx` (SITE_URL constant for CopyPageDropdown)
|
||||||
- `convex/rss.ts`
|
- `convex/http.ts` (SITE_URL, SITE_NAME constants)
|
||||||
- `index.html`
|
- `convex/rss.ts` (SITE_URL, SITE_TITLE, SITE_DESCRIPTION)
|
||||||
- `public/llms.txt`
|
- `netlify/edge-functions/mcp.ts` (SITE_URL, SITE_NAME, MCP_SERVER_NAME)
|
||||||
- `public/robots.txt`
|
- `scripts/send-newsletter.ts` (default SITE_URL)
|
||||||
- `public/openapi.yaml`
|
- `index.html` (meta tags, JSON-LD, page title)
|
||||||
- `public/.well-known/ai-plugin.json`
|
- `public/llms.txt` (site info, GitHub link)
|
||||||
|
- `public/robots.txt` (sitemap URL)
|
||||||
Theme is now configured in `src/config/siteConfig.ts` (via the `defaultTheme` field).
|
- `public/openapi.yaml` (server URL, site name, example URLs)
|
||||||
|
- `public/.well-known/ai-plugin.json` (plugin metadata)
|
||||||
|
|
||||||
### Step 4: Review and deploy
|
### Step 4: Review and deploy
|
||||||
|
|
||||||
@@ -83,17 +84,19 @@ Edit each file individually following the guide below.
|
|||||||
|
|
||||||
| File | What to Update |
|
| File | What to Update |
|
||||||
| ----------------------------------- | ------------------------------------------------------------ |
|
| ----------------------------------- | ------------------------------------------------------------ |
|
||||||
| `src/config/siteConfig.ts` | Site name, bio, GitHub username, gitHubRepo config, features |
|
| `src/config/siteConfig.ts` | Site name, bio, GitHub username, gitHubRepo config, default theme, features |
|
||||||
| `src/pages/Home.tsx` | Intro paragraph, footer links |
|
| `src/pages/Home.tsx` | Intro paragraph, footer links |
|
||||||
| `src/pages/Post.tsx` | `SITE_URL`, `SITE_NAME` constants |
|
| `src/pages/Post.tsx` | `SITE_URL`, `SITE_NAME` constants |
|
||||||
|
| `src/pages/DocsPage.tsx` | `SITE_URL` constant |
|
||||||
| `convex/http.ts` | `SITE_URL`, `SITE_NAME` constants |
|
| `convex/http.ts` | `SITE_URL`, `SITE_NAME` constants |
|
||||||
| `convex/rss.ts` | `SITE_URL`, `SITE_TITLE`, `SITE_DESCRIPTION` |
|
| `convex/rss.ts` | `SITE_URL`, `SITE_TITLE`, `SITE_DESCRIPTION` |
|
||||||
|
| `netlify/edge-functions/mcp.ts` | `SITE_URL`, `SITE_NAME`, `MCP_SERVER_NAME` constants |
|
||||||
|
| `scripts/send-newsletter.ts` | Default `SITE_URL` constant |
|
||||||
| `index.html` | Meta tags, JSON-LD, page title |
|
| `index.html` | Meta tags, JSON-LD, page title |
|
||||||
| `public/llms.txt` | Site info, GitHub link |
|
| `public/llms.txt` | Site info, GitHub link |
|
||||||
| `public/robots.txt` | Sitemap URL |
|
| `public/robots.txt` | Sitemap URL |
|
||||||
| `public/openapi.yaml` | Server URL, site name |
|
| `public/openapi.yaml` | Server URL, site name, example URLs |
|
||||||
| `public/.well-known/ai-plugin.json` | Plugin metadata |
|
| `public/.well-known/ai-plugin.json` | Plugin metadata |
|
||||||
| `src/config/siteConfig.ts` | Default theme (`defaultTheme` field) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1234,16 +1237,19 @@ GitHub Repo Config (for AI service links):
|
|||||||
- Content Path: public/raw
|
- Content Path: public/raw
|
||||||
|
|
||||||
Update these files:
|
Update these files:
|
||||||
1. src/config/siteConfig.ts - site name, bio, GitHub username, gitHubRepo config
|
1. src/config/siteConfig.ts - site name, bio, GitHub username, gitHubRepo config, defaultTheme
|
||||||
2. src/pages/Home.tsx - intro paragraph and footer section with all creator links
|
2. src/pages/Home.tsx - intro paragraph and footer section with all creator links
|
||||||
3. src/pages/Post.tsx - SITE_URL and SITE_NAME constants
|
3. src/pages/Post.tsx - SITE_URL and SITE_NAME constants
|
||||||
4. convex/http.ts - SITE_URL and SITE_NAME constants
|
4. src/pages/DocsPage.tsx - SITE_URL constant
|
||||||
5. convex/rss.ts - SITE_URL, SITE_TITLE, SITE_DESCRIPTION
|
5. convex/http.ts - SITE_URL and SITE_NAME constants
|
||||||
6. index.html - all meta tags, JSON-LD, title
|
6. convex/rss.ts - SITE_URL, SITE_TITLE, SITE_DESCRIPTION
|
||||||
7. public/llms.txt - site info and GitHub link
|
7. netlify/edge-functions/mcp.ts - SITE_URL, SITE_NAME, MCP_SERVER_NAME constants
|
||||||
8. public/robots.txt - header comment and sitemap URL
|
8. scripts/send-newsletter.ts - default SITE_URL constant
|
||||||
9. public/openapi.yaml - API title, server URL, contact URL
|
9. index.html - all meta tags, JSON-LD, title
|
||||||
10. public/.well-known/ai-plugin.json - plugin metadata and contact email
|
10. public/llms.txt - site info and GitHub link
|
||||||
|
11. public/robots.txt - header comment and sitemap URL
|
||||||
|
12. public/openapi.yaml - API title, server URL, contact URL, example URLs
|
||||||
|
13. public/.well-known/ai-plugin.json - plugin metadata and contact email
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
19
TASK.md
19
TASK.md
@@ -4,10 +4,27 @@
|
|||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
v2.8.4 ready. AI service links now use local /raw URLs with simplified prompt.
|
v2.8.6 ready. Fork configuration script now updates 14 files for complete site branding.
|
||||||
|
|
||||||
## Completed
|
## Completed
|
||||||
|
|
||||||
|
- [x] Fork configuration improvements
|
||||||
|
- [x] Updated `scripts/configure-fork.ts` to update 3 additional files (DocsPage.tsx, mcp.ts, send-newsletter.ts)
|
||||||
|
- [x] Improved `updateOpenApiYaml()` to handle all example URLs in OpenAPI spec
|
||||||
|
- [x] Changed logoGallery hrefs from hardcoded markdown.fast URLs to relative URLs
|
||||||
|
- [x] Updated `FORK_CONFIG.md` with complete file list (14 files, was 11)
|
||||||
|
- [x] Updated `content/blog/fork-configuration-guide.md` with accurate file count
|
||||||
|
- [x] Added missing options to `fork-config.json.example` (statsPage, mcpServer, imageLightbox)
|
||||||
|
|
||||||
|
- [x] Search result highlighting and scroll-to-match
|
||||||
|
- [x] Created `useSearchHighlighting.ts` hook with polling mechanism to wait for content load
|
||||||
|
- [x] Search query passed via `?q=` URL parameter for highlighting on destination page
|
||||||
|
- [x] All matching text highlighted with theme-appropriate colors (dark/light/tan/cloud)
|
||||||
|
- [x] First match scrolls into view centered in viewport with header offset
|
||||||
|
- [x] Highlights pulse on arrival, fade to subtle after 4 seconds
|
||||||
|
- [x] Press Escape to clear highlights
|
||||||
|
- [x] Updated SearchModal.tsx, BlogPost.tsx, Post.tsx, global.css
|
||||||
|
|
||||||
- [x] Update AI service links to use local /raw URLs
|
- [x] Update AI service links to use local /raw URLs
|
||||||
- [x] Changed ChatGPT, Claude, Perplexity links from GitHub raw URLs to `/raw/{slug}.md`
|
- [x] Changed ChatGPT, Claude, Perplexity links from GitHub raw URLs to `/raw/{slug}.md`
|
||||||
- [x] Simplified AI prompt to "Read this URL and summarize it:"
|
- [x] Simplified AI prompt to "Read this URL and summarize it:"
|
||||||
|
|||||||
38
changelog.md
38
changelog.md
@@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## [2.8.6] - 2026-01-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Fork configuration script now updates 14 files (was 11)
|
||||||
|
- Added `src/pages/DocsPage.tsx` (SITE_URL constant)
|
||||||
|
- Added `netlify/edge-functions/mcp.ts` (SITE_URL, SITE_NAME, MCP_SERVER_NAME)
|
||||||
|
- Added `scripts/send-newsletter.ts` (default SITE_URL)
|
||||||
|
- Improved `public/openapi.yaml` handling for all example URLs
|
||||||
|
- Logo gallery hrefs now use relative URLs instead of hardcoded markdown.fast URLs
|
||||||
|
- Links like `/how-to-use-firecrawl`, `/docs`, `/setup-guide` work on any forked site
|
||||||
|
- Updated `fork-config.json.example` with missing options (statsPage, mcpServer, imageLightbox)
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Updated `scripts/configure-fork.ts` with new update functions: `updateDocsPageTsx()`, `updateMcpEdgeFunction()`, `updateSendNewsletter()`
|
||||||
|
- Updated `FORK_CONFIG.md` with complete file list and updated AI agent prompt
|
||||||
|
- Updated `content/blog/fork-configuration-guide.md` with accurate file count and output example
|
||||||
|
|
||||||
|
## [2.8.5] - 2026-01-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Search result highlighting and scroll-to-match feature
|
||||||
|
- Clicking a search result navigates to the exact match location (not just the heading)
|
||||||
|
- All matching text is highlighted with theme-appropriate colors
|
||||||
|
- Highlights pulse on arrival, then fade to subtle background after 4 seconds
|
||||||
|
- Press Escape to clear highlights
|
||||||
|
- Works across all four themes (dark, light, tan, cloud)
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Created `src/hooks/useSearchHighlighting.ts` hook with polling mechanism to wait for content load
|
||||||
|
- Updated `src/components/SearchModal.tsx` to pass search query via `?q=` URL parameter
|
||||||
|
- Updated `src/components/BlogPost.tsx` with article ref for highlighting
|
||||||
|
- Updated `src/pages/Post.tsx` to defer scroll handling to highlighting hook when `?q=` present
|
||||||
|
- Added `.search-highlight` and `.search-highlight-active` CSS styles with theme-specific colors
|
||||||
|
|
||||||
## [2.8.4] - 2026-01-03
|
## [2.8.4] - 2026-01-03
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ Open `fork-config.json` and update the values:
|
|||||||
npm run configure
|
npm run configure
|
||||||
```
|
```
|
||||||
|
|
||||||
The script reads your JSON file and updates all 11 configuration files automatically. You should see output like:
|
The script reads your JSON file and updates all 14 configuration files automatically. You should see output like:
|
||||||
|
|
||||||
```
|
```
|
||||||
Fork Configuration Script
|
Fork Configuration Script
|
||||||
@@ -77,14 +77,16 @@ Reading config from fork-config.json...
|
|||||||
Updating src/config/siteConfig.ts...
|
Updating src/config/siteConfig.ts...
|
||||||
Updating src/pages/Home.tsx...
|
Updating src/pages/Home.tsx...
|
||||||
Updating src/pages/Post.tsx...
|
Updating src/pages/Post.tsx...
|
||||||
|
Updating src/pages/DocsPage.tsx...
|
||||||
Updating convex/http.ts...
|
Updating convex/http.ts...
|
||||||
Updating convex/rss.ts...
|
Updating convex/rss.ts...
|
||||||
|
Updating netlify/edge-functions/mcp.ts...
|
||||||
|
Updating scripts/send-newsletter.ts...
|
||||||
Updating index.html...
|
Updating index.html...
|
||||||
Updating public/llms.txt...
|
Updating public/llms.txt...
|
||||||
Updating public/robots.txt...
|
Updating public/robots.txt...
|
||||||
Updating public/openapi.yaml...
|
Updating public/openapi.yaml...
|
||||||
Updating public/.well-known/ai-plugin.json...
|
Updating public/.well-known/ai-plugin.json...
|
||||||
Updating default theme in src/config/siteConfig.ts...
|
|
||||||
|
|
||||||
Configuration complete!
|
Configuration complete!
|
||||||
```
|
```
|
||||||
@@ -103,17 +105,19 @@ The configuration script updates these files:
|
|||||||
|
|
||||||
| File | What changes |
|
| File | What changes |
|
||||||
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `src/config/siteConfig.ts` | Site name, bio, GitHub username, gitHubRepo config, features (logo gallery, GitHub contributions, visitor map, blog page, posts display, homepage, right sidebar, footer, social footer, AI chat, newsletter, contact form, newsletter admin, stats page, MCP server, dashboard, image lightbox) |
|
| `src/config/siteConfig.ts` | Site name, bio, GitHub username, gitHubRepo config, default theme, features (logo gallery, GitHub contributions, visitor map, blog page, posts display, homepage, right sidebar, footer, social footer, AI chat, newsletter, contact form, newsletter admin, stats page, MCP server, dashboard, image lightbox) |
|
||||||
| `src/pages/Home.tsx` | Intro paragraph, footer links |
|
| `src/pages/Home.tsx` | Intro paragraph, footer links |
|
||||||
| `src/pages/Post.tsx` | SITE_URL, SITE_NAME constants |
|
| `src/pages/Post.tsx` | SITE_URL, SITE_NAME constants |
|
||||||
|
| `src/pages/DocsPage.tsx` | SITE_URL constant |
|
||||||
| `convex/http.ts` | SITE_URL, SITE_NAME constants |
|
| `convex/http.ts` | SITE_URL, SITE_NAME constants |
|
||||||
| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION |
|
| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION |
|
||||||
|
| `netlify/edge-functions/mcp.ts` | SITE_URL, SITE_NAME, MCP_SERVER_NAME constants |
|
||||||
|
| `scripts/send-newsletter.ts` | Default SITE_URL constant |
|
||||||
| `index.html` | Meta tags, JSON-LD, page title |
|
| `index.html` | Meta tags, JSON-LD, page title |
|
||||||
| `public/llms.txt` | Site info, GitHub link |
|
| `public/llms.txt` | Site info, GitHub link |
|
||||||
| `public/robots.txt` | Sitemap URL |
|
| `public/robots.txt` | Sitemap URL |
|
||||||
| `public/openapi.yaml` | Server URL, site name |
|
| `public/openapi.yaml` | Server URL, site name, example URLs |
|
||||||
| `public/.well-known/ai-plugin.json` | Plugin metadata |
|
| `public/.well-known/ai-plugin.json` | Plugin metadata |
|
||||||
| `src/config/siteConfig.ts` | Default theme (`defaultTheme` field) |
|
|
||||||
|
|
||||||
## Optional settings
|
## Optional settings
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,42 @@ docsSectionOrder: 4
|
|||||||
---
|
---
|
||||||
|
|
||||||
All notable changes to this project.
|
All notable changes to this project.
|
||||||

|
|
||||||
|
## v2.8.6
|
||||||
|
|
||||||
|
Released January 4, 2026
|
||||||
|
|
||||||
|
**Fork configuration improvements**
|
||||||
|
|
||||||
|
- Fork configuration script now updates 14 files (was 11)
|
||||||
|
- Added `src/pages/DocsPage.tsx` (SITE_URL constant)
|
||||||
|
- Added `netlify/edge-functions/mcp.ts` (SITE_URL, SITE_NAME, MCP_SERVER_NAME)
|
||||||
|
- Added `scripts/send-newsletter.ts` (default SITE_URL)
|
||||||
|
- Improved `public/openapi.yaml` handling for all example URLs
|
||||||
|
- Logo gallery hrefs now use relative URLs instead of hardcoded markdown.fast URLs
|
||||||
|
- Updated `fork-config.json.example` with missing options (statsPage, mcpServer, imageLightbox)
|
||||||
|
|
||||||
|
Updated files: `scripts/configure-fork.ts`, `src/config/siteConfig.ts`, `FORK_CONFIG.md`, `content/blog/fork-configuration-guide.md`, `fork-config.json.example`
|
||||||
|
|
||||||
|
## v2.8.5
|
||||||
|
|
||||||
|
Released January 3, 2026
|
||||||
|
|
||||||
|
**Search result highlighting and scroll-to-match**
|
||||||
|
|
||||||
|
- Clicking a search result now navigates to the exact match location (not just the nearest heading)
|
||||||
|
- All matching text is highlighted with theme-appropriate colors
|
||||||
|
- Highlights pulse on arrival, then fade to subtle background after 4 seconds
|
||||||
|
- Press Escape to clear highlights
|
||||||
|
- Works across all four themes (dark, light, tan, cloud)
|
||||||
|
|
||||||
|
**Technical details:**
|
||||||
|
|
||||||
|
- New `useSearchHighlighting.ts` hook with polling mechanism to wait for content load
|
||||||
|
- Search query passed via `?q=` URL parameter instead of hash anchor
|
||||||
|
- Theme-specific highlight colors matching existing design system
|
||||||
|
|
||||||
|
Updated files: `src/hooks/useSearchHighlighting.ts` (new), `src/components/SearchModal.tsx`, `src/components/BlogPost.tsx`, `src/pages/Post.tsx`, `src/styles/global.css`
|
||||||
|
|
||||||
## v2.8.4
|
## v2.8.4
|
||||||
|
|
||||||
|
|||||||
9
files.md
9
files.md
@@ -94,9 +94,10 @@ A brief description of each file in the codebase.
|
|||||||
|
|
||||||
### Hooks (`src/hooks/`)
|
### Hooks (`src/hooks/`)
|
||||||
|
|
||||||
| File | Description |
|
| File | Description |
|
||||||
| -------------------- | ------------------------------------------------ |
|
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `usePageTracking.ts` | Page view recording and active session heartbeat |
|
| `usePageTracking.ts` | Page view recording and active session heartbeat |
|
||||||
|
| `useSearchHighlighting.ts` | Search term highlighting and scroll-to-match. Reads `?q=` URL param, waits for content to load, highlights matches in DOM, scrolls to first match. |
|
||||||
|
|
||||||
### Styles (`src/styles/`)
|
### Styles (`src/styles/`)
|
||||||
|
|
||||||
@@ -225,7 +226,7 @@ Markdown files for static pages like About, Projects, Contact, Changelog.
|
|||||||
| `sync-posts.ts` | Syncs markdown files to Convex at build time (markdown sync v2). Generates `raw/index.md` with home.md content at top, posts/pages list, and footer.md content at bottom |
|
| `sync-posts.ts` | Syncs markdown files to Convex at build time (markdown sync v2). Generates `raw/index.md` with home.md content at top, posts/pages list, and footer.md content at bottom |
|
||||||
| `sync-discovery-files.ts` | Updates AGENTS.md, CLAUDE.md, and llms.txt with current app data |
|
| `sync-discovery-files.ts` | Updates AGENTS.md, CLAUDE.md, and llms.txt with current app data |
|
||||||
| `import-url.ts` | Imports external URLs as markdown posts (Firecrawl) |
|
| `import-url.ts` | Imports external URLs as markdown posts (Firecrawl) |
|
||||||
| `configure-fork.ts` | Automated fork configuration (reads fork-config.json). ES module compatible using fileURLToPath for __dirname equivalent. |
|
| `configure-fork.ts` | Automated fork configuration (reads fork-config.json, updates 14 files). ES module compatible using fileURLToPath for __dirname equivalent. |
|
||||||
| `send-newsletter.ts` | CLI tool for sending newsletter posts (npm run newsletter:send <slug>). Calls scheduleSendPostNewsletter mutation directly. |
|
| `send-newsletter.ts` | CLI tool for sending newsletter posts (npm run newsletter:send <slug>). Calls scheduleSendPostNewsletter mutation directly. |
|
||||||
| `send-newsletter-stats.ts` | CLI tool for sending weekly stats summary (npm run newsletter:send:stats). Calls scheduleSendStatsSummary mutation directly. |
|
| `send-newsletter-stats.ts` | CLI tool for sending weekly stats summary (npm run newsletter:send:stats). Calls scheduleSendStatsSummary mutation directly. |
|
||||||
| `sync-server.ts` | Local HTTP server for executing sync commands from Dashboard UI. Runs on localhost:3001 with optional token authentication. Whitelisted commands only. Part of markdown sync v2. |
|
| `sync-server.ts` | Local HTTP server for executing sync commands from Dashboard UI. Runs on localhost:3001 with optional token authentication. Whitelisted commands only. Part of markdown sync v2. |
|
||||||
|
|||||||
@@ -162,6 +162,20 @@
|
|||||||
"dayOfWeek": 0,
|
"dayOfWeek": 0,
|
||||||
"subject": "Weekly Digest"
|
"subject": "Weekly Digest"
|
||||||
},
|
},
|
||||||
|
"statsPage": {
|
||||||
|
"enabled": true,
|
||||||
|
"showInNav": true
|
||||||
|
},
|
||||||
|
"mcpServer": {
|
||||||
|
"enabled": true,
|
||||||
|
"endpoint": "/mcp",
|
||||||
|
"publicRateLimit": 50,
|
||||||
|
"authenticatedRateLimit": 1000,
|
||||||
|
"requireAuth": false
|
||||||
|
},
|
||||||
|
"imageLightbox": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"requireAuth": false
|
"requireAuth": false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# llms.txt - Information for AI assistants and LLMs
|
# llms.txt - Information for AI assistants and LLMs
|
||||||
# Learn more: https://llmstxt.org/
|
# Learn more: https://llmstxt.org/
|
||||||
# Last updated: 2026-01-04T04:52:17.081Z
|
# Last updated: 2026-01-04T05:50:22.820Z
|
||||||
|
|
||||||
> Your content is instantly available to browsers, LLMs, and AI agents.
|
> Your content is instantly available to browsers, LLMs, and AI agents.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,43 @@ Date: 2026-01-04
|
|||||||
---
|
---
|
||||||
|
|
||||||
All notable changes to this project.
|
All notable changes to this project.
|
||||||

|
|
||||||
|
## v2.8.5
|
||||||
|
|
||||||
|
Released January 3, 2026
|
||||||
|
|
||||||
|
**Search result highlighting and scroll-to-match**
|
||||||
|
|
||||||
|
- Clicking a search result now navigates to the exact match location (not just the nearest heading)
|
||||||
|
- All matching text is highlighted with theme-appropriate colors
|
||||||
|
- Highlights pulse on arrival, then fade to subtle background after 4 seconds
|
||||||
|
- Press Escape to clear highlights
|
||||||
|
- Works across all four themes (dark, light, tan, cloud)
|
||||||
|
|
||||||
|
**Technical details:**
|
||||||
|
|
||||||
|
- New `useSearchHighlighting.ts` hook with polling mechanism to wait for content load
|
||||||
|
- Search query passed via `?q=` URL parameter instead of hash anchor
|
||||||
|
- Theme-specific highlight colors matching existing design system
|
||||||
|
|
||||||
|
Updated files: `src/hooks/useSearchHighlighting.ts` (new), `src/components/SearchModal.tsx`, `src/components/BlogPost.tsx`, `src/pages/Post.tsx`, `src/styles/global.css`
|
||||||
|
|
||||||
|
## v2.8.4
|
||||||
|
|
||||||
|
Released January 3, 2026
|
||||||
|
|
||||||
|
**AI service links now use local /raw URLs**
|
||||||
|
|
||||||
|
- ChatGPT, Claude, and Perplexity links now use local `/raw/{slug}.md` URLs instead of GitHub raw URLs
|
||||||
|
- Simplified AI prompt from multi-line instructions to "Read this URL and summarize it:"
|
||||||
|
- No longer requires git push for AI links to work (synced content available immediately)
|
||||||
|
|
||||||
|
**Technical details:**
|
||||||
|
|
||||||
|
- Updated URL construction to use `window.location.origin` for consistency
|
||||||
|
- Removed unused `siteConfig` import and `getGitHubRawUrl` function
|
||||||
|
|
||||||
|
Updated files: `src/components/CopyPageDropdown.tsx`
|
||||||
|
|
||||||
## v2.8.3
|
## v2.8.3
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ Open `fork-config.json` and update the values:
|
|||||||
npm run configure
|
npm run configure
|
||||||
```
|
```
|
||||||
|
|
||||||
The script reads your JSON file and updates all 11 configuration files automatically. You should see output like:
|
The script reads your JSON file and updates all 14 configuration files automatically. You should see output like:
|
||||||
|
|
||||||
```
|
```
|
||||||
Fork Configuration Script
|
Fork Configuration Script
|
||||||
@@ -67,14 +67,16 @@ Reading config from fork-config.json...
|
|||||||
Updating src/config/siteConfig.ts...
|
Updating src/config/siteConfig.ts...
|
||||||
Updating src/pages/Home.tsx...
|
Updating src/pages/Home.tsx...
|
||||||
Updating src/pages/Post.tsx...
|
Updating src/pages/Post.tsx...
|
||||||
|
Updating src/pages/DocsPage.tsx...
|
||||||
Updating convex/http.ts...
|
Updating convex/http.ts...
|
||||||
Updating convex/rss.ts...
|
Updating convex/rss.ts...
|
||||||
|
Updating netlify/edge-functions/mcp.ts...
|
||||||
|
Updating scripts/send-newsletter.ts...
|
||||||
Updating index.html...
|
Updating index.html...
|
||||||
Updating public/llms.txt...
|
Updating public/llms.txt...
|
||||||
Updating public/robots.txt...
|
Updating public/robots.txt...
|
||||||
Updating public/openapi.yaml...
|
Updating public/openapi.yaml...
|
||||||
Updating public/.well-known/ai-plugin.json...
|
Updating public/.well-known/ai-plugin.json...
|
||||||
Updating default theme in src/config/siteConfig.ts...
|
|
||||||
|
|
||||||
Configuration complete!
|
Configuration complete!
|
||||||
```
|
```
|
||||||
@@ -93,17 +95,19 @@ The configuration script updates these files:
|
|||||||
|
|
||||||
| File | What changes |
|
| File | What changes |
|
||||||
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `src/config/siteConfig.ts` | Site name, bio, GitHub username, gitHubRepo config, features (logo gallery, GitHub contributions, visitor map, blog page, posts display, homepage, right sidebar, footer, social footer, AI chat, newsletter, contact form, newsletter admin, stats page, MCP server, dashboard, image lightbox) |
|
| `src/config/siteConfig.ts` | Site name, bio, GitHub username, gitHubRepo config, default theme, features (logo gallery, GitHub contributions, visitor map, blog page, posts display, homepage, right sidebar, footer, social footer, AI chat, newsletter, contact form, newsletter admin, stats page, MCP server, dashboard, image lightbox) |
|
||||||
| `src/pages/Home.tsx` | Intro paragraph, footer links |
|
| `src/pages/Home.tsx` | Intro paragraph, footer links |
|
||||||
| `src/pages/Post.tsx` | SITE_URL, SITE_NAME constants |
|
| `src/pages/Post.tsx` | SITE_URL, SITE_NAME constants |
|
||||||
|
| `src/pages/DocsPage.tsx` | SITE_URL constant |
|
||||||
| `convex/http.ts` | SITE_URL, SITE_NAME constants |
|
| `convex/http.ts` | SITE_URL, SITE_NAME constants |
|
||||||
| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION |
|
| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION |
|
||||||
|
| `netlify/edge-functions/mcp.ts` | SITE_URL, SITE_NAME, MCP_SERVER_NAME constants |
|
||||||
|
| `scripts/send-newsletter.ts` | Default SITE_URL constant |
|
||||||
| `index.html` | Meta tags, JSON-LD, page title |
|
| `index.html` | Meta tags, JSON-LD, page title |
|
||||||
| `public/llms.txt` | Site info, GitHub link |
|
| `public/llms.txt` | Site info, GitHub link |
|
||||||
| `public/robots.txt` | Sitemap URL |
|
| `public/robots.txt` | Sitemap URL |
|
||||||
| `public/openapi.yaml` | Server URL, site name |
|
| `public/openapi.yaml` | Server URL, site name, example URLs |
|
||||||
| `public/.well-known/ai-plugin.json` | Plugin metadata |
|
| `public/.well-known/ai-plugin.json` | Plugin metadata |
|
||||||
| `src/config/siteConfig.ts` | Default theme (`defaultTheme` field) |
|
|
||||||
|
|
||||||
## Optional settings
|
## Optional settings
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,16 @@
|
|||||||
* - src/config/siteConfig.ts (site name, bio, GitHub username, features)
|
* - src/config/siteConfig.ts (site name, bio, GitHub username, features)
|
||||||
* - src/pages/Home.tsx (intro paragraph, footer section)
|
* - src/pages/Home.tsx (intro paragraph, footer section)
|
||||||
* - src/pages/Post.tsx (SITE_URL, SITE_NAME constants)
|
* - src/pages/Post.tsx (SITE_URL, SITE_NAME constants)
|
||||||
|
* - src/pages/DocsPage.tsx (SITE_URL constant)
|
||||||
* - convex/http.ts (SITE_URL, SITE_NAME constants)
|
* - convex/http.ts (SITE_URL, SITE_NAME constants)
|
||||||
* - convex/rss.ts (SITE_URL, SITE_TITLE, SITE_DESCRIPTION)
|
* - convex/rss.ts (SITE_URL, SITE_TITLE, SITE_DESCRIPTION)
|
||||||
* - index.html (meta tags, JSON-LD, title)
|
* - index.html (meta tags, JSON-LD, title)
|
||||||
* - public/llms.txt (site info, API endpoints)
|
* - public/llms.txt (site info, API endpoints)
|
||||||
* - public/robots.txt (sitemap URL)
|
* - public/robots.txt (sitemap URL)
|
||||||
* - public/openapi.yaml (server URL, site name)
|
* - public/openapi.yaml (server URL, site name, example URLs)
|
||||||
* - public/.well-known/ai-plugin.json (plugin metadata)
|
* - public/.well-known/ai-plugin.json (plugin metadata)
|
||||||
* - src/context/ThemeContext.tsx (default theme)
|
* - netlify/edge-functions/mcp.ts (SITE_URL, SITE_NAME constants)
|
||||||
|
* - scripts/send-newsletter.ts (SITE_URL fallback)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
@@ -453,6 +455,19 @@ function updatePostTsx(config: ForkConfig): void {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update DocsPage.tsx
|
||||||
|
function updateDocsPageTsx(config: ForkConfig): void {
|
||||||
|
console.log("\nUpdating src/pages/DocsPage.tsx...");
|
||||||
|
|
||||||
|
updateFile("src/pages/DocsPage.tsx", [
|
||||||
|
// Match any existing SITE_URL value (https://...)
|
||||||
|
{
|
||||||
|
search: /const SITE_URL = "https:\/\/[^"]+";/,
|
||||||
|
replace: `const SITE_URL = "${config.siteUrl}";`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Update convex/http.ts
|
// Update convex/http.ts
|
||||||
function updateConvexHttp(config: ForkConfig): void {
|
function updateConvexHttp(config: ForkConfig): void {
|
||||||
console.log("\nUpdating convex/http.ts...");
|
console.log("\nUpdating convex/http.ts...");
|
||||||
@@ -750,6 +765,8 @@ function updateOpenApiYaml(config: ForkConfig): void {
|
|||||||
console.log("\nUpdating public/openapi.yaml...");
|
console.log("\nUpdating public/openapi.yaml...");
|
||||||
|
|
||||||
const githubUrl = `https://github.com/${config.githubUsername}/${config.githubRepo}`;
|
const githubUrl = `https://github.com/${config.githubUsername}/${config.githubRepo}`;
|
||||||
|
// Extract domain from siteUrl for example URLs (without www. if present)
|
||||||
|
const siteUrlForExamples = config.siteUrl.replace(/^https?:\/\/(www\.)?/, "https://");
|
||||||
|
|
||||||
updateFile("public/openapi.yaml", [
|
updateFile("public/openapi.yaml", [
|
||||||
// Match any title ending with API
|
// Match any title ending with API
|
||||||
@@ -767,15 +784,30 @@ function updateOpenApiYaml(config: ForkConfig): void {
|
|||||||
search: /- url: https:\/\/[^\s]+\n\s+description: Production server/,
|
search: /- url: https:\/\/[^\s]+\n\s+description: Production server/,
|
||||||
replace: `- url: ${config.siteUrl}\n description: Production server`,
|
replace: `- url: ${config.siteUrl}\n description: Production server`,
|
||||||
},
|
},
|
||||||
// Match any example site name
|
// Match site name example in schema (line 31)
|
||||||
{
|
{
|
||||||
search: /example: .+\n\s+url:/g,
|
search: /example: markdown sync framework/,
|
||||||
replace: `example: ${config.siteName}\n url:`,
|
replace: `example: ${config.siteName}`,
|
||||||
},
|
},
|
||||||
// Match any example URL (for site URL)
|
// Match site URL example in schema (line 34)
|
||||||
{
|
{
|
||||||
search: /example: https:\/\/[^\s]+\n\s+posts:/,
|
search: /example: https:\/\/markdown\.fast\n(\s+)posts:/,
|
||||||
replace: `example: ${config.siteUrl}\n posts:`,
|
replace: `example: ${siteUrlForExamples}\n$1posts:`,
|
||||||
|
},
|
||||||
|
// Match post URL example (line 167)
|
||||||
|
{
|
||||||
|
search: /example: https:\/\/markdown\.fast\/how-to-build-blog/,
|
||||||
|
replace: `example: ${siteUrlForExamples}/how-to-build-blog`,
|
||||||
|
},
|
||||||
|
// Match markdown URL example (line 170)
|
||||||
|
{
|
||||||
|
search: /example: https:\/\/markdown\.fast\/api\/post\?slug=how-to-build-blog/,
|
||||||
|
replace: `example: ${siteUrlForExamples}/api/post?slug=how-to-build-blog`,
|
||||||
|
},
|
||||||
|
// Match any remaining markdown.fast URLs
|
||||||
|
{
|
||||||
|
search: /https:\/\/(www\.)?markdown\.fast/g,
|
||||||
|
replace: siteUrlForExamples,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -823,6 +855,47 @@ function updateThemeConfig(config: ForkConfig): void {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update netlify/edge-functions/mcp.ts
|
||||||
|
function updateMcpEdgeFunction(config: ForkConfig): void {
|
||||||
|
console.log("\nUpdating netlify/edge-functions/mcp.ts...");
|
||||||
|
|
||||||
|
updateFile("netlify/edge-functions/mcp.ts", [
|
||||||
|
// Match any existing SITE_URL constant
|
||||||
|
{
|
||||||
|
search: /const SITE_URL = "https:\/\/[^"]+";/,
|
||||||
|
replace: `const SITE_URL = "${config.siteUrl}";`,
|
||||||
|
},
|
||||||
|
// Match any existing SITE_NAME constant
|
||||||
|
{
|
||||||
|
search: /const SITE_NAME = "[^"]+";/,
|
||||||
|
replace: `const SITE_NAME = "${config.siteName}";`,
|
||||||
|
},
|
||||||
|
// Match any existing MCP_SERVER_NAME constant (create from site name)
|
||||||
|
{
|
||||||
|
search: /const MCP_SERVER_NAME = "[^"]+";/,
|
||||||
|
replace: `const MCP_SERVER_NAME = "${config.siteName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "")}-mcp";`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update scripts/send-newsletter.ts
|
||||||
|
function updateSendNewsletter(config: ForkConfig): void {
|
||||||
|
console.log("\nUpdating scripts/send-newsletter.ts...");
|
||||||
|
|
||||||
|
updateFile("scripts/send-newsletter.ts", [
|
||||||
|
// Match any existing SITE_URL fallback in comment
|
||||||
|
{
|
||||||
|
search: /\* - SITE_URL: Your site URL \(default: https:\/\/[^)]+\)/,
|
||||||
|
replace: `* - SITE_URL: Your site URL (default: ${config.siteUrl})`,
|
||||||
|
},
|
||||||
|
// Match any existing SITE_URL fallback in code
|
||||||
|
{
|
||||||
|
search: /const siteUrl = process\.env\.SITE_URL \|\| "https:\/\/[^"]+";/,
|
||||||
|
replace: `const siteUrl = process.env.SITE_URL || "${config.siteUrl}";`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Main function
|
// Main function
|
||||||
function main(): void {
|
function main(): void {
|
||||||
console.log("Fork Configuration Script");
|
console.log("Fork Configuration Script");
|
||||||
@@ -837,6 +910,7 @@ function main(): void {
|
|||||||
updateSiteConfig(config);
|
updateSiteConfig(config);
|
||||||
updateHomeTsx(config);
|
updateHomeTsx(config);
|
||||||
updatePostTsx(config);
|
updatePostTsx(config);
|
||||||
|
updateDocsPageTsx(config);
|
||||||
updateConvexHttp(config);
|
updateConvexHttp(config);
|
||||||
updateConvexRss(config);
|
updateConvexRss(config);
|
||||||
updateIndexHtml(config);
|
updateIndexHtml(config);
|
||||||
@@ -845,6 +919,8 @@ function main(): void {
|
|||||||
updateOpenApiYaml(config);
|
updateOpenApiYaml(config);
|
||||||
updateAiPluginJson(config);
|
updateAiPluginJson(config);
|
||||||
updateThemeConfig(config);
|
updateThemeConfig(config);
|
||||||
|
updateMcpEdgeFunction(config);
|
||||||
|
updateSendNewsletter(config);
|
||||||
|
|
||||||
console.log("\n=========================");
|
console.log("\n=========================");
|
||||||
console.log("Configuration complete!");
|
console.log("Configuration complete!");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useRef } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import remarkBreaks from "remark-breaks";
|
import remarkBreaks from "remark-breaks";
|
||||||
@@ -10,6 +10,7 @@ import { useTheme } from "../context/ThemeContext";
|
|||||||
import NewsletterSignup from "./NewsletterSignup";
|
import NewsletterSignup from "./NewsletterSignup";
|
||||||
import ContactForm from "./ContactForm";
|
import ContactForm from "./ContactForm";
|
||||||
import siteConfig from "../config/siteConfig";
|
import siteConfig from "../config/siteConfig";
|
||||||
|
import { useSearchHighlighting } from "../hooks/useSearchHighlighting";
|
||||||
|
|
||||||
// Whitelisted domains for iframe embeds (YouTube and Twitter/X only)
|
// Whitelisted domains for iframe embeds (YouTube and Twitter/X only)
|
||||||
const ALLOWED_IFRAME_DOMAINS = [
|
const ALLOWED_IFRAME_DOMAINS = [
|
||||||
@@ -483,6 +484,10 @@ export default function BlogPost({
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const isLightboxEnabled = siteConfig.imageLightbox?.enabled !== false;
|
const isLightboxEnabled = siteConfig.imageLightbox?.enabled !== false;
|
||||||
|
|
||||||
|
// Search highlighting - scrolls to and highlights search terms from URL
|
||||||
|
const articleRef = useRef<HTMLElement>(null);
|
||||||
|
useSearchHighlighting({ containerRef: articleRef });
|
||||||
|
|
||||||
const getCodeTheme = () => {
|
const getCodeTheme = () => {
|
||||||
switch (theme) {
|
switch (theme) {
|
||||||
case "dark":
|
case "dark":
|
||||||
@@ -741,7 +746,7 @@ export default function BlogPost({
|
|||||||
if (hasInlineEmbeds) {
|
if (hasInlineEmbeds) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<article className="blog-post-content">
|
<article ref={articleRef} className="blog-post-content">
|
||||||
{segments.map((segment, index) => {
|
{segments.map((segment, index) => {
|
||||||
if (segment.type === "newsletter") {
|
if (segment.type === "newsletter") {
|
||||||
// Newsletter signup inline
|
// Newsletter signup inline
|
||||||
@@ -777,7 +782,7 @@ export default function BlogPost({
|
|||||||
// No inline embeds, render content normally
|
// No inline embeds, render content normally
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<article className="blog-post-content">
|
<article ref={articleRef} className="blog-post-content">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm, remarkBreaks]}
|
remarkPlugins={[remarkGfm, remarkBreaks]}
|
||||||
rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema]]}
|
rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema]]}
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (results[selectedIndex]) {
|
if (results[selectedIndex]) {
|
||||||
const result = results[selectedIndex];
|
const result = results[selectedIndex];
|
||||||
const url = result.anchor ? `/${result.slug}#${result.anchor}` : `/${result.slug}`;
|
// Pass search query as URL param for highlighting on destination page
|
||||||
|
const url = `/${result.slug}?q=${encodeURIComponent(searchQuery)}`;
|
||||||
navigate(url);
|
navigate(url);
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
@@ -68,8 +69,9 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Handle clicking on a result
|
// Handle clicking on a result
|
||||||
const handleResultClick = (slug: string, anchor?: string) => {
|
const handleResultClick = (slug: string) => {
|
||||||
const url = anchor ? `/${slug}#${anchor}` : `/${slug}`;
|
// Pass search query as URL param for highlighting on destination page
|
||||||
|
const url = `/${slug}?q=${encodeURIComponent(searchQuery)}`;
|
||||||
navigate(url);
|
navigate(url);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -136,7 +138,7 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
|||||||
<li key={result._id}>
|
<li key={result._id}>
|
||||||
<button
|
<button
|
||||||
className={`search-result-item ${index === selectedIndex ? "selected" : ""}`}
|
className={`search-result-item ${index === selectedIndex ? "selected" : ""}`}
|
||||||
onClick={() => handleResultClick(result.slug, result.anchor)}
|
onClick={() => handleResultClick(result.slug)}
|
||||||
onMouseEnter={() => setSelectedIndex(index)}
|
onMouseEnter={() => setSelectedIndex(index)}
|
||||||
>
|
>
|
||||||
<div className="search-result-icon">
|
<div className="search-result-icon">
|
||||||
|
|||||||
@@ -411,27 +411,27 @@ export const siteConfig: SiteConfig = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/images/logos/firecrawl.svg",
|
src: "/images/logos/firecrawl.svg",
|
||||||
href: "https://www.markdown.fast/how-to-use-firecrawl",
|
href: "/how-to-use-firecrawl",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/images/logos/markdown.svg",
|
src: "/images/logos/markdown.svg",
|
||||||
href: "https://markdown.fast/docs",
|
href: "/docs",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/images/logos/react.svg",
|
src: "/images/logos/react.svg",
|
||||||
href: "https://markdown.fast/setup-guide",
|
href: "/setup-guide",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/images/logos/agentmail.svg",
|
src: "/images/logos/agentmail.svg",
|
||||||
href: "https://www.markdown.fast/how-to-use-agentmail/",
|
href: "/how-to-use-agentmail",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/images/logos/mcp.svg",
|
src: "/images/logos/mcp.svg",
|
||||||
href: "https://www.markdown.fast/how-to-use-mcp-server/",
|
href: "/how-to-use-mcp-server",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/images/logos/workos.svg",
|
src: "/images/logos/workos.svg",
|
||||||
href: "https://www.markdown.fast/how-to-setup-workos",
|
href: "/how-to-setup-workos",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
position: "above-footer",
|
position: "above-footer",
|
||||||
|
|||||||
206
src/hooks/useSearchHighlighting.ts
Normal file
206
src/hooks/useSearchHighlighting.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import { useEffect, useRef, useCallback } from "react";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
|
interface UseSearchHighlightingOptions {
|
||||||
|
containerRef: React.RefObject<HTMLElement | null>;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maximum time to wait for content to load (ms)
|
||||||
|
const MAX_WAIT_TIME = 5000;
|
||||||
|
// How often to check for content (ms)
|
||||||
|
const POLL_INTERVAL = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to highlight search terms on the page and scroll to the first match.
|
||||||
|
* Reads the `?q=` query parameter from the URL and highlights all occurrences.
|
||||||
|
* Waits for content to load before attempting to highlight.
|
||||||
|
*/
|
||||||
|
export function useSearchHighlighting({
|
||||||
|
containerRef,
|
||||||
|
enabled = true,
|
||||||
|
}: UseSearchHighlightingOptions): { searchQuery: string | null } {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const searchQuery = searchParams.get("q");
|
||||||
|
const hasHighlighted = useRef(false);
|
||||||
|
const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
// Clear highlights function
|
||||||
|
const clearHighlights = useCallback(() => {
|
||||||
|
document.querySelectorAll("mark.search-highlight").forEach((mark) => {
|
||||||
|
const parent = mark.parentNode;
|
||||||
|
if (parent) {
|
||||||
|
parent.replaceChild(document.createTextNode(mark.textContent || ""), mark);
|
||||||
|
parent.normalize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Check if container has meaningful content (not just whitespace)
|
||||||
|
const hasContent = useCallback((container: HTMLElement): boolean => {
|
||||||
|
// Check for actual text content or child elements
|
||||||
|
const textContent = container.textContent?.trim() || "";
|
||||||
|
const hasChildElements = container.querySelectorAll("p, h1, h2, h3, h4, h5, h6, li, td").length > 0;
|
||||||
|
return textContent.length > 50 || hasChildElements;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Perform the actual highlighting
|
||||||
|
const performHighlighting = useCallback(
|
||||||
|
(container: HTMLElement, query: string) => {
|
||||||
|
// Escape special regex characters
|
||||||
|
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const regex = new RegExp(`(${escapedQuery})`, "gi");
|
||||||
|
|
||||||
|
// Walk text nodes and find matches
|
||||||
|
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
|
||||||
|
acceptNode: (node) => {
|
||||||
|
const parent = node.parentElement;
|
||||||
|
if (!parent) return NodeFilter.FILTER_REJECT;
|
||||||
|
// Skip code blocks and already highlighted content
|
||||||
|
const tagName = parent.tagName.toUpperCase();
|
||||||
|
if (tagName === "CODE" || tagName === "PRE" || tagName === "SCRIPT" || tagName === "STYLE") {
|
||||||
|
return NodeFilter.FILTER_REJECT;
|
||||||
|
}
|
||||||
|
if (parent.classList.contains("search-highlight")) {
|
||||||
|
return NodeFilter.FILTER_REJECT;
|
||||||
|
}
|
||||||
|
return NodeFilter.FILTER_ACCEPT;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const textNodes: Text[] = [];
|
||||||
|
let node: Node | null;
|
||||||
|
while ((node = walker.nextNode())) {
|
||||||
|
if (regex.test(node.textContent || "")) {
|
||||||
|
textNodes.push(node as Text);
|
||||||
|
}
|
||||||
|
regex.lastIndex = 0; // Reset regex state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight each match
|
||||||
|
let firstMark: HTMLElement | null = null;
|
||||||
|
textNodes.forEach((textNode) => {
|
||||||
|
const text = textNode.textContent || "";
|
||||||
|
const parts = text.split(regex);
|
||||||
|
|
||||||
|
if (parts.length <= 1) return;
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
parts.forEach((part, index) => {
|
||||||
|
if (index % 2 === 1) {
|
||||||
|
// This is a match
|
||||||
|
const mark = document.createElement("mark");
|
||||||
|
mark.className = "search-highlight search-highlight-active";
|
||||||
|
mark.textContent = part;
|
||||||
|
fragment.appendChild(mark);
|
||||||
|
if (!firstMark) firstMark = mark;
|
||||||
|
} else if (part) {
|
||||||
|
fragment.appendChild(document.createTextNode(part));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
textNode.parentNode?.replaceChild(fragment, textNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
return firstMark;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Main highlighting effect
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !searchQuery || hasHighlighted.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Poll until content is available or timeout
|
||||||
|
const attemptHighlight = () => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
|
||||||
|
// Give up after MAX_WAIT_TIME
|
||||||
|
if (elapsed > MAX_WAIT_TIME) {
|
||||||
|
// Clean up URL even if we couldn't highlight
|
||||||
|
searchParams.delete("q");
|
||||||
|
setSearchParams(searchParams, { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if container exists and has content
|
||||||
|
if (!container || !hasContent(container)) {
|
||||||
|
// Try again after POLL_INTERVAL
|
||||||
|
pollTimeoutRef.current = setTimeout(attemptHighlight, POLL_INTERVAL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content is ready, perform highlighting
|
||||||
|
const firstMark = performHighlighting(container, searchQuery);
|
||||||
|
hasHighlighted.current = true;
|
||||||
|
|
||||||
|
// Scroll to first match with offset for fixed header
|
||||||
|
if (firstMark) {
|
||||||
|
// Small delay to ensure DOM has updated after highlighting
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!firstMark) return;
|
||||||
|
const headerOffset = 80;
|
||||||
|
const elementRect = firstMark.getBoundingClientRect();
|
||||||
|
const absoluteElementTop = elementRect.top + window.pageYOffset;
|
||||||
|
const offsetPosition = absoluteElementTop - headerOffset - window.innerHeight / 2 + 50;
|
||||||
|
|
||||||
|
window.scrollTo({
|
||||||
|
top: Math.max(0, offsetPosition),
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-fade highlights after 4 seconds (remove active class only)
|
||||||
|
setTimeout(() => {
|
||||||
|
document.querySelectorAll(".search-highlight-active").forEach((el) => {
|
||||||
|
el.classList.remove("search-highlight-active");
|
||||||
|
});
|
||||||
|
}, 4000);
|
||||||
|
|
||||||
|
// Clean up URL (remove ?q= param) after highlighting is applied
|
||||||
|
setTimeout(() => {
|
||||||
|
searchParams.delete("q");
|
||||||
|
setSearchParams(searchParams, { replace: true });
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start polling
|
||||||
|
pollTimeoutRef.current = setTimeout(attemptHighlight, POLL_INTERVAL);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (pollTimeoutRef.current) {
|
||||||
|
clearTimeout(pollTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [searchQuery, containerRef, enabled, searchParams, setSearchParams, hasContent, performHighlighting]);
|
||||||
|
|
||||||
|
// Clear highlights on Escape key
|
||||||
|
useEffect(() => {
|
||||||
|
if (!searchQuery && !hasHighlighted.current) return;
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
clearHighlights();
|
||||||
|
hasHighlighted.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [searchQuery, clearHighlights]);
|
||||||
|
|
||||||
|
// Reset hasHighlighted when component unmounts or query changes
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
hasHighlighted.current = false;
|
||||||
|
};
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
return { searchQuery };
|
||||||
|
}
|
||||||
@@ -56,6 +56,8 @@ import {
|
|||||||
ChatText,
|
ChatText,
|
||||||
SpinnerGap,
|
SpinnerGap,
|
||||||
CaretDown,
|
CaretDown,
|
||||||
|
ArrowsOut,
|
||||||
|
ArrowsIn,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import siteConfig from "../config/siteConfig";
|
import siteConfig from "../config/siteConfig";
|
||||||
import AIChatView from "../components/AIChatView";
|
import AIChatView from "../components/AIChatView";
|
||||||
@@ -525,6 +527,19 @@ function DashboardContent() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Keyboard shortcut: Cmd+. to toggle sidebar
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === ".") {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleSidebar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [toggleSidebar]);
|
||||||
|
|
||||||
// Toast notifications state
|
// Toast notifications state
|
||||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||||
|
|
||||||
@@ -1115,12 +1130,20 @@ function DashboardContent() {
|
|||||||
|
|
||||||
{/* Write Post Section */}
|
{/* Write Post Section */}
|
||||||
{activeSection === "write-post" && (
|
{activeSection === "write-post" && (
|
||||||
<WriteSection contentType="post" />
|
<WriteSection
|
||||||
|
contentType="post"
|
||||||
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
|
setSidebarCollapsed={setSidebarCollapsed}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Write Page Section */}
|
{/* Write Page Section */}
|
||||||
{activeSection === "write-page" && (
|
{activeSection === "write-page" && (
|
||||||
<WriteSection contentType="page" />
|
<WriteSection
|
||||||
|
contentType="page"
|
||||||
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
|
setSidebarCollapsed={setSidebarCollapsed}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* AI Agent Section */}
|
{/* AI Agent Section */}
|
||||||
@@ -2033,6 +2056,13 @@ const POST_FIELDS = [
|
|||||||
{ name: "blogFeatured", required: false, example: "true" },
|
{ name: "blogFeatured", required: false, example: "true" },
|
||||||
{ name: "newsletter", required: false, example: "true" },
|
{ name: "newsletter", required: false, example: "true" },
|
||||||
{ name: "contactForm", required: false, example: "true" },
|
{ name: "contactForm", required: false, example: "true" },
|
||||||
|
{ name: "unlisted", required: false, example: "true" },
|
||||||
|
{ name: "docsSection", required: false, example: "true" },
|
||||||
|
{ name: "docsSectionOrder", required: false, example: "1" },
|
||||||
|
{ name: "docsSectionGroup", required: false, example: '"Setup"' },
|
||||||
|
{ name: "docsSectionGroupOrder", required: false, example: "1" },
|
||||||
|
{ name: "docsSectionGroupIcon", required: false, example: '"Rocket"' },
|
||||||
|
{ name: "docsLanding", required: false, example: "true" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Frontmatter field definitions for pages (matches Write.tsx)
|
// Frontmatter field definitions for pages (matches Write.tsx)
|
||||||
@@ -2065,6 +2095,13 @@ const PAGE_FIELDS = [
|
|||||||
{ name: "aiChat", required: false, example: "true" },
|
{ name: "aiChat", required: false, example: "true" },
|
||||||
{ name: "newsletter", required: false, example: "true" },
|
{ name: "newsletter", required: false, example: "true" },
|
||||||
{ name: "contactForm", required: false, example: "true" },
|
{ name: "contactForm", required: false, example: "true" },
|
||||||
|
{ name: "unlisted", required: false, example: "true" },
|
||||||
|
{ name: "docsSection", required: false, example: "true" },
|
||||||
|
{ name: "docsSectionOrder", required: false, example: "1" },
|
||||||
|
{ name: "docsSectionGroup", required: false, example: '"Setup"' },
|
||||||
|
{ name: "docsSectionGroupOrder", required: false, example: "1" },
|
||||||
|
{ name: "docsSectionGroupIcon", required: false, example: '"Rocket"' },
|
||||||
|
{ name: "docsLanding", required: false, example: "true" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Generate frontmatter template based on content type
|
// Generate frontmatter template based on content type
|
||||||
@@ -2129,11 +2166,75 @@ With sidebar layout enabled, headings automatically appear in the table of conte
|
|||||||
// localStorage keys for dashboard write
|
// localStorage keys for dashboard write
|
||||||
const DASHBOARD_WRITE_POST_CONTENT = "dashboard_write_post_content";
|
const DASHBOARD_WRITE_POST_CONTENT = "dashboard_write_post_content";
|
||||||
const DASHBOARD_WRITE_PAGE_CONTENT = "dashboard_write_page_content";
|
const DASHBOARD_WRITE_PAGE_CONTENT = "dashboard_write_page_content";
|
||||||
|
const DASHBOARD_WRITE_FOCUS_MODE = "dashboard_write_focus_mode";
|
||||||
|
const DASHBOARD_WRITE_FRONTMATTER_COLLAPSED = "dashboard_write_frontmatter_collapsed";
|
||||||
|
|
||||||
function WriteSection({ contentType }: { contentType: "post" | "page" }) {
|
function WriteSection({
|
||||||
|
contentType,
|
||||||
|
sidebarCollapsed,
|
||||||
|
setSidebarCollapsed,
|
||||||
|
}: {
|
||||||
|
contentType: "post" | "page";
|
||||||
|
sidebarCollapsed: boolean;
|
||||||
|
setSidebarCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}) {
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [copiedField, setCopiedField] = useState<string | null>(null);
|
const [copiedField, setCopiedField] = useState<string | null>(null);
|
||||||
|
const [focusMode, setFocusMode] = useState(() => {
|
||||||
|
const saved = localStorage.getItem(DASHBOARD_WRITE_FOCUS_MODE);
|
||||||
|
return saved === "true";
|
||||||
|
});
|
||||||
|
const [frontmatterCollapsed, setFrontmatterCollapsed] = useState(() => {
|
||||||
|
const saved = localStorage.getItem(DASHBOARD_WRITE_FRONTMATTER_COLLAPSED);
|
||||||
|
// Default to collapsed in focus mode
|
||||||
|
return saved === "true";
|
||||||
|
});
|
||||||
|
// Store previous sidebar state before entering focus mode
|
||||||
|
const [prevSidebarState, setPrevSidebarState] = useState<boolean | null>(null);
|
||||||
|
|
||||||
|
// Toggle focus mode
|
||||||
|
const toggleFocusMode = useCallback(() => {
|
||||||
|
setFocusMode((prev) => {
|
||||||
|
const newValue = !prev;
|
||||||
|
localStorage.setItem(DASHBOARD_WRITE_FOCUS_MODE, String(newValue));
|
||||||
|
// When entering focus mode, save sidebar state and collapse it
|
||||||
|
if (newValue) {
|
||||||
|
setPrevSidebarState(sidebarCollapsed);
|
||||||
|
setSidebarCollapsed(true);
|
||||||
|
setFrontmatterCollapsed(true);
|
||||||
|
localStorage.setItem(DASHBOARD_WRITE_FRONTMATTER_COLLAPSED, "true");
|
||||||
|
} else {
|
||||||
|
// When exiting focus mode, restore previous sidebar state
|
||||||
|
if (prevSidebarState !== null) {
|
||||||
|
setSidebarCollapsed(prevSidebarState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
}, [sidebarCollapsed, setSidebarCollapsed, prevSidebarState]);
|
||||||
|
|
||||||
|
// Toggle frontmatter sidebar
|
||||||
|
const toggleFrontmatter = useCallback(() => {
|
||||||
|
setFrontmatterCollapsed((prev) => {
|
||||||
|
const newValue = !prev;
|
||||||
|
localStorage.setItem(DASHBOARD_WRITE_FRONTMATTER_COLLAPSED, String(newValue));
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Keyboard shortcut: Escape to exit focus mode
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape" && focusMode) {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleFocusMode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [focusMode, toggleFocusMode]);
|
||||||
|
|
||||||
// localStorage key based on content type
|
// localStorage key based on content type
|
||||||
const storageKey =
|
const storageKey =
|
||||||
@@ -2269,7 +2370,9 @@ published: false
|
|||||||
const fields = contentType === "post" ? POST_FIELDS : PAGE_FIELDS;
|
const fields = contentType === "post" ? POST_FIELDS : PAGE_FIELDS;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard-write-section">
|
<div
|
||||||
|
className={`dashboard-write-section ${focusMode ? "focus-mode" : ""} ${frontmatterCollapsed ? "frontmatter-collapsed" : ""}`}
|
||||||
|
>
|
||||||
{/* Write Actions Header */}
|
{/* Write Actions Header */}
|
||||||
<div className="dashboard-write-header">
|
<div className="dashboard-write-header">
|
||||||
<div className="dashboard-write-title">
|
<div className="dashboard-write-title">
|
||||||
@@ -2303,6 +2406,17 @@ published: false
|
|||||||
<Download size={16} />
|
<Download size={16} />
|
||||||
<span>Download .md</span>
|
<span>Download .md</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={toggleFocusMode}
|
||||||
|
className={`dashboard-action-btn focus-toggle ${focusMode ? "active" : ""}`}
|
||||||
|
title={focusMode ? "Exit focus mode (Esc)" : "Enter focus mode"}
|
||||||
|
>
|
||||||
|
{focusMode ? (
|
||||||
|
<ArrowsIn size={16} weight="regular" />
|
||||||
|
) : (
|
||||||
|
<ArrowsOut size={16} weight="regular" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2333,9 +2447,18 @@ published: false
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Frontmatter Sidebar */}
|
{/* Frontmatter Sidebar */}
|
||||||
<aside className="dashboard-write-sidebar">
|
<aside
|
||||||
|
className={`dashboard-write-sidebar ${frontmatterCollapsed ? "collapsed" : ""}`}
|
||||||
|
>
|
||||||
<div className="dashboard-write-sidebar-header">
|
<div className="dashboard-write-sidebar-header">
|
||||||
<span>Frontmatter</span>
|
<span>Frontmatter</span>
|
||||||
|
<button
|
||||||
|
onClick={toggleFrontmatter}
|
||||||
|
className="dashboard-write-sidebar-toggle"
|
||||||
|
title={frontmatterCollapsed ? "Expand" : "Collapse"}
|
||||||
|
>
|
||||||
|
<SidebarSimple size={16} weight="regular" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="dashboard-write-fields">
|
<div className="dashboard-write-fields">
|
||||||
<div className="write-fields-section">
|
<div className="write-fields-section">
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import { useQuery } from "convex/react";
|
|||||||
import { api } from "../../convex/_generated/api";
|
import { api } from "../../convex/_generated/api";
|
||||||
import DocsLayout from "../components/DocsLayout";
|
import DocsLayout from "../components/DocsLayout";
|
||||||
import BlogPost from "../components/BlogPost";
|
import BlogPost from "../components/BlogPost";
|
||||||
|
import CopyPageDropdown from "../components/CopyPageDropdown";
|
||||||
import { extractHeadings } from "../utils/extractHeadings";
|
import { extractHeadings } from "../utils/extractHeadings";
|
||||||
import siteConfig from "../config/siteConfig";
|
import siteConfig from "../config/siteConfig";
|
||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
|
// Site URL for CopyPageDropdown - update when forking
|
||||||
|
const SITE_URL = "https://www.markdown.fast";
|
||||||
|
|
||||||
export default function DocsPage() {
|
export default function DocsPage() {
|
||||||
// Fetch landing page content (checks pages first, then posts)
|
// Fetch landing page content (checks pages first, then posts)
|
||||||
const landingPage = useQuery(api.pages.getDocsLandingPage);
|
const landingPage = useQuery(api.pages.getDocsLandingPage);
|
||||||
@@ -64,10 +68,25 @@ export default function DocsPage() {
|
|||||||
// If we have landing content, render it with DocsLayout
|
// If we have landing content, render it with DocsLayout
|
||||||
if (landingContent) {
|
if (landingContent) {
|
||||||
const headings = extractHeadings(landingContent.content);
|
const headings = extractHeadings(landingContent.content);
|
||||||
|
const description =
|
||||||
|
"description" in landingContent
|
||||||
|
? landingContent.description
|
||||||
|
: "excerpt" in landingContent
|
||||||
|
? landingContent.excerpt
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocsLayout headings={headings} currentSlug={landingContent.slug}>
|
<DocsLayout headings={headings} currentSlug={landingContent.slug}>
|
||||||
<article className="docs-article">
|
<article className="docs-article">
|
||||||
|
<div className="docs-article-actions">
|
||||||
|
<CopyPageDropdown
|
||||||
|
title={landingContent.title}
|
||||||
|
content={landingContent.content}
|
||||||
|
url={`${SITE_URL}/${landingContent.slug}`}
|
||||||
|
slug={landingContent.slug}
|
||||||
|
description={description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<header className="docs-article-header">
|
<header className="docs-article-header">
|
||||||
<h1 className="docs-article-title">{landingContent.title}</h1>
|
<h1 className="docs-article-title">{landingContent.title}</h1>
|
||||||
{"description" in landingContent && landingContent.description && (
|
{"description" in landingContent && landingContent.description && (
|
||||||
|
|||||||
@@ -88,7 +88,12 @@ export default function Post({
|
|||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
// Scroll to hash anchor after content loads
|
// Scroll to hash anchor after content loads
|
||||||
|
// Skip if there's a search query - let the highlighting hook handle scroll
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// If there's a search query, the highlighting hook handles scrolling to the match
|
||||||
|
const searchQuery = new URLSearchParams(location.search).get("q");
|
||||||
|
if (searchQuery) return;
|
||||||
|
|
||||||
if (!location.hash) return;
|
if (!location.hash) return;
|
||||||
if (page === undefined && post === undefined) return;
|
if (page === undefined && post === undefined) return;
|
||||||
|
|
||||||
@@ -102,7 +107,7 @@ export default function Post({
|
|||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [location.hash, page, post]);
|
}, [location.hash, location.search, page, post]);
|
||||||
|
|
||||||
// Update sidebar context with headings for mobile menu
|
// Update sidebar context with headings for mobile menu
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -663,7 +668,7 @@ export default function Post({
|
|||||||
<p className="post-description">{post.description}</p>
|
<p className="post-description">{post.description}</p>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
{/* Blog post sharing links */}
|
{/* Blog post content - raw markdown or rendered */}
|
||||||
<BlogPost content={post.content} slug={post.slug} pageType="post" />
|
<BlogPost content={post.content} slug={post.slug} pageType="post" />
|
||||||
|
|
||||||
<footer className="post-footer">
|
<footer className="post-footer">
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import {
|
|||||||
Warning,
|
Warning,
|
||||||
TextAa,
|
TextAa,
|
||||||
ChatCircle,
|
ChatCircle,
|
||||||
|
ArrowsOut,
|
||||||
|
ArrowsIn,
|
||||||
|
SidebarSimple,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { Moon, Sun, Cloud } from "lucide-react";
|
import { Moon, Sun, Cloud } from "lucide-react";
|
||||||
import { Half2Icon } from "@radix-ui/react-icons";
|
import { Half2Icon } from "@radix-ui/react-icons";
|
||||||
@@ -60,6 +63,12 @@ const POST_FIELDS = [
|
|||||||
{ name: "newsletter", required: false, example: "true" },
|
{ name: "newsletter", required: false, example: "true" },
|
||||||
{ name: "contactForm", required: false, example: "true" },
|
{ name: "contactForm", required: false, example: "true" },
|
||||||
{ name: "unlisted", required: false, example: "true" },
|
{ name: "unlisted", required: false, example: "true" },
|
||||||
|
{ name: "docsSection", required: false, example: "true" },
|
||||||
|
{ name: "docsSectionOrder", required: false, example: "1" },
|
||||||
|
{ name: "docsSectionGroup", required: false, example: '"Setup"' },
|
||||||
|
{ name: "docsSectionGroupOrder", required: false, example: "1" },
|
||||||
|
{ name: "docsSectionGroupIcon", required: false, example: '"Rocket"' },
|
||||||
|
{ name: "docsLanding", required: false, example: "true" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Frontmatter field definitions for pages
|
// Frontmatter field definitions for pages
|
||||||
@@ -92,6 +101,13 @@ const PAGE_FIELDS = [
|
|||||||
{ name: "aiChat", required: false, example: "true" },
|
{ name: "aiChat", required: false, example: "true" },
|
||||||
{ name: "newsletter", required: false, example: "true" },
|
{ name: "newsletter", required: false, example: "true" },
|
||||||
{ name: "contactForm", required: false, example: "true" },
|
{ name: "contactForm", required: false, example: "true" },
|
||||||
|
{ name: "unlisted", required: false, example: "true" },
|
||||||
|
{ name: "docsSection", required: false, example: "true" },
|
||||||
|
{ name: "docsSectionOrder", required: false, example: "1" },
|
||||||
|
{ name: "docsSectionGroup", required: false, example: '"Setup"' },
|
||||||
|
{ name: "docsSectionGroupOrder", required: false, example: "1" },
|
||||||
|
{ name: "docsSectionGroupIcon", required: false, example: '"Rocket"' },
|
||||||
|
{ name: "docsLanding", required: false, example: "true" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Generate frontmatter template based on content type
|
// Generate frontmatter template based on content type
|
||||||
@@ -157,6 +173,9 @@ With sidebar layout enabled, headings automatically appear in the table of conte
|
|||||||
const STORAGE_KEY_CONTENT = "markdown_write_content";
|
const STORAGE_KEY_CONTENT = "markdown_write_content";
|
||||||
const STORAGE_KEY_TYPE = "markdown_write_type";
|
const STORAGE_KEY_TYPE = "markdown_write_type";
|
||||||
const STORAGE_KEY_FONT = "markdown_write_font";
|
const STORAGE_KEY_FONT = "markdown_write_font";
|
||||||
|
const STORAGE_KEY_SIDEBAR = "markdown_write_sidebar_collapsed";
|
||||||
|
const STORAGE_KEY_FOCUS = "markdown_write_focus_mode";
|
||||||
|
const STORAGE_KEY_FRONTMATTER = "markdown_write_frontmatter_collapsed";
|
||||||
|
|
||||||
// Font family definitions (matches global.css options)
|
// Font family definitions (matches global.css options)
|
||||||
// Note: Write page uses its own font state for local editing, but respects global font on mount
|
// Note: Write page uses its own font state for local editing, but respects global font on mount
|
||||||
@@ -192,6 +211,18 @@ export default function Write() {
|
|||||||
const [copiedField, setCopiedField] = useState<string | null>(null);
|
const [copiedField, setCopiedField] = useState<string | null>(null);
|
||||||
const [font, setFont] = useState<"serif" | "sans" | "monospace">("sans");
|
const [font, setFont] = useState<"serif" | "sans" | "monospace">("sans");
|
||||||
const [isAIChatMode, setIsAIChatMode] = useState(false);
|
const [isAIChatMode, setIsAIChatMode] = useState(false);
|
||||||
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY_SIDEBAR);
|
||||||
|
return saved === "true";
|
||||||
|
});
|
||||||
|
const [focusMode, setFocusMode] = useState(() => {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY_FOCUS);
|
||||||
|
return saved === "true";
|
||||||
|
});
|
||||||
|
const [frontmatterCollapsed, setFrontmatterCollapsed] = useState(() => {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY_FRONTMATTER);
|
||||||
|
return saved === "true";
|
||||||
|
});
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
// Check if AI chat is enabled for write page
|
// Check if AI chat is enabled for write page
|
||||||
@@ -271,6 +302,56 @@ export default function Write() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Toggle sidebar collapsed state
|
||||||
|
const toggleSidebar = useCallback(() => {
|
||||||
|
setSidebarCollapsed((prev) => {
|
||||||
|
const newValue = !prev;
|
||||||
|
localStorage.setItem(STORAGE_KEY_SIDEBAR, String(newValue));
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Toggle focus mode (hides sidebars and header for distraction-free writing)
|
||||||
|
const toggleFocusMode = useCallback(() => {
|
||||||
|
setFocusMode((prev) => {
|
||||||
|
const newValue = !prev;
|
||||||
|
localStorage.setItem(STORAGE_KEY_FOCUS, String(newValue));
|
||||||
|
// When entering focus mode, collapse frontmatter by default
|
||||||
|
if (newValue) {
|
||||||
|
setFrontmatterCollapsed(true);
|
||||||
|
localStorage.setItem(STORAGE_KEY_FRONTMATTER, "true");
|
||||||
|
}
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Toggle frontmatter sidebar
|
||||||
|
const toggleFrontmatter = useCallback(() => {
|
||||||
|
setFrontmatterCollapsed((prev) => {
|
||||||
|
const newValue = !prev;
|
||||||
|
localStorage.setItem(STORAGE_KEY_FRONTMATTER, String(newValue));
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Keyboard shortcut: Cmd+. to toggle sidebar
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === ".") {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleSidebar();
|
||||||
|
}
|
||||||
|
// Escape to exit focus mode
|
||||||
|
if (e.key === "Escape" && focusMode) {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleFocusMode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [toggleSidebar, toggleFocusMode, focusMode]);
|
||||||
|
|
||||||
// Handle type change and update content template
|
// Handle type change and update content template
|
||||||
const handleTypeChange = (newType: "post" | "page") => {
|
const handleTypeChange = (newType: "post" | "page") => {
|
||||||
if (newType === contentType) return;
|
if (newType === contentType) return;
|
||||||
@@ -338,14 +419,25 @@ export default function Write() {
|
|||||||
const fields = contentType === "post" ? POST_FIELDS : PAGE_FIELDS;
|
const fields = contentType === "post" ? POST_FIELDS : PAGE_FIELDS;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="write-layout">
|
<div
|
||||||
|
className={`write-layout ${sidebarCollapsed ? "sidebar-collapsed" : ""} ${focusMode ? "focus-mode" : ""} ${frontmatterCollapsed ? "frontmatter-collapsed" : ""}`}
|
||||||
|
>
|
||||||
{/* Left Sidebar: Type selector */}
|
{/* Left Sidebar: Type selector */}
|
||||||
<aside className="write-sidebar-left">
|
<aside
|
||||||
|
className={`write-sidebar-left ${sidebarCollapsed ? "collapsed" : ""}`}
|
||||||
|
>
|
||||||
<div className="write-sidebar-header">
|
<div className="write-sidebar-header">
|
||||||
<Link to="/" className="write-logo-link" title="Back to home">
|
<Link to="/" className="write-logo-link" title="Back to home">
|
||||||
<House size={20} weight="regular" />
|
<House size={20} weight="regular" />
|
||||||
<span>Home</span>
|
<span>Home</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
<button
|
||||||
|
className="write-sidebar-toggle"
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||||
|
>
|
||||||
|
<SidebarSimple size={20} weight="regular" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="write-nav">
|
<nav className="write-nav">
|
||||||
@@ -434,31 +526,46 @@ export default function Write() {
|
|||||||
{/* Main writing area */}
|
{/* Main writing area */}
|
||||||
<main className="write-main">
|
<main className="write-main">
|
||||||
<div className="write-main-header">
|
<div className="write-main-header">
|
||||||
<h1 className="write-main-title">
|
<div className="write-header-left">
|
||||||
{isAIChatMode
|
<h1 className="write-main-title">
|
||||||
? "Agent"
|
{isAIChatMode
|
||||||
: contentType === "post"
|
? "Agent"
|
||||||
? "Blog Post"
|
: contentType === "post"
|
||||||
: "Page"}
|
? "Blog Post"
|
||||||
</h1>
|
: "Page"}
|
||||||
{!isAIChatMode && (
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="write-header-actions">
|
||||||
|
{!isAIChatMode && (
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className={`write-copy-btn ${copied ? "copied" : ""}`}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Check size={16} weight="bold" />
|
||||||
|
<span>Copied</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CopySimple size={16} />
|
||||||
|
<span>Copy All</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
onClick={toggleFocusMode}
|
||||||
className={`write-copy-btn ${copied ? "copied" : ""}`}
|
className={`write-focus-btn ${focusMode ? "active" : ""}`}
|
||||||
|
title={focusMode ? "Exit focus mode (Esc)" : "Enter focus mode"}
|
||||||
>
|
>
|
||||||
{copied ? (
|
{focusMode ? (
|
||||||
<>
|
<ArrowsIn size={18} weight="regular" />
|
||||||
<Check size={16} weight="bold" />
|
|
||||||
<span>Copied</span>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<ArrowsOut size={18} weight="regular" />
|
||||||
<CopySimple size={16} />
|
|
||||||
<span>Copy All</span>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Conditionally render textarea or AI chat */}
|
{/* Conditionally render textarea or AI chat */}
|
||||||
@@ -500,9 +607,18 @@ export default function Write() {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Right Sidebar: Frontmatter fields */}
|
{/* Right Sidebar: Frontmatter fields */}
|
||||||
<aside className="write-sidebar-right">
|
<aside
|
||||||
|
className={`write-sidebar-right ${frontmatterCollapsed ? "collapsed" : ""}`}
|
||||||
|
>
|
||||||
<div className="write-sidebar-header">
|
<div className="write-sidebar-header">
|
||||||
<span className="write-sidebar-title">Frontmatter</span>
|
<span className="write-sidebar-title">Frontmatter</span>
|
||||||
|
<button
|
||||||
|
onClick={toggleFrontmatter}
|
||||||
|
className="write-sidebar-toggle"
|
||||||
|
title={frontmatterCollapsed ? "Expand" : "Collapse"}
|
||||||
|
>
|
||||||
|
<SidebarSimple size={16} weight="regular" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="write-fields">
|
<div className="write-fields">
|
||||||
|
|||||||
@@ -3625,6 +3625,46 @@ body {
|
|||||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Search result highlighting on destination page */
|
||||||
|
.search-highlight {
|
||||||
|
background-color: var(--search-highlight-bg);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 1px 2px;
|
||||||
|
margin: 0 -2px;
|
||||||
|
transition: background-color 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-highlight-active {
|
||||||
|
background-color: var(--search-highlight-active);
|
||||||
|
animation: search-highlight-pulse 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes search-highlight-pulse {
|
||||||
|
0% { background-color: var(--accent); }
|
||||||
|
100% { background-color: var(--search-highlight-active); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme-specific search highlight colors */
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--search-highlight-bg: rgba(250, 250, 250, 0.15);
|
||||||
|
--search-highlight-active: rgba(250, 250, 250, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
--search-highlight-bg: rgba(17, 17, 17, 0.08);
|
||||||
|
--search-highlight-active: rgba(17, 17, 17, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="tan"] {
|
||||||
|
--search-highlight-bg: rgba(139, 115, 85, 0.15);
|
||||||
|
--search-highlight-active: rgba(139, 115, 85, 0.30);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="cloud"] {
|
||||||
|
--search-highlight-bg: rgba(100, 116, 139, 0.12);
|
||||||
|
--search-highlight-active: rgba(100, 116, 139, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile responsive search modal */
|
/* Mobile responsive search modal */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.search-modal-backdrop {
|
.search-modal-backdrop {
|
||||||
@@ -5528,11 +5568,271 @@ body {
|
|||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sidebar Header with Toggle */
|
||||||
|
.write-sidebar-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Toggle Button */
|
||||||
|
.write-sidebar-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-sidebar-toggle:hover {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Layout */
|
||||||
|
.write-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus Mode Toggle Button */
|
||||||
|
.write-focus-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-focus-btn:hover {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-focus-btn.active {
|
||||||
|
background: var(--text-primary);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
border-color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Collapsed State */
|
||||||
|
.write-layout.sidebar-collapsed {
|
||||||
|
grid-template-columns: 56px 1fr 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-sidebar-left.collapsed {
|
||||||
|
width: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-sidebar-left.collapsed .write-logo-link span,
|
||||||
|
.write-sidebar-left.collapsed .write-nav-label,
|
||||||
|
.write-sidebar-left.collapsed .write-nav-item span,
|
||||||
|
.write-sidebar-left.collapsed .write-warning {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-sidebar-left.collapsed .write-sidebar-header {
|
||||||
|
padding: 1rem 0.75rem 0.75rem;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-sidebar-left.collapsed .write-logo-link {
|
||||||
|
padding: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-sidebar-left.collapsed .write-nav {
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-sidebar-left.collapsed .write-nav-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-sidebar-left.collapsed .write-nav-item {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Frontmatter Toggle Button */
|
||||||
|
.write-frontmatter-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-frontmatter-btn:hover {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-frontmatter-btn.active {
|
||||||
|
background: var(--text-primary);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
border-color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right Sidebar Collapsed State */
|
||||||
|
.write-sidebar-right.collapsed {
|
||||||
|
width: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-sidebar-right.collapsed .write-fields {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-sidebar-right.collapsed .write-sidebar-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-sidebar-right.collapsed .write-sidebar-header {
|
||||||
|
padding: 1rem 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus Mode State - shows collapsed left sidebar with toggleable frontmatter sidebar */
|
||||||
|
.write-layout.focus-mode {
|
||||||
|
grid-template-columns: 56px 1fr 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode.frontmatter-collapsed {
|
||||||
|
grid-template-columns: 56px 1fr 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* In focus mode, force left sidebar to collapsed state */
|
||||||
|
.write-layout.focus-mode .write-sidebar-left {
|
||||||
|
width: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-sidebar-left .write-logo-link span,
|
||||||
|
.write-layout.focus-mode .write-sidebar-left .write-nav-label,
|
||||||
|
.write-layout.focus-mode .write-sidebar-left .write-nav-item span,
|
||||||
|
.write-layout.focus-mode .write-sidebar-left .write-warning {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-sidebar-left .write-sidebar-header {
|
||||||
|
padding: 1rem 0.75rem 0.75rem;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-sidebar-left .write-logo-link {
|
||||||
|
padding: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-sidebar-left .write-nav {
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-sidebar-left .write-nav-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-sidebar-left .write-nav-item {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-main-header {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-main-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-header-left {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-header-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-copy-btn {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-copy-btn:hover {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-textarea {
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write-layout.focus-mode .write-main-footer {
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 0.5rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive: Tablet */
|
/* Responsive: Tablet */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.write-layout {
|
.write-layout {
|
||||||
grid-template-columns: 200px 1fr 240px;
|
grid-template-columns: 200px 1fr 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.write-layout.sidebar-collapsed {
|
||||||
|
grid-template-columns: 56px 1fr 240px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive: Mobile */
|
/* Responsive: Mobile */
|
||||||
@@ -8114,6 +8414,7 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dashboard Header */
|
/* Dashboard Header */
|
||||||
@@ -8344,6 +8645,9 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.625rem;
|
padding: 0.625rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dashboard List View */
|
/* Dashboard List View */
|
||||||
@@ -10199,7 +10503,9 @@ body {
|
|||||||
.dashboard-write-section {
|
.dashboard-write-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 500px;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-write-header {
|
.dashboard-write-header {
|
||||||
@@ -10259,6 +10565,7 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-write-main {
|
.dashboard-write-main {
|
||||||
@@ -10328,7 +10635,8 @@ body {
|
|||||||
background-color: var(--background-secondary);
|
background-color: var(--background-secondary);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-write-sidebar-header {
|
.dashboard-write-sidebar-header {
|
||||||
@@ -10337,12 +10645,14 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-write-fields {
|
.dashboard-write-fields {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.write-fields-section {
|
.write-fields-section {
|
||||||
@@ -10466,6 +10776,146 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dashboard Write Focus Mode Toggle Button */
|
||||||
|
.dashboard-action-btn.focus-toggle {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-action-btn.focus-toggle span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-action-btn.focus-toggle.active {
|
||||||
|
background: var(--text-primary);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
border-color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Frontmatter Toggle Button */
|
||||||
|
.dashboard-action-btn.frontmatter-toggle {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-action-btn.frontmatter-toggle.active {
|
||||||
|
background: var(--text-primary);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
border-color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Write Sidebar Toggle */
|
||||||
|
.dashboard-write-sidebar-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-sidebar-toggle:hover {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Write Sidebar Header with Toggle */
|
||||||
|
.dashboard-write-sidebar-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Write Sidebar Collapsed State */
|
||||||
|
.dashboard-write-sidebar.collapsed {
|
||||||
|
width: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-sidebar.collapsed .dashboard-write-fields {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-sidebar.collapsed .dashboard-write-sidebar-header span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-sidebar.collapsed .dashboard-write-sidebar-header {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Write Focus Mode State */
|
||||||
|
.dashboard-write-section.focus-mode {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-section.focus-mode .dashboard-write-header {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-section.focus-mode .dashboard-write-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-section.focus-mode .dashboard-write-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-section.focus-mode .dashboard-write-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-section.focus-mode .dashboard-write-main {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-section.focus-mode .dashboard-write-sidebar {
|
||||||
|
width: 280px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-section.focus-mode.frontmatter-collapsed .dashboard-write-sidebar {
|
||||||
|
width: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-section.focus-mode .dashboard-write-textarea {
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-section.focus-mode .dashboard-write-footer {
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 0.5rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-write-section.focus-mode .dashboard-write-warning {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Config field note styling */
|
/* Config field note styling */
|
||||||
.config-field-note {
|
.config-field-note {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
Reference in New Issue
Block a user