From d3753c98aa67de7e085cc07747d125b0d82c4d15 Mon Sep 17 00:00:00 2001 From: nousantx Date: Sat, 16 Nov 2024 19:37:54 +0700 Subject: [PATCH 1/3] Add preact app for better UI and performance --- app/.gitignore | 24 ++ app/index.html | 20 + app/package.json | 23 ++ app/public/vite.svg | 1 + app/src/app.jsx | 348 ++++++++++++++++ app/src/assets/preact.svg | 1 + app/src/index.css | 12 + app/src/lib/color.js | 28 ++ app/src/lib/image.js | 0 app/src/lib/init.js | 91 +++++ app/src/lib/saveToFile.js | 0 app/src/main.jsx | 5 + app/src/script.js | 51 +++ app/vite.config.js | 7 + app/yarn.lock | 812 ++++++++++++++++++++++++++++++++++++++ 15 files changed, 1423 insertions(+) create mode 100644 app/.gitignore create mode 100644 app/index.html create mode 100644 app/package.json create mode 100644 app/public/vite.svg create mode 100644 app/src/app.jsx create mode 100644 app/src/assets/preact.svg create mode 100644 app/src/index.css create mode 100644 app/src/lib/color.js create mode 100644 app/src/lib/image.js create mode 100644 app/src/lib/init.js create mode 100644 app/src/lib/saveToFile.js create mode 100644 app/src/main.jsx create mode 100644 app/src/script.js create mode 100644 app/vite.config.js create mode 100644 app/yarn.lock diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..c310252 --- /dev/null +++ b/app/index.html @@ -0,0 +1,20 @@ + + + + + + + Vite + Preact + + + + + +
+ + + diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..1d2845a --- /dev/null +++ b/app/package.json @@ -0,0 +1,23 @@ +{ + "name": "app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "preact": "^10.24.3" + }, + "devDependencies": { + "@nousantx/color-generator": "^1.4.1", + "@nousantx/list-attribute": "^0.1.0", + "@nousantx/someutils": "^0.3.1", + "@preact/preset-vite": "^2.9.1", + "@tenoxui/core": "^1.3.0-alpha.3", + "@tenoxui/property": "^1.4.5", + "vite": "^5.4.10" + } +} diff --git a/app/public/vite.svg b/app/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/app/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/app.jsx b/app/src/app.jsx new file mode 100644 index 0000000..a0e4a84 --- /dev/null +++ b/app/src/app.jsx @@ -0,0 +1,348 @@ +import { useLayoutEffect, useState, useRef } from 'preact/hooks' +import { init, tenoxuiConfig } from './lib/init' +import { MakeTenoxUI } from '@tenoxui/core/full' + +export function App() { + init() + const [htmlContent, setHtmlContent] = useState('
') + const [error, setError] = useState('') + const [scale, setScale] = useState(1) + const [outputFormat, setOutputFormat] = useState('png') + const [width, setWidth] = useState(1000) + const [height, setHeight] = useState(600) + + const canvasRef = useRef(null) + const previewRef = useRef(null) + const fileInputRef = useRef(null) + + function saveDesign() { + try { + const designData = { + html: htmlContent, + width, + height, + scale, + format: outputFormat + } + + const blob = new Blob([JSON.stringify(designData, null, 2)], { type: 'application/json' }) + const link = document.createElement('a') + link.href = URL.createObjectURL(blob) + link.download = 'tenoxui.design.json' + link.click() + URL.revokeObjectURL(link.href) + } catch (error) { + setError(`Failed to save design: ${error.message}`) + } + } + + function loadDesign(event) { + const file = event.target.files[0] + if (!file) return + + const reader = new FileReader() + reader.onload = (e) => { + try { + const designData = JSON.parse(e.target.result) + setHtmlContent(designData.html) + setWidth(designData.width || 1000) + setHeight(designData.height || 1000) + setScale(parseFloat(designData.scale) || 1) + setOutputFormat(designData.format || 'png') + + // Reset file input so the same file can be loaded again + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + + // Generate preview after a short delay to ensure state updates have propagated + setTimeout(() => { + generateImage() + }, 100) + } catch (error) { + setError(`Failed to load design file: ${error.message}`) + } + } + reader.readAsText(file) + } + + async function getGoogleFontsStyles() { + const googleFontsLink = document.getElementById('google-fonts') + if (!googleFontsLink) return '' + + try { + const response = await fetch(googleFontsLink.href) + const css = await response.text() + const fontFaceRules = [] + const fontFaceRegex = /@font-face\s*{[^}]+}/g + const matches = css.match(fontFaceRegex) + + if (matches) { + for (const rule of matches) { + const urlMatch = rule.match(/url\(([^)]+)\)/) + if (urlMatch) { + let fontUrl = urlMatch[1].replace(/['\"]/g, '') + try { + const fontResponse = await fetch(fontUrl) + const fontBuffer = await fontResponse.arrayBuffer() + const base64Font = btoa(String.fromCharCode(...new Uint8Array(fontBuffer))) + const fontFormat = fontUrl.endsWith('woff2') + ? 'woff2' + : fontUrl.endsWith('woff') + ? 'woff' + : fontUrl.endsWith('ttf') + ? 'truetype' + : 'opentype' + const processedRule = rule.replace( + /url\([^)]+\)/, + `url(data:application/font-${fontFormat};charset=utf-8;base64,${base64Font})` + ) + fontFaceRules.push(processedRule) + } catch (error) { + console.warn('Failed to fetch font:', fontUrl, error) + fontFaceRules.push(rule) + } + } + } + } + + return fontFaceRules.join('\n') + } catch (error) { + console.error('Failed to process Google Fonts:', error) + return '' + } + } + + async function generateSVG() { + try { + const fontFaceRules = await getGoogleFontsStyles() + const scaledWidth = width * scale + const scaledHeight = height * scale + const temp = document.createElement('div') + temp.innerHTML = htmlContent + + const contentDiv = temp.querySelector('div') + if (contentDiv) { + contentDiv.classList.add(`[transform]-[scale(${scale})]`, '[transform-origin]-[top_left]') + } + + temp.querySelectorAll('*').forEach((element) => { + new MakeTenoxUI({ element, ...tenoxuiConfig }).useDOM() + }) + + // Remove unnecessary attributes + const removeAttributesAndElements = (element) => { + if (element.tagName.toLowerCase() !== 'style') { + Array.from(element.attributes).forEach((attr) => { + if (attr.name !== 'style') { + element.removeAttribute(attr.name) + } + }) + Array.from(element.children).forEach((child) => { + if (child.tagName.toLowerCase() !== 'style') { + removeAttributesAndElements(child) + } + }) + } + } + removeAttributesAndElements(temp) + + const svgData = ` + + + + + + +
${temp.innerHTML}
+
+
` + + return svgData.replace(/>\s+<') + } catch (error) { + throw new Error(`SVG generation failed: ${error.message}`) + } + } + + async function generateImage() { + try { + setError('') + const ctx = canvasRef.current.getContext('2d') + canvasRef.current.width = width + canvasRef.current.height = height + + // Calculate scaled dimensions + const scaledWidth = width * scale + const scaledHeight = height * scale + + canvasRef.current.width = scaledWidth + canvasRef.current.height = scaledHeight + + ctx.clearRect(0, 0, scaledWidth, scaledHeight) + + const svgData = await generateSVG() + const img = new Image() + img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData) + + await new Promise((resolve, reject) => { + img.onload = resolve + img.onerror = reject + }) + + ctx.drawImage(img, 0, 0) + } catch (error) { + setError(`Failed to generate image: ${error.message}`) + } + } + + async function downloadImage() { + try { + setError('') + const link = document.createElement('a') + link.download = `generated-image.${outputFormat}` + + if (outputFormat === 'svg') { + const svgData = await generateSVG() + const blob = new Blob([svgData], { type: 'image/svg+xml' }) + link.href = URL.createObjectURL(blob) + } else { + await generateImage() + link.href = canvasRef.current.toDataURL(`image/${outputFormat}`) + } + + link.click() + + if (outputFormat === 'svg') { + URL.revokeObjectURL(link.href) + } + } catch (error) { + setError(`Failed to download image: ${error.message}`) + } + } + + useLayoutEffect(() => { + if (!previewRef.current) return + const tuiInstances = new Map() + + function initializeTenoxUI(config) { + tuiInstances.clear() + previewRef.current.querySelectorAll('*').forEach((element) => { + const instance = new MakeTenoxUI({ + element, + ...config + }).useDOM() + tuiInstances.set(element, instance) + }) + } + + previewRef.current.innerHTML = htmlContent + + try { + initializeTenoxUI(tenoxuiConfig) + } catch (error) { + console.error('Error initializing TenoxUI:', error) + setError(`TenoxUI initialization failed: ${error.message}`) + } + + return () => { + tuiInstances.clear() + } + }, [htmlContent]) + + return ( +
+
+ +