Merge remote-tracking branch 'penpot/develop' into token-studio-develop

This commit is contained in:
Florian Schroedl 2024-06-03 10:51:04 +02:00
commit dc14933f3a
116 changed files with 7413 additions and 6245 deletions

View file

@ -130,6 +130,10 @@
(def worker-uri
(obj/get global "penpotWorkerURI" "/js/worker.js"))
(defn external-feature-flag [flag value]
(when-let [fn (obj/get global "externalFeatureFlag")]
(fn flag value)))
;; --- Helper Functions
(defn ^boolean check-browser? [candidate]

View file

@ -249,9 +249,18 @@
(deleteObject [_ id]
(set! file (fb/delete-object file (uuid/uuid id))))
(getId [_]
(:id file))
(getCurrentPageId [_]
(:current-page-id file))
(asMap [_]
(clj->js file))
(newId [_]
(uuid/next))
(export [_]
(->> (export-file file)
(rx/subs!
@ -261,7 +270,8 @@
(dom/trigger-download (:name file) export-blob))))))))
(defn create-file-export [^string name]
(File. (fb/create-file name)))
(binding [cfeat/*current* cfeat/default-features]
(File. (fb/create-file name))))
(defn exports []
#js {:createFile create-file-export})

View file

@ -343,9 +343,9 @@
(watch [_ state _]
(let [on-success (:on-success opts identity)
on-error (:on-error opts rx/throw)
profile (:profile state)]
(->> (rp/cmd! :update-profile (dissoc profile :props))
profile (:profile state)
params (select-keys profile [:fullname :lang :theme])]
(->> (rp/cmd! :update-profile params)
(rx/tap on-success)
(rx/catch on-error))))))

View file

@ -119,7 +119,14 @@
(->> stream
(rx/filter (ptk/type? :layout/update))
(rx/map deref)
(rx/map #(update-layout-positions %))
;; We buffer the updates to the layout so if there are many changes at the same time
;; they are process together. It will get a better performance.
(rx/buffer-time 100)
(rx/filter #(d/not-empty? %))
(rx/map
(fn [data]
(let [ids (reduce #(into %1 (:ids %2)) #{} data)]
(update-layout-positions {:ids ids}))))
(rx/take-until stopper))))))
(defn finalize

View file

@ -76,6 +76,7 @@
:is-transparent (and opacity (> 1 opacity))
:grid-area area
:read-only read-only?)
:role "button"
:data-readonly (str read-only?)
:on-click on-click
:title (color-title color)}

View file

@ -144,6 +144,7 @@
(def ^:icon img (icon-xref :img))
(def ^:icon interaction (icon-xref :interaction))
(def ^:icon join-nodes (icon-xref :join-nodes))
(def ^:icon external-link (icon-xref :external-link))
(def ^:icon justify-content-column-around (icon-xref :justify-content-column-around))
(def ^:icon justify-content-column-between (icon-xref :justify-content-column-between))
(def ^:icon justify-content-column-center (icon-xref :justify-content-column-center))
@ -160,13 +161,13 @@
(def ^:icon library (icon-xref :library))
(def ^:icon locate (icon-xref :locate))
(def ^:icon lock (icon-xref :lock))
(def ^:icon margin (icon-xref :margin))
(def ^:icon margin-bottom (icon-xref :margin-bottom))
(def ^:icon margin-left (icon-xref :margin-left))
(def ^:icon margin-left-right (icon-xref :margin-left-right))
(def ^:icon margin-right (icon-xref :margin-right))
(def ^:icon margin-top-bottom (icon-xref :margin-top-bottom))
(def ^:icon margin-top (icon-xref :margin-top))
(def ^:icon margin (icon-xref :margin))
(def ^:icon margin-top-bottom (icon-xref :margin-top-bottom))
(def ^:icon mask (icon-xref :mask))
(def ^:icon masked (icon-xref :masked))
(def ^:icon menu (icon-xref :menu))
@ -179,11 +180,11 @@
(def ^:icon open-link (icon-xref :open-link))
(def ^:icon padding-bottom (icon-xref :padding-bottom))
(def ^:icon padding-extended (icon-xref :padding-extended))
(def ^:icon padding-left-right (icon-xref :padding-left-right))
(def ^:icon padding-left (icon-xref :padding-left))
(def ^:icon padding-left-right (icon-xref :padding-left-right))
(def ^:icon padding-right (icon-xref :padding-right))
(def ^:icon padding-top-bottom (icon-xref :padding-top-bottom))
(def ^:icon padding-top (icon-xref :padding-top))
(def ^:icon padding-top-bottom (icon-xref :padding-top-bottom))
(def ^:icon path (icon-xref :path))
(def ^:icon pentool (icon-xref :pentool))
(def ^:icon picker (icon-xref :picker))
@ -192,11 +193,12 @@
(def ^:icon rectangle (icon-xref :rectangle))
(def ^:icon reload (icon-xref :reload))
(def ^:icon remove-icon (icon-xref :remove))
(def ^:icon rgba-complementary (icon-xref :rgba-complementary))
(def ^:icon rgba (icon-xref :rgba))
(def ^:icon rgba-complementary (icon-xref :rgba-complementary))
(def ^:icon rocket (icon-xref :rocket))
(def ^:icon rotation (icon-xref :rotation))
(def ^:icon row-reverse (icon-xref :row-reverse))
(def ^:icon row (icon-xref :row))
(def ^:icon row-reverse (icon-xref :row-reverse))
(def ^:icon search (icon-xref :search))
(def ^:icon separate-nodes (icon-xref :separate-nodes))
(def ^:icon shown (icon-xref :shown))
@ -218,6 +220,7 @@
(def ^:icon svg (icon-xref :svg))
(def ^:icon swatches (icon-xref :swatches))
(def ^:icon switch (icon-xref :switch))
(def ^:icon text (icon-xref :text))
(def ^:icon text-align-center (icon-xref :text-align-center))
(def ^:icon text-align-left (icon-xref :text-align-left))
(def ^:icon text-align-right (icon-xref :text-align-right))
@ -239,7 +242,6 @@
(def ^:icon text-top (icon-xref :text-top))
(def ^:icon text-underlined (icon-xref :text-underlined))
(def ^:icon text-uppercase (icon-xref :text-uppercase))
(def ^:icon text (icon-xref :text))
(def ^:icon thumbnail (icon-xref :thumbnail))
(def ^:icon tick (icon-xref :tick))
(def ^:icon to-corner (icon-xref :to-corner))
@ -258,7 +260,6 @@
(def ^:icon view-as-list (icon-xref :view-as-list))
(def ^:icon wrap (icon-xref :wrap))
(def ^:icon loader-pencil
(mf/html
[:svg

View file

@ -142,7 +142,9 @@
(modal/show! {:type :onboarding-newsletter})
(contains? cf/flags :onboarding-team)
(modal/show! {:type :onboarding-team}))))]
(modal/show! {:type :onboarding-team}))))
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
(mf/with-effect [@slide]
(when (not= :start @slide)
@ -151,8 +153,8 @@
(fn []
(reset! klass nil)
(tm/dispose! sem))))
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated {:class (dm/str @klass " " (stl/css :animated))}
(case @slide
:start [:& onboarding-welcome {:next #(navigate :opensource)}]

View file

@ -7,6 +7,7 @@
(ns app.main.ui.onboarding.newsletter
(:require-macros [app.main.style :as stl])
(:require
[app.config :as cf]
[app.main.data.messages :as msg]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
@ -35,9 +36,11 @@
(st/emit! (when (or @newsletter-updates @newsletter-news)
(msg/success message))
(modal/show {:type :onboarding-team})
(du/update-profile-props {:newsletter-updates @newsletter-updates :newsletter-news @newsletter-news}))))]
(du/update-profile-props {:newsletter-updates @newsletter-updates :newsletter-news @newsletter-news}))))
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated.fadeInDown {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-left)}
[:img {:src "images/deco-newsletter.png"

View file

@ -287,9 +287,11 @@
(modal/show! {:type :onboarding-team})
:else
(modal/hide!)))))]
(modal/hide!)))))
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div {:class (stl/css :modal-container)
:ref container}
(case @step

View file

@ -9,6 +9,7 @@
(:require
[app.common.data.macros :as dmc]
[app.common.spec :as us]
[app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.messages :as msg]
@ -84,14 +85,16 @@
::ev/origin "onboarding"
:step 1}))))
teams (mf/deref refs/teams)]
teams (mf/deref refs/teams)
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
(mf/with-effect [teams]
(when (> (count teams) 1)
(st/emit! (modal/hide))))
(when (< (count teams) 2)
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated.fadeIn {:class (stl/css :modal-container)}
[:& team-modal-left]
[:div {:class (stl/css :separator)}]
@ -212,9 +215,11 @@
(if (> (count emails) 0)
(on-invite-now form)
(on-invite-later form))
(modal/hide!))))]
(modal/hide!))))
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated.fadeIn {:class (stl/css :modal-container)}
[:& team-modal-left]

View file

@ -8,196 +8,203 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.config :as cf]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
;; TODO: Review all copies and alt text
(defmethod c/render-release-notes "2.0"
[{:keys [slide klass next finish navigate version]}]
(mf/html
(case slide
:start
[:div {:class (stl/css :modal-overlay)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-intro-image.png"
:class (stl/css :start-image)
:border "0"
:alt "A graphic illustration with Penpot style"}]
(let [onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
(mf/html
(case slide
:start
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-intro-image.png"
:class (stl/css :start-image)
:border "0"
:alt "A graphic illustration with Penpot style"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Welcome to Penpot 2.0! "]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Welcome to Penpot 2.0! "]
[:div {:class (stl/css :version-tag)}
(dm/str "Version " version)]]
[:div {:class (stl/css :version-tag)}
(dm/str "Version " version)]]
[:div {:class (stl/css :features-block)}
[:p {:class (stl/css :feature-content)}
[:spam {:class (stl/css :feature-title)}
"CSS Grid Layout: "]
"Bring your designs to life, knowing that what you create is what developers code."]
[:div {:class (stl/css :features-block)}
[:p {:class (stl/css :feature-content)}
[:spam {:class (stl/css :feature-title)}
"CSS Grid Layout: "]
"Bring your designs to life, knowing that what you create is what developers code."]
[:p {:class (stl/css :feature-content)}
[:spam {:class (stl/css :feature-title)}
"Sleeker UI: "]
"Weve polished Penpot to make your experience smoother and more enjoyable."]
[:p {:class (stl/css :feature-content)}
[:spam {:class (stl/css :feature-title)}
"Sleeker UI: "]
"Weve polished Penpot to make your experience smoother and more enjoyable."]
[:p {:class (stl/css :feature-content)}
[:spam {:class (stl/css :feature-title)}
"New Components System: "]
"Managing and using your design components got a whole lot better."]
[:p {:class (stl/css :feature-content)}
[:spam {:class (stl/css :feature-title)}
"New Components System: "]
"Managing and using your design components got a whole lot better."]
[:p {:class (stl/css :feature-content)}
"And thats not all - weve fined tuned performance and "
"accessibility to give you a better and more fluid design experience."]
[:p {:class (stl/css :feature-content)}
"And thats not all - weve fined tuned performance and "
"accessibility to give you a better and more fluid design experience."]
[:p {:class (stl/css :feature-content)}
" Ready to dive in? Let 's get started!"]]
[:p {:class (stl/css :feature-content)}
" Ready to dive in? Let 's get started!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
0
[:div {:class (stl/css :modal-overlay)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-css-grid.gif"
:class (stl/css :start-image)
:border "0"
:alt "Penpot's CSS Grid Layout"}]
0
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-css-grid.gif"
:class (stl/css :start-image)
:border "0"
:alt "Penpot's CSS Grid Layout"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"CSS Grid Layout - Design Meets Development"]]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"CSS Grid Layout - Design Meets Development"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"The much-awaited Grid Layout introduces 2-dimensional"
" layout capabilities to Penpot, allowing for the creation"
" of adaptive layouts by leveraging the power of CSS properties."]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"The much-awaited Grid Layout introduces 2-dimensional"
" layout capabilities to Penpot, allowing for the creation"
" of adaptive layouts by leveraging the power of CSS properties."]
[:p {:class (stl/css :feature-content)}
"Its a host of new features, including columns and"
" rows management, flexible units such as FR (fractions),"
" the ability to create and name areas, and tons of new "
"and unique possibilities within a design tool."]
[:p {:class (stl/css :feature-content)}
"Its a host of new features, including columns and"
" rows management, flexible units such as FR (fractions),"
" the ability to create and name areas, and tons of new "
"and unique possibilities within a design tool."]
[:p {:class (stl/css :feature-content)}
"Designers will learn CSS basics while working, "
"and as always with Penpot, developers can pick"
" up the design as code to take it from there."]]
[:p {:class (stl/css :feature-content)}
"Designers will learn CSS basics while working, "
"and as always with Penpot, developers can pick"
" up the design as code to take it from there."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
1
[:div {:class (stl/css :modal-overlay)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-new-ui.gif"
:class (stl/css :start-image)
:border "0"
:alt "Penpot's UI Makeover"}]
1
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-new-ui.gif"
:class (stl/css :start-image)
:border "0"
:alt "Penpot's UI Makeover"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"UI Makeover - Smoother, Sharper, and Simply More Fun"]]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"UI Makeover - Smoother, Sharper, and Simply More Fun"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"We've completely overhauled Penpot's user interface. "
"The improvements in consistency, the introduction of "
"new microinteractions, and attention to countless details"
" will significantly enhance the productivity and enjoyment of using Penpot."]
[:p {:class (stl/css :feature-content)}
"Furthermore, weve made several accessibility improvements, "
"with better color contrast, keyboard navigation,"
" and adherence to other best practices."]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"We've completely overhauled Penpot's user interface. "
"The improvements in consistency, the introduction of "
"new microinteractions, and attention to countless details"
" will significantly enhance the productivity and enjoyment of using Penpot."]
[:p {:class (stl/css :feature-content)}
"Furthermore, weve made several accessibility improvements, "
"with better color contrast, keyboard navigation,"
" and adherence to other best practices."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
2
[:div {:class (stl/css :modal-overlay)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-components.gif"
:class (stl/css :start-image)
:border "0"
:alt "Penpot's new components system"}]
2
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-components.gif"
:class (stl/css :start-image)
:border "0"
:alt "Penpot's new components system"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"New Components System"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"The new Penpot components system improves"
" control over instances, including their "
"inheritances and properties overrides. "
"Main components are now accessible as design"
" elements, allowing a better updating "
"workflow through instant changes synchronization."]
[:p {:class (stl/css :feature-content)}
"And thats not all, there are new capabilities "
"such as component swapping and annotations "
"that will help you to better manage your design systems."]]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"New Components System"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"The new Penpot components system improves"
" control over instances, including their "
"inheritances and properties overrides. "
"Main components are now accessible as design"
" elements, allowing a better updating "
"workflow through instant changes synchronization."]
[:p {:class (stl/css :feature-content)}
"And thats not all, there are new capabilities "
"such as component swapping and annotations "
"that will help you to better manage your design systems."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
3
[:div {:class (stl/css :modal-overlay)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-html.gif"
:class (stl/css :start-image)
:border "0"
:alt " Penpot's HTML code generator"}]
3
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-html.gif"
:class (stl/css :start-image)
:border "0"
:alt " Penpot's HTML code generator"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"And much more"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"In addition to all of this, weve included several other requested improvements:"]
[:ul {:class (stl/css :feature-list)}
[:li "Access HTML markup code directly in inspect mode"]
[:li "Images are now treated as element fills, maintaining their aspect ratio on resize, ideal for flexible designs"]
[:li "Enjoy new color themes with options for both dark and light modes"]
[:li "Feel the speed boost! Enjoy a smoother experience with a bunch of performance improvements"]]]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"And much more"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"In addition to all of this, weve included several other requested improvements:"]
[:ul {:class (stl/css :feature-list)}
[:li "Access HTML markup code directly in inspect mode"]
[:li "Images are now treated as element fills, maintaining their aspect ratio on resize, ideal for flexible designs"]
[:li "Enjoy new color themes with options for both dark and light modes"]
[:li "Feel the speed boost! Enjoy a smoother experience with a bunch of performance improvements"]]]
[:div {:class (stl/css :navigation)}
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 4}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]]))))

View file

@ -372,15 +372,14 @@
(defn calculate-position
"Calculates the style properties for the given coordinates and position"
[{vh :height} position x y]
(let [;; picker height in pixels
h 510
(let [;; picker size in pixels
h 510
w 284
;; Checks for overflow outside the viewport height
max-y (- vh h)
rulers? (mf/deref refs/rulers?)
left-offset (if rulers? 40 18)
x-pos 400]
right-offset (+ w 40)]
(cond
(or (nil? x) (nil? y))
@ -388,9 +387,9 @@
(= position :left)
(if (> y max-y)
#js {:left (dm/str (- x x-pos) "px")
#js {:left (dm/str (- x right-offset) "px")
:bottom "1rem"}
#js {:left (dm/str (- x x-pos) "px")
#js {:left (dm/str (- x right-offset) "px")
:top (dm/str (- y 70) "px")})
(= position :right)
@ -440,6 +439,7 @@
(on-close @last-change)))
[:div {:class (stl/css :colorpicker-tooltip)
:data-testid "colorpicker"
:style style}
[:& colorpicker {:data data

View file

@ -629,13 +629,13 @@
(when (d/not-empty? plugins)
[:div {:class (stl/css :separator)}])
(for [[idx {:keys [name url]}] (d/enumerate plugins)]
(for [[idx {:keys [name] :as manifest}] (d/enumerate plugins)]
[:> dropdown-menu-item* {:key (dm/str "plugins-menu-" idx)
:on-click #(uwp/open-plugin! url)
:on-click #(uwp/open-plugin! manifest)
:class (stl/css :submenu-item)
:on-key-down (fn [event]
(when (kbd/enter? event)
#(uwp/open-plugin! url)))}
#(uwp/open-plugin! manifest)))}
[:span {:class (stl/css :item-name)} name]])])))
(mf/defc menu

View file

@ -13,6 +13,7 @@
[app.main.ui.components.search-bar :refer [search-bar]]
[app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.icons :as i]
[app.util.avatars :as avatars]
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj]
@ -24,26 +25,30 @@
(mf/defc plugin-entry
[{:keys [index _icon url name description on-open-plugin on-remove-plugin]}]
[{:keys [index manifest on-open-plugin on-remove-plugin]}]
(let [handle-open-click
(let [{:keys [host icon name description]} manifest
handle-open-click
(mf/use-callback
(mf/deps index url on-open-plugin)
(mf/deps index manifest on-open-plugin)
(fn []
(when on-open-plugin
(on-open-plugin index url))))
(on-open-plugin manifest))))
handle-delete-click
(mf/use-callback
(mf/deps index url on-remove-plugin)
(mf/deps index on-remove-plugin)
(fn []
(when on-remove-plugin
(on-remove-plugin index url))))]
(on-remove-plugin index))))]
[:div {:class (stl/css :plugins-list-element)}
[:div {:class (stl/css :plugin-icon)} ""]
[:div {:class (stl/css :plugin-icon)}
[:img {:src (if (some? icon)
(dm/str host icon)
(avatars/generate {:name name}))}]]
[:div {:class (stl/css :plugin-description)}
[:div {:class (stl/css :plugin-title)} name]
[:div {:class (stl/css :plugin-summary)} description]]
[:div {:class (stl/css :plugin-summary)} (d/nilv description "")]]
[:button {:class (stl/css :open-button)
:on-click handle-open-click} (tr "workspace.plugins.button-open")]
[:button {:class (stl/css :trash-button)
@ -65,8 +70,15 @@
(.setItem ls "plugins" plugins-val)))
(defn open-plugin!
[url]
(.ɵloadPlugin js/window #js {:manifest url}))
[{:keys [name description host code icon permissions]}]
(.ɵloadPlugin
js/window #js
{:name name
:description description
:host host
:code code
:icon icon
:permissions (apply array permissions)}))
(mf/defc plugin-management-dialog
{::mf/register modal/components
@ -107,7 +119,20 @@
(rx/subs!
(fn [body]
(let [name (obj/get body "name")
new-state (conj plugins-state {:name name :url plugin-url})]
desc (obj/get body "description")
code (obj/get body "code")
icon (obj/get body "icon")
permissions (obj/get body "permissions")
origin (obj/get (js/URL. plugin-url) "origin")
new-state
(conj plugins-state
{:name name
:description desc
:host origin
:code code
:icon icon
:permissions (->> permissions (mapv str))})]
(reset! input-status* :success)
(reset! plugin-url* "")
(reset! plugins-state* new-state)
@ -117,18 +142,18 @@
handle-open-plugin
(mf/use-callback
(fn [_ url]
(open-plugin! url)
(fn [manifest]
(open-plugin! manifest)
(modal/hide!)))
handle-remove-plugin
(mf/use-callback
(mf/deps plugins-state)
(fn [rm-idx _]
(fn [plugin-index]
(let [new-state
(into []
(keep-indexed (fn [idx item]
(when (not= idx rm-idx) item)))
(when (not= idx plugin-index) item)))
plugins-state)]
(reset! plugins-state* new-state)
@ -160,22 +185,22 @@
[:hr]
[:& title-bar {:collapsable false
:title (tr "workspace.plugins.installed-plugins")}]
(if (empty? plugins-state)
[:div {:class (stl/css :plugins-empty)}
[:div {:class (stl/css :plugins-empty-logo)} i/logo-icon]
[:div {:class (stl/css :plugins-empty-text)} (tr "workspace.plugins.empty-plugins")]]
[:div {:class (stl/css :plugins-empty-logo)} i/rocket]
[:div {:class (stl/css :plugins-empty-text)} (tr "workspace.plugins.empty-plugins")]
[:a {:class (stl/css :plugins-link) :href "#"}
(tr "workspace.plugins.plugin-list-link") i/external-link]]
[:div {:class (stl/css :plugins-list)}
[:*
[:& title-bar {:collapsable false
:title (tr "workspace.plugins.installed-plugins")}]
(for [[idx {:keys [name url]}] (d/enumerate plugins-state)]
[:& plugin-entry {:key (dm/str "plugin-" idx)
:name name
:url url
:index idx
:icon nil
:description "Nullam ullamcorper ligula ac felis commodo pulvinar."
:on-open-plugin handle-open-plugin
:on-remove-plugin handle-remove-plugin}])])]]]))
[:div {:class (stl/css :plugins-list)}
(for [[idx manifest] (d/enumerate plugins-state)]
[:& plugin-entry {:key (dm/str "plugin-" idx)
:index idx
:manifest manifest
:on-open-plugin handle-open-plugin
:on-remove-plugin handle-remove-plugin}])]])]]]))

View file

@ -18,6 +18,10 @@
max-height: $s-472;
width: $s-472;
max-width: $s-472;
hr {
border-color: $db-tertiary;
}
}
.close-btn {
@ -31,7 +35,7 @@
.modal-title {
@include headlineMediumTypography;
margin-block-end: $s-16;
margin-block-end: $s-32;
color: var(--modal-title-foreground-color);
}
@ -39,6 +43,7 @@
display: flex;
flex-direction: column;
height: $s-380;
padding-bottom: $s-16;
}
.primary-button {
@ -88,7 +93,7 @@
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
gap: $s-12;
}
.plugins-list-element {
@ -101,56 +106,61 @@
min-height: $s-32;
width: $s-32;
height: $s-32;
background: #b1b2b5;
background: var(--button-secondary-background-color-rest);
padding: $s-2;
border-radius: $s-4;
}
.plugin-description {
display: flex;
flex-direction: column;
gap: $s-8;
width: 100%;
}
.plugin-title {
@include bodyMediumTypography;
color: #ffffff;
color: $df-primary;
}
.plugin-summary {
@include bodySmallTypography;
color: #8f9da3;
color: $df-secondary;
}
.plugins-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
margin-top: 3rem;
gap: $s-20;
margin-top: $s-16;
}
.plugins-empty-logo {
width: 44px;
height: 44px;
width: $s-44;
height: $s-44;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background: #212426;
background: $db-tertiary;
svg {
width: 16px;
height: 16px;
fill: #8f9da3;
width: $s-16;
height: $s-16;
fill: none;
stroke: $df-secondary;
stroke-width: 0.8px;
}
}
.plugins-empty-text {
@include bodySmallTypography;
color: white;
color: $df-primary;
}
div.input-error {
border: 1px solid var(--input-border-color-error);
border: $s-1 solid var(--input-border-color-error);
}
.info {
@ -165,3 +175,19 @@ div.input-error {
color: var(--input-border-color-success);
}
}
.plugins-link {
color: $da-primary;
font-size: $fs-12;
display: inline-flex;
align-items: center;
gap: $s-4;
svg {
margin-top: calc(-1 * var($s-2));
width: $s-12;
height: $s-12;
stroke: $da-primary;
fill: none;
}
}

View file

@ -11,11 +11,20 @@
[app.main.store :as st]
[app.plugins.api :as api]
[app.util.globals :refer [global]]
[app.util.object :as obj]))
[app.util.object :as obj]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn init!
[]
(when (features/active-feature? @st/state "plugins/runtime")
(when-let [init-runtime (obj/get global "initPluginsRuntime")]
(let [context (api/create-context)]
(init-runtime context)))))
(->> st/stream
(rx/filter (ptk/type? ::features/initialize))
(rx/take 1)
;; We need to wait to the init event to finish
(rx/observe-on :async)
(rx/subs!
(fn []
(when (features/active-feature? @st/state "plugins/runtime")
(when-let [init-runtime (obj/get global "initPluginsRuntime")]
(let [context (api/create-context)]
(init-runtime context))))))))

View file

@ -52,6 +52,10 @@
[_ type callback]
(events/add-listener type callback))
(removeListener
[_ listener-id]
(events/remove-listener listener-id))
(getViewport
[_]
(viewport/create-proxy))

View file

@ -14,6 +14,14 @@
(defmulti handle-state-change (fn [type _] type))
(defmethod handle-state-change "finish"
[_ old-val new-val]
(let [old-file-id (:current-file-id old-val)
new-file-id (:current-file-id new-val)]
(if (and (some? old-file-id) (nil? new-file-id))
(str old-file-id)
::not-changed)))
(defmethod handle-state-change "filechange"
[_ old-val new-val]
(let [old-file (:workspace-file old-val)
@ -72,3 +80,6 @@
;; return the generated key
key))
(defn remove-listener
[key]
(remove-watch st/state key))

View file

@ -7,9 +7,11 @@
(ns app.plugins.library
"RPC for plugins runtime."
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.record :as cr]
[app.main.data.workspace.libraries :as dwl]
[app.main.store :as st]
[app.plugins.utils :as u]))
@ -53,13 +55,32 @@
{:name "id" :get (fn [_] (dm/str id))}
{:name "name"
:get #(-> % u/proxy->library-color :name)}
:get #(-> % u/proxy->library-color :name)
:set
(fn [_ value]
(if (and (some? value) (string? value))
(st/emit! (dwl/rename-color file-id id value))
(u/display-not-valid :library-color-name value)))}
{:name "color"
:get #(-> % u/proxy->library-color :color)}
:get #(-> % u/proxy->library-color :color)
:set
(fn [self value]
(if (and (some? value) (string? value) (cc/valid-hex-color? value))
(let [color (-> (u/proxy->library-color self)
(assoc :color value))]
(st/emit! (dwl/update-color color file-id)))
(u/display-not-valid :library-color-color value)))}
{:name "opacity"
:get #(-> % u/proxy->library-color :opacity)}
:get #(-> % u/proxy->library-color :opacity)
:set
(fn [self value]
(if (and (some? value) (number? value) (>= value 0) (<= value 1))
(let [color (-> (u/proxy->library-color self)
(assoc :opacity value))]
(st/emit! (dwl/update-color color file-id)))
(u/display-not-valid :library-color-color value)))}
{:name "gradient"
:get #(-> % u/proxy->library-color :gradient u/to-js)}
@ -96,8 +117,7 @@
{:name "$id" :enumerable false :get (constantly id)}
{:name "$file" :enumerable false :get (constantly file-id)}
{:name "id" :get (fn [_] (dm/str id))}
{:name "name"
:get #(-> % u/proxy->library-component :name)}))
{:name "name" :get #(-> % u/proxy->library-component :name)}))
(deftype Library [$id]
Object)

View file

@ -28,8 +28,9 @@
(findShapes
[_]
;; Returns a lazy (iterable) of all available shapes
(let [page (locate-page $file $id)]
(apply array (sequence (map shape/shape-proxy) (keys (:objects page)))))))
(when (and (some? $file) (some? $id))
(let [page (locate-page $file $id)]
(apply array (sequence (map shape/shape-proxy) (keys (:objects page))))))))
(crc/define-properties!
PageProxy

View file

@ -7,6 +7,7 @@
(ns app.plugins.shape
"RPC for plugins runtime."
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.files.helpers :as cfh]
[app.common.record :as crc]
@ -14,19 +15,30 @@
[app.common.text :as txt]
[app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl]
[app.common.types.shape.radius :as ctsr]
[app.common.uuid :as uuid]
[app.main.data.workspace :as udw]
[app.main.data.workspace.changes :as dwc]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.texts :as dwt]
[app.main.store :as st]
[app.plugins.flex :as flex]
[app.plugins.grid :as grid]
[app.plugins.utils :as utils :refer [locate-objects locate-shape proxy->shape array-to-js]]
[app.util.object :as obj]))
[app.util.object :as obj]
[app.util.text-editor :as ted]))
(declare shape-proxy)
(defn text-props
[shape]
(d/merge
(dwt/current-root-values {:shape shape :attrs txt/root-attrs})
(dwt/current-paragraph-values {:shape shape :attrs txt/paragraph-attrs})
(dwt/current-text-values {:shape shape :attrs txt/text-node-attrs})))
(deftype ShapeProxy [$file $page $id]
Object
(resize
@ -145,37 +157,52 @@
{:name "borderRadius"
:get #(-> % proxy->shape :rx)
:set (fn [self value]
(let [id (obj/get self "$id")]
(let [id (obj/get self "$id")
shape (proxy->shape self)]
(when (us/safe-int? value)
(st/emit! (dwc/update-shapes [id] #(assoc % :rx value :ry value))))))}
(when (or (not (ctsr/has-radius? shape)) (ctsr/radius-4? shape))
(st/emit! (dwc/update-shapes [id] ctsr/switch-to-radius-1)))
(st/emit! (dwc/update-shapes [id] #(ctsr/set-radius-1 % value))))))}
{:name "borderRadiusTopLeft"
:get #(-> % proxy->shape :r1)
:set (fn [self value]
(let [id (obj/get self "$id")]
(let [id (obj/get self "$id")
shape (proxy->shape self)]
(when (us/safe-int? value)
(st/emit! (dwc/update-shapes [id] #(assoc % :r1 value))))))}
(when (or (not (ctsr/has-radius? shape)) (not (ctsr/radius-4? shape)))
(st/emit! (dwc/update-shapes [id] ctsr/switch-to-radius-4)))
(st/emit! (dwc/update-shapes [id] #(ctsr/set-radius-4 % :r1 value))))))}
{:name "borderRadiusTopRight"
:get #(-> % proxy->shape :r2)
:set (fn [self value]
(let [id (obj/get self "$id")]
(let [id (obj/get self "$id")
shape (proxy->shape self)]
(when (us/safe-int? value)
(st/emit! (dwc/update-shapes [id] #(assoc % :r2 value))))))}
(when (or (not (ctsr/has-radius? shape)) (not (ctsr/radius-4? shape)))
(st/emit! (dwc/update-shapes [id] ctsr/switch-to-radius-4)))
(st/emit! (dwc/update-shapes [id] #(ctsr/set-radius-4 % :r2 value))))))}
{:name "borderRadiusBottomRight"
:get #(-> % proxy->shape :r3)
:set (fn [self value]
(let [id (obj/get self "$id")]
(let [id (obj/get self "$id")
shape (proxy->shape self)]
(when (us/safe-int? value)
(st/emit! (dwc/update-shapes [id] #(assoc % :r3 value))))))}
(when (or (not (ctsr/has-radius? shape)) (not (ctsr/radius-4? shape)))
(st/emit! (dwc/update-shapes [id] ctsr/switch-to-radius-4)))
(st/emit! (dwc/update-shapes [id] #(ctsr/set-radius-4 % :r3 value))))))}
{:name "borderRadiusBottomLeft"
:get #(-> % proxy->shape :r4)
:set (fn [self value]
(let [id (obj/get self "$id")]
(let [id (obj/get self "$id")
shape (proxy->shape self)]
(when (us/safe-int? value)
(st/emit! (dwc/update-shapes [id] #(assoc % :r4 value))))))}
(when (or (not (ctsr/has-radius? shape)) (not (ctsr/radius-4? shape)))
(st/emit! (dwc/update-shapes [id] ctsr/switch-to-radius-4)))
(st/emit! (dwc/update-shapes [id] #(ctsr/set-radius-4 % :r4 value))))))}
{:name "opacity"
:get #(-> % proxy->shape :opacity)
@ -196,15 +223,35 @@
:get #(-> % proxy->shape :shadow array-to-js)
:set (fn [self value]
(let [id (obj/get self "$id")
value (mapv #(utils/from-js %) value)]
(st/emit! (dwc/update-shapes [id] #(assoc % :shadows value)))))}
value (mapv (fn [val]
;; Merge default shadow properties
(d/patch-object
{:id (uuid/next)
:style :drop-shadow
:color {:color clr/black :opacity 0.2}
:offset-x 4
:offset-y 4
:blur 4
:spread 0
:hidden false}
(utils/from-js val #{:style :type})))
value)]
(st/emit! (dwc/update-shapes [id] #(assoc % :shadow value)))))}
{:name "blur"
:get #(-> % proxy->shape :blur utils/to-js)
:set (fn [self value]
(let [id (obj/get self "$id")
value (utils/from-js value)]
(st/emit! (dwc/update-shapes [id] #(assoc % :blur value)))))}
(if (nil? value)
(st/emit! (dwc/update-shapes [id] #(dissoc % :blur)))
(let [id (obj/get self "$id")
value
(d/patch-object
{:id (uuid/next)
:type :layer-blur
:value 4
:hidden false}
(utils/from-js value))]
(st/emit! (dwc/update-shapes [id] #(assoc % :blur value))))))}
{:name "exports"
:get #(-> % proxy->shape :exports array-to-js)
@ -301,7 +348,9 @@
;; Strokes and fills
{:name "fills"
:get #(-> % proxy->shape :fills array-to-js)
:get #(if (cfh/text-shape? data)
(-> % proxy->shape text-props :fills array-to-js)
(-> % proxy->shape :fills array-to-js))
:set (fn [self value]
(let [id (obj/get self "$id")
value (mapv #(utils/from-js %) value)]
@ -311,7 +360,7 @@
:get #(-> % proxy->shape :strokes array-to-js)
:set (fn [self value]
(let [id (obj/get self "$id")
value (mapv #(utils/from-js %) value)]
value (mapv #(utils/from-js % #{:stroke-style :stroke-alignment}) value)]
(st/emit! (dwc/update-shapes [id] #(assoc % :strokes value)))))}
{:name "layoutChild"
@ -397,18 +446,93 @@
(obj/unset! "addFlexLayout")))
(cond-> (cfh/text-shape? data)
(-> (crc/add-properties!
{:name "characters"
:get #(-> % proxy->shape :content txt/content->text)
:set (fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwc/update-shapes [id] #(txt/change-text % value)))))})
(crc/add-properties!
{:name "characters"
:get #(-> % proxy->shape :content txt/content->text)
:set
(fn [self value]
(let [id (obj/get self "$id")]
;; The user is currently editing the text. We need to update the
;; editor as well
(when (contains? (:workspace-editor-state @st/state) id)
(let [shape (proxy->shape self)
editor
(-> shape
(txt/change-text value)
:content
ted/import-content
ted/create-editor-state)]
(st/emit! (dwt/update-editor-state shape editor))))
(st/emit! (dwc/update-shapes [id] #(txt/change-text % value)))))}
(crc/add-properties!
{:name "growType"
:get #(-> % proxy->shape :grow-type d/name)
:set (fn [self value]
(let [id (obj/get self "$id")
value (keyword value)]
(when (contains? #{:auto-width :auto-height :fixed} value)
(st/emit! (dwc/update-shapes [id] #(assoc % :grow-type value))))))})))))))
{:name "growType"
:get #(-> % proxy->shape :grow-type d/name)
:set
(fn [self value]
(let [id (obj/get self "$id")
value (keyword value)]
(when (contains? #{:auto-width :auto-height :fixed} value)
(st/emit! (dwc/update-shapes [id] #(assoc % :grow-type value))))))}
{:name "fontId"
:get #(-> % proxy->shape text-props :font-id)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:font-id value}))))}
{:name "fontFamily"
:get #(-> % proxy->shape text-props :font-family)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:font-id value}))))}
{:name "fontVariantId"
:get #(-> % proxy->shape text-props :font-variant-id)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:font-id value}))))}
{:name "fontSize"
:get #(-> % proxy->shape text-props :font-size)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:font-id value}))))}
{:name "fontWeight"
:get #(-> % proxy->shape text-props :font-weight)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:font-id value}))))}
{:name "fontStyle"
:get #(-> % proxy->shape text-props :font-style)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:font-style value}))))}
{:name "lineHeight"
:get #(-> % proxy->shape text-props :line-height)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:line-height value}))))}
{:name "letterSpacing"
:get #(-> % proxy->shape text-props :letter-spacing)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:letter-spacing value}))))}
{:name "textTransform"
:get #(-> % proxy->shape text-props :text-transform)
:set
(fn [self value]
(let [id (obj/get self "$id")]
(st/emit! (dwt/update-attrs id {:text-transform value}))))}))))))

View file

@ -56,38 +56,44 @@
(defn proxy->file
[proxy]
(let [id (obj/get proxy "$id")]
(locate-file id)))
(when (some? id)
(locate-file id))))
(defn proxy->page
[proxy]
(let [file-id (obj/get proxy "$file")
id (obj/get proxy "$id")]
(locate-page file-id id)))
(when (and (some? file-id) (some? id))
(locate-page file-id id))))
(defn proxy->shape
[proxy]
(let [file-id (obj/get proxy "$file")
page-id (obj/get proxy "$page")
id (obj/get proxy "$id")]
(locate-shape file-id page-id id)))
(when (and (some? file-id) (some? page-id) (some? id))
(locate-shape file-id page-id id))))
(defn proxy->library-color
[proxy]
(let [file-id (obj/get proxy "$file")
id (obj/get proxy "$id")]
(locate-library-color file-id id)))
(when (and (some? file-id) (some? id))
(locate-library-color file-id id))))
(defn proxy->library-typography
[proxy]
(let [file-id (obj/get proxy "$file")
id (obj/get proxy "$id")]
(locate-library-color file-id id)))
(when (and (some? file-id) (some? id))
(locate-library-color file-id id))))
(defn proxy->library-component
[proxy]
(let [file-id (obj/get proxy "$file")
id (obj/get proxy "$id")]
(locate-library-color file-id id)))
(when (and (some? file-id) (some? id))
(locate-library-color file-id id))))
(defn get-data
([self attr]
@ -118,30 +124,32 @@
(defn from-js
"Converts the object back to js"
[obj]
(when (some? obj)
(let [process-node
(fn process-node [node]
(reduce-kv
(fn [m k v]
(let [k (keyword (str/kebab k))
v (cond (map? v)
(process-node v)
([obj]
(from-js obj #{:type}))
([obj keyword-keys]
(when (some? obj)
(let [process-node
(fn process-node [node]
(reduce-kv
(fn [m k v]
(let [k (keyword (str/kebab k))
v (cond (map? v)
(process-node v)
(vector? v)
(mapv process-node v)
(vector? v)
(mapv process-node v)
(and (string? v) (re-matches us/uuid-rx v))
(uuid/uuid v)
(and (string? v) (re-matches us/uuid-rx v))
(uuid/uuid v)
(= k :type)
(keyword v)
(contains? keyword-keys k)
(keyword v)
:else v)]
(assoc m k v)))
{}
node))]
(process-node (js->clj obj)))))
:else v)]
(assoc m k v)))
{}
node))]
(process-node (js->clj obj))))))
(defn to-js
"Converts to javascript an camelize the keys"
@ -180,3 +188,7 @@
(remove-watch ret-v ::watcher)
(resolve value)))))]
[ret-v ret-p]))
(defn display-not-valid
[code value]
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code)))