From 5ef726b67123da72b5a41a1c764a24249720a9e1 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Mon, 27 Jul 2026 09:44:48 +0100 Subject: [PATCH] Desktop, tablet and mobile style overrides * Responsive inheritance: * tablet inherits desktop * mobile inherits desktop and tablet * Width and minimum-height controls * Padding controls for all four sides * Margin controls for all four sides * Text alignment * Flex display controls * Item alignment and justification * Background colour * Text colour * Border colour * Border width * Corner radius * Responsive override indicators on canvas components * Clear styles for the active breakpoint * Live canvas updates while editing --- .../Resources/views/admin/form.blade.php | 75 ++++++++++++++++++- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/Modules/Content/Resources/views/admin/form.blade.php b/Modules/Content/Resources/views/admin/form.blade.php index 16fd829..dbec35c 100644 --- a/Modules/Content/Resources/views/admin/form.blade.php +++ b/Modules/Content/Resources/views/admin/form.blade.php @@ -105,6 +105,10 @@ function normaliseNode(node) { node.id ||= uid(); node.type ||= 'field'; + node.styles ||= { desktop: {}, tablet: {}, mobile: {} }; + node.styles.desktop ||= {}; + node.styles.tablet ||= {}; + node.styles.mobile ||= {}; if (node.type === 'columns') { node.columns ||= [{ children: [] }, { children: [] }]; node.columns.forEach(column => { @@ -127,6 +131,51 @@ layout.forEach(normaliseNode); let selectedId = layout[0]?.id || null; let dragId = null; + let activeBreakpoint = 'desktop'; + + + const styleDefaults = { + width: '', minHeight: '', textAlign: '', paddingTop: '', paddingRight: '', paddingBottom: '', paddingLeft: '', + marginTop: '', marginRight: '', marginBottom: '', marginLeft: '', backgroundColor: '', color: '', + borderColor: '', borderWidth: '', borderRadius: '', display: '', alignItems: '', justifyContent: '' + }; + + function resolvedStyles(node) { + const desktop = node.styles?.desktop || {}; + const tablet = node.styles?.tablet || {}; + const mobile = node.styles?.mobile || {}; + if (activeBreakpoint === 'desktop') return { ...styleDefaults, ...desktop }; + if (activeBreakpoint === 'tablet') return { ...styleDefaults, ...desktop, ...tablet }; + return { ...styleDefaults, ...desktop, ...tablet, ...mobile }; + } + + function cssValue(value, unit = 'px') { + if (value === '' || value === null || value === undefined) return ''; + if (typeof value === 'number' || /^-?\d+(\.\d+)?$/.test(String(value))) return `${value}${unit}`; + return String(value); + } + + function nodeStyle(node) { + const s = resolvedStyles(node); + const declarations = { + width: s.width, minHeight: cssValue(s.minHeight), textAlign: s.textAlign, + paddingTop: cssValue(s.paddingTop), paddingRight: cssValue(s.paddingRight), + paddingBottom: cssValue(s.paddingBottom), paddingLeft: cssValue(s.paddingLeft), + marginTop: cssValue(s.marginTop), marginRight: cssValue(s.marginRight), + marginBottom: cssValue(s.marginBottom), marginLeft: cssValue(s.marginLeft), + backgroundColor: s.backgroundColor, color: s.color, borderColor: s.borderColor, + borderWidth: cssValue(s.borderWidth), borderRadius: cssValue(s.borderRadius), + borderStyle: s.borderWidth ? 'solid' : '', display: s.display, alignItems: s.alignItems, + justifyContent: s.justifyContent + }; + return Object.entries(declarations).filter(([, value]) => value !== '').map(([key, value]) => `${key.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}:${value}`).join(';'); + } + + function responsiveBadge(node) { + const hasTablet = Object.values(node.styles?.tablet || {}).some(value => value !== ''); + const hasMobile = Object.values(node.styles?.mobile || {}).some(value => value !== ''); + return `${hasTablet ? 'T' : ''}${hasMobile ? 'M' : ''}`; + } const library = document.getElementById('builder-library'); const layoutLibrary = document.getElementById('layout-library'); @@ -217,12 +266,12 @@ if (node.type === 'field') { const definition = schema[node.key] || {}; const value = data[node.key]; - return `
${nodeToolbar(node)}

${escapeHtml(value === true ? 'Enabled' : value === false ? 'Disabled' : value || 'Click to add content')}

`; + return `
${nodeToolbar(node)}

${escapeHtml(value === true ? 'Enabled' : value === false ? 'Disabled' : value || 'Click to add content')}

`; } if (node.type === 'columns') { - return `
${nodeToolbar(node)}
${node.columns.map((column, index) => `
${renderDropZone(node.id, `column:${index}`, `Column ${index + 1}`)}
${column.children.map(renderNode).join('')}
`).join('')}
`; + return `
${nodeToolbar(node)}
${node.columns.map((column, index) => `
${renderDropZone(node.id, `column:${index}`, `Column ${index + 1}`)}
${column.children.map(renderNode).join('')}
`).join('')}
`; } - return `
${nodeToolbar(node)}
${renderDropZone(node.id, 'children')}
${(node.children || []).map(renderNode).join('')}
`; + return `
${nodeToolbar(node)}
${renderDropZone(node.id, 'children')}
${(node.children || []).map(renderNode).join('')}
`; } function bindCanvas() { @@ -295,8 +344,24 @@ const node = found.node; if (node.type === 'field') return renderFieldInspector(node); document.getElementById('inspector-help').textContent = node.label || titleFor(node.type); - inspector.innerHTML = `
${node.type === 'columns' ? `` : ''}

Drag components using their cards. Drop them into sections, containers, columns, or the page root.

`; + const current = node.styles[activeBreakpoint] || (node.styles[activeBreakpoint] = {}); + const input = (key, label, type = 'text', placeholder = '') => ``; + const select = (key, label, options) => ``; + inspector.innerHTML = `
+
${activeBreakpoint[0].toUpperCase() + activeBreakpoint.slice(1)} stylesTablet and mobile inherit larger breakpoints until overridden.
+ + ${node.type === 'columns' ? `` : ''} +
Layout
${input('width','Width','text','100%')}${input('minHeight','Min height','number')}${select('display','Display',[['','Default'],['block','Block'],['flex','Flex']])}${select('textAlign','Text align',[['','Default'],['left','Left'],['center','Centre'],['right','Right']])}${select('alignItems','Align items',[['','Default'],['flex-start','Start'],['center','Centre'],['flex-end','End'],['stretch','Stretch']])}${select('justifyContent','Justify',[['','Default'],['flex-start','Start'],['center','Centre'],['space-between','Space between'],['flex-end','End']])}
+
Spacing
${input('paddingTop','Padding top','number')}${input('paddingRight','Padding right','number')}${input('paddingBottom','Padding bottom','number')}${input('paddingLeft','Padding left','number')}${input('marginTop','Margin top','number')}${input('marginRight','Margin right','number')}${input('marginBottom','Margin bottom','number')}${input('marginLeft','Margin left','number')}
+
Appearance
${input('backgroundColor','Background','color')}${input('color','Text colour','color')}${input('borderColor','Border colour','color')}${input('borderWidth','Border width','number')}${input('borderRadius','Corner radius','number')}
+ +
`; document.getElementById('node-label')?.addEventListener('input', event => { node.label = event.target.value; sync(); renderCanvas(); }); + inspector.querySelectorAll('[data-style-key]').forEach(control => control.addEventListener('input', event => { + node.styles[activeBreakpoint][control.dataset.styleKey] = event.target.value; + sync(); renderCanvas(); + })); + document.getElementById('clear-breakpoint-styles')?.addEventListener('click', () => { node.styles[activeBreakpoint] = {}; update(); }); document.getElementById('column-count')?.addEventListener('change', event => { const count = Number(event.target.value); while (node.columns.length < count) node.columns.push({ id: uid(), children: [] }); @@ -327,9 +392,11 @@ } }); document.querySelectorAll('.builder-width').forEach(button => button.addEventListener('click', () => { + activeBreakpoint = button.textContent.trim().toLowerCase(); document.getElementById('builder-viewport').style.maxWidth = button.dataset.width; document.querySelectorAll('.builder-width').forEach(item => item.classList.remove('font-medium','bg-zinc-100','dark:bg-zinc-800')); button.classList.add('font-medium','bg-zinc-100','dark:bg-zinc-800'); + renderCanvas(); renderInspector(); })); document.getElementById('content-document-form').addEventListener('submit', sync); document.getElementById('content-type-select')?.addEventListener('change', event => {