From 4927146540ebf817de50ecce7b672266729a8233 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:42:37 +0100 Subject: [PATCH 1/4] Allow 2 gizmo rows in small canvas --- main/view.js | 53 +++++++++++++--------------------------------------- style.css | 38 ++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 57 deletions(-) diff --git a/main/view.js b/main/view.js index 814b4e6c..81b7117b 100644 --- a/main/view.js +++ b/main/view.js @@ -160,6 +160,14 @@ function resizeCanvas() { areaWidth = Math.max(1, areaWidth - Math.round(buttons + padding)); } else { areaHeight -= 60; //Gizmos visible + // Second row of buttons, once the toolbar is narrow enough to wrap. + const row = gizmoButtons.querySelector('.gizmo-buttons-inner'); + const firstButton = row?.querySelector('.gizmo-button'); + if (row && firstButton) { + const extraRows = + row.getBoundingClientRect().height - firstButton.getBoundingClientRect().height; + if (extraRows > 0) areaHeight = Math.max(1, areaHeight - Math.round(extraRows)); + } const status = document.getElementById('gizmoStatus'); if (status) { const statusStyle = getComputedStyle(status); @@ -763,8 +771,7 @@ class PanelResizer { this.startX = 0; this.startCanvasWidth = 0; this.startCodeWidth = 0; - // Floor for either panel; the canvas side is additionally clamped to the - // gizmo toolbar's natural width so its buttons never wrap onto a second row. + // Floor for either panel. this.minPanelFloor = 300; this.minCanvasWidth = this.minPanelFloor; this.touchActivationPointerId = null; @@ -829,7 +836,6 @@ class PanelResizer { this.startCanvasWidth = canvasRect.width; this.startCodeWidth = codeRect.width; - // Cache the gizmo-based minimum once per drag (button set is stable mid-drag). this.minCanvasWidth = this.getMinCanvasWidth(); // Add visual feedback @@ -918,7 +924,7 @@ class PanelResizer { const newCanvasWidth = this.startCanvasWidth + deltaX; const newCodeWidth = this.startCodeWidth - deltaX; - // Ensure minimum widths (canvas side must keep the gizmo row on one line) + // Ensure minimum widths if (newCanvasWidth >= this.minCanvasWidth && newCodeWidth >= this.minPanelFloor) { const totalWidth = mainRect.width; const canvasFlexBasis = (newCanvasWidth / totalWidth) * 100; @@ -989,43 +995,10 @@ class PanelResizer { e.preventDefault(); } - // Smallest canvas-panel width that still fits the gizmo toolbar on one line. - // Sums the toolbar buttons (plus gaps and horizontal padding) so the result is - // the single-line row width regardless of current wrapping. Reading each - // button's own width avoids counting the absolutely-positioned shape-menu - // dropdown nested inside #shape-menu, which would otherwise inflate the min. - // The buttons live in an inner wrapper (.gizmo-buttons-inner) that carries the - // flex layout + gap; #gizmoButtons itself carries the horizontal padding. + // Below 440px the toolbar drops to a 5-per-row grid (style.css container + // query), which fits well inside the floor. getMinCanvasWidth() { - const gizmo = document.getElementById('gizmoButtons'); - if (!gizmo) return this.minPanelFloor; - - const row = gizmo.querySelector('.gizmo-buttons-inner') || gizmo; - - const paddingStyle = window.getComputedStyle(gizmo); - const paddingX = - (parseFloat(paddingStyle.paddingLeft) || 0) + - (parseFloat(paddingStyle.paddingRight) || 0); - - const rowStyle = window.getComputedStyle(row); - const gap = parseFloat(rowStyle.columnGap || rowStyle.gap) || 0; - - let total = paddingX; - let visibleChildren = 0; - for (const child of row.children) { - const childStyle = window.getComputedStyle(child); - if (childStyle.display === 'none') continue; - // getBoundingClientRect excludes margins, so add them explicitly. - total += - child.getBoundingClientRect().width + - (parseFloat(childStyle.marginLeft) || 0) + - (parseFloat(childStyle.marginRight) || 0); - visibleChildren += 1; - } - if (visibleChildren > 1) total += gap * (visibleChildren - 1); - - // +1 guards against sub-pixel rounding tipping the last icon onto a new row. - return Math.max(this.minPanelFloor, Math.ceil(total) + 1); + return this.minPanelFloor; } // A focusable role="separator" needs a value: the canvas panel's share of the split. diff --git a/style.css b/style.css index 58a76bee..a2c03483 100644 --- a/style.css +++ b/style.css @@ -542,7 +542,9 @@ button { background: transparent; color: #ffffff; cursor: pointer; - transition: transform 0.12s ease, color 0.12s ease; + transition: + transform 0.12s ease, + color 0.12s ease; } .canvas-stopped-overlay__play .icon, @@ -587,6 +589,7 @@ button { display: flex; flex-direction: column; overflow: hidden; + container-type: inline-size; } #codePanel { @@ -771,15 +774,8 @@ button { background-color: var(--color-focus); } -/* On phone widths the toolbar no longer fits on one line. Force a - deliberate 5-per-row grid (2 tidy rows for the current 10 buttons) - rather than letting flex-wrap overflow unevenly. Columns are sized to - content (not 1fr) and the group is centred, so the buttons stay packed - together instead of spreading across the full width. Shared by portrait - phones and phones in landscape (docked beside the canvas, see below); - tablets still fit one row in either orientation. */ -@media (max-width: 600px), - (max-width: 1024px) and (orientation: landscape) and (max-height: 500px) { +/* Compact gizmos on small screens */ +@container (max-width: 440px) { .gizmo-buttons-inner { display: grid; grid-template-columns: repeat(5, auto); @@ -791,10 +787,6 @@ button { .gizmo-buttons-inner #shape-menu { margin-left: 0; } -} - -/* Too narrow for the longest prompts on one line, so give the status two. */ -@media (max-width: 600px) { .gizmo-status { height: 3.2em; white-space: normal; @@ -802,6 +794,18 @@ button { } } +@media (max-width: 1024px) and (orientation: landscape) and (max-height: 500px) { + .gizmo-buttons-inner { + display: grid; + grid-template-columns: repeat(5, auto); + justify-content: center; + gap: 5px; + } + .gizmo-buttons-inner #shape-menu { + margin-left: 0; + } +} + .bigbutton.active { box-shadow: inset 0 0 0 2px var(--color-primary); } @@ -1468,7 +1472,7 @@ button { background-color: var(--color-button-bg-hover); } -.example-tab[aria-selected="true"] { +.example-tab[aria-selected='true'] { background-color: var(--color-primary); border-color: var(--color-primary); color: var(--color-text-on-primary); @@ -1989,7 +1993,7 @@ body.color-picker-open #renderCanvas { list-style: none; } -/* On phones the shape button is centred in the 2-row gizmo grid, so the +/* Once the toolbar is a 2-row grid the shape button is centred in it, so the popup's old anchor (bottom-left of that button) ran partway off the right edge. Instead anchor it to the whole gizmo bar and span full width, dropping straight down into the space below the toolbar (its height is @@ -1997,7 +2001,7 @@ body.color-picker-open #renderCanvas { stretched to fill the full width so there's no empty grey band on the right, and the tiles are shrunk to suit the smaller screen. Placed after the desktop rules above so these overrides win on source order. */ -@media (max-width: 600px) { +@container (max-width: 440px) { #gizmoButtons { position: relative; } From 9d37228c2899131edf0f0b824ca2590df41b8eed Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:00:27 +0100 Subject: [PATCH 2/4] Fix add shape panel --- style.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/style.css b/style.css index a2c03483..d658e1d9 100644 --- a/style.css +++ b/style.css @@ -2014,9 +2014,9 @@ body.color-picker-open #renderCanvas { right: 0; top: 100%; /* directly below the toolbar */ bottom: auto; - /* top:100% lands below the toolbar's 25px bottom padding, leaving a wide - gap; pull the menu back up to sit just under the buttons. */ - margin-top: -20px; + /* Pull back up past the bar's 25px bottom padding and the .gizmo-status + block below the buttons (24px margin + 3.2rem) to sit just under them. */ + margin-top: calc(-44px - 3.2rem); width: auto; max-width: 100%; padding: 10px; /* symmetric so the grey side gaps match */ From 9c364f8fc24a0c3ec643d6e3efa238521f510702 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:31:59 +0100 Subject: [PATCH 3/4] Move chevrons if narrow desktop --- style.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/style.css b/style.css index d658e1d9..2ecf3d23 100644 --- a/style.css +++ b/style.css @@ -2055,6 +2055,19 @@ body.color-picker-open #renderCanvas { } } +/* A narrow canvas panel has no grey margin beside the tiles to hold the + chevrons, and #canvasArea would clip them, so bring them back inside. */ +@media (min-width: 601px) { + @container (max-width: 440px) { + .scrollable-container .scroll-button.left { + left: 0; + } + .scrollable-container .scroll-button.right { + right: 0; + } + } +} + /* Landscape: open rightward from the plus button, across the rest of the toolbar and the grey space beyond it, so the canvas stays clear. #shape-menu goes static so the popup resolves against #gizmoButtons, whose left edge is From b4c1ba5d884bb1dd162c7b0a26db4c88e86e35db Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:11:17 +0100 Subject: [PATCH 4/4] Argh --- main/view.js | 53 +++++++++++++++++++++++++++++++++++++++------------- style.css | 51 +++++++++++++++++--------------------------------- 2 files changed, 57 insertions(+), 47 deletions(-) diff --git a/main/view.js b/main/view.js index 81b7117b..814b4e6c 100644 --- a/main/view.js +++ b/main/view.js @@ -160,14 +160,6 @@ function resizeCanvas() { areaWidth = Math.max(1, areaWidth - Math.round(buttons + padding)); } else { areaHeight -= 60; //Gizmos visible - // Second row of buttons, once the toolbar is narrow enough to wrap. - const row = gizmoButtons.querySelector('.gizmo-buttons-inner'); - const firstButton = row?.querySelector('.gizmo-button'); - if (row && firstButton) { - const extraRows = - row.getBoundingClientRect().height - firstButton.getBoundingClientRect().height; - if (extraRows > 0) areaHeight = Math.max(1, areaHeight - Math.round(extraRows)); - } const status = document.getElementById('gizmoStatus'); if (status) { const statusStyle = getComputedStyle(status); @@ -771,7 +763,8 @@ class PanelResizer { this.startX = 0; this.startCanvasWidth = 0; this.startCodeWidth = 0; - // Floor for either panel. + // Floor for either panel; the canvas side is additionally clamped to the + // gizmo toolbar's natural width so its buttons never wrap onto a second row. this.minPanelFloor = 300; this.minCanvasWidth = this.minPanelFloor; this.touchActivationPointerId = null; @@ -836,6 +829,7 @@ class PanelResizer { this.startCanvasWidth = canvasRect.width; this.startCodeWidth = codeRect.width; + // Cache the gizmo-based minimum once per drag (button set is stable mid-drag). this.minCanvasWidth = this.getMinCanvasWidth(); // Add visual feedback @@ -924,7 +918,7 @@ class PanelResizer { const newCanvasWidth = this.startCanvasWidth + deltaX; const newCodeWidth = this.startCodeWidth - deltaX; - // Ensure minimum widths + // Ensure minimum widths (canvas side must keep the gizmo row on one line) if (newCanvasWidth >= this.minCanvasWidth && newCodeWidth >= this.minPanelFloor) { const totalWidth = mainRect.width; const canvasFlexBasis = (newCanvasWidth / totalWidth) * 100; @@ -995,10 +989,43 @@ class PanelResizer { e.preventDefault(); } - // Below 440px the toolbar drops to a 5-per-row grid (style.css container - // query), which fits well inside the floor. + // Smallest canvas-panel width that still fits the gizmo toolbar on one line. + // Sums the toolbar buttons (plus gaps and horizontal padding) so the result is + // the single-line row width regardless of current wrapping. Reading each + // button's own width avoids counting the absolutely-positioned shape-menu + // dropdown nested inside #shape-menu, which would otherwise inflate the min. + // The buttons live in an inner wrapper (.gizmo-buttons-inner) that carries the + // flex layout + gap; #gizmoButtons itself carries the horizontal padding. getMinCanvasWidth() { - return this.minPanelFloor; + const gizmo = document.getElementById('gizmoButtons'); + if (!gizmo) return this.minPanelFloor; + + const row = gizmo.querySelector('.gizmo-buttons-inner') || gizmo; + + const paddingStyle = window.getComputedStyle(gizmo); + const paddingX = + (parseFloat(paddingStyle.paddingLeft) || 0) + + (parseFloat(paddingStyle.paddingRight) || 0); + + const rowStyle = window.getComputedStyle(row); + const gap = parseFloat(rowStyle.columnGap || rowStyle.gap) || 0; + + let total = paddingX; + let visibleChildren = 0; + for (const child of row.children) { + const childStyle = window.getComputedStyle(child); + if (childStyle.display === 'none') continue; + // getBoundingClientRect excludes margins, so add them explicitly. + total += + child.getBoundingClientRect().width + + (parseFloat(childStyle.marginLeft) || 0) + + (parseFloat(childStyle.marginRight) || 0); + visibleChildren += 1; + } + if (visibleChildren > 1) total += gap * (visibleChildren - 1); + + // +1 guards against sub-pixel rounding tipping the last icon onto a new row. + return Math.max(this.minPanelFloor, Math.ceil(total) + 1); } // A focusable role="separator" needs a value: the canvas panel's share of the split. diff --git a/style.css b/style.css index 2ecf3d23..67a02ac0 100644 --- a/style.css +++ b/style.css @@ -542,9 +542,7 @@ button { background: transparent; color: #ffffff; cursor: pointer; - transition: - transform 0.12s ease, - color 0.12s ease; + transition: transform 0.12s ease, color 0.12s ease; } .canvas-stopped-overlay__play .icon, @@ -589,7 +587,6 @@ button { display: flex; flex-direction: column; overflow: hidden; - container-type: inline-size; } #codePanel { @@ -774,8 +771,15 @@ button { background-color: var(--color-focus); } -/* Compact gizmos on small screens */ -@container (max-width: 440px) { +/* On phone widths the toolbar no longer fits on one line. Force a + deliberate 5-per-row grid (2 tidy rows for the current 10 buttons) + rather than letting flex-wrap overflow unevenly. Columns are sized to + content (not 1fr) and the group is centred, so the buttons stay packed + together instead of spreading across the full width. Shared by portrait + phones and phones in landscape (docked beside the canvas, see below); + tablets still fit one row in either orientation. */ +@media (max-width: 600px), + (max-width: 1024px) and (orientation: landscape) and (max-height: 500px) { .gizmo-buttons-inner { display: grid; grid-template-columns: repeat(5, auto); @@ -787,6 +791,10 @@ button { .gizmo-buttons-inner #shape-menu { margin-left: 0; } +} + +/* Too narrow for the longest prompts on one line, so give the status two. */ +@media (max-width: 600px) { .gizmo-status { height: 3.2em; white-space: normal; @@ -794,18 +802,6 @@ button { } } -@media (max-width: 1024px) and (orientation: landscape) and (max-height: 500px) { - .gizmo-buttons-inner { - display: grid; - grid-template-columns: repeat(5, auto); - justify-content: center; - gap: 5px; - } - .gizmo-buttons-inner #shape-menu { - margin-left: 0; - } -} - .bigbutton.active { box-shadow: inset 0 0 0 2px var(--color-primary); } @@ -1472,7 +1468,7 @@ button { background-color: var(--color-button-bg-hover); } -.example-tab[aria-selected='true'] { +.example-tab[aria-selected="true"] { background-color: var(--color-primary); border-color: var(--color-primary); color: var(--color-text-on-primary); @@ -1993,7 +1989,7 @@ body.color-picker-open #renderCanvas { list-style: none; } -/* Once the toolbar is a 2-row grid the shape button is centred in it, so the +/* On phones the shape button is centred in the 2-row gizmo grid, so the popup's old anchor (bottom-left of that button) ran partway off the right edge. Instead anchor it to the whole gizmo bar and span full width, dropping straight down into the space below the toolbar (its height is @@ -2001,7 +1997,7 @@ body.color-picker-open #renderCanvas { stretched to fill the full width so there's no empty grey band on the right, and the tiles are shrunk to suit the smaller screen. Placed after the desktop rules above so these overrides win on source order. */ -@container (max-width: 440px) { +@media (max-width: 600px) { #gizmoButtons { position: relative; } @@ -2055,19 +2051,6 @@ body.color-picker-open #renderCanvas { } } -/* A narrow canvas panel has no grey margin beside the tiles to hold the - chevrons, and #canvasArea would clip them, so bring them back inside. */ -@media (min-width: 601px) { - @container (max-width: 440px) { - .scrollable-container .scroll-button.left { - left: 0; - } - .scrollable-container .scroll-button.right { - right: 0; - } - } -} - /* Landscape: open rightward from the plus button, across the rest of the toolbar and the grey space beyond it, so the canvas stays clear. #shape-menu goes static so the popup resolves against #gizmoButtons, whose left edge is