@1urso/generic-editor 0.1.47 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18546,5 +18546,5 @@ const GenericEditor = (n) => /* @__PURE__ */ jsx(EditorProvider, {
18546
18546
  availableProps: n.layout.props,
18547
18547
  theme: n.theme,
18548
18548
  children: /* @__PURE__ */ jsx(EditorContent, { ...n })
18549
- }), generateHTML = (n, _, E = {}) => Function("elements", "data", "options", getRendererCode() + "\nreturn renderTemplate(elements, data, options);")(n, _, E), getRendererCode = () => "\n/**\n * Render Template\n * @param {Array} elements - The JSON configuration of elements\n * @param {Object|Array} data - The data object to inject (Object for single, Array for list)\n * @param {Object} options - { isList: boolean, listSettings: { sortProp: string, sortOrder: 'asc'|'desc', newestPosition: 'top'|'bottom', scrollDirection: 'up'|'down', containerHeight: number }, canvasHeight: number }\n * @returns {string} - The generated HTML string\n */\nfunction renderTemplate(elements, data, options = {}) {\n const { isList, listSettings, canvasHeight } = options;\n\n const measureTextHeight = (text, width, fontFamily, fontSize, lineHeightMultiplier = 1.2) => {\n if (!text) return 0;\n try {\n const canvas = document.createElement('canvas');\n const context = canvas.getContext('2d');\n if (!context) return 0;\n context.font = `${fontSize}px ${fontFamily}`;\n const words = String(text).split(' ');\n let line = '';\n let lineCount = 1;\n for (let i = 0; i < words.length; i++) {\n const testLine = line + words[i] + ' ';\n const metrics = context.measureText(testLine);\n const testWidth = metrics.width;\n if (testWidth > width && i > 0) {\n line = words[i] + ' ';\n lineCount++;\n } else {\n line = testLine;\n }\n }\n const explicitLines = String(text).split('\\n').length - 1;\n lineCount += explicitLines;\n return Math.ceil(lineCount * fontSize * lineHeightMultiplier);\n } catch (_) {\n return 0;\n }\n };\n\n const computeLayout = (elements, itemData) => {\n const layoutElements = JSON.parse(JSON.stringify(elements));\n \n const isInside = (inner, outer) => {\n const eps = 0.1;\n return (\n inner.x >= outer.x - eps &&\n inner.x + inner.width <= outer.x + outer.width + eps &&\n inner.y >= outer.y - eps &&\n inner.y + inner.height <= outer.y + outer.height + eps\n );\n };\n\n const autoGrowElements = layoutElements\n .filter(el => (el.type === 'text' || el.type === 'text-container') && el.autoGrow)\n .sort((a, b) => a.y - b.y);\n\n autoGrowElements.forEach(textEl => {\n let content = textEl.content;\n content = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {\n const val = itemData[key.trim()];\n return val !== undefined && val !== null ? String(val) : match;\n });\n \n const fontSize = parseInt(String((textEl.style && textEl.style.fontSize) || 16));\n const fontFamily = String((textEl.style && textEl.style.fontFamily) || 'Arial');\n \n const isHorizontal = textEl.type === 'text-container' && textEl.containerExpansion === 'horizontal';\n \n if (isHorizontal) {\n // Horizontal expansion: Update width only\n // Requires canvas context which is available in measureTextHeight scope or we create new one\n // For simplicity, we can't easily access the measure logic here if it's not exposed, \n // but measureTextHeight is available in this scope.\n // However measureTextHeight calculates HEIGHT. We need WIDTH.\n \n try {\n const canvas = document.createElement('canvas');\n const context = canvas.getContext('2d');\n if (context) {\n context.font = `${fontSize}px ${fontFamily}`;\n const metrics = context.measureText(content);\n const padding = parseInt(String((textEl.style && textEl.style.padding) || 0)) * 2;\n const newWidth = Math.ceil(metrics.width + padding);\n if (newWidth > textEl.width) {\n textEl.width = newWidth;\n }\n }\n } catch(e) {}\n } else {\n // Vertical Expansion\n const measuredHeight = measureTextHeight(content, textEl.width, fontFamily, fontSize);\n const designHeight = textEl.height;\n const delta = measuredHeight - designHeight;\n \n if (delta > 0) {\n const originalBottom = textEl.y + designHeight;\n const originalTextRect = {\n x: textEl.x,\n y: textEl.y,\n width: textEl.width,\n height: designHeight\n };\n \n textEl.height = measuredHeight;\n \n layoutElements.forEach(other => {\n if (other.id === textEl.id) return;\n \n if (isInside(originalTextRect, other)) {\n other.height += delta;\n }\n \n if (other.y >= originalBottom) {\n other.y += delta;\n }\n });\n }\n }\n });\n \n let maxY = 0;\n layoutElements.forEach(el => {\n const bottom = el.y + el.height;\n if (bottom > maxY) maxY = bottom;\n });\n \n return { layoutElements, maxY };\n };\n\n const computeItemHeight = (elements, itemData, fallbackHeight) => {\n const { maxY } = computeLayout(elements, itemData);\n return fallbackHeight ? Math.max(maxY, fallbackHeight) : maxY;\n };\n\n const formatValue = (value, formatting) => {\n if (!formatting || formatting.type === 'text') return value !== undefined && value !== null ? String(value) : '';\n if (value === undefined || value === null) return '';\n\n if (formatting.type === 'boolean') {\n const isTrue = String(value) === 'true' || value === true || (typeof value === 'number' && value > 0);\n return isTrue ? (formatting.trueLabel || 'Sim') : (formatting.falseLabel || 'Não');\n }\n\n if (formatting.type === 'date') {\n try {\n const date = new Date(value);\n if (isNaN(date.getTime())) return String(value);\n \n if (formatting.dateFormat) {\n const d = date.getDate().toString().padStart(2, '0');\n const m = (date.getMonth() + 1).toString().padStart(2, '0');\n const y = date.getFullYear();\n const H = date.getHours().toString().padStart(2, '0');\n const M = date.getMinutes().toString().padStart(2, '0');\n const S = date.getSeconds().toString().padStart(2, '0');\n \n return formatting.dateFormat\n .replace('DD', d)\n .replace('MM', m)\n .replace('YYYY', String(y))\n .replace('HH', H)\n .replace('mm', M)\n .replace('ss', S);\n }\n return date.toLocaleDateString();\n } catch (e) { return String(value); }\n }\n\n if (formatting.type === 'number') {\n const num = parseFloat(value);\n if (isNaN(num)) return String(value);\n \n if (formatting.numberFormat === 'currency') {\n return (formatting.currencySymbol || 'R$') + ' ' + num.toFixed(formatting.decimalPlaces || 2);\n }\n if (formatting.numberFormat === 'percent') {\n return num.toFixed(formatting.decimalPlaces || 0) + '%';\n }\n if (formatting.decimalPlaces !== undefined) {\n return num.toFixed(formatting.decimalPlaces);\n }\n return num.toFixed(formatting.decimalPlaces || 0);\n }\n \n return String(value);\n };\n\n const checkCondition = (propValue, operator, ruleValue) => {\n const val = String(propValue).toLowerCase();\n const target = String(ruleValue).toLowerCase();\n \n switch (operator) {\n case 'equals': return val === target;\n case 'notEquals': return val !== target;\n case 'contains': return val.includes(target);\n case 'greaterThan': return parseFloat(val) > parseFloat(target);\n case 'lessThan': return parseFloat(val) < parseFloat(target);\n case 'truthy': return !!propValue;\n case 'falsy': return !propValue;\n default: return false;\n }\n };\n\n const camelToKebab = (string) => {\n return string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();\n };\n\n const hex8ToRgba = (hex) => {\n const m = /^#([0-9a-fA-F]{8})$/.exec(hex);\n if (!m) return hex;\n const h = m[1];\n const r = parseInt(h.slice(0, 2), 16);\n const g = parseInt(h.slice(2, 4), 16);\n const b = parseInt(h.slice(4, 6), 16);\n const a = parseInt(h.slice(6, 8), 16) / 255;\n return `rgba(${r}, ${g}, ${b}, ${a})`;\n };\n\n const styleObjectToString = (style) => {\n if (!style) return '';\n const pxProps = [\n 'width', 'height', 'top', 'left', 'right', 'bottom', \n 'fontSize', 'borderRadius', 'padding', 'margin', 'borderWidth',\n 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius'\n ];\n \n return Object.entries(style)\n .map(([key, value]) => {\n if (value === undefined || value === null) return '';\n const cssKey = camelToKebab(key);\n let cssValue = (typeof value === 'number' && pxProps.includes(key)) ? value + 'px' : value;\n if (typeof cssValue === 'string') {\n if (/^#([0-9a-fA-F]{8})$/.test(cssValue)) {\n cssValue = hex8ToRgba(cssValue);\n }\n }\n return `${cssKey}: ${cssValue}`;\n })\n .filter(Boolean)\n .join('; ');\n };\n\n const getAnimationStyles = (anim) => {\n if (!anim || anim.type === 'none') return {};\n return {\n animationName: anim.type,\n animationDuration: (anim.duration || 1) + 's',\n animationDelay: (anim.delay || 0) + 's',\n animationIterationCount: anim.iterationCount || 1,\n animationTimingFunction: anim.timingFunction || 'ease',\n animationFillMode: 'both'\n };\n };\n\n const keyframesCss = `\n @keyframes slideIn {\n from { opacity: 0; transform: translateY(20px); }\n to { opacity: 1; transform: translateY(0); }\n }\n @keyframes fadeIn { \n from { opacity: 0; } \n to { opacity: 1; } \n }\n @keyframes slideInLeft { \n from { opacity: 0; transform: translateX(-50px); } \n to { opacity: 1; transform: translateX(0); } \n }\n @keyframes slideInRight { \n from { opacity: 0; transform: translateX(50px); } \n to { opacity: 1; transform: translateX(0); } \n }\n @keyframes slideInUp { \n from { opacity: 0; transform: translateY(50px); } \n to { opacity: 1; transform: translateY(0); } \n }\n @keyframes slideInDown { \n from { opacity: 0; transform: translateY(-50px); } \n to { opacity: 1; transform: translateY(0); } \n }\n @keyframes zoomIn { \n from { opacity: 0; transform: scale(0.5); } \n to { opacity: 1; transform: scale(1); } \n }\n @keyframes bounceIn {\n 0% { opacity: 0; transform: scale(0.3); }\n 50% { opacity: 1; transform: scale(1.05); }\n 70% { transform: scale(0.9); }\n 100% { transform: scale(1); }\n }\n @keyframes pulse {\n 0% { transform: scale(1); }\n 50% { transform: scale(1.05); }\n 100% { transform: scale(1); }\n }\n @keyframes shake {\n 0%, 100% { transform: translateX(0); }\n 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }\n 20%, 40%, 60%, 80% { transform: translateX(5px); }\n }\n @keyframes spin { \n from { transform: rotate(0deg); } \n to { transform: rotate(360deg); } \n }\n \n /* Improved / Smoother Animations */\n @keyframes smoothSlideUp {\n 0% { opacity: 0; transform: translateY(30px); }\n 100% { opacity: 1; transform: translateY(0); }\n }\n @keyframes popIn {\n 0% { opacity: 0; transform: scale(0.8) translateY(10px); }\n 100% { opacity: 1; transform: scale(1) translateY(0); }\n }\n @keyframes blurIn {\n 0% { opacity: 0; filter: blur(10px); }\n 100% { opacity: 1; filter: blur(0); }\n }\n `;\n\n const renderItem = (itemData, index = 0, offsetY = 0) => {\n const { layoutElements } = computeLayout(elements, itemData);\n \n // Group elements into logical rows based on Y position overlap to support dynamic height\n layoutElements.sort((a, b) => a.y - b.y);\n \n const rows = [];\n let currentRow = null;\n \n layoutElements.forEach(el => {\n const elBottom = el.y + el.height;\n if (!currentRow) {\n currentRow = { startY: el.y, endY: elBottom, elements: [el] };\n rows.push(currentRow);\n } else {\n // If this element starts significantly after the current row ends, start a new row\n if (el.y >= currentRow.endY - 5) { \n currentRow = { startY: el.y, endY: elBottom, elements: [el] };\n rows.push(currentRow);\n } else {\n currentRow.elements.push(el);\n currentRow.endY = Math.max(currentRow.endY, elBottom);\n }\n }\n });\n\n return rows.map(row => {\n // Find autoGrow elements in this row\n const flowElements = row.elements.filter(el => (el.type === 'text' || el.type === 'text-container') && el.autoGrow);\n \n // Sort flow elements by X position to render them in correct order\n // This is CRITICAL for correct flow layout\n flowElements.sort((a, b) => a.x - b.x);\n\n // Calculate Row Min Height (based on static elements and initial size of flow elements)\n let minHeight = 0;\n row.elements.forEach(el => {\n // For static elements, bottom relative to row start\n // For flow elements, we also use their initial design height as minimum\n const bottom = (el.y - row.startY) + el.height;\n if (bottom > minHeight) minHeight = bottom;\n });\n\n const rowStyle = styleObjectToString({\n position: 'relative',\n width: '100%',\n minHeight: minHeight,\n });\n\n const childrenHtml = row.elements.map(element => {\n let content = element.content;\n let imgSrc = '';\n\n // Resolve Content & Formatting\n if (element.type === 'text' || element.type === 'text-container') {\n content = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {\n const val = itemData[key.trim()];\n if (val === undefined || val === null) return match;\n if (element.formatting) {\n return formatValue(val, element.formatting);\n }\n return String(val);\n });\n } else if (element.type === 'image') {\n if (element.dataBinding) {\n const val = itemData[element.dataBinding];\n if (val !== undefined && val !== null) {\n imgSrc = String(val);\n } else {\n imgSrc = content;\n }\n } else {\n imgSrc = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {\n const val = itemData[key.trim()];\n return val !== undefined && val !== null ? String(val) : match;\n });\n }\n }\n\n // Resolve Conditional Styles\n let conditionalStyles = {};\n if (element.conditions) {\n element.conditions.forEach(rule => {\n const propVal = itemData[rule.property];\n if (checkCondition(propVal, rule.operator, rule.value)) {\n conditionalStyles = { ...conditionalStyles, ...rule.style };\n }\n });\n }\n\n // Resolve Style Bindings\n let bindingStyles = {};\n if (element.styleBindings) {\n Object.entries(element.styleBindings).forEach(([styleProp, variableName]) => {\n const val = itemData[variableName];\n if (val !== undefined && val !== null) {\n bindingStyles[styleProp] = String(val);\n }\n });\n }\n\n const isFlow = flowElements.includes(element);\n \n let marginLeft = undefined;\n if (isFlow) {\n // Calculate margin based on distance from previous flow element\n const flowIndex = flowElements.indexOf(element);\n if (flowIndex > 0) {\n const prev = flowElements[flowIndex - 1];\n const prevEnd = prev.x + prev.width;\n const gap = element.x - prevEnd;\n marginLeft = gap > 0 ? gap : 0;\n } else {\n marginLeft = element.x;\n }\n }\n\n const baseStyle = {\n position: isFlow ? 'relative' : 'absolute',\n left: isFlow ? undefined : element.x,\n top: isFlow ? undefined : (element.y - row.startY + offsetY),\n marginLeft: isFlow ? marginLeft : undefined,\n marginTop: isFlow ? (element.y - row.startY) : undefined,\n width: element.width,\n height: element.autoGrow ? 'auto' : element.height,\n transform: element.rotation ? `rotate(${element.rotation}deg)` : undefined,\n overflow: element.autoGrow ? 'visible' : 'hidden',\n whiteSpace: (element.type === 'text-container' && element.autoGrow && element.containerExpansion === 'horizontal') ? 'nowrap' : (element.autoGrow ? 'pre-wrap' : undefined),\n wordBreak: element.autoGrow ? 'break-word' : undefined,\n display: isFlow ? 'inline-block' : undefined,\n verticalAlign: isFlow ? 'top' : undefined,\n boxSizing: 'border-box', // Ensure padding doesn't affect dimensions\n ...element.style,\n ...conditionalStyles,\n ...bindingStyles\n };\n \n const styleString = styleObjectToString(baseStyle);\n\n if (element.type === 'text' || element.type === 'text-container') {\n return `<div style=\"${styleString}\">${content}</div>`;\n } else if (element.type === 'image') {\n const imgStyle = styleObjectToString({\n width: '100%',\n height: '100%',\n objectFit: element.style?.objectFit || 'cover',\n display: 'block'\n });\n return `<div style=\"${styleString}\"><img src=\"${imgSrc}\" alt=\"Element\" style=\"${imgStyle}\" /></div>`;\n } else if (element.type === 'box') {\n return `<div style=\"${styleString}\"></div>`;\n } else if (element.type === 'checkbox') {\n let isChecked = false;\n if (element.dataBinding) {\n const val = itemData[element.dataBinding];\n isChecked = val === true || String(val) === 'true';\n }\n const checkboxStyle = styleObjectToString({\n ...baseStyle,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center'\n });\n return `<div style=\"${checkboxStyle}\"><input type=\"checkbox\" ${isChecked ? 'checked' : ''} disabled style=\"width:100%;height:100%;margin:0;\" /></div>`;\n }\n return '';\n }).join('\\n');\n \n return `<div class=\"list-row\" style=\"${rowStyle}\">${childrenHtml}</div>`;\n }).join('\\n');\n };\n\nif (isList && Array.isArray(data)) {\n // Calculate per-item height respecting autoGrow\n // Sort data\n let listData = [...data];\n if (listSettings && listSettings.sortProp) {\n const prop = listSettings.sortProp;\n const order = listSettings.sortOrder === 'asc' ? 1 : -1;\n listData.sort((a, b) => {\n const valA = a[prop];\n const valB = b[prop];\n if (valA < valB) return -1 * order;\n if (valA > valB) return 1 * order;\n return 0;\n });\n }\n\n // Handle newest position\n if (listSettings && listSettings.newestPosition === 'top') {\n listData.reverse();\n }\n\n // Generate HTML for all items\n const itemsHtml = listData.map((item, index) => {\n const itemHtml = renderItem(item, index, 0);\n const itemContainerStyle = styleObjectToString({\n position: 'relative',\n minHeight: canvasHeight + 'px',\n width: '100%'\n });\n\n return `<div class=\"list-item\" style=\"${itemContainerStyle}\">${itemHtml}</div>`;\n }).join('\\n');\n\n // Animation Styles based on settings\n const scrollDirection = (listSettings && listSettings.scrollDirection) || 'down';\n const containerHeight = (listSettings && listSettings.containerHeight) ? listSettings.containerHeight + 'px' : '100%';\n \n const justify = (listSettings && listSettings.newestPosition === 'top') ? 'flex-start' : 'flex-end';\n\n // Entry Animation from settings\n const entryAnim = listSettings && listSettings.entryAnimation ? listSettings.entryAnimation : { type: 'slideIn', duration: 0.3, timingFunction: 'ease-out' };\n const entryAnimName = entryAnim.type === 'none' ? 'none' : entryAnim.type;\n const entryAnimDuration = entryAnim.duration + 's';\n const entryAnimTiming = entryAnim.timingFunction || 'ease-out';\n\n const animationCss = `\n ${keyframesCss}\n\n .list-wrapper {\n display: flex;\n flex-direction: column;\n justify-content: ${justify};\n height: ${containerHeight};\n width: 100%;\n overflow-y: auto;\n overflow-x: hidden;\n box-sizing: border-box;\n padding: 10px;\n }\n .list-item {\n flex-shrink: 0;\n animation: ${entryAnimName} ${entryAnimDuration} ${entryAnimTiming};\n margin-bottom: 10px;\n width: 100%;\n position: relative;\n }\n `;\n \n const scrollScript = scrollDirection === 'up' \n ? `<script>\n document.addEventListener('DOMContentLoaded', () => {\n const wrapper = document.querySelector('.list-wrapper');\n if(wrapper) wrapper.scrollTop = wrapper.scrollHeight;\n });\n <\/script>`\n : '';\n\n // Inject Smart Script for Dynamic Updates\n const injectionScript = `\n <script>\n (function() {\n try {\n const elements = ${JSON.stringify(elements)};\n const formatValue = ${formatValue.toString()};\n const checkCondition = ${checkCondition.toString()};\n const camelToKebab = ${camelToKebab.toString()};\n const hex8ToRgba = ${hex8ToRgba.toString()};\n const styleObjectToString = ${styleObjectToString.toString()};\n const getAnimationStyles = ${getAnimationStyles.toString()};\n const renderItem = ${renderItem.toString()};\n\n const measureTextHeight = ${measureTextHeight.toString()};\n const computeLayout = ${computeLayout.toString()};\n const computeItemHeight = ${computeItemHeight.toString()};\n const itemHeightFallback = ${canvasHeight || 0};\n const newestPosition = \"${(listSettings && listSettings.newestPosition) || 'bottom'}\";\n const scrollDirection = \"${(listSettings && listSettings.scrollDirection) || 'down'}\";\n\n window.addItem = function(data) {\n const wrapper = document.querySelector('.list-wrapper');\n if (!wrapper) return;\n\n const itemHtml = renderItem(data, 0, 0);\n const itemContainerStyle = styleObjectToString({\n position: 'relative',\n minHeight: itemHeightFallback + 'px',\n width: '100%'\n });\n\n const div = document.createElement('div');\n div.className = 'list-item';\n div.setAttribute('style', itemContainerStyle);\n div.innerHTML = itemHtml;\n\n if (newestPosition === 'top') {\n wrapper.insertBefore(div, wrapper.firstChild);\n } else {\n wrapper.appendChild(div);\n }\n \n if (scrollDirection === 'up') {\n wrapper.scrollTop = wrapper.scrollHeight;\n }\n };\n } catch(e) { console.error(\"Smart List Init Error\", e); }\n })();\n <\/script>\n `;\n\n return `\n <style>${animationCss}</style>\n <div class=\"list-wrapper\">\n ${itemsHtml}\n </div>\n ${scrollScript}\n ${injectionScript}\n `;\n }\n\n // Single Item\n const contentHtml = renderItem(data);\n return `<div style=\"position: relative; width: 100%; height: 100%; overflow: hidden;\">${contentHtml}</div>`;\n}\n";
18549
+ }), generateHTML = (n, _, E = {}) => Function("elements", "data", "options", getRendererCode() + "\nreturn renderTemplate(elements, data, options);")(n, _, E), getRendererCode = () => "\n/**\n * Render Template\n * @param {Array} elements - The JSON configuration of elements\n * @param {Object|Array} data - The data object to inject (Object for single, Array for list)\n * @param {Object} options - { isList: boolean, listSettings: { sortProp: string, sortOrder: 'asc'|'desc', newestPosition: 'top'|'bottom', scrollDirection: 'up'|'down', containerHeight: number }, canvasHeight: number }\n * @returns {string} - The generated HTML string\n */\nfunction renderTemplate(elements, data, options = {}) {\n const { isList, listSettings, canvasHeight } = options;\n\n const measureTextHeight = (text, width, fontFamily, fontSize, lineHeightMultiplier = 1.2) => {\n if (!text) return 0;\n try {\n const canvas = document.createElement('canvas');\n const context = canvas.getContext('2d');\n if (!context) return 0;\n context.font = `${fontSize}px ${fontFamily}`;\n const words = String(text).split(' ');\n let line = '';\n let lineCount = 1;\n for (let i = 0; i < words.length; i++) {\n const testLine = line + words[i] + ' ';\n const metrics = context.measureText(testLine);\n const testWidth = metrics.width;\n if (testWidth > width && i > 0) {\n line = words[i] + ' ';\n lineCount++;\n } else {\n line = testLine;\n }\n }\n const explicitLines = String(text).split('\\n').length - 1;\n lineCount += explicitLines;\n return Math.ceil(lineCount * fontSize * lineHeightMultiplier);\n } catch (_) {\n return 0;\n }\n };\n\n const computeLayout = (elements, itemData) => {\n const layoutElements = JSON.parse(JSON.stringify(elements));\n \n const isInside = (inner, outer) => {\n const eps = 0.1;\n return (\n inner.x >= outer.x - eps &&\n inner.x + inner.width <= outer.x + outer.width + eps &&\n inner.y >= outer.y - eps &&\n inner.y + inner.height <= outer.y + outer.height + eps\n );\n };\n\n const autoGrowElements = layoutElements\n .filter(el => (el.type === 'text' || el.type === 'text-container') && el.autoGrow)\n .sort((a, b) => a.y - b.y);\n\n autoGrowElements.forEach(textEl => {\n let content = textEl.content;\n content = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {\n const val = itemData[key.trim()];\n return val !== undefined && val !== null ? String(val) : match;\n });\n \n const fontSize = parseInt(String((textEl.style && textEl.style.fontSize) || 16));\n const fontFamily = String((textEl.style && textEl.style.fontFamily) || 'Arial');\n \n const isHorizontal = textEl.type === 'text-container' && textEl.containerExpansion === 'horizontal';\n \n if (isHorizontal) {\n try {\n const canvas = document.createElement('canvas');\n const context = canvas.getContext('2d');\n if (context) {\n context.font = `${fontSize}px ${fontFamily}`;\n const metrics = context.measureText(content);\n const padding = parseInt(String((textEl.style && textEl.style.padding) || 0)) * 2;\n const newWidth = Math.ceil(metrics.width + padding);\n if (newWidth > textEl.width) {\n textEl.width = newWidth;\n }\n }\n } catch(e) {}\n } else {\n const measuredHeight = measureTextHeight(content, textEl.width, fontFamily, fontSize);\n const designHeight = textEl.height;\n const delta = measuredHeight - designHeight;\n \n if (delta > 0) {\n const originalBottom = textEl.y + designHeight;\n const originalTextRect = {\n x: textEl.x,\n y: textEl.y,\n width: textEl.width,\n height: designHeight\n };\n \n textEl.height = measuredHeight;\n \n layoutElements.forEach(other => {\n if (other.id === textEl.id) return;\n \n if (isInside(originalTextRect, other)) {\n other.height += delta;\n }\n \n if (other.y >= originalBottom) {\n other.y += delta;\n }\n });\n }\n }\n });\n \n let maxY = 0;\n layoutElements.forEach(el => {\n const bottom = el.y + el.height;\n if (bottom > maxY) maxY = bottom;\n });\n \n return { layoutElements, maxY };\n };\n\n const computeItemHeight = (elements, itemData, fallbackHeight) => {\n const { maxY } = computeLayout(elements, itemData);\n return fallbackHeight ? Math.max(maxY, fallbackHeight) : maxY;\n };\n\n const formatValue = (value, formatting) => {\n if (!formatting || formatting.type === 'text') return value !== undefined && value !== null ? String(value) : '';\n if (value === undefined || value === null) return '';\n\n if (formatting.type === 'boolean') {\n const isTrue = String(value) === 'true' || value === true || (typeof value === 'number' && value > 0);\n return isTrue ? (formatting.trueLabel || 'Sim') : (formatting.falseLabel || 'Não');\n }\n\n if (formatting.type === 'date') {\n try {\n const date = new Date(value);\n if (isNaN(date.getTime())) return String(value);\n \n if (formatting.dateFormat) {\n const d = date.getDate().toString().padStart(2, '0');\n const m = (date.getMonth() + 1).toString().padStart(2, '0');\n const y = date.getFullYear();\n const H = date.getHours().toString().padStart(2, '0');\n const M = date.getMinutes().toString().padStart(2, '0');\n const S = date.getSeconds().toString().padStart(2, '0');\n \n return formatting.dateFormat\n .replace('DD', d)\n .replace('MM', m)\n .replace('YYYY', String(y))\n .replace('HH', H)\n .replace('mm', M)\n .replace('ss', S);\n }\n return date.toLocaleDateString();\n } catch (e) { return String(value); }\n }\n\n if (formatting.type === 'number') {\n const num = parseFloat(value);\n if (isNaN(num)) return String(value);\n \n if (formatting.numberFormat === 'currency') {\n return (formatting.currencySymbol || 'R$') + ' ' + num.toFixed(formatting.decimalPlaces || 2);\n }\n if (formatting.numberFormat === 'percent') {\n return num.toFixed(formatting.decimalPlaces || 0) + '%';\n }\n if (formatting.decimalPlaces !== undefined) {\n return num.toFixed(formatting.decimalPlaces);\n }\n return num.toFixed(formatting.decimalPlaces || 0);\n }\n \n return String(value);\n };\n\n const checkCondition = (propValue, operator, ruleValue) => {\n const val = String(propValue).toLowerCase();\n const target = String(ruleValue).toLowerCase();\n \n switch (operator) {\n case 'equals': return val === target;\n case 'notEquals': return val !== target;\n case 'contains': return val.includes(target);\n case 'greaterThan': return parseFloat(val) > parseFloat(target);\n case 'lessThan': return parseFloat(val) < parseFloat(target);\n case 'truthy': return !!propValue;\n case 'falsy': return !propValue;\n default: return false;\n }\n };\n\n const camelToKebab = (string) => {\n return string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();\n };\n\n const hex8ToRgba = (hex) => {\n const m = /^#([0-9a-fA-F]{8})$/.exec(hex);\n if (!m) return hex;\n const h = m[1];\n const r = parseInt(h.slice(0, 2), 16);\n const g = parseInt(h.slice(2, 4), 16);\n const b = parseInt(h.slice(4, 6), 16);\n const a = parseInt(h.slice(6, 8), 16) / 255;\n return `rgba(${r}, ${g}, ${b}, ${a})`;\n };\n\n const styleObjectToString = (style) => {\n if (!style) return '';\n const pxProps = [\n 'width', 'height', 'top', 'left', 'right', 'bottom', \n 'fontSize', 'borderRadius', 'padding', 'margin', 'borderWidth',\n 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius'\n ];\n \n return Object.entries(style)\n .map(([key, value]) => {\n if (value === undefined || value === null) return '';\n const cssKey = camelToKebab(key);\n let cssValue = (typeof value === 'number' && pxProps.includes(key)) ? value + 'px' : value;\n if (typeof cssValue === 'string') {\n if (/^#([0-9a-fA-F]{8})$/.test(cssValue)) {\n cssValue = hex8ToRgba(cssValue);\n }\n }\n return `${cssKey}: ${cssValue}`;\n })\n .filter(Boolean)\n .join('; ');\n };\n\n const getAnimationStyles = (anim) => {\n if (!anim || anim.type === 'none') return {};\n return {\n animationName: anim.type,\n animationDuration: (anim.duration || 1) + 's',\n animationDelay: (anim.delay || 0) + 's',\n animationIterationCount: anim.iterationCount || 1,\n animationTimingFunction: anim.timingFunction || 'ease',\n animationFillMode: 'both'\n };\n };\n\n const keyframesCss = `\n @keyframes slideIn {\n from { opacity: 0; transform: translateY(20px); }\n to { opacity: 1; transform: translateY(0); }\n }\n @keyframes fadeIn { \n from { opacity: 0; } \n to { opacity: 1; } \n }\n @keyframes slideInLeft { \n from { opacity: 0; transform: translateX(-50px); } \n to { opacity: 1; transform: translateX(0); } \n }\n @keyframes slideInRight { \n from { opacity: 0; transform: translateX(50px); } \n to { opacity: 1; transform: translateX(0); } \n }\n @keyframes slideInUp { \n from { opacity: 0; transform: translateY(50px); } \n to { opacity: 1; transform: translateY(0); } \n }\n @keyframes slideInDown { \n from { opacity: 0; transform: translateY(-50px); } \n to { opacity: 1; transform: translateY(0); } \n }\n @keyframes zoomIn { \n from { opacity: 0; transform: scale(0.5); } \n to { opacity: 1; transform: scale(1); } \n }\n @keyframes bounceIn {\n 0% { opacity: 0; transform: scale(0.3); }\n 50% { opacity: 1; transform: scale(1.05); }\n 70% { transform: scale(0.9); }\n 100% { transform: scale(1); }\n }\n @keyframes pulse {\n 0% { transform: scale(1); }\n 50% { transform: scale(1.05); }\n 100% { transform: scale(1); }\n }\n @keyframes shake {\n 0%, 100% { transform: translateX(0); }\n 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }\n 20%, 40%, 60%, 80% { transform: translateX(5px); }\n }\n @keyframes spin { \n from { transform: rotate(0deg); } \n to { transform: rotate(360deg); } \n }\n \n /* Improved / Smoother Animations */\n @keyframes smoothSlideUp {\n 0% { opacity: 0; transform: translateY(30px); }\n 100% { opacity: 1; transform: translateY(0); }\n }\n @keyframes popIn {\n 0% { opacity: 0; transform: scale(0.8) translateY(10px); }\n 100% { opacity: 1; transform: scale(1) translateY(0); }\n }\n @keyframes blurIn {\n 0% { opacity: 0; filter: blur(10px); }\n 100% { opacity: 1; filter: blur(0); }\n }\n `;\n\n const renderItem = (itemData, index = 0, offsetY = 0) => {\n const { layoutElements } = computeLayout(elements, itemData);\n return layoutElements.map(element => {\n let content = element.content;\n let imgSrc = '';\n\n // Resolve Content & Formatting\n if (element.type === 'text' || element.type === 'text-container') {\n content = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {\n const val = itemData[key.trim()];\n if (val === undefined || val === null) return match;\n if (element.formatting) {\n return formatValue(val, element.formatting);\n }\n return String(val);\n });\n } else if (element.type === 'image') {\n if (element.dataBinding) {\n const val = itemData[element.dataBinding];\n if (val !== undefined && val !== null) {\n imgSrc = String(val);\n } else {\n imgSrc = content;\n }\n } else {\n imgSrc = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {\n const val = itemData[key.trim()];\n return val !== undefined && val !== null ? String(val) : match;\n });\n }\n }\n\n // Resolve Conditional Styles\n let conditionalStyles = {};\n if (element.conditions) {\n element.conditions.forEach(rule => {\n const propVal = itemData[rule.property];\n if (checkCondition(propVal, rule.operator, rule.value)) {\n conditionalStyles = { ...conditionalStyles, ...rule.style };\n }\n });\n }\n\n // Resolve Style Bindings\n let bindingStyles = {};\n if (element.styleBindings) {\n Object.entries(element.styleBindings).forEach(([styleProp, variableName]) => {\n const val = itemData[variableName];\n if (val !== undefined && val !== null) {\n bindingStyles[styleProp] = String(val);\n }\n });\n }\n\n const baseStyle = {\n position: 'absolute',\n left: element.x,\n top: element.y + offsetY,\n width: element.width,\n height: element.autoGrow ? 'auto' : element.height,\n transform: element.rotation ? `rotate(${element.rotation}deg)` : undefined,\n overflow: element.autoGrow ? 'visible' : 'hidden',\n whiteSpace: (element.type === 'text-container' && element.autoGrow && element.containerExpansion === 'horizontal') ? 'nowrap' : (element.autoGrow ? 'pre-wrap' : undefined),\n wordBreak: element.autoGrow ? 'break-word' : undefined,\n ...element.style,\n ...conditionalStyles,\n ...bindingStyles\n };\n \n if (element.type === 'text' && !baseStyle.padding) {\n // baseStyle.padding = '8px'; // Removed default padding to respect resize box\n }\n \n const styleString = styleObjectToString(baseStyle);\n\n if (element.type === 'text' || element.type === 'text-container') {\n return `<div style=\"${styleString}\">${content}</div>`;\n } else if (element.type === 'image') {\n const imgStyle = styleObjectToString({\n width: '100%',\n height: '100%',\n objectFit: element.style?.objectFit || 'cover',\n display: 'block'\n });\n return `<div style=\"${styleString}\"><img src=\"${imgSrc}\" alt=\"Element\" style=\"${imgStyle}\" /></div>`;\n } else if (element.type === 'box') {\n return `<div style=\"${styleString}\"></div>`;\n } else if (element.type === 'checkbox') {\n let isChecked = false;\n if (element.dataBinding) {\n const val = itemData[element.dataBinding];\n isChecked = val === true || String(val) === 'true';\n }\n const checkboxStyle = styleObjectToString({\n ...baseStyle,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center'\n });\n return `<div style=\"${checkboxStyle}\"><input type=\"checkbox\" ${isChecked ? 'checked' : ''} disabled style=\"width:100%;height:100%;margin:0;\" /></div>`;\n }\n return '';\n }).join('\\n');\n };\n\n if (isList && Array.isArray(data)) {\n // Calculate per-item height respecting autoGrow\n // Sort data\n let listData = [...data];\n if (listSettings && listSettings.sortProp) {\n const prop = listSettings.sortProp;\n const order = listSettings.sortOrder === 'asc' ? 1 : -1;\n listData.sort((a, b) => {\n const valA = a[prop];\n const valB = b[prop];\n if (valA < valB) return -1 * order;\n if (valA > valB) return 1 * order;\n return 0;\n });\n }\n\n // Handle newest position\n if (listSettings && listSettings.newestPosition === 'top') {\n listData.reverse();\n }\n\n // Generate HTML for all items\n const itemsHtml = listData.map((item, index) => {\n const itemHtml = renderItem(item, index, 0);\n const itemHeight = computeItemHeight(elements, item, canvasHeight);\n const itemContainerStyle = styleObjectToString({\n position: 'relative',\n height: itemHeight,\n width: '100%'\n });\n\n return `<div class=\"list-item\" style=\"${itemContainerStyle}\">${itemHtml}</div>`;\n }).join('\\n');\n\n // Animation Styles based on settings\n const scrollDirection = (listSettings && listSettings.scrollDirection) || 'down';\n const containerHeight = (listSettings && listSettings.containerHeight) ? listSettings.containerHeight + 'px' : '100%';\n \n const justify = (listSettings && listSettings.newestPosition === 'top') ? 'flex-start' : 'flex-end';\n\n // Entry Animation from settings\n const entryAnim = listSettings && listSettings.entryAnimation ? listSettings.entryAnimation : { type: 'slideIn', duration: 0.3, timingFunction: 'ease-out' };\n const entryAnimName = entryAnim.type === 'none' ? 'none' : entryAnim.type;\n const entryAnimDuration = entryAnim.duration + 's';\n const entryAnimTiming = entryAnim.timingFunction || 'ease-out';\n\n const animationCss = `\n ${keyframesCss}\n\n .list-wrapper {\n display: flex;\n flex-direction: column;\n justify-content: ${justify};\n height: ${containerHeight};\n width: 100%;\n overflow-y: auto;\n overflow-x: hidden;\n box-sizing: border-box;\n padding: 10px;\n }\n .list-item {\n flex-shrink: 0;\n animation: ${entryAnimName} ${entryAnimDuration} ${entryAnimTiming};\n margin-bottom: 10px;\n width: 100%;\n position: relative;\n }\n `;\n \n const scrollScript = scrollDirection === 'up' \n ? `<script>\n document.addEventListener('DOMContentLoaded', () => {\n const wrapper = document.querySelector('.list-wrapper');\n if(wrapper) wrapper.scrollTop = wrapper.scrollHeight;\n });\n <\/script>`\n : '';\n\n // Inject Smart Script for Dynamic Updates\n const injectionScript = `\n <script>\n (function() {\n try {\n const elements = ${JSON.stringify(elements)};\n const formatValue = ${formatValue.toString()};\n const checkCondition = ${checkCondition.toString()};\n const camelToKebab = ${camelToKebab.toString()};\n const hex8ToRgba = ${hex8ToRgba.toString()};\n const styleObjectToString = ${styleObjectToString.toString()};\n const getAnimationStyles = ${getAnimationStyles.toString()};\n const renderItem = ${renderItem.toString()};\n\n const measureTextHeight = ${measureTextHeight.toString()};\n const computeLayout = ${computeLayout.toString()};\n const computeItemHeight = ${computeItemHeight.toString()};\n const itemHeightFallback = ${canvasHeight || 0};\n const newestPosition = \"${(listSettings && listSettings.newestPosition) || 'bottom'}\";\n const scrollDirection = \"${(listSettings && listSettings.scrollDirection) || 'down'}\";\n\n window.addItem = function(data) {\n const wrapper = document.querySelector('.list-wrapper');\n if (!wrapper) return;\n\n const itemHtml = renderItem(data, 0, 0);\n const itemHeight = computeItemHeight(elements, data, itemHeightFallback);\n const itemContainerStyle = styleObjectToString({\n position: 'relative',\n height: itemHeight,\n width: '100%'\n });\n\n const div = document.createElement('div');\n div.className = 'list-item';\n div.setAttribute('style', itemContainerStyle);\n div.innerHTML = itemHtml;\n\n if (newestPosition === 'top') {\n wrapper.insertBefore(div, wrapper.firstChild);\n } else {\n wrapper.appendChild(div);\n }\n \n if (scrollDirection === 'up') {\n wrapper.scrollTop = wrapper.scrollHeight;\n }\n };\n } catch(e) { console.error(\"Smart List Init Error\", e); }\n })();\n <\/script>\n `;\n\n return `\n <style>${animationCss}</style>\n <div class=\"list-wrapper\">\n ${itemsHtml}\n </div>\n ${scrollScript}\n ${injectionScript}\n `;\n }\n\n // Single Item\n const contentHtml = renderItem(data);\n return `<div style=\"position: relative; width: 100%; height: 100%; overflow: hidden;\">${contentHtml}</div>`;\n}\n";
18550
18550
  export { GenericEditor as EditorContent, generateHTML };
