bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
17 lines • 16.6 kB
JSON
{
"name": "inscriptions-components-htmlinscriptionbutton",
"type": "registry:component",
"dependencies": [
"js-1sat-ord"
],
"devDependencies": [],
"registryDependencies": [],
"files": [
{
"path": "components/inscriptions/components/HTMLInscriptionButton.tsx",
"type": "registry:component",
"content": "/**\n * HTMLInscriptionButton Component\n *\n * Component for creating HTML inscriptions with live preview\n * Supports HTML5 content with sandboxed iframe preview\n */\n\nimport {\n\tCodeIcon,\n\tExclamationTriangleIcon,\n\tEyeOpenIcon,\n} from \"@radix-ui/react-icons\";\nimport type { PreMAP } from \"js-1sat-ord\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { cn } from \"../../../lib/utils.js\";\nimport { Badge } from \"../../ui/badge.js\";\nimport { Button } from \"../../ui/button.js\";\nimport { Input } from \"../../ui/input.js\";\nimport { Switch } from \"../../ui/switch.js\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../../ui/tabs.js\";\nimport { Textarea } from \"../../ui/textarea.js\";\nimport { InscriptionButton } from \"./InscriptionButton.js\";\n\nconst HTML_TEMPLATES = {\n\tblank: \"\",\n\tbasic: `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>My Inscription</title>\n <style>\n body {\n font-family: Arial, sans-serif;\n margin: 20px;\n background-color: #f0f0f0;\n }\n h1 {\n color: #333;\n }\n </style>\n</head>\n<body>\n <h1>Hello, Blockchain!</h1>\n <p>This is my HTML inscription.</p>\n</body>\n</html>`,\n\tinteractive: `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Interactive Inscription</title>\n <style>\n body {\n font-family: Arial, sans-serif;\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n margin: 0;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n }\n .container {\n text-align: center;\n padding: 2rem;\n background: white;\n border-radius: 10px;\n box-shadow: 0 10px 30px rgba(0,0,0,0.2);\n }\n button {\n padding: 10px 20px;\n font-size: 16px;\n background: #667eea;\n color: white;\n border: none;\n border-radius: 5px;\n cursor: pointer;\n transition: transform 0.2s;\n }\n button:hover {\n transform: scale(1.05);\n }\n #counter {\n font-size: 48px;\n margin: 20px 0;\n color: #667eea;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <h1>Interactive Counter</h1>\n <div id=\"counter\">0</div>\n <button onclick=\"increment()\">Click Me!</button>\n </div>\n <script>\n let count = 0;\n function increment() {\n count++;\n document.getElementById('counter').textContent = count;\n }\n </script>\n</body>\n</html>`,\n\tcanvas: `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Canvas Art</title>\n <style>\n body {\n margin: 0;\n padding: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n background: #000;\n }\n canvas {\n border: 2px solid #fff;\n }\n </style>\n</head>\n<body>\n <canvas id=\"canvas\" width=\"400\" height=\"400\"></canvas>\n <script>\n const canvas = document.getElementById('canvas');\n const ctx = canvas.getContext('2d');\n \n // Create gradient\n const gradient = ctx.createLinearGradient(0, 0, 400, 400);\n gradient.addColorStop(0, '#ff006e');\n gradient.addColorStop(0.5, '#8338ec');\n gradient.addColorStop(1, '#3a86ff');\n \n // Draw circles\n for (let i = 0; i < 50; i++) {\n ctx.beginPath();\n ctx.arc(\n Math.random() * 400,\n Math.random() * 400,\n Math.random() * 50 + 10,\n 0,\n Math.PI * 2\n );\n ctx.fillStyle = gradient;\n ctx.globalAlpha = Math.random() * 0.5 + 0.1;\n ctx.fill();\n }\n </script>\n</body>\n</html>`,\n};\n\nexport interface HTMLInscriptionButtonProps {\n\t/** Initial HTML content */\n\tinitialHTML?: string;\n\t/** Button text */\n\tbuttonText?: string;\n\t/** Dialog title */\n\tdialogTitle?: string;\n\t/** Button variant */\n\tvariant?: \"solid\" | \"soft\" | \"outline\" | \"ghost\";\n\t/** Button size */\n\tsize?: \"1\" | \"2\" | \"3\" | \"4\";\n\t/** Button color */\n\tcolor?: \"blue\" | \"green\" | \"red\" | \"gray\";\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/** Whether button is disabled */\n\tdisabled?: boolean;\n\t/** Show fee estimate */\n\tshowFeeEstimate?: boolean;\n\t/** Max HTML length */\n\tmaxLength?: number;\n\t/** Enable templates */\n\tenableTemplates?: boolean;\n\t/** Allow scripts in preview */\n\tallowScriptsInPreview?: boolean;\n\t/** Success callback */\n\tonSuccess?: (txid: string, html: string) => void;\n\t/** Error callback */\n\tonError?: (error: Error) => void;\n}\n\nexport function HTMLInscriptionButton({\n\tinitialHTML = \"\",\n\tbuttonText = \"Inscribe HTML\",\n\tdialogTitle = \"Create HTML Inscription\",\n\tvariant = \"solid\",\n\tsize = \"2\",\n\tcolor = \"blue\",\n\tclassName = \"\",\n\tdisabled = false,\n\tshowFeeEstimate = true,\n\tmaxLength = 500000, // ~500KB\n\tenableTemplates = true,\n\tallowScriptsInPreview = true,\n\tonSuccess,\n\tonError,\n}: HTMLInscriptionButtonProps) {\n\tconst [html, setHTML] = useState(initialHTML);\n\tconst [selectedTab, setSelectedTab] = useState(\"editor\");\n\tconst [previewUrl, setPreviewUrl] = useState<string>(\"\");\n\tconst [addMetadata, setAddMetadata] = useState(false);\n\tconst [metadataTitle, setMetadataTitle] = useState(\"\");\n\n\t// Validation\n\tconst [errors, setErrors] = useState<Record<string, string>>({});\n\n\t// Generate preview URL\n\tuseEffect(() => {\n\t\tif (html && html.length < 8000) {\n\t\t\t// Base64 encode for preview\n\t\t\tconst base64 = btoa(unescape(encodeURIComponent(html)));\n\t\t\tsetPreviewUrl(`https://ordfs.network/preview/${base64}`);\n\t\t} else {\n\t\t\tsetPreviewUrl(\"\");\n\t\t}\n\t}, [html]);\n\n\t// Extract title from HTML\n\tuseEffect(() => {\n\t\tconst titleMatch = html.match(/<title>(.*?)<\\/title>/i);\n\t\tif (titleMatch && !metadataTitle) {\n\t\t\tsetMetadataTitle(titleMatch[1] || \"\");\n\t\t}\n\t}, [html, metadataTitle]);\n\n\t// Generate metadata\n\tconst generateMetadata = useCallback((): PreMAP | undefined => {\n\t\tif (!addMetadata) return undefined;\n\n\t\tconst metadata: PreMAP = {\n\t\t\tapp: \"bigblocks\",\n\t\t\ttype: \"inscription\",\n\t\t\tsubType: \"html\",\n\t\t};\n\n\t\tif (metadataTitle) {\n\t\t\tmetadata.name = metadataTitle;\n\t\t}\n\n\t\t// Check for features\n\t\tif (html.includes(\"<script\")) metadata.hasScripts = \"true\";\n\t\tif (html.includes(\"<canvas\")) metadata.hasCanvas = \"true\";\n\t\tif (html.includes(\"<video\") || html.includes(\"<audio\"))\n\t\t\tmetadata.hasMedia = \"true\";\n\n\t\treturn metadata;\n\t}, [addMetadata, metadataTitle, html]);\n\n\t// Validate HTML\n\tconst validateForm = useCallback((): boolean => {\n\t\tconst newErrors: Record<string, string> = {};\n\n\t\tif (!html || html.trim().length === 0) {\n\t\t\tnewErrors.html = \"HTML content is required\";\n\t\t} else if (html.length > maxLength) {\n\t\t\tnewErrors.html = `HTML exceeds maximum length of ${maxLength} characters`;\n\t\t}\n\n\t\t// Basic HTML validation\n\t\tconst openTags = (html.match(/<[^/][^>]*>/g) || []).length;\n\t\tconst closeTags = (html.match(/<\\/[^>]+>/g) || []).length;\n\t\tif (Math.abs(openTags - closeTags) > 5) {\n\t\t\tnewErrors.html = \"HTML appears to have unclosed tags\";\n\t\t}\n\n\t\tsetErrors(newErrors);\n\t\treturn Object.keys(newErrors).length === 0;\n\t}, [html, maxLength]);\n\n\t// Handle template selection\n\tconst handleTemplateSelect = useCallback(\n\t\t(templateKey: keyof typeof HTML_TEMPLATES) => {\n\t\t\tsetHTML(HTML_TEMPLATES[templateKey]);\n\t\t},\n\t\t[],\n\t);\n\n\t// Handle success\n\tconst handleSuccess = useCallback(\n\t\t(txid: string) => {\n\t\t\tonSuccess?.(txid, html);\n\t\t},\n\t\t[html, onSuccess],\n\t);\n\n\t// Calculate HTML size\n\tconst htmlSize = useMemo(() => {\n\t\tconst bytes = new Blob([html]).size;\n\t\tif (bytes < 1024) return `${bytes} bytes`;\n\t\tif (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n\t\treturn `${(bytes / 1024 / 1024).toFixed(2)} MB`;\n\t}, [html]);\n\n\t// Check for potentially dangerous content\n\tconst hasScripts = html.includes(\"<script\");\n\tconst hasIframes = html.includes(\"<iframe\");\n\tconst hasExternalResources =\n\t\thtml.includes(\"http://\") || html.includes(\"https://\");\n\n\treturn (\n\t\t<InscriptionButton\n\t\t\tbuttonText={buttonText}\n\t\t\tdialogTitle={dialogTitle}\n\t\t\tvariant={variant}\n\t\t\tsize={size}\n\t\t\tcolor={color}\n\t\t\tclassName={className}\n\t\t\tdisabled={disabled}\n\t\t\ticon={<CodeIcon />}\n\t\t\tcontent={validateForm() ? html : undefined}\n\t\t\tcontentType=\"text/html\"\n\t\t\tmetadata={generateMetadata()}\n\t\t\tshowFeeEstimate={showFeeEstimate}\n\t\t\tonSuccess={handleSuccess}\n\t\t\tonError={onError}\n\t\t>\n\t\t\t<Tabs value={selectedTab} onValueChange={setSelectedTab}>\n\t\t\t\t<TabsList>\n\t\t\t\t\t<TabsTrigger value=\"editor\">\n\t\t\t\t\t\t<CodeIcon className=\"mr-1 h-4 w-4\" />\n\t\t\t\t\t\tEditor\n\t\t\t\t\t</TabsTrigger>\n\t\t\t\t\t<TabsTrigger value=\"preview\" disabled={!html || html.length > 8000}>\n\t\t\t\t\t\t<EyeOpenIcon className=\"mr-1 h-4 w-4\" />\n\t\t\t\t\t\tPreview\n\t\t\t\t\t</TabsTrigger>\n\t\t\t\t\t{enableTemplates && (\n\t\t\t\t\t\t<TabsTrigger value=\"templates\">Templates</TabsTrigger>\n\t\t\t\t\t)}\n\t\t\t\t</TabsList>\n\n\t\t\t\t<div className=\"mt-3\">\n\t\t\t\t\t<TabsContent value=\"editor\">\n\t\t\t\t\t\t<div className=\"flex flex-col gap-3\">\n\t\t\t\t\t\t\t{/* HTML editor */}\n\t\t\t\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t<div className=\"flex justify-between items-center\">\n\t\t\t\t\t\t\t\t\t<p className=\"text-sm font-medium\">HTML Content</p>\n\t\t\t\t\t\t\t\t\t<Badge\n\t\t\t\t\t\t\t\t\t\tvariant={\n\t\t\t\t\t\t\t\t\t\t\thtml.length > maxLength ? \"destructive\" : \"secondary\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{html.length.toLocaleString()} /{\" \"}\n\t\t\t\t\t\t\t\t\t\t{maxLength.toLocaleString()} chars\n\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<Textarea\n\t\t\t\t\t\t\t\t\tplaceholder=\"Enter your HTML content...\"\n\t\t\t\t\t\t\t\t\tvalue={html}\n\t\t\t\t\t\t\t\t\tonChange={(e) => setHTML(e.target.value)}\n\t\t\t\t\t\t\t\t\tclassName=\"min-h-[300px] font-mono text-xs\"\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t{errors.html && (\n\t\t\t\t\t\t\t\t\t<p className=\"text-xs text-red-600\">{errors.html}</p>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">{htmlSize}</p>\n\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t{/* Warnings */}\n\t\t\t\t\t\t\t{(hasScripts || hasIframes || hasExternalResources) && (\n\t\t\t\t\t\t\t\t<div className=\"flex flex-col gap-2 p-3 bg-orange-50 dark:bg-orange-950/20 rounded-lg border border-orange-200 dark:border-orange-800\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t<ExclamationTriangleIcon className=\"h-4 w-4 text-orange-600 dark:text-orange-400\" />\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm font-medium text-orange-900 dark:text-orange-100\">\n\t\t\t\t\t\t\t\t\t\t\tContent Warnings\n\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"flex flex-col gap-1\">\n\t\t\t\t\t\t\t\t\t\t{hasScripts && (\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-xs text-orange-800 dark:text-orange-200\">\n\t\t\t\t\t\t\t\t\t\t\t\t• Contains JavaScript code\n\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t{hasIframes && (\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-xs text-orange-800 dark:text-orange-200\">\n\t\t\t\t\t\t\t\t\t\t\t\t• Contains embedded iframes\n\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t{hasExternalResources && (\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-xs text-orange-800 dark:text-orange-200\">\n\t\t\t\t\t\t\t\t\t\t\t\t• References external resources (may not load)\n\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t{/* Metadata */}\n\t\t\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t<Switch\n\t\t\t\t\t\t\t\t\tchecked={addMetadata}\n\t\t\t\t\t\t\t\t\tonCheckedChange={setAddMetadata}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<p className=\"text-sm\">Add Metadata</p>\n\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t{addMetadata && (\n\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\tplaceholder=\"Page Title\"\n\t\t\t\t\t\t\t\t\tvalue={metadataTitle}\n\t\t\t\t\t\t\t\t\tonChange={(e) => setMetadataTitle(e.target.value || \"\")}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</TabsContent>\n\n\t\t\t\t\t<TabsContent value=\"preview\">\n\t\t\t\t\t\t<div className=\"flex flex-col gap-3\">\n\t\t\t\t\t\t\t{html.length > 8000 ? (\n\t\t\t\t\t\t\t\t<div className=\"flex items-center justify-center p-6 bg-muted rounded-lg border\">\n\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\tHTML too large for preview (max 8KB)\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t) : previewUrl ? (\n\t\t\t\t\t\t\t\t<div className=\"relative\">\n\t\t\t\t\t\t\t\t\t<iframe\n\t\t\t\t\t\t\t\t\t\tsrc={previewUrl}\n\t\t\t\t\t\t\t\t\t\ttitle=\"HTML Preview\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"w-full h-[400px] border rounded-lg bg-background\"\n\t\t\t\t\t\t\t\t\t\tsandbox={\n\t\t\t\t\t\t\t\t\t\t\tallowScriptsInPreview ? \"allow-scripts\" : undefined\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<Badge\n\t\t\t\t\t\t\t\t\t\tclassName=\"absolute top-2 right-2\"\n\t\t\t\t\t\t\t\t\t\tvariant={allowScriptsInPreview ? \"outline\" : \"secondary\"}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{allowScriptsInPreview\n\t\t\t\t\t\t\t\t\t\t\t? \"Scripts Enabled\"\n\t\t\t\t\t\t\t\t\t\t\t: \"Scripts Disabled\"}\n\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<div className=\"flex items-center justify-center p-6 bg-muted rounded-lg border\">\n\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\tNo content to preview\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</TabsContent>\n\n\t\t\t\t\t{enableTemplates && (\n\t\t\t\t\t\t<TabsContent value=\"templates\">\n\t\t\t\t\t\t\t<div className=\"flex flex-col gap-3\">\n\t\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\t\tChoose a template to get started:\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t\t\t\tonClick={() => handleTemplateSelect(\"blank\")}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tBlank HTML\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t\t\t\tonClick={() => handleTemplateSelect(\"basic\")}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tBasic Page\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t\t\t\tonClick={() => handleTemplateSelect(\"interactive\")}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tInteractive Demo\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t\t\t\tonClick={() => handleTemplateSelect(\"canvas\")}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tCanvas Art\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</TabsContent>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</Tabs>\n\t\t</InscriptionButton>\n\t);\n}\n",
"target": "<%- config.aliases.components %>/htmlinscriptionbutton.tsx"
}
]
}