From 853174635ba5e36c5e42e667c2c4f76af2afc67c Mon Sep 17 00:00:00 2001 From: Jignesh D Maru Date: Sat, 18 Apr 2026 01:02:27 +0530 Subject: [PATCH 01/22] Add files via upload --- src/components/ElementsPalette.tsx | 163 +++++ src/components/LayersPanel.tsx | 167 +++++ src/components/VisualEditor.tsx | 883 +++++++++++++++++-------- src/main.tsx | 12 +- src/pages/SEOPage.tsx | 994 +++++++++++++++++++++++++++++ 5 files changed, 1949 insertions(+), 270 deletions(-) create mode 100644 src/components/ElementsPalette.tsx create mode 100644 src/components/LayersPanel.tsx create mode 100644 src/pages/SEOPage.tsx diff --git a/src/components/ElementsPalette.tsx b/src/components/ElementsPalette.tsx new file mode 100644 index 0000000..de01f31 --- /dev/null +++ b/src/components/ElementsPalette.tsx @@ -0,0 +1,163 @@ +import React, { useState } from 'react'; + +export interface PaletteItem { + icon: string; + label: string; + tag: string; + defaultHtml: string; + category: string; +} + +const PALETTE_ITEMS: PaletteItem[] = [ + { icon: '▭', label: 'Div', tag: 'div', category: 'Layout', defaultHtml: '
Div block
' }, + { icon: '⬛', label: 'Section', tag: 'section', category: 'Layout', defaultHtml: '
Section
' }, + { icon: '▤', label: 'Header', tag: 'header', category: 'Layout', defaultHtml: '
Header
' }, + { icon: '▣', label: 'Footer', tag: 'footer', category: 'Layout', defaultHtml: '' }, + { icon: 'H1', label: 'Heading 1', tag: 'h1', category: 'Text', defaultHtml: '

Heading 1

' }, + { icon: 'H2', label: 'Heading 2', tag: 'h2', category: 'Text', defaultHtml: '

Heading 2

' }, + { icon: 'H3', label: 'Heading 3', tag: 'h3', category: 'Text', defaultHtml: '

Heading 3

' }, + { icon: '¶', label: 'Paragraph', tag: 'p', category: 'Text', defaultHtml: '

Type your text here.

' }, + { icon: '🔗', label: 'Link', tag: 'a', category: 'Text', defaultHtml: 'Link text' }, + { icon: '⌚', label: 'Span', tag: 'span', category: 'Text', defaultHtml: 'Inline text' }, + { icon: '⏩', label: 'Button', tag: 'button', category: 'UI', defaultHtml: '' }, + { icon: '☐', label: 'Input', tag: 'input', category: 'UI', defaultHtml: '' }, + { icon: '▽', label: 'Select', tag: 'select', category: 'UI', defaultHtml: '' }, + { icon: '☷', label: 'Textarea', tag: 'textarea', category: 'UI', defaultHtml: '' }, + { icon: '🖼', label: 'Image', tag: 'img', category: 'Media', defaultHtml: 'Image' }, + { icon: '—', label: 'Divider', tag: 'hr', category: 'Layout', defaultHtml: '
' }, + { icon: '•', label: 'List (ul)', tag: 'ul', category: 'Text', defaultHtml: '' }, + { icon: '1.', label: 'List (ol)', tag: 'ol', category: 'Text', defaultHtml: '
  1. First
  2. Second
  3. Third
' }, + { icon: '⊞', label: 'Table', tag: 'table', category: 'Layout', defaultHtml: '
Col 1Col 2
CellCell
' }, + { icon: '◱', label: 'Card', tag: 'div', category: 'UI', defaultHtml: '

Card Title

Card description goes here.

' }, + { icon: '☰', label: 'Nav', tag: 'nav', category: 'Layout', defaultHtml: '' }, + { icon: '◫', label: 'Flex Row', tag: 'div', category: 'Layout', defaultHtml: '
Item
Item
' }, + { icon: '⊟', label: 'Grid', tag: 'div', category: 'Layout', defaultHtml: '
Grid Item
Grid Item
Grid Item
Grid Item
' }, +]; + +const CATEGORIES = ['Layout', 'Text', 'UI', 'Media']; + +interface Props { + onInsert: (html: string) => void; + onDragStart?: (html: string) => void; + onDragEnd?: () => void; +} + +const ElementsPalette: React.FC = ({ onInsert, onDragStart, onDragEnd }) => { + const [activeCategory, setActiveCategory] = useState('All'); + const [search, setSearch] = useState(''); + const [dragging, setDragging] = useState(null); + + const cats = ['All', ...CATEGORIES]; + const items = PALETTE_ITEMS.filter(item => { + const matchCat = activeCategory === 'All' || item.category === activeCategory; + const matchQ = !search || item.label.toLowerCase().includes(search.toLowerCase()) || item.tag.toLowerCase().includes(search.toLowerCase()); + return matchCat && matchQ; + }); + + return ( +
+
+ Elements +
+ +
+ setSearch(e.target.value)} + placeholder="Search elements…" + style={{ + width: '100%', background: '#2a2a2a', border: '1px solid #3a3a3a', + borderRadius: 4, padding: '4px 8px', fontSize: 11, color: '#ccc', + outline: 'none', boxSizing: 'border-box', fontFamily: 'inherit', + }} + /> +
+ +
+ {cats.map(c => ( + + ))} +
+ +
+ {items.map((item, i) => ( +
{ + setDragging(item); + e.dataTransfer.setData('text/html-element', item.defaultHtml); + e.dataTransfer.effectAllowed = 'copy'; + onDragStart?.(item.defaultHtml); + }} + onDragEnd={() => { setDragging(null); onDragEnd?.(); }} + onClick={() => onInsert(item.defaultHtml)} + title={`Click or drag to insert <${item.tag}>`} + style={{ + display: 'flex', + alignItems: 'center', + gap: 8, + padding: '5px 8px', + borderRadius: 5, + cursor: 'grab', + marginBottom: 1, + background: dragging === item ? 'rgba(229,164,90,0.15)' : 'transparent', + border: '1px solid transparent', + transition: 'background 0.1s, border-color 0.1s', + userSelect: 'none', + }} + onMouseEnter={e => { + (e.currentTarget as HTMLElement).style.background = 'rgba(255,255,255,0.06)'; + (e.currentTarget as HTMLElement).style.borderColor = '#333'; + }} + onMouseLeave={e => { + (e.currentTarget as HTMLElement).style.background = dragging === item ? 'rgba(229,164,90,0.15)' : 'transparent'; + (e.currentTarget as HTMLElement).style.borderColor = 'transparent'; + }} + > + 2 ? 9 : 13, color: '#e5a45a', + fontFamily: 'monospace', fontWeight: 700, + }}> + {item.icon} + +
+
{item.label}
+
<{item.tag}>
+
+ drag +
+ ))} +
+ +
+ Click or drag to insert +
+
+ ); +}; + +export default ElementsPalette; diff --git a/src/components/LayersPanel.tsx b/src/components/LayersPanel.tsx new file mode 100644 index 0000000..b4d084e --- /dev/null +++ b/src/components/LayersPanel.tsx @@ -0,0 +1,167 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +const SKIP = new Set(['script', 'style', 'meta', 'link', 'title', 'base', 'noscript', 'head']); + +export interface LayerNode { + el: HTMLElement; + tag: string; + id: string; + cls: string; + children: LayerNode[]; + depth: number; +} + +function buildTree(el: HTMLElement, depth = 0): LayerNode | null { + const tag = el.tagName?.toLowerCase(); + if (!tag || SKIP.has(tag)) return null; + const children: LayerNode[] = []; + for (const child of Array.from(el.children)) { + const node = buildTree(child as HTMLElement, depth + 1); + if (node) children.push(node); + } + return { el, tag, id: el.id, cls: Array.from(el.classList).join(' '), children, depth }; +} + +function label(node: LayerNode) { + if (node.id) return `<${node.tag}#${node.id}>`; + if (node.cls) return `<${node.tag}.${node.cls.split(' ')[0]}>`; + return `<${node.tag}>`; +} + +interface RowProps { + node: LayerNode; + selectedEl: HTMLElement | null; + multiSel: Set; + onSelect: (el: HTMLElement, shift: boolean) => void; + onMove: (el: HTMLElement, dir: 'up' | 'down') => void; +} + +function LayerRow({ node, selectedEl, multiSel, onSelect, onMove }: RowProps) { + const [open, setOpen] = useState(true); + const isSel = node.el === selectedEl || multiSel.has(node.el); + + return ( +
+
{ e.stopPropagation(); onSelect(node.el, e.shiftKey); }} + style={{ + display: 'flex', + alignItems: 'center', + paddingLeft: 8 + node.depth * 12, + paddingRight: 4, + height: 24, + cursor: 'pointer', + background: isSel + ? (node.el === selectedEl ? 'rgba(229,164,90,0.18)' : 'rgba(100,160,255,0.12)') + : 'transparent', + borderLeft: isSel + ? `2px solid ${node.el === selectedEl ? '#e5a45a' : '#64a0ff'}` + : '2px solid transparent', + fontSize: 11, + color: isSel ? (node.el === selectedEl ? '#e5a45a' : '#7ab8f5') : '#aaa', + userSelect: 'none', + gap: 4, + transition: 'background 0.1s', + }} + onMouseEnter={e => { + if (!isSel) (e.currentTarget as HTMLElement).style.background = 'rgba(255,255,255,0.05)'; + }} + onMouseLeave={e => { + if (!isSel) (e.currentTarget as HTMLElement).style.background = 'transparent'; + }} + > + {node.children.length > 0 ? ( + { e.stopPropagation(); setOpen(o => !o); }} + style={{ fontSize: 9, color: '#666', width: 12, textAlign: 'center', flexShrink: 0 }} + > + {open ? '▾' : '▸'} + + ) : ( + + )} + + {label(node)} + + {isSel && node.el === selectedEl && ( + + + + + )} +
+ {open && node.children.map((child, i) => ( + + ))} +
+ ); +} + +interface LayersPanelProps { + iframeDoc: Document | null; + selectedEl: HTMLElement | null; + multiSel: Set; + tick: number; + onSelect: (el: HTMLElement, shift: boolean) => void; + onMove: (el: HTMLElement, dir: 'up' | 'down') => void; +} + +const LayersPanel: React.FC = ({ iframeDoc, selectedEl, multiSel, tick, onSelect, onMove }) => { + const [tree, setTree] = useState(null); + + useEffect(() => { + if (!iframeDoc?.body) { setTree(null); return; } + setTree(buildTree(iframeDoc.body)); + }, [iframeDoc, tick]); + + return ( +
+
+ Layers +
+
+ {tree ? ( + + ) : ( +
+ No HTML loaded +
+ )} +
+
+ Shift+click to multi-select +
+
+ ); +}; + +export default LayersPanel; diff --git a/src/components/VisualEditor.tsx b/src/components/VisualEditor.tsx index ab88c0a..a3daaea 100644 --- a/src/components/VisualEditor.tsx +++ b/src/components/VisualEditor.tsx @@ -1,6 +1,8 @@ import React, { useRef, useEffect, useState, useCallback } from 'react'; import { useEditorStore } from '../store/editorStore'; import { useContextMenu } from './ContextMenu'; +import LayersPanel from './LayersPanel'; +import ElementsPalette from './ElementsPalette'; const HANDLES = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'] as const; type Handle = typeof HANDLES[number]; @@ -17,6 +19,7 @@ function getHandlePos(h: Handle, r: { left: number; top: number; width: number; } const SKIP_TAGS = new Set(['html', 'head', 'body', 'script', 'style', 'meta', 'link', 'title', 'base', 'noscript']); +const GRID = 8; const STYLE_PROPS = [ 'color', 'background-color', 'background', 'font-size', 'font-weight', 'font-family', @@ -31,6 +34,8 @@ const STYLE_PROPS = [ 'flex-wrap', 'background-size', 'background-image', 'background-position', 'background-repeat', ]; +function snap(v: number) { return Math.round(v / GRID) * GRID; } + function cssEscape(value: string) { return value.replace(/["\\#.:,[\]>+~*^$|= !]/g, '\\$&'); } @@ -65,7 +70,26 @@ function collectStyles(el: HTMLElement, win?: Window | null) { return styles; } -/* ── Global drag-capture helpers (shared with App.tsx resizer) ── */ +function buildBreadcrumb(el: HTMLElement): HTMLElement[] { + const path: HTMLElement[] = []; + let node: HTMLElement | null = el; + while (node && node.tagName) { + const tag = node.tagName.toLowerCase(); + if (tag === 'html') break; + path.unshift(node); + node = node.parentElement; + } + return path; +} + +function breadcrumbLabel(el: HTMLElement) { + const tag = el.tagName.toLowerCase(); + if (el.id) return `${tag}#${el.id}`; + const cls = Array.from(el.classList).filter(Boolean); + if (cls.length) return `${tag}.${cls[0]}`; + return tag; +} + function showDragCapture(cursor: string) { document.body.style.cursor = cursor; document.body.style.userSelect = 'none'; @@ -79,17 +103,18 @@ function hideDragCapture() { if (overlay) overlay.style.display = 'none'; } +type PreviewSize = 'full' | 'desktop' | 'tablet' | 'mobile'; +const PREVIEW_WIDTHS: Record = { + full: undefined, desktop: 1280, tablet: 768, mobile: 375, +}; + const VisualEditor: React.FC = () => { const { - files, - updateFileContent, - setSelectedElement, - addConsoleEntry, - timelineAnimationStyle, - setSelectedSelector, - setVisualBridge, - selectedSelector, + files, updateFileContent, setSelectedElement, addConsoleEntry, + timelineAnimationStyle, setSelectedSelector, setVisualBridge, selectedSelector, + showNotification, } = useEditorStore(); + const iframeRef = useRef(null); const [srcDoc, setSrcDoc] = useState(''); const rebuildTimerRef = useRef | null>(null); @@ -110,14 +135,31 @@ const VisualEditor: React.FC = () => { const eventsCleanupRef = useRef void)>(null); const [interaction, setInteraction] = useState<'select' | 'interact'>('select'); + // Multi-select + const [multiSel, setMultiSel] = useState>(new Set()); + const multiSelRef = useRef>(new Set()); + + // Responsive preview + const [previewSize, setPreviewSize] = useState('full'); + + // Panels visibility + const [showLayers, setShowLayers] = useState(true); + const [showPalette, setShowPalette] = useState(true); + + // Drag from palette + const [paletteDropping, setPaletteDropping] = useState(false); + const [isDraggingFromPalette, setIsDraggingFromPalette] = useState(false); + const pendingInsertRef = useRef(null); + + // iframe doc ref for layers + const [iframeDoc, setIframeDoc] = useState(null); + useEffect(() => { iframeOffRef.current = iframeOff; }, [iframeOff]); useEffect(() => { hovElRef.current = hovEl; }, [hovEl]); useEffect(() => { selElRef.current = selEl; }, [selEl]); + useEffect(() => { multiSelRef.current = multiSel; }, [multiSel]); useEffect(() => { - return () => { - eventsCleanupRef.current?.(); - eventsCleanupRef.current = null; - }; + return () => { eventsCleanupRef.current?.(); eventsCleanupRef.current = null; }; }, []); /* ── Build srcdoc ── */ @@ -139,7 +181,6 @@ const VisualEditor: React.FC = () => { else { html = `${tag}\n${html}`; } } }); - // Inject JS (so pages that rely on scripts don't render blank in Visual mode) files.filter(f => f.type === 'js').forEach(js => { const tag = ` + + + + + + + + + + + + + + +

Main Heading – ONE per page

+

Section Heading

+

Sub-section

+

Smallest
+ + +

Paragraph – basic building block of text

+Bold / important +Italic / emphasis +Fine print +SEO +Highlighted text +Deleted text +Inserted text +inline code +

+  multi-line
+  code block
+
+
A quote
+An inline quotation +Source title + + +Link text +Descriptive alt text – crucial for Image SEO +
+ Sales chart 2024 +
Fig 1: Sales chart 2024
+
+ + + + +
    +
  • Item one
  • +
  • Item two
  • +
+
    +
  1. Step one
  2. +
  3. Step two
  4. +
+
+
Term
+
Definition
+
+ + + + + + + + + + + + + + +
Monthly sales
MonthSales
January₹ 50,000
February₹ 62,000
Total₹ 1,12,000
+ + +
+ + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
+ +
+
+
+ + + +
+ Click to expand +

Hidden content shown on click

+
+ +

Modal dialog content

+ +
+70% +60% + + +inline element – doesn't break line +
block element – takes full width
`, + + cssBasic: `/* ── Selectors ── */ +* { box-sizing: border-box; } /* universal */ +body { margin: 0; } /* element */ +.card { padding: 1rem; } /* class */ +#hero { height: 100vh; } /* id */ +a:hover { color: orange; } /* pseudo-class */ +p::first-line { font-weight: bold; } /* pseudo-element */ +input[type="email"] { border: 1px solid #ccc; } /* attribute */ + +/* ── CSS Variables ── */ +:root { + --color-primary: #e34c26; + --color-bg: #1e1e1e; + --font-size-base: 1rem; + --radius: 0.5rem; +} + +/* ── Box Model ── */ +.box { + width: 300px; + height: 200px; + padding: 16px 24px; /* top-bottom left-right */ + margin: 0 auto; /* centered */ + border: 2px solid #333; + border-radius: var(--radius); + outline: 2px dashed red; /* outside border, no layout impact */ +} + +/* ── Backgrounds ── */ +.hero { + background-color: #1e1e1e; + background-image: url('/og-image.png'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background: linear-gradient(135deg, #e34c26, #264de4); +} + +/* ── Typography ── */ +body { + font-family: 'Inter', system-ui, -apple-system, sans-serif; + font-size: 16px; + line-height: 1.6; + color: #f0f0f0; +} +h1 { font-size: clamp(1.8rem, 5vw, 3rem); font-weight: 700; } +p { text-align: justify; letter-spacing: 0.02em; } +a { text-decoration: none; color: var(--color-primary); } + +/* ── Colors ── */ +.badge { color: #fff; background: rgba(227,76,38,0.9); } +.muted { color: rgb(160, 160, 160); } +.success { color: hsl(142, 70%, 45%); } +.glass { background: rgba(255,255,255,0.05); backdrop-filter: blur(10px); } + +/* ── Display & Positioning ── */ +.hidden { display: none; } +.inline { display: inline; } +.block { display: block; } +.flex-row { display: flex; gap: 1rem; align-items: center; } +.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; } + +.relative-parent { position: relative; } +.absolute-child { position: absolute; top: 0; right: 0; } +.fixed-nav { position: fixed; top: 0; left: 0; width: 100%; z-index: 100; } +.sticky-header { position: sticky; top: 0; } + +/* ── Flexbox ── */ +.navbar { + display: flex; + justify-content: space-between; /* main axis */ + align-items: center; /* cross axis */ + flex-wrap: wrap; + gap: 1rem; +} +.card-grid { + display: flex; + flex-direction: row; + flex: 1; /* grow to fill space */ +} + +/* ── Grid ── */ +.layout { + display: grid; + grid-template-columns: 250px 1fr 300px; /* sidebar | main | aside */ + grid-template-rows: auto 1fr auto; + grid-template-areas: + "header header header" + "sidebar main aside" + "footer footer footer"; + min-height: 100vh; + gap: 1rem; +} +.layout > header { grid-area: header; } +.layout > main { grid-area: main; } +.layout > aside { grid-area: aside; } +.layout > footer { grid-area: footer; } + +/* ── Transitions & Animations ── */ +.btn { + transition: background 0.3s ease, transform 0.2s ease; +} +.btn:hover { + background: var(--color-primary); + transform: translateY(-2px); +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} +.fade-in { + animation: fadeIn 0.6s ease forwards; +} + +/* ── Responsive (Mobile-first) ── */ +/* Base = mobile */ +.container { width: 100%; padding: 0 1rem; } + +/* Tablet 768px+ */ +@media (min-width: 768px) { + .container { max-width: 768px; margin: 0 auto; } + .grid-2 { grid-template-columns: 1fr 1fr; } +} + +/* Desktop 1024px+ */ +@media (min-width: 1024px) { + .container { max-width: 1200px; } +} + +/* Dark/Light mode */ +@media (prefers-color-scheme: dark) { + body { background: #1e1e1e; color: #f0f0f0; } +} + +/* ── Shadows ── */ +.card { box-shadow: 0 4px 6px rgba(0,0,0,0.3); } +.card:hover { box-shadow: 0 20px 40px rgba(0,0,0,0.5); } + +/* ── Overflow & Scroll ── */ +.scroll-x { overflow-x: auto; white-space: nowrap; } +.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.clamp { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }`, + + imageSEO: ` + + + +image + + + +HTML Editor with Monaco code editor and live preview side by side + + + + + + +Hero banner + + +Feature screenshot + + +Hero + + +
+ Bar chart showing monthly page views Jan–Dec 2024 +
Fig 1: Monthly page views increased 3× in 2024
+
+ + + + + + Hero image + + + +HTML Editor interface + + + + + + + + + + + + + +`, + + ogFavicon: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`, +}; + +type Tab = "charset" | "htmltags" | "css" | "imageseo" | "ogfavicon"; + +const tabs: { id: Tab; label: string; icon: string }[] = [ + { id: "charset", label: "meta charset", icon: "🔤" }, + { id: "htmltags", label: "HTML Tags", icon: "🏷️" }, + { id: "css", label: "CSS Basics", icon: "🎨" }, + { id: "imageseo", label: "Image SEO", icon: "🖼️" }, + { id: "ogfavicon", label: "OG + Favicon", icon: "🔗" }, +]; + +function CodeBlock({ code, label, variant }: { code: string; label?: string; variant?: "ok" | "bad" | "default" }) { + const [copied, setCopied] = useState(false); + + const copy = () => { + navigator.clipboard.writeText(code).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }; + + const borderColor = + variant === "ok" ? "#22c55e" : variant === "bad" ? "#ef4444" : "#3f3f46"; + + return ( +
+ {label && ( +
+ {variant === "ok" && ( + + ✓ WORKING + + )} + {variant === "bad" && ( + + ✗ NOT WORKING + + )} + + {label} + +
+ )} +
+
+          {code}
+        
+ +
+
+ ); +} + +function CharsetSection() { + const [previewMode, setPreviewMode] = useState<"working" | "broken">("working"); + + const workingHtml = `
✓ meta charset="UTF-8" present

All scripts render correctly ✓

🇮🇳 Hindi: नमस्ते दुनिया

🇯🇵 Japanese: こんにちは世界

🇨🇳 Chinese: 你好世界

🇸🇦 Arabic: مرحبا بالعالم

🇷🇺 Russian: Привет мир

🇬🇷 Greek: Γεια σου κόσμε

Symbols: © ® ™ € £ ¥ ₹ § ¶ ∞ ≠ ≤ ≥ ±

Quotes: "curly" 'single' « guillemets »

Emoji: 🚀🎉✅🔥💻

`; + + const brokenHtml = `
✗ No meta charset – browser guesses encoding

Text may appear garbled ✗

Hindi attempt: नमसà¥à¤¤à¥‡ (should be नमस्ते)

Quotes attempt: “curly� (should be "curly")

Copyright: © (should be ©)

Euro: € (should be €)

Dash: â€" (should be —)

⚠️ Without <meta charset="UTF-8"> the browser defaults to ISO-8859-1 or Windows-1252, mangling multi-byte UTF-8 characters into garbage bytes.
`; + + return ( +
+

+ <meta charset="UTF-8"> — Working vs Not Working +

+

+ The meta charset tag + tells the browser which character encoding the page uses. Without it, browsers + guess — often incorrectly — causing non-Latin scripts and special symbols to appear as garbled bytes. + Always place it as the very first tag inside <head>. +

+ + + + +
+
+ Live Preview: +
+
+ + +
+