@1urso/generic-editor 0.1.46 → 0.1.48

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 // 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 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 ? element.x : 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 ...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 // 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 // Split into Static (absolute) and Flow (relative/dynamic) elements\n const staticElements = [];\n const flowElements = [];\n \n layoutElements.forEach(el => {\n if ((el.type === 'text' || el.type === 'text-container') && el.autoGrow) {\n flowElements.push(el);\n } else {\n staticElements.push(el);\n }\n });\n\n // 1. Render Static Elements (Absolute Positioning)\n // They form the \"base layer\" and don't affect the flow\n const staticHtml = staticElements.map(element => renderSingleElement(element, itemData, false)).join('\\n');\n\n // 2. Render Flow Elements (Relative Positioning)\n // Group them into \"Visual Lines\" based on Y position\n flowElements.sort((a, b) => a.y - b.y);\n \n const flowLines = [];\n let currentLine = null;\n \n flowElements.forEach(el => {\n if (!currentLine) {\n currentLine = { y: el.y, elements: [el] };\n flowLines.push(currentLine);\n } else {\n // If element is within 10px vertically, consider it same line\n if (Math.abs(el.y - currentLine.y) <= 10) {\n currentLine.elements.push(el);\n } else {\n currentLine = { y: el.y, elements: [el] };\n flowLines.push(currentLine);\n }\n }\n });\n\n // Render Flow Lines\n let previousLineBottomDesign = 0; // Track where the previous line ended in DESIGN coordinates\n\n const flowHtml = flowLines.map((line, lineIndex) => {\n // Sort elements in line by X\n line.elements.sort((a, b) => a.x - b.x);\n\n // Calculate margin-top for the line container\n // If it's the first line, margin is its Y position.\n // If subsequent line, margin is the gap between this line's design Y and previous line's design bottom.\n let marginTop = 0;\n if (lineIndex === 0) {\n marginTop = line.y;\n } else {\n marginTop = Math.max(0, line.y - previousLineBottomDesign);\n }\n\n // Update previousLineBottomDesign for the NEXT iteration\n // We use the MAX bottom of elements in this line\n let maxLineHeight = 0;\n line.elements.forEach(el => {\n if (el.height > maxLineHeight) maxLineHeight = el.height;\n });\n previousLineBottomDesign = line.y + maxLineHeight;\n\n const lineStyle = styleObjectToString({\n position: 'relative',\n width: '100%',\n marginTop: marginTop,\n // We don't set height, let it grow\n display: 'block', // Ensure it breaks to new line\n whiteSpace: 'nowrap' // Keep inline-block children on same line\n });\n\n const childrenHtml = line.elements.map(element => {\n // Calculate margin-left relative to previous element in line\n const elemIndex = line.elements.indexOf(element);\n let marginLeft = 0;\n if (elemIndex === 0) {\n marginLeft = element.x;\n } else {\n const prev = line.elements[elemIndex - 1];\n const prevEnd = prev.x + prev.width;\n marginLeft = Math.max(0, element.x - prevEnd);\n }\n\n // Pass computed margin to render function\n return renderSingleElement(element, itemData, true, { marginLeft, marginTop: 0 }); // marginTop handled by line container\n }).join('\\n');\n\n return `<div class=\"flow-line\" style=\"${lineStyle}\">${childrenHtml}</div>`;\n }).join('\\n');\n\n return staticHtml + '\\n' + flowHtml;\n };\n\n const renderSingleElement = (element, itemData, isFlow, flowStyles = {}) => {\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: isFlow ? 'relative' : 'absolute',\n left: isFlow ? undefined : element.x,\n top: isFlow ? undefined : element.y,\n marginLeft: isFlow ? flowStyles.marginLeft : undefined,\n marginTop: isFlow ? flowStyles.marginTop : 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',\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 };\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";
18550
18550
  export { GenericEditor as EditorContent, generateHTML };
