Compare commits

...

15 Commits

Author SHA1 Message Date
Dwindi Ramadhana
1047642909 fix(diagram): correct flowchart template edge connections to match diagram logic 2026-06-14 20:06:01 +07:00
Dwindi Ramadhana
8caf6fbba5 fix(diagram): add missing Copy icon import for DiagramEditor export section 2026-06-14 19:57:25 +07:00
Dwindi Ramadhana
3727ace366 fix(build): remove unused imports in DiagramEditor and ReactFlowEditor 2026-06-14 18:45:14 +07:00
Dwindi Ramadhana
81c399ab42 fix(build): add missing DiagramEditor and FullscreenAdBanner files to git tracking 2026-06-14 16:05:40 +07:00
Dwindi Ramadhana
518b0127d2 fix(diagram): resolve DOMURL undefined error causing build failure 2026-06-14 15:55:08 +07:00
Dwindi Ramadhana
deb2bf0b8a fix(ads): adjust internal iframe styles to minimize bottom gap 2026-06-14 15:43:42 +07:00
Dwindi Ramadhana
e4ccff4bbf chore: update TODO.md to mark Markdown Editor MVP tasks as completed 2026-06-14 13:30:37 +07:00
Dwindi Ramadhana
0b1cfbdabd fix(layout): remove global overflow-x-hidden to restore sticky sidebar behavior 2026-06-14 13:15:51 +07:00
Dwindi Ramadhana
c580a5f7b0 fix(markdown-editor): align max width and formatting across read and edit views 2026-06-14 12:35:18 +07:00
Dwindi Ramadhana
9232052508 chore(ads): update adsterra anti-adblock custom domain to downconvenientmagnetic 2026-06-14 12:32:16 +07:00
Dwindi Ramadhana
fcbfeb44f8 fix(build): remove unused import EyeOff to pass strict CI build 2026-06-14 11:52:19 +07:00
Dwindi Ramadhana
dd0b98e077 chore: remove metadata files created by SSD 2026-06-14 01:01:03 +07:00
Dwindi Ramadhana
dcba58c2b9 chore: ignore OS meta files and update changelog for WYSIWYG and Object Editor updates 2026-06-14 00:56:38 +07:00
Dwindi Ramadhana
7b3dce06ea refactor(markdown-editor): migrate to tiptap for WYSIWYG editing, standardize UI spacing, and update export engine 2026-06-14 00:54:01 +07:00
Dwindi Ramadhana
13e694aa82 feat: add multidimensional search, preview mode prioritization, and collapse/expand all to Object Editor 2026-06-13 20:11:07 +07:00
29 changed files with 5470 additions and 2485 deletions

Binary file not shown.

BIN
._package-lock.json generated

Binary file not shown.

Binary file not shown.

3
.gitignore vendored
View File

@@ -11,7 +11,6 @@ node_modules/
/backup /backup
# Misc # Misc
.DS_Store
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
@@ -46,4 +45,6 @@ pids
*.swo *.swo
# OS generated files # OS generated files
.DS_Store
Thumbs.db Thumbs.db
._*

View File

@@ -131,37 +131,36 @@ Build a comprehensive suite of developer tools with a focus on:
--- ---
#### Priority 3: AdSense Integration 💵 #### Priority 3: Adsterra Integration 💵
**Status:** ⏳ In Progress (Awaiting approval) **Status:** ✅ Completed
**Timeline:** 1 day **Timeline:** 1 day
**Impact:** HIGH - Start earning revenue **Impact:** HIGH - Start earning revenue
**Steps:** **Steps:**
1. Apply for Google AdSense account 1. Apply for Adsterra account
2. Add AdSense script to `index.html` 2. Add Adsterra anti-adblock script to `index.html` and components
3. Create ad units in AdSense dashboard 3. Create ad units in Adsterra dashboard
4. Implement ad components with AdSense code 4. Implement ad components with Adsterra code
5. Test ad display and responsiveness 5. Test ad display and responsiveness
6. Monitor ad performance
**Ad Units Needed:** **Ad Units Needed:**
- Desktop Sidebar 1 (300x250) - Desktop Sidebar 1 (300x250)
- Desktop Sidebar 2 (300x250) - Desktop Sidebar 2 (300x250)
- Desktop Sidebar 3 (300x250) - Desktop Sidebar 3 (300x250)
- Mobile Bottom Banner (320x50) - Mobile Bottom Banner (320x50)
**Compliance:** **Compliance:**
- Add Privacy Policy page - Add Privacy Policy page
- Add Terms of Service page - Add Terms of Service page
- Cookie consent banner (if required) - Cookie consent banner
- GDPR compliance (if applicable) - GDPR compliance
--- ---
### Phase 2: Content Expansion (Week 3-6) ### Phase 2: Content Expansion (Week 3-6)
#### Markdown Editor 📝 #### Markdown Editor 📝
**Status:** ✅ Completed (October 22, 2025) **Status:** ✅ Completed (June 14, 2026)
**Timeline:** 1-2 weeks **Timeline:** 1-2 weeks
**Impact:** HIGH - Major new feature, attracts new users **Impact:** HIGH - Major new feature, attracts new users
@@ -170,29 +169,25 @@ Build a comprehensive suite of developer tools with a focus on:
- Create New (empty/sample) - Create New (empty/sample)
- URL Import (fetch markdown from GitHub, Gist, etc.) - URL Import (fetch markdown from GitHub, Gist, etc.)
- Paste (markdown, HTML auto-convert, plain text) - Paste (markdown, HTML auto-convert, plain text)
- Open Files (.md, .txt, .html, .docx) - Open Files (.md, .txt)
- **Editor:** - **Editor:**
- CodeMirror with markdown syntax highlighting - Tiptap-powered WYSIWYG Rich Text Editor
- Split view (editor + live preview) - Fallback Raw Markdown CodeMirror Editor
- View modes: Split, Editor Only, Preview Only, Fullscreen - View modes: Read, Edit, Markdown
- Markdown toolbar (Bold, Italic, Headers, Links, Images, Code, Lists, Tables) - Toolbar (Bold, Italic, Headers, Links, Images, Code, Lists, Tables)
- Line numbers
- Word count & statistics - Word count & statistics
- **Preview:** - **Preview:**
- Live rendering (marked + DOMPurify) - Auto-generated HTML parsing
- Syntax highlighting for code blocks (highlight.js) - Syntax highlighting for code blocks (highlight.js)
- GitHub Flavored Markdown support
- Table of Contents auto-generation
- Mermaid diagram rendering (in preview)
- **Export:** - **Export:**
- Markdown (.md) - Standard, GFM, CommonMark - Markdown (.md)
- HTML (.html) - Standalone with CSS - HTML (.html)
- HTML Content Body
- Plain Text (.txt) - Plain Text (.txt)
- PDF (.pdf) - via html2pdf - PDF (.pdf) - via html2pdf
- DOCX (.docx) - via docx library
**Advanced Features (Post-MVP):** **Advanced Features (Post-MVP):**
- Tables support (GitHub-style) - Tables support (GitHub-style)

368
TODO.md
View File

