Remove HTML Preview Tool from navigation and add to gitignore

- Removed HTML Preview Tool from navigation menu in Layout.js
- Cleaned up unused Code import
- Added HTML Preview related files to .gitignore
- Project builds successfully without HTML Preview Tool
This commit is contained in:
dwindown
2025-08-04 13:04:11 +07:00
parent e1bc8d193d
commit 76c0a0d014
9 changed files with 897 additions and 159 deletions

12
.gitignore vendored
View File

@@ -8,6 +8,7 @@ node_modules/
# Production
/build
/backup
# Misc
.DS_Store
@@ -21,6 +22,17 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
# HTML Preview Tool (abandoned)
src/pages/HtmlPreviewTool.js
src/pages/HtmlPreviewTool.js.backup
src/pages/components/PreviewFrame*.js
src/pages/components/ElementEditor.js
src/pages/components/InspectorSidebar.js
src/pages/components/Toolbar.js
src/pages/components/CodeInputs.js
src/pages/components/SimplePreviewFrame.js
src/pages/components/PreviewServer.js
# Runtime data
pids
*.pid

322
package-lock.json generated
View File

@@ -8,14 +8,23 @@
"name": "developer-tools-mvp",
"version": "1.0.0",
"dependencies": {
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/search": "^6.5.11",
"@codemirror/theme-one-dark": "^6.1.3",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"@uiw/react-codemirror": "^4.24.2",
"codemirror": "^5.65.19",
"diff-match-patch": "^1.0.5",
"js-beautify": "^1.14.9",
"js-beautify": "^1.15.4",
"lucide-react": "^0.263.1",
"papaparse": "^5.4.1",
"react": "^18.2.0",
"react-codemirror2": "^8.0.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
@@ -2112,6 +2121,144 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"license": "MIT"
},
"node_modules/@codemirror/autocomplete": {
"version": "6.18.6",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
"integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-css": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/css": "^1.1.7"
}
},
"node_modules/@codemirror/lang-html": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/css": "^1.1.0",
"@lezer/html": "^1.3.0"
}
},
"node_modules/@codemirror/lang-javascript": {
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/javascript": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz",
"integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.8.5",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
"integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.5.11",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
"integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.38.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
"integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@csstools/normalize.css": {
"version": "12.1.1",
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz",
@@ -3774,6 +3921,69 @@
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
"license": "MIT"
},
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
},
"node_modules/@lezer/css": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz",
"integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/html": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/javascript": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz",
"integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -5081,6 +5291,74 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@uiw/codemirror-extensions-basic-setup": {
"version": "4.24.2",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.24.2.tgz",
"integrity": "sha512-wW/gjLRvVUeYyhdh2TApn25cvdcR+Rhg6R/j3eTOvXQzU1HNzNYCVH4YKVIfgtfdM/Xs+N8fkk+rbr1YvBppCg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@codemirror/autocomplete": ">=6.0.0",
"@codemirror/commands": ">=6.0.0",
"@codemirror/language": ">=6.0.0",
"@codemirror/lint": ">=6.0.0",
"@codemirror/search": ">=6.0.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/view": ">=6.0.0"
}
},
"node_modules/@uiw/react-codemirror": {
"version": "4.24.2",
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.24.2.tgz",
"integrity": "sha512-kp7DhTq4RR+M2zJBQBrHn1dIkBrtOskcwJX4vVsKGByReOvfMrhqRkGTxYMRDTX6x75EG2mvBJPDKYcUQcHWBw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.6",
"@codemirror/commands": "^6.1.0",
"@codemirror/state": "^6.1.1",
"@codemirror/theme-one-dark": "^6.0.0",
"@uiw/codemirror-extensions-basic-setup": "4.24.2",
"codemirror": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.11.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/theme-one-dark": ">=6.0.0",
"@codemirror/view": ">=6.0.0",
"codemirror": ">=6.0.0",
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@uiw/react-codemirror/node_modules/codemirror": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
@@ -6786,6 +7064,12 @@
"node": ">=4"
}
},
"node_modules/codemirror": {
"version": "5.65.19",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.19.tgz",
"integrity": "sha512-+aFkvqhaAVr1gferNMuN8vkTSrWIFvzlMV9I2KBLCWS2WpZ2+UAkZjlMZmEuT+gcXTi6RrGQCkWq1/bDtGqhIA==",
"license": "MIT"
},
"node_modules/collect-v8-coverage": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
@@ -7033,6 +7317,12 @@
"node": ">=10"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -17075,6 +17365,16 @@
"node": ">=14"
}
},
"node_modules/react-codemirror2": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-8.0.1.tgz",
"integrity": "sha512-ZALowE5sGK1t66i0Fm1hoJLWT/iZHNjaAmcjwgYl9gyl2v1sqWwxzWHLmgq6K9KCk7p5+I+U3jMF1vsLaIkrmA==",
"license": "MIT",
"peerDependencies": {
"codemirror": "5.x",
"react": ">=15.5 <=18.x"
}
},
"node_modules/react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@@ -19048,6 +19348,12 @@
"webpack": "^5.0.0"
}
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"license": "MIT"
},
"node_modules/stylehacks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
@@ -19834,9 +20140,9 @@
}
},
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"license": "Apache-2.0",
"peer": true,
"bin": {
@@ -19844,7 +20150,7 @@
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
"node": ">=4.2.0"
}
},
"node_modules/unbox-primitive": {
@@ -20096,6 +20402,12 @@
"browser-process-hrtime": "^1.0.0"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/w3c-xmlserializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",

View File

@@ -4,24 +4,33 @@
"description": "Web Developer Tools MVP - Utilities Toolkit",
"private": true,
"dependencies": {
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/search": "^6.5.11",
"@codemirror/theme-one-dark": "^6.1.3",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"@uiw/react-codemirror": "^4.24.2",
"codemirror": "^5.65.19",
"diff-match-patch": "^1.0.5",
"js-beautify": "^1.15.4",
"lucide-react": "^0.263.1",
"papaparse": "^5.4.1",
"react": "^18.2.0",
"react-codemirror2": "^8.0.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"js-beautify": "^1.14.9",
"diff-match-patch": "^1.0.5",
"papaparse": "^5.4.1",
"serialize-javascript": "^6.0.0",
"lucide-react": "^0.263.1",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"tailwindcss": "^3.3.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.24"
"postcss": "^8.4.24",
"tailwindcss": "^3.3.0"
},
"scripts": {
"start": "react-scripts start",

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Code2, Home, ChevronDown, Menu, X, Database, FileText, Link as LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare, Code } from 'lucide-react';
import { Home, Hash, FileText, Key, Palette, QrCode, FileSpreadsheet, Wand2, GitCompare, Menu, X } from 'lucide-react';
import ThemeToggle from './ThemeToggle';
const Layout = ({ children }) => {
@@ -41,7 +41,7 @@ const Layout = ({ children }) => {
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },
{ path: '/beautifier', name: 'Beautifier Tool', icon: Wand2, description: 'Beautify/minify code' },
{ path: '/diff', name: 'Diff Tool', icon: GitCompare, description: 'Compare text differences' },
{ path: '/html-preview', name: 'HTML Preview', icon: Code, description: 'Render HTML, CSS, JS' }
];
return (

View File

@@ -316,7 +316,7 @@ const HtmlPreviewTool = () => {
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Code Editor</h2>
</div>
<div className="flex-1 p-4 overflow-y-auto">
<div className="flex-1 flex flex-col p-4">
<CodeInputs
htmlInput={htmlInput}
setHtmlInput={setHtmlInput}
@@ -381,7 +381,7 @@ const HtmlPreviewTool = () => {
<ToolLayout title="HTML Preview Tool">
<div className={`flex h-full ${inspectedElementInfo ? 'gap-4' : 'gap-6'}`}>
{/* Left column - Code inputs */}
<div className={`space-y-6 transition-all duration-300 ${
<div className={`flex flex-col transition-all duration-300 ${
inspectedElementInfo ? 'flex-1' : 'w-1/2'
}`}>
<CodeInputs

View File

@@ -1,5 +1,15 @@
import React, { useState } from 'react';
import { ChevronDown, ChevronUp, Maximize2, Minimize2 } from 'lucide-react';
import React, { useState, useRef } from 'react';
import { Search, Copy, Download } from 'lucide-react';
import { Controlled as CodeMirror } from 'react-codemirror2';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material.css';
import 'codemirror/mode/xml/xml';
import 'codemirror/mode/css/css';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/addon/search/search';
import 'codemirror/addon/search/searchcursor';
import 'codemirror/addon/dialog/dialog';
import 'codemirror/addon/dialog/dialog.css';
const CodeInputs = ({
htmlInput,
@@ -10,130 +20,193 @@ const CodeInputs = ({
setJsInput,
isFullscreen
}) => {
const [showCss, setShowCss] = useState(false);
const [showJs, setShowJs] = useState(false);
const [htmlExtended, setHtmlExtended] = useState(true); // Default: HTML box extended
const [cssExtended, setCssExtended] = useState(false);
const [jsExtended, setJsExtended] = useState(false);
const [activeTab, setActiveTab] = useState('html');
const htmlEditorRef = useRef(null);
const cssEditorRef = useRef(null);
const jsEditorRef = useRef(null);
const getTextareaHeight = (type, isExtended) => {
if (isFullscreen) {
// In fullscreen, give HTML much more space as main script area
if (type === 'html') {
return isExtended ? 'h-[32rem]' : 'h-96'; // Much taller for HTML in fullscreen to balance with iPhone frame
} else {
return isExtended ? 'h-64' : 'h-32'; // CSS/JS remain smaller
}
// Handle search functionality
const handleSearch = (editorRef) => {
if (editorRef.current && editorRef.current.editor) {
editorRef.current.editor.execCommand('find');
}
};
// In non-fullscreen, give much more space for HTML as main script area
if (type === 'html') {
if (!showCss && !showJs) {
// When CSS/JS hidden, HTML gets maximum space
return isExtended ? 'h-[36rem]' : 'h-[28rem]'; // Much taller for main script to balance with iPhone frame
} else {
// When CSS/JS visible, HTML still gets substantial space
return isExtended ? 'h-[28rem]' : 'h-96';
// Handle copy functionality
const handleCopy = async (content) => {
try {
await navigator.clipboard.writeText(content);
} catch (err) {
console.error('Failed to copy:', err);
}
} else {
// CSS and JS boxes remain smaller
return isExtended ? 'h-64' : 'h-32';
};
// Handle export functionality
const handleExport = (content, filename) => {
const blob = new Blob([content], { type: 'text/plain' });
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);
};
// Get current editor ref based on active tab
const getCurrentEditorRef = () => {
switch (activeTab) {
case 'html': return htmlEditorRef;
case 'css': return cssEditorRef;
case 'js': return jsEditorRef;
default: return htmlEditorRef;
}
};
// Get current content based on active tab
const getCurrentContent = () => {
switch (activeTab) {
case 'html': return htmlInput;
case 'css': return cssInput;
case 'js': return jsInput;
default: return htmlInput;
}
};
// Get filename for export based on active tab
const getExportFilename = () => {
switch (activeTab) {
case 'html': return 'code.html';
case 'css': return 'styles.css';
case 'js': return 'script.js';
default: return 'code.txt';
}
};
return (
<div className="space-y-4">
{/* HTML Input */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
HTML
</label>
<div className="flex flex-col h-full">
{/* Tab Navigation */}
<div className="flex border-b border-gray-200 dark:border-gray-700">
{[
{ id: 'html', label: 'HTML' },
{ id: 'css', label: 'CSS' },
{ id: 'js', label: 'JavaScript' }
].map((tab) => (
<button
onClick={() => setHtmlExtended(!htmlExtended)}
className="flex items-center space-x-1 px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
title={htmlExtended ? 'Minimize HTML box' : 'Maximize HTML box'}
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
{htmlExtended ? <Minimize2 className="w-3 h-3" /> : <Maximize2 className="w-3 h-3" />}
<span>{htmlExtended ? 'Min' : 'Max'}</span>
{tab.label}
</button>
))}
</div>
{/* Action buttons above editor */}
<div className="flex items-center justify-end space-x-2 p-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => handleSearch(getCurrentEditorRef())}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
title="Search"
>
<Search className="w-3 h-3" />
Search
</button>
<button
onClick={() => handleCopy(getCurrentContent())}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
title="Copy"
>
<Copy className="w-3 h-3" />
Copy
</button>
<button
onClick={() => handleExport(getCurrentContent(), getExportFilename())}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
title="Export"
>
<Download className="w-3 h-3" />
Export
</button>
</div>
<textarea
{/* Code Editor */}
<div className="flex-1">
{activeTab === 'html' && (
<CodeMirror
ref={htmlEditorRef}
value={htmlInput}
onChange={(e) => setHtmlInput(e.target.value)}
placeholder="Enter your HTML code here..."
className={`w-full ${getTextareaHeight('html', htmlExtended)} 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-blue-500 focus:border-transparent`}
onBeforeChange={(editor, data, value) => setHtmlInput(value)}
options={{
mode: 'xml',
theme: 'material',
lineNumbers: true,
lineWrapping: true,
autoCloseTags: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2,
extraKeys: {
'Ctrl-F': 'findPersistent',
'Cmd-F': 'findPersistent'
}
}}
className="h-full"
/>
)}
{/* Toggle buttons for CSS and JS */}
<div className="flex space-x-2">
<button
onClick={() => setShowCss(!showCss)}
className="flex items-center space-x-1 px-3 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
{showCss ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
<span>CSS</span>
</button>
<button
onClick={() => setShowJs(!showJs)}
className="flex items-center space-x-1 px-3 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
{showJs ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
<span>JS</span>
</button>
</div>
</div>
{/* CSS Input */}
{showCss && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
CSS
</label>
<button
onClick={() => setCssExtended(!cssExtended)}
className="flex items-center space-x-1 px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
title={cssExtended ? 'Minimize CSS box' : 'Maximize CSS box'}
>
{cssExtended ? <Minimize2 className="w-3 h-3" /> : <Maximize2 className="w-3 h-3" />}
<span>{cssExtended ? 'Min' : 'Max'}</span>
</button>
</div>
<textarea
{activeTab === 'css' && (
<CodeMirror
ref={cssEditorRef}
value={cssInput}
onChange={(e) => setCssInput(e.target.value)}
placeholder="Enter your CSS code here..."
className={`w-full ${getTextareaHeight('css', cssExtended)} 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-blue-500 focus:border-transparent`}
onBeforeChange={(editor, data, value) => setCssInput(value)}
options={{
mode: 'css',
theme: 'material',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2,
extraKeys: {
'Ctrl-F': 'findPersistent',
'Cmd-F': 'findPersistent'
}
}}
className="h-full"
/>
</div>
)}
{/* JS Input */}
{showJs && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
JavaScript
</label>
<button
onClick={() => setJsExtended(!jsExtended)}
className="flex items-center space-x-1 px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
title={jsExtended ? 'Minimize JS box' : 'Maximize JS box'}
>
{jsExtended ? <Minimize2 className="w-3 h-3" /> : <Maximize2 className="w-3 h-3" />}
<span>{jsExtended ? 'Min' : 'Max'}</span>
</button>
</div>
<textarea
{activeTab === 'js' && (
<CodeMirror
ref={jsEditorRef}
value={jsInput}
onChange={(e) => setJsInput(e.target.value)}
placeholder="Enter your JavaScript code here..."
className={`w-full ${getTextareaHeight('js', jsExtended)} 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-blue-500 focus:border-transparent`}
onBeforeChange={(editor, data, value) => setJsInput(value)}
options={{
mode: 'javascript',
theme: 'material',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2,
extraKeys: {
'Ctrl-F': 'findPersistent',
'Cmd-F': 'findPersistent'
}
}}
className="h-full"
/>
</div>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,213 @@
import React, { useState, useRef } from 'react';
import { Search, Copy, Download } from 'lucide-react';
import { Controlled as CodeMirror } from 'react-codemirror2';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material.css';
import 'codemirror/mode/xml/xml';
import 'codemirror/mode/css/css';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/addon/search/search';
import 'codemirror/addon/search/searchcursor';
import 'codemirror/addon/dialog/dialog';
import 'codemirror/addon/dialog/dialog.css';
const CodeInputs = ({
htmlInput,
setHtmlInput,
cssInput,
setCssInput,
jsInput,
setJsInput,
isFullscreen
}) => {
const [activeTab, setActiveTab] = useState('html');
const htmlEditorRef = useRef(null);
const cssEditorRef = useRef(null);
const jsEditorRef = useRef(null);
// Handle search functionality
const handleSearch = (editorRef) => {
if (editorRef.current && editorRef.current.editor) {
editorRef.current.editor.execCommand('find');
}
};
// Handle copy functionality
const handleCopy = async (content) => {
try {
await navigator.clipboard.writeText(content);
} catch (err) {
console.error('Failed to copy:', err);
}
};
// Handle export functionality
const handleExport = (content, filename) => {
const blob = new Blob([content], { type: 'text/plain' });
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);
};
// Get current editor ref based on active tab
const getCurrentEditorRef = () => {
switch (activeTab) {
case 'html': return htmlEditorRef;
case 'css': return cssEditorRef;
case 'js': return jsEditorRef;
default: return htmlEditorRef;
}
};
// Get current content based on active tab
const getCurrentContent = () => {
switch (activeTab) {
case 'html': return htmlInput;
case 'css': return cssInput;
case 'js': return jsInput;
default: return htmlInput;
}
};
// Get filename for export based on active tab
const getExportFilename = () => {
switch (activeTab) {
case 'html': return 'code.html';
case 'css': return 'styles.css';
case 'js': return 'script.js';
default: return 'code.txt';
}
};
return (
<div className="flex flex-col h-full">
{/* Tab Navigation */}
<div className="flex border-b border-gray-200 dark:border-gray-700">
{[
{ id: 'html', label: 'HTML' },
{ id: 'css', label: 'CSS' },
{ id: 'js', label: 'JavaScript' }
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Action buttons above editor */}
<div className="flex items-center justify-end space-x-2 p-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => handleSearch(getCurrentEditorRef())}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
title="Search"
>
<Search className="w-3 h-3" />
Search
</button>
<button
onClick={() => handleCopy(getCurrentContent())}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
title="Copy"
>
<Copy className="w-3 h-3" />
Copy
</button>
<button
onClick={() => handleExport(getCurrentContent(), getExportFilename())}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
title="Export"
>
<Download className="w-3 h-3" />
Export
</button>
</div>
{/* Code Editor */}
<div className="flex-1">
{activeTab === 'html' && (
<CodeMirror
ref={htmlEditorRef}
value={htmlInput}
onBeforeChange={(editor, data, value) => setHtmlInput(value)}
options={{
mode: 'xml',
theme: 'material',
lineNumbers: true,
lineWrapping: true,
autoCloseTags: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2,
extraKeys: {
'Ctrl-F': 'findPersistent',
'Cmd-F': 'findPersistent'
}
}}
className="h-full"
/>
)}
{activeTab === 'css' && (
<CodeMirror
ref={cssEditorRef}
value={cssInput}
onBeforeChange={(editor, data, value) => setCssInput(value)}
options={{
mode: 'css',
theme: 'material',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2,
extraKeys: {
'Ctrl-F': 'findPersistent',
'Cmd-F': 'findPersistent'
}
}}
className="h-full"
/>
)}
{activeTab === 'js' && (
<CodeMirror
ref={jsEditorRef}
value={jsInput}
onBeforeChange={(editor, data, value) => setJsInput(value)}
options={{
mode: 'javascript',
theme: 'material',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2,
extraKeys: {
'Ctrl-F': 'findPersistent',
'Cmd-F': 'findPersistent'
}
}}
className="h-full"
/>
)}
</div>
</div>
);
};
export default CodeInputs;

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
import { Copy } from 'lucide-react';
const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameRef, selectedElementInfo }) => {
console.log('🔍 ELEMENT EDITOR: Received props:', { selectedElementInfo, previewFrameRef: !!previewFrameRef });
@@ -10,14 +11,14 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
if (selectedElementInfo) {
const elementInfo = {
tagName: selectedElementInfo.tagName,
innerText: selectedElementInfo.textContent || '',
innerHTML: selectedElementInfo.innerHTML || selectedElementInfo.textContent || '',
id: selectedElementInfo.attributes.id || '',
className: selectedElementInfo.attributes.class || '',
cascadeId: selectedElementInfo.cascadeId,
isContainer: selectedElementInfo.attributes.children?.length > 0 || isContainerElement(selectedElementInfo.tagName),
};
// Add all other attributes
// Add all other attributes (excluding cascadeId from display)
Object.entries(selectedElementInfo.attributes).forEach(([name, value]) => {
if (!['id', 'class', 'data-original', 'data-cascade-id'].includes(name)) {
elementInfo[name] = value;
@@ -68,7 +69,7 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
if (previewFrameRef?.current && edited?.cascadeId) {
let success = false;
if (field === 'innerText') {
if (field === 'innerHTML') {
success = previewFrameRef.current.updateElementText(edited.cascadeId, value);
} else if (field === 'className') {
success = previewFrameRef.current.updateElementClass(edited.cascadeId, value);
@@ -143,12 +144,48 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
if (!edited) return <p className="dark:text-gray-300">Loading editor...</p>;
const otherAttributes = Object.keys(edited || {}).filter(
key => key !== 'tagName' && key !== 'id' && key !== 'className' && key !== 'innerText' && key !== 'isContainer'
// Filter out system attributes for display (hide cascadeId)
const otherAttributes = Object.keys(edited).filter(key =>
!['tagName', 'innerHTML', 'id', 'className', 'cascadeId', 'isContainer'].includes(key)
);
// Handle copy element functionality
const handleCopyElement = async () => {
if (!edited) return;
try {
// Create a clean element string without cascade attributes
const elementString = `<${edited.tagName}${edited.id ? ` id="${edited.id}"` : ''}${edited.className ? ` class="${edited.className}"` : ''}${otherAttributes.map(attr => ` ${attr}="${edited[attr]}"`).join('')}>${edited.innerHTML || ''}</${edited.tagName}>`;
await navigator.clipboard.writeText(elementString);
console.log('✅ Element copied to clipboard');
} catch (err) {
console.error('❌ Failed to copy element:', err);
}
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Edit {edited.tagName.toUpperCase()}
</h3>
<div className="flex items-center space-x-2">
<button
onClick={handleCopyElement}
className="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
title="Copy element"
>
<Copy className="w-4 h-4" />
</button>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
</button>
</div>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Tag Name</label>
<textarea
@@ -189,34 +226,26 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
/>
</div>
{/* Only show innerText field for non-container elements */}
{!edited.isContainer && (
{/* Show innerHTML field for all elements */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Inner Text
<span className="text-xs text-gray-500 ml-2">(text content)</span>
Inner HTML
<span className="text-xs text-gray-500 ml-2">(HTML content inside element)</span>
</label>
<textarea
ref={(el) => textareaRefs.current.innerText = el}
value={edited.innerText || ''}
ref={(el) => textareaRefs.current.innerHTML = el}
value={edited.innerHTML || ''}
onChange={(e) => {
handleFieldChange('innerText', e.target.value);
handleFieldChange('innerHTML', e.target.value);
autoResizeTextarea(e.target);
}}
rows="2"
rows="3"
className="w-full p-2 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 overflow-hidden"
placeholder="Enter HTML content or text..."
/>
</div>
)}
{/* Show container hint for container elements */}
{edited.isContainer && (
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md">
<p className="text-sm text-blue-700 dark:text-blue-300">
📦 This is a container element. Edit its ID, class, or other attributes instead of text content.
</p>
</div>
)}
{otherAttributes.map(attr => (
<div key={attr} className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">{attr}</label>

View File

@@ -0,0 +1,90 @@
import React from 'react';
import { Smartphone, Tablet, Monitor, RotateCcw, Maximize, Minimize, Eye, EyeOff } from 'lucide-react';
const SimpleToolbar = ({
selectedDevice,
setSelectedDevice,
isFullscreen,
onRefresh,
onToggleFullscreen,
onToggleSidebar,
showSidebar,
isSidebarExpanded
}) => {
const devices = [
{ id: 'mobile', label: 'Mobile', icon: Smartphone },
{ id: 'tablet', label: 'Tablet', icon: Tablet },
{ id: 'desktop', label: 'Desktop', icon: Monitor }
];
return (
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center justify-between">
{/* Device selection */}
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 mr-3">
Device:
</span>
{devices.map((device) => {
const Icon = device.icon;
const isDisabled = isSidebarExpanded && device.id !== 'desktop';
return (
<button
key={device.id}
onClick={() => !isDisabled && setSelectedDevice(device.id)}
disabled={isDisabled}
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
selectedDevice === device.id
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
: isDisabled
? 'text-gray-400 dark:text-gray-600 cursor-not-allowed'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
title={isDisabled ? 'Disabled when sidebar is expanded' : `Switch to ${device.label} view`}
>
<Icon className="w-4 h-4" />
{device.label}
</button>
);
})}
</div>
{/* Action buttons */}
<div className="flex items-center space-x-2">
{/* Refresh button */}
<button
onClick={onRefresh}
className="flex items-center gap-2 px-3 py-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
title="Refresh preview"
>
<RotateCcw className="w-4 h-4" />
Refresh
</button>
{/* Sidebar toggle */}
<button
onClick={onToggleSidebar}
className="flex items-center gap-2 px-3 py-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
title={showSidebar ? 'Hide sidebar' : 'Show sidebar'}
>
{showSidebar ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
{showSidebar ? 'Hide' : 'Show'} Sidebar
</button>
{/* Fullscreen toggle */}
<button
onClick={onToggleFullscreen}
className="flex items-center gap-2 px-3 py-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
>
{isFullscreen ? <Minimize className="w-4 h-4" /> : <Maximize className="w-4 h-4" />}
{isFullscreen ? 'Exit' : 'Enter'} Fullscreen
</button>
</div>
</div>
</div>
);
};
export default SimpleToolbar;