@@ -378,153 +378,202 @@ function renderTemplate(elements, data, options = {}) {
378
378
  const renderItem = (itemData, index = 0, offsetY = 0) => {
379
379
  const { layoutElements } = computeLayout(elements, itemData);
380
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;
381
+ // Split into Static (absolute) and Flow (relative/dynamic) elements
382
+ const staticElements = [];
383
+ const flowElements = [];
386
384
 
387
385
  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);
386
+ if ((el.type === 'text' || el.type === 'text-container') && el.autoGrow) {
387
+ flowElements.push(el);
388
+ } else {
389
+ staticElements.push(el);
390
+ }
391
+ });
392
+
393
+ // 1. Render Static Elements (Absolute Positioning)
394
+ // They form the "base layer" and don't affect the flow
395
+ const staticHtml = staticElements.map(element => renderSingleElement(element, itemData, false)).join('\\n');
396
+
397
+ // 2. Render Flow Elements (Relative Positioning)
398
+ // Group them into "Visual Lines" based on Y position
399
+ flowElements.sort((a, b) => a.y - b.y);
400
+
401
+ const flowLines = [];
402
+ let currentLine = null;
403
+
404
+ flowElements.forEach(el => {
405
+ if (!currentLine) {
406
+ currentLine = { y: el.y, elements: [el] };
407
+ flowLines.push(currentLine);
392
408
  } 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);
409
+ // If element is within 10px vertically, consider it same line
410
+ if (Math.abs(el.y - currentLine.y) <= 10) {
411
+ currentLine.elements.push(el);
397
412
  } else {
398
- currentRow.elements.push(el);
399
- currentRow.endY = Math.max(currentRow.endY, elBottom);
413
+ currentLine = { y: el.y, elements: [el] };
414
+ flowLines.push(currentLine);
400
415
  }
401
416
  }
402
417
  });