@@ -179,310 +179,168 @@
--- ---
### 💵 Priority 3: AdSense Integration (1 day) - ⏳ IN PROGRESS ### 💵 Priority 3: Adsterra Integration (1 day) - ✅ COMPLETED
#### AdSense Setup #### Adsterra Setup
- [ ] Apply for Google AdSense account - [x] Apply for Adsterra publisher account
- [ ] Provide website URL - [x] Add website URL
- [ ] Provide contact information - [x] Receive approval
- [ ] Wait for approval (can take 1-3 days)
- [ ] Verify site ownership (add verification code)
#### Ad Units Creation #### Ad Units Creation
- [ ] Log in to AdSense dashboard - [x] Create ad unit: Desktop Sidebar 1 (300x250)
- [ ] Create ad unit: Desktop Sidebar 1 (300x250) - [x] Create ad unit: Desktop Sidebar 2 (300x250)
- [ ] Create ad unit: Desktop Sidebar 2 (300x250) - [x] Create ad unit: Mobile Bottom Banner (320x50)
- [ ] Create ad unit: Desktop Sidebar 3 (300x250) - [x] Copy ad unit codes
- [ ] Create ad unit: Mobile Bottom Banner (320x50) - [x] Request Anti-Adblock custom domain
- [ ] Copy ad unit codes
#### Implementation #### Implementation
- [ ] Add AdSense script to `public/index.html` - [x] Update `AdBlock.jsx` with Adsterra iframe code
```html - [x] Update `MobileAdBanner.jsx` with Adsterra iframe code
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXX" - [x] Update custom Anti-Adblock domain (`downconvenientmagnetic.com`)
crossorigin="anonymous"></script>
```
- [ ] Update `AdBlock.jsx` with AdSense code
```jsx
<ins className="adsbygoogle"
style={{ display: 'block' }}
data-ad-client="ca-pub-XXXXXXXX"
data-ad-slot="XXXXXXXXXX"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
```
- [ ] Update `MobileAdBanner.jsx` with AdSense code
- [ ] Initialize ads: `(adsbygoogle = window.adsbygoogle || []).push({});`
#### Testing #### Testing
- [ ] Test ad display on desktop - [x] Test ad display on desktop
- [ ] Test ad display on mobile - [x] Test ad display on mobile
- [ ] Verify ads load correctly - [x] Verify ads load correctly
- [ ] Check for console errors - [x] Check for console errors
- [ ] Test with ad blocker (should show message) - [x] Test on different devices
- [ ] Test on different browsers (Chrome, Firefox, Safari)
- [ ] Test on different devices
#### Monitoring #### Monitoring
- [ ] Set up AdSense reporting - [x] Monitor ad impressions
- [ ] Monitor ad impressions - [x] Monitor ad clicks
- [ ] Monitor ad clicks - [x] Track CTR (Click-Through Rate)
- [ ] Monitor ad revenue
- [ ] Track CTR (Click-Through Rate)
- [ ] Identify best-performing ad units
#### Compliance #### Compliance
- [ ] Create Privacy Policy page - [x] Create Privacy Policy page
- [ ] Data collection disclosure - [x] Data collection disclosure
- [ ] Cookie usage disclosure - [x] Cookie usage disclosure
- [ ] Third-party services (AdSense) - [x] Third-party services (Adsterra)
- [ ] User rights (GDPR) - [x] User rights (GDPR)
- [ ] Create Terms of Service page - [x] Create Terms of Service page
- [ ] Acceptable use policy - [x] Acceptable use policy
- [ ] Disclaimer - [x] Disclaimer
- [ ] Limitation of liability - [x] Limitation of liability
- [ ] Add cookie consent banner (if required) - [x] Add cookie consent banner
- [ ] Show on first visit - [x] Add "Privacy Policy" link in footer
- [ ] Allow accept/decline - [x] Add "Terms of Service" link in footer
- [ ] Store preference
- [ ] Add "About Ads" link in footer
- [ ] Add "Privacy Policy" link in footer
- [ ] Add "Terms of Service" link in footer
#### Optimization
- [ ] Test different ad placements
- [ ] Test different ad sizes
- [ ] Monitor ad viewability
- [ ] Optimize for higher CTR
- [ ] A/B test ad positions (optional)
--- ---
## 📋 Phase 2: Content Expansion ## 📋 Phase 2: Content Expansion
### 📝 Markdown Editor - MVP (1-2 weeks) - ✅ COMPLETED (Oct 22, 2025) ### 📝 Markdown Editor - MVP (1-2 weeks) - ✅ COMPLETED
#### Planning #### Planning
- [ ] Finalize feature list for MVP - [x] Finalize feature list for MVP
- [ ] Design UI mockup (split view) - [x] Design UI mockup (WYSIWYG layout)
- [ ] Plan component structure - [x] Plan component structure
- [ ] Choose markdown parser (marked vs markdown-it) - [x] Implement Tiptap integration
- [ ] Plan export formats - [x] Plan export formats
#### Project Setup #### Project Setup
- [ ] Create `MarkdownEditor.jsx` page - [x] Create `MarkdownEditor.jsx` page
- [ ] Set up routing (`/markdown-editor`) - [x] Create `RichMarkdownEditor.js` component
- [ ] Add to navigation menu - [x] Set up routing (`/markdown-editor`)
- [ ] Add to homepage tools list - [x] Add to navigation menu
- [x] Add to homepage tools list
#### Input Section #### Input Section
- [ ] Implement Create New tab - [x] Implement Create New tab
- [ ] Start Empty button - [x] Start Empty button
- [ ] Load Sample button (with example markdown) - [x] Load Sample button (with example markdown)
- [ ] Tip box - [x] Tip box
- [ ] Implement URL tab - [x] Implement URL tab
- [ ] Use AdvancedURLFetch component - [x] Use AdvancedURLFetch component
- [ ] Support GitHub raw URLs - [x] Support GitHub raw URLs
- [ ] Support Gist URLs - [x] Support Gist URLs
- [ ] Test with various markdown sources - [x] Test with various markdown sources
- [ ] Implement Paste tab - [x] Implement Paste tab
- [ ] CodeMirror editor - [x] CodeMirror editor
- [ ] Markdown syntax highlighting - [x] Markdown syntax highlighting
- [ ] Auto-detect markdown - [x] Parse button
- [ ] Parse button - [x] Implement Open tab
- [ ] Collapse after parse - [x] Support .md files
- [ ] Implement Open tab - [x] Support .txt files
- [ ] Support .md files - [x] Auto-load on file selection
- [ ] Support .txt files
- [ ] Support .html files (convert to markdown)
- [ ] Support .docx files (convert to markdown)
- [ ] Auto-load on file selection
#### Editor Section #### Editor Section
- [ ] Set up CodeMirror for markdown - [x] Implement WYSIWYG Editor (Tiptap)
- [ ] Install @codemirror/lang-markdown - [x] Install `@tiptap/react` and `tiptap-markdown`
- [ ] Configure markdown mode - [x] Add standard text formatting (bold, italic, strike)
- [ ] Add syntax highlighting - [x] Add block formatting (headers, quotes, lists)
- [ ] Add line numbers - [x] Add inline code and code block extensions
- [ ] Add line wrapping - [x] Set up Lowlight syntax highlighting
- [ ] Implement split view layout - [x] Implement view mode toggle
- [ ] Editor pane (left) - [x] Read mode (Clean preview default)
- [ ] Preview pane (right) - [x] Edit mode (WYSIWYG Tiptap)
- [ ] Resizable divider (optional) - [x] Markdown mode (Raw CodeMirror)
- [ ] Implement view mode toggle - [x] Fullscreen mode
- [ ] Split view (default) - [x] Add editor features
- [ ] Editor only - [x] Word count
- [ ] Preview only - [x] Character count
- [ ] Fullscreen mode - [x] Line count
- [ ] Add markdown toolbar - [x] Reading time estimate
- [ ] Bold button (Ctrl+B)
- [ ] Italic button (Ctrl+I)
- [ ] H1 button
- [ ] H2 button
- [ ] H3 button
- [ ] Link button (Ctrl+K)
- [ ] Image button
- [ ] Code button (Ctrl+`)
- [ ] Quote button
- [ ] Unordered list button
- [ ] Ordered list button
- [ ] Table button
- [ ] Add editor features
- [ ] Word count
- [ ] Character count
- [ ] Line count
- [ ] Reading time estimate
#### Preview Section #### Preview Section
- [ ] Set up markdown parser (marked) - [x] Build robust HTML to Markdown / Markdown to HTML sync
- [ ] Install marked - [x] Set up markdown fallback parser (marked)
- [ ] Install DOMPurify - [x] GitHub Flavored Markdown support (Tables, task lists)
- [ ] Configure marked options - [x] Custom code block rendering with Copy button in Read mode
- [ ] Implement live preview
- [ ] Real-time rendering
- [ ] Debounce for performance
- [ ] Scroll sync (optional)
- [ ] Add syntax highlighting for code blocks
- [ ] Install highlight.js
- [ ] Configure languages
- [ ] Apply highlighting
- [ ] Add GitHub Flavored Markdown support
- [ ] Tables
- [ ] Strikethrough
- [ ] Task lists
- [ ] Autolinks
- [ ] Implement Table of Contents
- [ ] Auto-generate from headers
- [ ] Clickable links
- [ ] Collapsible (optional)
- [ ] Add mermaid diagram rendering
- [ ] Install mermaid
- [ ] Detect mermaid code blocks
- [ ] Render diagrams
- [ ] Error handling
#### Export Section #### Export Section
- [ ] Create collapsible export section - [x] Create collapsible export section
- [ ] Implement Markdown export - [x] Implement Markdown export
- [ ] Standard Markdown - [x] Copy to clipboard
- [ ] GitHub Flavored Markdown - [x] Download as .md file
- [ ] CommonMark - [x] Implement HTML export
- [ ] Copy to clipboard - [x] Standalone HTML with CSS
- [ ] Download as .md file - [x] Download as .html file
- [ ] Implement HTML export - [x] Implement HTML Content export
- [ ] Standalone HTML with CSS - [x] Strip React/Tailwind wrapper classes
- [ ] Inline styles - [x] Download body HTML only
- [ ] Include syntax highlighting CSS - [x] Implement Plain Text export
- [ ] Copy to clipboard - [x] Strip markdown syntax via regex
- [ ] Download as .html file - [x] Download as .txt file
- [ ] Implement Plain Text export - [x] Implement PDF export
- [ ] Strip all formatting - [x] Install html2pdf.js
- [ ] Copy to clipboard - [x] Inject CSS print media rules to prevent pre overflow
- [ ] Download as .txt file - [x] Download as .pdf file
- [ ] Implement PDF export
- [ ] Install html2pdf.js
- [ ] Convert HTML to PDF
- [ ] Maintain formatting
- [ ] Download as .pdf file
- [ ] Implement DOCX export
- [ ] Install docx library
- [ ] Convert markdown to DOCX
- [ ] Maintain formatting
- [ ] Download as .docx file
#### Conversion Features
- [ ] HTML to Markdown conversion
- [ ] Install turndown
- [ ] Convert on paste (if HTML detected)
- [ ] Convert on file open (.html)
- [ ] DOCX to Markdown conversion
- [ ] Install mammoth.js
- [ ] Convert on file open (.docx)
- [ ] Extract text and formatting
#### Usage Tips
- [ ] Create collapsible Usage Tips section
- [ ] Add Input Methods tips
- [ ] Add Editor Features tips
- [ ] Add Markdown Syntax tips
- [ ] Add Export Options tips
- [ ] Add Data Privacy tips
#### Data Loss Prevention #### Data Loss Prevention
- [ ] Implement hasUserData() function - [x] Implement `hasUserData()` function
- [ ] Implement hasModifiedData() function - [x] Implement `hasModifiedData()` function
- [ ] Add confirmation modal for tab changes - [x] Add confirmation modal for tab changes
- [ ] Add confirmation for Create New buttons - [x] Add confirmation for Create New buttons
#### Testing #### Testing
- [ ] Test all input methods - [x] Test all input methods
- [ ] Test markdown rendering - [x] Test Tiptap to Markdown serialization
- [ ] Test all export formats - [x] Test all export formats
- [ ] Test HTML to Markdown conversion - [x] Test code syntax highlighting
- [ ] Test DOCX import - [x] Test view mode toggle
- [ ] Test mermaid diagrams - [x] Test toolbar buttons
- [ ] Test code syntax highlighting - [x] Test responsive design
- [ ] Test Table of Contents - [x] Test dark mode
- [ ] Test view mode toggle
- [ ] Test toolbar buttons
- [ ] Test keyboard shortcuts
- [ ] Test responsive design
- [ ] Test dark mode
- [ ] Test on mobile devices
#### Documentation
- [ ] Add to EDITOR_TOOL_GUIDE.md
- [ ] Create user guide
- [ ] Add screenshots
- [ ] Create tutorial video (optional)
--- ---
### 📝 Markdown Editor - Post-MVP (Future) ### 📝 Markdown Editor - Post-MVP (Future)
#### Advanced Markdown Features #### Advanced Markdown Features
- [ ] Add table support (GitHub-style)
- [ ] Add task lists (checkboxes)
- [ ] Add footnotes support - [ ] Add footnotes support
- [ ] Add emoji support (:smile:) - [ ] Add emoji support (WYSIWYG picker)
- [ ] Add math equations (KaTeX) - [ ] Add math equations (KaTeX)
- [ ] Install katex - [ ] Add mermaid diagram rendering
- [ ] Detect math blocks - [ ] Implement Table of Contents auto-generation
- [ ] Render equations
#### Templates
- [ ] Create README.md template
- [ ] Create Documentation template
- [ ] Create Blog post template
- [ ] Create Meeting notes template
- [ ] Create Project proposal template
- [ ] Add template selector UI
- [ ] Allow custom templates
#### Utilities #### Utilities
- [ ] Add markdown linter - [ ] Add markdown linter
- [ ] Check for common issues
- [ ] Suggest improvements
- [ ] Show warnings
- [ ] Add link checker
- [ ] Validate URLs
- [ ] Check for broken links
- [ ] Show status
- [ ] Add format beautifier - [ ] Add format beautifier
- [ ] Clean up markdown
- [ ] Consistent formatting
- [ ] Fix indentation
- [ ] Add image optimizer - [ ] Add image optimizer
- [ ] Compress images
- [ ] Convert to base64
- [ ] Optimize for web
#### Enhanced Features #### Enhanced Features
- [ ] Add keyboard shortcuts
- [ ] Add auto-save (localStorage) - [ ] Add auto-save (localStorage)
- [ ] Add export history
- [ ] Add version history - [ ] Add version history
- [ ] Add collaborative editing (future)
--- ---

2425
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,22 +18,37 @@
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@tailwindcss/typography": "^0.5.20",
"@testing-library/jest-dom": "^6.8.0", "@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@tiptap/extension-code-block-lowlight": "^3.26.1",
"@tiptap/extension-image": "^3.26.1",
"@tiptap/extension-link": "^3.26.1",
"@tiptap/extension-table": "^3.26.1",
"@tiptap/extension-table-cell": "^3.26.1",
"@tiptap/extension-table-header": "^3.26.1",
"@tiptap/extension-table-row": "^3.26.1",
"@tiptap/extension-task-item": "^3.26.1",
"@tiptap/extension-task-list": "^3.26.1",
"@tiptap/react": "^3.26.1",
"@tiptap/starter-kit": "^3.26.1",
"@uiw/react-codemirror": "^4.25.1", "@uiw/react-codemirror": "^4.25.1",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",
"dompurify": "^3.3.0", "dompurify": "^3.3.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"html-to-image": "^1.11.13",
"html2pdf.js": "^0.12.1", "html2pdf.js": "^0.12.1",
"js-beautify": "^1.15.4", "js-beautify": "^1.15.4",
"jspdf": "^3.0.3", "jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2", "jspdf-autotable": "^5.0.2",
"lowlight": "^3.3.0",
"lucide-react": "^0.540.0", "lucide-react": "^0.540.0",
"marked": "^16.4.1", "marked": "^16.4.1",
"marked-emoji": "^2.0.1", "marked-emoji": "^2.0.1",
"mermaid": "^11.15.0",
"papaparse": "^5.5.3", "papaparse": "^5.5.3",
"react": "18.3.1", "react": "18.3.1",
"react-diff-view": "^3.3.2", "react-diff-view": "^3.3.2",
@@ -41,9 +56,12 @@
"react-helmet-async": "^2.0.5", "react-helmet-async": "^2.0.5",
"react-router-dom": "6.26.2", "react-router-dom": "6.26.2",
"react-snap": "^1.23.0", "react-snap": "^1.23.0",
"react-zoom-pan-pinch": "^4.0.3",
"reactflow": "^11.11.4", "reactflow": "^11.11.4",
"serialize-javascript": "^6.0.0", "serialize-javascript": "^6.0.0",
"serve": "^14.2.4", "serve": "^14.2.4",
"tailwindcss-typography": "^3.1.0",
"tiptap-markdown": "^0.9.0",
"turndown": "^7.2.1", "turndown": "^7.2.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },

View File

@@ -1,5 +1,22 @@
{ {
"changelog": [ "changelog": [
{
"date": "2026-06-14",
"changes": [
{
"datetime": "2026-06-14T10:00:00+07:00",
"type": "feature",
"title": "Major Markdown Editor Rewrite: WYSIWYG Experience",
"description": "Completely rebuilt the Markdown Editor to feature a true WYSIWYG (What You See Is What You Get) interface using Tiptap. You can now edit rich text visually like a Word document, while seamlessly converting back and forth to raw Markdown and clean HTML."
},
{
"datetime": "2026-06-14T09:30:00+07:00",
"type": "enhancement",
"title": "Object Editor: Data Preview & Multidimensional Search",
"description": "The Object Editor now defaults to a fast Read-Only preview when you paste JSON data. We also added an incredibly powerful multidimensional search bar that instantly filters, highlights, and expands nested nodes matching your query."
}
]
},
{ {
"date": "2026-02-18", "date": "2026-02-18",
"changes": [ "changes": [
@@ -291,4 +308,4 @@
] ]
} }
] ]
} }

View File

@@ -1,29 +1,37 @@
import React, { useEffect, Suspense, lazy } from 'react'; import React, { useEffect, Suspense, lazy } from "react";
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import {
import { HelmetProvider } from 'react-helmet-async'; BrowserRouter as Router,
import Layout from './components/Layout'; Routes,
import ErrorBoundary from './components/ErrorBoundary'; Route,
import Loading from './components/Loading'; Navigate,
import { initGA } from './utils/analytics'; } from "react-router-dom";
import { HelmetProvider } from "react-helmet-async";
import Layout from "./components/Layout";
import ErrorBoundary from "./components/ErrorBoundary";
import Loading from "./components/Loading";
import { initGA } from "./utils/analytics";
import './index.css'; import "./index.css";
const Home = lazy(() => import('./pages/Home')); const Home = lazy(() => import("./pages/Home"));
const UrlTool = lazy(() => import('./pages/UrlTool')); const UrlTool = lazy(() => import("./pages/UrlTool"));
const Base64Tool = lazy(() => import('./pages/Base64Tool')); const Base64Tool = lazy(() => import("./pages/Base64Tool"));
const BeautifierTool = lazy(() => import('./pages/BeautifierTool')); const BeautifierTool = lazy(() => import("./pages/BeautifierTool"));
const DiffTool = lazy(() => import('./pages/DiffTool')); const DiffTool = lazy(() => import("./pages/DiffTool"));
const TextLengthTool = lazy(() => import('./pages/TextLengthTool')); const TextLengthTool = lazy(() => import("./pages/TextLengthTool"));
const ObjectEditor = lazy(() => import('./pages/ObjectEditor')); const ObjectEditor = lazy(() => import("./pages/ObjectEditor"));
const TableEditor = lazy(() => import('./pages/TableEditor')); const TableEditor = lazy(() => import("./pages/TableEditor"));
const InvoiceEditor = lazy(() => import('./pages/InvoiceEditor')); const InvoiceEditor = lazy(() => import("./pages/InvoiceEditor"));
const MarkdownEditor = lazy(() => import('./pages/MarkdownEditor')); const MarkdownEditor = lazy(() => import("./pages/MarkdownEditor"));
const InvoicePreview = lazy(() => import('./pages/InvoicePreview')); const DiagramEditor = lazy(() => import("./pages/DiagramEditor"));
const InvoicePreviewMinimal = lazy(() => import('./pages/InvoicePreviewMinimal')); const InvoicePreview = lazy(() => import("./pages/InvoicePreview"));
const ReleaseNotes = lazy(() => import('./pages/ReleaseNotes')); const InvoicePreviewMinimal = lazy(
const TermsOfService = lazy(() => import('./pages/TermsOfService')); () => import("./pages/InvoicePreviewMinimal"),
const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy')); );
const NotFound = lazy(() => import('./pages/NotFound')); const ReleaseNotes = lazy(() => import("./pages/ReleaseNotes"));
const TermsOfService = lazy(() => import("./pages/TermsOfService"));
const PrivacyPolicy = lazy(() => import("./pages/PrivacyPolicy"));
const NotFound = lazy(() => import("./pages/NotFound"));
function App() { function App() {
// Initialize Google Analytics on app startup // Initialize Google Analytics on app startup
@@ -37,25 +45,32 @@ function App() {
<Router> <Router>
<Layout> <Layout>
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/url" element={<UrlTool />} /> <Route path="/url" element={<UrlTool />} />
<Route path="/base64" element={<Base64Tool />} /> <Route path="/base64" element={<Base64Tool />} />
<Route path="/beautifier" element={<BeautifierTool />} /> <Route path="/beautifier" element={<BeautifierTool />} />
<Route path="/diff" element={<DiffTool />} /> <Route path="/diff" element={<DiffTool />} />
<Route path="/text-length" element={<TextLengthTool />} /> <Route path="/text-length" element={<TextLengthTool />} />
<Route path="/object-editor" element={<ObjectEditor />} /> <Route path="/object-editor" element={<ObjectEditor />} />
<Route path="/table-editor" element={<TableEditor />} /> <Route path="/table-editor" element={<TableEditor />} />
<Route path="/invoice-editor" element={<InvoiceEditor />} /> <Route path="/invoice-editor" element={<InvoiceEditor />} />
<Route path="/markdown-editor" element={<MarkdownEditor />} /> <Route path="/markdown-editor" element={<MarkdownEditor />} />
<Route path="/invoice-preview" element={<InvoicePreview />} /> <Route path="/diagram-editor" element={<DiagramEditor />} />
<Route path="/invoice-preview-minimal" element={<InvoicePreviewMinimal />} /> <Route path="/invoice-preview" element={<InvoicePreview />} />
<Route path="/whats-new" element={<Navigate to="/release-notes" replace />} /> <Route
<Route path="/release-notes" element={<ReleaseNotes />} /> path="/invoice-preview-minimal"
<Route path="/privacy" element={<PrivacyPolicy />} /> element={<InvoicePreviewMinimal />}
<Route path="/terms" element={<TermsOfService />} /> />
<Route path="*" element={<NotFound />} /> <Route
</Routes> path="/whats-new"
element={<Navigate to="/release-notes" replace />}
/>
<Route path="/release-notes" element={<ReleaseNotes />} />
<Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/terms" element={<TermsOfService />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense> </Suspense>
</Layout> </Layout>
</Router> </Router>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,7 +3,7 @@ import React, { useEffect, useRef } from "react";
const AdBlock = ({ const AdBlock = ({
className = "", className = "",
adKey = "e0ca7c61c83457f093bbc2e261b43d31", adKey = "e0ca7c61c83457f093bbc2e261b43d31",
adDomain = "www.highperformanceformat.com", adDomain = "downconvenientmagnetic.com",
}) => { }) => {
const iframeRef = useRef(null); const iframeRef = useRef(null);

View File

@@ -0,0 +1,34 @@
import React from "react";
import { NodeViewWrapper, NodeViewContent } from "@tiptap/react";
const CodeBlockComponent = ({ node, updateAttributes, extension }) => {
return (
<NodeViewWrapper className="code-block-wrapper relative bg-[#0d1117] rounded-md overflow-hidden mb-[0.65em]">
<div className="code-block-header flex justify-between items-center px-4 py-2 bg-[#161b22] border border-[#30363d] border-b-0 rounded-t-md text-xs font-mono">
<select
contentEditable={false}
value={node.attrs.language || "text"}
onChange={(event) =>
updateAttributes({ language: event.target.value })
}
className="bg-transparent text-[#8b949e] border-none outline-none focus:ring-0 uppercase tracking-wider cursor-pointer"
>
<option value="text">text</option>
<option value="javascript">javascript</option>
<option value="typescript">typescript</option>
<option value="html">html</option>
<option value="css">css</option>
<option value="json">json</option>
<option value="bash">bash</option>
<option value="python">python</option>
<option value="sql">sql</option>
</select>
</div>
<pre className="!mt-0 !rounded-t-none !bg-transparent !border-[#30363d] !p-4 !text-[#e6edf3]">
<NodeViewContent as="code" />
</pre>
</NodeViewWrapper>
);
};
export default CodeBlockComponent;

View File

@@ -0,0 +1,107 @@
import React, { useEffect, useRef } from "react";
const FullscreenAdBanner = () => {
const desktopIframeRef = useRef(null);
const mobileIframeRef = useRef(null);
useEffect(() => {
// Initialize Desktop/Tablet Ad (728x90)
if (desktopIframeRef.current) {
const iframe = desktopIframeRef.current;
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(`
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; padding: 0; display: block; background: transparent; overflow: hidden; height: 100vh; }
</style>
</head>
<body>
<script type="text/javascript">
atOptions = {
'key' : '5d1186bf7f51a6e8732651b00fefc51b',
'format' : 'iframe',
'height' : 90,
'width' : 728,
'params' : {}
};
</script>
<script type="text/javascript" src="https://downconvenientmagnetic.com/5d1186bf7f51a6e8732651b00fefc51b/invoke.js"></script>
</body>
</html>
`);
doc.close();
}
// Initialize Mobile Ad (320x50) using the existing mobile key
if (mobileIframeRef.current) {
const iframe = mobileIframeRef.current;
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(`
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; padding: 0; display: block; background: transparent; overflow: hidden; height: 100vh; }
</style>
</head>
<body>
<script type="text/javascript">
atOptions = {
'key' : '2965bcf877388cafa84160592c550f5a',
'format' : 'iframe',
'height' : 50,
'width' : 320,
'params' : {}
};
</script>
<script type="text/javascript" src="https://downconvenientmagnetic.com/2965bcf877388cafa84160592c550f5a/invoke.js"></script>
</body>
</html>
`);
doc.close();
}
}, []);
return (
<>
{/* Desktop & Tablet View (>= 768px) */}
<div className="absolute bottom-0 left-0 right-0 z-50 justify-center bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 py-2 shadow-lg hidden md:flex">
<iframe
ref={desktopIframeRef}
style={{
width: "728px",
height: "90px",
border: "none",
maxWidth: "100%",
}}
title="Fullscreen Advertisement Desktop"
sandbox="allow-scripts allow-same-origin"
/>
</div>
{/* Mobile View (< 768px) */}
<div className="absolute bottom-0 left-0 right-0 z-50 flex justify-center items-end bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 shadow-lg md:hidden h-[51px]">
<iframe
ref={mobileIframeRef}
style={{
width: "320px",
height: "50px",
border: "none",
maxWidth: "100%",
display: "block",
}}
title="Fullscreen Advertisement Mobile"
sandbox="allow-scripts allow-same-origin"
/>
</div>
</>
);
};
export default FullscreenAdBanner;

View File

@@ -1,25 +1,37 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from "react";
import { useLocation } from 'react-router-dom'; import { useLocation } from "react-router-dom";
import ToolSidebar from './ToolSidebar'; import ToolSidebar from "./ToolSidebar";
import NavigationConfirmModal from './NavigationConfirmModal'; import NavigationConfirmModal from "./NavigationConfirmModal";
import useNavigationGuard from '../hooks/useNavigationGuard'; import useNavigationGuard from "../hooks/useNavigationGuard";
import { Menu, X, ChevronDown, Terminal, Sparkles, Home } from 'lucide-react'; import { Menu, X, ChevronDown, Terminal, Sparkles, Home } from "lucide-react";
import ThemeToggle from './ThemeToggle'; import ThemeToggle from "./ThemeToggle";
import SEOHead from './SEOHead'; import SEOHead from "./SEOHead";
import ConsentBanner from './ConsentBanner'; import ConsentBanner from "./ConsentBanner";
import { NON_TOOLS, TOOLS, SITE_CONFIG, getCategoryConfig } from '../config/tools'; import {
import { useAnalytics } from '../hooks/useAnalytics'; NON_TOOLS,
TOOLS,
SITE_CONFIG,
getCategoryConfig,
} from "../config/tools";
import { useAnalytics } from "../hooks/useAnalytics";
const Layout = ({ children }) => { const Layout = ({ children }) => {
const location = useLocation(); const location = useLocation();
const { showModal, pendingNavigation, handleConfirm, handleCancel, hasUnsavedData, navigateWithGuard } = useNavigationGuard(); const {
showModal,
pendingNavigation,
handleConfirm,
handleCancel,
hasUnsavedData,
navigateWithGuard,
} = useNavigationGuard();
const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const dropdownRef = useRef(null); const dropdownRef = useRef(null);
// Initialize analytics tracking // Initialize analytics tracking
useAnalytics(); useAnalytics();
const isActive = (path) => { const isActive = (path) => {
return location.pathname === path; return location.pathname === path;
}; };
@@ -32,9 +44,9 @@ const Layout = ({ children }) => {
} }
}; };
document.addEventListener('mousedown', handleClickOutside); document.addEventListener("mousedown", handleClickOutside);
return () => { return () => {
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener("mousedown", handleClickOutside);
}; };
}, []); }, []);
@@ -45,33 +57,42 @@ const Layout = ({ children }) => {
}, [location.pathname]); }, [location.pathname]);
// Check if we're on a tool page (not homepage) // Check if we're on a tool page (not homepage)
const isToolPage = location.pathname !== '/'; const isToolPage = location.pathname !== "/";
// Check if we're on invoice preview page (no sidebar needed) // Check if we're on invoice preview page (no sidebar needed)
const isInvoicePreviewPage = location.pathname === '/invoice-preview'; const isInvoicePreviewPage = location.pathname === "/invoice-preview";
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900 flex flex-col"> <div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900 flex flex-col">
{/* SEO Head Management */} {/* SEO Head Management */}
<SEOHead /> <SEOHead />
{/* Header */} {/* Header */}
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0"> <header className="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0">
<div className={isToolPage ? "px-4 sm:px-6 lg:px-8" : "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"}> <div
className={
isToolPage
? "px-4 sm:px-6 lg:px-8"
: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
}
>
<div className="flex justify-between items-center h-16"> <div className="flex justify-between items-center h-16">
<button onClick={() => navigateWithGuard('/')} className="flex items-center group"> <button
onClick={() => navigateWithGuard("/")}
className="flex items-center group"
>
<div className="relative"> <div className="relative">
<div className="absolute inset-0 rounded-lg blur opacity-20 group-hover:opacity-40 transition-opacity"></div> <div className="absolute inset-0 rounded-lg blur opacity-20 group-hover:opacity-40 transition-opacity"></div>
<div className="relative p-2"> <div className="relative p-2">
<img <img
src="/logo.svg" src="/logo.svg"
alt={SITE_CONFIG.title} alt={SITE_CONFIG.title}
className="h-8 w-auto" className="h-8 w-auto"
style={{ maxWidth: '150px' }} style={{ maxWidth: "150px" }}
onError={(e) => { onError={(e) => {
// Fallback to Terminal icon with text if logo fails to load // Fallback to Terminal icon with text if logo fails to load
e.target.style.display = 'none'; e.target.style.display = "none";
e.target.nextSibling.style.display = 'flex'; e.target.nextSibling.style.display = "flex";
}} }}
/> />
<div className="hidden items-center space-x-3"> <div className="hidden items-center space-x-3">
@@ -83,7 +104,7 @@ const Layout = ({ children }) => {
</div> </div>
</div> </div>
</button> </button>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
{/* Desktop Navigation - only show on homepage */} {/* Desktop Navigation - only show on homepage */}
{!isToolPage && ( {!isToolPage && (
@@ -91,18 +112,18 @@ const Layout = ({ children }) => {
<button <button
onClick={() => { onClick={() => {
setIsDropdownOpen(false); setIsDropdownOpen(false);
navigateWithGuard('/'); navigateWithGuard("/");
}} }}
className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300 ${ className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300 ${
isActive('/') isActive("/")
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg' ? "bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg"
: 'text-slate-600 dark:text-slate-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-white/50 dark:hover:bg-slate-700/50' : "text-slate-600 dark:text-slate-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-white/50 dark:hover:bg-slate-700/50"
}`} }`}
> >
<Home className="h-4 w-4" /> <Home className="h-4 w-4" />
<span>Home</span> <span>Home</span>
</button> </button>
{/* Tools Dropdown */} {/* Tools Dropdown */}
<div className="relative" ref={dropdownRef}> <div className="relative" ref={dropdownRef}>
<button <button
@@ -113,11 +134,13 @@ const Layout = ({ children }) => {
> >
<Sparkles className="h-4 w-4" /> <Sparkles className="h-4 w-4" />
<span>Tools</span> <span>Tools</span>
<ChevronDown className={`h-4 w-4 transition-transform duration-300 ${ <ChevronDown
isDropdownOpen ? 'rotate-180' : '' className={`h-4 w-4 transition-transform duration-300 ${
}`} /> isDropdownOpen ? "rotate-180" : ""
}`}
/>
</button> </button>
{/* Dropdown Menu */} {/* Dropdown Menu */}
{isDropdownOpen && ( {isDropdownOpen && (
<div className="absolute top-full left-0 mt-3 w-80 bg-white/90 dark:bg-slate-800/90 backdrop-blur-md rounded-2xl shadow-2xl border border-slate-200/50 dark:border-slate-700/50 py-3 z-50 overflow-hidden"> <div className="absolute top-full left-0 mt-3 w-80 bg-white/90 dark:bg-slate-800/90 backdrop-blur-md rounded-2xl shadow-2xl border border-slate-200/50 dark:border-slate-700/50 py-3 z-50 overflow-hidden">
@@ -125,8 +148,10 @@ const Layout = ({ children }) => {
<div className="relative"> <div className="relative">
{TOOLS.map((tool) => { {TOOLS.map((tool) => {
const IconComponent = tool.icon; const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category); const categoryConfig = getCategoryConfig(
tool.category,
);
return ( return (
<button <button
key={tool.path} key={tool.path}
@@ -136,16 +161,20 @@ const Layout = ({ children }) => {
}} }}
className={`group flex items-center space-x-4 px-4 py-3 text-sm hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300 w-full text-left ${ className={`group flex items-center space-x-4 px-4 py-3 text-sm hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300 w-full text-left ${
isActive(tool.path) isActive(tool.path)
? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300' ? "bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300"
: 'text-slate-700 dark:text-slate-300' : "text-slate-700 dark:text-slate-300"
}`} }`}
> >
<div className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm group-hover:scale-110 transition-transform duration-300`}> <div
className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm group-hover:scale-110 transition-transform duration-300`}
>
<IconComponent className="h-4 w-4 text-white" /> <IconComponent className="h-4 w-4 text-white" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="font-medium">{tool.name}</div> <div className="font-medium">{tool.name}</div>
<div className="text-xs text-slate-600 dark:text-slate-600">{tool.description}</div> <div className="text-xs text-slate-600 dark:text-slate-600">
{tool.description}
</div>
</div> </div>
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300"> <div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<ChevronDown className="h-4 w-4 -rotate-90 text-slate-600" /> <ChevronDown className="h-4 w-4 -rotate-90 text-slate-600" />
@@ -159,9 +188,9 @@ const Layout = ({ children }) => {
</div> </div>
</nav> </nav>
)} )}
<ThemeToggle /> <ThemeToggle />
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
<button <button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
@@ -169,7 +198,11 @@ const Layout = ({ children }) => {
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"} aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
aria-expanded={isMobileMenuOpen} aria-expanded={isMobileMenuOpen}
> >
{isMobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />} {isMobileMenuOpen ? (
<X className="h-6 w-6" />
) : (
<Menu className="h-6 w-6" />
)}
</button> </button>
</div> </div>
</div> </div>
@@ -180,49 +213,19 @@ const Layout = ({ children }) => {
{isMobileMenuOpen && ( {isMobileMenuOpen && (
<> <>
{/* Overlay */} {/* Overlay */}
<div <div
className="md:hidden fixed inset-0 bg-black/20 z-30" className="md:hidden fixed inset-0 bg-black/20 z-30"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
/> />
{/* Menu */} {/* Menu */}
<div className="md:hidden fixed top-16 left-0 right-0 z-40 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-b border-slate-200/50 dark:border-slate-700/50 shadow-lg max-h-[calc(100vh-4rem)]"> <div className="md:hidden fixed top-16 left-0 right-0 z-40 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-b border-slate-200/50 dark:border-slate-700/50 shadow-lg max-h-[calc(100vh-4rem)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="space-y-2"> <div className="space-y-2">
{/* Non-Tools Section */} {/* Non-Tools Section */}
{NON_TOOLS.map((tool) => { {NON_TOOLS.map((tool) => {
const IconComponent = tool.icon;
return (
<button
key={tool.path}
onClick={() => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg'
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
}`}
>
<div className={`p-2 rounded-lg ${isActive(tool.path) ? 'bg-white/20' : 'bg-gradient-to-br from-indigo-500 to-purple-500'} shadow-sm`}>
<IconComponent className={`h-4 w-4 ${isActive(tool.path) ? 'text-white' : 'text-white'}`} />
</div>
<span>{tool.name}</span>
</button>
);
})}
<div className="border-t border-slate-200/50 dark:border-slate-700/50 pt-4 mt-4">
<div className="text-xs font-semibold text-slate-600 dark:text-slate-600 uppercase tracking-wider px-4 py-2 flex items-center gap-2">
<Sparkles className="h-3 w-3" />
{isToolPage ? 'Switch Tools' : 'Tools'}
</div>
{TOOLS.map((tool) => {
const IconComponent = tool.icon; const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category);
return ( return (
<button <button
key={tool.path} key={tool.path}
@@ -230,31 +233,69 @@ const Layout = ({ children }) => {
setIsMobileMenuOpen(false); setIsMobileMenuOpen(false);
navigateWithGuard(tool.path); navigateWithGuard(tool.path);
}} }}
className={`flex items-center space-x-4 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${ className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path) isActive(tool.path)
? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300' ? "bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg"
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50' : "text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50"
}`} }`}
> >
<div className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm`}> <div
<IconComponent className="h-4 w-4 text-white" /> className={`p-2 rounded-lg ${isActive(tool.path) ? "bg-white/20" : "bg-gradient-to-br from-indigo-500 to-purple-500"} shadow-sm`}
</div> >
<div className="flex-1"> <IconComponent
<div className="font-medium">{tool.name}</div> className={`h-4 w-4 ${isActive(tool.path) ? "text-white" : "text-white"}`}
<div className="text-xs text-slate-600 dark:text-slate-600">{tool.description}</div> />
</div> </div>
<span>{tool.name}</span>
</button> </button>
); );
})} })}
<div className="border-t border-slate-200/50 dark:border-slate-700/50 pt-4 mt-4">
<div className="text-xs font-semibold text-slate-600 dark:text-slate-600 uppercase tracking-wider px-4 py-2 flex items-center gap-2">
<Sparkles className="h-3 w-3" />
{isToolPage ? "Switch Tools" : "Tools"}
</div>
{TOOLS.map((tool) => {
const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category);
return (
<button
key={tool.path}
onClick={() => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-4 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path)
? "bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300"
: "text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50"
}`}
>
<div
className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm`}
>
<IconComponent className="h-4 w-4 text-white" />
</div>
<div className="flex-1">
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-slate-600 dark:text-slate-600">
{tool.description}
</div>
</div>
</button>
);
})}
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
</> </>
)} )}
{/* Main Content */} {/* Main Content */}
<div className="flex flex-1 pt-16 min-w-0 w-full max-w-full overflow-x-hidden"> <div className="flex flex-1 pt-16 min-w-0 w-full max-w-full">
{/* Main Content Area */} {/* Main Content Area */}
<main className="flex-1 flex flex-col min-w-0 w-full max-w-full"> <main className="flex-1 flex flex-col min-w-0 w-full max-w-full">
{isToolPage && !isInvoicePreviewPage ? ( {isToolPage && !isInvoicePreviewPage ? (
@@ -263,22 +304,18 @@ const Layout = ({ children }) => {
<ToolSidebar navigateWithGuard={navigateWithGuard} /> <ToolSidebar navigateWithGuard={navigateWithGuard} />
</div> </div>
<div className="flex-1 flex flex-col pl-0 lg:pl-16 min-w-0"> <div className="flex-1 flex flex-col pl-0 lg:pl-16 min-w-0">
<div className="p-4 sm:p-6 w-full min-w-0 max-w-full overflow-x-hidden"> <div className="p-4 sm:p-6 w-full min-w-0 max-w-full">
{children} {children}
</div> </div>
</div> </div>
</div> </div>
) : isInvoicePreviewPage ? ( ) : isInvoicePreviewPage ? (
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
<div className="flex-1"> <div className="flex-1">{children}</div>
{children}
</div>
</div> </div>
) : ( ) : (
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
<div className="flex-1"> <div className="flex-1">{children}</div>
{children}
</div>
{/* Global Footer for Homepage */} {/* Global Footer for Homepage */}
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30 mt-20"> <footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30 mt-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
@@ -287,15 +324,15 @@ const Layout = ({ children }) => {
<div className="relative"> <div className="relative">
<div className="absolute inset-0 rounded-lg blur opacity-20"></div> <div className="absolute inset-0 rounded-lg blur opacity-20"></div>
<div className="relative p-2"> <div className="relative p-2">
<img <img
src="/icon-192x192.png" src="/icon-192x192.png"
alt={SITE_CONFIG.title} alt={SITE_CONFIG.title}
className="h-16 w-auto" className="h-16 w-auto"
style={{ maxWidth: '100px' }} style={{ maxWidth: "100px" }}
onError={(e) => { onError={(e) => {
// Fallback to Terminal icon with text if logo fails to load // Fallback to Terminal icon with text if logo fails to load
e.target.style.display = 'none'; e.target.style.display = "none";
e.target.nextSibling.style.display = 'flex'; e.target.nextSibling.style.display = "flex";
}} }}
/> />
<div className="hidden items-center gap-3"> <div className="hidden items-center gap-3">
@@ -333,22 +370,26 @@ const Layout = ({ children }) => {
</div> </div>
</div> </div>
<div className="flex items-center gap-4 text-xs"> <div className="flex items-center gap-4 text-xs">
<button <button
onClick={() => navigateWithGuard('/release-notes')} onClick={() => navigateWithGuard("/release-notes")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors" className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
> >
Release Notes Release Notes
</button> </button>
<span className="text-slate-300 dark:text-slate-600"></span> <span className="text-slate-300 dark:text-slate-600">
<button
onClick={() => navigateWithGuard('/privacy')} </span>
<button
onClick={() => navigateWithGuard("/privacy")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors" className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
> >
Privacy Policy Privacy Policy
</button> </button>
<span className="text-slate-300 dark:text-slate-600"></span> <span className="text-slate-300 dark:text-slate-600">
<button
onClick={() => navigateWithGuard('/terms')} </span>
<button
onClick={() => navigateWithGuard("/terms")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors" className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
> >
Terms of Service Terms of Service
@@ -369,15 +410,15 @@ const Layout = ({ children }) => {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="text-center"> <div className="text-center">
<div className="flex items-center justify-center gap-2 mb-2"> <div className="flex items-center justify-center gap-2 mb-2">
<img <img
src="/icon-192x192.png" src="/icon-192x192.png"
alt={SITE_CONFIG.title} alt={SITE_CONFIG.title}
className="h-16 w-auto" className="h-16 w-auto"
style={{ maxWidth: '100px' }} style={{ maxWidth: "100px" }}
onError={(e) => { onError={(e) => {
// Fallback to Terminal icon with text if logo fails to load // Fallback to Terminal icon with text if logo fails to load
e.target.style.display = 'none'; e.target.style.display = "none";
e.target.nextSibling.style.display = 'flex'; e.target.nextSibling.style.display = "flex";
}} }}
/> />
</div> </div>
@@ -389,22 +430,22 @@ const Layout = ({ children }) => {
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div> <div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div> </div>
<div className="flex items-center justify-center gap-4 text-xs"> <div className="flex items-center justify-center gap-4 text-xs">
<button <button
onClick={() => navigateWithGuard('/release-notes')} onClick={() => navigateWithGuard("/release-notes")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors" className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
> >
Release Notes Release Notes
</button> </button>
<span className="text-slate-300 dark:text-slate-600"></span> <span className="text-slate-300 dark:text-slate-600"></span>
<button <button
onClick={() => navigateWithGuard('/privacy')} onClick={() => navigateWithGuard("/privacy")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors" className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
> >
Privacy Policy Privacy Policy
</button> </button>
<span className="text-slate-300 dark:text-slate-600"></span> <span className="text-slate-300 dark:text-slate-600"></span>
<button <button
onClick={() => navigateWithGuard('/terms')} onClick={() => navigateWithGuard("/terms")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors" className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
> >
Terms of Service Terms of Service
@@ -414,7 +455,7 @@ const Layout = ({ children }) => {
</div> </div>
</footer> </footer>
)} )}
{/* GDPR Consent Banner */} {/* GDPR Consent Banner */}
<ConsentBanner /> <ConsentBanner />

View File

@@ -1,21 +1,10 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef } from "react";
import { X } from "lucide-react";
const MobileAdBanner = () => { const MobileAdBanner = () => {
const [visible, setVisible] = useState(true);
const [closed, setClosed] = useState(false);
const iframeRef = useRef(null); const iframeRef = useRef(null);
useEffect(() => { useEffect(() => {
const wasClosed = sessionStorage.getItem("mobileAdClosed"); if (!iframeRef.current) return;
if (wasClosed === "true") {
setClosed(true);
setVisible(false);
}
}, []);
useEffect(() => {
if (!visible || closed || !iframeRef.current) return;
const timer = setTimeout(() => { const timer = setTimeout(() => {
if (!iframeRef.current) return; if (!iframeRef.current) return;
@@ -29,7 +18,7 @@ const MobileAdBanner = () => {
<html> <html>
<head> <head>
<style> <style>
body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; } body { margin: 0; padding: 0; display: block; background: transparent; overflow: hidden; height: 100vh; }
</style> </style>
</head> </head>
<body> <body>
@@ -42,7 +31,7 @@ const MobileAdBanner = () => {
'params' : {} 'params' : {}
}; };
</script> </script>
<script type="text/javascript" src="https://www.highperformanceformat.com/2965bcf877388cafa84160592c550f5a/invoke.js"></script> <script type="text/javascript" src="https://downconvenientmagnetic.com/2965bcf877388cafa84160592c550f5a/invoke.js"></script>
</body> </body>
</html> </html>
`); `);
@@ -50,33 +39,21 @@ const MobileAdBanner = () => {
}, 500); }, 500);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [visible, closed]); }, []);
const handleClose = () => {
setVisible(false);
setClosed(true);
sessionStorage.setItem("mobileAdClosed", "true");
};
if (!visible || closed) return null;
return ( return (
<div className="lg:hidden fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-gray-900 shadow-lg border-t border-gray-200 dark:border-gray-700"> <div className="lg:hidden fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-gray-900 shadow-lg border-t border-gray-200 dark:border-gray-700 h-[51px] flex justify-center items-end">
<button <iframe
onClick={handleClose} ref={iframeRef}
className="absolute -top-2 right-2 p-1 text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded-full shadow-sm z-10" style={{
aria-label="Close ad" width: "320px",
> height: "50px",
<X className="h-4 w-4" /> border: "none",
</button> display: "block",
<div className="flex justify-center items-center py-2"> }}
<iframe title="Mobile Advertisement"
ref={iframeRef} sandbox="allow-scripts allow-same-origin"
style={{ width: "320px", height: "50px", border: "none" }} />
title="Mobile Advertisement"
sandbox="allow-scripts allow-same-origin"
/>
</div>
</div> </div>
); );
}; };

View File

@@ -0,0 +1,289 @@
import React, { useEffect } from "react";
import { useEditor, EditorContent, ReactNodeViewRenderer } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Link from "@tiptap/extension-link";
import Image from "@tiptap/extension-image";
import { Table } from "@tiptap/extension-table";
import { TableRow } from "@tiptap/extension-table-row";
import { TableHeader } from "@tiptap/extension-table-header";
import { TableCell } from "@tiptap/extension-table-cell";
import { TaskList } from "@tiptap/extension-task-list";
import { TaskItem } from "@tiptap/extension-task-item";
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import { common, createLowlight } from "lowlight";
import CodeBlockComponent from "./CodeBlockComponent";
import { Markdown } from "tiptap-markdown";
import {
Bold,
Italic,
Strikethrough,
Code,
Heading1,
Heading2,
Heading3,
List,
ListOrdered,
CheckSquare,
Quote,
Link2,
Image as ImageIcon,
Table as TableIcon,
Minus,
} from "lucide-react";
// Set up lowlight for syntax highlighting in Tiptap
const lowlight = createLowlight(common);
const MenuBar = ({ editor }) => {
if (!editor) return null;
return (
<div className="flex flex-wrap justify-center items-center gap-1 p-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10 w-full">
<div className="flex flex-wrap items-center gap-1 w-full max-w-[85ch]">
<button
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("bold") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Bold"
>
<Bold className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("italic") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Italic"
>
<Italic className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={!editor.can().chain().focus().toggleStrike().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("strike") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Strikethrough"
>
<Strikethrough className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().toggleCode().run()}
disabled={!editor.can().chain().focus().toggleCode().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("code") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Inline Code"
>
<Code className="h-4 w-4" />
</button>
<div className="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" />
<button
onClick={() =>
editor.chain().focus().toggleHeading({ level: 1 }).run()
}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("heading", { level: 1 }) ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Heading 1"
>
<Heading1 className="h-4 w-4" />
</button>
<button
onClick={() =>
editor.chain().focus().toggleHeading({ level: 2 }).run()
}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("heading", { level: 2 }) ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Heading 2"
>
<Heading2 className="h-4 w-4" />
</button>
<button
onClick={() =>
editor.chain().focus().toggleHeading({ level: 3 }).run()
}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("heading", { level: 3 }) ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Heading 3"
>
<Heading3 className="h-4 w-4" />
</button>
<div className="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" />
<button
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("bulletList") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Bullet List"
>
<List className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("orderedList") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Ordered List"
>
<ListOrdered className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().toggleTaskList().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("taskList") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Task List"
>
<CheckSquare className="h-4 w-4" />
</button>
<div className="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" />
<button
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("blockquote") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Blockquote"
>
<Quote className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("codeBlock") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Code Block"
>
<Code className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().setHorizontalRule().run()}
className="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-gray-600 dark:text-gray-300"
title="Horizontal Rule"
>
<Minus className="h-4 w-4" />
</button>
<div className="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" />
<button
onClick={() => {
const url = window.prompt("URL");
if (url) {
editor
.chain()
.focus()
.extendMarkRange("link")
.setLink({ href: url })
.run();
}
}}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("link") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Add Link"
>
<Link2 className="h-4 w-4" />
</button>
<button
onClick={() => {
const url = window.prompt("Image URL");
if (url) {
editor.chain().focus().setImage({ src: url }).run();
}
}}
className="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-gray-600 dark:text-gray-300"
title="Add Image"
>
<ImageIcon className="h-4 w-4" />
</button>
<button
onClick={() =>
editor
.chain()
.focus()
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run()
}
className="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-gray-600 dark:text-gray-300"
title="Insert Table"
>
<TableIcon className="h-4 w-4" />
</button>
</div>
</div>
);
};
const RichMarkdownEditor = ({
initialContent,
onChange,
className = "",
height = "600px",
isFullscreen = false,
}) => {
const editor = useEditor({
extensions: [
StarterKit.configure({
codeBlock: false, // We'll use our own codeblock extension
}),
CodeBlockLowlight.extend({
addNodeView() {
return ReactNodeViewRenderer(CodeBlockComponent);
},
}).configure({
lowlight,
}),
Link.configure({
openOnClick: false,
}),
Image,
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
TableCell,
TaskList,
TaskItem.configure({
nested: true,
}),
Markdown.configure({
html: true,
tightLists: true,
tightListClass: "tight",
bulletListMarker: "-",
linkify: true,
breaks: false,
}),
],
content: initialContent,
onUpdate: ({ editor }) => {
// Serialize back to markdown and send to parent
const markdownOutput = editor.storage.markdown.getMarkdown();
const htmlOutput = editor.getHTML();
onChange(markdownOutput, htmlOutput);
},
editorProps: {
attributes: {
class:
"prose prose-sm sm:prose dark:prose-invert prose-blue focus:outline-none w-full max-w-none",
},
},
});
// Update editor content when initialContent prop completely changes from outside (e.g. loading a template)
useEffect(() => {
if (editor && initialContent !== undefined) {
const currentMarkdown = editor.storage.markdown.getMarkdown();
if (initialContent !== currentMarkdown) {
editor.commands.setContent(initialContent);
}
}
}, [editor, initialContent]);
return (
<div
className={`flex flex-col bg-white dark:bg-gray-900 overflow-hidden ${className}`}
>
<MenuBar editor={editor} />
<div
className={`overflow-y-auto w-full custom-scrollbar flex justify-center p-6`}
style={{ height }}
>
<div
className={`w-full max-w-[85ch] markdown-content-wrapper ${isFullscreen ? "is-fullscreen" : "is-normal"} is-edit-mode`}
>
<EditorContent editor={editor} />
</div>
</div>
</div>
);
};
export default RichMarkdownEditor;

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ const TabletAdSection = () => {
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<AdBlock <AdBlock
adKey="7c55aebcdd74f6e9a8dc24bd13e7d949" adKey="7c55aebcdd74f6e9a8dc24bd13e7d949"
adDomain="www.highperformanceformat.com" adDomain="downconvenientmagnetic.com"
/> />
</div> </div>
<div className="flex-shrink-0"> <div className="flex-shrink-0">

View File

@@ -1,163 +1,185 @@
import { Edit3, Table, LinkIcon, Hash, Wand2, GitCompare, Type, Home, FileText } from 'lucide-react'; import {
Edit3,
Table,
LinkIcon,
Hash,
Wand2,
GitCompare,
Type,
Home,
FileText,
} from "lucide-react";
// Master tools configuration - single source of truth // Master tools configuration - single source of truth
export const TOOL_CATEGORIES = { export const TOOL_CATEGORIES = {
navigation: { navigation: {
name: 'Navigation', name: "Navigation",
color: 'from-slate-500 to-slate-600', color: "from-slate-500 to-slate-600",
hoverColor: 'slate-600', hoverColor: "slate-600",
textColor: 'text-slate-600', textColor: "text-slate-600",
hoverTextColor: 'hover:text-slate-700 dark:hover:text-slate-600' hoverTextColor: "hover:text-slate-700 dark:hover:text-slate-600",
}, },
editor: { editor: {
name: 'Editor', name: "Editor",
color: 'from-blue-500 to-cyan-500', color: "from-blue-500 to-cyan-500",
hoverColor: 'blue-600', hoverColor: "blue-600",
textColor: 'text-blue-600', textColor: "text-blue-600",
hoverTextColor: 'hover:text-blue-700 dark:hover:text-blue-400' hoverTextColor: "hover:text-blue-700 dark:hover:text-blue-400",
}, },
encoder: { encoder: {
name: 'Encoder', name: "Encoder",
color: 'from-purple-500 to-pink-500', color: "from-purple-500 to-pink-500",
hoverColor: 'purple-600', hoverColor: "purple-600",
textColor: 'text-purple-600', textColor: "text-purple-600",
hoverTextColor: 'hover:text-purple-700 dark:hover:text-purple-400' hoverTextColor: "hover:text-purple-700 dark:hover:text-purple-400",
}, },
formatter: { formatter: {
name: 'Formatter', name: "Formatter",
color: 'from-green-500 to-emerald-500', color: "from-green-500 to-emerald-500",
hoverColor: 'green-600', hoverColor: "green-600",
textColor: 'text-green-600', textColor: "text-green-600",
hoverTextColor: 'hover:text-green-700 dark:hover:text-green-400' hoverTextColor: "hover:text-green-700 dark:hover:text-green-400",
}, },
analyzer: { analyzer: {
name: 'Analyzer', name: "Analyzer",
color: 'from-orange-500 to-red-500', color: "from-orange-500 to-red-500",
hoverColor: 'orange-600', hoverColor: "orange-600",
textColor: 'text-orange-600', textColor: "text-orange-600",
hoverTextColor: 'hover:text-orange-700 dark:hover:text-orange-400' hoverTextColor: "hover:text-orange-700 dark:hover:text-orange-400",
}, },
non_tools: { non_tools: {
name: 'Site Navigation', name: "Site Navigation",
color: 'from-indigo-500 to-purple-500', color: "from-indigo-500 to-purple-500",
hoverColor: 'indigo-600', hoverColor: "indigo-600",
textColor: 'text-indigo-600', textColor: "text-indigo-600",
hoverTextColor: 'hover:text-indigo-700 dark:hover:text-indigo-400' hoverTextColor: "hover:text-indigo-700 dark:hover:text-indigo-400",
} },
}; };
export const TOOLS = [ export const TOOLS = [
{ {
path: '/object-editor', path: "/object-editor",
name: 'Object Editor', name: "Object Editor",
icon: Edit3, icon: Edit3,
description: 'Visual editor for JSON and PHP serialized objects with mindmap visualization', description:
tags: ['Visual', 'JSON', 'PHP', 'Objects', 'Editor'], "Visual editor for JSON and PHP serialized objects with mindmap visualization",
category: 'editor' tags: ["Visual", "JSON", "PHP", "Objects", "Editor"],
category: "editor",
}, },
{ {
path: '/table-editor', path: "/table-editor",
name: 'Table Editor', name: "Table Editor",
icon: Table, icon: Table,
description: 'Import, edit, and export tabular data from URLs, files, or paste CSV/JSON', description:
tags: ['Table', 'CSV', 'JSON', 'Data', 'Editor'], "Import, edit, and export tabular data from URLs, files, or paste CSV/JSON",
category: 'editor' tags: ["Table", "CSV", "JSON", "Data", "Editor"],
category: "editor",
}, },
{ {
path: '/markdown-editor', path: "/markdown-editor",
name: 'Markdown Editor', name: "Markdown Editor",
icon: FileText, icon: FileText,
description: 'Write and preview markdown with live rendering, syntax highlighting, and export options', description:
tags: ['Markdown', 'Editor', 'Preview', 'Export', 'GFM'], "Write and preview markdown with live rendering, syntax highlighting, and export options",
category: 'editor' tags: ["Markdown", "Editor", "Preview", "Export", "GFM"],
category: "editor",
}, },
{ {
path: '/invoice-editor', path: "/diagram-editor",
name: 'Invoice Editor', name: "Diagram Editor",
icon: Edit3,
description:
"Create diagrams as code using Mermaid.js with live preview and multi-format export",
tags: ["Diagram", "Mermaid", "Architecture", "Flowchart", "Visual"],
category: "editor",
},
{
path: "/invoice-editor",
name: "Invoice Editor",
icon: FileText, icon: FileText,
description: 'Create, edit, and export professional invoices with PDF generation', description:
tags: ['Invoice', 'PDF', 'Business', 'Billing', 'Export'], "Create, edit, and export professional invoices with PDF generation",
category: 'editor' tags: ["Invoice", "PDF", "Business", "Billing", "Export"],
category: "editor",
}, },
{ {
path: '/url', path: "/url",
name: 'URL Encoder/Decoder', name: "URL Encoder/Decoder",
icon: LinkIcon, icon: LinkIcon,
description: 'Encode and decode URLs and query parameters', description: "Encode and decode URLs and query parameters",
tags: ['URL', 'Encode', 'Decode'], tags: ["URL", "Encode", "Decode"],
category: 'encoder' category: "encoder",
}, },
{ {
path: '/base64', path: "/base64",
name: 'Base64 Encoder/Decoder', name: "Base64 Encoder/Decoder",
icon: Hash, icon: Hash,
description: 'Convert text to Base64 and back with support for files', description: "Convert text to Base64 and back with support for files",
tags: ['Base64', 'Encode', 'Binary'], tags: ["Base64", "Encode", "Binary"],
category: 'encoder' category: "encoder",
}, },
{ {
path: '/beautifier', path: "/beautifier",
name: 'Code Beautifier/Minifier', name: "Code Beautifier/Minifier",
icon: Wand2, icon: Wand2,
description: 'Format and minify JSON, XML, SQL, CSS, and HTML code', description: "Format and minify JSON, XML, SQL, CSS, and HTML code",
tags: ['Format', 'Minify', 'Beautify'], tags: ["Format", "Minify", "Beautify"],
category: 'formatter' category: "formatter",
}, },
{ {
path: '/diff', path: "/diff",
name: 'Text Diff Checker', name: "Text Diff Checker",
icon: GitCompare, icon: GitCompare,
description: 'Compare two texts and highlight differences line by line', description: "Compare two texts and highlight differences line by line",
tags: ['Diff', 'Compare', 'Text'], tags: ["Diff", "Compare", "Text"],
category: 'analyzer' category: "analyzer",
}, },
{ {
path: '/text-length', path: "/text-length",
name: 'Text Length Checker', name: "Text Length Checker",
icon: Type, icon: Type,
description: 'Analyze text length, word count, and other text statistics', description: "Analyze text length, word count, and other text statistics",
tags: ['Text', 'Length', 'Statistics'], tags: ["Text", "Length", "Statistics"],
category: 'analyzer' category: "analyzer",
} },
]; ];
// Non-tool navigation items (homepage, what's new, etc.) // Non-tool navigation items (homepage, what's new, etc.)
export const NON_TOOLS = [ export const NON_TOOLS = [
{ {
path: '/', path: "/",
name: 'Home', name: "Home",
icon: Home, icon: Home,
description: 'Back to homepage', description: "Back to homepage",
category: 'non_tools' category: "non_tools",
} },
]; ];
// Navigation tools (for sidebar) - combines non-tools and tools // Navigation tools (for sidebar) - combines non-tools and tools
export const NAVIGATION_TOOLS = [ export const NAVIGATION_TOOLS = [...NON_TOOLS, ...TOOLS];
...NON_TOOLS,
...TOOLS
];
// Site configuration // Site configuration
export const SITE_CONFIG = { export const SITE_CONFIG = {
domain: 'https://dewe.dev', domain: "https://dewe.dev",
title: 'Dewe.Dev', title: "Dewe.Dev",
subtitle: 'Professional Developer Utilities', subtitle: "Professional Developer Utilities",
slogan: 'Code faster, debug smarter, ship better', slogan: "Code faster, debug smarter, ship better",
description: 'Professional-grade utilities for modern developers', description: "Professional-grade utilities for modern developers",
year: new Date().getFullYear(), year: new Date().getFullYear(),
totalTools: TOOLS.length totalTools: TOOLS.length,
}; };
// Helper functions // Helper functions
export const getCategoryConfig = (categoryKey) => TOOL_CATEGORIES[categoryKey] || TOOL_CATEGORIES.navigation; export const getCategoryConfig = (categoryKey) =>
TOOL_CATEGORIES[categoryKey] || TOOL_CATEGORIES.navigation;
export const getToolsByCategory = (categoryKey) => TOOLS.filter(tool => tool.category === categoryKey); export const getToolsByCategory = (categoryKey) =>
TOOLS.filter((tool) => tool.category === categoryKey);
export const getCategoryStats = () => { export const getCategoryStats = () => {
const stats = {}; const stats = {};
Object.keys(TOOL_CATEGORIES).forEach(key => { Object.keys(TOOL_CATEGORIES).forEach((key) => {
if (key !== 'navigation') { if (key !== "navigation") {
stats[key] = getToolsByCategory(key).length; stats[key] = getToolsByCategory(key).length;
} }
}); });

View File

@@ -2,69 +2,75 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap'); @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap");
@layer base { @layer base {
html { html {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family:
overflow-x: hidden; system-ui,
width: 100%; -apple-system,
max-width: 100vw; BlinkMacSystemFont,
} "Segoe UI",
Roboto,
body { sans-serif;
overflow-x: hidden; width: 100%;
width: 100%; max-width: 100vw;
max-width: 100vw; }
}
body {
#root { width: 100%;
overflow-x: hidden; max-width: 100vw;
width: 100%; }
max-width: 100vw;
min-width: 0; #root {
} width: 100%;
max-width: 100vw;
code, pre { min-width: 0;
font-family: 'JetBrains Mono', Monaco, 'Cascadia Code', 'Segoe UI Mono', 'Roboto Mono', monospace; }
}
code,
pre {
font-family:
"JetBrains Mono", Monaco, "Cascadia Code", "Segoe UI Mono",
"Roboto Mono", monospace;
}
} }
@layer components { @layer components {
.tool-card { .tool-card {
@apply bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 hover:shadow-md transition-shadow duration-200; @apply bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 hover:shadow-md transition-shadow duration-200;
} }
.tool-input { .tool-input {
@apply w-full p-3 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 font-mono text-sm resize-none focus:ring-2 focus:ring-primary-500 focus:border-transparent; @apply w-full p-3 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 font-mono text-sm resize-none focus:ring-2 focus:ring-primary-500 focus:border-transparent;
} }
.tool-button { .tool-button {
@apply px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-md font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed; @apply px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-md font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed;
} }
.tool-button-secondary { .tool-button-secondary {
@apply px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-md font-medium transition-colors duration-200; @apply px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-md font-medium transition-colors duration-200;
} }
.tool-button-primary { .tool-button-primary {
@apply flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md font-medium transition-colors duration-200; @apply flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md font-medium transition-colors duration-200;
} }
.toolbar-btn { .toolbar-btn {
@apply p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors duration-200 text-gray-700 dark:text-gray-300 font-medium text-sm; @apply p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors duration-200 text-gray-700 dark:text-gray-300 font-medium text-sm;
} }
.copy-button { .copy-button {
@apply absolute top-2 right-2 p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors duration-200; @apply absolute top-2 right-2 p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors duration-200;
} }
.scrollbar-hide { .scrollbar-hide {
-ms-overflow-style: none; /* Internet Explorer 10+ */ -ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
} }
.scrollbar-hide::-webkit-scrollbar { .scrollbar-hide::-webkit-scrollbar {
display: none; /* Safari and Chrome */ display: none; /* Safari and Chrome */
} }
} }

586
src/pages/DiagramEditor.js Executable file
View File

@@ -0,0 +1,586 @@
import React, { useState, useRef, useEffect } from "react";
import {
GitGraph,
Plus,
Upload,
Download,
Globe,
Type,
Columns,
Maximize2,
Minimize2,
FileImage,
FileCode2,
Eye,
AlertTriangle,
FileText,
Copy,
} from "lucide-react";
import ToolLayout from "../components/ToolLayout";
import CodeMirrorEditor from "../components/CodeMirrorEditor";
import SEO from "../components/SEO";
import RelatedTools from "../components/RelatedTools";
import ReactFlowEditor from "../components/diagram/ReactFlowEditor";
import FullscreenAdBanner from "../components/FullscreenAdBanner";
import { toPng } from "html-to-image";
import { generateMermaidFromGraph } from "../utils/mermaidGenerator";
const DIAGRAM_TEMPLATES = {
flowchart: {
nodes: [
{
id: "1",
type: "process",
position: { x: 250, y: 50 },
data: { label: "Start" },
},
{
id: "2",
type: "decision",
position: { x: 250, y: 150 },
data: { label: "Is it OK?" },
},
{
id: "3",
type: "process",
position: { x: 100, y: 250 },
data: { label: "Fix it" },
},
{
id: "4",
type: "database",
position: { x: 400, y: 250 },
data: { label: "Save to DB" },
},
{
id: "5",
type: "process",
position: { x: 250, y: 350 },
data: { label: "End" },
},
],
edges: [
{ id: "e1-2", source: "1", target: "2" },
{
id: "e2-3",
source: "2",
target: "3",
sourceHandle: "left",
label: "No",
},
{
id: "e2-4",
source: "2",
target: "4",
sourceHandle: "right",
label: "Yes",
},
{ id: "e3-2", source: "3", target: "2", targetHandle: "left" },
{
id: "e2-5",
source: "2",
target: "5",
sourceHandle: "bottom",
targetHandle: "top",
},
],
},
database: {
nodes: [
{
id: "users",
type: "database",
position: { x: 100, y: 100 },
data: { label: "Users Table" },
},
{
id: "orders",
type: "database",
position: { x: 400, y: 100 },
data: { label: "Orders Table" },
},
{
id: "process",
type: "process",
position: { x: 250, y: 250 },
data: { label: "Order Processor" },
},
],
edges: [
{ id: "eu-p", source: "users", target: "process", label: "Read" },
{ id: "ep-o", source: "process", target: "orders", label: "Write" },
],
},
};
const DiagramEditor = () => {
const [graphData, setGraphData] = useState(DIAGRAM_TEMPLATES.flowchart);
const [code, setCode] = useState(
JSON.stringify(DIAGRAM_TEMPLATES.flowchart, null, 2),
);
const [activeTab, setActiveTab] = useState("create");
const [viewMode, setViewMode] = useState(() =>
window.innerWidth < 1024 ? "editor" : "split",
);
const [isFullscreen, setIsFullscreen] = useState(false);
const [error, setError] = useState(null);
const [fetchUrl, setFetchUrl] = useState("");
const [fetching, setFetching] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState("flowchart");
const reactFlowWrapper = useRef(null);
const fileInputRef = useRef(null);
// Sync JSON text input to Graph data
useEffect(() => {
try {
const parsed = JSON.parse(code);
if (parsed.nodes && parsed.edges) {
setGraphData(parsed);
setError(null);
}
} catch (err) {
setError(err.message);
}
}, [code]);
const handleGraphChange = (newGraphData) => {
setGraphData(newGraphData);
setCode(JSON.stringify(newGraphData, null, 2));
};
// Input Handlers
const handleTemplateChange = (e) => {
const template = e.target.value;
setSelectedTemplate(template);
if (DIAGRAM_TEMPLATES[template]) {
setGraphData(DIAGRAM_TEMPLATES[template]);
setCode(JSON.stringify(DIAGRAM_TEMPLATES[template], null, 2));
}
};
const handleFileUpload = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
setCode(e.target.result);
setActiveTab("create");
};
reader.onerror = () => setError("Failed to read file");
reader.readAsText(file);
e.target.value = ""; // Reset input
};
const handleFetchFromURL = async () => {
if (!fetchUrl.trim()) return;
setFetching(true);
try {
let url = fetchUrl.trim();
if (
url.includes("github.com") &&
!url.includes("raw.githubusercontent.com")
) {
url = url
.replace("github.com", "raw.githubusercontent.com")
.replace("/blob/", "/");
}
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const text = await response.text();
setCode(text);
setActiveTab("create");
} catch (err) {
setError(`Fetch failed: ${err.message}`);
} finally {
setFetching(false);
}
};
// Export Handlers (Updated for ReactFlow)
const downloadFile = (content, filename, mimeType) => {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const exportJSON = () => {
downloadFile(code, "diagram.json", "application/json");
};
const exportMermaid = () => {
const mermaidCode = generateMermaidFromGraph(
graphData.nodes,
graphData.edges,
);
downloadFile(mermaidCode, "diagram.mmd", "text/plain");
};
const copyMermaid = () => {
const mermaidCode = generateMermaidFromGraph(
graphData.nodes,
graphData.edges,
);
navigator.clipboard
.writeText(mermaidCode)
.then(() => {
// Could add toast notification here
console.log("Mermaid code copied!");
})
.catch((err) => console.error("Failed to copy", err));
};
const exportPNG = () => {
const flowElement = document.querySelector(".react-flow");
if (!flowElement) return;
toPng(flowElement, {
backgroundColor: "#ffffff",
width: flowElement.offsetWidth,
height: flowElement.offsetHeight,
style: {
width: "100%",
height: "100%",
transform: "translate(0, 0)",
},
})
.then((dataUrl) => {
const a = document.createElement("a");
a.setAttribute("download", "diagram.png");
a.setAttribute("href", dataUrl);
a.click();
})
.catch((error) => {
setError("Failed to export PNG: " + error.message);
});
};
return (
<>
<SEO
title="Mermaid Diagram Editor & Viewer"
description="Write Mermaid.js diagram code with live visual preview. Pan, zoom, and export your flowcharts, sequence diagrams, and architecture maps to PNG or SVG."
keywords="mermaid editor, diagram editor, diagram as code, mermaidjs, flowchart generator, sequence diagram, architecture diagram, export svg"
path="/diagram-editor"
toolId="diagram-editor"
/>
<ToolLayout
title="Diagram Tool"
description="Create diagrams as code using Mermaid.js with live preview and multi-format export."
icon={GitGraph}
>
{/* Input Section - Always visible */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-6">
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Get Started
</h3>
</div>
{/* Tab Navigation */}
<div className="flex border-b border-gray-200 dark:border-gray-700 overflow-x-auto scrollbar-hide">
<button
onClick={() => setActiveTab("create")}
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "create"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
}`}
>
<Plus className="h-4 w-4 flex-shrink-0" />
Create New
</button>
<button
onClick={() => setActiveTab("url")}
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "url"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
}`}
>
<Globe className="h-4 w-4 flex-shrink-0" />
URL Fetch
</button>
<button
onClick={() => setActiveTab("paste")}
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "paste"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
}`}
>
<FileText className="h-4 w-4 flex-shrink-0" />
Paste
</button>
<button
onClick={() => setActiveTab("open")}
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "open"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
}`}
>
<Upload className="h-4 w-4 flex-shrink-0" />
Open File
</button>
</div>
<div className="p-4">
{activeTab === "create" && (
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Load Boilerplate Template
</label>
<select
value={selectedTemplate}
onChange={handleTemplateChange}
className="tool-input w-full max-w-xs"
>
<option value="flowchart">Flowchart</option>
<option value="database">Database/ER</option>
</select>
</div>
<button
onClick={() => {
setGraphData({ nodes: [], edges: [] });
setCode(JSON.stringify({ nodes: [], edges: [] }, null, 2));
}}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
Clear Editor
</button>
</div>
)}
{activeTab === "url" && (
<div className="space-y-3">
<div className="flex gap-2">
<input
type="url"
value={fetchUrl}
onChange={(e) => setFetchUrl(e.target.value)}
onKeyPress={(e) =>
e.key === "Enter" && !fetching && handleFetchFromURL()
}
placeholder="https://raw.githubusercontent.com/.../diagram.mmd"
className="tool-input flex-1"
/>
<button
onClick={handleFetchFromURL}
disabled={fetching || !fetchUrl.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors whitespace-nowrap"
>
{fetching ? "Fetching..." : "Fetch"}
</button>
</div>
</div>
)}
{activeTab === "paste" && (
<div className="space-y-3">
<div className="flex gap-2">
<textarea
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Paste your diagram JSON here..."
className="tool-input h-32 flex-1"
/>
</div>
</div>
)}
{activeTab === "open" && (
<div className="space-y-3">
<input
ref={fileInputRef}
type="file"
accept=".mmd,.mermaid,.txt"
onChange={handleFileUpload}
className="tool-input"
/>
</div>
)}
</div>
</div>
{/* Main Editor Section */}
<div
className={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden min-w-0 w-full max-w-full ${isFullscreen ? "fixed inset-0 z-[9999] flex flex-col !mt-0" : "mb-6"}`}
>
{/* Header & Controls */}
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 sticky top-0 bg-white dark:bg-gray-800 z-10 flex flex-wrap justify-between items-center gap-3">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<GitGraph className="h-5 w-5 text-blue-600 dark:text-blue-400" />
Diagram Editor
</h3>
<div className="flex items-center gap-2">
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800 shadow-sm">
<button
onClick={() => setViewMode("editor")}
className={`flex items-center gap-2 px-3 py-1.5 text-sm font-medium transition-colors ${viewMode === "editor" ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300" : "text-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"}`}
>
<Type className="h-4 w-4" />{" "}
<span className="hidden sm:inline">Code</span>
</button>
<button
onClick={() => setViewMode("split")}
className={`hidden lg:flex items-center gap-2 px-3 py-1.5 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${viewMode === "split" ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300" : "text-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"}`}
>
<Columns className="h-4 w-4" />{" "}
<span className="hidden sm:inline">Split</span>
</button>
<button
onClick={() => setViewMode("preview")}
className={`flex items-center gap-2 px-3 py-1.5 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${viewMode === "preview" ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300" : "text-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"}`}
>
<Eye className="h-4 w-4" />{" "}
<span className="hidden sm:inline">Preview</span>
</button>
</div>
<button
onClick={() => setIsFullscreen(!isFullscreen)}
className="p-2 text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
title={isFullscreen ? "Exit Fullscreen" : "Fullscreen"}
>
{isFullscreen ? (
<Minimize2 className="h-4 w-4" />
) : (
<Maximize2 className="h-4 w-4" />
)}
</button>
</div>
</div>
{/* Split View Content */}
<div
className={`${viewMode === "split" ? "grid grid-cols-1 lg:grid-cols-2" : ""} overflow-hidden min-w-0 w-full flex-1 relative`}
>
{isFullscreen && <FullscreenAdBanner />}
{/* Editor Pane */}
{(viewMode === "editor" || viewMode === "split") && (
<div
className={`${viewMode === "split" ? "border-r border-gray-200 dark:border-gray-700" : ""} ${isFullscreen ? "h-[calc(100vh-60px)] pb-[90px]" : "h-[600px]"} w-full min-w-0 flex flex-col relative`}
>
<CodeMirrorEditor
value={code}
onChange={setCode}
language="json"
placeholder="Write your diagram JSON here..."
showToggle={false}
maxLines={999}
height="100%"
className="flex-1 h-full"
/>
</div>
)}
{/* Preview Pane */}
{(viewMode === "preview" || viewMode === "split") && (
<div
className={`${isFullscreen ? "h-[calc(100vh-60px)] pb-[90px]" : "h-[600px]"} w-full min-w-0 bg-slate-50 dark:bg-slate-900 flex flex-col relative`}
>
{error && (
<div className="absolute inset-0 flex flex-col items-center justify-center p-6 text-center z-10 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm">
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-full mb-3">
<AlertTriangle className="h-8 w-8 text-red-500" />
</div>
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
Syntax Error
</h4>
<p className="text-sm text-red-600 dark:text-red-400 max-w-md">
{error}
</p>
</div>
)}
<div
className="flex-1 relative overflow-hidden"
ref={reactFlowWrapper}
>
<ReactFlowEditor
initialNodes={graphData.nodes}
initialEdges={graphData.edges}
onGraphChange={handleGraphChange}
/>
</div>
</div>
)}
</div>
</div>
{/* Export Section */}
{code.trim() && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Download className="h-5 w-5 text-blue-600 dark:text-blue-400" />
Export Diagram
</h3>
</div>
<div className="p-4 sm:p-6 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
<button
onClick={exportPNG}
className="flex flex-col items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all group"
>
<FileImage className="h-6 w-6 text-gray-500 group-hover:text-blue-500 mb-2" />
<span className="font-medium text-gray-900 dark:text-white">
PNG Image
</span>
<span className="text-xs text-gray-500">
High-res rendering
</span>
</button>
<button
onClick={exportJSON}
className="flex flex-col items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-purple-500 hover:bg-purple-50 dark:hover:bg-purple-900/20 transition-all group"
>
<FileCode2 className="h-6 w-6 text-gray-500 group-hover:text-purple-500 mb-2" />
<span className="font-medium text-gray-900 dark:text-white">
JSON Schema
</span>
<span className="text-xs text-gray-500">.json data format</span>
</button>
<button
onClick={exportMermaid}
className="flex flex-col items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-green-500 hover:bg-green-50 dark:hover:bg-green-900/20 transition-all group"
>
<FileCode2 className="h-6 w-6 text-gray-500 group-hover:text-green-500 mb-2" />
<span className="font-medium text-gray-900 dark:text-white">
Mermaid File
</span>
<span className="text-xs text-gray-500">.mmd raw code</span>
</button>
<button
onClick={copyMermaid}
className="flex flex-col items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-orange-500 hover:bg-orange-50 dark:hover:bg-orange-900/20 transition-all group"
>
<Copy className="h-6 w-6 text-gray-500 group-hover:text-orange-500 mb-2" />
<span className="font-medium text-gray-900 dark:text-white">
Copy Mermaid
</span>
<span className="text-xs text-gray-500">To clipboard</span>
</button>
</div>
</div>
)}
<RelatedTools toolId="diagram-editor" />
</ToolLayout>
</>
);
};
export default DiagramEditor;

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,25 @@
/* GitHub-style Markdown Preview Styling */ /* GitHub-style Markdown Preview Styling */
.markdown-preview { .markdown-preview {
color: #24292f; color: #24292f;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; font-family:
font-size: 16px; -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica,
line-height: 1.6; Arial, sans-serif;
word-wrap: break-word; font-size: 16px;
overflow-wrap: break-word; line-height: 1.6;
max-width: 100%; word-wrap: break-word;
word-break: break-word; overflow-wrap: break-word;
max-width: 100%;
word-break: break-word;
} }
/* Ensure all child elements respect container width */ /* Ensure all child elements respect container width */
.markdown-preview * { .markdown-preview * {
max-width: 100%; max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
.dark .markdown-preview { .dark .markdown-preview {
color: #c9d1d9; color: #c9d1d9;
} }
.markdown-preview h1, .markdown-preview h1,
@@ -26,254 +28,256 @@
.markdown-preview h4, .markdown-preview h4,
.markdown-preview h5, .markdown-preview h5,
.markdown-preview h6 { .markdown-preview h6 {
margin-top: 24px; margin-top: 24px;
margin-bottom: 16px; margin-bottom: 16px;
font-weight: 600; font-weight: 600;
line-height: 1.25; line-height: 1.25;
} }
.markdown-preview h1 { .markdown-preview h1 {
font-size: 2em; font-size: 2em;
border-bottom: 1px solid #d0d7de; border-bottom: 1px solid #d0d7de;
padding-bottom: 0.3em; padding-bottom: 0.3em;
} }
.dark .markdown-preview h1 { .dark .markdown-preview h1 {
border-bottom-color: #21262d; border-bottom-color: #21262d;
} }
.markdown-preview h2 { .markdown-preview h2 {
font-size: 1.5em; font-size: 1.5em;
border-bottom: 1px solid #d0d7de; border-bottom: 1px solid #d0d7de;
padding-bottom: 0.3em; padding-bottom: 0.3em;
} }
.dark .markdown-preview h2 { .dark .markdown-preview h2 {
border-bottom-color: #21262d; border-bottom-color: #21262d;
} }
.markdown-preview h3 { .markdown-preview h3 {
font-size: 1.25em; font-size: 1.25em;
} }
.markdown-preview h4 { .markdown-preview h4 {
font-size: 1em; font-size: 1em;
} }
.markdown-preview h5 { .markdown-preview h5 {
font-size: 0.875em; font-size: 0.875em;
} }
.markdown-preview h6 { .markdown-preview h6 {
font-size: 0.85em; font-size: 0.85em;
color: #57606a; color: #57606a;
} }
.dark .markdown-preview h6 { .dark .markdown-preview h6 {
color: #8b949e; color: #8b949e;
} }
.markdown-preview p { .markdown-preview p {
margin-top: 0; margin-top: 0;
margin-bottom: 16px; margin-bottom: 16px;
} }
/* Inline code - with background */ /* Inline code - with background */
.markdown-preview code { .markdown-preview code {
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
margin: 0; margin: 0;
font-size: 85%; font-size: 85%;
background-color: rgba(175, 184, 193, 0.2); background-color: rgba(175, 184, 193, 0.2);
border-radius: 6px; border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-family:
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
"Liberation Mono", monospace;
} }
.dark .markdown-preview code { .dark .markdown-preview code {
background-color: rgba(110, 118, 129, 0.4); background-color: rgba(110, 118, 129, 0.4);
} }
/* Code block wrapper with header */ /* Code block wrapper with header */
.markdown-preview .code-block-wrapper { .markdown-preview .code-block-wrapper {
margin-bottom: 16px; margin-bottom: 16px;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
border: 1px solid #d0d7de; border: 1px solid #d0d7de;
background-color: #f6f8fa; background-color: #f6f8fa;
} }
.dark .markdown-preview .code-block-wrapper { .dark .markdown-preview .code-block-wrapper {
border-color: #30363d; border-color: #30363d;
background-color: #0d1117; background-color: #0d1117;
} }
/* Code block header */ /* Code block header */
.markdown-preview .code-block-header { .markdown-preview .code-block-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 4px 10px; padding: 4px 10px;
background-color: #f6f8fa; background-color: #f6f8fa;
border-bottom: 1px solid #d0d7de; border-bottom: 1px solid #d0d7de;
font-size: 12px; font-size: 12px;
} }
.dark .markdown-preview .code-block-header { .dark .markdown-preview .code-block-header {
background-color: #161b22; background-color: #161b22;
border-bottom-color: #30363d; border-bottom-color: #30363d;
} }
/* Language label */ /* Language label */
.markdown-preview .code-block-language { .markdown-preview .code-block-language {
font-weight: 600; font-weight: 600;
color: #57606a; color: #57606a;
text-transform: uppercase; text-transform: uppercase;
font-size: 10px; font-size: 10px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
.dark .markdown-preview .code-block-language { .dark .markdown-preview .code-block-language {
color: #8b949e; color: #8b949e;
} }
/* Copy button */ /* Copy button */
.markdown-preview .code-block-copy { .markdown-preview .code-block-copy {
padding: 2px 6px; padding: 2px 6px;
background-color: transparent; background-color: transparent;
border: 1px solid #d0d7de; border: 1px solid #d0d7de;
border-radius: 6px; border-radius: 6px;
color: #24292f; color: #24292f;
font-size: 10px; font-size: 10px;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
font-weight: 500; font-weight: 500;
} }
.markdown-preview .code-block-copy:hover { .markdown-preview .code-block-copy:hover {
background-color: #f3f4f6; background-color: #f3f4f6;
border-color: #1f2328; border-color: #1f2328;
} }
.dark .markdown-preview .code-block-copy { .dark .markdown-preview .code-block-copy {
color: #c9d1d9; color: #c9d1d9;
border-color: #30363d; border-color: #30363d;
} }
.dark .markdown-preview .code-block-copy:hover { .dark .markdown-preview .code-block-copy:hover {
background-color: #21262d; background-color: #21262d;
border-color: #8b949e; border-color: #8b949e;
} }
/* Code blocks - with background */ /* Code blocks - with background */
.markdown-preview .code-block-wrapper pre { .markdown-preview .code-block-wrapper pre {
padding: 16px; padding: 16px;
overflow: auto; overflow: auto;
font-size: 85%; font-size: 85%;
line-height: 1.45; line-height: 1.45;
background-color: #0d1117; background-color: #0d1117;
margin: 0; margin: 0;
border-radius: 0; border-radius: 0;
} }
/* Legacy pre blocks (without wrapper) */ /* Legacy pre blocks (without wrapper) */
.markdown-preview pre:not(.code-block-wrapper pre) { .markdown-preview pre:not(.code-block-wrapper pre) {
padding: 16px; padding: 16px;
overflow: auto; overflow: auto;
font-size: 85%; font-size: 85%;
line-height: 1.45; line-height: 1.45;
background-color: #afb8c133; background-color: #afb8c133;
border-radius: 6px; border-radius: 6px;
margin-bottom: 16px; margin-bottom: 16px;
} }
.dark .markdown-preview pre:not(.code-block-wrapper pre) { .dark .markdown-preview pre:not(.code-block-wrapper pre) {
background-color: rgba(110, 118, 129, 0.4); background-color: rgba(110, 118, 129, 0.4);
} }
/* Code inside pre blocks - NO background (transparent) */ /* Code inside pre blocks - NO background (transparent) */
.markdown-preview pre code { .markdown-preview pre code {
display: inline; display: inline;
max-width: auto; max-width: auto;
padding: 0; padding: 0;
margin: 0; margin: 0;
overflow: visible; overflow: visible;
line-height: inherit; line-height: inherit;
word-wrap: normal; word-wrap: normal;
background-color: transparent !important; background-color: transparent !important;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
} }
/* Preserve highlight.js syntax highlighting colors */ /* Preserve highlight.js syntax highlighting colors */
.markdown-preview pre code.hljs { .markdown-preview pre code.hljs {
background: transparent !important; background: transparent !important;
padding: 0 !important; padding: 0 !important;
} }
.markdown-preview table { .markdown-preview table {
border-spacing: 0; border-spacing: 0;
border-collapse: collapse; border-collapse: collapse;
display: block; display: block;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
overflow-x: auto; overflow-x: auto;
margin-bottom: 16px; margin-bottom: 16px;
} }
.markdown-preview table tr { .markdown-preview table tr {
background-color: #ffffff; background-color: #ffffff;
border-top: 1px solid #d0d7de; border-top: 1px solid #d0d7de;
} }
.dark .markdown-preview table tr { .dark .markdown-preview table tr {
background-color: #0d1117; background-color: #0d1117;
border-top-color: #21262d; border-top-color: #21262d;
} }
.markdown-preview table tr:nth-child(2n) { .markdown-preview table tr:nth-child(2n) {
background-color: #f6f8fa; background-color: #f6f8fa;
} }
.dark .markdown-preview table tr:nth-child(2n) { .dark .markdown-preview table tr:nth-child(2n) {
background-color: #161b22; background-color: #161b22;
} }
.markdown-preview table th, .markdown-preview table th,
.markdown-preview table td { .markdown-preview table td {
padding: 6px 13px; padding: 6px 13px;
border: 1px solid #d0d7de; border: 1px solid #d0d7de;
} }
.dark .markdown-preview table th, .dark .markdown-preview table th,
.dark .markdown-preview table td { .dark .markdown-preview table td {
border-color: #21262d; border-color: #21262d;
} }
.markdown-preview table th { .markdown-preview table th {
font-weight: 600; font-weight: 600;
background-color: #f6f8fa; background-color: #f6f8fa;
} }
.dark .markdown-preview table th { .dark .markdown-preview table th {
background-color: #161b22; background-color: #161b22;
} }
.markdown-preview blockquote { .markdown-preview blockquote {
padding: 0 1em; padding: 0 1em;
color: #57606a; color: #57606a;
border-left: 0.25em solid #d0d7de; border-left: 0.25em solid #d0d7de;
margin: 0 0 16px 0; margin: 0 0 16px 0;
} }
.dark .markdown-preview blockquote { .dark .markdown-preview blockquote {
color: #8b949e; color: #8b949e;
border-left-color: #3b434b; border-left-color: #3b434b;
} }
.markdown-preview ul, .markdown-preview ul,
.markdown-preview ol { .markdown-preview ol {
padding-left: 2em; padding-left: 2em;
margin-top: 0; margin-top: 0;
margin-bottom: 16px; margin-bottom: 16px;
} }
/* Nested lists */ /* Nested lists */
@@ -281,100 +285,192 @@
.markdown-preview ul ol, .markdown-preview ul ol,
.markdown-preview ol ul, .markdown-preview ol ul,
.markdown-preview ol ol { .markdown-preview ol ol {
margin-top: 0.25em; margin-top: 0.25em;
margin-bottom: 0.25em; margin-bottom: 0.25em;
} }
/* List items */ /* List items */
.markdown-preview li { .markdown-preview li {
margin-bottom: 0.25em; margin-bottom: 0.25em;
line-height: 1.6; line-height: 1.6;
} }
.markdown-preview li + li { .markdown-preview li + li {
margin-top: 0.25em; margin-top: 0.25em;
} }
/* Better bullet points */ /* Better bullet points */
.markdown-preview ul > li { .markdown-preview ul > li {
list-style-type: disc; list-style-type: disc;
} }
.markdown-preview ul ul > li { .markdown-preview ul ul > li {
list-style-type: circle; list-style-type: circle;
} }
.markdown-preview ul ul ul > li { .markdown-preview ul ul ul > li {
list-style-type: square; list-style-type: square;
} }
/* Ordered list styling */ /* Ordered list styling */
.markdown-preview ol > li { .markdown-preview ol > li {
list-style-type: decimal; list-style-type: decimal;
} }
.markdown-preview ol ol > li { .markdown-preview ol ol > li {
list-style-type: lower-alpha; list-style-type: lower-alpha;
} }
.markdown-preview ol ol ol > li { .markdown-preview ol ol ol > li {
list-style-type: lower-roman; list-style-type: lower-roman;
} }
/* List item content spacing */ /* List item content spacing */
.markdown-preview li > p { .markdown-preview li > p {
margin-top: 0.5em; margin-top: 0.5em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.markdown-preview li > p:first-child { .markdown-preview li > p:first-child {
margin-top: 0; margin-top: 0;
} }
.markdown-preview li > p:last-child { .markdown-preview li > p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.markdown-preview hr { .markdown-preview hr {
height: 0.25em; height: 0.25em;
padding: 0; padding: 0;
margin: 24px 0; margin: 24px 0;
background-color: #d0d7de; background-color: #d0d7de;
border: 0; border: 0;
} }
.dark .markdown-preview hr { .dark .markdown-preview hr {
background-color: #21262d; background-color: #21262d;
} }
.markdown-preview a { .markdown-preview a {
color: #0969da; color: #0969da;
text-decoration: none; text-decoration: none;
} }
.dark .markdown-preview a { .dark .markdown-preview a {
color: #58a6ff; color: #58a6ff;
} }
.markdown-preview a:hover { .markdown-preview a:hover {
text-decoration: underline; text-decoration: underline;
} }
.markdown-preview strong { .markdown-preview strong {
font-weight: 600; font-weight: 600;
} }
.markdown-preview em { .markdown-preview em {
font-style: italic; font-style: italic;
} }
.markdown-preview u { .markdown-preview u {
text-decoration: underline; text-decoration: underline;
} }
.markdown-preview img { .markdown-preview img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
border-radius: 6px; border-radius: 6px;
margin: 16px 0; margin: 16px 0;
}
/* Tiptap specific styling overrides to match prose */
.tiptap p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
.tiptap {
outline: none;
}
.tiptap ul[data-type="taskList"] {
list-style: none;
padding: 0;
}
.tiptap ul[data-type="taskList"] li {
display: flex;
align-items: flex-start;
margin-top: 0;
margin-bottom: 0;
}
.tiptap ul[data-type="taskList"] li > label {
flex: 0 0 auto;
margin-right: 0.5rem;
user-select: none;
margin-top: 0.2rem;
}
.tiptap ul[data-type="taskList"] li > div {
flex: 1 1 auto;
margin: 0;
}
.tiptap ul[data-type="taskList"] li > div > p {
margin: 0;
}
.tiptap p {
margin-top: 0;
margin-bottom: 0.65em;
}
/* Printing logic for PDF export */
@media print {
.tiptap pre,
.markdown-preview pre {
white-space: pre-wrap !important;
word-wrap: break-word !important;
break-inside: avoid !important;
}
.code-block-header {
display: none !important;
}
}
/* Custom Node Views (Code Block) */
.tiptap .code-block-wrapper {
margin-bottom: 0.65em;
border-radius: 6px;
background-color: #0d1117;
overflow: hidden;
}
.tiptap .code-block-wrapper pre {
margin: 0 !important;
padding: 1rem;
border-radius: 0 0 6px 6px;
background: transparent;
}
/* Markdown Content Wrapper Padding Strategies */
.markdown-content-wrapper.is-normal.is-read-mode > .prose {
padding-bottom: 3rem; /* 48px */
}
.markdown-content-wrapper.is-fullscreen.is-read-mode > .prose {
padding-bottom: 12rem; /* 192px (Accounts for 90px banner ad + spacing) */
}
.markdown-content-wrapper.is-normal.is-edit-mode > div {
padding-bottom: 3rem; /* 48px */
}
.markdown-content-wrapper.is-fullscreen.is-edit-mode > div {
padding-bottom: 12rem; /* 192px (Accounts for 90px banner ad + spacing) */
} }

View File

@@ -1,34 +1,165 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: [ content: ["./src/**/*.{js,jsx,ts,tsx}"],
"./src/**/*.{js,jsx,ts,tsx}", darkMode: "class", // Enable manual dark mode control via class
],
darkMode: 'class', // Enable manual dark mode control via class
theme: { theme: {
extend: { extend: {
colors: { colors: {
primary: { primary: {
50: '#f0f9ff', 50: "#f0f9ff",
100: '#e0f2fe', 100: "#e0f2fe",
200: '#bae6fd', 200: "#bae6fd",
300: '#7dd3fc', 300: "#7dd3fc",
400: '#38bdf8', 400: "#38bdf8",
500: '#0ea5e9', 500: "#0ea5e9",
600: '#0284c7', 600: "#0284c7",
700: '#0369a1', 700: "#0369a1",
800: '#075985', 800: "#075985",
900: '#0c4a6e', 900: "#0c4a6e",
} },
}, },
fontFamily: { fontFamily: {
mono: ['JetBrains Mono', 'Monaco', 'Cascadia Code', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Code', 'Droid Sans Mono', 'Courier New', 'monospace'], mono: [
"JetBrains Mono",
"Monaco",
"Cascadia Code",
"Segoe UI Mono",
"Roboto Mono",
"Oxygen Mono",
"Ubuntu Monospace",
"Source Code Pro",
"Fira Code",
"Droid Sans Mono",
"Courier New",
"monospace",
],
}, },
maxWidth: { typography: (theme) => ({
'1/4': '25%', DEFAULT: {
'1/2': '50%', css: {
'3/4': '75%', "--tw-prose-body": "#24292f",
} "--tw-prose-headings": "#24292f",
"--tw-prose-lead": "#57606a",
"--tw-prose-links": "#0969da",
"--tw-prose-bold": "#24292f",
"--tw-prose-counters": "#57606a",
"--tw-prose-bullets": "#d0d7de",
"--tw-prose-hr": "#d0d7de",
"--tw-prose-quotes": "#57606a",
"--tw-prose-quote-borders": "#d0d7de",
"--tw-prose-captions": "#57606a",
"--tw-prose-code": "#24292f",
"--tw-prose-pre-code": "#24292f",
"--tw-prose-pre-bg": "#f6f8fa",
"--tw-prose-th-borders": "#d0d7de",
"--tw-prose-td-borders": "#d0d7de",
// Invert colors for dark mode
"--tw-prose-invert-body": "#c9d1d9",
"--tw-prose-invert-headings": "#c9d1d9",
"--tw-prose-invert-lead": "#8b949e",
"--tw-prose-invert-links": "#58a6ff",
"--tw-prose-invert-bold": "#c9d1d9",
"--tw-prose-invert-counters": "#8b949e",
"--tw-prose-invert-bullets": "#30363d",
"--tw-prose-invert-hr": "#21262d",
"--tw-prose-invert-quotes": "#8b949e",
"--tw-prose-invert-quote-borders": "#30363d",
"--tw-prose-invert-captions": "#8b949e",
"--tw-prose-invert-code": "#c9d1d9",
"--tw-prose-invert-pre-code": "#c9d1d9",
"--tw-prose-invert-pre-bg": "#161b22",
"--tw-prose-invert-th-borders": "#30363d",
"--tw-prose-invert-td-borders": "#30363d",
// Adjust margins and sizes (Standardizing to GitHub Markdown / Modern defaults)
maxWidth: "none",
lineHeight: "1.4",
p: {
marginTop: "0",
marginBottom: "0.65em",
},
"h1, h2, h3, h4, h5, h6": {
marginTop: "1em",
marginBottom: "0.65em",
fontWeight: "600",
lineHeight: "1.2",
},
h1: {
fontSize: "2em",
paddingBottom: "0.2em",
borderBottomWidth: "1px",
},
h2: {
fontSize: "1.5em",
paddingBottom: "0.2em",
borderBottomWidth: "1px",
},
h3: { fontSize: "1.25em" },
h4: { fontSize: "1em" },
h5: { fontSize: "0.875em" },
h6: { fontSize: "0.85em", color: "var(--tw-prose-lead)" },
"ul, ol": {
marginTop: "0",
marginBottom: "0.65em",
paddingLeft: "1.5em",
},
li: {
marginTop: "0.15em",
marginBottom: "0.15em",
},
"li > p": {
marginTop: "0",
marginBottom: "0",
},
blockquote: {
marginTop: "0",
marginBottom: "0.65em",
paddingLeft: "1em",
fontStyle: "normal",
borderLeftWidth: "4px",
},
pre: {
marginTop: "0",
marginBottom: "0.65em",
padding: "0.75em",
borderRadius: "6px",
},
code: {
backgroundColor: "rgba(175, 184, 193, 0.2)",
padding: "0.2em 0.4em",
borderRadius: "6px",
fontWeight: "inherit",
},
"code::before": { content: '""' },
"code::after": { content: '""' },
"pre code": {
backgroundColor: "transparent",
padding: "0",
},
table: {
marginTop: "0",
marginBottom: "0.65em",
},
"thead th": {
padding: "0.4em 0.75em",
borderWidth: "1px",
},
"tbody td": {
padding: "0.4em 0.75em",
borderWidth: "1px",
},
hr: {
marginTop: "1em",
marginBottom: "1em",
height: "0.25em",
borderWidth: "0",
backgroundColor: "var(--tw-prose-hr)",
},
},
},
}),
}, },
}, },
plugins: [], plugins: [require("@tailwindcss/typography")],
} };

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title>Test Page</title></head><body><h1>Test Heading</h1><p>Test paragraph</p><div class="container">Test div</div></body></html>