Compare commits

...

7 Commits

Author SHA1 Message Date
Andrey Antukh
e0910db99e 🐛 Fix incorrect handling of numeric values layout padding and gap
Fixes https://github.com/penpot/penpot/issues/8113
2026-01-21 09:38:44 +01:00
Luis de Dios
079b3fbfad ♻️ Extract and create panel title component (#8090) 2026-01-20 18:56:25 +01:00
Andrey Antukh
299f628951 Merge pull request #8123 from penpot/GlobalStar117-fix/token-validation-crash
🐛 Fix Penpot crash when setting some name in Design tokens
2026-01-20 18:53:05 +01:00
David Barragán Merino
32d0fe6463 🔧 Use selfhosted runner 01 to generate the bundle 2026-01-20 18:09:18 +01:00
Andrey Antukh
cecd3d4a90 📎 Update changelog 2026-01-20 16:00:57 +01:00
Eva Marco
1c2c0987f5 🐛 Fix schema validation for references from other sets 2026-01-20 15:51:43 +01:00
Globalstar117
0418147e74 🐛 Add error handler to token form validation to prevent crash
When creating a token with a name that conflicts with existing
hierarchical token names (e.g., 'accent-color' when 'accent-color.blue.dark'
exists), the validation throws an error via rx/throw. However, the
rx/subs! subscriber in generic_form.cljs had no error handler, causing
an unhandled exception that resulted in an 'Internal Error' crash.

This fix adds an error handler that:
1. Catches validation errors from the reactive stream
2. Uses humanize-errors to convert them to user-friendly messages
3. Displays the error in the form's extra-errors field

Before: Crash with 'Internal Error' dialog
After: Form shows validation error message

Fixes #8110

---
This is a Gittensor contribution.
gittensor:user:GlobalStar117
2026-01-20 15:51:25 +01:00
25 changed files with 193 additions and 157 deletions

View File

@@ -40,7 +40,7 @@ on:
jobs:
build-bundle:
name: Build and Upload Penpot Bundle
runs-on: ubuntu-24.04
runs-on: penpot-runner-01
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

View File

@@ -23,7 +23,9 @@
- Fix wrong image in the onboarding invitation block [Taiga #13040](https://tree.taiga.io/project/penpot/issue/13040)
- Fix wrong register image [Taiga #12955](https://tree.taiga.io/project/penpot/task/12955)
- Fix error message on components doesn't close automatically [Taiga #12012](https://tree.taiga.io/project/penpot/issue/12012)
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
- Fix incorrect default option on tokens import dialog [Github #8051](https://github.com/penpot/penpot/pull/8051)
- Fix unhandled exception tokens creation dialog [Github #8110](https://github.com/penpot/penpot/issues/8110)
## 2.13.0 (Unreleased)

View File

@@ -17,7 +17,6 @@
--app-background: var(--color-background-primary);
--loader-background: var(--color-background-primary);
--panel-title-background-color: var(--color-background-secondary);
// BUTTONS
--button-foreground-hover: var(--color-accent-primary);

View File

@@ -483,6 +483,9 @@
(def workspace-active-theme-paths
(l/derived (d/nilf ctob/get-active-theme-paths) tokens-lib))
(def workspace-all-tokens-map
(l/derived (d/nilf ctob/get-all-tokens-map) tokens-lib))
(defn token-sets-at-path-all-active
[group-path]
(l/derived

View File

@@ -38,6 +38,7 @@
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.ds.product.milestone :refer [milestone*]]
[app.main.ui.ds.product.milestone-group :refer [milestone-group*]]
[app.main.ui.ds.product.panel-title :refer [panel-title*]]
[app.main.ui.ds.storybook :as sb]
[app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
[app.main.ui.ds.utilities.date :refer [date*]]
@@ -81,6 +82,7 @@
:Milestone milestone*
:MilestoneGroup milestone-group*
:Date date*
:PanelTitle panel-title*
:set-default-translations
(fn [data]

View File

@@ -0,0 +1,34 @@
;; 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
(ns app.main.ui.ds.product.panel-title
(:require-macros
[app.main.style :as stl])
(:require
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
(def ^:private schema:panel-title
[:map
[:class {:optional true} :string]
[:text :string]
[:on-close {:optional true} fn?]])
(mf/defc panel-title*
{::mf/schema schema:panel-title}
[{:keys [class text on-close] :rest props}]
(let [props
(mf/spread-props props {:class [class (stl/css :panel-title)]})]
[:> :div props
[:span {:class (stl/css :panel-title-text)} text]
(when on-close
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.close")
:on-click on-close
:icon i/close}])]))

View File

@@ -0,0 +1,26 @@
{ /* 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 */ }
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
import * as PanelTitle from "./panel_title.stories";
<Meta title="Product/PanelTitle" />
# PanelTitle
The `panel-title*` is used as a header for some sidebar sections.
<Canvas of={PanelTitle.Default} />
## Technical notes
The only mandatory parameter is `text`. Usually you'll want to pass a function property `on-close` that will be called when the user clicks on the close button on the right.
```clj
[:> panel-title* {:class class
:text text
:on-close on-close}]
```

View File

@@ -0,0 +1,25 @@
// 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 "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
@use "ds/typography.scss" as t;
.panel-title {
display: flex;
align-items: center;
justify-content: center;
block-size: $sz-32;
border-radius: $br-8;
background-color: var(--color-background-secondary);
}
.panel-title-text {
@include t.use-typography("headline-small");
flex-grow: 1;
text-align: center;
color: var(--color-foreground-primary);
}

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import Components from "@target/components";
const { PanelTitle } = Components;
export default {
title: "Product/PanelTitle",
component: PanelTitle,
argTypes: {
text: {
control: { type: "text" },
},
},
args: {
text: "Lorem ipsum",
onClose: () => null,
},
render: ({ ...args }) => <PanelTitle {...args} />,
};
export const Default = {};

View File

@@ -16,9 +16,9 @@
[app.main.ui.comments :as cmt]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.ds.product.panel-title :refer [panel-title*]]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -121,15 +121,12 @@
(st/emit! (with-meta (dcmt/open-thread thread) {::ev/origin "viewer"}))
(st/emit! (dwcm/navigate-to-comment thread)))))]
[:div {:class (stl/css-case :comments-section true
:from-viewer from-viewer)}
[:div {:class (stl/css-case :comments-section-title true
:viewer-title from-viewer)}
[:span (tr "labels.comments")]
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.close")
:on-click close-section
:icon i/close}]]
[:div {:class (stl/css-case :comments-section true
:from-viewer from-viewer)}
[:> panel-title* {:class (stl/css :comments-title)
:text (tr "labels.comments")
:on-close close-section}]
[:button {:class (stl/css :mode-dropdown-wrapper)
:on-click toggle-mode-selector}

View File

@@ -18,25 +18,8 @@
padding: 0 deprecated.$s-8;
}
.comments-section-title {
@include deprecated.flexCenter;
@include deprecated.uppercaseTitleTipography;
position: relative;
height: deprecated.$s-32;
min-height: deprecated.$s-32;
margin: deprecated.$s-8 deprecated.$s-8 0 deprecated.$s-8;
border-radius: deprecated.$br-8;
background-color: var(--panel-title-background-color);
span {
@include deprecated.flexCenter;
flex-grow: 1;
color: var(--title-foreground-color-hover);
}
}
.viewer-title {
margin: 0;
margin-block-start: deprecated.$s-8;
.comments-title {
margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
}
.mode-dropdown-wrapper {

View File

@@ -11,12 +11,11 @@
[app.common.data.macros :as dm]
[app.main.data.workspace :as dw]
[app.main.store :as st]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.product.panel-title :refer [panel-title*]]
[app.main.ui.icons :as deprecated-icon]
[app.util.debug :as dbg]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc debug-panel*
@@ -35,12 +34,9 @@
(st/emit! (dw/remove-layout-flag :debug-panel))))]
[:div {:class (dm/str class " " (stl/css :debug-panel))}
[:div {:class (stl/css :panel-title)}
[:span "Debugging tools"]
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.close")
:on-click handle-close
:icon i/close}]]
[:> panel-title* {:class (stl/css :debug-panel-title)
:text (tr "workspace.debug.title")
:on-close handle-close}]
[:div {:class (stl/css :debug-panel-inner)}
(for [option (sort-by d/name dbg/options)]

View File

@@ -12,21 +12,12 @@
background-color: var(--panel-background-color);
}
.panel-title {
@include deprecated.flexCenter;
@include deprecated.uppercaseTitleTipography;
position: relative;
height: deprecated.$s-32;
min-height: deprecated.$s-32;
margin: deprecated.$s-8 deprecated.$s-8 0 deprecated.$s-8;
border-radius: deprecated.$br-8;
background-color: var(--panel-title-background-color);
.debug-panel-title {
margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
}
span {
@include deprecated.flexCenter;
flex-grow: 1;
color: var(--title-foreground-color-hover);
}
.debug-panel-inner {
padding: deprecated.$s-16 deprecated.$s-8;
}
.checkbox-wrapper {
@@ -39,7 +30,3 @@
@extend .checkbox-icon;
cursor: pointer;
}
.debug-panel-inner {
padding: deprecated.$s-16 deprecated.$s-8;
}

View File

@@ -13,7 +13,7 @@
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.ds.product.panel-title :refer [panel-title*]]
[debug :as dbg]
[rumext.v2 :as mf]))
@@ -125,11 +125,9 @@
(map (d/getf objects)))]
[:div {:class (stl/css :shape-info)}
[:div {:class (stl/css :shape-info-title)}
[:span "Debug"]
[:div {:class (stl/css :close-button)
:on-click #(dbg/disable! :shape-panel)}
deprecated-icon/close]]
[:> panel-title* {:class (stl/css :shape-info-title)
:text "Debug"
:on-close #(dbg/disable! :shape-panel)}]
(if (empty? selected)
[:div {:class (stl/css :attrs-container)} "No shapes selected"]

View File

@@ -16,34 +16,7 @@
}
.shape-info-title {
@include deprecated.flexCenter;
@include deprecated.uppercaseTitleTipography;
position: relative;
height: deprecated.$s-32;
min-height: deprecated.$s-32;
margin: deprecated.$s-8 deprecated.$s-8 0 deprecated.$s-8;
border-radius: deprecated.$br-8;
background-color: var(--panel-title-background-color);
span {
@include deprecated.flexCenter;
flex-grow: 1;
color: var(--title-foreground-color-hover);
}
}
.close-button {
@extend .button-tertiary;
position: absolute;
right: deprecated.$s-2;
top: deprecated.$s-2;
height: deprecated.$s-28;
width: deprecated.$s-28;
border-radius: deprecated.$br-6;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
}
.attrs-container {

View File

@@ -13,23 +13,6 @@
background-color: var(--panel-background-color);
}
.history-toolbox-title {
@include deprecated.flexCenter;
@include deprecated.uppercaseTitleTipography;
position: relative;
height: deprecated.$s-32;
min-height: deprecated.$s-32;
margin: deprecated.$s-8 deprecated.$s-8 0 deprecated.$s-8;
border-radius: deprecated.$br-8;
background-color: var(--panel-title-background-color);
span {
@include deprecated.flexCenter;
flex-grow: 1;
color: var(--title-foreground-color-hover);
}
}
.history-entry-empty {
display: flex;
flex-direction: column;

View File

@@ -366,7 +366,7 @@
(mf/use-fn
(mf/deps on-change ids)
(fn [value attr event]
(if (or (string? value) (int? value))
(if (or (string? value) (number? value))
(on-change :simple attr value event)
(do
(let [resolved-value (:resolved-value (first value))
@@ -480,7 +480,7 @@
(mf/use-fn
(mf/deps on-change ids)
(fn [value attr event]
(if (or (string? value) (int? value))
(if (or (string? value) (number? value))
(on-change :multiple attr value event)
(do
(let [resolved-value (:resolved-value (first value))]
@@ -713,7 +713,7 @@
(mf/use-fn
(mf/deps on-change wrap-type ids)
(fn [value event attr]
(if (or (string? value) (int? value))
(if (or (string? value) (number? value))
(on-change (= "nowrap" wrap-type) attr value event)
(do
(let [resolved-value (:resolved-value (first value))]

View File

@@ -18,8 +18,8 @@
[app.main.data.workspace.shortcuts]
[app.main.store :as st]
[app.main.ui.components.search-bar :refer [search-bar*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.main.ui.ds.product.panel-title :refer [panel-title*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.strings :refer [matches-search]]
@@ -487,13 +487,9 @@
(dom/focus! (dom/get-element "shortcut-search")))
[:div {:class (dm/str class " " (stl/css :shortcuts))}
[:div {:class (stl/css :shortcuts-header)}
[:div {:class (stl/css :shortcuts-title)} (tr "shortcuts.title")]
[:> icon-button* {:variant "ghost"
:icon i/close
:class (stl/css :shortcuts-close-button)
:on-click close-fn
:aria-label (tr "labels.close")}]]
[:> panel-title* {:class (stl/css :shortcuts-title)
:text (tr "shortcuts.title")
:on-close close-fn}]
[:div {:class (stl/css :search-field)}
[:> search-bar* {:on-change on-search-term-change-2

View File

@@ -18,27 +18,8 @@
margin: deprecated.$s-16 deprecated.$s-12 deprecated.$s-4 deprecated.$s-12;
}
.shortcuts-header {
@include deprecated.flexCenter;
@include deprecated.uppercaseTitleTipography;
position: relative;
height: deprecated.$s-32;
padding: deprecated.$s-2 deprecated.$s-2 deprecated.$s-2 0;
margin: deprecated.$s-4 deprecated.$s-4 0 deprecated.$s-4;
border-radius: deprecated.$br-6;
background-color: var(--panel-title-background-color);
.shortcuts-title {
@include deprecated.flexCenter;
flex-grow: 1;
color: var(--title-foreground-color-hover);
}
.shortcuts-close-button {
position: absolute;
right: 0;
top: 0;
}
.shortcuts-title {
margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
}
.section {

View File

@@ -140,6 +140,9 @@
error
(get-in @form [:errors input-name])
extra-error
(get-in @form [:extra-errors input-name])
value
(get-in @form [:data input-name] "")
@@ -247,9 +250,14 @@
:hint-type (:type hint)})
props
(if (and error touched?)
(cond
(and error touched?)
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
(and extra-error touched?)
(mf/spread-props props {:hint-type "error"
:hint-message (:message extra-error)})
:else
props)]
(mf/with-effect [resolve-stream tokens token input-name]

View File

@@ -23,21 +23,19 @@
(let [token-type
(or (:type token) token-type)
tokens-in-selected-set
(mf/deref refs/workspace-all-tokens-in-selected-set)
token-path
(mf/with-memo [token]
(cft/token-name->path (:name token)))
tokens-tree-in-selected-set
(mf/with-memo [token-path tokens-in-selected-set]
(-> (ctob/tokens-tree tokens-in-selected-set)
all-tokens (mf/deref refs/workspace-all-tokens-map)
all-tokens
(mf/with-memo [token-path all-tokens]
(-> (ctob/tokens-tree all-tokens)
(d/dissoc-in token-path)))
props
(mf/spread-props props {:token-type token-type
:tokens-tree-in-selected-set tokens-tree-in-selected-set
:tokens-in-selected-set tokens-in-selected-set
:all-token-tree all-tokens
:token token})
text-case-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")})
text-decoration-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")})

View File

@@ -16,6 +16,7 @@
[app.main.data.helpers :as dh]
[app.main.data.modal :as modal]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.tokens.propagation :as dwtp]
[app.main.data.workspace.tokens.remapping :as remap]
@@ -88,14 +89,13 @@
action
is-create
selected-token-set-id
tokens-tree-in-selected-set
all-token-tree
token-type
make-schema
input-component
initial
type
value-subfield
tokens-in-selected-set
input-value-placeholder] :as props}]
(let [make-schema (or make-schema default-make-schema)
@@ -124,6 +124,9 @@
tokens
(mf/deref refs/workspace-active-theme-sets-tokens)
tokens-in-selected-set
(mf/deref refs/workspace-all-tokens-in-selected-set)
tokens
(mf/with-memo [tokens tokens-in-selected-set token]
;; Ensure that the resolved value uses the currently editing token
@@ -134,8 +137,8 @@
(assoc (:name token) token)))
schema
(mf/with-memo [tokens-tree-in-selected-set active-tab]
(make-schema tokens-tree-in-selected-set active-tab))
(mf/with-memo [all-token-tree active-tab]
(make-schema all-token-tree active-tab))
initial
(mf/with-memo [token]
@@ -224,7 +227,12 @@
:description description}))
(dwtl/toggle-token-path path)
(dwtp/propagate-workspace-tokens)
(modal/hide!))))))))))]
(modal/hide!)))))
;; WORKAROUND: display validation errors in the form instead of crashing
(fn [{:keys [errors]}]
(let [error-messages (wte/humanize-errors errors)
error-message (first error-messages)]
(swap! form assoc-in [:extra-errors :value] {:message error-message}))))))))]
[:> fc/form* {:class (stl/css :form-wrapper)
:form form

View File

@@ -228,7 +228,7 @@
:class (stl/css :main-toolbar-options-button)
:icon i/bug
:aria-pressed (contains? layout :debug-panel)
:aria-label "Debugging tool"
:aria-label (tr "workspace.toolbar.debug")
:tooltip-placement "bottom"
:on-click toggle-debug-panel}]])]]

View File

@@ -5476,6 +5476,10 @@ msgstr "Delete row and shapes"
msgid "workspace.context-menu.grid-track.row.duplicate"
msgstr "Duplicate row"
#: src/app/main/ui/workspace/sidebar/debug.cljs:37
msgid "workspace.debug.title"
msgstr "Debugging tools"
#: src/app/main/ui/workspace/sidebar/layers.cljs:512
msgid "workspace.focus.focus-mode"
msgstr "Focus mode"
@@ -8421,6 +8425,10 @@ msgstr "Comments (%s)"
msgid "workspace.toolbar.curve"
msgstr "Curve (%s)"
#: src/app/main/ui/workspace/top_toolbar.cljs:231
msgid "workspace.toolbar.debug"
msgstr "Debugging tools"
#: src/app/main/ui/workspace/top_toolbar.cljs:172
msgid "workspace.toolbar.ellipse"
msgstr "Ellipse (%s)"

View File

@@ -5461,6 +5461,10 @@ msgstr "Borrar fila con el contenido"
msgid "workspace.context-menu.grid-track.row.duplicate"
msgstr "Duplicar fila"
#: src/app/main/ui/workspace/sidebar/debug.cljs:37
msgid "workspace.debug.title"
msgstr "Herramientas de depuración"
#: src/app/main/ui/workspace/sidebar/layers.cljs:512
msgid "workspace.focus.focus-mode"
msgstr "Modo foco"
@@ -7965,7 +7969,7 @@ msgstr "Line height (multiplicador, px o %) o {alias}"
#: src/app/main/data/workspace/tokens/errors.cljs:57
msgid "workspace.tokens.missing-references"
msgstr "Referéncias de tokens no encontradas:"
msgstr "Referencias de tokens no encontradas: "
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
msgid "workspace.tokens.more-options"
@@ -8282,6 +8286,10 @@ msgstr "Comentarios (%s)"
msgid "workspace.toolbar.curve"
msgstr "Curva (%s)"
#: src/app/main/ui/workspace/top_toolbar.cljs:231
msgid "workspace.toolbar.debug"
msgstr "Herramientas de depuración"
#: src/app/main/ui/workspace/top_toolbar.cljs:172
msgid "workspace.toolbar.ellipse"
msgstr "Elipse (%s)"