403
418
 
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
- // Calculate Row Min Height (based on static elements and initial size of flow elements)
409
- let minHeight = 0;
410
- row.elements.forEach(el => {
411
- // For static elements, bottom relative to row start
412
- // For flow elements, we also use their initial design height as minimum
413
- const bottom = (el.y - row.startY) + el.height;
414
- if (bottom > minHeight) minHeight = bottom;
419
+ // Render Flow Lines
420
+ let previousLineBottomDesign = 0; // Track where the previous line ended in DESIGN coordinates
421
+
422
+ const flowHtml = flowLines.map((line, lineIndex) => {
423
+ // Sort elements in line by X
424
+ line.elements.sort((a, b) => a.x - b.x);
425
+
426
+ // Calculate margin-top for the line container
427
+ // If it's the first line, margin is its Y position.
428
+ // If subsequent line, margin is the gap between this line's design Y and previous line's design bottom.
429
+ let marginTop = 0;
430
+ if (lineIndex === 0) {
431
+ marginTop = line.y;
432
+ } else {
433
+ marginTop = Math.max(0, line.y - previousLineBottomDesign);
434
+ }
435
+
436
+ // Update previousLineBottomDesign for the NEXT iteration
437
+ // We use the MAX bottom of elements in this line
438
+ let maxLineHeight = 0;
439
+ line.elements.forEach(el => {
440
+ if (el.height > maxLineHeight) maxLineHeight = el.height;
415
441
  });
442
+ previousLineBottomDesign = line.y + maxLineHeight;
416
443
 
417
- const rowStyle = styleObjectToString({
444
+ const lineStyle = styleObjectToString({
418
445
  position: 'relative',
419
446
  width: '100%',
420
- minHeight: minHeight,
447
+ marginTop: marginTop,
448
+ // We don't set height, let it grow
449
+ display: 'block', // Ensure it breaks to new line
450
+ whiteSpace: 'nowrap' // Keep inline-block children on same line
421
451
  });
422
452
 
423
- const childrenHtml = row.elements.map(element => {
424
- let content = element.content;
425
- let imgSrc = '';
426
-
427
- // Resolve Content & Formatting
428
- if (element.type === 'text' || element.type === 'text-container') {
429
- content = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {
430
- const val = itemData[key.trim()];
431
- if (val === undefined || val === null) return match;
432
- if (element.formatting) {
433
- return formatValue(val, element.formatting);
434
- }
435
- return String(val);
436
- });
437
- } else if (element.type === 'image') {
438
- if (element.dataBinding) {
439
- const val = itemData[element.dataBinding];
440
- if (val !== undefined && val !== null) {
441
- imgSrc = String(val);
442
- } else {
443
- imgSrc = content;
444
- }
445
- } else {
446
- imgSrc = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {
447
- const val = itemData[key.trim()];
448
- return val !== undefined && val !== null ? String(val) : match;
449
- });
450
- }
453
+ const childrenHtml = line.elements.map(element => {
454
+ // Calculate margin-left relative to previous element in line
455
+ const elemIndex = line.elements.indexOf(element);
456
+ let marginLeft = 0;
457
+ if (elemIndex === 0) {
458
+ marginLeft = element.x;
459
+ } else {
460
+ const prev = line.elements[elemIndex - 1];
461
+ const prevEnd = prev.x + prev.width;
462
+ marginLeft = Math.max(0, element.x - prevEnd);
451
463
  }
452
464
 
453
- // Resolve Conditional Styles
454
- let conditionalStyles = {};
455
- if (element.conditions) {
456
- element.conditions.forEach(rule => {
457
- const propVal = itemData[rule.property];
458
- if (checkCondition(propVal, rule.operator, rule.value)) {
459
- conditionalStyles = { ...conditionalStyles, ...rule.style };
460
- }
461
- });
465
+ // Pass computed margin to render function
466
+ return renderSingleElement(element, itemData, true, { marginLeft, marginTop: 0 }); // marginTop handled by line container
467
+ }).join('\\n');
468
+
469
+ return \`<div class="flow-line" style="\${lineStyle}">\${childrenHtml}</div>\`;
470
+ }).join('\\n');
471
+
472
+ return staticHtml + '\\n' + flowHtml;
473
+ };
474
+
475
+ const renderSingleElement = (element, itemData, isFlow, flowStyles = {}) => {
476
+ let content = element.content;
477
+ let imgSrc = '';
478
+
479
+ // Resolve Content & Formatting
480
+ if (element.type === 'text' || element.type === 'text-container') {
481
+ content = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {
482
+ const val = itemData[key.trim()];
483
+ if (val === undefined || val === null) return match;
484
+ if (element.formatting) {
485
+ return formatValue(val, element.formatting);
462
486
  }
487
+ return String(val);
488
+ });
489
+ } else if (element.type === 'image') {
490
+ if (element.dataBinding) {
491
+ const val = itemData[element.dataBinding];
492
+ if (val !== undefined && val !== null) {
493
+ imgSrc = String(val);
494
+ } else {
495
+ imgSrc = content;
496
+ }
497
+ } else {
498
+ imgSrc = content.replace(/\\{\\{(.*?)\\}\\}/g, (match, key) => {
499
+ const val = itemData[key.trim()];
500
+ return val !== undefined && val !== null ? String(val) : match;
501
+ });
502
+ }
503
+ }
463
504
 
464
- // Resolve Style Bindings
465
- let bindingStyles = {};
466
- if (element.styleBindings) {
467
- Object.entries(element.styleBindings).forEach(([styleProp, variableName]) => {
468
- const val = itemData[variableName];
469
- if (val !== undefined && val !== null) {
470
- bindingStyles[styleProp] = String(val);
471
- }
472
- });
505
+ // Resolve Conditional Styles
506
+ let conditionalStyles = {};
507
+ if (element.conditions) {
508
+ element.conditions.forEach(rule => {
509
+ const propVal = itemData[rule.property];
510
+ if (checkCondition(propVal, rule.operator, rule.value)) {
511
+ conditionalStyles = { ...conditionalStyles, ...rule.style };
473
512
  }
513
+ });
514
+ }
474
515
 
475
- const isFlow = flowElements.includes(element);
476
- const baseStyle = {
477
- position: isFlow ? 'relative' : 'absolute',
478
- left: isFlow ? undefined : element.x,
479
- top: isFlow ? undefined : (element.y - row.startY + offsetY),
480
- marginLeft: isFlow ? element.x : undefined,
481
- marginTop: isFlow ? (element.y - row.startY) : undefined,
482
- width: element.width,
483
- height: element.autoGrow ? 'auto' : element.height,
484
- transform: element.rotation ? \`rotate(\${element.rotation}deg)\` : undefined,
485
- overflow: element.autoGrow ? 'visible' : 'hidden',
486
- whiteSpace: (element.type === 'text-container' && element.autoGrow && element.containerExpansion === 'horizontal') ? 'nowrap' : (element.autoGrow ? 'pre-wrap' : undefined),
487
- wordBreak: element.autoGrow ? 'break-word' : undefined,
488
- display: isFlow ? 'inline-block' : undefined,
489
- verticalAlign: isFlow ? 'top' : undefined,
490
- ...element.style,
491
- ...conditionalStyles,
492
- ...bindingStyles
493
- };
494
-
495
- const styleString = styleObjectToString(baseStyle);
496
-
497
- if (element.type === 'text' || element.type === 'text-container') {
498
- return \`<div style="\${styleString}">\${content}</div>\`;
499
- } else if (element.type === 'image') {
500
- const imgStyle = styleObjectToString({
501
- width: '100%',
502
- height: '100%',
503
- objectFit: element.style?.objectFit || 'cover',
504
- display: 'block'
505
- });
506
- return \`<div style="\${styleString}"><img src="\${imgSrc}" alt="Element" style="\${imgStyle}" /></div>\`;
507
- } else if (element.type === 'box') {
508
- return \`<div style="\${styleString}"></div>\`;
509
- } else if (element.type === 'checkbox') {
510
- let isChecked = false;
511
- if (element.dataBinding) {
512
- const val = itemData[element.dataBinding];
513
- isChecked = val === true || String(val) === 'true';
514
- }
515
- const checkboxStyle = styleObjectToString({
516
- ...baseStyle,
517
- display: 'flex',
518
- alignItems: 'center',
519
- justifyContent: 'center'
520
- });
521
- return \`<div style="\${checkboxStyle}"><input type="checkbox" \${isChecked ? 'checked' : ''} disabled style="width:100%;height:100%;margin:0;" /></div>\`;
516
+ // Resolve Style Bindings
517
+ let bindingStyles = {};
518
+ if (element.styleBindings) {
519
+ Object.entries(element.styleBindings).forEach(([styleProp, variableName]) => {
520
+ const val = itemData[variableName];
521
+ if (val !== undefined && val !== null) {
522
+ bindingStyles[styleProp] = String(val);
522
523
  }
523
- return '';
524
- }).join('\\n');
525
-
526
- return \`<div class="list-row" style="\${rowStyle}">\${childrenHtml}</div>\`;
527
- }).join('\\n');
524
+ });
525
+ }
526
+
527
+ const baseStyle = {
528
+ position: isFlow ? 'relative' : 'absolute',
529
+ left: isFlow ? undefined : element.x,
530
+ top: isFlow ? undefined : element.y,
531
+ marginLeft: isFlow ? flowStyles.marginLeft : undefined,
532
+ marginTop: isFlow ? flowStyles.marginTop : undefined,
533
+ width: element.width,
534
+ height: element.autoGrow ? 'auto' : element.height,
535
+ transform: element.rotation ? \`rotate(\${element.rotation}deg)\` : undefined,
536
+ overflow: element.autoGrow ? 'visible' : 'hidden',
537
+ whiteSpace: (element.type === 'text-container' && element.autoGrow && element.containerExpansion === 'horizontal') ? 'nowrap' : (element.autoGrow ? 'pre-wrap' : undefined),
538
+ wordBreak: element.autoGrow ? 'break-word' : undefined,
539
+ display: isFlow ? 'inline-block' : undefined,
540
+ verticalAlign: isFlow ? 'top' : undefined,
541
+ boxSizing: 'border-box',
542
+ ...element.style,
543
+ ...conditionalStyles,
544
+ ...bindingStyles
545
+ };
546
+
547
+ const styleString = styleObjectToString(baseStyle);
548
+
549
+ if (element.type === 'text' || element.type === 'text-container') {
550
+ return \`<div style="\${styleString}">\${content}</div>\`;
551
+ } else if (element.type === 'image') {
552
+ const imgStyle = styleObjectToString({
553
+ width: '100%',
554
+ height: '100%',
555
+ objectFit: element.style?.objectFit || 'cover',
556
+ display: 'block'
557
+ });
558
+ return \`<div style="\${styleString}"><img src="\${imgSrc}" alt="Element" style="\${imgStyle}" /></div>\`;
559
+ } else if (element.type === 'box') {
560
+ return \`<div style="\${styleString}"></div>\`;
561
+ } else if (element.type === 'checkbox') {
562
+ let isChecked = false;
563
+ if (element.dataBinding) {
564
+ const val = itemData[element.dataBinding];
565
+ isChecked = val === true || String(val) === 'true';
566
+ }
567
+ const checkboxStyle = styleObjectToString({
568
+ ...baseStyle,
569
+ display: 'flex',
570
+ alignItems: 'center',
571
+ justifyContent: 'center'
572
+ });
573
+ return \`<div style="\${checkboxStyle}"><input type="checkbox" \${isChecked ? 'checked' : ''} disabled style="width:100%;height:100%;margin:0;" /></div>\`;
574
+ }
575
+ return '';
576
+ };
528
577
  };
529
578
 
530
579
  if (isList && Array.isArray(data)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1urso/generic-editor",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },