🎉 Add selection size badge below bounding box (#9210)

* 🎉 Add selection size badge below bounding box

Signed-off-by: bittoby <218712309+bittoby@users.noreply.github.com>

* 💄 Address review comments

Signed-off-by: bittoby <218712309+bittoby@users.noreply.github.com>

* 💄 Move selection size badge text styles to SCSS class

---------

Signed-off-by: bittoby <218712309+bittoby@users.noreply.github.com>
Signed-off-by: BitToby <218712309+bittoby@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
BitToby 2026-05-08 02:20:24 -10:00 committed by GitHub
parent bb93928099
commit 6aeccb1208
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 96 additions and 0 deletions

View File

@ -8,6 +8,8 @@
### :sparkles: New features & Enhancements
- Show a read-only W × H size badge below the bounding box of the current selection so dimensions are visible directly on the canvas (initial implementation: always-on badge, single visual variant, no live-resize integration; remaining spec items follow in subsequent PRs) [Github #9205](https://github.com/penpot/penpot/issues/9205)
### :bug: Bugs fixed
- Harden Nginx responses with standard security headers and hide upstream `X-Powered-By` headers

View File

@ -78,6 +78,32 @@ test("User draws a rect", async ({ page }) => {
await expect(workspacePage.canvas).toHaveScreenshot();
});
test("Selection size badge appears on selection and hides on deselect", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
/get\-file\?/,
"workspace/get-file-not-empty.json",
);
await workspacePage.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
const badge = page.locator(".selection-size-badge");
await expect(badge).toHaveCount(0);
await workspacePage.clickLeafLayer("Rectangle");
await expect(badge).toBeVisible();
await workspacePage.page.keyboard.press("Escape");
await expect(badge).toHaveCount(0);
});
test("User makes a group", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();

View File

@ -5,6 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.measurements
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
@ -43,6 +44,13 @@
(def distance-pill-height 16)
(def distance-line-stroke 1)
(def ^:private ^:const selection-badge-bg-color "var(--color-accent-tertiary)")
(def ^:private ^:const selection-badge-height 16)
(def ^:private ^:const selection-badge-padding-x 6)
(def ^:private ^:const selection-badge-vertical-gap 8)
(def ^:private ^:const selection-badge-border-radius 2)
(def ^:private ^:const selection-badge-char-width 6.5)
;; ------------------------------------------------
;; HELPERS
@ -179,6 +187,35 @@
:stroke hover-color
:stroke-width selection-rect-width}}]]))
(mf/defc selection-size-badge*
[{:keys [selrect zoom]}]
(let [{:keys [x y width height]} selrect
size-label (dm/str (fmt/format-number width) " x " (fmt/format-number height))
badge-height (/ selection-badge-height zoom)
padding-x (/ selection-badge-padding-x zoom)
gap (/ selection-badge-vertical-gap zoom)
radius (/ selection-badge-border-radius zoom)
text-width (* (count size-label) (/ selection-badge-char-width zoom))
badge-width (+ text-width (* 2 padding-x))
center-x (+ x (/ width 2))
badge-x (- center-x (/ badge-width 2))
badge-y (+ y height gap)
text-y (+ badge-y (/ badge-height 2))]
[:g.selection-size-badge {:pointer-events "none"}
[:rect {:x badge-x
:y badge-y
:width badge-width
:height badge-height
:rx radius
:ry radius
:style {:fill selection-badge-bg-color}}]
[:text {:class (stl/css :badge-text)
:x center-x
:y text-y
:text-anchor "middle"
:dominant-baseline "middle"}
size-label]]))
(mf/defc distance-display* [{:keys [from to zoom bounds]}]
(let [fixed-x (if (gsh/fully-contained? from to)
(+ (:x to) (/ (:width to) 2))
@ -244,6 +281,7 @@
:bounds bounds
:zoom zoom}]
[:> size-display* {:selrect selected-selrect :zoom zoom}]
[:> selection-size-badge* {:selrect selected-selrect :zoom zoom}]
(if (or (not hover-shape) (not hover-selected-shape?))
(when (and frame (not= uuid/zero (:id frame)))

View File

@ -0,0 +1,13 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
.badge-text {
fill: var(--app-black);
font-size: calc(deprecated.$fs-12 / var(--zoom));
font-family: "worksans", "vazirmatn", sans-serif;
}

View File

@ -500,6 +500,14 @@
:zoom zoom
:modifiers modifiers}])
(when (and (seq selected-shapes)
(not transform)
(not text-editing?)
(not edition))
[:> msr/selection-size-badge*
{:selrect (gsh/shapes->rect selected-shapes)
:zoom zoom}])
(when show-measures?
[:> msr/measurement*
{:bounds vbox

View File

@ -653,6 +653,15 @@
{:shape (get base-objects edition)
:zoom zoom}])
(when (and (seq selected-shapes)
(not transform)
(not text-editing?)
(not edition)
(not page-transition?))
[:> msr/selection-size-badge*
{:selrect (gsh/shapes->rect selected-shapes)
:zoom zoom}])
(when show-measures?
[:> msr/measurement*
{:bounds vbox