🎉 Create variant from the viewport (#7357)

* 🎉 Create variant from the viewport

* ♻️ Use DS styles and new component syntax

* 📎 PR changes
This commit is contained in:
Luis de Dios
2025-09-30 18:15:17 +02:00
committed by GitHub
parent 0f67730198
commit de5ff227d2
7 changed files with 184 additions and 143 deletions

View File

@@ -34,6 +34,7 @@
- Show current Penpot version [Taiga #11603](https://tree.taiga.io/project/penpot/us/11603)
- Switch several variant copies at the same time [Taiga #11411](https://tree.taiga.io/project/penpot/us/11411)
- Invitations management improvements [Taiga #3479](https://tree.taiga.io/project/penpot/us/3479)
- Alternative ways of creating variants - Button Viewport [Taiga #11931](https://tree.taiga.io/project/penpot/us/11931)
### :bug: Bugs fixed

View File

@@ -384,11 +384,6 @@
--dashboard-list-foreground-color: var(--color-foreground-primary);
--dashboard-list-text-foreground-color: var(--color-foreground-secondary);
--flow-tag-background-color: var(--color-background-tertiary);
--flow-tag-foreground-color: var(--color-foreground-secondary);
--flow-tag-background-color-hover: var(--color-background-quaternary);
--flow-tag-foreground-color-hover: var(--color-accent-primary);
--communication-tag-background-color: var(--color-foreground-primary);
--communication-tag-foreground-color: var(--color-background-tertiary);

View File

@@ -29,6 +29,7 @@ $pink-400: #ff6fe0;
$purple-200: #e1d2f5;
$purple-400: #bb97d8;
$purple-500: #a977d1;
$purple-600: #8c33eb;
$purple-700: #6911d4;
$purple-600-10: #8c33eb1a;
@@ -76,6 +77,8 @@ $grayish-red: #bfbfbf;
--color-accent-quaternary: #{$pink-400};
--color-accent-overlay: #{$purple-700-60};
--color-accent-select: #{$purple-600-10};
--color-accent-action: #{$purple-400};
--color-accent-action-hover: #{$purple-500};
--color-accent-success: #{$green-500};
--color-background-success: #{$green-200};
@@ -98,6 +101,9 @@ $grayish-red: #bfbfbf;
--color-foreground-primary: #{$black};
--color-foreground-secondary: #{$blue-teal-700};
--color-static-white: #{$white};
--color-static-black: #{$black};
--color-shadow-dark: #{color.change($gray-200, $alpha: 0.6)};
--color-overlay-default: #{$white-60};
--color-overlay-onboarding: #{$white-90};
@@ -119,6 +125,8 @@ $grayish-red: #bfbfbf;
--color-accent-quaternary: #{$pink-400};
--color-accent-overlay: #{$mint-150-60};
--color-accent-select: #{$mint-250-10};
--color-accent-action: #{$purple-400};
--color-accent-action-hover: #{$purple-500};
--color-accent-success: #{$green-500};
--color-background-success: #{$green-950};
@@ -141,6 +149,9 @@ $grayish-red: #bfbfbf;
--color-foreground-primary: #{$white};
--color-foreground-secondary: #{$grayish-blue-500};
--color-static-white: #{$white};
--color-static-black: #{$black};
--color-shadow-dark: #{color.change($black, $alpha: 0.6)};
--color-overlay-default: #{$gray-950-60};
--color-overlay-onboarding: #{$gray-950-90};

View File

@@ -12,11 +12,13 @@
[app.common.files.helpers :as cfh]
[app.common.geom.shapes :as gsh]
[app.common.types.color :as clr]
[app.common.types.component :as ctk]
[app.common.types.path :as path]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctt]
[app.common.types.shape.layout :as ctl]
[app.main.data.workspace.modifiers :as dwm]
[app.main.data.workspace.variants :as dwv]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -51,7 +53,7 @@
[app.main.ui.workspace.viewport.selection :as selection]
[app.main.ui.workspace.viewport.snap-distances :as snap-distances]
[app.main.ui.workspace.viewport.snap-points :as snap-points]
[app.main.ui.workspace.viewport.top-bar :refer [path-edition-bar* grid-edition-bar* view-only-bar*]]
[app.main.ui.workspace.viewport.top-bar :refer [grid-edition-bar* path-edition-bar* view-only-bar*]]
[app.main.ui.workspace.viewport.utils :as utils]
[app.main.ui.workspace.viewport.viewport-ref :refer [create-viewport-ref]]
[app.main.ui.workspace.viewport.widgets :as widgets]
@@ -261,7 +263,11 @@
single-select? (= (count selected-shapes) 1)
first-shape (first selected-shapes)
first-shape (first selected-shapes)
show-add-variant? (and single-select?
(or (ctk/is-variant-container? first-shape)
(ctk/is-variant? first-shape)))
show-padding?
(and (nil? transform)
@@ -288,7 +294,13 @@
(:y first-shape)
(:y selected-frame))
rule-area-size (/ rulers/ruler-area-size zoom)]
rule-area-size (/ rulers/ruler-area-size zoom)
add-variant
(mf/use-fn
(mf/deps first-shape)
#(st/emit!
(dwv/add-new-variant (:id first-shape))))]
(hooks/setup-dom-events zoom disable-paste in-viewport? read-only? drawing-tool path-drawing?)
(hooks/setup-viewport-size vport viewport-ref)
@@ -536,11 +548,11 @@
:alt? @alt?
:shift? @shift?}])
[:& widgets/frame-titles
[:> widgets/frame-titles*
{:objects base-objects
:selected selected
:zoom zoom
:show-artboard-names? show-artboard-names?
:is-show-artboard-names show-artboard-names?
:on-frame-enter on-frame-enter
:on-frame-leave on-frame-leave
:on-frame-select on-frame-select
@@ -571,9 +583,8 @@
:focus focus}])
(when show-pixel-grid?
[:& widgets/pixel-grid
{:vbox vbox
:zoom zoom}])
[:> widgets/pixel-grid* {:vbox vbox
:zoom zoom}])
(when show-snap-points?
[:& snap-points/snap-points
@@ -596,13 +607,17 @@
:page-id page-id}])
(when show-cursor-tooltip?
[:& widgets/cursor-tooltip
{:zoom zoom
:tooltip tooltip}])
[:> widgets/cursor-tooltip* {:zoom zoom
:tooltip tooltip}])
(when show-selrect?
[:& widgets/selection-rect {:data selrect
:zoom zoom}])
[:> widgets/selection-rect* {:data selrect
:zoom zoom}])
(when show-add-variant?
[:> widgets/button-add* {:shape first-shape
:zoom zoom
:on-click add-variant}])
(when show-presence?
[:& presence/active-cursors

View File

@@ -22,8 +22,8 @@
[app.main.store :as st]
[app.main.streams :as ms]
[app.main.ui.context :as ctx]
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.viewport.utils :as vwu]
[app.util.debug :as dbg]
[app.util.dom :as dom]
@@ -32,7 +32,7 @@
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(mf/defc pixel-grid
(mf/defc pixel-grid*
[{:keys [vbox zoom]}]
[:g.pixel-grid
[:defs
@@ -53,8 +53,8 @@
:fill (str "url(#pixel-grid)")
:style {:pointer-events "none"}}]])
(mf/defc cursor-tooltip
[{:keys [zoom tooltip] :as props}]
(mf/defc cursor-tooltip*
[{:keys [zoom tooltip]}]
(let [coords (some-> (hooks/use-rxsub ms/mouse-position)
(gpt/divide (gpt/point zoom zoom)))
pos-x (- (:x coords) 100)
@@ -63,9 +63,9 @@
[:foreignObject {:width 200 :height 100 :style {:text-align "center"}}
[:span tooltip]]]))
(mf/defc selection-rect
(mf/defc selection-rect*
{:wrap [mf/memo]}
[{:keys [data zoom] :as props}]
[{:keys [data zoom]}]
(when data
[:rect.selection-rect
{:x (:x data)
@@ -83,14 +83,14 @@
{::mf/wrap [mf/memo
#(mf/deferred % ts/raf)]
::mf/forward-ref true}
[{:keys [frame selected? zoom show-artboard-names? show-id? on-frame-enter
on-frame-leave on-frame-select grid-edition?]} external-ref]
[{:keys [frame zoom is-selected is-show-artboard-names is-show-id is-grid-edition
on-frame-enter on-frame-leave on-frame-select]} external-ref]
(let [workspace-read-only? (mf/use-ctx ctx/workspace-read-only?)
;; Note that we don't use mf/deref to avoid a repaint dependency here
objects (deref refs/workspace-page-objects)
color (if selected?
color (if is-selected
(if (or (ctn/in-any-component? objects frame) (ctk/is-variant-container? frame))
"var(--assets-component-hightlight)"
"var(--color-accent-tertiary)")
@@ -134,7 +134,7 @@
is-variant? (:is-variant-container frame)
text-width (* (:width frame) zoom)
show-icon? (and (or (:use-for-thumbnail frame) grid-edition? main-instance? is-variant?)
show-icon? (and (or (:use-for-thumbnail frame) is-grid-edition main-instance? is-variant?)
(not (<= text-width 15)))
text-pos-x (if show-icon? 15 0)
@@ -182,8 +182,8 @@
(when (not (:hidden frame))
[:g.frame-title {:id (dm/str "frame-title-" (:id frame))
:data-edit-grid grid-edition?
:transform (vwu/title-transform frame zoom grid-edition?)
:data-edit-grid is-grid-edition
:transform (vwu/title-transform frame zoom is-grid-edition)
:pointer-events (when (:blocked frame) "none")}
(when show-icon?
[:svg {:x 0
@@ -193,12 +193,12 @@
:class "workspace-frame-icon"
:style {:stroke color
:fill "none"}
:visibility (if show-artboard-names? "visible" "hidden")}
:visibility (if is-show-artboard-names "visible" "hidden")}
(cond
(:use-for-thumbnail frame) [:use {:href "#icon-boards-thumbnail"}]
grid-edition? [:use {:href "#icon-grid"}]
main-instance? [:use {:href "#icon-component"}]
is-variant? [:use {:href "#icon-component"}])])
is-grid-edition [:use {:href "#icon-grid"}]
main-instance? [:use {:href "#icon-component"}]
is-variant? [:use {:href "#icon-component"}])])
(if ^boolean edition?
;; Case when edition? is true
@@ -206,12 +206,12 @@
:y -15
:width (max 0 (- text-width text-pos-x))
:height 22
:class (stl/css :workspace-frame-label-wrapper)
:class (stl/css :frame-title-wrapper)
:style {:fill color}
:visibility (if show-artboard-names? "visible" "hidden")}
:visibility (if is-show-artboard-names "visible" "hidden")}
[:input {:type "text"
:class (stl/css :workspace-frame-label
:element-name-input)
:class (stl/css :frame-title-label
:frame-title-input)
:style {:color color}
:auto-focus true
:on-key-down on-key-down
@@ -223,10 +223,10 @@
:y -11
:width (max 0 (- text-width text-pos-x))
:height 20
:class (stl/css :workspace-frame-label-wrapper)
:class (stl/css :frame-title-wrapper)
:style {:fill color}
:visibility (if show-artboard-names? "visible" "hidden")}
[:div {:class (stl/css :workspace-frame-label)
:visibility (if is-show-artboard-names "visible" "hidden")}
[:div {:class (stl/css :frame-title-label)
:style {:color color}
:ref ref
:on-pointer-down on-pointer-down
@@ -234,31 +234,24 @@
:on-context-menu on-context-menu
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave}
(if show-id?
(if is-show-id
(dm/str (:id frame) " - " (:name frame))
(:name frame))]])])))
(mf/defc frame-titles
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[props]
(let [objects (unchecked-get props "objects")
zoom (unchecked-get props "zoom")
selected (or (unchecked-get props "selected") #{})
show-artboard-names? (unchecked-get props "show-artboard-names?")
on-frame-enter (unchecked-get props "on-frame-enter")
on-frame-leave (unchecked-get props "on-frame-leave")
on-frame-select (unchecked-get props "on-frame-select")
shapes (ctt/get-frames objects {:skip-copies? true})
shapes (if (dbg/enabled? :shape-titles)
(into (set shapes)
(map (d/getf objects))
selected)
shapes)
focus (unchecked-get props "focus")
(mf/defc frame-titles*
{::mf/wrap [mf/memo]}
[{:keys [objects zoom selected focus is-show-artboard-names
on-frame-enter on-frame-leave on-frame-select]}]
(let [selected (or selected #{})
shapes (ctt/get-frames objects {:skip-copies? true})
shapes (if (dbg/enabled? :shape-titles)
(into (set shapes)
(map (d/getf objects))
selected)
shapes)
edition (mf/deref refs/selected-edition)
grid-edition? (ctl/grid-layout? objects edition)]
edition (mf/deref refs/selected-edition)
grid-edition? (ctl/grid-layout? objects edition)]
[:g.frame-titles
(for [{:keys [id parent-id] :as shape} shapes]
@@ -268,17 +261,16 @@
(or (empty? focus) (contains? focus id)))
[:& frame-title {:key (dm/str "frame-title-" id)
:frame shape
:selected? (contains? selected id)
:zoom zoom
:show-artboard-names? show-artboard-names?
:show-id? (dbg/enabled? :shape-titles)
:is-selected (contains? selected id)
:is-show-artboard-names is-show-artboard-names
:is-show-id (dbg/enabled? :shape-titles)
:is-grid-edition (and (= id edition) grid-edition?)
:on-frame-enter on-frame-enter
:on-frame-leave on-frame-leave
:on-frame-select on-frame-select
:grid-edition? (and (= id edition) grid-edition?)}]))]))
:on-frame-select on-frame-select}]))]))
(mf/defc frame-flow*
{::mf/props :obj}
[{:keys [flow frame is-selected zoom on-frame-enter on-frame-leave on-frame-select]}]
(let [x (dm/get-prop frame :x)
y (dm/get-prop frame :y)
@@ -323,18 +315,18 @@
:width 100000
:height 24
:transform (vwu/text-transform pos zoom)}
[:div {:class (stl/css-case :flow-badge true
:selected is-selected)}
[:div {:class (stl/css :content)
[:div {:class (stl/css :frame-flow-badge-wrapper)}
[:div {:class (stl/css-case :frame-flow-badge-content true
:selected is-selected)
:on-pointer-down on-pointer-down
:on-double-click on-double-click
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave}
deprecated-icon/play
[:> icon* {:icon-id i/play
:size "s"}]
[:span flow-name]]]]))
(mf/defc frame-flows*
{::mf/props :obj}
[{:keys [flows objects zoom selected on-frame-enter on-frame-leave on-frame-select]}]
[:g.frame-flows
(for [[flow-id flow] flows]
@@ -349,3 +341,39 @@
:on-frame-leave on-frame-leave
:on-frame-select on-frame-select}]))])
(mf/defc button-add*
[{:keys [shape zoom on-click]}]
(let [{:keys [x2 y2 height]} (:selrect shape)
center-x (+ x2 (/ 22 zoom))
center-y (- y2 (/ height 2))
rect-x (- center-x (/ 16 zoom))
rect-y (- center-y (/ 16 zoom))
rect-sz (/ 32 zoom)
rect-r (/ 8 zoom)
icon-x (- center-x (/ 8 zoom))
icon-y (- center-y (/ 8 zoom))
icon-sz (/ 16 zoom)
handle-click
(mf/use-fn
(mf/deps on-click)
#(when (fn? on-click)
(on-click)))]
[:g {:class (stl/css :button-add-wrapper)
:on-click handle-click}
[:rect {:x rect-x
:y rect-y
:width rect-sz
:height rect-sz
:rx rect-r
:ry rect-r}]
[:use {:class (stl/css :button-add-icon)
:x icon-x
:y icon-y
:width icon-sz
:height icon-sz
:href "#icon-add"}]]))

View File

@@ -4,69 +4,49 @@
//
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/colors.scss" as *;
@use "ds/typography.scss" as t;
.flow-element {
display: flex;
align-items: center;
.frame-flow-badge-wrapper {
--frame-flow-badge-background-color: var(--color-background-tertiary);
--frame-flow-badge-background-color-hover: var(--color-background-quaternary);
--frame-flow-badge-foreground-color: var(--color-foreground-secondary);
--frame-flow-badge-foreground-color-hover: var(--color-accent-primary);
.flow-name {
cursor: pointer;
}
& input.element-name {
background: transparent;
}
}
.flow-badge {
cursor: pointer;
display: flex;
.content {
@include deprecated.bodySmallTypography;
display: flex;
align-items: center;
height: deprecated.$s-24;
border-radius: deprecated.$br-6;
background-color: var(--flow-tag-background-color);
svg {
@extend .button-icon;
height: deprecated.$s-24;
width: deprecated.$s-12;
stroke: var(--icon-foreground);
margin: 0 deprecated.$s-8;
}
}
span {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-right: deprecated.$s-8;
color: var(--flow-tag-foreground-color);
}
}
.frame-flow-badge-content {
@include t.use-typography("body-small");
display: flex;
align-items: center;
justify-content: center;
gap: var(--sp-xs);
border-radius: $br-6;
padding-inline-start: var(--sp-xs);
padding-inline-end: var(--sp-s);
height: var(--sp-xxl);
background-color: var(--frame-flow-badge-background-color);
color: var(--frame-flow-badge-foreground-color);
&.selected .content,
&:hover .content {
background-color: var(--flow-tag-background-color-hover);
svg {
stroke: var(--flow-tag-foreground-color-hover);
}
span {
color: var(--flow-tag-foreground-color-hover);
}
&:hover,
&.selected {
--frame-flow-badge-foreground-color: var(--frame-flow-badge-foreground-color-hover);
--frame-flow-badge-background-color: var(--frame-flow-badge-background-color-hover);
}
}
.workspace-frame-label-wrapper {
.frame-title-wrapper {
--frame-title-input-border-color-focus: var(--color-accent-primary);
pointer-events: none;
}
.workspace-frame-label {
font-size: deprecated.$fs-12;
color: black;
.frame-title-label {
@include t.use-typography("body-small");
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
@@ -74,16 +54,29 @@
pointer-events: all;
}
.element-name-input {
@include deprecated.removeInputStyle;
@include deprecated.bodySmallTypography;
@include deprecated.removeInputStyle;
.frame-title-input {
@include t.use-typography("body-small");
flex-grow: 1;
height: 20px;
margin: 0;
padding-left: deprecated.$s-6;
border: deprecated.$s-1 solid var(--input-border-color-focus);
color: var(--layer-row-foreground-color);
width: 100%;
max-width: initial;
background: none;
outline: none;
height: var(--sp-xl);
padding-inline-start: var(--sp-s);
border: $b-1 solid var(--frame-title-input-border-color-focus);
}
.button-add-wrapper {
--button-add-background-color: var(--color-accent-action);
--button-add-background-color-hover: var(--color-accent-action-hover);
--button-add-icon-color: var(--color-static-white);
fill: var(--button-add-background-color);
&:hover {
--button-add-background-color: var(--button-add-background-color-hover);
}
}
.button-add-icon {
stroke: var(--button-add-icon-color);
}

View File

@@ -516,11 +516,11 @@
:alt? @alt?
:shift? @shift?}])
[:& widgets/frame-titles
[:> widgets/frame-titles*
{:objects (with-meta objects-modified nil)
:selected selected
:zoom zoom
:show-artboard-names? show-artboard-names?
:is-show-artboard-names show-artboard-names?
:on-frame-enter on-frame-enter
:on-frame-leave on-frame-leave
:on-frame-select on-frame-select
@@ -551,9 +551,8 @@
:focus focus}])
(when show-pixel-grid?
[:& widgets/pixel-grid
{:vbox vbox
:zoom zoom}])
[:> widgets/pixel-grid* {:vbox vbox
:zoom zoom}])
(when show-snap-points?
[:& snap-points/snap-points
@@ -576,13 +575,12 @@
:page-id page-id}])
(when show-cursor-tooltip?
[:& widgets/cursor-tooltip
{:zoom zoom
:tooltip tooltip}])
[:> widgets/cursor-tooltip* {:zoom zoom
:tooltip tooltip}])
(when show-selrect?
[:& widgets/selection-rect {:data selrect
:zoom zoom}])
[:> widgets/selection-rect* {:data selrect
:zoom zoom}])
(when show-presence?
[:& presence/active-cursors