From 326822594103dd79712d9f1875d6c0c9ead7bc6c Mon Sep 17 00:00:00 2001
From: Pablo Alba <pablo.alba@kaleidos.net>
Date: Mon, 17 Feb 2025 16:47:25 +0100
Subject: [PATCH] :tada: Add variations POC

---
 backend/src/app/binfile/common.clj            |   5 +
 common/src/app/common/features.cljc           |   4 +-
 .../src/app/common/files/changes_builder.cljc |  57 ++--
 common/src/app/common/logic/libraries.cljc    |   5 +-
 common/src/app/common/logic/variants.cljc     |  62 +++++
 common/src/app/common/types/component.cljc    |  13 +
 .../src/app/common/types/components_list.cljc |  18 +-
 common/src/app/common/types/container.cljc    |   2 +-
 .../logic/comp_creation_test.cljc             |   2 +
 frontend/resources/images/icons/variant.svg   |   3 +
 .../app/main/data/workspace/libraries.cljs    |  24 +-
 .../src/app/main/data/workspace/variants.cljs | 197 ++++++++++++++
 .../app/main/ui/components/shape_icon.cljs    |   7 +-
 .../ds/controls/shared/options_dropdown.scss  |   1 +
 frontend/src/app/main/ui/icons.cljs           |   1 +
 .../ui/workspace/sidebar/assets/common.cljs   |  13 +-
 .../main/ui/workspace/sidebar/layer_item.cljs |   7 +-
 .../main/ui/workspace/sidebar/layer_name.cljs |   4 +-
 .../sidebar/options/menus/component.cljs      | 251 ++++++++++++++++--
 .../sidebar/options/menus/component.scss      |  71 ++++-
 .../sidebar/options/shapes/frame.cljs         | 101 +++----
 .../main/ui/workspace/viewport/widgets.cljs   |   8 +-
 frontend/translations/en.po                   |   9 +
 frontend/translations/es.po                   |  10 +
 24 files changed, 751 insertions(+), 124 deletions(-)
 create mode 100644 common/src/app/common/logic/variants.cljc
 create mode 100644 frontend/resources/images/icons/variant.svg
 create mode 100644 frontend/src/app/main/data/workspace/variants.cljs

diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj
index a0b256660..c068128a8 100644
--- a/backend/src/app/binfile/common.clj
+++ b/backend/src/app/binfile/common.clj
@@ -556,6 +556,11 @@
       "fdata/shape-data-type"
       nil
 
+      ;; There is no migration needed, but we don't want to allow
+      ;; copy paste nor import of variant files into no-variant teams
+      "variants/v1"
+      nil
+
       (ex/raise :type :internal
                 :code :no-migration-defined
                 :hint (str/ffmt "no migation for feature '%' on file importation" feature)
diff --git a/common/src/app/common/features.cljc b/common/src/app/common/features.cljc
index 0ced5b1d8..fbb7d2785 100644
--- a/common/src/app/common/features.cljc
+++ b/common/src/app/common/features.cljc
@@ -52,7 +52,8 @@
     "plugins/runtime"
     "design-tokens/v1"
     "text-editor/v2"
-    "render-wasm/v1"})
+    "render-wasm/v1"
+    "variants/v1"})
 
 ;; A set of features enabled by default
 (def default-features
@@ -111,6 +112,7 @@
     :feature-design-tokens "design-tokens/v1"
     :feature-text-editor-v2 "text-editor/v2"
     :feature-render-wasm "render-wasm/v1"
+    :feature-variants "variants/v1"
     nil))
 
 (defn migrate-legacy-features
diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc
index 2fb307e92..95a781090 100644
--- a/common/src/app/common/files/changes_builder.cljc
+++ b/common/src/app/common/files/changes_builder.cljc
@@ -861,7 +861,6 @@
 
 (defn move-token-set-group-before
   [changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?]}]
-  (prn prev-before-path prev-before-group?)
   (-> changes
       (update :redo-changes conj {:type :move-token-set-group-before
                                   :from-path from-path
@@ -971,31 +970,37 @@
          (apply-changes-local)))))
 
 (defn update-component
-  [changes id update-fn]
-  (assert-library! changes)
-  (let [library-data   (::library-data (meta changes))
-        prev-component (get-in library-data [:components id])
-        new-component  (update-fn prev-component)]
-    (if prev-component
-      (-> changes
-          (update :redo-changes conj {:type :mod-component
-                                      :id id
-                                      :name (:name new-component)
-                                      :path (:path new-component)
-                                      :main-instance-id (:main-instance-id new-component)
-                                      :main-instance-page (:main-instance-page new-component)
-                                      :annotation (:annotation new-component)
-                                      :objects (:objects new-component) ;; this won't exist in components-v2 (except for deleted components)
-                                      :modified-at (:modified-at new-component)})
-          (update :undo-changes conj {:type :mod-component
-                                      :id id
-                                      :name (:name prev-component)
-                                      :path (:path prev-component)
-                                      :main-instance-id (:main-instance-id prev-component)
-                                      :main-instance-page (:main-instance-page prev-component)
-                                      :annotation (:annotation prev-component)
-                                      :objects (:objects prev-component)}))
-      changes)))
+  ([changes id update-fn]
+   (let [library-data   (::library-data (meta changes))
+         prev-component (get-in library-data [:components id])]
+     (update-component changes id prev-component update-fn)))
+  ([changes id prev-component update-fn]
+   (assert-library! changes)
+   (let [new-component  (update-fn prev-component)]
+     (if prev-component
+       (-> changes
+           (update :redo-changes conj {:type :mod-component
+                                       :id id
+                                       :name (:name new-component)
+                                       :path (:path new-component)
+                                       :main-instance-id (:main-instance-id new-component)
+                                       :main-instance-page (:main-instance-page new-component)
+                                       :annotation (:annotation new-component)
+                                       :variant-id (:variant-id new-component)
+                                       :variant-properties (:variant-properties new-component)
+                                       :objects (:objects new-component) ;; this won't exist in components-v2 (except for deleted components)
+                                       :modified-at (:modified-at new-component)})
+           (update :undo-changes conj {:type :mod-component
+                                       :id id
+                                       :name (:name prev-component)
+                                       :path (:path prev-component)
+                                       :main-instance-id (:main-instance-id prev-component)
+                                       :main-instance-page (:main-instance-page prev-component)
+                                       :annotation (:annotation prev-component)
+                                       :variant-id (:variant-id prev-component)
+                                       :variant-properties (:variant-properties prev-component)
+                                       :objects (:objects prev-component)}))
+       changes))))
 
 (defn delete-component
   [changes id page-id]
diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc
index a090fe690..efb0ba3fb 100644
--- a/common/src/app/common/logic/libraries.cljc
+++ b/common/src/app/common/logic/libraries.cljc
@@ -152,7 +152,7 @@
 
 (defn generate-duplicate-component
   "Create a new component copied from the one with the given id."
-  [changes library component-id components-v2]
+  [changes library component-id new-component-id components-v2]
   (let [component          (ctkl/get-component (:data library) component-id)
         new-name           (:name component)
 
@@ -160,7 +160,7 @@
                              (ctf/get-component-page (:data library) component))
 
         new-component-id   (when components-v2
-                             (uuid/next))
+                             new-component-id)
 
         [new-component-shape new-component-shapes  ; <- null in components-v2
          new-main-instance-shape new-main-instance-shapes]
@@ -181,6 +181,7 @@
                            (:id main-instance-page)
                            (:annotation component)))))
 