@@ -125,12 +125,6 @@ function renderTemplate(elements, data, options = {}) {
125
125
  const isHorizontal = textEl.type === 'text-container' && textEl.containerExpansion === 'horizontal';
126
126
 
127
127
  if (isHorizontal) {
128
- // Horizontal expansion: Update width only
129
- // Requires canvas context which is available in measureTextHeight scope or we create new one
130
- // For simplicity, we can't easily access the measure logic here if it's not exposed,
131
- // but measureTextHeight is available in this scope.
132
- // However measureTextHeight calculates HEIGHT. We need WIDTH.
133
-
134
128
  try {
135
129
  const canvas = document.createElement('canvas');
136
130
  const context = canvas.getContext('2d');
@@ -145,7 +139,6 @@ function renderTemplate(elements, data, options = {}) {
145
139
  }
146
140
  } catch(e) {}
147
141
  } else {
148
- // Vertical Expansion
149
142
  const measuredHeight = measureTextHeight(content, textEl.width, fontFamily, fontSize);
150
143
  const designHeight = textEl.height;
151
144
  const delta = measuredHeight - designHeight;
@@ -377,207 +370,141 @@ function renderTemplate(elements, data, options = {}) {
377
370
 
378
371
  const renderItem = (itemData, index = 0, offsetY = 0) => {
379
372
  const { layoutElements } = computeLayout(elements, itemData);
380
-
381
- // Group elements into logical rows based on Y position overlap to support dynamic height
382
- layoutElements.sort((a, b) => a.y - b.y);
383
-
384
- const rows = [];
385
- let currentRow = null;
386
-
387
- layoutElements.forEach(el => {
388
- const elBottom = el.y + el.height;
389
- if (!currentRow) {
390
- currentRow = { startY: el.y, endY: elBottom, elements: [el] };
391
- rows.push(currentRow);
392
- } else {
393
- // If this element starts significantly after the current row ends, start a new row
394
- if (el.y >= currentRow.endY - 5) {
395
- currentRow = { startY: el.y, endY: elBottom, elements: [el] };
396
- rows.push(currentRow);
397
- } else {
398
- currentRow.elements.push(el);
399
- currentRow.endY = Math.max(currentRow.endY, elBottom);
400
- }
401
- }
402
- });
403
-
404
- return rows.map(row => {
405
- // Find autoGrow elements in this row
406
- const flowElements = row.elements.filter(el => (el.type === 'text' || el.type === 'text-container') && el.autoGrow);
407
-
408
- // Sort flow elements by X position to render them in correct order
409
- // This is CRITICAL for correct flow layout
410
- flowElements.sort((a, b) => a.x - b.x);
411
-
412
- // Calculate Row Min Height (based on static elements and initial size of flow elements)
413
- let minHeight = 0;
414
- row.elements.forEach(el => {
415
- // For static elements, bottom relative to row start
416
- // For flow elements, we also use their initial design height as minimum
417
- const bottom = (el.y - row.startY) + el.height;
418
- if (bottom > minHeight) minHeight = bottom;
419
- });
420
-
421
- const rowStyle = styleObjectToString({
422
- position: 'relative',
423
- width: '100%',
424
- minHeight: minHeight,
425
- });
426
-
427
- const childrenHtml = row.elements.map(element => {
428
- let content = element.content;
429
- let imgSrc = '';
430
-
431
- // Resolve Content & Formatting
432
- if (element.type === 'text' || element.type === 'text-container') {
433
- content = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {
373
+ return layoutElements.map(element => {
374
+ let content = element.content;
375
+ let imgSrc = '';
376
+
377
+ // Resolve Content & Formatting
378
+ if (element.type === 'text' || element.type === 'text-container') {
379
+ content = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {
380
+ const val = itemData[key.trim()];
381
+ if (val === undefined || val === null) return match;
382
+ if (element.formatting) {
383
+ return formatValue(val, element.formatting);
384
+ }
385
+ return String(val);
386
+ });
387
+ } else if (element.type === 'image') {
388
+ if (element.dataBinding) {
389
+ const val = itemData[element.dataBinding];
390
+ if (val !== undefined && val !== null) {
391
+ imgSrc = String(val);
392
+ } else {
393
+ imgSrc = content;
394
+ }
395
+ } else {
396
+ imgSrc = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {
434
397
  const val = itemData[key.trim()];
435
- if (val === undefined || val === null) return match;
436
- if (element.formatting) {
437
- return formatValue(val, element.formatting);
438
- }
439
- return String(val);
440
- });
441
- } else if (element.type === 'image') {
442
- if (element.dataBinding) {
443
- const val = itemData[element.dataBinding];
444
- if (val !== undefined && val !== null) {
445
- imgSrc = String(val);
446
- } else {
447
- imgSrc = content;
448
- }
449
- } else {
450
- imgSrc = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {
451
- const val = itemData[key.trim()];
452
- return val !== undefined && val !== null ? String(val) : match;
453
- });
454
- }
455
- }
456
-
457
- // Resolve Conditional Styles
458
- let conditionalStyles = {};
459
- if (element.conditions) {
460
- element.conditions.forEach(rule => {
461
- const propVal = itemData[rule.property];
462
- if (checkCondition(propVal, rule.operator, rule.value)) {
463
- conditionalStyles = { ...conditionalStyles, ...rule.style };
464
- }
465
- });
466
- }
467
-
468
- // Resolve Style Bindings
469
- let bindingStyles = {};
470
- if (element.styleBindings) {
471
- Object.entries(element.styleBindings).forEach(([styleProp, variableName]) => {
472
- const val = itemData[variableName];
473
- if (val !== undefined && val !== null) {
474
- bindingStyles[styleProp] = String(val);
475
- }
398
+ return val !== undefined && val !== null ? String(val) : match;
476
399
  });
477
- }
400
+ }
401
+ }
478
402
 
479
- const isFlow = flowElements.includes(element);
480
-
481
- let marginLeft = undefined;
482
- if (isFlow) {
483
- // Calculate margin based on distance from previous flow element
484
- const flowIndex = flowElements.indexOf(element);
485
- if (flowIndex > 0) {
486
- const prev = flowElements[flowIndex - 1];
487
- const prevEnd = prev.x + prev.width;
488
- const gap = element.x - prevEnd;
489
- marginLeft = gap > 0 ? gap : 0;
490
- } else {
491
- marginLeft = element.x;
403
+ // Resolve Conditional Styles
404
+ let conditionalStyles = {};
405
+ if (element.conditions) {
406
+ element.conditions.forEach(rule => {
407
+ const propVal = itemData[rule.property];
408
+ if (checkCondition(propVal, rule.operator, rule.value)) {
409
+ conditionalStyles = { ...conditionalStyles, ...rule.style };
492
410
  }
493
- }
411
+ });
412
+ }
494
413
 
495
- const baseStyle = {
496
- position: isFlow ? 'relative' : 'absolute',
497
- left: isFlow ? undefined : element.x,
498
- top: isFlow ? undefined : (element.y - row.startY + offsetY),
499
- marginLeft: isFlow ? marginLeft : undefined,
500
- marginTop: isFlow ? (element.y - row.startY) : undefined,
501
- width: element.width,
502
- height: element.autoGrow ? 'auto' : element.height,
503
- transform: element.rotation ? \`rotate(\${element.rotation}deg)\` : undefined,
504
- overflow: element.autoGrow ? 'visible' : 'hidden',
505
- whiteSpace: (element.type === 'text-container' && element.autoGrow && element.containerExpansion === 'horizontal') ? 'nowrap' : (element.autoGrow ? 'pre-wrap' : undefined),
506
- wordBreak: element.autoGrow ? 'break-word' : undefined,
507
- display: isFlow ? 'inline-block' : undefined,
508
- verticalAlign: isFlow ? 'top' : undefined,
509
- boxSizing: 'border-box', // Ensure padding doesn't affect dimensions
510
- ...element.style,
511
- ...conditionalStyles,
512
- ...bindingStyles
513
- };
514
-
515
- const styleString = styleObjectToString(baseStyle);
516
-
517
- if (element.type === 'text' || element.type === 'text-container') {
518
- return \`<div style="\${styleString}">\${content}</div>\`;
519
- } else if (element.type === 'image') {
520
- const imgStyle = styleObjectToString({
521
- width: '100%',
522
- height: '100%',
523
- objectFit: element.style?.objectFit || 'cover',
524
- display: 'block'
525
- });
526
- return \`<div style="\${styleString}"><img src="\${imgSrc}" alt="Element" style="\${imgStyle}" /></div>\`;
527
- } else if (element.type === 'box') {
528
- return \`<div style="\${styleString}"></div>\`;
529
- } else if (element.type === 'checkbox') {
530
- let isChecked = false;
531
- if (element.dataBinding) {
532
- const val = itemData[element.dataBinding];
533
- isChecked = val === true || String(val) === 'true';
414
+ // Resolve Style Bindings
415
+ let bindingStyles = {};
416
+ if (element.styleBindings) {
417
+ Object.entries(element.styleBindings).forEach(([styleProp, variableName]) => {
418
+ const val = itemData[variableName];
419
+ if (val !== undefined && val !== null) {
420
+ bindingStyles[styleProp] = String(val);
534
421
  }
535
- const checkboxStyle = styleObjectToString({
536
- ...baseStyle,
537
- display: 'flex',
538
- alignItems: 'center',
539
- justifyContent: 'center'
540
- });
541
- return \`<div style="\${checkboxStyle}"><input type="checkbox" \${isChecked ? 'checked' : ''} disabled style="width:100%;height:100%;margin:0;" /></div>\`;
542
- }
543
- return '';
544
- }).join('\\n');
422
+ });
423
+ }
424
+
425
+ const baseStyle = {
426
+ position: 'absolute',
427
+ left: element.x,
428
+ top: element.y + offsetY,
429
+ width: element.width,
430
+ height: element.autoGrow ? 'auto' : element.height,
431
+ transform: element.rotation ? \`rotate(\${element.rotation}deg)\` : undefined,
432
+ overflow: element.autoGrow ? 'visible' : 'hidden',
433
+ whiteSpace: (element.type === 'text-container' && element.autoGrow && element.containerExpansion === 'horizontal') ? 'nowrap' : (element.autoGrow ? 'pre-wrap' : undefined),
434
+ wordBreak: element.autoGrow ? 'break-word' : undefined,
435
+ ...element.style,
436
+ ...conditionalStyles,
437
+ ...bindingStyles
438
+ };
545
439
 
546
- return \`<div class="list-row" style="\${rowStyle}">\${childrenHtml}</div>\`;
440
+ if (element.type === 'text' && !baseStyle.padding) {
441
+ // baseStyle.padding = '8px'; // Removed default padding to respect resize box
442
+ }
443
+
444
+ const styleString = styleObjectToString(baseStyle);
445
+
446
+ if (element.type === 'text' || element.type === 'text-container') {
447
+ return \`<div style="\${styleString}">\${content}</div>\`;
448
+ } else if (element.type === 'image') {
449
+ const imgStyle = styleObjectToString({
450
+ width: '100%',
451
+ height: '100%',
452
+ objectFit: element.style?.objectFit || 'cover',
453
+ display: 'block'
454
+ });
455
+ return \`<div style="\${styleString}"><img src="\${imgSrc}" alt="Element" style="\${imgStyle}" /></div>\`;
456
+ } else if (element.type === 'box') {
457
+ return \`<div style="\${styleString}"></div>\`;
458
+ } else if (element.type === 'checkbox') {
459
+ let isChecked = false;
460
+ if (element.dataBinding) {
461
+ const val = itemData[element.dataBinding];
462
+ isChecked = val === true || String(val) === 'true';
463
+ }
464
+ const checkboxStyle = styleObjectToString({
465
+ ...baseStyle,
466
+ display: 'flex',
467
+ alignItems: 'center',
468
+ justifyContent: 'center'
469
+ });
470
+ return \`<div style="\${checkboxStyle}"><input type="checkbox" \${isChecked ? 'checked' : ''} disabled style="width:100%;height:100%;margin:0;" /></div>\`;
471
+ }
472
+ return '';
547
473
  }).join('\\n');
548
474
  };
549
475
 
550
- if (isList && Array.isArray(data)) {
551
- // Calculate per-item height respecting autoGrow
552
- // Sort data
553
- let listData = [...data];
554
- if (listSettings && listSettings.sortProp) {
555
- const prop = listSettings.sortProp;
556
- const order = listSettings.sortOrder === 'asc' ? 1 : -1;
557
- listData.sort((a, b) => {
558
- const valA = a[prop];
559
- const valB = b[prop];
560
- if (valA < valB) return -1 * order;
561
- if (valA > valB) return 1 * order;
562
- return 0;
563
- });
564
- }
476
+ if (isList && Array.isArray(data)) {
477
+ // Calculate per-item height respecting autoGrow
478
+ // Sort data
479
+ let listData = [...data];
480
+ if (listSettings && listSettings.sortProp) {
481
+ const prop = listSettings.sortProp;
482
+ const order = listSettings.sortOrder === 'asc' ? 1 : -1;
483
+ listData.sort((a, b) => {
484
+ const valA = a[prop];
485
+ const valB = b[prop];
486
+ if (valA < valB) return -1 * order;
487
+ if (valA > valB) return 1 * order;
488
+ return 0;
489
+ });
490
+ }
565
491
 
566
- // Handle newest position
567
- if (listSettings && listSettings.newestPosition === 'top') {
568
- listData.reverse();
569
- }
492
+ // Handle newest position
493
+ if (listSettings && listSettings.newestPosition === 'top') {
494
+ listData.reverse();
495
+ }
570
496
 
571
- // Generate HTML for all items
572
- const itemsHtml = listData.map((item, index) => {
573
- const itemHtml = renderItem(item, index, 0);
574
- const itemContainerStyle = styleObjectToString({
575
- position: 'relative',
576
- minHeight: canvasHeight + 'px',
577
- width: '100%'
578
- });
497
+ // Generate HTML for all items
498
+ const itemsHtml = listData.map((item, index) => {
499
+ const itemHtml = renderItem(item, index, 0);
500
+ const itemHeight = computeItemHeight(elements, item, canvasHeight);
501
+ const itemContainerStyle = styleObjectToString({
502
+ position: 'relative',
503
+ height: itemHeight,
504
+ width: '100%'
505
+ });
579
506
 
580
- return \`<div class="list-item" style="\${itemContainerStyle}">\${itemHtml}</div>\`;
507
+ return \`<div class="list-item" style="\${itemContainerStyle}">\${itemHtml}</div>\`;
581
508
  }).join('\\n');
582
509
 
583
510
  // Animation Styles based on settings
@@ -650,9 +577,10 @@ if (isList && Array.isArray(data)) {
650
577
  if (!wrapper) return;
651
578
 
652
579
  const itemHtml = renderItem(data, 0, 0);
580
+ const itemHeight = computeItemHeight(elements, data, itemHeightFallback);
653
581
  const itemContainerStyle = styleObjectToString({
654
582
  position: 'relative',
655
- minHeight: itemHeightFallback + 'px',
583
+ height: itemHeight,
656
584
  width: '100%'
657
585
  });
658
586
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1urso/generic-editor",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },