From de5ff227d29e12827ed1a736aaae1410326a0be9 Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Tue, 30 Sep 2025 18:15:17 +0200 Subject: [PATCH] :tada: Create variant from the viewport (#7357) * :tada: Create variant from the viewport * :recycle: Use DS styles and new component syntax * :paperclip: PR changes --- CHANGES.md | 1 + .../styles/common/refactor/design-tokens.scss | 5 - frontend/src/app/main/ui/ds/colors.scss | 11 ++ .../src/app/main/ui/workspace/viewport.cljs | 41 ++++-- .../main/ui/workspace/viewport/widgets.cljs | 138 +++++++++++------- .../main/ui/workspace/viewport/widgets.scss | 113 +++++++------- .../app/main/ui/workspace/viewport_wasm.cljs | 18 +-- 7 files changed, 184 insertions(+), 143 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a7ba906908..a7cb4695ac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/frontend/resources/styles/common/refactor/design-tokens.scss b/frontend/resources/styles/common/refactor/design-tokens.scss index 54ccc918ca..3caa88cdd5 100644 --- a/frontend/resources/styles/common/refactor/design-tokens.scss +++ b/frontend/resources/styles/common/refactor/design-tokens.scss @@ -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); diff --git a/frontend/src/app/main/ui/ds/colors.scss b/frontend/src/app/main/ui/ds/colors.scss index d3f01a8a16..aa3d93bce6 100644 --- a/frontend/src/app/main/ui/ds/colors.scss +++ b/frontend/src/app/main/ui/ds/colors.scss @@ -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}; diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index f602191ac0..1837f32089 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index b017e7bd78..e494af80ef 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -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"}]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.scss b/frontend/src/app/main/ui/workspace/viewport/widgets.scss index d14cf482e4..180c3f6433 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.scss +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.scss @@ -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); } diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 3ebfd80860..67e4a1d9cc 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -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