+
 (defn generate-instantiate-component
   "Generate changes to create a new instance from a component."
   ([changes objects file-id component-id position page libraries]
diff --git a/common/src/app/common/logic/variants.cljc b/common/src/app/common/logic/variants.cljc
new file mode 100644
index 000000000..5c07a90aa
--- /dev/null
+++ b/common/src/app/common/logic/variants.cljc
@@ -0,0 +1,62 @@
+;; 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.common.logic.variants
+  (:require
+   [app.common.files.changes-builder :as pcb]
+   [cuerdas.core :as str]))
+
+
+(defn properties-to-name
+  [properties]
+  (->> properties
+       (map :value)
+       (str/join ", ")))
+
+(defn generate-update-property-name
+  [changes related-components pos new-name]
+  (reduce (fn [changes component]
+            (pcb/update-component
+             changes (:id component)
+             #(assoc-in % [:variant-properties pos :name] new-name)))
+          changes
+          related-components))
+
+
+(defn generate-remove-property
+  [changes related-components pos]
+  (reduce (fn [changes component]
+            (let [props   (:variant-properties component)
+                  props   (vec (concat (subvec props 0 pos) (subvec props (inc pos))))
+                  main-id (:main-instance-id component)
+                  name    (properties-to-name props)]
+              (-> changes
+                  (pcb/update-component (:id component) #(assoc % :variant-properties props))
+                  (pcb/update-shapes [main-id] #(assoc % :variant-name name)))))
+          changes
+          related-components))
+
+
+(defn generate-update-property-value
+  [changes component-id main-id pos value name]
+  (-> changes
+      (pcb/update-component component-id #(assoc-in % [:variant-properties pos :value] value))
+      (pcb/update-shapes [main-id] #(assoc % :variant-name name))))
+
+(defn generate-add-new-property
+  [changes related-components property-name]
+  (let [[_ changes]
+        (reduce (fn [[num changes] component]
+                  (let [props        (-> (or (:variant-properties component) [])
+                                         (conj {:name property-name :value (str "Value" num)}))
+                        main-id      (:main-instance-id component)
+                        variant-name (properties-to-name props)]
+                    [(inc num)
+                     (-> changes
+                         (pcb/update-component (:id component) #(assoc % :variant-properties props))
+                         (pcb/update-shapes [main-id] #(assoc % :variant-name variant-name)))]))
+                [1 changes]
+                related-components)]
+    changes))
diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc
index b26461f2b..aba6e673a 100644
--- a/common/src/app/common/types/component.cljc
+++ b/common/src/app/common/types/component.cljc
@@ -215,6 +215,19 @@
   (and (= shape-id (:main-instance-id component))
        (= page-id (:main-instance-page component))))
 
+
+(defn is-variant?
+  "Check if this shape or component is a variant component"
+  [item]
+  (some? (:variant-id item)))
+
+
+(defn is-variant-container?
+  "Check if this shape is a variant container"
+  [shape]
+  (:is-variant-container shape))
+
+
 (defn set-touched-group
   [touched group]
   (when group
diff --git a/common/src/app/common/types/components_list.cljc b/common/src/app/common/types/components_list.cljc
index 8165c2d23..1a2edd786 100644
--- a/common/src/app/common/types/components_list.cljc
+++ b/common/src/app/common/types/components_list.cljc
@@ -48,7 +48,7 @@
                        (wrap-object-fn)))))))
 
 (defn mod-component
-  [file-data {:keys [id name path main-instance-id main-instance-page objects annotation modified-at]}]
+  [file-data {:keys [id name path main-instance-id main-instance-page objects annotation variant-id variant-properties modified-at]}]
   (let [wrap-objects-fn cfeat/*wrap-with-objects-map-fn*]
     (d/update-in-when file-data [:components id]
                       (fn [component]
@@ -76,10 +76,22 @@
                                          (assoc :annotation annotation)
 
                                          (nil? annotation)
-                                         (dissoc :annotation))
+                                         (dissoc :annotation)
+
+                                         (some? variant-id)
+                                         (assoc :variant-id variant-id)
+
+                                         (nil? variant-id)
+                                         (dissoc :variant-id)
+
+                                         (some? variant-properties)
+                                         (assoc :variant-properties variant-properties)
+
+                                         (nil? variant-properties)
+                                         (dissoc :variant-properties))
                               diff     (set/difference
                                         (ctk/diff-components component new-comp)
-                                        #{:annotation :modified-at})] ;; The set of properties that doesn't mark a component as touched
+                                        #{:annotation :modified-at :variant-id :variant-properties})] ;; The set of properties that doesn't mark a component as touched
 
                           (if (empty? diff)
                             new-comp
diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc
index ae9ac3648..38d222735 100644
--- a/common/src/app/common/types/container.cljc
+++ b/common/src/app/common/types/container.cljc
@@ -406,7 +406,7 @@
              (cond-> new-shape
                :always
                (-> (gsh/move delta)
-                   (dissoc :touched))
+                   (dissoc :touched :variant-id :variant-name))
 
                (and main-instance? root?)
                (assoc :main-instance true)
diff --git a/common/test/common_tests/logic/comp_creation_test.cljc b/common/test/common_tests/logic/comp_creation_test.cljc
index c59c14bc1..a57aa7693 100644
--- a/common/test/common_tests/logic/comp_creation_test.cljc
+++ b/common/test/common_tests/logic/comp_creation_test.cljc
@@ -20,6 +20,7 @@
    [app.common.types.component :as ctk]
    [app.common.types.components-list :as ctkl]
    [app.common.types.shape-tree :as ctst]
+   [app.common.uuid :as uuid]
    [clojure.test :as t]))
 
 (t/use-fixtures :each thi/test-fixture)
@@ -288,6 +289,7 @@
         changes (cll/generate-duplicate-component (pcb/empty-changes)
                                                   file
                                                   (:id component)
+                                                  (uuid/next)
                                                   true)
 
         file'   (thf/apply-changes file changes)
diff --git a/frontend/resources/images/icons/variant.svg b/frontend/resources/images/icons/variant.svg
new file mode 100644
index 000000000..5b12adc57
--- /dev/null
+++ b/frontend/resources/images/icons/variant.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M8.75 1.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm-1.5 13.5a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM8.75 8a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0ZM5.375 4.625a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm0 6.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm5.25 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm0-6.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM1.25 8.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm13.5-1.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/>
+</svg>
diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs
index 2cf8ad292..1efe71409 100644
--- a/frontend/src/app/main/data/workspace/libraries.cljs
+++ b/frontend/src/app/main/data/workspace/libraries.cljs
@@ -520,17 +520,19 @@
 
 (defn duplicate-component
   "Create a new component copied from the one with the given id."
-  [library-id component-id]
-  (ptk/reify ::duplicate-component
-    ptk/WatchEvent
-    (watch [it state _]
-      (let [libraries          (dsh/lookup-libraries state)
-            library            (get libraries library-id)
-            components-v2      (features/active-feature? state "components/v2")
-            changes (-> (pcb/empty-changes it nil)
-                        (cll/generate-duplicate-component library component-id components-v2))]
+  ([library-id component-id]
+   (duplicate-component library-id component-id (uuid/next)))
+  ([library-id component-id new-component-id]
+   (ptk/reify ::duplicate-component
+     ptk/WatchEvent
+     (watch [it state _]
+       (let [libraries          (dsh/lookup-libraries state)
+             library            (get libraries library-id)
+             components-v2      (features/active-feature? state "components/v2")
+             changes (-> (pcb/empty-changes it nil)
+                         (cll/generate-duplicate-component library component-id new-component-id components-v2))]
 
-        (rx/of (dch/commit-changes changes))))))
+         (rx/of (dch/commit-changes changes)))))))
 
 (defn delete-component
   "Delete the component with the given id, from the current file library."
@@ -984,7 +986,7 @@
                  second)
             0)))))
 
-(defn- component-swap
+(defn component-swap
   "Swaps a component with another one"
   [shape file-id id-new-component]
   (dm/assert! (uuid? id-new-component))
diff --git a/frontend/src/app/main/data/workspace/variants.cljs b/frontend/src/app/main/data/workspace/variants.cljs
new file mode 100644
index 000000000..e7d965340
--- /dev/null
+++ b/frontend/src/app/main/data/workspace/variants.cljs
@@ -0,0 +1,197 @@
+;; 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.data.workspace.variants
+  (:require
+   [app.common.colors :as clr]
+   [app.common.data.macros :as dm]
+   [app.common.files.changes-builder :as pcb]
+   [app.common.logic.variants :as clv]
+   [app.common.types.components-list :as ctcl]
+   [app.common.uuid :as uuid]
+   [app.main.data.changes :as dch]
+   [app.main.data.helpers :as dsh]
+   [app.main.data.workspace.colors :as cl]
+   [app.main.data.workspace.libraries :as dwl]
+   [app.main.data.workspace.shape-layout :as dwsl]
+   [app.main.data.workspace.shapes :as dwsh]
+   [app.main.data.workspace.undo :as dwu]
+   [beicon.v2.core :as rx]
+   [potok.v2.core :as ptk]))
+
+(defn find-related-components
+  [data objects variant-id]
+  (->> (dm/get-in objects [variant-id :shapes])
+       (map #(dm/get-in objects [% :component-id]))
+       (map #(ctcl/get-component data % true))))
+
+(defn update-property-name
+  "Update the variant property name on the position pos
+   in all the components with this variant-id"
+  [variant-id pos new-name]
+  (ptk/reify ::update-property-name
+    ptk/WatchEvent
+    (watch [it state _]
+      (let [page-id (:current-page-id state)
+            data    (dsh/lookup-file-data state)
+            objects (-> (dsh/get-page data page-id)
+                        (get :objects))
+            related-components (find-related-components data objects variant-id)
+            changes (-> (pcb/empty-changes it page-id)
+                        (pcb/with-library-data data)
+                        (clv/generate-update-property-name related-components pos new-name))
+            undo-id (js/Symbol)]
+        (rx/of
+         (dwu/start-undo-transaction undo-id)
+         (dch/commit-changes changes)
+         (dwu/commit-undo-transaction undo-id))))))
+
+(defn update-property-value
+  "Updates the variant property value on the position pos in a component"
+  [component-id pos value]
+  (ptk/reify ::update-property-value
+    ptk/WatchEvent
+    (watch [it state _]
+      (let [page-id    (:current-page-id state)
+            data       (dsh/lookup-file-data state)
+            objects    (-> (dsh/get-page data page-id)
+                           (get :objects))
+            component  (ctcl/get-component data component-id true)
+            main-id    (:main-instance-id component)
+            properties (-> (:variant-properties component)
+                           (update pos assoc :value value))
+
+            name       (clv/properties-to-name properties)
+
+            changes    (-> (pcb/empty-changes it page-id)
+                           (pcb/with-library-data data)
+                           (pcb/with-objects objects)
+                           (clv/generate-update-property-value component-id main-id pos value name))
+            undo-id    (js/Symbol)]
+        (rx/of
+         (dwu/start-undo-transaction undo-id)
+         (dch/commit-changes changes)
+         (dwu/commit-undo-transaction undo-id))))))
+
+
+(defn remove-property
+  "Remove the variant property on the position pos
+   in all the components with this variant-id"
+  [variant-id pos]
+  (ptk/reify ::remove-property
+    ptk/WatchEvent
+    (watch [it state _]
+      (let [page-id (:current-page-id state)
+            data    (dsh/lookup-file-data state)
+            objects (-> (dsh/get-page data page-id)
+                        (get :objects))
+            related-components (find-related-components data objects variant-id)
+
+            changes (-> (pcb/empty-changes it page-id)
+                        (pcb/with-library-data data)
+                        (pcb/with-objects objects)
+                        (clv/generate-remove-property related-components pos))
+
+            undo-id (js/Symbol)]
+        (rx/of
+         (dwu/start-undo-transaction undo-id)
+         (dch/commit-changes changes)
+         (dwu/commit-undo-transaction undo-id))))))
+
+
+
+(defn add-new-property
+  "Add a new variant property to all the components with this variant-id"
+  [variant-id]
+  (ptk/reify ::add-new-property
+    ptk/WatchEvent
+    (watch [it state _]
+      (let [page-id (:current-page-id state)
+            data    (dsh/lookup-file-data state)
+            objects (-> (dsh/get-page data page-id)
+                        (get :objects))
+
+            related-components (find-related-components data objects variant-id)
+
+
+            property-name (str "Property" (-> related-components
+                                              first
+                                              :variant-properties
+                                              count
+                                              inc))
+
+            changes (-> (pcb/empty-changes it page-id)
+                        (pcb/with-library-data data)
+                        (pcb/with-objects objects)
+                        (clv/generate-add-new-property related-components property-name))
+
+
+            undo-id (js/Symbol)]
+        (rx/of
+         (dwu/start-undo-transaction undo-id)
+         (dch/commit-changes changes)
+         (dwu/commit-undo-transaction undo-id))))))
+
+(defn set-variant-id
+  "Sets the variant-id on a component"
+  [component-id variant-id]
+  (ptk/reify ::set-variant-id
+    ptk/WatchEvent
+    (watch [it state _]
+      (let [page-id (:current-page-id state)
+            data    (dsh/lookup-file-data state)
+            changes (-> (pcb/empty-changes it page-id)
+                        (pcb/with-library-data data)
+                        (pcb/update-component component-id #(assoc % :variant-id variant-id)))
+            undo-id  (js/Symbol)]
+        (rx/of
+         (dwu/start-undo-transaction undo-id)
+         (dch/commit-changes changes)
+         (dwu/commit-undo-transaction undo-id))))))
+
+(defn transform-in-variant
+  "Given the id of a main shape of a component, creates a variant structure for
+   that component"
+  [id]
+  (ptk/reify ::transform-in-variant
+    ptk/WatchEvent
+    (watch [_ state _]
+      (let [variant-id (uuid/next)
+            variant-vec [variant-id]
+            new-component-id (uuid/next)
+            file-id      (:current-file-id state)
+            page-id      (:current-page-id state)
+            objects      (dsh/lookup-page-objects state file-id page-id)
+            main         (get objects id)
+            main-id      (:id main)
+            undo-id      (js/Symbol)]
+
+        (rx/of
+         (dwu/start-undo-transaction undo-id)
+         (dwsh/create-artboard-from-selection variant-id)
+         (cl/remove-all-fills variant-vec {:color clr/black :opacity 1})
+         (dwsl/create-layout-from-id variant-id :flex)
+         (dwsh/update-shapes variant-vec #(assoc % :layout-item-h-sizing :auto
+                                                 :layout-item-v-sizing :auto
+                                                 :layout-padding {:p1 30 :p2 30 :p3 30 :p4 30}
+                                                 :layout-gap     {:row-gap 0 :column-gap 20}
+                                                 :name (:name main)
+                                                 :r1 20
+                                                 :r2 20
+                                                 :r3 20
+                                                 :r4 20
+                                                 :is-variant-container true))
+         (dwsh/update-shapes [main-id] #(assoc % :layout-item-h-sizing :fix :layout-item-v-sizing :fix :variant-id variant-id))
+         (cl/add-stroke variant-vec {:stroke-alignment :inner
+                                     :stroke-style :solid
+                                     :stroke-color "#bb97d8" ;; todo use color var?
+                                     :stroke-opacity 1
+                                     :stroke-width 2})
+         (dwl/duplicate-component file-id (:component-id main) new-component-id)
+         (set-variant-id (:component-id main) variant-id)
+         (set-variant-id new-component-id variant-id)
+         (add-new-property variant-id)
+         (dwu/commit-undo-transaction undo-id))))))
diff --git a/frontend/src/app/main/ui/components/shape_icon.cljs b/frontend/src/app/main/ui/components/shape_icon.cljs
index 060213681..f53154b03 100644
--- a/frontend/src/app/main/ui/components/shape_icon.cljs
+++ b/frontend/src/app/main/ui/components/shape_icon.cljs
@@ -17,10 +17,15 @@
   [{:keys [shape main-instance?]}]
   (if (ctk/instance-head? shape)
     (if main-instance?
-      i/component
+      (if (ctk/is-variant? shape)
+        i/variant
+        i/component)
       i/component-copy)
     (case (:type shape)
       :frame (cond
+               (ctk/is-variant-container? shape)
+               i/component
+
                (and (ctl/flex-layout? shape) (ctl/col? shape))
                i/flex-horizontal
 
diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss
index 7ca4d5b36..f86dc6dde 100644
--- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss
+++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss
@@ -26,6 +26,7 @@
   max-height: $sz-400;
   overflow-y: auto;
   overflow-x: hidden;
+  z-index: var(--z-index-dropdown);
 }
 
 .option {
diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs
index c73c7d1bd..3c23a7c0e 100644
--- a/frontend/src/app/main/ui/icons.cljs
+++ b/frontend/src/app/main/ui/icons.cljs
@@ -257,6 +257,7 @@
 (def ^:icon v2-icon-2 (icon-xref :v2-icon-2))
 (def ^:icon v2-icon-3 (icon-xref :v2-icon-3))
 (def ^:icon v2-icon-4 (icon-xref :v2-icon-4))
+(def ^:icon variant (icon-xref :variant))
 (def ^:icon vertical-align-items-center (icon-xref :vertical-align-items-center))
 (def ^:icon vertical-align-items-end (icon-xref :vertical-align-items-end))
 (def ^:icon vertical-align-items-start (icon-xref :vertical-align-items-start))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs
index 371cfc624..a03c3d6e1 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs
@@ -20,6 +20,8 @@
    [app.main.data.workspace :as dw]
    [app.main.data.workspace.libraries :as dwl]
    [app.main.data.workspace.undo :as dwu]
+   [app.main.data.workspace.variants :as dwv]
+   [app.main.features :as features]
    [app.main.refs :as refs]
    [app.main.render :refer [component-svg component-svg-thumbnail]]
    [app.main.store :as st]
@@ -372,6 +374,8 @@
         can-detach? (and (seq copies)
                          (every? #(not (ctn/has-any-copy-parent? objects %)) copies))
 
+        variants? (features/use-feature "variants/v1")
+
 
         do-detach-component
         #(st/emit! (dwl/detach-components (map :id copies)))
@@ -405,6 +409,10 @@
         do-create-annotation
         #(st/emit! (dw/set-annotations-id-for-create id))
 
+        do-add-variant
+        #(when variants?
+           (st/emit! (dwv/transform-in-variant id)))
+
         do-show-local-component
         #(st/emit! (dwl/go-to-local-component :id component-id))
 
@@ -454,5 +462,8 @@
                          :action do-show-component})
                       (when can-update-main?
                         {:title (tr "workspace.shape.menu.update-main")
-                         :action do-update-component})]]
+                         :action do-update-component})
+                      (when (and variants? (not multi) main-instance?)
+                        {:title (tr "workspace.shape.menu.add-variant")
+                         :action do-add-variant})]]
     (filter (complement nil?) menu-entries)))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs
index 0ebb9849c..81d1ef59d 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs
@@ -16,6 +16,7 @@
    [app.common.uuid :as uuid]
    [app.main.data.workspace :as dw]
    [app.main.data.workspace.collapse :as dwc]
+   [app.main.features :as features]
    [app.main.refs :as refs]
    [app.main.store :as st]
    [app.main.ui.components.shape-icon :as sic]
@@ -53,7 +54,10 @@
                            (= uuid/zero (:parent-id item)))
         absolute?      (ctl/item-absolute? item)
         components-v2  (mf/use-ctx ctx/components-v2)
-        main-instance? (or (not components-v2) (:main-instance item))]
+        main-instance? (or (not components-v2) (:main-instance item))
+        variants?      (features/use-feature "variants/v1")
+        is-variant?    (when variants? (ctk/is-variant? item))
+        variant-name   (when is-variant? (:variant-name item))]
     [:*
      [:div {:id id
             :ref dref
@@ -130,6 +134,7 @@
                        :is-selected selected?
                        :type-comp component-tree?
                        :type-frame (cfh/frame-shape? item)
+                       :variant-name variant-name
                        :is-hidden hidden?}]
 
        (when (not read-only?)
diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs
index 5765041a6..41d8240f4 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs
@@ -29,7 +29,7 @@
    ::mf/forward-ref true}
   [{:keys [shape-id shape-name is-shape-touched disabled-double-click
            on-start-edit on-stop-edit depth parent-size is-selected
-           type-comp type-frame is-hidden is-blocked]} external-ref]
+           type-comp type-frame variant-name is-hidden is-blocked]} external-ref]
   (let [edition*         (mf/use-state false)
         edition?         (deref edition*)
 
@@ -38,6 +38,8 @@
 
         shape-for-rename (mf/deref lens:shape-for-rename)
 
+        shape-name       (d/nilv variant-name shape-name)
+
         has-path?        (str/includes? shape-name "/")
 
         start-edit
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs
index 86fb956a2..53fd9dc65 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs
@@ -11,10 +11,13 @@
    [app.common.files.helpers :as cfh]
    [app.common.types.component :as ctk]
    [app.common.types.file :as ctf]
+   [app.main.data.helpers :as dsh]
    [app.main.data.modal :as modal]
    [app.main.data.workspace :as dw]
    [app.main.data.workspace.libraries :as dwl]
    [app.main.data.workspace.specialized-panel :as dwsp]
+   [app.main.data.workspace.variants :as dwv]
+   [app.main.features :as features]
    [app.main.refs :as refs]
    [app.main.store :as st]
    [app.main.ui.components.dropdown :refer [dropdown]]
@@ -23,6 +26,9 @@
    [app.main.ui.components.select :refer [select]]
    [app.main.ui.components.title-bar :refer [title-bar]]
    [app.main.ui.context :as ctx]
+   [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
+   [app.main.ui.ds.controls.combobox :refer [combobox*]]
+   [app.main.ui.ds.controls.input-with-values :refer [input-with-values*]]
    [app.main.ui.hooks :as h]
    [app.main.ui.icons :as i]
    [app.main.ui.workspace.sidebar.assets.common :as cmm]
@@ -225,6 +231,78 @@
         (when (or editing? creating?)
           [:div {:class (stl/css  :counter)} (str size "/300")])]])))
 
+
+(mf/defc component-variant*
+  [{:keys [component shape data page-id]}]
+  (let [id-component (:id component)
+        properties   (:variant-properties component)
+        variant-id   (:variant-id component)
+        objects      (-> (dsh/get-page data page-id)
+                         (get :objects))
+
+        related-components (dwv/find-related-components data objects variant-id)
+
+        flat-comps ;; Get a list like [{:id 0 :prop1 "v1" :prop2 "v2"} {:id 1, :prop1 "v3" :prop2 "v4"}]
+        (map (fn [{:keys [id variant-properties]}]
+               (into {:id id}
+                     (map (fn [{:keys [name value]}] [(keyword name) value])
+                          variant-properties)))
+             related-components)
+
+        get-options
+        (mf/use-fn
+         (mf/deps related-components)
+         (fn [prop-name]
+           (->> related-components
+                (mapcat (fn [item]
+                          (map :value (filter (fn [prop] (= (:name prop) prop-name))
+                                              (:variant-properties item)))))
+                (filter some?)
+                distinct
+                (map (fn [val] {:label val :id val})))))
+
+        filter-matching
+        (mf/use-fn
+         (mf/deps flat-comps)
+         (fn [id exclude-key]
+           (let [reference-item (first (filter #(= (:id %) id) flat-comps))
+                 reference-values (dissoc reference-item :id exclude-key)]
+
+             (->> flat-comps
+                  (filter (fn [item]
+                            (= (dissoc item :id exclude-key) reference-values)))
+                  (map (fn [item] {:label (get item exclude-key) :value (:id item)}))))))
+
+
+        change-property-value
+        (mf/use-fn
+         (mf/deps id-component)
+         (fn [pos value]
+           (when-not (str/empty? value)
+             (st/emit! (dwv/update-property-value id-component pos value)))))
+
+        switch-component
+        (mf/use-fn
+         (mf/deps shape)
+         (fn [id]
+           (st/emit! (dwl/component-swap shape (:component-file shape) id))))]
+    [:*
+     (for [[pos prop] (map vector (range) properties)]
+
+       [:div {:key (str (:id shape) (:name prop)) :class (stl/css :variant-property-container)}
+        (if (ctk/main-instance? shape)
+          [:*
+           [:span {:class (stl/css :variant-property-name :variant-property-name-bg)} (:name prop)]
+           [:> combobox* {:default-selected (str (or (:value prop) ""))
+                          :options (clj->js (get-options (:name prop)))
+                          :on-change (partial change-property-value pos)}]]
+
+          [:*
+           [:span {:class (stl/css :variant-property-name)} (:name prop)]
+           [:& select {:default-value id-component
+                       :options (filter-matching id-component (keyword (:name prop)))
+                       :on-change switch-component}]])])]))
+
 (mf/defc component-swap-item
   {::mf/props :obj}
   [{:keys [item loop shapes file-id root-shape container component-id is-search listing-thumbs]}]
@@ -508,36 +586,43 @@
                 :on-click (partial do-action action)}
            [:span {:class (stl/css :dropdown-label)} title]]))]]))
 
+
+
 (mf/defc component-menu
   {::mf/props :obj}
   [{:keys [shapes swap-opened?]}]
-  (let [current-file-id     (mf/use-ctx ctx/current-file-id)
+  (let [current-file-id (mf/use-ctx ctx/current-file-id)
+        current-page-id (mf/use-ctx ctx/current-page-id)
 
-        libraries           (deref refs/libraries)
-        current-file        (get libraries current-file-id)
+        libraries       (deref refs/libraries)
+        current-file    (get libraries current-file-id)
+        data            (get-in libraries [current-file-id :data])
 
-        state*              (mf/use-state
-                             #(do {:show-content true
-                                   :menu-open false}))
-        state               (deref state*)
-        open?               (:show-content state)
-        menu-open?          (:menu-open state)
+        state*          (mf/use-state
+                         #(do {:show-content true
+                               :menu-open false}))
+        state           (deref state*)
+        open?           (:show-content state)
+        menu-open?      (:menu-open state)
 
-        shapes              (filter ctk/instance-head? shapes)
-        multi               (> (count shapes) 1)
-        copies              (filter ctk/in-component-copy? shapes)
-        can-swap?           (boolean (seq copies))
+        shapes          (filter ctk/instance-head? shapes)
+        multi           (> (count shapes) 1)
+        copies          (filter ctk/in-component-copy? shapes)
+        can-swap?       (boolean (seq copies))
 
         ;; For when it's only one shape
-        shape               (first shapes)
-        id                  (:id shape)
-        shape-name          (:name shape)
+        shape           (first shapes)
+        id              (:id shape)
+        shape-name      (:name shape)
 
-        component           (ctf/resolve-component shape
-                                                   current-file
-                                                   libraries
-                                                   {:include-deleted? true})
-        main-instance?      (ctk/main-instance? shape)
+        component       (ctf/resolve-component shape
+                                               current-file
+                                               libraries
+                                               {:include-deleted? true})
+
+        variants?       (features/use-feature "variants/v1")
+        is-variant?     (when variants? (ctk/is-variant? component))
+        main-instance?  (ctk/main-instance? shape)
 
         toggle-content
         (mf/use-fn #(swap! state* update :show-content not))
@@ -576,9 +661,9 @@
          (fn []
            (swap! state* update :render inc)))
 
-        menu-entries         (cmm/generate-components-menu-entries shapes true)
-        show-menu?           (seq menu-entries)
-        path (->> component (:path) (cfh/split-path) (cfh/join-path-with-dot))]
+        menu-entries (cmm/generate-components-menu-entries shapes true)
+        show-menu?   (seq menu-entries)
+        path         (->> component (:path) (cfh/split-path) (cfh/join-path-with-dot))]
 
     (when (seq shapes)
       [:div {:class (stl/css :element-set)}
@@ -612,7 +697,9 @@
 
             [:span {:class (stl/css :component-icon)}
              (if main-instance?
-               i/component
+               (if is-variant?
+                 i/variant
+                 i/component)
                i/component-copy)]
 
             [:div {:class (stl/css :name-wrapper)}
@@ -643,5 +730,119 @@
 
           (when (and (not swap-opened?) (not multi))
             [:& component-annotation {:id id :shape shape :component component :rerender-fn rerender-fn}])
+
+          (when (and is-variant? (not swap-opened?) (not multi))
+            [:> component-variant* {:component component :shape shape :data data :page-id current-page-id}])
+
           (when (dbg/enabled? :display-touched)
             [:div ":touched " (str (:touched shape))])])])))
+
+
+(mf/defc variant-menu*
+  [{:keys [shapes]}]
+  (let [;; TODO check multi. What is shown? User can change properties like width?
+        multi              (> (count shapes) 1)
+
+        shape              (first shapes)
+        shape-name         (:name shape)
+
+        libraries          (deref refs/libraries)
+        current-file-id    (mf/use-ctx ctx/current-file-id)
+        current-page-id    (mf/use-ctx ctx/current-page-id)
+        data               (get-in libraries [current-file-id :data])
+
+        objects            (-> (dsh/get-page data current-page-id)
+                               (get :objects))
+
+        first-variant      (get objects (first (:shapes shape)))
+        variant-id         (:variant-id first-variant)
+
+        properties         (->> (dwv/find-related-components data objects variant-id)
+                                (mapcat :variant-properties)
+                                (group-by :name)
+                                (map (fn [[k v]] {:name k :values (map :value v)})))
+
+        menu-open*         (mf/use-state false)
+        menu-open?         (deref menu-open*)
+
+
+        menu-entries       [{:title (tr "workspace.shape.menu.add-variant-property")
+                             :action #(st/emit! (dwv/add-new-property variant-id))}]
+
+        show-menu?         (seq menu-entries)
+
+        on-menu-click
+        (mf/use-fn
+         (mf/deps menu-open* menu-open?)
+         (fn [event]
+           (dom/prevent-default event)
+           (dom/stop-propagation event)
+           (reset! menu-open* (not menu-open?))))
+
+        on-menu-close
+        (mf/use-fn
+         (mf/deps menu-open*)
+         #(reset! menu-open* false))
+
+        update-property-name
+        (mf/use-fn
+         (mf/deps variant-id)
+         (fn [pos new-name]
+           (st/emit! (dwv/update-property-name variant-id pos new-name))))
+
+        remove-property
+        (mf/use-fn
+         (mf/deps variant-id)
+         (fn [pos]
+           (when (> (count properties) 1)
+             (st/emit! (dwv/remove-property variant-id pos)))))]
+    (when (seq shapes)
+      [:div {:class (stl/css :element-set)}
+       [:div {:class (stl/css :element-title)}
+
+
+        [:& title-bar {:collapsable  false
+                       :title        (tr "workspace.options.component")
+                       :class        (stl/css :title-spacing-component)}
+         [:span {:class (stl/css :copy-text)}
+          (tr "workspace.options.component.main")]]]
+
+       [:div {:class (stl/css :element-content)}
+        [:div {:class (stl/css-case :component-wrapper true
+                                    :with-actions show-menu?
+                                    :without-actions (not show-menu?))}
+         [:button {:class (stl/css-case :component-name-wrapper true
+                                        :with-main true
+                                        :swappeable false)}
+
+          [:span {:class (stl/css :component-icon)} i/component]
+
+          [:div {:class (stl/css :name-wrapper)}
+           [:div {:class (stl/css :component-name)}
+            [:span {:class (stl/css :component-name-inside)}
+             (if multi
+               (tr "settings.multiple")
+               (cfh/last-path shape-name))]]]]
+
+
+         (when show-menu?
+           [:div {:class (stl/css :component-actions)}
+            [:button {:class (stl/css-case :menu-btn true
+                                           :selected menu-open?)
+                      :on-click on-menu-click}
+             i/menu]
+
+            [:& component-ctx-menu {:show menu-open?
+                                    :on-close on-menu-close
+                                    :menu-entries menu-entries
+                                    :main-instance true}]])]
+        [:*
+         (for [[pos property] (map vector (range) properties)]
+           (let [val (str/join ", " (:values property))]
+             [:div {:key (str (:id shape) (:name property)) :class (stl/css :variant-property-row)}
+              [:> input-with-values* {:name (:name property) :values val :on-blur (partial update-property-name pos)}]
+              [:> icon-button* {:variant "ghost"
+                                :aria-label (tr "workspace.shape.menu.remove-variant-property")
+                                :on-click (partial remove-property pos)
+                                :icon "remove"
+                                :disabled (<= (count properties) 1)}]]))]]])))
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss
index 030d64cac..4c210a876 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss
@@ -4,7 +4,9 @@
 //
 // Copyright (c) KALEIDOS INC
 
+@use "../../../../ds/typography.scss" as t;
 @import "refactor/common-refactor.scss";
+
 .element-set {
   margin: 0;
   padding-top: $s-8;
@@ -33,6 +35,7 @@
   @include flexCenter;
   width: $s-12;
   height: 100%;
+
   svg {
     height: $s-12;
     width: $s-12;
@@ -54,6 +57,7 @@
 
   &.without-actions {
     padding-right: 0.5rem;
+
     .component-name-wrapper {
       width: 100%;
       border-radius: $br-8;
@@ -71,6 +75,7 @@
   border-radius: $br-8 0 0 $br-8;
   background-color: var(--assets-item-background-color);
   color: var(--assets-item-name-foreground-color-hover);
+
   &:hover {
     background-color: var(--assets-item-background-color-hover);
     color: var(--assets-item-name-foreground-color-hover);
@@ -81,6 +86,7 @@
   @include flexCenter;
   height: $s-32;
   width: $s-12;
+
   svg {
     @extend .button-icon-small;
     stroke: var(--icon-foreground);
@@ -129,14 +135,17 @@
   border-radius: 0 $br-8 $br-8 0;
   background-color: var(--assets-item-background-color);
   color: var(--assets-item-name-foreground-color-hover);
+
   svg {
     @extend .button-icon;
     min-height: $s-16;
     min-width: $s-16;
   }
+
   &:hover {
     background-color: var(--assets-item-background-color-hover);
     color: var(--assets-item-name-foreground-color-hover);
+
     &.selected {
       @extend .button-icon-selected;
     }
@@ -177,6 +186,7 @@
 
 .icon-wrapper {
   display: flex;
+
   svg {
     @extend .button-icon-small;
     stroke: var(--icon-foreground);
@@ -192,9 +202,11 @@
   border: 0;
   font-size: $fs-12;
   color: var(--input-foreground-color-active);
+
   &::placeholder {
     color: var(--input-foreground-color-disabled);
   }
+
   &:focus-visible {
     border-color: var(--input-border-outline-color-active);
   }
@@ -205,8 +217,10 @@
   @include flexCenter;
   height: $s-16;
   width: $s-16;
+
   .clear-icon {
     @include flexCenter;
+
     svg {
       @extend .button-icon-small;
       stroke: var(--icon-foreground);
@@ -218,6 +232,7 @@
   @include flexCenter;
   width: $s-12;
   margin-left: $s-8;
+
   svg {
     @extend .button-icon-small;
     stroke: var(--icon-foreground);
@@ -240,6 +255,7 @@
 .back-arrow {
   @include flexCenter;
   height: $s-32;
+
   svg {
     height: $s-12;
     width: $s-12;
@@ -329,6 +345,7 @@
   --assets-component-current-border-color: var(--assets-component-border-color);
   border: $s-4 solid var(--assets-component-current-border-color);
   cursor: pointer;
+
   img {
     height: auto;
     width: auto;
@@ -337,12 +354,14 @@
     pointer-events: none;
     border: 0;
   }
+
   svg {
     height: 100%;
     width: 100%;
     stroke: none;
     object-fit: contain;
   }
+
   .component-name {
     @include bodySmallTypography;
     @include textEllipsis;
@@ -358,6 +377,7 @@
 
   &:hover {
     background-color: var(--assets-item-background-color-hover);
+
     .component-name {
       display: block;
       color: var(--assets-item-name-foreground-color-hover);
@@ -367,6 +387,7 @@
 
   &.selected {
     --assets-component-current-border-color: var(--assets-item-border-color);
+
     .component-name {
       color: var(--assets-item-name-foreground-color-hover);
     }
@@ -375,9 +396,11 @@
   &.disabled {
     background: var(--assets-component-background-color-disabled);
     cursor: auto;
+
     svg {
       cursor: auto;
     }
+
     .component-name {
       background: linear-gradient(
         to top,
@@ -461,8 +484,10 @@
     @include textEllipsis;
     color: var(--assets-item-name-foreground-color);
   }
+
   &:hover {
     color: var(--assets-item-name-foreground-color-hover);
+
     .component-group-name {
       color: var(--assets-item-name-foreground-color-hover);
     }
@@ -472,6 +497,7 @@
 .arrow-icon {
   @include flexCenter;
   height: $s-32;
+
   svg {
     height: $s-12;
     width: $s-12;
@@ -525,12 +551,14 @@
       margin: 0;
       padding: 0;
       cursor: pointer;
+
       svg {
         @extend .button-icon;
         stroke: var(--icon-foreground);
         width: $s-16;
         height: $s-16;
       }
+
       &.expanded svg {
         transform: rotate(90deg);
       }
@@ -562,6 +590,7 @@
 
       &.icon-tick.invalid:hover {
         cursor: default;
+
         svg {
           stroke: var(--icon-foreground);
         }
@@ -589,8 +618,10 @@
 
   &.editing {
     border: $s-1 solid var(--input-border-color-success);
+
     .annotation-title {
       border-bottom: $s-1 solid var(--entry-border-color-disabled);
+
       .icon {
         display: flex;
       }
@@ -603,9 +634,11 @@
 
   &.creating {
     border: $s-1 solid var(--input-border-color-success);
+
     .annotation-title .icon {
       display: flex;
     }
+
     textarea {
       min-height: $s-252;
     }
@@ -613,6 +646,7 @@
 
   .hidden {
     display: none;
+
     svg {
       display: none;
     }
@@ -650,7 +684,8 @@
       -moz-box-shadow: none;
       box-shadow: none;
 
-      resize: none; /*remove the resize handle on the bottom right*/
+      resize: none;
+      /*remove the resize handle on the bottom right*/
     }
 
     textarea,
@@ -666,3 +701,37 @@
     }
   }
 }
+
+.variant-property-row {
+  @include flexRow;
+  justify-content: space-between;
+  width: 100%;
+  margin-block-start: $s-12;
+}
+
+.variant-property-container {
+  @include t.use-typography("body-small");
+  width: 100%;
+  display: flex;
+  gap: var(--sp-xs);
+}
+
+.variant-property-name-bg {
+  border-radius: $br-8;
+  background-color: var(--assets-item-background-color);
+}
+
+.variant-property-name {
+  color: var(--color-foreground-primary);
+
+  width: $s-104;
+
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: var(--sp-xxxl);
+
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs
index 6d7a0b433..cad5009a7 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs
@@ -8,10 +8,11 @@
   (:require
    [app.common.data.macros :as dm]
    [app.common.types.shape.layout :as ctl]
+   [app.main.features :as features]
    [app.main.refs :as refs]
    [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]]
    [app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu*]]
-   [app.main.ui.workspace.sidebar.options.menus.component :refer [component-menu]]
+   [app.main.ui.workspace.sidebar.options.menus.component :refer [component-menu variant-menu*]]
    [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]]
    [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs-shape fill-menu]]
    [app.main.ui.workspace.sidebar.options.menus.frame-grid :refer [frame-grid]]
@@ -66,58 +67,62 @@
         is-layout-container?      (ctl/any-layout? shape)
         is-flex-layout?           (ctl/flex-layout? shape)
         is-grid-layout?           (ctl/grid-layout? shape)
-        is-layout-child-absolute? (ctl/item-absolute? shape)]
+        is-layout-child-absolute? (ctl/item-absolute? shape)
+        variants?                 (features/use-feature "variants/v1")
+        is-variant?               (when variants? (:is-variant-container shape))]
 
-    [:*
-     [:& layer-menu {:ids ids
-                     :type shape-type
-                     :values layer-values}]
-     [:> measures-menu* {:ids ids
-                         :values measure-values
-                         :type shape-type
-                         :shape shape}]
+    (if is-variant?
+      [:> variant-menu* {:shapes [shape]}]
+      [:*
+       [:& layer-menu {:ids ids
+                       :type shape-type
+                       :values layer-values}]
+       [:> measures-menu* {:ids ids
+                           :values measure-values
+                           :type shape-type
+                           :shape shape}]
 
-     [:& component-menu {:shapes [shape]}]
+       [:& component-menu {:shapes [shape]}]
 
-     [:& layout-container-menu
-      {:type shape-type
-       :ids [(:id shape)]
-       :values layout-container-values
-       :multiple false}]
+       [:& layout-container-menu
+        {:type shape-type
+         :ids [(:id shape)]
+         :values layout-container-values
+         :multiple false}]
 
-     (when (and (= (count ids) 1) is-layout-child? is-grid-parent?)
-       [:& grid-cell/options
-        {:shape (first parents)
-         :cell (ctl/get-cell-by-shape-id (first parents) (first ids))}])
+       (when (and (= (count ids) 1) is-layout-child? is-grid-parent?)
+         [:& grid-cell/options
+          {:shape (first parents)
+           :cell (ctl/get-cell-by-shape-id (first parents) (first ids))}])
 
-     (when (or is-layout-child? is-layout-container?)
-       [:& layout-item-menu
-        {:ids ids
-         :type shape-type
-         :values layout-item-values
-         :is-flex-parent? is-flex-parent?
-         :is-grid-parent? is-grid-parent?
-         :is-flex-layout? is-flex-layout?
-         :is-grid-layout? is-grid-layout?
-         :is-layout-child? is-layout-child?
-         :is-layout-container? is-layout-container?
-         :shape shape}])
+       (when (or is-layout-child? is-layout-container?)
+         [:& layout-item-menu
+          {:ids ids
+           :type shape-type
+           :values layout-item-values
+           :is-flex-parent? is-flex-parent?
+           :is-grid-parent? is-grid-parent?
+           :is-flex-layout? is-flex-layout?
+           :is-grid-layout? is-grid-layout?
+           :is-layout-child? is-layout-child?
+           :is-layout-container? is-layout-container?
+           :shape shape}])
 
-     (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
-       [:& constraints-menu {:ids ids
-                             :values constraint-values}])
+       (when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
+         [:& constraints-menu {:ids ids
+                               :values constraint-values}])
 
-     [:& fill-menu {:ids ids
-                    :type shape-type
-                    :values (select-keys shape fill-attrs-shape)}]
-     [:& stroke-menu {:ids ids
+       [:& fill-menu {:ids ids
                       :type shape-type
-                      :values stroke-values}]
-     [:> color-selection-menu* {:type shape-type
-                                :shapes shapes-with-children
-                                :file-id file-id
-                                :libraries shared-libs}]
-     [:> shadow-menu* {:ids ids :values (get shape :shadow)}]
-     [:& blur-menu {:ids ids
-                    :values (select-keys shape [:blur])}]
-     [:& frame-grid {:shape shape}]]))
+                      :values (select-keys shape fill-attrs-shape)}]
+       [:& stroke-menu {:ids ids
+                        :type shape-type
+                        :values stroke-values}]
+       [:> color-selection-menu* {:type shape-type
+                                  :shapes shapes-with-children
+                                  :file-id file-id
+                                  :libraries shared-libs}]
+       [:> shadow-menu* {:ids ids :values (get shape :shadow)}]
+       [:& blur-menu {:ids ids
+                      :values (select-keys shape [:blur])}]
+       [:& frame-grid {:shape shape}]])))
diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs
index 3386d3b9a..dcc3f1eef 100644
--- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs
@@ -18,6 +18,7 @@
    [app.main.data.common :as dcm]
    [app.main.data.workspace :as dw]
    [app.main.data.workspace.interactions :as dwi]
+   [app.main.features :as features]
    [app.main.refs :as refs]
    [app.main.store :as st]
    [app.main.streams :as ms]
@@ -131,9 +132,11 @@
            (on-frame-leave (:id frame))))
 
         main-instance? (ctk/main-instance? frame)
+        variants?      (features/use-feature "variants/v1")
+        is-variant?    (when variants? (:is-variant-container frame))
 
         text-width (* (:width frame) zoom)
-        show-icon? (and (or (:use-for-thumbnail frame) grid-edition? main-instance?)
+        show-icon? (and (or (:use-for-thumbnail frame) grid-edition? main-instance? is-variant?)
                         (not (<= text-width 15)))
         text-pos-x (if show-icon? 15 0)
 
@@ -196,7 +199,8 @@
           (cond
             (:use-for-thumbnail frame) [:use {:href "#icon-boards-thumbnail"}]
             grid-edition? [:use {:href "#icon-grid"}]
-            main-instance? [:use {:href "#icon-component"}])])
+            main-instance? [:use {:href "#icon-component"}]
+            is-variant?  [:use {:href "#icon-component"}])])
 
        (if ^boolean edition?
            ;; Case when edition? is true
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index 3db863a14..7f77c3944 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -6210,6 +6210,15 @@ msgstr "Selection to board"
 msgid "workspace.shape.menu.create-component"
 msgstr "Create component"
 
+msgid "workspace.shape.menu.add-variant"
+msgstr "Add variant"
+
+msgid "workspace.shape.menu.add-variant-property"
+msgstr "Add new property"
+
+msgid "workspace.shape.menu.remove-variant-property"
+msgstr "Remove property"
+
 #: src/app/main/ui/workspace/context_menu.cljs:565
 msgid "workspace.shape.menu.create-multiple-components"
 msgstr "Create multiple components"
diff --git a/frontend/translations/es.po b/frontend/translations/es.po
index c30a0976d..57b34b36b 100644
--- a/frontend/translations/es.po
+++ b/frontend/translations/es.po
@@ -6224,6 +6224,16 @@ msgstr "Tablero de selección"
 msgid "workspace.shape.menu.create-component"
 msgstr "Crear componente"
 
+msgid "workspace.shape.menu.add-variant"
+msgstr "Añadir variante"
+
+msgid "workspace.shape.menu.add-variant-property"
+msgstr "Añadir nueva propiedad"
+
+msgid "workspace.shape.menu.remove-variant-property"
+msgstr "Eliminar propiedad"
+
+
 #: src/app/main/ui/workspace/context_menu.cljs:565
 msgid "workspace.shape.menu.create-multiple-components"
 msgstr "Crear múltiples componentes"