Dashboard 2 template - Making text scale with the widget size

AI and I have been attempting to satisfy this requirement:

  • The dashboard 2 page has a grid theme.
  • There is a ui-template which may be specified at various sizes eg 6x2 grid units
  • Within the template is a div which displays msg.payload. The text is as large as possible to fit within 80% of the widget width and 40% of it's height.
  • As the widget display size changes, eg when a phone is rotated or when the list of pages sidebar is shown, the text and div have to adapt too.

[{"id":"22c62ff56e95cf20","type":"group","z":"f8f7abaa42bf3ef9","name":"v2","style":{"label":true},"nodes":["12f5f3d40972dc09","f9df8cf85847ae11","8b4ff721d203aba2","7f27ecc771562dad"],"x":14,"y":219,"w":392,"h":162},{"id":"12f5f3d40972dc09","type":"ui-template","z":"f8f7abaa42bf3ef9","g":"22c62ff56e95cf20","group":"bd0dc9d68d230789","page":"","ui":"","name":"Weather Card 4x2","order":2,"width":"4","height":"2","format":"<template>\n    <div class=\"dashboard-container\">\n        <div class=\"responsive-text\">\n            {{ msg.payload }}\n        </div>\n        <div class=\"dimension-text\"></div>\n    </div>\n</template>\n\n<script>\n(function() {\n    function updateDimensions(container) {\n        const dimText = container.querySelector(\".dimension-text\");\n        const respText = container.querySelector(\".responsive-text\");\n        if (!dimText || !respText) return;\n\n        const { width, height } = container.getBoundingClientRect();\n        const maxHeight = height * 0.4;\n        const maxWidth = width * 0.75;\n\n        // Allow natural height up to maxHeight\n        respText.style.height = \"auto\";\n        respText.style.maxHeight = maxHeight + \"px\";\n        respText.style.maxWidth = maxWidth + \"px\";\n\n        // Initial font-size guess\n        let newFontSize = Math.floor(maxHeight * 0.8);\n        respText.style.fontSize = newFontSize + \"px\";\n        respText.style.lineHeight = Math.round(newFontSize * 1.05) + \"px\";\n\n        let textHeight = respText.scrollHeight;\n\n        // Too tall → shrink\n        if (textHeight > maxHeight) {\n            const scaleFactor = maxHeight / textHeight;\n            newFontSize = Math.floor(newFontSize * scaleFactor);\n            respText.style.fontSize = newFontSize + \"px\";\n            respText.style.lineHeight = Math.round(newFontSize * 1.05) + \"px\";\n        }\n\n        // Too wide → shrink\n        const textWidth = respText.scrollWidth;\n        const boxWidth = respText.clientWidth;\n        if (textWidth > boxWidth) {\n            const scaleFactor = boxWidth / textWidth;\n            newFontSize = Math.floor(newFontSize * scaleFactor);\n            respText.style.fontSize = newFontSize + \"px\";\n            respText.style.lineHeight = Math.round(newFontSize * 1.05) + \"px\";\n        }\n\n        // Display widget + div info\n        const divRect = respText.getBoundingClientRect();\n        dimText.textContent =\n            `Widget ${Math.round(width)}px × ${Math.round(height)}px | div: ${Math.round(divRect.width)}px × ${Math.round(divRect.height)}px | font-size: ${newFontSize}px`;\n    }\n\n    function observeContainers() {\n        document.querySelectorAll(\".dashboard-container\").forEach(container => {\n            if (!container.__observed) {\n                updateDimensions(container);\n\n                const ro = new ResizeObserver(() => updateDimensions(container));\n                ro.observe(container);\n\n                const respText = container.querySelector(\".responsive-text\");\n                if (respText) {\n                    const mo = new MutationObserver(() => updateDimensions(container));\n                    mo.observe(respText, { childList: true, characterData: true, subtree: true });\n                }\n\n                container.__observed = true;\n            }\n        });\n    }\n\n    if (document.readyState === \"loading\") {\n        document.addEventListener(\"DOMContentLoaded\", observeContainers);\n    } else {\n        observeContainers();\n    }\n\n    setInterval(observeContainers, 500);\n})();\n</script>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":290,"y":260,"wires":[[]]},{"id":"f9df8cf85847ae11","type":"ui-template","z":"f8f7abaa42bf3ef9","g":"22c62ff56e95cf20","group":"bd0dc9d68d230789","page":"","ui":"","name":"Weather Card 6x4","order":4,"width":6,"height":"4","format":"<template>\n    <div class=\"dashboard-container\">\n        <div class=\"responsive-text\">\n            {{ msg.payload }}\n        </div>\n        <div class=\"dimension-text\"></div>\n    </div>\n</template>\n\n<script>\n(function() {\n    function updateDimensions(container) {\n        const dimText = container.querySelector(\".dimension-text\");\n        const respText = container.querySelector(\".responsive-text\");\n        if (!dimText || !respText) return;\n\n        const { width, height } = container.getBoundingClientRect();\n        const maxHeight = height * 0.4;\n        const maxWidth = width * 0.75;\n\n        // Allow natural height up to maxHeight\n        respText.style.height = \"auto\";\n        respText.style.maxHeight = maxHeight + \"px\";\n        respText.style.maxWidth = maxWidth + \"px\";\n\n        // Initial font-size guess\n        let newFontSize = Math.floor(maxHeight * 0.8);\n        respText.style.fontSize = newFontSize + \"px\";\n        respText.style.lineHeight = Math.round(newFontSize * 1.05) + \"px\";\n\n        let textHeight = respText.scrollHeight;\n\n        // Too tall → shrink\n        if (textHeight > maxHeight) {\n            const scaleFactor = maxHeight / textHeight;\n            newFontSize = Math.floor(newFontSize * scaleFactor);\n            respText.style.fontSize = newFontSize + \"px\";\n            respText.style.lineHeight = Math.round(newFontSize * 1.05) + \"px\";\n        }\n\n        // Too wide → shrink\n        const textWidth = respText.scrollWidth;\n        const boxWidth = respText.clientWidth;\n        if (textWidth > boxWidth) {\n            const scaleFactor = boxWidth / textWidth;\n            newFontSize = Math.floor(newFontSize * scaleFactor);\n            respText.style.fontSize = newFontSize + \"px\";\n            respText.style.lineHeight = Math.round(newFontSize * 1.05) + \"px\";\n        }\n\n        // Display widget + div info\n        const divRect = respText.getBoundingClientRect();\n        dimText.textContent =\n            `Widget ${Math.round(width)}px × ${Math.round(height)}px | div: ${Math.round(divRect.width)}px × ${Math.round(divRect.height)}px | font-size: ${newFontSize}px`;\n    }\n\n    function observeContainers() {\n        document.querySelectorAll(\".dashboard-container\").forEach(container => {\n            if (!container.__observed) {\n                updateDimensions(container);\n\n                const ro = new ResizeObserver(() => updateDimensions(container));\n                ro.observe(container);\n\n                const respText = container.querySelector(\".responsive-text\");\n                if (respText) {\n                    const mo = new MutationObserver(() => updateDimensions(container));\n                    mo.observe(respText, { childList: true, characterData: true, subtree: true });\n                }\n\n                container.__observed = true;\n            }\n        });\n    }\n\n    if (document.readyState === \"loading\") {\n        document.addEventListener(\"DOMContentLoaded\", observeContainers);\n    } else {\n        observeContainers();\n    }\n\n    setInterval(observeContainers, 500);\n})();\n</script>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":290,"y":300,"wires":[[]]},{"id":"8b4ff721d203aba2","type":"ui-template","z":"f8f7abaa42bf3ef9","g":"22c62ff56e95cf20","group":"","page":"42e2d917b7e20f69","ui":"","name":"CSS","order":0,"width":0,"height":0,"head":"","format":"/*\n  Dashboard Responsive Text Template\n  Version: v2\n  Variant: responsive-text auto height, max 40%\n*/\n\n.dashboard-container {\n    display: flex;\n    flex-direction: column;\n    justify-content: flex-start;\n    align-items: flex-start;\n    height: 100%;\n    width: 100%;\n    background-color: lemonchiffon;\n    border: 3px solid black;\n    border-radius: 5px;\n    overflow: hidden;\n    position: relative;\n    padding: 4px;\n    box-sizing: border-box;\n}\n\n.responsive-text {\n    display: inline-block;      /* shrink-to-fit width/height */\n    max-width: 75%;             /* cap width */\n    max-height: 40%;            /* cap height */\n    border: 2px solid red;\n    color: #333;\n    font-family: \"Times New Roman\", serif;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    overflow: hidden;\n    box-sizing: border-box;\n    line-height: 1;\n    padding-bottom: 0.15em;     /* allow room for descenders */\n}\n\n.dimension-text {\n    font-size: 0.8rem;\n    color: #555;\n    margin-top: 4px;\n}\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"page:style","className":"","x":110,"y":340,"wires":[[]]},{"id":"7f27ecc771562dad","type":"ui-button-group","z":"f8f7abaa42bf3ef9","g":"22c62ff56e95cf20","name":"","group":"87928b14d093bc1d","order":2,"width":6,"height":1,"label":"button group","className":"","rounded":true,"useThemeColors":true,"passthru":false,"options":[{"label":"Short","icon":"","value":"Quito","valueType":"str","color":"#009933"},{"label":"Medium","icon":"","value":"České Budějovice","valueType":"str","color":"#999999"},{"label":"Long","icon":"","value":"Antananarivo International Airport","valueType":"str","color":"#ff6666"}],"topic":"topic","topicType":"msg","x":110,"y":280,"wires":[["12f5f3d40972dc09","f9df8cf85847ae11"]]},{"id":"bd0dc9d68d230789","type":"ui-group","name":"v2","page":"42e2d917b7e20f69","width":"10","height":1,"order":2,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"42e2d917b7e20f69","type":"ui-page","name":"v2","ui":"e9c974f7c1d080d1","path":"/v2","icon":"home","layout":"grid","theme":"c757d5c250b7d202","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":1,"className":"","visible":true,"disabled":false},{"id":"87928b14d093bc1d","type":"ui-group","name":"Buttons","page":"42e2d917b7e20f69","width":6,"height":1,"order":1,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"e9c974f7c1d080d1","type":"ui-base","name":"My Dashboard","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"headerContent":"page","navigationStyle":"default","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":1,"showDisconnectNotification":true,"allowInstall":true},{"id":"c757d5c250b7d202","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094CE","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"d7d0a35ee5de47a6","type":"global-config","env":[],"modules":{"@flowfuse/node-red-dashboard":"1.28.0"}}]

There are some display sizes where the text gets cropped, maybe not ideal but OK.

  1. This uses a good deal of javascript, which I would prefer to avoid, but chatgpt cannot come up with a fully working version in pure CSS. Can you do better?
  2. Is there a simpler way to achieve this responsive text in a template widget?