diff --git a/README.md b/README.md index dff4de3838..4fcbb7c0b4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ # PENPOT # +We’re excited to share that Uxbox is now Penpot! We’re changing the name, but keeping the same project essence. Stay in the loop for more news comming early 2021. Alpha release is close! + ![PENPOT](https://raw.githubusercontent.com/penpot/penpot/develop/docs/screenshot.png) @@ -18,8 +20,6 @@ currently at an early development stage but we are working hard to bring you the beta version as soon as possible. Follow the project progress in Twitter or Github and stay tuned! -[See SVG specification](https://www.w3.org/Graphics/SVG/) - ## SVG based ## @@ -27,6 +27,7 @@ Penpot works with SVG, a standard format, for all your designs and prototypes . This means that all your stuff in Penpot is portable and editable in many other vector tools and easy to use on the web. +[See SVG specification](https://www.w3.org/Graphics/SVG/) ## Contributing ## diff --git a/backend/resources/emails/change-email/en.html b/backend/resources/emails/change-email/en.html index 563fae9355..7aa3fd6136 100644 --- a/backend/resources/emails/change-email/en.html +++ b/backend/resources/emails/change-email/en.html @@ -204,7 +204,7 @@ -
The UXBOX team.
+
The Penpot team.
@@ -250,7 +250,7 @@
-
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
+
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
@@ -310,7 +310,7 @@ @@ -350,7 +350,7 @@
- +
@@ -370,7 +370,7 @@
- +
@@ -450,7 +450,7 @@
- +
-
UXBOX © 2020 | Made with <3 and Open Source
+
Penpot © 2020 | Made with <3 and Open Source
diff --git a/backend/resources/emails/invite-to-team/en.html b/backend/resources/emails/invite-to-team/en.html index 3a77a7a6ec..0f85d5a463 100644 --- a/backend/resources/emails/invite-to-team/en.html +++ b/backend/resources/emails/invite-to-team/en.html @@ -194,7 +194,7 @@ -
The UXBOX team.
+
The Penpot team.
@@ -240,7 +240,7 @@
-
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
+
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
@@ -300,7 +300,7 @@ @@ -340,7 +340,7 @@
- +
@@ -360,7 +360,7 @@
- +
@@ -440,7 +440,7 @@
- +
-
UXBOX © 2020 | Made with <3 and Open Source
+
Penpot © 2020 | Made with <3 and Open Source
diff --git a/backend/resources/emails/password-recovery/en.html b/backend/resources/emails/password-recovery/en.html index 820caf219e..2262325079 100644 --- a/backend/resources/emails/password-recovery/en.html +++ b/backend/resources/emails/password-recovery/en.html @@ -199,7 +199,7 @@ -
The UXBOX team.
+
The Penpot team.
@@ -245,7 +245,7 @@
-
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
+
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
@@ -305,7 +305,7 @@ @@ -345,7 +345,7 @@
- +
@@ -365,7 +365,7 @@
- +
@@ -445,7 +445,7 @@
- +
-
UXBOX © 2020 | Made with <3 and Open Source
+
Penpot © 2020 | Made with <3 and Open Source
diff --git a/backend/resources/emails/register/en.html b/backend/resources/emails/register/en.html index 3f49393ddd..dd2f7a69fe 100644 --- a/backend/resources/emails/register/en.html +++ b/backend/resources/emails/register/en.html @@ -173,7 +173,7 @@ -
Thanks for signing up for your UXBOX account! Please verify your email using the link below adn get started building mockups and prototypes today!
+
Thanks for signing up for your Penpot account! Please verify your email using the link below adn get started building mockups and prototypes today!
@@ -194,7 +194,7 @@ -
The UXBOX team.
+
The Penpot team.
@@ -240,7 +240,7 @@
-
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
+
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
@@ -300,7 +300,7 @@ @@ -340,7 +340,7 @@
- +
@@ -360,7 +360,7 @@
- +
@@ -440,7 +440,7 @@
- +
-
UXBOX © 2020 | Made with <3 and Open Source
+
Penpot © 2020 | Made with <3 and Open Source
diff --git a/backend/scripts/build.sh b/backend/scripts/build.sh index 8da54b34d4..2be3d8d2f1 100755 --- a/backend/scripts/build.sh +++ b/backend/scripts/build.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash CLASSPATH=`(clojure -Spath)` -NEWCP="./resources:./main:./common" +NEWCP="./main:./common" rm -rf ./target/dist mkdir -p ./target/dist/deps @@ -16,6 +16,7 @@ done cp ./resources/log4j2-bundle.xml ./target/dist/log4j2.xml cp -r ./src ./target/dist/main +cp -r ./resources/emails ./target/dist/main/ cp -r ../common ./target/dist/common echo $NEWCP > ./target/dist/classpath; diff --git a/backend/src/app/cli/fixtures.clj b/backend/src/app/cli/fixtures.clj index ee71ad267b..5e5c9f07fd 100644 --- a/backend/src/app/cli/fixtures.clj +++ b/backend/src/app/cli/fixtures.clj @@ -83,7 +83,7 @@ :fullname (str "Profile " index) :password "123123" :demo? true - :email (str "profile" index ".test@uxbox.io")}) + :email (str "profile" index ".test@penpot.app")}) team-id (:default-team-id prof) owner-id id] (let [project-ids (collect (partial create-project conn team-id owner-id) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 68dcd5bc3c..9a6802d067 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -22,24 +22,24 @@ (def defaults {:http-server-port 6060 :http-server-cors "http://localhost:3449" - :database-uri "postgresql://127.0.0.1/uxbox" - :database-username "uxbox" - :database-password "uxbox" + :database-uri "postgresql://127.0.0.1/penpot" + :database-username "penpot" + :database-password "penpot" :secret-key "default" :media-directory "resources/public/media" :assets-directory "resources/public/static" :public-uri "http://localhost:3449/" - :redis-uri "redis://redis/0" + :redis-uri "redis://localhost/0" :media-uri "http://localhost:3449/media/" :assets-uri "http://localhost:3449/static/" :image-process-max-threads 2 - :sendmail-backend "console" - :sendmail-reply-to "no-reply@example.com" - :sendmail-from "no-reply@example.com" + :smtp-enabled false + :smtp-default-reply-to "no-reply@example.com" + :smtp-default-from "no-reply@example.com" :allow-demo-users true :registration-enabled true @@ -79,13 +79,12 @@ (s/def ::media-uri ::us/string) (s/def ::media-directory ::us/string) (s/def ::secret-key ::us/string) -(s/def ::sendmail-backend ::us/string) -(s/def ::sendmail-backend-apikey ::us/string) -(s/def ::sendmail-reply-to ::us/email) -(s/def ::sendmail-from ::us/email) +(s/def ::smtp-enabled ::us/boolean) +(s/def ::smtp-default-reply-to ::us/email) +(s/def ::smtp-default-from ::us/email) (s/def ::smtp-host ::us/string) (s/def ::smtp-port ::us/integer) -(s/def ::smtp-user (s/nilable ::us/string)) +(s/def ::smtp-username (s/nilable ::us/string)) (s/def ::smtp-password (s/nilable ::us/string)) (s/def ::smtp-tls ::us/boolean) (s/def ::smtp-ssl ::us/boolean) @@ -127,6 +126,7 @@ ::gitlab-client-id ::gitlab-client-secret ::gitlab-base-uri + ::redis-uri ::public-uri ::database-username ::database-password @@ -136,13 +136,12 @@ ::media-directory ::media-uri ::secret-key - ::sendmail-reply-to - ::sendmail-from - ::sendmail-backend - ::sendmail-backend-apikey + ::smtp-default-from + ::smtp-default-reply-to + ::smtp-enabled ::smtp-host ::smtp-port - ::smtp-user + ::smtp-username ::smtp-password ::smtp-tls ::smtp-ssl @@ -170,8 +169,8 @@ (reduce-kv (fn [acc k v] (cond-> acc - (str/starts-with? (name k) "uxbox-") - (assoc (keyword (subs (name k) 6)) v) + (str/starts-with? (name k) "penpot-") + (assoc (keyword (subs (name k) 7)) v) (str/starts-with? (name k) "app-") (assoc (keyword (subs (name k) 4)) v))) @@ -188,7 +187,7 @@ [env] (assoc (read-config env) :redis-uri "redis://redis/1" - :database-uri "postgresql://postgres/uxbox_test" + :database-uri "postgresql://postgres/penpot_test" :media-directory "/tmp/app/media" :assets-directory "/tmp/app/static" :migrations-verbose false)) @@ -198,3 +197,15 @@ (def default-deletion-delay (dt/duration {:hours 48})) + +(defn smtp + [cfg] + {:host (:smtp-host cfg "localhost") + :port (:smtp-port cfg 25) + :default-reply-to (:smtp-default-reply-to cfg) + :default-from (:smtp-default-from cfg) + :tls (:smtp-tls cfg) + :enabled (:smtp-enabled cfg) + :username (:smtp-username cfg) + :password (:smtp-password cfg)}) + diff --git a/backend/src/app/emails.clj b/backend/src/app/emails.clj index 9bef19b2ee..5d471add07 100644 --- a/backend/src/app/emails.clj +++ b/backend/src/app/emails.clj @@ -29,25 +29,20 @@ ;; --- Public API (defn render - [email context] - (let [defaults {:from (:sendmail-from cfg/config) - :reply-to (:sendmail-reply-to cfg/config)}] - (email (merge defaults context)))) + [email-factory context] + (email-factory context)) (defn send! "Schedule the email for sending." - ([email context] (send! db/pool email context)) - ([conn email-factory context] - (us/verify fn? email-factory) - (us/verify map? context) - (let [defaults {:from (:sendmail-from cfg/config) - :reply-to (:sendmail-reply-to cfg/config)} - data (merge defaults context) - email (email-factory data)] - (tasks/submit! conn {:name "sendmail" - :delay 0 - :priority 200 - :props email})))) + [conn email-factory context] + (us/verify fn? email-factory) + (us/verify map? context) + (let [email (email-factory context)] + (tasks/submit! conn {:name "sendmail" + :delay 0 + :max-retries 1 + :priority 200 + :props email}))) ;; --- Emails @@ -57,7 +52,7 @@ (def register "A new profile registration welcome email." - (emails/build ::register default-context)) + (emails/template-factory ::register default-context)) (s/def ::token ::us/string) (s/def ::password-recovery @@ -65,7 +60,7 @@ (def password-recovery "A password recovery notification email." - (emails/build ::password-recovery default-context)) + (emails/template-factory ::password-recovery default-context)) (s/def ::pending-email ::us/email) (s/def ::change-email @@ -73,7 +68,7 @@ (def change-email "Password change confirmation email" - (emails/build ::change-email default-context)) + (emails/template-factory ::change-email default-context)) (s/def :internal.emails.invite-to-team/invited-by ::us/string) (s/def :internal.emails.invite-to-team/team ::us/string) @@ -86,4 +81,4 @@ (def invite-to-team "Teams member invitation email." - (emails/build ::invite-to-team default-context)) + (emails/template-factory ::invite-to-team default-context)) diff --git a/backend/src/app/services/queries/files.clj b/backend/src/app/services/queries/files.clj index 0aa6b134d2..8fa7106ec0 100644 --- a/backend/src/app/services/queries/files.clj +++ b/backend/src/app/services/queries/files.clj @@ -247,6 +247,7 @@ (def ^:private sql:file-libraries "select fl.*, + ? as is_indirect, flr.synced_at as synced_at from file as fl inner join file_library_rel as flr on (flr.library_file_id = fl.id) @@ -254,8 +255,23 @@ and fl.deleted_at is null") (defn retrieve-file-libraries - [conn file-id] - (into [] decode-row-xf (db/exec! conn [sql:file-libraries file-id]))) + [conn is-indirect file-id] + (let [direct-libraries + (into [] decode-row-xf (db/exec! conn [sql:file-libraries is-indirect file-id])) + + select-distinct + (fn [used-libraries new-libraries] + (remove (fn [new-library] + (some #(= (:id %) (:id new-library)) used-libraries)) + new-libraries))] + + (reduce (fn [used-libraries library] + (concat used-libraries + (select-distinct + used-libraries + (retrieve-file-libraries conn true (:id library))))) + direct-libraries + direct-libraries))) (s/def ::file-libraries (s/keys :req-un [::profile-id ::file-id])) @@ -264,7 +280,7 @@ [{:keys [profile-id file-id] :as params}] (db/with-atomic [conn db/pool] (check-edition-permissions! conn profile-id file-id) - (retrieve-file-libraries conn file-id))) + (retrieve-file-libraries conn false file-id))) ;; --- Query: Single File Library diff --git a/backend/src/app/services/queries/viewer.clj b/backend/src/app/services/queries/viewer.clj index ffcff177ae..bfd5b9aaee 100644 --- a/backend/src/app/services/queries/viewer.clj +++ b/backend/src/app/services/queries/viewer.clj @@ -49,9 +49,12 @@ project (retrieve-project conn (:project-id file)) page (get-in file [:data :pages-index page-id]) - bundle {:file (dissoc file :data) + file-library (select-keys (:data file) [:colors :media :typographies]) + bundle {:file (-> (dissoc file :data) + (merge file-library)) :page (get-in file [:data :pages-index page-id]) - :project project}] + :project project} + ] (if (string? share-token) (do (check-shared-token! conn file-id page-id share-token) diff --git a/backend/src/app/tasks/sendmail.clj b/backend/src/app/tasks/sendmail.clj index ea16ed33bf..54706b487e 100644 --- a/backend/src/app/tasks/sendmail.clj +++ b/backend/src/app/tasks/sendmail.clj @@ -9,91 +9,32 @@ (ns app.tasks.sendmail (:require - [clojure.data.json :as json] [clojure.tools.logging :as log] - [postal.core :as postal] [app.common.data :as d] [app.common.exceptions :as ex] + [app.util.emails :as emails] [app.config :as cfg] - [app.metrics :as mtx] - [app.util.http :as http])) + [app.metrics :as mtx])) -(defmulti sendmail (fn [config email] (:sendmail-backend config))) - -(defmethod sendmail "console" +(defn- send-console! [config email] - (let [out (with-out-str - (println "email console dump:") - (println "******** start email" (:id email) "**********") - (println " from: " (:from email)) - (println " to: " (:to email "---")) - (println " reply-to: " (:reply-to email)) - (println " subject: " (:subject email)) - (println " content:") - (doseq [item (:content email)] - (when (= (:type item) "text/plain") - (println (:value item)))) - (println "******** end email "(:id email) "**********"))] - (log/info out))) - -(defmethod sendmail "sendgrid" - [config email] - (let [apikey (:sendmail-backend-apikey config) - dest (mapv #(array-map :email %) (:to email)) - params {:personalizations [{:to dest - :subject (:subject email)}] - :from {:email (:from email)} - :reply_to {:email (:reply-to email)} - :content (:content email)} - headers {"Authorization" (str "Bearer " apikey) - "Content-Type" "application/json"} - body (json/write-str params)] - - - (try - (let [response (http/send! {:method :post - :headers headers - :uri "https://api.sendgrid.com/v3/mail/send" - :body body})] - (when-not (= 202 (:status response)) - (log/error "Unexpected status from sendgrid:" (pr-str response)))) - (catch Throwable error - (log/error "Error on sending email to sendgrid:" (pr-str error)))))) - -(defn- get-smtp-config - [config] - {:host (:smtp-host config) - :port (:smtp-port config) - :user (:smtp-user config) - :pass (:smtp-password config) - :ssl (:smtp-ssl config) - :tls (:smtp-tls config)}) - -(defn- email->postal - [email] - {:from (:from email) - :to (:to email) - :subject (:subject email) - :body (d/concat [:alternative] - (map (fn [{:keys [type value]}] - {:type (str type "; charset=utf-8") - :content value}) - (:content email)))}) - -(defmethod sendmail "smtp" - [config email] - (let [config (get-smtp-config config) - email (email->postal email) - result (postal/send-message config email)] - (when (not= (:error result) :SUCCESS) - (ex/raise :type :sendmail-error - :code :email-not-sent - :context result)))) + (let [baos (java.io.ByteArrayOutputStream.) + mesg (emails/smtp-message config email)] + (.writeTo mesg baos) + (let [out (with-out-str + (println "email console dump:") + (println "******** start email" (:id email) "**********") + (println (.toString baos)) + (println "******** end email "(:id email) "**********"))] + (log/info out)))) (defn handler {:app.tasks/name "sendmail"} [{:keys [props] :as task}] - (sendmail cfg/config props)) + (let [config (cfg/smtp cfg/config)] + (if (:enabled config) + (emails/send! config props) + (send-console! config props)))) (mtx/instrument-with-summary! {:var #'handler diff --git a/backend/src/app/util/emails.clj b/backend/src/app/util/emails.clj index d962b4be49..2558c52077 100644 --- a/backend/src/app/util/emails.clj +++ b/backend/src/app/util/emails.clj @@ -2,7 +2,10 @@ ;; 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) 2019 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2019-2020 Andrey Antukh (ns app.util.emails (:require @@ -11,27 +14,187 @@ [cuerdas.core :as str] [app.common.spec :as us] [app.common.exceptions :as ex] - [app.util.template :as tmpl])) + [app.util.template :as tmpl]) + (:import + java.util.Properties + javax.mail.Message + javax.mail.Transport + javax.mail.Message$RecipientType + javax.mail.PasswordAuthentication + javax.mail.Session + javax.mail.internet.InternetAddress + javax.mail.internet.MimeMultipart + javax.mail.internet.MimeBodyPart + javax.mail.internet.MimeMessage)) -;; --- Impl. +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Email Building +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn build-address + [v charset] + (try + (cond + (string? v) + (InternetAddress. v nil charset) + + (map? v) + (InternetAddress. (:addr v) + (:name v) + (:charset v charset)) + + :else + (throw (ex-info "Invalid address" {:data v}))) + (catch Exception e + (throw (ex-info "Invalid address" {:data v} e))))) + +(defn- resolve-recipient-type + [type] + (case type + :to Message$RecipientType/TO + :cc Message$RecipientType/CC + :bcc Message$RecipientType/BCC)) + +(defn- assign-recipient + [^MimeMessage mmsg type address charset] + (if (sequential? address) + (reduce #(assign-recipient %1 type %2 charset) mmsg address) + (let [address (build-address address charset) + type (resolve-recipient-type type)] + (.addRecipient mmsg type address) + mmsg))) + +(defn- assign-recipients + [mmsg {:keys [to cc bcc charset] :or {charset "utf-8"} :as params}] + (cond-> mmsg + (some? to) (assign-recipient :to to charset) + (some? cc) (assign-recipient :cc cc charset) + (some? bcc) (assign-recipient :bcc bcc charset))) + +(defn- assign-from + [mmsg {:keys [from charset] :or {charset "utf-8"}}] + (when from + (let [from (build-address from charset)] + (.setFrom ^MimeMessage mmsg ^InternetAddress from)))) + +(defn- assign-reply-to + [mmsg {:keys [defaut-reply-to]} {:keys [reply-to charset] :or {charset "utf-8"}}] + (let [reply-to (or reply-to defaut-reply-to)] + (when reply-to + (let [reply-to (build-address reply-to charset) + reply-to (into-array InternetAddress [reply-to])] + (.setReplyTo ^MimeMessage mmsg reply-to))))) + +(defn- assign-subject + [mmsg {:keys [subject charset] :or {charset "utf-8"}}] + (assert (string? subject) "subject is mandatory") + (.setSubject ^MimeMessage mmsg + ^String subject + ^String charset)) + +(defn- assign-extra-headers + [^MimeMessage mmsg {:keys [headers custom-data] :as params}] + (let [headers (assoc headers "X-Sereno-Custom-Data" custom-data)] + (reduce-kv (fn [^MimeMessage mmsg k v] + (doto mmsg + (.addHeader (name k) (str v)))) + mmsg + headers))) + +(defn- assign-body + [^MimeMessage mmsg {:keys [body charset] :or {charset "utf-8"}}] + (let [mpart (MimeMultipart. "mixed")] + (cond + (string? body) + (let [bpart (MimeBodyPart.)] + (.setContent bpart ^String body (str "text/plain; charset=" charset)) + (.addBodyPart mpart bpart)) + + (vector? body) + (let [mmp (MimeMultipart. "alternative") + mbp (MimeBodyPart.)] + (.addBodyPart mpart mbp) + (.setContent mbp mmp) + (doseq [item body] + (let [mbp (MimeBodyPart.)] + (.setContent mbp + ^String (:content item) + ^String (str (:type item "text/plain") "; charset=" charset)) + (.addBodyPart mmp mbp)))) + + (map? body) + (let [bpart (MimeBodyPart.)] + (.setContent bpart + ^String (:content body) + ^String (str (:type body "text/plain") "; charset=" charset)) + (.addBodyPart mpart bpart)) + + :else + (throw (ex-info "Unsupported type" {:body body}))) + (.setContent mmsg mpart) + mmsg)) + +(defn- build-message + [cfg session params] + (let [mmsg (MimeMessage. ^Session session)] + (assign-recipients mmsg params) + (assign-from mmsg params) + (assign-reply-to mmsg cfg params) + (assign-subject mmsg params) + (assign-extra-headers mmsg params) + (assign-body mmsg params) + (.saveChanges mmsg) + mmsg)) + +(defn- opts->props + [{:keys [username tls host port timeout default-from] + :or {timeout 30000} + :as opts}] + (reduce-kv + (fn [^Properties props k v] + (if (nil? v) + props + (doto props (.put ^String k ^String (str v))))) + (Properties.) + {"mail.user" username + "mail.host" host + "mail.smtp.auth" (boolean username) + "mail.smtp.starttls.enable" tls + "mail.smtp.starttls.required" tls + "mail.smtp.host" host + "mail.smtp.port" port + "mail.smtp.from" default-from + "mail.smtp.user" username + "mail.smtp.timeout" timeout + "mail.smtp.connectiontimeout" timeout})) + +(defn smtp-session + [{:keys [debug] :or {debug false} :as opts}] + (let [props (opts->props opts) + session (Session/getInstance props)] + (.setDebug session debug) + session)) + +(defn smtp-message + [cfg message] + (let [^Session session (smtp-session cfg)] + (build-message cfg session message))) + +;; TODO: specs for smtp config + +(defn send! + [cfg message] + (let [^MimeMessage message (smtp-message cfg message)] + (Transport/send message (:username cfg) (:password cfg)) + nil)) + +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Template Email Building +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def ^:private email-path "emails/%(id)s/%(lang)s.%(type)s") -(defn- build-base-email - [data context] - (when-not (s/valid? ::parsed-email data) - (ex/raise :type :internal - :code :template-parse-error - :hint "Seems like the email template has invalid data." - :contex data)) - {:subject (:subject data) - :content (cond-> [] - (:body-text data) (conj {:type "text/plain" - :value (:body-text data)}) - (:body-html data) (conj {:type "text/html" - :value (:body-html data)}))}) - -(defn- render-email-part +(defn- render-email-template-part [type id context] (let [lang (:lang context :en) path (str/format email-path {:id (name id) @@ -40,34 +203,37 @@ (some-> (io/resource path) (tmpl/render context)))) -(defn- impl-build-email +(defn- build-email-template [id context] (let [lang (:lang context :en) - subj (render-email-part :subj id context) - html (render-email-part :html id context) - text (render-email-part :txt id context)] - + subj (render-email-template-part :subj id context) + text (render-email-template-part :txt id context) + html (render-email-template-part :html id context)] + (when (or (not subj) + (not text) + (not html)) + (ex/raise :type :internal + :code :missing-email-templates)) {:subject subj - :content (cond-> [] - text (conj {:type "text/plain" - :value text}) - html (conj {:type "text/html" - :value html}))})) - -;; --- Public API + :body [{:type "text/plain" + :content text} + {:type "text/html" + :content html}]})) (s/def ::priority #{:high :low}) -(s/def ::to ::us/email) +(s/def ::to (s/or :sigle ::us/email + :multi (s/coll-of ::us/email))) (s/def ::from ::us/email) (s/def ::reply-to ::us/email) (s/def ::lang string?) +(s/def ::custom-data ::us/string) (s/def ::context (s/keys :req-un [::to] - :opt-un [::reply-to ::from ::lang ::priority])) + :opt-un [::reply-to ::from ::lang ::priority ::custom-data])) -(defn build - ([id] (build id {})) +(defn template-factory + ([id] (template-factory id {})) ([id extra-context] (s/assert keyword? id) (fn [context] @@ -79,13 +245,21 @@ (extra-context) extra-context) context) - email (impl-build-email id context)] + email (build-email-template id context)] (when-not email (ex/raise :type :internal :code :email-template-does-not-exists :hint "seems like the template is wrong or does not exists." - ::id id)) + :context {:id id})) (cond-> (assoc email :id (name id)) - (:to context) (assoc :to [(:to context)]) - (:from context) (assoc :from (:from context)) - (:reply-to context) (assoc :reply-to (:reply-to context))))))) + (:custom-data context) + (assoc :custom-data (:custom-data context)) + + (:from context) + (assoc :from (:from context)) + + (:reply-to context) + (assoc :reply-to (:reply-to context)) + + (:to context) + (assoc :to (:to context))))))) diff --git a/common/app/common/data.cljc b/common/app/common/data.cljc index c938e0b757..3034e369b5 100644 --- a/common/app/common/data.cljc +++ b/common/app/common/data.cljc @@ -136,6 +136,16 @@ [mfn coll] (into {} (map (fn [[key val]] [key (mfn key val)]) coll))) +(defn filterm + "Filter values of a map that satisfy a predicate" + [pred coll] + (into {} (filter pred coll))) + +(defn removem + "Remove values of a map that satisfy a predicate" + [pred coll] + (into {} (remove pred coll))) + (defn map-perm "Maps a function to each pair of values that can be combined inside the function without repetition. diff --git a/common/app/common/geom/shapes.cljc b/common/app/common/geom/shapes.cljc index ed053344f3..b191ed5bf9 100644 --- a/common/app/common/geom/shapes.cljc +++ b/common/app/common/geom/shapes.cljc @@ -542,6 +542,14 @@ (< ry1 sy2) (> ry2 sy1)))) +(defn fully-contained? + "Checks if one rect is fully inside the other" + [rect other] + (and (<= (:x1 rect) (:x1 other)) + (>= (:x2 rect) (:x2 other)) + (<= (:y1 rect) (:y1 other)) + (>= (:y2 rect) (:y2 other)))) + (defn has-point? [shape position] (let [{:keys [x y]} position @@ -956,3 +964,4 @@ :width width :height height :x1 x :y1 y :x2 (+ x width) :y2 (+ y height)}))) + diff --git a/common/app/common/pages.cljc b/common/app/common/pages.cljc index 348d98b47b..c5ad442da7 100644 --- a/common/app/common/pages.cljc +++ b/common/app/common/pages.cljc @@ -523,7 +523,8 @@ (s/keys :req-un [::id ::name :internal.changes.add-component/shapes])) (defmethod change-spec :mod-component [_] - (s/keys :req-un [::id ::name :internal.changes.add-component/shapes])) + (s/keys :req-un [::id] + :opt-un [::name :internal.changes.add-component/shapes])) (defmethod change-spec :del-component [_] (s/keys :req-un [::id])) @@ -962,11 +963,14 @@ :objects (d/index-by :id shapes)})) (defmethod process-change :mod-component - [data {:keys [id name shapes]}] + [data {:keys [id name objects]}] (update-in data [:components id] - #(assoc % - :name name - :objects (d/index-by :id shapes)))) + #(cond-> % + (some? name) + (assoc :name name) + + (some? objects) + (assoc :objects objects)))) (defmethod process-change :del-component [data {:keys [id]}] diff --git a/common/app/common/pages_helpers.cljc b/common/app/common/pages_helpers.cljc index 28f5a3b40a..e9722ec2dc 100644 --- a/common/app/common/pages_helpers.cljc +++ b/common/app/common/pages_helpers.cljc @@ -35,13 +35,34 @@ (defn get-root-shape "Get the root shape linked to a component for this shape, if any" [shape objects] - (if (:component-root? shape) + (if (:component-id shape) shape (if-let [parent-id (:parent-id shape)] (get-root-shape (get objects (:parent-id shape)) objects) nil))) +(defn get-container + [page-id component-id local-file] + (if (some? page-id) + (get-in local-file [:pages-index page-id]) + (get-in local-file [:components component-id]))) + +(defn get-shape + [container shape-id] + (get-in container [:objects shape-id])) + +(defn get-component + [component-id file-id local-library libraries] + (let [file (if (nil? file-id) + local-library + (get-in libraries [file-id :data]))] + (get-in file [:components component-id]))) + +(defn get-component-root + [component] + (get-in component [:objects (:id component)])) + (defn get-children "Retrieve all children ids recursively for a given object" [id objects] diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 6493e6f112..75e04f06f9 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -4,7 +4,7 @@ LABEL maintainer="Andrey Antukh " ARG DEBIAN_FRONTEND=noninteractive ARG EXTERNAL_UID=1000 -ENV NODE_VERSION=v12.19.0 \ +ENV NODE_VERSION=v14.15.0 \ CLOJURE_VERSION=1.10.1.727 \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 @@ -31,9 +31,9 @@ RUN set -ex; \ rm -rf /var/lib/apt/lists/*; RUN set -ex; \ - useradd -m -g users -s /bin/bash -u $EXTERNAL_UID uxbox; \ - passwd uxbox -d; \ - echo "uxbox ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers + useradd -m -g users -s /bin/bash -u $EXTERNAL_UID penpot; \ + passwd penpot -d; \ + echo "penpot ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers RUN set -ex; \ apt-get -qq update; \ @@ -124,8 +124,8 @@ COPY files/start-tmux.sh /home/start-tmux.sh COPY files/entrypoint.sh /home/entrypoint.sh COPY files/init.sh /home/init.sh -USER uxbox -WORKDIR /home/uxbox +USER penpot +WORKDIR /home/penpot RUN set -ex; \ git clone https://github.com/creationix/nvm.git .nvm; \ diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml index bbbbfa40d0..21978864fe 100644 --- a/docker/devenv/docker-compose.yaml +++ b/docker/devenv/docker-compose.yaml @@ -14,20 +14,19 @@ volumes: services: main: privileged: true - image: "uxbox-devenv" + image: "penpot-devenv" build: context: "." - container_name: "uxbox-devenv-main" + container_name: "penpot-devenv-main" stop_signal: SIGINT depends_on: - postgres - - smtp - redis volumes: - - "user_data:/home/uxbox/" - - "${PWD}:/home/uxbox/uxbox" + - "user_data:/home/penpot/" + - "${PWD}:/home/penpot/penpot" - ./files/nginx.conf:/etc/nginx/nginx.conf ports: @@ -39,32 +38,22 @@ services: - 9090:9090 environment: - - APP_DATABASE_URI=postgresql://postgres/uxbox - - APP_DATABASE_USERNAME=uxbox - - APP_DATABASE_PASSWORD=uxbox - - APP_SENDMAIL_BACKEND=console - - APP_SMTP_HOST=smtp - - APP_SMTP_PORT=25 - - smtp: - container_name: "uxbox-devenv-smtp" - image: mwader/postfix-relay:latest - restart: always - environment: - - POSTFIX_myhostname=smtp.uxbox.io - - OPENDKIM_DOMAINS=smtp.uxbox.io + - APP_DATABASE_URI=postgresql://postgres/penpot + - APP_DATABASE_USERNAME=penpot + - APP_DATABASE_PASSWORD=penpot + - APP_REDIS_URI=redis://redis/0 postgres: image: postgres:13 command: postgres -c config_file=/etc/postgresql.conf - container_name: "uxbox-devenv-postgres" + container_name: "penpot-devenv-postgres" restart: always stop_signal: SIGINT environment: - POSTGRES_INITDB_ARGS=--data-checksums - - POSTGRES_DB=uxbox - - POSTGRES_USER=uxbox - - POSTGRES_PASSWORD=uxbox + - POSTGRES_DB=penpot + - POSTGRES_USER=penpot + - POSTGRES_PASSWORD=penpot volumes: - ./files/postgresql.conf:/etc/postgresql.conf - ./files/postgresql_init.sql:/docker-entrypoint-initdb.d/init.sql @@ -72,6 +61,6 @@ services: redis: image: redis:6 - hostname: "uxbox-devenv-redis" - container_name: "uxbox-devenv-redis" + hostname: "penpot-devenv-redis" + container_name: "penpot-devenv-redis" restart: always diff --git a/docker/devenv/files/entrypoint.sh b/docker/devenv/files/entrypoint.sh index bbf4504367..87f47077f4 100755 --- a/docker/devenv/files/entrypoint.sh +++ b/docker/devenv/files/entrypoint.sh @@ -2,11 +2,11 @@ set -e -sudo cp /root/.bashrc /home/uxbox/.bashrc -sudo cp /root/.vimrc /home/uxbox/.vimrc -sudo cp /root/.tmux.conf /home/uxbox/.tmux.conf +sudo cp /root/.bashrc /home/penpot/.bashrc +sudo cp /root/.vimrc /home/penpot/.vimrc +sudo cp /root/.tmux.conf /home/penpot/.tmux.conf -source /home/uxbox/.bashrc -sudo chown uxbox:users /home/uxbox +source /home/penpot/.bashrc +sudo chown penpot:users /home/penpot exec "$@" diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index e59675deb3..2ae400e691 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -60,7 +60,7 @@ http { etag off; location / { - root /home/uxbox/uxbox/frontend/resources/public; + root /home/penpot/penpot/frontend/resources/public; try_files $uri /index.html; add_header Cache-Control "no-cache, max-age=0"; } @@ -74,7 +74,7 @@ http { } location /playground { - alias /home/uxbox/uxbox/experiments/; + alias /home/penpot/penpot/experiments/; add_header Cache-Control "no-cache, max-age=0"; autoindex on; } @@ -86,7 +86,7 @@ http { } location /media { - alias /home/uxbox/uxbox/backend/resources/public/media; + alias /home/penpot/penpot/backend/resources/public/media; } } } diff --git a/docker/devenv/files/postgresql_init.sql b/docker/devenv/files/postgresql_init.sql index 72b25901e3..3f174e897e 100644 --- a/docker/devenv/files/postgresql_init.sql +++ b/docker/devenv/files/postgresql_init.sql @@ -1 +1 @@ -CREATE DATABASE uxbox_test; +CREATE DATABASE penpot_test; diff --git a/docker/devenv/files/start-tmux.sh b/docker/devenv/files/start-tmux.sh index db46596d39..e658069656 100755 --- a/docker/devenv/files/start-tmux.sh +++ b/docker/devenv/files/start-tmux.sh @@ -6,36 +6,36 @@ set -e; source ~/.bashrc echo "[start-tmux.sh] Installing node dependencies" -pushd ~/uxbox/frontend/ +pushd ~/penpot/frontend/ yarn install popd -pushd ~/uxbox/exporter/ +pushd ~/penpot/exporter/ yarn install popd -tmux -2 new-session -d -s uxbox +tmux -2 new-session -d -s penpot -tmux new-window -t uxbox:1 -n 'shadow watch' -tmux select-window -t uxbox:1 -tmux send-keys -t uxbox 'cd uxbox/frontend' enter C-l -tmux send-keys -t uxbox 'npx shadow-cljs watch main' enter +tmux new-window -t penpot:1 -n 'shadow watch' +tmux select-window -t penpot:1 +tmux send-keys -t penpot 'cd penpot/frontend' enter C-l +tmux send-keys -t penpot 'npx shadow-cljs watch main' enter -tmux new-window -t uxbox:2 -n 'exporter' -tmux select-window -t uxbox:2 -tmux send-keys -t uxbox 'cd uxbox/exporter' enter C-l -tmux send-keys -t uxbox 'npx shadow-cljs watch main' enter +tmux new-window -t penpot:2 -n 'exporter' +tmux select-window -t penpot:2 +tmux send-keys -t penpot 'cd penpot/exporter' enter C-l +tmux send-keys -t penpot 'npx shadow-cljs watch main' enter tmux split-window -v -tmux send-keys -t uxbox 'cd uxbox/exporter' enter C-l -tmux send-keys -t uxbox './scripts/wait-and-start.sh' enter +tmux send-keys -t penpot 'cd penpot/exporter' enter C-l +tmux send-keys -t penpot './scripts/wait-and-start.sh' enter -tmux new-window -t uxbox:3 -n 'backend' -tmux select-window -t uxbox:3 -tmux send-keys -t uxbox 'cd uxbox/backend' enter C-l -tmux send-keys -t uxbox './scripts/start-dev' enter +tmux new-window -t penpot:3 -n 'backend' +tmux select-window -t penpot:3 +tmux send-keys -t penpot 'cd penpot/backend' enter C-l +tmux send-keys -t penpot './scripts/start-dev' enter -tmux rename-window -t uxbox:0 'gulp' -tmux select-window -t uxbox:0 -tmux send-keys -t uxbox 'cd uxbox/frontend' enter C-l -tmux send-keys -t uxbox 'npx gulp --theme=${UXBOX_THEME} watch' enter +tmux rename-window -t penpot:0 'gulp' +tmux select-window -t penpot:0 +tmux send-keys -t penpot 'cd penpot/frontend' enter C-l +tmux send-keys -t penpot 'npx gulp --theme=${PENPOT_THEME} watch' enter -tmux -2 attach-session -t uxbox +tmux -2 attach-session -t penpot diff --git a/frontend/.nvmrc b/frontend/.nvmrc index 605427f4ad..55d1782166 100644 --- a/frontend/.nvmrc +++ b/frontend/.nvmrc @@ -1 +1 @@ -v12.18.3 \ No newline at end of file +v14.15.0 diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index ef893ddb8a..4e12b670d9 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -46,7 +46,7 @@ function scssPipeline(options) { const touch = (_path) => { return new Promise((resolve, reject) => { - return fs.utimes(file.path, new Date(), new Date(), () => { + return fs.utimes(_path, new Date(), new Date(), () => { resolve(_path); }); }) @@ -78,7 +78,7 @@ function scssPipeline(options) { .then(() => render(input)) .then((res) => postprocess(res, input, output)) .then(async (res) => { - await write(output, res); + await write(output, res.css); await touch(output); return res; }) @@ -158,19 +158,6 @@ function templatePipeline(options) { const locales = readLocales(); const manifest = readManifest(); - const defaultConf = [ - "var appDemoWarning = null;", - "var appLoginWithLDAP = null;", - "var appPublicURI = null;", - "var appGoogleClientID = null;", - "var appGitlabClientID = null;", - "var appDeployDate = null;", - "var appDeployCommit = null;" - ]; - - fs.writeFileSync(__dirname + "/resources/public/js/config.js", - defaultConf.join("\n")); - const tmpl = mustache({ ts: ts, th: th, diff --git a/frontend/package.json b/frontend/package.json index 1cb1fcb125..c7d4f4b482 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,8 @@ }, "dependencies": { "date-fns": "^2.15.0", + "highlight.js": "^10.3.1", + "js-beautify": "^1.13.0", "map-stream": "0.0.7", "mousetrap": "^1.6.5", "randomcolor": "^0.6.2", diff --git a/frontend/resources/images/email/uxbox-title.png b/frontend/resources/images/email/uxbox-title.png index 4e9cb501f6..45869e7581 100644 Binary files a/frontend/resources/images/email/uxbox-title.png and b/frontend/resources/images/email/uxbox-title.png differ diff --git a/frontend/resources/images/icons/artboard.svg b/frontend/resources/images/icons/artboard.svg index c970eab898..c913973004 100644 --- a/frontend/resources/images/icons/artboard.svg +++ b/frontend/resources/images/icons/artboard.svg @@ -1 +1,3 @@ - + + + diff --git a/frontend/resources/images/icons/layers.svg b/frontend/resources/images/icons/layers.svg index 9c01ea37f8..3968ee0dbe 100644 --- a/frontend/resources/images/icons/layers.svg +++ b/frontend/resources/images/icons/layers.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/frontend/resources/images/icons/library.svg b/frontend/resources/images/icons/library.svg index 1be040dba5..4b5457f7fc 100644 --- a/frontend/resources/images/icons/library.svg +++ b/frontend/resources/images/icons/library.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/frontend/resources/images/icons/palette.svg b/frontend/resources/images/icons/palette.svg index a1f15d6ebf..80ab784018 100644 --- a/frontend/resources/images/icons/palette.svg +++ b/frontend/resources/images/icons/palette.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/frontend/resources/images/icons/pencil.svg b/frontend/resources/images/icons/pencil.svg index 0ef2915b07..8382b124f7 100644 --- a/frontend/resources/images/icons/pencil.svg +++ b/frontend/resources/images/icons/pencil.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/frontend/resources/images/icons/undo-history.svg b/frontend/resources/images/icons/undo-history.svg index 2f9db520a5..42a4cc1c7b 100644 --- a/frontend/resources/images/icons/undo-history.svg +++ b/frontend/resources/images/icons/undo-history.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/frontend/resources/images/icons/uxbox-logo.svg b/frontend/resources/images/icons/uxbox-logo.svg index 3f99aa09dc..4493b6d91f 100644 --- a/frontend/resources/images/icons/uxbox-logo.svg +++ b/frontend/resources/images/icons/uxbox-logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/resources/images/penpot-login.jpg b/frontend/resources/images/penpot-login.jpg index 3c9409fb5c..430b7537aa 100644 Binary files a/frontend/resources/images/penpot-login.jpg and b/frontend/resources/images/penpot-login.jpg differ diff --git a/frontend/resources/images/penpot-login2.jpg b/frontend/resources/images/penpot-login2.jpg new file mode 100644 index 0000000000..3c9409fb5c Binary files /dev/null and b/frontend/resources/images/penpot-login2.jpg differ diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 22187701ba..111dad5212 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -978,6 +978,12 @@ "en" : "Left" } }, + "handoff.attributes.layout.radius" : { + "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/layout.cljs:60" ], + "translations" : { + "en" : "Radius" + } + }, "handoff.attributes.layout.rotation" : { "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/layout.cljs:60" ], "translations" : { @@ -2118,6 +2124,14 @@ "es" : "Borrar" } }, + "workspace.assets.duplicate" : { + "translations" : { + "en" : "Duplicate", + "fr" : "", + "ru" : "", + "es" : "Duplicar" + } + }, "workspace.assets.edit" : { "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:305", "src/app/main/ui/workspace/sidebar/assets.cljs:433" ], "translations" : { @@ -2679,6 +2693,12 @@ "es" : "Color de fondo" } }, + "workspace.options.component" : { + "translations" : { + "en" : "Component", + "es" : "Componente" + } + }, "workspace.options.design" : { "used-in" : [ "src/app/main/ui/workspace/sidebar/options.cljs:69" ], "translations" : { @@ -3427,6 +3447,11 @@ "en" : "Go to master component file" } }, + "workspace.shape.menu.show-master" : { + "translations" : { + "en" : "Show master component" + } + }, "workspace.shape.menu.group" : { "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:110" ], "translations" : { diff --git a/frontend/resources/styles/common/dependencies/highlightjs-theme.scss b/frontend/resources/styles/common/dependencies/highlightjs-theme.scss new file mode 100644 index 0000000000..ba9963ff5d --- /dev/null +++ b/frontend/resources/styles/common/dependencies/highlightjs-theme.scss @@ -0,0 +1,81 @@ +/* +Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/ +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #23241f; +} + +.hljs, +.hljs-tag, +.hljs-subst { + color: #f8f8f2; +} + +.hljs-strong, +.hljs-emphasis { + color: #a8a8a2; +} + +.hljs-bullet, +.hljs-quote, +.hljs-number, +.hljs-regexp, +.hljs-literal, +.hljs-link { + color: #ae81ff; +} + +.hljs-code, +.hljs-title, +.hljs-section, +.hljs-selector-class { + color: #a6e22e; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-name, +.hljs-attr { + color: #f92672; +} + +.hljs-symbol, +.hljs-attribute { + color: #66d9ef; +} + +.hljs-params, +.hljs-class .hljs-title { + color: #f8f8f2; +} + +.hljs-string, +.hljs-type, +.hljs-built_in, +.hljs-builtin-name, +.hljs-selector-id, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-addition, +.hljs-variable, +.hljs-template-variable { + color: #e6db74; +} + +.hljs-comment, +.hljs-deletion, +.hljs-meta { + color: #75715e; +} diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index a438f64642..b8860a4b43 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -19,6 +19,7 @@ @import 'common/dependencies/reset'; @import 'common/dependencies/animations'; @import 'common/dependencies/z-index'; +@import 'common/dependencies/highlightjs-theme'; //################################################# // Layouts diff --git a/frontend/resources/styles/main/layouts/handoff.scss b/frontend/resources/styles/main/layouts/handoff.scss index 2eb5c2548d..bd7e626a1f 100644 --- a/frontend/resources/styles/main/layouts/handoff.scss +++ b/frontend/resources/styles/main/layouts/handoff.scss @@ -1,3 +1,5 @@ +$width-settings-bar: 16rem; + .handoff-layout { display: grid; grid-template-rows: 40px auto; @@ -28,6 +30,42 @@ } } -.handoff-layout .settings-bar.settings-bar-left { - left: 0; +.handoff-layout { + .viewer-preview { + flex-wrap: nowrap; + } + .settings-bar { + transition: width 0.2s; + &.expanded { + width: $width-settings-bar * 3; + } + + &.settings-bar-right, + &.settings-bar-left { + position: relative; + left: unset; + right: unset; + + .settings-bar-inside { + padding-top: 0.5rem; + } + } + } + + .handoff-svg-wrapper { + flex: 1; + overflow: hidden; + flex-direction: column; + justify-content: flex-start; + } + + .handoff-svg-container { + display: grid; + width: 100%; + height: calc(100% - 35px); + overflow: auto; + align-items: center; + justify-content: safe center; + margin: 0 auto; + } } diff --git a/frontend/resources/styles/main/layouts/login.scss b/frontend/resources/styles/main/layouts/login.scss index 9442c515e5..b1c4edda99 100644 --- a/frontend/resources/styles/main/layouts/login.scss +++ b/frontend/resources/styles/main/layouts/login.scss @@ -25,18 +25,19 @@ background-color:#2C233E; background-image: url("/images/penpot-login.jpg"); background-size: cover; + background-position: center; .tagline { text-align: center; width: 280px; - font-size: $fs24; + font-size: $fs18; margin-top: 25px; - color: white; + color: #2C233E; } .logo { svg { - fill: white; + fill: #2C233E; width: 200px; height: 80px; } diff --git a/frontend/resources/styles/main/layouts/main-layout.scss b/frontend/resources/styles/main/layouts/main-layout.scss index edabb7c481..22a1cd54c9 100644 --- a/frontend/resources/styles/main/layouts/main-layout.scss +++ b/frontend/resources/styles/main/layouts/main-layout.scss @@ -17,7 +17,7 @@ background-color: $color-white; display: grid; grid-template-rows: 50px 1fr; - grid-template-columns: 40px 180px 1fr; + grid-template-columns: 40px 220px 1fr; height: 100vh; .dashboard-sidebar { diff --git a/frontend/resources/styles/main/partials/dashboard-header.scss b/frontend/resources/styles/main/partials/dashboard-header.scss index 30fae12c71..f20595b41b 100644 --- a/frontend/resources/styles/main/partials/dashboard-header.scss +++ b/frontend/resources/styles/main/partials/dashboard-header.scss @@ -12,7 +12,7 @@ background-color: $color-white; display: flex; height: 63px; - padding: $x-small $small; + padding: $x-small $medium $x-small $small; position: relative; z-index: 10; justify-content: space-between; diff --git a/frontend/resources/styles/main/partials/dashboard-sidebar.scss b/frontend/resources/styles/main/partials/dashboard-sidebar.scss index 3a100db1bd..04f1daf17a 100644 --- a/frontend/resources/styles/main/partials/dashboard-sidebar.scss +++ b/frontend/resources/styles/main/partials/dashboard-sidebar.scss @@ -40,11 +40,11 @@ top: 50px; z-index: 12; max-height: 30rem; - min-width: 189px; + min-width: 230px; } .options-dropdown { - left: 80px; + left: 117px; top: 50px; z-index: 12; max-height: 30rem; @@ -63,7 +63,7 @@ .switch-options { display: flex; max-width: 22px; - min-width: 22px; + min-width: 28px; border-left: 1px solid $color-gray-10; justify-content: center; align-items: center; @@ -111,7 +111,7 @@ .team-text { color: $color-gray-60; @include text-ellipsis; - width: 100px; + width: 130px; } } @@ -191,7 +191,7 @@ border-radius: $br-small; content: ""; height: 26px; - margin-right: 6px; + margin-right: $small; width: 4px; } @@ -266,7 +266,7 @@ font-size: $fs14; padding: 6px; margin: 0; - max-width: 170px; + max-width: 195px; width: 100%; height: 40px; } @@ -368,7 +368,7 @@ color: $color-black; margin: 10px 5px; font-size: $fs14; - max-width: 135px; + max-width: 160px; } img { @@ -378,6 +378,13 @@ width: 25px; } + svg { + height: 10px; + margin-left: auto; + margin-right: $small; + width: 10px; + } + .dropdown { left: 15px; bottom: 45px; diff --git a/frontend/resources/styles/main/partials/dashboard-team.scss b/frontend/resources/styles/main/partials/dashboard-team.scss index f4ad20e5dd..6c0f4fa019 100644 --- a/frontend/resources/styles/main/partials/dashboard-team.scss +++ b/frontend/resources/styles/main/partials/dashboard-team.scss @@ -71,7 +71,7 @@ z-index: 12; top: 30px; left: 0px; - width: 125px; + width: 168px; hr { margin: 0; @@ -83,7 +83,7 @@ align-items: center; color: $color-gray-60; cursor: pointer; - font-size: $fs12; + font-size: $fs14; height: 31px; padding: 5px 16px; @@ -123,7 +123,6 @@ display: flex; max-width: 324px; width: 324px; - height: 100px; background-color: $color-white; flex-direction: column; padding: 12px; @@ -139,7 +138,7 @@ .name { margin-top: 10px; - font-size: $fs32; + font-size: $fs28; color: $color-black; @include text-ellipsis; margin-right: 90px; diff --git a/frontend/resources/styles/main/partials/dashboard.scss b/frontend/resources/styles/main/partials/dashboard.scss index 58cf587998..2aed67220e 100644 --- a/frontend/resources/styles/main/partials/dashboard.scss +++ b/frontend/resources/styles/main/partials/dashboard.scss @@ -12,7 +12,7 @@ border-top-right-radius: $br-huge; border-top-left-radius: $br-huge; flex: 1 0 0; - margin-right: $small; + margin-right: $medium; overflow-y: auto; &.search { diff --git a/frontend/resources/styles/main/partials/handoff.scss b/frontend/resources/styles/main/partials/handoff.scss index adac8714ee..3f407617fe 100644 --- a/frontend/resources/styles/main/partials/handoff.scss +++ b/frontend/resources/styles/main/partials/handoff.scss @@ -15,6 +15,42 @@ justify-content: center; } +.expand-button, +.copy-button { + visibility: hidden; + opacity: 0; + transition: opacity 0.3s; + position: absolute; + background: none; + border: none; + padding: 0; + cursor: pointer; + + svg { + width: 16px; + height: 16px; + fill: $color-gray-20; + transition: fill 0.3s; + + &:hover { + fill: $color-primary; + } + } +} + +.expand-button { + right: 24px; + top: -1px; + + svg { + transform: rotate(45deg); + } +} +.copy-button { + right: 0; + top: 0; +} + .attributes-block { user-select: text; @@ -30,30 +66,6 @@ border-bottom: none; } - .attributes-copy-button { - visibility: hidden; - opacity: 0; - transition: opacity 0.3s; - position: absolute; - right: 0; - top: 0; - background: none; - border: none; - padding: 0; - cursor: pointer; - - svg { - width: 16px; - height: 16px; - fill: $color-gray-20; - transition: fill 0.3s; - - &:hover { - fill: $color-primary; - } - } - } - .attributes-label { color: $color-gray-20; } @@ -68,7 +80,7 @@ padding: 0.5rem; font-size: $fs14; - .attributes-copy-button { + .copy-button { padding: 0.5rem; margin-top: 0.25rem; } @@ -84,7 +96,7 @@ .attributes-value { width: 50%; } - .attributes-copy-button { + .copy-button { padding: 1rem 0.5rem; margin-top: 0.25rem; } @@ -92,9 +104,32 @@ .attributes-color-row { display: flex; + flex-direction: column; padding: 1rem 0; position: relative; - align-items: center; + + .attributes-color-id { + display: flex; + align-items: center; + + & > * { + margin: 0 0.5rem; + } + margin-bottom: 0.5rem; + } + + .attributes-color-value { + display: flex; + align-items: center; + + & > * { + margin: 0 0.5rem; + } + + & :last-child { + margin-right: 0; + } + } .color-text { width: 3rem; @@ -108,20 +143,18 @@ .color-bullet { width: 24px; height: 24px; + border-radius: $br-small; + border: 1px solid $color-gray-60; } - .attributes-copy-button { + + .hide-color .color-bullet { + visibility: hidden; + } + .copy-button { padding: 1rem 0.5rem; margin-top: 0.25rem; } - & > * { - margin: 0 0.5rem; - } - - & :last-child { - margin-right: 0; - } - select { font-size: $fs12; margin: 0; @@ -160,7 +193,7 @@ white-space: pre-wrap; } - .attributes-copy-button { + .copy-button { padding: 0.5rem; margin-top: 0.25rem; } @@ -212,6 +245,22 @@ justify-content: space-between; } + .attributes-typography-name-row { + position: relative; + margin-top: 0.5rem; + border: 1px solid $color-black; + border-radius: 4px; + margin: 0.5rem; + display: flex; + flex-direction: row; + align-items: center; + + .copy-button { + padding: 0.5rem; + margin-top: 0.25rem; + } + } + .attributes-typography-row { position: relative; margin: 0.5rem; @@ -220,6 +269,7 @@ .typography-sample { font-size: $fs16; } + } .download-button { @@ -246,11 +296,90 @@ .attributes-shadow-row, .attributes-stroke-row, .attributes-typography-row, - .attributes-content-row { - &:hover .attributes-copy-button { - visibility: visible; - opacity: 1; + .attributes-content-row, + .attributes-typography-name-row { + &:hover { + .expand-button, + .copy-button { + visibility: visible; + opacity: 1; + } } } - + + .attributes-shadow-block { + border-top: 1px solid $color-gray-60; + } + + .attributes-shadow-blocks :first-child { + border-top: none; + } +} + +.code-block { + margin-top: 0.5rem; + border-top: 1px solid $color-gray-60; + + .code-row-lang { + position: relative; + display: flex; + flex-direction: row; + margin: 0.5rem; + + &:hover { + .expand-button, + .copy-button { + visibility: visible; + opacity: 1; + } + } + + .code-selection { + height: 100%; + margin: 0; + padding: 0.5rem; + width: 4.5rem; + font-size: $fs12; + background: $color-gray-50; + color: $color-gray-10; + border-radius: 2px; + border: 1px solid $color-gray-30; + background-image: url("/images/icons/arrow-down-white.svg"); + background-repeat: no-repeat; + background-position: 90% 48%; + background-size: 8px; + } + .expand-button, + .copy-button { + margin-top: 8px; + } + } + + .code-row-display { + margin: 0.5rem; + font-size: $fs14; + + .code-display { + border-radius: 4px; + padding: 1rem; + overflow: hidden; + white-space: pre-wrap; + background: $color-gray-60; + user-select: text; + + .hljs-attr { + color: #a6e22e; + } + .hljs-comment { + color: $color-gray-30; + } + .hljs-string { + color: #66d9ef; + } + } + } +} + +.element-options > :first-child { + border-top: none; } diff --git a/frontend/resources/styles/main/partials/sidebar-assets.scss b/frontend/resources/styles/main/partials/sidebar-assets.scss index 7fdb78372b..29938b2386 100644 --- a/frontend/resources/styles/main/partials/sidebar-assets.scss +++ b/frontend/resources/styles/main/partials/sidebar-assets.scss @@ -100,8 +100,8 @@ } option { - color: $color-gray-60; background: $color-white; + color: $color-gray-60; font-size: $fs12; } } @@ -213,9 +213,28 @@ left: 0; bottom: 0; width: 100%; + padding: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + + &.editing { + display: block; + } + + .editable-label-input { + border: 1px solid $color-gray-20; + border-radius: 3px; + font-size: $fs11; + padding: 2px; + margin: 0; + height: unset; + width: 100%; + } + + .editable-label-close { + display: none; + } } .grid-cell:hover { diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index 25274009b7..b24ef1c5e4 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -295,6 +295,7 @@ } } + .custom-select-dropdown { background-color: $color-white; border-radius: $br-small; @@ -307,7 +308,6 @@ top: 30px; z-index: 12; - .presets { width: 200px; } @@ -471,6 +471,37 @@ } } +.element-set-content .component-row { + display: flex; + align-items: center; + font-size: $fs12; + color: $color-gray-10; + + svg { + fill: $color-gray-20; + height: 16px; + width: 16px; + margin-right: $small; + } + + .row-actions { + margin-left: auto; + cursor: pointer; + + svg { + fill: $color-gray-20; + height: 8px; + width: 8px; + } + + .context-menu-items { + right: 0.5rem; + left: unset; + top: 0; + } + } +} + .grid-option .custom-select { margin-bottom: 0; } @@ -921,8 +952,6 @@ border-right: none; } - - .size-option .custom-select-dropdown { cursor: pointer; max-height: 16rem; diff --git a/frontend/resources/styles/main/partials/sidebar-layers.scss b/frontend/resources/styles/main/partials/sidebar-layers.scss index dced031cc7..25e42fe072 100644 --- a/frontend/resources/styles/main/partials/sidebar-layers.scss +++ b/frontend/resources/styles/main/partials/sidebar-layers.scss @@ -154,11 +154,11 @@ border-right: 1px solid $color-gray-40; border-top: 1px solid $color-gray-40; position: absolute; - width: 0.5rem; - height: 0.5rem; + width: 6px; + height: 6px; transform: rotate(-45deg); top: -1px; - left: -5px; + left: -4px; } } diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss index bc3e60c6d3..e03353aec6 100644 --- a/frontend/resources/styles/main/partials/sidebar.scss +++ b/frontend/resources/styles/main/partials/sidebar.scss @@ -16,6 +16,11 @@ $width-settings-bar: 16rem; position: fixed; right: 0; width: $width-settings-bar; + + &.expanded { + width: $width-settings-bar * 3; + } + z-index: 10; overflow-y: auto; diff --git a/frontend/resources/styles/main/partials/viewer-header.scss b/frontend/resources/styles/main/partials/viewer-header.scss index a123a74713..97e076b1be 100644 --- a/frontend/resources/styles/main/partials/viewer-header.scss +++ b/frontend/resources/styles/main/partials/viewer-header.scss @@ -247,8 +247,7 @@ position: absolute; left: 0; z-index: 12; - width: 200px; - max-height: 30rem; + max-height: 31rem; min-width: 7rem; overflow-y: auto; @@ -261,7 +260,7 @@ cursor: pointer; font-size: $fs14; display: flex; - padding: $small $small $small 25px; + padding: $small $medium $small 25px; &.selected { background-image: url(/images/icons/tick.svg); diff --git a/frontend/resources/styles/main/partials/workspace-header.scss b/frontend/resources/styles/main/partials/workspace-header.scss index fb340c3f40..060688ff9d 100644 --- a/frontend/resources/styles/main/partials/workspace-header.scss +++ b/frontend/resources/styles/main/partials/workspace-header.scss @@ -149,7 +149,7 @@ li { cursor: pointer; font-size: $fs14; - padding: $small $x-small; + padding: $small; display: flex; justify-content: space-between; diff --git a/frontend/src/app/main/data/modal.cljs b/frontend/src/app/main/data/modal.cljs index cd0a7c2c54..2d5181bb7f 100644 --- a/frontend/src/app/main/data/modal.cljs +++ b/frontend/src/app/main/data/modal.cljs @@ -20,18 +20,20 @@ (defn show ([props] (show (uuid/next) (:type props) props)) - ([type props] (show (uuid/next) type props)) + ([type props] + (show (uuid/next) type props)) ([id type props] (ptk/reify ::show-modal ptk/UpdateEvent (update [_ state] (assoc state ::modal {:id id - :type type - :props props - :allow-click-outside false}))))) + :type type + :props props + :allow-click-outside false}))))) + (defn update-props ([type props] - (ptk/reify ::show-modal + (ptk/reify ::update-modal-props ptk/UpdateEvent (update [_ state] (cond-> state diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 285286a7d4..67c1310abf 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -56,7 +56,7 @@ :selected #{} :collapsed #{} - :hover #{}})) + :hover nil})) ptk/WatchEvent (watch [_ state stream] @@ -72,8 +72,10 @@ (let [params (cond-> {:page-id page-id :file-id file-id} (string? token) (assoc :share-token token))] - (->> (rp/query :viewer-bundle params) - (rx/map bundle-fetched) + (->> (rx/zip (rp/query :viewer-bundle params) + (rp/query :file-libraries {:file-id file-id})) + (rx/first) + (rx/map #(apply bundle-fetched %)) #_(rx/catch (fn [error-data] (rx/of (rt/nav :not-found))))))))) @@ -87,7 +89,7 @@ (vec)))) (defn bundle-fetched - [{:keys [project file page share-token] :as bundle}] + [{:keys [project file page share-token] :as bundle} libraries] (us/verify ::bundle bundle) (ptk/reify ::file-fetched ptk/UpdateEvent @@ -95,7 +97,8 @@ (let [objects (:objects page) frames (extract-frames objects)] (-> state - (assoc :viewer-data {:project project + (assoc :viewer-libraries (into {} (map #(vector (:id %) %) libraries)) + :viewer-data {:project project :objects objects :file file :page page @@ -317,8 +320,7 @@ (ptk/reify ::hover-shape ptk/UpdateEvent (update [_ state] - (update-in state [:viewer-local :hover] (if hover? conj disj) id)))) - + (assoc-in state [:viewer-local :hover] (when hover? id))))) ;; --- Shortcuts diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 66d4269fef..ef40318313 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -43,6 +43,7 @@ [clojure.set :as set] [clojure.set :as set] [cuerdas.core :as str] + ;; [cljs.pprint :refer [pprint]] [potok.core :as ptk])) ;; (log/set-level! :trace) @@ -84,6 +85,19 @@ :snap-grid :dynamic-alignment}) +(def layout-names + {:assets + {:del #{:sitemap :layers :document-history } + :add #{:assets}} + + :document-history + {:del #{:assets :layers :sitemap} + :add #{:document-history}} + + :layers + {:del #{:document-history :assets} + :add #{:sitemap :layers}}}) + (s/def ::options-mode #{:design :prototype}) (def workspace-local-default @@ -103,11 +117,23 @@ :picked-color nil :picked-color-select false}) -(def initialize-layout +(declare ensure-layout) + +(defn initialize-layout + [layout-name] + (us/verify (s/nilable ::us/keyword) layout-name) (ptk/reify ::initialize-layout ptk/UpdateEvent (update [_ state] - (assoc state :workspace-layout default-layout)))) + (update state :workspace-layout + (fn [layout] + (or layout default-layout)))) + + ptk/WatchEvent + (watch [_ state stream] + (if (and layout-name (contains? layout-names layout-name)) + (rx/of (ensure-layout layout-name)) + (rx/of (ensure-layout :layers)))))) (defn initialize-file [project-id file-id] @@ -388,35 +414,22 @@ ;; --- Toggle layout flag -(def layout-flags - {:assets - {:del #{:sitemap :layers :document-history } - :add #{:assets}} - - :document-history - {:del #{:assets :layers :sitemap} - :add #{:document-history}} - - :layers - {:del #{:document-history :assets} - :add #{:sitemap :layers}}}) - -(defn- ensure-layout - [layout] - (assert (contains? layout-flags layout) - (str "unexpected layout name: " layout)) +(defn ensure-layout + [layout-name] + (assert (contains? layout-names layout-name) + (str "unexpected layout name: " layout-name)) (ptk/reify ::ensure-layout ptk/UpdateEvent (update [_ state] (update state :workspace-layout (fn [stored] - (let [todel (get-in layout-flags [layout :del] #{}) - toadd (get-in layout-flags [layout :add] #{})] + (let [todel (get-in layout-names [layout-name :del] #{}) + toadd (get-in layout-names [layout-name :add] #{})] (-> stored (set/difference todel) (set/union toadd)))))))) -(defn- toggle-layout-flags +(defn toggle-layout-flags [& flags] (ptk/reify ::toggle-layout-flags ptk/UpdateEvent @@ -1187,6 +1200,18 @@ qparams {:page-id page-id}] (rx/of (rt/nav :workspace pparams qparams)))))) +(defn go-to-layout + [layout] + (us/verify ::layout-flag layout) + (ptk/reify ::go-to-layout + ptk/WatchEvent + (watch [_ state stream] + (let [project-id (get-in state [:workspace-project :id]) + file-id (get-in state [:workspace-file :id]) + page-id (get-in state [:current-page-id]) + pparams {:file-id file-id :project-id project-id} + qparams {:page-id page-id :layout (name layout)}] + (rx/of (rt/nav :workspace pparams qparams)))))) (def go-to-file (ptk/reify ::go-to-file diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 44aa7881f0..9fb6ef150c 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -27,10 +27,13 @@ [app.util.i18n :refer [tr]] [app.util.router :as rt] [app.util.time :as dt] + [app.util.logging :as log] [beicon.core :as rx] [cljs.spec.alpha :as s] [potok.core :as ptk])) +(log/set-level! :warn) + (declare sync-file) (defn default-color-name [color] @@ -125,423 +128,6 @@ :object prev}] (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) -(def add-component - (ptk/reify ::add-component - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (:current-page-id state) - objects (dwc/lookup-page-objects state page-id) - selected (get-in state [:workspace-local :selected]) - shapes (dws/shapes-for-grouping objects selected)] - (when-not (empty? shapes) - (let [;; If the selected shape is a group, we can use it. If not, - ;; we need to create a group before creating the component. - [group rchanges uchanges] - (if (and (= (count shapes) 1) - (= (:type (first shapes)) :group)) - [(first shapes) [] []] - (dws/prepare-create-group page-id shapes "Component-" true)) - - [new-shape new-shapes updated-shapes] - (dwlh/make-component-shape group objects) - - rchanges (conj rchanges - {:type :add-component - :id (:id new-shape) - :name (:name new-shape) - :shapes new-shapes}) - - rchanges (into rchanges - (map (fn [updated-shape] - {:type :mod-obj - :page-id page-id - :id (:id updated-shape) - :operations [{:type :set - :attr :component-id - :val (:component-id updated-shape)} - {:type :set - :attr :component-file - :val nil} - {:type :set - :attr :component-root? - :val (:component-root? updated-shape)} - {:type :set - :attr :shape-ref - :val (:shape-ref updated-shape)} - {:type :set - :attr :touched - :val (:touched updated-shape)}]}) - updated-shapes)) - - uchanges (conj uchanges - {:type :del-component - :id (:id new-shape)}) - - uchanges (into uchanges - (map (fn [updated-shape] - (let [original-shape (get objects (:id updated-shape))] - {:type :mod-obj - :page-id page-id - :id (:id updated-shape) - :operations [{:type :set - :attr :component-id - :val (:component-id original-shape)} - {:type :set - :attr :component-file - :val (:component-file original-shape)} - {:type :set - :attr :component-root? - :val (:component-root? original-shape)} - {:type :set - :attr :shape-ref - :val (:shape-ref original-shape)} - {:type :set - :attr :touched - :val (:touched original-shape)}]})) - updated-shapes))] - - - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) - (dws/select-shapes (d/ordered-set (:id group)))))))))) - -(defn delete-component - [{:keys [id] :as params}] - (us/assert ::us/uuid id) - (ptk/reify ::delete-component - ptk/WatchEvent - (watch [_ state stream] - (let [component (get-in state [:workspace-data :components id]) - - rchanges [{:type :del-component - :id id}] - - uchanges [{:type :add-component - :id id - :name (:name component) - :shapes (vals (:objects component))}]] - - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) - -(defn instantiate-component - [file-id component-id position] - (us/assert (s/nilable ::us/uuid) file-id) - (us/assert ::us/uuid component-id) - (ptk/reify ::instantiate-component - ptk/WatchEvent - (watch [_ state stream] - (let [component (if (nil? file-id) - (get-in state [:workspace-data :components component-id]) - (get-in state [:workspace-libraries file-id :data :components component-id])) - component-shape (get-in component [:objects (:id component)]) - - orig-pos (gpt/point (:x component-shape) (:y component-shape)) - delta (gpt/subtract position orig-pos) - - page-id (:current-page-id state) - objects (dwc/lookup-page-objects state page-id) - unames (atom (dwc/retrieve-used-names objects)) - - frame-id (cph/frame-id-by-position objects (gpt/add orig-pos delta)) - - update-new-shape - (fn [new-shape original-shape] - (let [new-name - (dwc/generate-unique-name @unames (:name new-shape))] - - (swap! unames conj new-name) - - (cond-> new-shape - true - (as-> $ - (assoc $ :name new-name) - (geom/move $ delta) - (assoc $ :frame-id frame-id) - (assoc $ :parent-id - (or (:parent-id $) (:frame-id $))) - (assoc $ :shape-ref (:id original-shape)) - (dissoc $ :touched)) - - (nil? (:parent-id original-shape)) - (assoc :component-id (:id original-shape) - :component-root? true) - - (and (nil? (:parent-id original-shape)) (some? file-id)) - (assoc :component-file file-id) - - (and (nil? (:parent-id original-shape)) (nil? file-id)) - (dissoc :component-file) - - (some? (:parent-id original-shape)) - (dissoc :component-root?)))) - - [new-shape new-shapes _] - (cph/clone-object component-shape - nil - (get component :objects) - update-new-shape) - - rchanges (map (fn [obj] - {:type :add-obj - :id (:id obj) - :page-id page-id - :frame-id (:frame-id obj) - :parent-id (:parent-id obj) - :obj obj}) - new-shapes) - - uchanges (map (fn [obj] - {:type :del-obj - :id (:id obj) - :page-id page-id}) - new-shapes)] - - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) - (dws/select-shapes (d/ordered-set (:id new-shape)))))))) - -(defn detach-component - [id] - (us/assert ::us/uuid id) - (ptk/reify ::detach-component - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (:current-page-id state) - objects (dwc/lookup-page-objects state page-id) - shapes (cph/get-object-with-children id objects) - - rchanges (map (fn [obj] - {:type :mod-obj - :page-id page-id - :id (:id obj) - :operations [{:type :set - :attr :component-id - :val nil} - {:type :set - :attr :component-file - :val nil} - {:type :set - :attr :shape-ref - :val nil}]}) - shapes) - - uchanges (map (fn [obj] - {:type :mod-obj - :page-id page-id - :id (:id obj) - :operations [{:type :set - :attr :component-id - :val (:component-id obj)} - {:type :set - :attr :component-file - :val (:component-file obj)} - {:type :set - :attr :shape-ref - :val (:shape-ref obj)}]}) - shapes)] - - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) - -(defn nav-to-component-file - [file-id] - (us/assert ::us/uuid file-id) - (ptk/reify ::nav-to-component-file - ptk/WatchEvent - (watch [_ state stream] - (let [file (get-in state [:workspace-libraries file-id]) - pparams {:project-id (:project-id file) - :file-id (:id file)} - qparams {:page-id (first (get-in file [:data :pages]))}] - (st/emit! (rt/nav-new-window :workspace pparams qparams)))))) - -(defn ext-library-changed - [file-id modified-at changes] - (us/assert ::us/uuid file-id) - (us/assert ::cp/changes changes) - (ptk/reify ::ext-library-changed - ptk/UpdateEvent - (update [_ state] - (-> state - (assoc-in [:workspace-libraries file-id :modified-at] modified-at) - (d/update-in-when [:workspace-libraries file-id :data] - cp/process-changes changes))))) - -(defn reset-component - [id] - (us/assert ::us/uuid id) - (ptk/reify ::reset-component - ptk/WatchEvent - (watch [_ state stream] - ;; ===== Uncomment this to debug ===== - ;; (js/console.info "##### RESET-COMPONENT of shape" (str id)) - (let [page-id (:current-page-id state) - page (get-in state [:workspace-data :pages-index page-id]) - objects (dwc/lookup-page-objects state page-id) - shape (get objects id) - file-id (get shape :component-file) - - [all-shapes component root-component] - (dwlh/resolve-shapes-and-components shape - objects - state - true) - - ;; ===== Uncomment this to debug ===== - ;; _ (js/console.info "shape" (:name shape) "<- component" (:name component)) - ;; _ (js/console.debug "all-shapes" (clj->js all-shapes)) - ;; _ (js/console.debug "component" (clj->js component)) - ;; _ (js/console.debug "root-component" (clj->js root-component)) - - [rchanges uchanges] - (dwlh/generate-sync-shape-and-children-components shape - all-shapes - component - root-component - (:id page) - nil - true)] - - ;; ===== Uncomment this to debug ===== - ;; (js/console.debug "rchanges" (clj->js rchanges)) - - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) - -(defn update-component - [id] - (us/assert ::us/uuid id) - (ptk/reify ::update-component - ptk/WatchEvent - (watch [_ state stream] - ;; ===== Uncomment this to debug ===== - ;; (js/console.info "##### UPDATE-COMPONENT of shape" (str id)) - (let [page-id (:current-page-id state) - objects (dwc/lookup-page-objects state page-id) - shape (get objects id) - file-id (get shape :component-file) - - [all-shapes component root-component] - (dwlh/resolve-shapes-and-components shape - objects - state - true) - - ;; ===== Uncomment this to debug ===== - ;; _ (js/console.info "shape" (:name shape) "-> component" (:name component)) - ;; _ (js/console.debug "all-shapes" (clj->js all-shapes)) - ;; _ (js/console.debug "component" (clj->js component)) - ;; _ (js/console.debug "root-component" (clj->js root-component)) - - [rchanges uchanges] - (dwlh/generate-sync-shape-inverse shape - all-shapes - component - root-component - page-id)] - - ;; ===== Uncomment this to debug ===== - ;; (js/console.debug "rchanges" (clj->js rchanges)) - - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) - -(declare sync-file-2nd-stage) - -(defn sync-file - [file-id] - (us/assert (s/nilable ::us/uuid) file-id) - (ptk/reify ::sync-file - ptk/UpdateEvent - (update [_ state] - (if file-id - (assoc-in state [:workspace-libraries file-id :synced-at] (dt/now)) - state)) - - ptk/WatchEvent - (watch [_ state stream] - ;; ===== Uncomment this to debug ===== - ;; (js/console.info "##### SYNC-FILE" (str (or file-id "local"))) - (let [library-changes [(dwlh/generate-sync-library :components file-id state) - (dwlh/generate-sync-library :colors file-id state) - (dwlh/generate-sync-library :typographies file-id state)] - file-changes [(dwlh/generate-sync-file :components file-id state) - (dwlh/generate-sync-file :colors file-id state) - (dwlh/generate-sync-file :typographies file-id state)] - rchanges (d/concat [] - (->> library-changes (remove nil?) (map first) (flatten)) - (->> file-changes (remove nil?) (map first) (flatten))) - uchanges (d/concat [] - (->> library-changes (remove nil?) (map second) (flatten)) - (->> file-changes (remove nil?) (map second) (flatten)))] - ;; ===== Uncomment this to debug ===== - ;; (js/console.debug "rchanges" (clj->js rchanges)) - (rx/concat - (rx/of (dm/hide-tag :sync-dialog)) - (when rchanges - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))) - (when file-id - (rp/mutation :update-sync - {:file-id (get-in state [:workspace-file :id]) - :library-id file-id})) - (when (some? library-changes) - (rx/of (sync-file-2nd-stage file-id)))))))) - -(defn sync-file-2nd-stage - "If some components have been modified, we need to launch another synchronization - to update the instances of the changed components." - ;; TODO: this does not work if there are multiple nested components. Only the - ;; first level will be updated. - ;; To solve this properly, it would be better to launch another sync-file - ;; recursively. But for this not to cause an infinite loop, we need to - ;; implement updated-at at component level, to detect what components have - ;; not changed, and then not to apply sync and terminate the loop. - [file-id] - (us/assert (s/nilable ::us/uuid) file-id) - (ptk/reify ::sync-file-2nd-stage - ptk/WatchEvent - (watch [_ state stream] - ;; ===== Uncomment this to debug ===== - ;; (js/console.info "##### SYNC-FILE" (str (or file-id "local")) "(2nd stage)") - (let [[rchanges1 uchanges1] (dwlh/generate-sync-file :components nil state) - [rchanges2 uchanges2] (dwlh/generate-sync-library :components file-id state) - rchanges (d/concat rchanges1 rchanges2) - uchanges (d/concat uchanges1 uchanges2)] - (when rchanges - ;; ===== Uncomment this to debug ===== - ;; (js/console.debug "rchanges" (clj->js rchanges)) - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))) - -(def ignore-sync - (ptk/reify ::sync-file - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-file :ignore-sync-until] (dt/now))) - - ptk/WatchEvent - (watch [_ state stream] - (rp/mutation :ignore-sync - {:file-id (get-in state [:workspace-file :id]) - :date (dt/now)})))) - -(defn notify-sync-file - [file-id] - (us/assert ::us/uuid file-id) - (ptk/reify ::notify-sync-file - ptk/WatchEvent - (watch [_ state stream] - (let [libraries-need-sync (filter #(> (:modified-at %) (:synced-at %)) - (vals (get state :workspace-libraries))) - do-update #(do (apply st/emit! (map (fn [library] - (sync-file (:id library))) - libraries-need-sync)) - (st/emit! dm/hide)) - do-dismiss #(do (st/emit! ignore-sync) - (st/emit! dm/hide))] - (rx/of (dm/info-dialog - (tr "workspace.updates.there-are-updates") - :inline-actions - [{:label (tr "workspace.updates.update") - :callback do-update} - {:label (tr "workspace.updates.dismiss") - :callback do-dismiss}] - :sync-dialog)))))) - (defn add-typography ([typography] (add-typography typography true)) ([typography edit?] @@ -586,3 +172,470 @@ uchg {:type :add-typography :typography prev}] (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) + +(def add-component + "Add a new component to current file library, from the currently selected shapes" + (ptk/reify ::add-component + ptk/WatchEvent + (watch [_ state stream] + (let [page-id (:current-page-id state) + objects (dwc/lookup-page-objects state page-id) + selected (get-in state [:workspace-local :selected]) + shapes (dws/shapes-for-grouping objects selected)] + (when-not (empty? shapes) + (let [;; If the selected shape is a group, we can use it. If not, + ;; we need to create a group before creating the component. + [group rchanges uchanges] + (if (and (= (count shapes) 1) + (= (:type (first shapes)) :group)) + [(first shapes) [] []] + (dws/prepare-create-group page-id shapes "Component-" true)) + + [new-shape new-shapes updated-shapes] + (dwlh/make-component-shape group objects) + + rchanges (conj rchanges + {:type :add-component + :id (:id new-shape) + :name (:name new-shape) + :shapes new-shapes}) + + rchanges (into rchanges + (map (fn [updated-shape] + {:type :mod-obj + :page-id page-id + :id (:id updated-shape) + :operations [{:type :set + :attr :component-id + :val (:component-id updated-shape)} + {:type :set + :attr :component-file + :val (:component-file updated-shape)} + {:type :set + :attr :component-root? + :val (:component-root? updated-shape)} + {:type :set + :attr :shape-ref + :val (:shape-ref updated-shape)} + {:type :set + :attr :touched + :val (:touched updated-shape)}]}) + updated-shapes)) + + uchanges (conj uchanges + {:type :del-component + :id (:id new-shape)}) + + uchanges (into uchanges + (map (fn [updated-shape] + (let [original-shape (get objects (:id updated-shape))] + {:type :mod-obj + :page-id page-id + :id (:id updated-shape) + :operations [{:type :set + :attr :component-id + :val (:component-id original-shape)} + {:type :set + :attr :component-file + :val (:component-file original-shape)} + {:type :set + :attr :component-root? + :val (:component-root? original-shape)} + {:type :set + :attr :shape-ref + :val (:shape-ref original-shape)} + {:type :set + :attr :touched + :val (:touched original-shape)}]})) + updated-shapes))] + + + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (dws/select-shapes (d/ordered-set (:id group)))))))))) + +(defn rename-component + [id new-name] + (us/assert ::us/uuid id) + (us/assert ::us/string new-name) + (ptk/reify ::rename-component + ptk/WatchEvent + (watch [_ state stream] + (let [component (get-in state [:workspace-data :components id]) + objects (get component :objects) + new-objects (assoc-in objects + [(:id component) :name] + new-name) + + rchanges [{:type :mod-component + :id id + :name new-name + :objects new-objects}] + + uchanges [{:type :mod-component + :id id + :name (:name component) + :objects objects}]] + + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) + +(defn duplicate-component + "Create a new component copied from the one with the given id." + [{:keys [id] :as params}] + (ptk/reify ::duplicate-component + ptk/WatchEvent + (watch [_ state stream] + (let [component (cph/get-component id + nil + (get state :workspace-data) + nil) + all-components (vals (get-in state [:workspace-data :components])) + unames (set (map :name all-components)) + new-name (dwc/generate-unique-name unames (:name component)) + + [new-shape new-shapes updated-shapes] + (dwlh/duplicate-component component) + + rchanges [{:type :add-component + :id (:id new-shape) + :name new-name + :shapes new-shapes}] + + uchanges [{:type :del-component + :id (:id new-shape)}]] + + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) + +(defn delete-component + "Delete the component with the given id, from the current file library." + [{:keys [id] :as params}] + (us/assert ::us/uuid id) + (ptk/reify ::delete-component + ptk/WatchEvent + (watch [_ state stream] + (let [component (get-in state [:workspace-data :components id]) + + rchanges [{:type :del-component + :id id}] + + uchanges [{:type :add-component + :id id + :name (:name component) + :shapes (vals (:objects component))}]] + + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) + +(defn instantiate-component + "Create a new shape in the current page, from the component with the given id + in the given file library (if file-id is nil, take it from the current file library)." + [file-id component-id position] + (us/assert (s/nilable ::us/uuid) file-id) + (us/assert ::us/uuid component-id) + (us/assert ::us/point position) + (ptk/reify ::instantiate-component + ptk/WatchEvent + (watch [_ state stream] + (let [component (if (nil? file-id) + (get-in state [:workspace-data :components component-id]) + (get-in state [:workspace-libraries file-id :data :components component-id])) + component-shape (get-in component [:objects (:id component)]) + + orig-pos (gpt/point (:x component-shape) (:y component-shape)) + delta (gpt/subtract position orig-pos) + + page-id (:current-page-id state) + objects (dwc/lookup-page-objects state page-id) + unames (atom (dwc/retrieve-used-names objects)) + + frame-id (cph/frame-id-by-position objects (gpt/add orig-pos delta)) + + update-new-shape + (fn [new-shape original-shape] + (let [new-name + (dwc/generate-unique-name @unames (:name new-shape))] + + (swap! unames conj new-name) + + (cond-> new-shape + true + (as-> $ + (assoc $ :name new-name) + (geom/move $ delta) + (assoc $ :frame-id frame-id) + (assoc $ :parent-id + (or (:parent-id $) (:frame-id $)))) + + (nil? (:shape-ref original-shape)) + (assoc :shape-ref (:id original-shape)) + + (nil? (:parent-id original-shape)) + (assoc :component-id (:id original-shape) + :component-root? true) + + (and (nil? (:parent-id original-shape)) (some? file-id)) + (assoc :component-file file-id) + + (and (nil? (:parent-id original-shape)) (nil? file-id)) + (dissoc :component-file) + + (and (some? (:component-id original-shape)) + (nil? (:component-file original-shape)) + (some? file-id)) + (assoc :component-file file-id) + + (some? (:parent-id original-shape)) + (dissoc :component-root?)))) + + [new-shape new-shapes _] + (cph/clone-object component-shape + nil + (get component :objects) + update-new-shape) + + rchanges (map (fn [obj] + {:type :add-obj + :id (:id obj) + :page-id page-id + :frame-id (:frame-id obj) + :parent-id (:parent-id obj) + :obj obj}) + new-shapes) + + uchanges (map (fn [obj] + {:type :del-obj + :id (:id obj) + :page-id page-id}) + new-shapes)] + + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (dws/select-shapes (d/ordered-set (:id new-shape)))))))) + +(defn detach-component + "Remove all references to components in the shape with the given id, + and all its children, at the current page." + [id] + (us/assert ::us/uuid id) + (ptk/reify ::detach-component + ptk/WatchEvent + (watch [_ state stream] + (let [page-id (:current-page-id state) + objects (dwc/lookup-page-objects state page-id) + shapes (cph/get-object-with-children id objects) + + rchanges (map (fn [obj] + {:type :mod-obj + :page-id page-id + :id (:id obj) + :operations [{:type :set + :attr :component-id + :val nil} + {:type :set + :attr :component-file + :val nil} + {:type :set + :attr :component-root? + :val nil} + {:type :set + :attr :shape-ref + :val nil}]}) + shapes) + + uchanges (map (fn [obj] + {:type :mod-obj + :page-id page-id + :id (:id obj) + :operations [{:type :set + :attr :component-id + :val (:component-id obj)} + {:type :set + :attr :component-file + :val (:component-file obj)} + {:type :set + :attr :component-root? + :val (:component-root? obj)} + {:type :set + :attr :shape-ref + :val (:shape-ref obj)}]}) + shapes)] + + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) + +(defn nav-to-component-file + [file-id] + (us/assert ::us/uuid file-id) + (ptk/reify ::nav-to-component-file + ptk/WatchEvent + (watch [_ state stream] + (let [file (get-in state [:workspace-libraries file-id]) + pparams {:project-id (:project-id file) + :file-id (:id file)} + qparams {:page-id (first (get-in file [:data :pages])) + :layout :assets}] + (st/emit! (rt/nav-new-window :workspace pparams qparams)))))) + +(defn ext-library-changed + [file-id modified-at changes] + (us/assert ::us/uuid file-id) + (us/assert ::cp/changes changes) + (ptk/reify ::ext-library-changed + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-libraries file-id :modified-at] modified-at) + (d/update-in-when [:workspace-libraries file-id :data] + cp/process-changes changes))))) + +(defn reset-component + "Cancels all modifications in the shape with the given id, and all its children, in + the current page. Set all attributes equal to the ones in the linked component, + and untouched." + [id] + (us/assert ::us/uuid id) + (ptk/reify ::reset-component + ptk/WatchEvent + (watch [_ state stream] + ;; ===== Uncomment this to debug ===== + (log/info :msg "RESET-COMPONENT of shape" :id (str id)) + (let [[rchanges uchanges] + (dwlh/generate-sync-shape-and-children-components (get state :current-page-id) + nil + id + (get state :workspace-data) + (get state :workspace-libraries) + true)] + ;; ===== Uncomment this to debug ===== + (log/debug :msg "RESET-COMPONENT finished" :js/rchanges rchanges) + + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) + +(defn update-component + "Modify the component linked to the shape with the given id, in the current page, so that + all attributes of its shapes are equal to the shape and its children. Also set all attributes + of the shape untouched." + [id] + (us/assert ::us/uuid id) + (ptk/reify ::update-component + ptk/WatchEvent + (watch [_ state stream] + ;; ===== Uncomment this to debug ===== + (log/info :msg "UPDATE-COMPONENT of shape" :id (str id)) + (let [page-id (:current-page-id state) + objects (dwc/lookup-page-objects state page-id) + shape (get objects id) + file-id (get shape :component-file) + + [rchanges uchanges] + (dwlh/generate-sync-shape-inverse (get state :current-page-id) + id + (get state :workspace-data) + (get state :workspace-libraries))] + + ;; ===== Uncomment this to debug ===== + (log/debug :msg "UPDATE-COMPONENT finished" :js/rchanges rchanges) + + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) + +(declare sync-file-2nd-stage) + +(defn sync-file + "Syhchronize the library file with the given id, with the current file. + Walk through all shapes in all pages that use some color, typography or + component of the library file, and copy the new values to the shapes. + Do it also for shapes inside components of the local file library." + [file-id] + (us/assert (s/nilable ::us/uuid) file-id) + (ptk/reify ::sync-file + ptk/UpdateEvent + (update [_ state] + (if file-id + (assoc-in state [:workspace-libraries file-id :synced-at] (dt/now)) + state)) + + ptk/WatchEvent + (watch [_ state stream] + ;; ===== Uncomment this to debug ===== + (log/info :msg "SYNC-FILE" :file (str (or file-id "local"))) + (let [library-changes [(dwlh/generate-sync-library :components file-id state) + (dwlh/generate-sync-library :colors file-id state) + (dwlh/generate-sync-library :typographies file-id state)] + file-changes [(dwlh/generate-sync-file :components file-id state) + (dwlh/generate-sync-file :colors file-id state) + (dwlh/generate-sync-file :typographies file-id state)] + rchanges (d/concat [] + (->> library-changes (remove nil?) (map first) (flatten)) + (->> file-changes (remove nil?) (map first) (flatten))) + uchanges (d/concat [] + (->> library-changes (remove nil?) (map second) (flatten)) + (->> file-changes (remove nil?) (map second) (flatten)))] + ;; ===== Uncomment this to debug ===== + (log/debug :msg "SYNC-FILE finished" :js/rchanges rchanges) + (rx/concat + (rx/of (dm/hide-tag :sync-dialog)) + (when rchanges + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))) + (when file-id + (rp/mutation :update-sync + {:file-id (get-in state [:workspace-file :id]) + :library-id file-id})) + (when (some? library-changes) + (rx/of (sync-file-2nd-stage file-id)))))))) + +(defn sync-file-2nd-stage + "If some components have been modified, we need to launch another synchronization + to update the instances of the changed components." + ;; TODO: this does not work if there are multiple nested components. Only the + ;; first level will be updated. + ;; To solve this properly, it would be better to launch another sync-file + ;; recursively. But for this not to cause an infinite loop, we need to + ;; implement updated-at at component level, to detect what components have + ;; not changed, and then not to apply sync and terminate the loop. + [file-id] + (us/assert (s/nilable ::us/uuid) file-id) + (ptk/reify ::sync-file-2nd-stage + ptk/WatchEvent + (watch [_ state stream] + ;; ===== Uncomment this to debug ===== + (log/info :msg "SYNC-FILE (2nd stage)" :file (str (or file-id "local"))) + (let [[rchanges1 uchanges1] (dwlh/generate-sync-file :components nil state) + [rchanges2 uchanges2] (dwlh/generate-sync-library :components file-id state) + rchanges (d/concat rchanges1 rchanges2) + uchanges (d/concat uchanges1 uchanges2)] + (when rchanges + ;; ===== Uncomment this to debug ===== + (log/debug :msg "SYNC-FILE (2nd stage) finished" :js/rchanges rchanges) + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))) + +(def ignore-sync + (ptk/reify ::sync-file + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-file :ignore-sync-until] (dt/now))) + + ptk/WatchEvent + (watch [_ state stream] + (rp/mutation :ignore-sync + {:file-id (get-in state [:workspace-file :id]) + :date (dt/now)})))) + +(defn notify-sync-file + [file-id] + (us/assert ::us/uuid file-id) + (ptk/reify ::notify-sync-file + ptk/WatchEvent + (watch [_ state stream] + (let [libraries-need-sync (filter #(> (:modified-at %) (:synced-at %)) + (vals (get state :workspace-libraries))) + do-update #(do (apply st/emit! (map (fn [library] + (sync-file (:id library))) + libraries-need-sync)) + (st/emit! dm/hide)) + do-dismiss #(do (st/emit! ignore-sync) + (st/emit! dm/hide))] + (rx/of (dm/info-dialog + (tr "workspace.updates.there-are-updates") + :inline-actions + [{:label (tr "workspace.updates.update") + :callback do-update} + {:label (tr "workspace.updates.dismiss") + :callback do-dismiss}] + :sync-dialog)))))) + diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index 6144cbcfb6..9298399738 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -15,8 +15,11 @@ [app.common.pages-helpers :as cph] [app.common.geom.point :as gpt] [app.common.pages :as cp] + [app.util.logging :as log] [app.util.text :as ut])) +(log/set-level! :warn) + (defonce empty-changes [[] []]) (defonce color-sync-attrs @@ -33,9 +36,12 @@ (declare has-asset-reference-fn) (declare get-assets) -(declare resolve-shapes-and-components) (declare generate-sync-shape-and-children-components) +(declare generate-sync-shape-and-children-normal) +(declare generate-sync-shape-and-children-nested) (declare generate-sync-shape-inverse) +(declare generate-sync-shape-inverse-normal) +(declare generate-sync-shape-inverse-nested) (declare generate-sync-shape<-component) (declare generate-sync-shape->component) (declare remove-component-and-ref) @@ -55,23 +61,25 @@ (assert (nil? (:component-id shape))) (assert (nil? (:component-file shape))) (assert (nil? (:shape-ref shape))) - (let [update-new-shape (fn [new-shape original-shape] + (let [;; Ensure that the component root is not an instance and + ;; it's no longer tied to a frame. + update-new-shape (fn [new-shape original-shape] (cond-> new-shape true - (assoc :frame-id nil) + (-> (assoc :frame-id nil) + (dissoc :component-root?)) (nil? (:parent-id new-shape)) (dissoc :component-id :component-file - :component-root? :shape-ref))) ;; Make the original shape an instance of the new component. ;; If one of the original shape children already was a component - ;; instance, the 'instanceness' is copied into the new component. + ;; instance, maintain this instanceness untouched. update-original-shape (fn [original-shape new-shape] (cond-> original-shape - true + (nil? (:shape-ref original-shape)) (-> (assoc :shape-ref (:id new-shape)) (dissoc :touched)) @@ -85,6 +93,16 @@ (cph/clone-object shape nil objects update-new-shape update-original-shape))) +(defn duplicate-component + "Clone the root shape of the component and all children. Generate new + ids from all of them." + [component] + (let [component-root (cph/get-component-root component)] + (cph/clone-object component-root + nil + (get component :objects) + identity))) + ;; ---- General library synchronization functions ---- @@ -92,10 +110,13 @@ "Generate changes to synchronize all shapes in all pages of the current file, with the given asset of the given library." [asset-type library-id state] - (s/assert #{:colors :components :typographies} asset-type) (s/assert (s/nilable ::us/uuid) library-id) + (log/info :msg "Sync local file with library" + :asset-type asset-type + :library (str (or library-id "local"))) + (let [library-items (if (nil? library-id) (get-in state [:workspace-data asset-type]) @@ -124,6 +145,11 @@ "Generate changes to synchronize all shapes inside components of the current file library, that use the given type of asset of the given library." [asset-type library-id state] + + (log/info :msg "Sync local components with library" + :asset-type asset-type + :library (str (or library-id "local"))) + (let [library-items (if (nil? library-id) (get-in state [:workspace-data asset-type]) @@ -151,6 +177,11 @@ "Generate changes to synchronize all shapes in a particular container (a page or a component) that are linked to the given library." [asset-type library-id state container page-id component-id] + + (if page-id + (log/debug :msg "Sync page in local file" :page-id page-id) + (log/debug :msg "Sync component in local library" :component-id component-id)) + (let [has-asset-reference? (has-asset-reference-fn asset-type library-id) linked-shapes (cph/select-objects has-asset-reference? container)] (loop [shapes (seq linked-shapes) @@ -176,7 +207,7 @@ [asset-type library-id] (case asset-type :components - (fn [shape] (and (:component-root? shape) + (fn [shape] (and (:component-id shape) (= (:component-file shape) library-id))) :colors @@ -214,19 +245,12 @@ (defmethod generate-sync-shape :components [_ library-id state objects page-id component-id shape] - (let [[all-shapes component root-component] - (resolve-shapes-and-components shape - objects - state - false)] - - (generate-sync-shape-and-children-components shape - all-shapes - component - root-component - page-id - component-id - false))) + (generate-sync-shape-and-children-components page-id + component-id + (:id shape) + (get state :workspace-data) + (get state :workspace-libraries) + false)) (defn- generate-sync-text-shape [shape page-id component-id update-node] (let [old-content (:content shape) @@ -321,44 +345,6 @@ (get-in state [:workspace-data asset-type]) (get-in state [:workspace-libraries library-id :data asset-type]))) -(defn- get-component - [state file-id component-id] - (let [components (if (nil? file-id) - (get-in state [:workspace-data :components]) - (get-in state [:workspace-libraries file-id :data :components]))] - (get components component-id))) - -(defn resolve-shapes-and-components - "Get all shapes inside a component instance, and the component they are - linked with. If follow-indirection? is true, and the shape corresponding - to the root shape is also a component instance, follow the link and get - the final component." - [shape objects state follow-indirection?] - (loop [all-shapes (cph/get-object-with-children (:id shape) objects) - local-objects objects - local-shape shape] - - (let [root-shape (cph/get-root-shape local-shape local-objects) - component (get-component state - (get root-shape :component-file) - (get root-shape :component-id)) - component-shape (get-in component [:objects (:shape-ref local-shape)])] - - (if (or (nil? (:component-id component-shape)) - (not follow-indirection?)) - [all-shapes component component-shape] - (let [resolve-indirection - (fn [shape] - (let [component-shape (get-in component [:objects (:shape-ref shape)])] - (-> shape - (assoc :shape-ref (:shape-ref component-shape)) - (d/assoc-when :component-id (:component-id component-shape)) - (d/assoc-when :component-file (:component-file component-shape))))) - new-shapes (map resolve-indirection all-shapes)] - (recur new-shapes - (:objects component) - component-shape)))))) - (defn generate-sync-shape-and-children-components "Generate changes to synchronize one shape that the root of a component instance, and all its children, from the given component. @@ -367,25 +353,121 @@ be copied to this one. If reset? is true, all changed attributes will be copied and the 'touched' flags in the instance shape will be cleared." - [root-shape all-shapes component root-component page-id component-id reset?] - (loop [shapes (seq all-shapes) - rchanges [] - uchanges []] - (let [shape (first shapes)] - (if (nil? shape) + [page-id component-id shape-id local-file libraries reset?] + (log/debug :msg "Sync shape and children" :shape (str shape-id) :reset? reset?) + (let [container (cph/get-container page-id component-id local-file) + shape (cph/get-shape container shape-id) + component (cph/get-component (:component-id shape) + (:component-file shape) + local-file + libraries) + root-shape shape + root-component (cph/get-component-root component)] + + (generate-sync-shape-and-children-normal page-id + component-id + container + shape + component + root-shape + root-component + reset?))) + +(defn- generate-sync-shape-and-children-normal + [page-id component-id container shape component root-shape root-component reset?] + (log/trace :msg "Sync shape (normal)" + :shape (str (:name shape)) + :component (:name component)) + (let [[rchanges uchanges] + (generate-sync-shape<-component shape + root-shape + root-component + component + page-id + component-id + reset?) + + children-ids (get shape :shapes [])] + + (loop [children-ids (seq children-ids) + rchanges rchanges + uchanges uchanges] + (let [child-id (first children-ids)] + (if (nil? child-id) + [rchanges uchanges] + (let [child-shape (cph/get-shape container child-id) + + [child-rchanges child-uchanges] + (if (nil? (:component-id child-shape)) + (generate-sync-shape-and-children-normal page-id + component-id + container + child-shape + component + root-shape + root-component + reset?) + (generate-sync-shape-and-children-nested page-id + component-id + container + child-shape + component + root-shape + root-component + reset?))] + (recur (next children-ids) + (d/concat rchanges child-rchanges) + (d/concat uchanges child-uchanges)))))))) + +(defn- generate-sync-shape-and-children-nested + [page-id component-id container shape component root-shape root-component reset?] + (log/trace :msg "Sync shape (nested)" + :shape (str (:name shape)) + :component (:name component)) + (let [component-shape (d/seek #(= (:shape-ref %) + (:shape-ref shape)) + (vals (:objects component))) + root-shape (if (:component-id shape) + shape + root-shape) + root-component (if (:component-id shape) + component-shape + root-component) + [rchanges uchanges] - (let [[shape-rchanges shape-uchanges] - (generate-sync-shape<-component - shape - root-shape - root-component - component - page-id - component-id - reset?)] - (recur (next shapes) - (d/concat rchanges shape-rchanges) - (d/concat uchanges shape-uchanges))))))) + (update-attrs shape + component-shape + root-shape + root-component + page-id + component-id + {:omit-touched? false + :reset-touched? false + :set-touched? false + :copy-touched? true}) + + children-ids (get shape :shapes [])] + + (loop [children-ids (seq children-ids) + rchanges rchanges + uchanges uchanges] + (let [child-id (first children-ids)] + (if (nil? child-id) + [rchanges uchanges] + (let [child-shape (cph/get-shape container child-id) + + [child-rchanges child-uchanges] + (generate-sync-shape-and-children-nested page-id + component-id + container + child-shape + component + root-shape + root-component + reset?)] + (recur (next children-ids) + (d/concat rchanges child-rchanges) + (d/concat uchanges child-uchanges)))))))) (defn- generate-sync-shape-inverse "Generate changes to update the component a shape is linked to, from @@ -395,23 +477,107 @@ shapes. And if the component shapes are, in turn, instances of a second component, their 'touched' flags will be set accordingly." - [root-shape all-shapes component root-component page-id] - (loop [shapes (seq all-shapes) - rchanges [] - uchanges []] - (let [shape (first shapes)] - (if (nil? shape) + [page-id shape-id local-file libraries] + (log/debug :msg "Sync inverse shape and children" :shape (str shape-id)) + (let [page (cph/get-container page-id nil local-file) + shape (cph/get-shape page shape-id) + component (cph/get-component (:component-id shape) + (:component-file shape) + local-file + libraries) + root-shape shape + root-component (cph/get-component-root component)] + + (generate-sync-shape-inverse-normal page + shape + component + root-shape + root-component))) + +(defn- generate-sync-shape-inverse-normal + [page shape component root-shape root-component] + (log/trace :msg "Sync shape inverse (normal)" + :shape (str (:name shape)) + :component (:name component)) + (let [[rchanges uchanges] + (generate-sync-shape->component shape + root-shape + root-component + component + (:id page)) + + children-ids (get shape :shapes [])] + + (loop [children-ids (seq children-ids) + rchanges rchanges + uchanges uchanges] + (let [child-id (first children-ids)] + (if (nil? child-id) + [rchanges uchanges] + (let [child-shape (cph/get-shape page child-id) + + [child-rchanges child-uchanges] + (if (nil? (:component-id child-shape)) + (generate-sync-shape-inverse-normal page + child-shape + component + root-shape + root-component) + (generate-sync-shape-inverse-nested page + child-shape + component + root-shape + root-component))] + (recur (next children-ids) + (d/concat rchanges child-rchanges) + (d/concat uchanges child-uchanges)))))))) + +(defn- generate-sync-shape-inverse-nested + [page shape component root-shape root-component] + (log/trace :msg "Sync shape inverse (nested)" + :shape (str (:name shape)) + :component (:name component)) + (let [component-shape (d/seek #(= (:shape-ref %) + (:shape-ref shape)) + (vals (:objects component))) + root-shape (if (:component-id shape) + shape + root-shape) + root-component (if (:component-id shape) + component-shape + root-component) + [rchanges uchanges] - (let [[shape-rchanges shape-uchanges] - (generate-sync-shape->component - shape - root-shape - root-component - component - page-id)] - (recur (next shapes) - (d/concat rchanges shape-rchanges) - (d/concat uchanges shape-uchanges))))))) + (update-attrs component-shape + shape + root-component + root-shape + nil + (:id component) + {:omit-touched? false + :reset-touched? false + :set-touched? false + :copy-touched? true}) + + children-ids (get shape :shapes [])] + + (loop [children-ids (seq children-ids) + rchanges rchanges + uchanges uchanges] + (let [child-id (first children-ids)] + (if (nil? child-id) + [rchanges uchanges] + (let [child-shape (cph/get-shape page child-id) + + [child-rchanges child-uchanges] + (generate-sync-shape-inverse-nested page + child-shape + component + root-shape + root-component)] + (recur (next children-ids) + (d/concat rchanges child-rchanges) + (d/concat uchanges child-uchanges)))))))) (defn- generate-sync-shape<-component "Generate changes to synchronize one shape that is linked to other shape @@ -436,18 +602,12 @@ "Generate changes to synchronize one shape inside a component, with other shape that is linked to it." [shape root-shape root-component component page-id] - ;; ===== Uncomment this to debug ===== - ;; (js/console.log "component" (clj->js component)) (if (nil? component) empty-changes (let [component-shape (get (:objects component) (:shape-ref shape))] - ;; ===== Uncomment this to debug ===== - ;; (js/console.log "component-shape" (clj->js component-shape)) (if (nil? component-shape) empty-changes - (let [;; ===== Uncomment this to debug ===== - ;; _(js/console.info "update" (:name shape) "->" (:name component-shape)) - [rchanges1 uchanges1] + (let [[rchanges1 uchanges1] (update-attrs component-shape shape root-component @@ -552,15 +712,17 @@ If set-touched? is true, the corresponding 'touched' flags will be set in dest shape if they are different than their current values." [dest-shape origin-shape dest-root origin-root page-id component-id - {:keys [omit-touched? reset-touched? set-touched?] :as options}] + {:keys [omit-touched? reset-touched? set-touched? copy-touched?] + :as options :or {omit-touched? false + reset-touched? false + set-touched? false + copy-touched? false}}] - ;; === Uncomment this to debug synchronization === - ;; (println "SYNC" - ;; "[C]" (:name origin-shape) - ;; "->" - ;; (if page-id "[W]" ["C"]) - ;; (:name dest-shape) - ;; (str options)) + (log/info :msg (str "SYNC " + (:name origin-shape) + " -> " + (if page-id "[W] " "[C] ") + (:name dest-shape))) (let [; The position attributes need a special sync algorith, because we do ; not synchronize the absolute position, but the position relative of @@ -582,16 +744,24 @@ (let [attr (first attrs)] (if (nil? attr) - (let [roperations (if reset-touched? + (let [roperations (cond + reset-touched? (conj roperations {:type :set-touched :touched nil}) + copy-touched? + (conj roperations + {:type :set-touched + :touched (:touched origin-shape)}) + :else roperations) - uoperations (if reset-touched? + uoperations (cond + (or reset-touched? copy-touched?) (conj uoperations {:type :set-touched :touched (:touched dest-shape)}) + :else uoperations) rchanges [(d/without-nils {:type :mod-obj diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index 7146814df8..cf8a129dd1 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -21,7 +21,6 @@ [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.circle :as circle] - [app.main.ui.shapes.icon :as icon] [app.main.ui.shapes.image :as image] [app.main.ui.shapes.path :as path] [app.main.ui.shapes.rect :as rect] @@ -85,7 +84,6 @@ (case (:type shape) :curve [:> path/path-shape opts] :text [:> text/text-shape opts] - :icon [:> icon/icon-shape opts] :rect [:> rect/rect-shape opts] :path [:> path/path-shape opts] :image [:> image/image-shape opts] diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 06b44ffda2..fd3a2c2cd4 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -39,7 +39,6 @@ ;; ---- Workspace refs - (def workspace-local (l/derived :workspace-local st/state)) @@ -56,7 +55,6 @@ (def selected-zoom (l/derived :zoom workspace-local)) - (def selected-drawing-tool (l/derived :tool workspace-drawing)) @@ -89,7 +87,6 @@ (assoc :pages (get-in file [:data :pages]))))) st/state =)) - (def workspace-file-colors (l/derived (fn [state] (when-let [file (:workspace-file state)] @@ -113,6 +110,15 @@ (def workspace-shared-files (l/derived :workspace-shared-files st/state)) +(def workspace-local-library + (l/derived (fn [state] + (select-keys (get state :workspace-data) + [:colors + :media + :typographies + :components])) + st/state)) + (def workspace-libraries (l/derived :workspace-libraries st/state)) diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index 5814c4a5d9..28936dd5f7 100644 --- a/frontend/src/app/main/store.cljs +++ b/frontend/src/app/main/store.cljs @@ -85,8 +85,9 @@ (logjs "state"))))) (defn ^:export dump-tree - ([] (dump-tree false)) - ([show-touched] + ([] (dump-tree false false)) + ([show-ids] (dump-tree show-ids false)) + ([show-ids show-touched] (let [page-id (get @state :current-page-id) objects (get-in @state [:workspace-data :pages-index page-id :objects]) components (get-in @state [:workspace-data :components]) @@ -98,6 +99,7 @@ (println (str/pad (str (str/repeat " " level) (:name shape) (when (seq (:touched shape)) "*") + (when show-ids (str/format " <%s>" (:id shape)))) {:length 20 :type :right}) (show-component shape objects)) @@ -107,7 +109,7 @@ (str (:touched shape))))) (when (:shapes shape) (dorun (for [shape-id (:shapes shape)] - (show-shape shape-id (inc level) objects))))))) + (show-shape shape-id (inc level) objects)))))) (show-component [shape objects] (if (nil? (:shape-ref shape)) @@ -129,7 +131,8 @@ (when component-file (str/format "<%s> " (:name component-file))) (:name component-shape) (if (or (:component-root? shape) - (nil? (:component-id shape))) + (nil? (:component-id shape)) + true) "" (let [component-id (:component-id shape) component-file-id (:component-file shape) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 66fddcf8e6..611ba44f1f 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -149,10 +149,12 @@ :workspace (let [project-id (uuid (get-in route [:params :path :project-id])) file-id (uuid (get-in route [:params :path :file-id])) - page-id (uuid (get-in route [:params :query :page-id]))] + page-id (uuid (get-in route [:params :query :page-id])) + layout-name (get-in route [:params :query :layout])] [:& workspace/workspace {:project-id project-id :file-id file-id :page-id page-id + :layout-name (keyword layout-name) :key file-id}]) :not-authorized diff --git a/frontend/src/app/main/ui/components/code_block.cljs b/frontend/src/app/main/ui/components/code_block.cljs new file mode 100644 index 0000000000..f1455233c1 --- /dev/null +++ b/frontend/src/app/main/ui/components/code_block.cljs @@ -0,0 +1,23 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.components.code-block + (:require + ["highlight.js" :as hljs] + [rumext.alpha :as mf])) + +(mf/defc code-block [{:keys [code type]}] + (let [block-ref (mf/use-ref)] + (mf/use-effect + (mf/deps code type block-ref) + (fn [] + (hljs/highlightBlock (mf/ref-val block-ref)))) + [:pre.code-display {:class type + :ref block-ref} code])) + diff --git a/frontend/src/app/main/ui/components/copy_button.cljs b/frontend/src/app/main/ui/components/copy_button.cljs new file mode 100644 index 0000000000..2f8e9d6107 --- /dev/null +++ b/frontend/src/app/main/ui/components/copy_button.cljs @@ -0,0 +1,35 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.components.copy-button + (:require + [beicon.core :as rx] + [rumext.alpha :as mf] + [app.util.webapi :as wapi] + [app.util.timers :as timers] + [app.main.ui.icons :as i])) + +(mf/defc copy-button [{:keys [data]}] + (let [just-copied (mf/use-state false)] + (mf/use-effect + (mf/deps @just-copied) + (fn [] + (when @just-copied + (let [sub (timers/schedule 1000 #(reset! just-copied false))] + ;; On unmount we dispose the timer + #(rx/-dispose sub))))) + + [:button.copy-button + {:on-click #(when-not @just-copied + (do + (reset! just-copied true) + (wapi/write-to-clipboard data)))} + (if @just-copied + i/tick + i/copy)])) diff --git a/frontend/src/app/main/ui/components/dropdown.cljs b/frontend/src/app/main/ui/components/dropdown.cljs index 209224bff9..9903c528e7 100644 --- a/frontend/src/app/main/ui/components/dropdown.cljs +++ b/frontend/src/app/main/ui/components/dropdown.cljs @@ -20,7 +20,7 @@ (if ref (let [target (dom/get-target event) parent (mf/ref-val ref)] - (when-not (.contains parent target) + (when-not (or (not parent) (.contains parent target)) (on-close))) (on-close))) diff --git a/frontend/src/app/main/ui/components/editable_label.cljs b/frontend/src/app/main/ui/components/editable_label.cljs index fa2de34878..6fac66f569 100644 --- a/frontend/src/app/main/ui/components/editable_label.cljs +++ b/frontend/src/app/main/ui/components/editable_label.cljs @@ -17,35 +17,43 @@ [app.util.data :refer [classnames]])) (mf/defc editable-label - [{:keys [ value on-change on-cancel edit readonly class-name]}] + [{:keys [value on-change on-cancel editing? disable-dbl-click? class-name]}] (let [input (mf/use-ref nil) state (mf/use-state (:editing false)) - is-editing (or edit (:editing @state)) + is-editing (:editing @state) start-editing (fn [] (swap! state assoc :editing true) (timers/schedule 100 #(dom/focus! (mf/ref-val input)))) stop-editing (fn [] (swap! state assoc :editing false)) + accept-editing (fn [] + (when (:editing @state) + (let [value (-> (mf/ref-val input) dom/get-value)] + (on-change value) + (stop-editing)))) cancel-editing (fn [] (stop-editing) (when on-cancel (on-cancel))) - on-dbl-click (fn [e] (when (not readonly) (start-editing))) + on-dbl-click (fn [e] (when (not disable-dbl-click?) (start-editing))) on-key-up (fn [e] (cond (kbd/esc? e) (cancel-editing) (kbd/enter? e) - (let [value (-> e dom/get-target dom/get-value)] - (on-change value) - (stop-editing)))) - ] + (accept-editing)))] + + (mf/use-effect + (mf/deps editing?) + (fn [] + (when (and editing? (not (:editing @state))) + (start-editing)))) (if is-editing [:div.editable-label {:class class-name} [:input.editable-label-input {:ref input :default-value value - :on-key-down on-key-up}] + :on-key-up on-key-up + :on-blur cancel-editing}] [:span.editable-label-close {:on-click cancel-editing} i/close]] [:span.editable-label {:class class-name - :on-double-click on-dbl-click} value] - ))) + :on-double-click on-dbl-click} value]))) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 333ab72d73..c4ceed6da6 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -393,18 +393,15 @@ [:li.recent-projects {:on-click go-projects :class-name (when projects? "current")} - i/recent [:span.element-title (t locale "labels.projects")]] [:li {:on-click go-drafts :class-name (when drafts? "current")} - i/file-html [:span.element-title (t locale "labels.drafts")]] [:li {:on-click go-libs :class-name (when libs? "current")} - i/library [:span.element-title (t locale "labels.shared-libraries")]]]] [:hr] @@ -442,6 +439,7 @@ [:div.profile-section {:on-click #(reset! show true)} [:img {:src photo}] [:span (:fullname profile)] + i/arrow-down [:& dropdown {:on-close #(reset! show false) :show @show} diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs new file mode 100644 index 0000000000..808548d6c5 --- /dev/null +++ b/frontend/src/app/main/ui/measurements.cljs @@ -0,0 +1,256 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.measurements + (:require + [rumext.alpha :as mf] + [cuerdas.core :as str] + [okulary.core :as l] + [app.common.data :as d] + [app.common.math :as mth] + [app.common.geom.shapes :as gsh] + [app.common.geom.point :as gpt] + [app.main.store :as st])) + +;; ------------------------------------------------ +;; CONSTANTS +;; ------------------------------------------------ + +(def font-size 10) +(def selection-rect-width 1) + +(def select-color "#1FDEA7") +(def select-guide-width 1) +(def select-guide-dasharray 5) + +(def hover-color "#DB00FF") +(def hover-color-text "#FFF") +(def hover-guide-width 1) + +(def size-display-color "#FFF") +(def size-display-opacity 0.7) +(def size-display-text-color "#000") +(def size-display-width-min 50) +(def size-display-width-max 75) +(def size-display-height 16) + +(def distance-color "#DB00FF") +(def distance-text-color "#FFF") +(def distance-border-radius 2) +(def distance-pill-width 40) +(def distance-pill-height 16) +(def distance-line-stroke 1) + +;; ------------------------------------------------ +;; HELPERS +;; ------------------------------------------------ + +(defn bound->selrect [bounds] + {:x (:x bounds) + :y (:y bounds) + :x1 (:x bounds) + :y1 (:y bounds) + :x2 (+ (:x bounds) (:width bounds)) + :y2 (+ (:y bounds) (:height bounds)) + :width (:width bounds) + :height (:height bounds)}) + +(defn calculate-guides + "Calculates coordinates for the selection guides" + [bounds selrect] + (let [{bounds-width :width bounds-height :height} bounds + {:keys [x y width height]} selrect] + [[(:x bounds) y (+ (:x bounds) bounds-width) y] + [(:x bounds) (+ y height) (+ (:x bounds) bounds-width) (+ y height)] + [x (:y bounds) x (+ (:y bounds) bounds-height)] + [(+ x width) (:y bounds) (+ x width) (+ (:y bounds) bounds-height)]])) + +(defn calculate-distance-lines + "Given a start/end from two shapes gives the distance lines" + [from-s from-e to-s to-e] + (let [ss (- to-s from-s) + se (- to-e from-s) + es (- to-s from-e) + ee (- to-e from-e)] + (cond-> [] + (or (and (neg? ss) (pos? se)) + (and (pos? ss) (neg? ee)) + (and (neg? ss) (> ss se))) + (conj [ from-s (+ from-s ss) ]) + + (or (and (neg? se) (<= ss se))) + (conj [ from-s (+ from-s se) ]) + + (or (and (pos? es) (<= es ee))) + (conj [ from-e (+ from-e es) ]) + + (or (and (pos? ee) (neg? es)) + (and (neg? ee) (pos? ss)) + (and (pos? ee) (< ee es))) + (conj [ from-e (+ from-e ee) ])))) + +;; ------------------------------------------------ +;; COMPONENTS +;; ------------------------------------------------ + +(mf/defc size-display [{:keys [type selrect zoom]}] + (let [{:keys [x y width height]} selrect + size-label (str/fmt "%s x %s" (mth/round width) (mth/round height)) + + rect-height (/ size-display-height zoom) + rect-width (/ (if (<= (count size-label) 9) + size-display-width-min + size-display-width-max) + zoom) + text-padding (/ 4 zoom)] + [:g.size-display + [:rect {:x (+ x (/ width 2) (- (/ rect-width 2))) + :y (- (+ y height) rect-height) + :width rect-width + :height rect-height + :style {:fill size-display-color + :fill-opacity size-display-opacity}}] + + [:text {:x (+ (+ x (/ width 2) (- (/ rect-width 2))) (/ rect-width 2)) + :y (- (+ y height (+ text-padding (/ rect-height 2))) rect-height) + :width rect-width + :height rect-height + :text-anchor "middle" + :style {:fill size-display-text-color + :font-size (/ font-size zoom)}} + size-label]])) + +(mf/defc distance-display-pill [{:keys [x y zoom distance bounds]}] + (let [distance-pill-width (/ distance-pill-width zoom) + distance-pill-height (/ distance-pill-height zoom) + distance-line-stroke (/ distance-line-stroke zoom) + font-size (/ font-size zoom) + text-padding (/ 3 zoom) + distance-border-radius (/ distance-border-radius zoom) + + {bounds-width :width bounds-height :height} bounds + + rect-x (- x (/ distance-pill-width 2)) + rect-y (- y (/ distance-pill-height 2)) + + text-x x + text-y (+ y text-padding) + + offset-x (cond (< rect-x (:x bounds)) (- (:x bounds) rect-x) + (> (+ rect-x distance-pill-width) (+ (:x bounds) bounds-width)) + (- (+ (:x bounds) bounds-width) (+ rect-x distance-pill-width)) + :else 0) + + offset-y (cond (< rect-y (:y bounds)) (- (:y bounds) rect-y) + (> (+ rect-y distance-pill-height) (+ (:y bounds) bounds-height)) + (- (+ (:y bounds) bounds-height) (+ rect-y distance-pill-height)) + :else 0)] + [:g.distance-pill + [:rect {:x (+ rect-x offset-x) + :y (+ rect-y offset-y) + :rx distance-border-radius + :ry distance-border-radius + :width distance-pill-width + :height distance-pill-height + :style {:fill distance-color}}] + + [:text {:x (+ text-x offset-x) + :y (+ text-y offset-y) + :rx distance-border-radius + :ry distance-border-radius + :text-anchor "middle" + :width distance-pill-width + :height distance-pill-height + :style {:fill distance-text-color + :font-size font-size}} + distance]])) + +(mf/defc selection-rect [{:keys [frame selrect zoom]}] + (let [{:keys [x y width height]} selrect + selection-rect-width (/ selection-rect-width zoom)] + [:g.selection-rect + [:rect {:x x + :y y + :width width + :height height + :style {:fill "transparent" + :stroke hover-color + :stroke-width selection-rect-width}}]])) + +(mf/defc distance-display [{:keys [type from to zoom frame bounds]}] + (let [fixed-x (if (gsh/fully-contained? from to) + (+ (:x to) (/ (:width to) 2)) + (+ (:x from) (/ (:width from) 2))) + fixed-y (if (gsh/fully-contained? from to) + (+ (:y to) (/ (:height to) 2)) + (+ (:y from) (/ (:height from) 2))) + + v-lines (->> (calculate-distance-lines (:y1 from) (:y2 from) (:y1 to) (:y2 to)) + (map (fn [[start end]] [fixed-x start fixed-x end]))) + + h-lines (->> (calculate-distance-lines (:x1 from) (:x2 from) (:x1 to) (:x2 to)) + (map (fn [[start end]] [start fixed-y end fixed-y]))) + + lines (d/concat [] v-lines h-lines)] + + (for [[x1 y1 x2 y2] lines] + (let [center-x (+ x1 (/ (- x2 x1) 2)) + center-y (+ y1 (/ (- y2 y1) 2)) + distance (gpt/distance (gpt/point x1 y1) (gpt/point x2 y2))] + [:g.distance-line {:key (str "line-%s-%s-%s-%s" x1 y1 x2 y2)} + [:line + {:x1 x1 + :y1 y1 + :x2 x2 + :y2 y2 + :style {:stroke distance-color + :stroke-width distance-line-stroke}}] + + [:& distance-display-pill + {:x center-x + :y center-y + :zoom zoom + :distance (str (mth/round distance) "px") + :bounds bounds}]])))) + +(mf/defc selection-guides [{:keys [bounds selrect zoom]}] + [:g.selection-guides + (for [[x1 y1 x2 y2] (calculate-guides bounds selrect)] + [:line {:x1 x1 + :y1 y1 + :x2 x2 + :y2 y2 + :style {:stroke select-color + :stroke-width (/ select-guide-width zoom) + :stroke-dasharray (/ select-guide-dasharray zoom)}}])]) + +(mf/defc measurement [{:keys [bounds frame selected-shapes hover-shape zoom]}] + (let [selected-selrect (gsh/selection-rect selected-shapes) + hover-selrect (:selrect hover-shape) + bounds-selrect (bound->selrect bounds)] + + (when (seq selected-shapes) + [:g.measurement-feedback {:pointer-events "none"} + [:& selection-guides {:selrect selected-selrect :bounds bounds :zoom zoom}] + [:& size-display {:selrect selected-selrect :zoom zoom}] + + (if (not hover-shape) + (when frame + [:g.hover-shapes + [:& distance-display {:from (:selrect frame) + :to selected-selrect + :zoom zoom + :bounds bounds-selrect}]]) + + [:g.hover-shapes + [:& selection-rect {:type :hover :selrect hover-selrect :zoom zoom}] + [:& size-display {:selrect hover-selrect :zoom zoom}] + [:& distance-display {:from hover-selrect :to selected-selrect :zoom zoom :bounds bounds-selrect}]])]))) + + diff --git a/frontend/src/app/main/ui/shapes/circle.cljs b/frontend/src/app/main/ui/shapes/circle.cljs index 0b442ea164..340186aa34 100644 --- a/frontend/src/app/main/ui/shapes/circle.cljs +++ b/frontend/src/app/main/ui/shapes/circle.cljs @@ -33,8 +33,7 @@ :cy cy :rx rx :ry ry - :transform transform - :id (str "shape-" id)}))] + :transform transform}))] [:& shape-custom-stroke {:shape shape :base-props props diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index 89a7374f3d..140b5fb570 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -97,7 +97,7 @@ (mf/defc layer-blur-filter [{:keys [filter-id params]}] - [:feGaussianBlur {:stdDeviation (/ (:value params) 2) + [:feGaussianBlur {:stdDeviation (:value params) :result filter-id}]) (mf/defc image-fix-filter [{:keys [filter-id]}] diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 952179e289..55b39bb1b9 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -31,7 +31,6 @@ (obj/merge! #js {:x 0 :y 0 - :id (str "shape-" id) :width width :height height}))] [:svg {:x x :y y :width width :height height diff --git a/frontend/src/app/main/ui/shapes/group.cljs b/frontend/src/app/main/ui/shapes/group.cljs index 6b4a5bf5c5..03bea3db6d 100644 --- a/frontend/src/app/main/ui/shapes/group.cljs +++ b/frontend/src/app/main/ui/shapes/group.cljs @@ -54,7 +54,6 @@ :y y :fill (if (debug? :group) "red" "transparent") :opacity 0.5 - :id (str "group-" id) :width width :height height}])]))) diff --git a/frontend/src/app/main/ui/shapes/icon.cljs b/frontend/src/app/main/ui/shapes/icon.cljs deleted file mode 100644 index bba2c92518..0000000000 --- a/frontend/src/app/main/ui/shapes/icon.cljs +++ /dev/null @@ -1,52 +0,0 @@ -;; 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/. -;; -;; This Source Code Form is "Incompatible With Secondary Licenses", as -;; defined by the Mozilla Public License, v. 2.0. -;; -;; Copyright (c) 2020 UXBOX Labs SL - -(ns app.main.ui.shapes.icon - (:require - [rumext.alpha :as mf] - [app.common.geom.shapes :as geom] - [app.main.ui.shapes.attrs :as attrs] - [app.main.ui.shapes.group :refer [mask-id-ctx]] - [app.util.object :as obj])) - -(mf/defc icon-shape - {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") - {:keys [id x y width height metadata rotation content]} shape - - mask-id (mf/use-ctx mask-id-ctx) - transform (geom/transform-matrix shape) - vbox (apply str (interpose " " (:view-box metadata))) - - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:x x - :y y - :transform transform - :id (str "shape-" id) - :width width - :height height - :viewBox vbox - :preserveAspectRatio "none" - :mask mask-id - :dangerouslySetInnerHTML #js {:__html content}}))] - [:g {:transform transform} - [:> "svg" props]])) - -(mf/defc icon-svg - [{:keys [shape] :as props}] - (let [{:keys [content id metadata]} shape - view-box (apply str (interpose " " (:view-box metadata))) - mask-id (mf/use-ctx mask-id-ctx) - props {:viewBox view-box - :id (str "shape-" id) - :mask mask-id - :dangerouslySetInnerHTML #js {:__html content}}] - [:& "svg" props])) diff --git a/frontend/src/app/main/ui/shapes/image.cljs b/frontend/src/app/main/ui/shapes/image.cljs index 0380ba3431..61ef85c791 100644 --- a/frontend/src/app/main/ui/shapes/image.cljs +++ b/frontend/src/app/main/ui/shapes/image.cljs @@ -43,7 +43,6 @@ #js {:x x :y y :transform transform - :id (str "shape-" id) :width width :height height :preserveAspectRatio "none" diff --git a/frontend/src/app/main/ui/shapes/path.cljs b/frontend/src/app/main/ui/shapes/path.cljs index a591df4871..677bc1649d 100644 --- a/frontend/src/app/main/ui/shapes/path.cljs +++ b/frontend/src/app/main/ui/shapes/path.cljs @@ -52,7 +52,6 @@ props (-> (attrs/extract-style-attrs shape) (obj/merge! #js {:transform transform - :id (str "shape-" id) :d pdata}))] (if background? [:g {:mask mask-id} diff --git a/frontend/src/app/main/ui/shapes/rect.cljs b/frontend/src/app/main/ui/shapes/rect.cljs index f2c8e98e97..555bafa5af 100644 --- a/frontend/src/app/main/ui/shapes/rect.cljs +++ b/frontend/src/app/main/ui/shapes/rect.cljs @@ -32,7 +32,6 @@ #js {:x x :y y :transform transform - :id (str "shape-" id) :width width :height height}))] diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 086f37bcb3..830a7057b2 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -18,27 +18,6 @@ [app.main.ui.shapes.gradients :as grad] [app.main.ui.context :as muc])) -(mf/defc background-blur [{:keys [shape]}] - (when-let [background-blur-filters (->> shape :blur (remove #(= (:type %) :layer-blur)) (remove :hidden))] - (for [filter background-blur-filters] - [:* - - - [:foreignObject {:key (str "blur_" (:id filter)) - :pointerEvents "none" - :x (:x shape) - :y (:y shape) - :width (:width shape) - :height (:height shape) - :transform (geom/transform-matrix shape)} - [:style ""] - [:div.backround-blur - {:style {:width "100%" - :height "100%" - ;; :backdrop-filter (str/format "blur(%spx)" (:value filter)) - :filter (str/format "blur(4px") - }}]]]))) - (mf/defc shape-container {::mf/wrap-props false} [props] @@ -50,24 +29,14 @@ group-props (-> props (obj/clone) (obj/without ["shape" "children"]) + (obj/set! "id" (str "shape-" (:id shape))) (obj/set! "className" "shape") - (obj/set! "data-type" (:type shape)) - (obj/set! "filter" (filters/filter-str filter-id shape))) - - ;;group-props (if (seq (:blur shape)) - ;; (obj/set! group-props "clip-path" (str/fmt "url(#%s)" (str "blur_" render-id))) - ;; group-props) - ] + (obj/set! "filter" (filters/filter-str filter-id shape)))] [:& (mf/provider muc/render-ctx) {:value render-id} [:> :g group-props [:defs [:& filters/filters {:shape shape :filter-id filter-id}] [:& grad/gradient {:shape shape :attr :fill-color-gradient}] - [:& grad/gradient {:shape shape :attr :stroke-color-gradient}] - - #_(when (:blur shape) - [:clipPath {:id (str "blur_" render-id)} - children])] - - [:& background-blur {:shape shape}] + [:& grad/gradient {:shape shape :attr :stroke-color-gradient}]] + children]])) diff --git a/frontend/src/app/main/ui/shapes/text.cljs b/frontend/src/app/main/ui/shapes/text.cljs index 240c0d0adb..3fbc1f972b 100644 --- a/frontend/src/app/main/ui/shapes/text.cljs +++ b/frontend/src/app/main/ui/shapes/text.cljs @@ -231,7 +231,6 @@ :y y :data-colors (retrieve-colors shape) :transform (geom/transform-matrix shape) - :id (str id) :width width :height height :mask mask-id} diff --git a/frontend/src/app/main/ui/viewer/handoff.cljs b/frontend/src/app/main/ui/viewer/handoff.cljs index cd1aa34cdd..fc06143f4d 100644 --- a/frontend/src/app/main/ui/viewer/handoff.cljs +++ b/frontend/src/app/main/ui/viewer/handoff.cljs @@ -37,7 +37,7 @@ (st/emit! (dv/select-shape (:id frame))))) (mf/defc render-panel - [{:keys [data local index]}] + [{:keys [data local index page-id file-id]}] (let [locale (mf/deref i18n/locale) frames (:frames data []) objects (:objects data) @@ -63,15 +63,18 @@ [:* [:& left-sidebar {:frame frame}] [:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)} - [:& render-frame-svg {:frame-id (:id frame) - :zoom (:zoom local) - :objects objects}]] - [:& right-sidebar {:frame frame}]])])) + [:div.handoff-svg-container + [:& render-frame-svg {:frame-id (:id frame) + :zoom (:zoom local) + :objects objects}]]] + [:& right-sidebar {:frame frame + :page-id page-id + :file-id file-id}]])])) (mf/defc handoff-content - [{:keys [data local index] :as props}] - (let [container (mf/use-ref) + [{:keys [data local index page-id file-id] :as props}] + (let [container (mf/use-ref) [toggle-fullscreen fullscreen?] (hooks/use-fullscreen container) on-mouse-wheel @@ -110,7 +113,9 @@ :screen :handoff}]) [:& render-panel {:data data :local local - :index index}]]])) + :index index + :page-id page-id + :file-id file-id}]]])) (mf/defc handoff [{:keys [file-id page-id index] :as props}] @@ -122,6 +127,8 @@ (let [data (mf/deref refs/viewer-data) local (mf/deref refs/viewer-local)] (when data - [:& handoff-content {:index index + [:& handoff-content {:file-id file-id + :page-id page-id + :index index :local local :data data}]))) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs index 2863ced879..4b29485eb1 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs @@ -12,6 +12,7 @@ [rumext.alpha :as mf] [app.util.i18n :as i18n] [app.common.geom.shapes :as gsh] + [app.main.ui.viewer.handoff.exports :refer [exports]] [app.main.ui.viewer.handoff.attributes.layout :refer [layout-panel]] [app.main.ui.viewer.handoff.attributes.fill :refer [fill-panel]] [app.main.ui.viewer.handoff.attributes.stroke :refer [stroke-panel]] @@ -20,13 +21,23 @@ [app.main.ui.viewer.handoff.attributes.image :refer [image-panel]] [app.main.ui.viewer.handoff.attributes.text :refer [text-panel]])) -(mf/defc attributes - [{:keys [shapes frame options]}] - (let [locale (mf/deref i18n/locale) - shapes (->> shapes - (map #(gsh/translate-to-frame % frame))) +(def type->options + {:multiple [:fill :stroke :image :text :shadow :blur] + :frame [:layout :fill] + :group [:layout] + :rect [:layout :fill :stroke :shadow :blur] + :circle [:layout :fill :stroke :shadow :blur] + :path [:layout :fill :stroke :shadow :blur] + :curve [:layout :fill :stroke :shadow :blur] + :image [:image :layout :shadow :blur] + :text [:layout :text :shadow :blur]}) - shape (first shapes)] +(mf/defc attributes + [{:keys [page-id file-id shapes frame]}] + (let [locale (mf/deref i18n/locale) + shapes (->> shapes (map #(gsh/translate-to-frame % frame))) + type (if (= (count shapes) 1) (-> shapes first :type) :multiple) + options (type->options type)] [:div.element-options (for [option options] [:> (case option @@ -39,5 +50,9 @@ :text text-panel) {:shapes shapes :frame frame - :locale locale}])])) - + :locale locale}]) + (when-not (= :multiple type) + [:& exports + {:shape (first shapes) + :page-id page-id + :file-id file-id}])])) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs index a84a91b9cb..203f5658c5 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs @@ -13,29 +13,30 @@ [cuerdas.core :as str] [app.util.i18n :refer [t]] [app.main.ui.icons :as i] - [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb]])) + [app.util.code-gen :as cg] + [app.main.ui.components.copy-button :refer [copy-button]])) (defn has-blur? [shape] (:blur shape)) -(defn copy-blur [shape] - (copy-cb shape - :blur - :to-prop "filter" - :format #(str/fmt "blur(%spx)" (:value %)))) +(defn copy-data [shape] + (cg/generate-css-props + shape + :blur + {:to-prop "filter" + :format #(str/fmt "blur(%spx)" (:value %))})) (mf/defc blur-panel [{:keys [shapes locale]}] - (let [shapes (->> shapes (filter has-blur?)) - handle-copy (when (= (count shapes) 1) (copy-blur (first shapes)))] + (let [shapes (->> shapes (filter has-blur?))] (when (seq shapes) [:div.attributes-block [:div.attributes-block-title [:div.attributes-block-title-text (t locale "handoff.attributes.blur")] - (when handle-copy - [:button.attributes-copy-button {:on-click handle-copy} i/copy])] + (when (= (count shapes) 1) + [:& copy-button {:data (copy-data (first shapes))}])] (for [shape shapes] [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.blur.value")] [:div.attributes-value (-> shape :blur :value) "px"] - [:button.attributes-copy-button {:on-click (copy-blur shape)} i/copy]])]))) + [:& copy-button {:data (copy-data shape)}]])]))) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs index b7ccd21fbf..8fe89b976e 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs @@ -11,71 +11,69 @@ (:require [rumext.alpha :as mf] [cuerdas.core :as str] + [okulary.core :as l] + [app.common.math :as mth] [app.util.dom :as dom] [app.util.i18n :refer [t] :as i18n] [app.util.color :as uc] - [app.common.math :as mth] - [app.main.ui.icons :as i] + [app.util.code-gen :as cg] [app.util.webapi :as wapi] + [app.main.ui.icons :as i] + [app.main.store :as st] + [app.main.ui.components.copy-button :refer [copy-button]] [app.main.ui.components.color-bullet :refer [color-bullet color-name]])) -(defn copy-cb [values properties & {:keys [to-prop format] :or {to-prop {}}}] - (fn [event] - (let [ - ;; We allow the :format and :to-prop to be a map for different properties - ;; or just a value for a single property. This code transform a single - ;; property to a uniform one - properties (if-not (coll? properties) [properties] properties) - format (if (not (map? format)) - (into {} (map #(vector % format) properties)) - format) +(def file-colors-ref + (l/derived (l/in [:viewer-data :file :colors]) st/state)) - to-prop (if (not (map? to-prop)) - (into {} (map #(vector % to-prop) properties)) - to-prop) +(defn make-colors-library-ref [file-id] + (let [get-library + (fn [state] + (get-in state [:viewer-libraries file-id :data :colors]))] + #(l/derived get-library st/state))) - default-format (fn [value] (str (mth/precision value 2) "px")) - format-property (fn [prop] - (let [css-prop (or (prop to-prop) (name prop))] - (str/fmt " %s: %s;" css-prop ((or (prop format) default-format) (prop values) values)))) +(mf/defc color-row [{:keys [color format copy-data on-change-format]}] + (let [locale (mf/deref i18n/locale) - text-props (->> properties - (remove #(let [value (get values %)] - (or (nil? value) (= value 0)))) - (map format-property) - (str/join "\n")) + colors-library-ref (mf/use-memo + (mf/deps (:file-id color)) + (make-colors-library-ref (:file-id color))) + colors-library (mf/deref colors-library-ref) - result (str/fmt "{\n%s\n}" text-props)] + file-colors (mf/deref file-colors-ref) - (wapi/write-to-clipboard result)))) - -(mf/defc color-row [{:keys [color format on-copy on-change-format]}] - (let [locale (mf/deref i18n/locale)] + color-library-name (get-in (or colors-library file-colors) [(:id color) :name])] [:div.attributes-color-row - [:& color-bullet {:color color}] + (when color-library-name + [:div.attributes-color-id + [:& color-bullet {:color color}] + [:div color-library-name]]) - (if (:gradient color) - [:& color-name {:color color}] - (case format - :rgba (let [[r g b a] (->> (uc/hex->rgba (:color color) (:opacity color)) (map #(mth/precision % 2)))] - [:div (str/fmt "%s, %s, %s, %s" r g b a)]) - :hsla (let [[h s l a] (->> (uc/hex->hsla (:color color) (:opacity color)) (map #(mth/precision % 2)))] - [:div (str/fmt "%s, %s, %s, %s" h s l a)]) - [:* - [:& color-name {:color color}] - (when-not (:gradient color) [:div (str (* 100 (:opacity color)) "%")])])) + [:div.attributes-color-value {:class (when color-library-name "hide-color")} + [:& color-bullet {:color color}] - (when-not (and on-change-format (:gradient color)) - [:select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)} - [:option {:value "hex"} - (t locale "handoff.attributes.color.hex")] + (if (:gradient color) + [:& color-name {:color color}] + (case format + :rgba (let [[r g b a] (->> (uc/hex->rgba (:color color) (:opacity color)) (map #(mth/precision % 2)))] + [:div (str/fmt "%s, %s, %s, %s" r g b a)]) + :hsla (let [[h s l a] (->> (uc/hex->hsla (:color color) (:opacity color)) (map #(mth/precision % 2)))] + [:div (str/fmt "%s, %s, %s, %s" h s l a)]) + [:* + [:& color-name {:color color}] + (when-not (:gradient color) [:div (str (* 100 (:opacity color)) "%")])])) - [:option {:value "rgba"} - (t locale "handoff.attributes.color.rgba")] + (when-not (and on-change-format (:gradient color)) + [:select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)} + [:option {:value "hex"} + (t locale "handoff.attributes.color.hex")] - [:option {:value "hsla"} - (t locale "handoff.attributes.color.hsla")]]) + [:option {:value "rgba"} + (t locale "handoff.attributes.color.rgba")] + + [:option {:value "hsla"} + (t locale "handoff.attributes.color.hsla")]])] + (when copy-data + [:& copy-button {:data copy-data}])])) - (when on-copy - [:button.attributes-copy-button {:on-click on-copy} i/copy])])) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs index c4c9d6e273..8afb6d5ba0 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs @@ -13,7 +13,9 @@ [app.util.i18n :refer [t]] [app.util.color :as uc] [app.main.ui.icons :as i] - [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb color-row]])) + [app.util.code-gen :as cg] + [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.viewer.handoff.attributes.common :refer [color-row]])) (def fill-attributes [:fill-color :fill-color-gradient]) @@ -21,8 +23,8 @@ {:color (:fill-color shape) :opacity (:fill-opacity shape) :gradient (:fill-color-gradient shape) - :id (:fill-ref-id shape) - :file-id (:fill-ref-file-id shape)}) + :id (:fill-color-ref-id shape) + :file-id (:fill-color-ref-file shape)}) (defn has-color? [shape] (and @@ -30,36 +32,31 @@ (or (:fill-color shape) (:fill-color-gradient shape)))) +(defn copy-data [shape] + (cg/generate-css-props + shape + fill-attributes + {:to-prop "background" + :format #(uc/color->background (shape->color shape))})) + (mf/defc fill-block [{:keys [shape locale]}] (let [color-format (mf/use-state :hex) - color (shape->color shape) - handle-copy (copy-cb shape - fill-attributes - :to-prop "background" - :format #(uc/color->background color))] + color (shape->color shape)] [:& color-row {:color color :format @color-format :on-change-format #(reset! color-format %) - :on-copy handle-copy}])) + :copy-data (copy-data shape)}])) (mf/defc fill-panel [{:keys [shapes locale]}] - (let [shapes (->> shapes (filter has-color?)) - handle-copy (when (= (count shapes) 1) - (copy-cb (first shapes) - fill-attributes - :to-prop "background" - :format #(-> shapes first shape->color uc/color->background)))] - + (let [shapes (->> shapes (filter has-color?))] (when (seq shapes) [:div.attributes-block [:div.attributes-block-title [:div.attributes-block-title-text (t locale "handoff.attributes.fill")] - (when handle-copy - [:button.attributes-copy-button - {:on-click handle-copy} - i/copy])] + (when (= (count shapes) 1) + [:& copy-button {:data (copy-data (first shapes))}])] (for [shape shapes] [:& fill-block {:key (str "fill-block-" (:id shape)) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs index 057c02369b..c4fd0c6397 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs @@ -14,7 +14,8 @@ [app.config :as cfg] [app.util.i18n :refer [t]] [app.main.ui.icons :as i] - [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb]])) + [app.util.code-gen :as cg] + [app.main.ui.components.copy-button :refer [copy-button]])) (defn has-image? [shape] (and (= (:type shape) :image))) @@ -30,12 +31,12 @@ [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.image.width")] [:div.attributes-value (-> shape :metadata :width) "px"] - [:button.attributes-copy-button {:on-click (copy-cb shape :width)} i/copy]] + [:& copy-button {:data (cg/generate-css-props shape :width)}]] [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.image.height")] [:div.attributes-value (-> shape :metadata :height) "px"] - [:button.attributes-copy-button {:on-click (copy-cb shape :height)} i/copy]] + [:& copy-button {:data (cg/generate-css-props shape :height)}]] (let [filename (last (str/split (-> shape :metadata :path) "/"))] [:a.download-button {:target "_blank" diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs index 748905449f..3aa984b7b8 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs @@ -14,13 +14,22 @@ [app.util.i18n :refer [t]] [app.common.math :as mth] [app.main.ui.icons :as i] - [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb]])) + [app.util.code-gen :as cg] + [app.main.ui.components.copy-button :refer [copy-button]])) -(defn copy-layout [shape] - (copy-cb shape - [:width :height :x :y :rotation] - :to-prop {:x "left" :y "top" :rotation "transform"} - :format {:rotation #(str/fmt "rotate(%sdeg)" %)})) +(def properties [:width :height :x :y :radius :rx]) +(def params + {:to-prop {:x "left" + :y "top" + :rotation "transform" + :rx "border-radius"} + :format {:rotation #(str/fmt "rotate(%sdeg)" %)}}) + +(defn copy-data + ([shape] + (apply copy-data shape properties)) + ([shape & properties] + (cg/generate-css-props shape properties params))) (mf/defc layout-block [{:keys [shape locale]}] @@ -28,57 +37,46 @@ [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.layout.width")] [:div.attributes-value (mth/precision (:width shape) 2) "px"] - [:button.attributes-copy-button - {:on-click (copy-cb shape :width)} - i/copy]] + [:& copy-button {:data (copy-data shape :width)}]] [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.layout.height")] [:div.attributes-value (mth/precision (:height shape) 2) "px"] - [:button.attributes-copy-button - {:on-click (copy-cb shape :height)} - i/copy]] + [:& copy-button {:data (copy-data shape :height)}]] (when (not= (:x shape) 0) [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.layout.left")] [:div.attributes-value (mth/precision (:x shape) 2) "px"] - [:button.attributes-copy-button - {:on-click (copy-cb shape :x :to-prop "left")} - i/copy]]) + [:& copy-button {:data (copy-data shape :x)}]]) (when (not= (:y shape) 0) [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.layout.top")] [:div.attributes-value (mth/precision (:y shape) 2) "px"] - [:button.attributes-copy-button - {:on-click (copy-cb shape :y :to-prop "top")} - i/copy]]) + [:& copy-button {:data (copy-data shape :y)}]]) + + (when (and (:rx shape) (not= (:rx shape) 0)) + [:div.attributes-unit-row + [:div.attributes-label (t locale "handoff.attributes.layout.radius")] + [:div.attributes-value (mth/precision (:rx shape) 2) "px"] + [:& copy-button {:data (copy-data shape :rx)}]]) (when (not= (:rotation shape 0) 0) [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.layout.rotation")] [:div.attributes-value (mth/precision (:rotation shape) 2) "deg"] - [:button.attributes-copy-button - {:on-click (copy-cb shape - :rotation - :to-prop "transform" - :format #(str/fmt "rotate(%sdeg)" %))} - i/copy]])]) + [:& copy-button {:data (copy-data shape :rotation)}]])]) (mf/defc layout-panel [{:keys [shapes locale]}] - (let [handle-copy (when (= (count shapes) 1) - (copy-layout (first shapes)))] - [:div.attributes-block - [:div.attributes-block-title - [:div.attributes-block-title-text (t locale "handoff.attributes.layout")] - (when handle-copy - [:button.attributes-copy-button - {:on-click handle-copy} - i/copy])] + [:div.attributes-block + [:div.attributes-block-title + [:div.attributes-block-title-text (t locale "handoff.attributes.layout")] + (when (= (count shapes) 1) + [:& copy-button {:data (copy-data (first shapes))}])] - (for [shape shapes] - [:& layout-block {:shape shape - :locale locale}])])) + (for [shape shapes] + [:& layout-block {:shape shape + :locale locale}])]) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs index 05ab8d2dca..44089cc463 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs @@ -12,23 +12,32 @@ [rumext.alpha :as mf] [cuerdas.core :as str] [app.util.i18n :refer [t]] - [app.util.color :as uc] + [app.util.code-gen :as cg] [app.main.ui.icons :as i] - [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb color-row]])) + [app.util.code-gen :as cg] + [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.viewer.handoff.attributes.common :refer [color-row]])) (defn has-shadow? [shape] (:shadow shape)) -(defn shadow->css [shadow] - (let [{:keys [style offset-x offset-y blur spread]} shadow - css-color (uc/color->background (:color shadow))] - (str - (if (= style :inner-shadow) "inset " "") - (str/fmt "%spx %spx %spx %spx %s" offset-x offset-y blur spread css-color)))) +(defn shape-copy-data [shape] + (cg/generate-css-props + shape + :shadow + {:to-prop "box-shadow" + :format #(str/join ", " (map cg/shadow->css (:shadow shape)))})) +(defn shadow-copy-data [shadow] + (cg/generate-css-props + shadow + :style + {:to-prop "box-shadow" + :format #(cg/shadow->css shadow)})) (mf/defc shadow-block [{:keys [shape locale shadow]}] - (let [color-format (mf/use-state :hex)] + (let [color-format (mf/use-state :hex) + copy-data (shadow-copy-data shadow)] [:div.attributes-shadow-block [:div.attributes-shadow-row [:div.attributes-label (->> shadow :style name (str "handoff.attributes.shadow.style.") (t locale))] @@ -48,32 +57,24 @@ [:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.spread")] [:div.attributes-value (str (:spread shadow))]] - [:button.attributes-copy-button - {:on-click (copy-cb shadow - :style - :to-prop "box-shadow" - :format #(shadow->css shadow))} - i/copy]] + [:& copy-button {:data (shadow-copy-data shadow)}]] + [:& color-row {:color (:color shadow) :format @color-format :on-change-format #(reset! color-format %)}]])) (mf/defc shadow-panel [{:keys [shapes locale]}] - (let [shapes (->> shapes (filter has-shadow?)) - handle-copy-shadow (when (= (count shapes) 1) - (copy-cb (first shapes) - :shadow - :to-prop "box-shadow" - :format #(str/join ", " (map shadow->css (:shadow (first shapes))))))] + (let [shapes (->> shapes (filter has-shadow?))] (when (seq shapes) [:div.attributes-block [:div.attributes-block-title [:div.attributes-block-title-text (t locale "handoff.attributes.shadow")] - (when handle-copy-shadow - [:button.attributes-copy-button {:on-click handle-copy-shadow} i/copy])] + (when (= (count shapes) 1) + [:& copy-button {:data (shape-copy-data (first shapes))}])] - (for [shape shapes] - (for [shadow (:shadow shape)] - [:& shadow-block {:shape shape - :locale locale - :shadow shadow}]))]))) + [:div.attributes-shadow-blocks + (for [shape shapes] + (for [shadow (:shadow shape)] + [:& shadow-block {:shape shape + :locale locale + :shadow shadow}]))]]))) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs index 6dc73f410d..fe26f8be5e 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs @@ -14,14 +14,16 @@ [app.util.i18n :refer [t]] [app.util.color :as uc] [app.main.ui.icons :as i] - [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb color-row]])) + [app.util.code-gen :as cg] + [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.viewer.handoff.attributes.common :refer [color-row]])) (defn shape->color [shape] {:color (:stroke-color shape) :opacity (:stroke-opacity shape) :gradient (:stroke-color-gradient shape) - :id (:stroke-ref-id shape) - :file-id (:stroke-ref-file-id shape)}) + :id (:stroke-color-ref-id shape) + :file-id (:stroke-color-ref-file shape)}) (defn format-stroke [shape] (let [width (:stroke-width shape) @@ -33,49 +35,46 @@ (and (:stroke-style shape) (not= (:stroke-style shape) :none))) +(defn copy-stroke-data [shape] + (cg/generate-css-props + shape + :stroke-style + {:to-prop "border" + :format #(format-stroke shape)})) + +(defn copy-color-data [shape] + (cg/generate-css-props + shape + :stroke-color + {:to-prop "border-color" + :format #(uc/color->background (shape->color shape))})) + (mf/defc stroke-block [{:keys [shape locale]}] (let [color-format (mf/use-state :hex) - color (shape->color shape) - handle-copy-stroke (copy-cb shape - :stroke-style - :to-prop "border" - :format #(format-stroke shape)) - - handle-copy-color (copy-cb shape - :stroke-color - :to-prop "border-color" - :format #(uc/color->background color))] - + color (shape->color shape)] [:* [:& color-row {:color color :format @color-format - :on-change-format #(reset! color-format %) - :on-copy handle-copy-color}] + :copy-data (copy-color-data shape) + :on-change-format #(reset! color-format %)}] [:div.attributes-stroke-row [:div.attributes-label (t locale "handoff.attributes.stroke.width")] [:div.attributes-value (:stroke-width shape) "px"] [:div.attributes-value (->> shape :stroke-style name (str "handoff.attributes.stroke.style.") (t locale))] [:div.attributes-label (->> shape :stroke-alignment name (str "handoff.attributes.stroke.alignment.") (t locale))] - [:button.attributes-copy-button {:on-click handle-copy-stroke} i/copy]]])) + [:& copy-button {:data (copy-stroke-data shape)}]]])) (mf/defc stroke-panel [{:keys [shapes locale]}] - (let [shapes (->> shapes (filter has-stroke?)) - handle-copy (when (= (count shapes) 1) - (copy-cb (first shapes) - :stroke-style - :to-prop "border" - :format #(format-stroke (first shapes))))] - + (let [shapes (->> shapes (filter has-stroke?))] (when (seq shapes) [:div.attributes-block [:div.attributes-block-title [:div.attributes-block-title-text (t locale "handoff.attributes.stroke")] - (when handle-copy - [:button.attributes-copy-button - {:on-click handle-copy} i/copy])] + (when (= (count shapes) 1) + [:& copy-button {:data (copy-stroke-data (first shapes))}])] (for [shape shapes] [:& stroke-block {:key (str "stroke-color-" (:id shape)) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs index b5847cc14d..9c1090f416 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs @@ -11,6 +11,7 @@ (:require [rumext.alpha :as mf] [cuerdas.core :as str] + [okulary.core :as l] [app.util.data :as d] [app.util.i18n :refer [t]] [app.util.color :as uc] @@ -18,11 +19,24 @@ [app.main.fonts :as fonts] [app.main.ui.icons :as i] [app.util.webapi :as wapi] - [app.main.ui.viewer.handoff.attributes.common :refer [copy-cb color-row]])) + [app.main.ui.viewer.handoff.attributes.common :refer [color-row]] + [app.util.code-gen :as cg] + [app.main.store :as st] + [app.main.ui.components.copy-button :refer [copy-button]])) (defn has-text? [shape] (:content shape)) +(def file-typographies-ref + (l/derived (l/in [:viewer-data :file :typographies]) st/state)) + +(defn make-typographies-library-ref [file-id] + (let [get-library + (fn [state] + (get-in state [:viewer-libraries file-id :data :typographies]))] + #(l/derived get-library st/state))) + + (def properties [:fill-color :fill-color-gradient :font-family @@ -37,100 +51,117 @@ {:color (:fill-color shape) :opacity (:fill-opacity shape) :gradient (:fill-color-gradient shape) - :id (:fill-ref-id shape) - :file-id (:fill-ref-file-id shape)}) + :id (:fill-color-ref-id shape) + :file-id (:fill-color-ref-file shape)}) -(defn format-style [color] - {:font-family #(str "'" % "'") - :font-style #(str "'" % "'") - :font-size #(str % "px") - :line-height #(str % "px") - :letter-spacing #(str % "px") - :text-decoration name - :text-transform name - :fill-color #(uc/color->background color) - :fill-color-gradient #(uc/color->background color)}) +(def params + {:to-prop {:fill-color "color" + :fill-color-gradient "color"} + :format {:font-family #(str "'" % "'") + :font-style #(str "'" % "'") + :font-size #(str % "px") + :line-height #(str % "px") + :letter-spacing #(str % "px") + :text-decoration name + :text-transform name + :fill-color #(-> %2 shape->color uc/color->background) + :fill-color-gradient #(-> %2 shape->color uc/color->background)}}) + +(defn copy-style-data + ([style] + (cg/generate-css-props style properties params)) + ([style & properties] + (cg/generate-css-props style properties params))) (mf/defc typography-block [{:keys [shape locale text style full-style]}] - (let [color-format (mf/use-state :hex) + (let [typography-library-ref (mf/use-memo + (mf/deps (:typography-ref-file style)) + (make-typographies-library-ref (:typography-ref-file style))) + typography-library (mf/deref typography-library-ref) + + file-typographies (mf/deref file-typographies-ref) + + color-format (mf/use-state :hex) color (shape->color style) - to-prop {:fill-color "color" - :fill-color-gradient "color"}] + + typography (get (or typography-library file-typographies) (:typography-ref-id style))] + [:div.attributes-text-block - [:div.attributes-typography-row - [:div.typography-sample - {:style {:font-family (:font-family full-style) - :font-weight (:font-weight full-style) - :font-style (:font-style full-style)}} - (t locale "workspace.assets.typography.sample")] - [:button.attributes-copy-button - {:on-click (copy-cb style properties - :to-prop to-prop - :format (format-style color))} - i/copy]] + (if (:typography-ref-id style) + [:div.attributes-typography-name-row + [:div.typography-entry + [:div.typography-sample + {:style {:font-family (:font-family typography) + :font-weight (:font-weight typography) + :font-style (:font-style typography)}} + (t locale "workspace.assets.typography.sample")]] + [:div.typography-entry-name (:name typography)] + [:& copy-button {:data (copy-style-data typography)}]] + + [:div.attributes-typography-row + [:div.typography-sample + {:style {:font-family (:font-family full-style) + :font-weight (:font-weight full-style) + :font-style (:font-style full-style)}} + (t locale "workspace.assets.typography.sample")] + [:& copy-button {:data (copy-style-data style)}]]) [:div.attributes-content-row [:pre.attributes-content (str/trim text)] - [:button.attributes-copy-button - {:on-click #(wapi/write-to-clipboard (str/trim text))} - i/copy]] + [:& copy-button {:data (str/trim text)}]] (when (or (:fill-color style) (:fill-color-gradient style)) [:& color-row {:format @color-format - :on-change-format #(reset! color-format %) :color (shape->color style) - :on-copy (copy-cb style - [:fill-color :fill-color-gradient] - :to-prop to-prop - :format (format-style color))}]) + :copy-data (copy-style-data style :fill-color :fill-color-gradient) + :on-change-format #(reset! color-format %)}]) (when (:font-id style) [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.typography.font-family")] [:div.attributes-value (-> style :font-id fonts/get-font-data :name)] - [:button.attributes-copy-button {:on-click (copy-cb style :font-family :format identity)} i/copy]]) + [:& copy-button {:data (copy-style-data style :font-family)}]]) (when (:font-style style) [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.typography.font-style")] [:div.attributes-value (str (:font-style style))] - [:button.attributes-copy-button {:on-click (copy-cb style :font-style :format identity)} i/copy]]) + [:& copy-button {:data (copy-style-data style :font-style)}]]) (when (:font-size style) [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.typography.font-size")] [:div.attributes-value (str (:font-size style)) "px"] - [:button.attributes-copy-button {:on-click (copy-cb style :font-size :format #(str % "px"))} i/copy]]) + [:& copy-button {:data (copy-style-data style :font-size)}]]) (when (:line-height style) [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.typography.line-height")] [:div.attributes-value (str (:line-height style)) "px"] - [:button.attributes-copy-button {:on-click (copy-cb style :line-height :format #(str % "px"))} i/copy]]) + [:& copy-button {:data (copy-style-data style :line-height)}]]) (when (:letter-spacing style) [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.typography.letter-spacing")] [:div.attributes-value (str (:letter-spacing style)) "px"] - [:button.attributes-copy-button {:on-click (copy-cb style :letter-spacing :format #(str % "px"))} i/copy]]) + [:& copy-button {:data (copy-style-data style :letter-spacing)}]]) (when (:text-decoration style) [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.typography.text-decoration")] [:div.attributes-value (->> style :text-decoration (str "handoff.attributes.typography.text-decoration.") (t locale))] - [:button.attributes-copy-button {:on-click (copy-cb style :text-decoration :format name)} i/copy]]) + [:& copy-button {:data (copy-style-data style :text-decoration)}]]) (when (:text-transform style) [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.typography.text-transform")] [:div.attributes-value (->> style :text-transform (str "handoff.attributes.typography.text-transform.") (t locale))] - [:button.attributes-copy-button {:on-click (copy-cb style :text-transform :format name)} i/copy]])])) + [:& copy-button {:data (copy-style-data style :text-transform)}]])])) (mf/defc text-block [{:keys [shape locale]}] (let [font (ut/search-text-attrs (:content shape) (keys ut/default-text-attrs)) - style-text-blocks (->> (keys ut/default-text-attrs) (ut/parse-style-text-blocks (:content shape)) (remove (fn [[style text]] (str/empty? (str/trim text)))) diff --git a/frontend/src/app/main/ui/viewer/handoff/code.cljs b/frontend/src/app/main/ui/viewer/handoff/code.cljs new file mode 100644 index 0000000000..19ff75c576 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/code.cljs @@ -0,0 +1,93 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.code + (:require + ["js-beautify" :as beautify] + [cuerdas.core :as str] + [rumext.alpha :as mf] + [app.util.i18n :as i18n] + [app.util.dom :as dom] + [app.util.code-gen :as cg] + [app.main.ui.icons :as i] + [app.common.geom.shapes :as gsh] + [app.main.ui.components.copy-button :refer [copy-button]] + [app.main.ui.components.code-block :refer [code-block]])) + +(defn generate-markup-code [type shapes] + (let [frame (dom/query js/document "#svg-frame") + markup-shape + (fn [shape] + (let [selector (str "#shape-" (:id shape) (when (= :text (:type shape)) " .root"))] + (when-let [el (and frame (dom/query frame selector))] + (str + (str/fmt "" (:name shape)) + (.-outerHTML el)))))] + (->> shapes + (map markup-shape ) + (remove nil?) + (str/join "\n\n")))) + +(defn format-code [code type] + (let [code (-> code + (str/replace "" "") + (str/replace "><" ">\n<"))] + (cond-> code + (= type "svg") (beautify/html #js {"indent_size" 2})))) + +(mf/defc code + [{:keys [shapes frame on-expand]}] + (let [style-type (mf/use-state "css") + markup-type (mf/use-state "svg") + + locale (mf/deref i18n/locale) + shapes (->> shapes + (map #(gsh/translate-to-frame % frame))) + + style-code (-> (cg/generate-style-code @style-type shapes) + (format-code "css")) + + markup-code (-> (mf/use-memo (mf/deps shapes) #(generate-markup-code @markup-type shapes)) + (format-code "svg"))] + [:div.element-options + [:div.code-block + [:div.code-row-lang + [:select.code-selection + [:option {:value "css"} "CSS"] + #_[:option {:value "sass"} "SASS"] + #_[:option {:value "less"} "Less"] + #_[:option {:value "stylus"} "Stylus"]] + + [:button.expand-button + {:on-click on-expand } + i/full-screen] + + [:& copy-button { :data style-code }]] + + [:div.code-row-display + [:& code-block {:type @style-type + :code style-code}]]] + + [:div.code-block + [:div.code-row-lang + [:select.code-selection + [:option "SVG"] + [:option "HTML"]] + + [:button.expand-button + {:on-click on-expand} + i/full-screen] + + [:& copy-button { :data markup-code }]] + + [:div.code-row-display + [:& code-block {:type @markup-type + :code markup-code}]]] + + ])) diff --git a/frontend/src/app/main/ui/viewer/handoff/exports.cljs b/frontend/src/app/main/ui/viewer/handoff/exports.cljs new file mode 100644 index 0000000000..4dbc303c3c --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/exports.cljs @@ -0,0 +1,132 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.exports + (:require + [rumext.alpha :as mf] + [beicon.core :as rx] + [app.util.i18n :refer [t] :as i18n] + [app.common.geom.shapes :as gsh] + [app.main.ui.icons :as i] + [app.common.data :as d] + [app.util.dom :as dom] + [app.main.store :as st] + [app.main.data.messages :as dm] + [app.main.ui.workspace.sidebar.options.exports :as we])) + +(mf/defc exports + [{:keys [shape page-id file-id] :as props}] + (let [locale (mf/deref i18n/locale) + exports (mf/use-state (:exports shape [])) + loading? (mf/use-state false) + + on-download + (mf/use-callback + (mf/deps shape @exports) + (fn [event] + (dom/prevent-default event) + (swap! loading? not) + (->> (we/request-export (assoc shape :page-id page-id :file-id file-id) @exports) + (rx/subs + (fn [{:keys [status body] :as response}] + (js/console.log status body) + (if (= status 200) + (we/trigger-download (:name shape) body) + (st/emit! (dm/error (t locale "errors.unexpected-error"))))) + (constantly nil) + (fn [] + (swap! loading? not)))))) + + add-export + (mf/use-callback + (mf/deps shape) + (fn [] + (let [xspec {:type :png + :suffix "" + :scale 1}] + (swap! exports conj xspec)))) + + delete-export + (mf/use-callback + (mf/deps shape) + (fn [index] + (swap! exports (fn [exports] + (let [[before after] (split-at index exports)] + (d/concat [] before (rest after))))))) + + on-scale-change + (mf/use-callback + (mf/deps shape) + (fn [index event] + (let [target (dom/get-target event) + value (dom/get-value target) + value (d/parse-double value)] + (swap! exports assoc-in [index :scale] value)))) + + on-suffix-change + (mf/use-callback + (mf/deps shape) + (fn [index event] + (let [target (dom/get-target event) + value (dom/get-value target)] + (swap! exports assoc-in [index :suffix] value)))) + + on-type-change + (mf/use-callback + (mf/deps shape) + (fn [index event] + (let [target (dom/get-target event) + value (dom/get-value target) + value (keyword value)] + (swap! exports assoc-in [index :type] value))))] + + (mf/use-effect + (mf/deps shape) + (fn [] + (reset! exports (:exports shape [])))) + + [:div.element-set.exports-options + [:div.element-set-title + [:span (t locale "workspace.options.export")] + [:div.add-page {:on-click add-export} i/close]] + + (when (seq @exports) + [:div.element-set-content + (for [[index export] (d/enumerate @exports)] + [:div.element-set-options-group + {:key index} + [:select.input-select {:on-change (partial on-scale-change index) + :value (:scale export)} + [:option {:value "0.5"} "0.5x"] + [:option {:value "0.75"} "0.75x"] + [:option {:value "1"} "1x"] + [:option {:value "1.5"} "1.5x"] + [:option {:value "2"} "2x"] + [:option {:value "4"} "4x"] + [:option {:value "6"} "6x"]] + + [:input.input-text {:on-change (partial on-suffix-change index) + :value (:suffix export)}] + [:select.input-select {:on-change (partial on-type-change index) + :value (name (:type export))} + [:option {:value "png"} "PNG"] + [:option {:value "jpeg"} "JPEG"] + [:option {:value "svg"} "SVG"]] + + [:div.delete-icon {:on-click (partial delete-export index)} + i/minus]]) + + [:div.btn-icon-dark.download-button + {:on-click (when-not @loading? on-download) + :class (dom/classnames :btn-disabled @loading?) + :disabled @loading?} + (if @loading? + (t locale "workspace.options.exporting-object") + (t locale "workspace.options.export-object"))]])])) + diff --git a/frontend/src/app/main/ui/viewer/handoff/render.cljs b/frontend/src/app/main/ui/viewer/handoff/render.cljs index ab760080f0..7f5c238dc6 100644 --- a/frontend/src/app/main/ui/viewer/handoff/render.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/render.cljs @@ -26,7 +26,6 @@ [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.group :as group] - [app.main.ui.shapes.icon :as icon] [app.main.ui.shapes.image :as image] [app.main.ui.shapes.path :as path] [app.main.ui.shapes.rect :as rect] @@ -111,7 +110,6 @@ [objects show-interactions?] (let [path-wrapper (shape-wrapper-factory path/path-shape) text-wrapper (shape-wrapper-factory text/text-shape) - icon-wrapper (shape-wrapper-factory icon/icon-shape) rect-wrapper (shape-wrapper-factory rect/rect-shape) image-wrapper (shape-wrapper-factory image/image-shape) circle-wrapper (shape-wrapper-factory circle/circle-shape)] @@ -130,7 +128,6 @@ (case (:type shape) :curve [:> path-wrapper opts] :text [:> text-wrapper opts] - :icon [:> icon-wrapper opts] :rect [:> rect-wrapper opts] :path [:> path-wrapper opts] :image [:> image-wrapper opts] @@ -163,7 +160,8 @@ (mf/deps objects) #(frame-container-factory objects))] - [:svg {:view-box vbox + [:svg {:id "svg-frame" + :view-box vbox :width width :height height :version "1.1" diff --git a/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs b/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs index 3a97ff6b2f..210503d6c6 100644 --- a/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs @@ -15,8 +15,9 @@ [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.components.tab-container :refer [tab-container tab-element]] + [app.main.ui.workspace.sidebar.layers :refer [element-icon]] [app.main.ui.viewer.handoff.attributes :refer [attributes]] - [app.main.ui.workspace.sidebar.layers :refer [element-icon]])) + [app.main.ui.viewer.handoff.code :refer [code]])) (defn make-selected-shapes-iref [] @@ -28,33 +29,15 @@ (mapv resolve-shape selected)))] #(l/derived selected->shapes st/state))) -(mf/defc attributes-panel [{:keys [frame shapes]}] - (let [type (if (= (count shapes) 1) - (-> shapes first :type) - :multiple)] - (let [options (case type - :multiple [:fill :stroke :image :text :shadow :blur] - :frame [:layout :fill] - :group [:layout] - :rect [:layout :fill :stroke :shadow :blur] - :circle [:layout :fill :stroke :shadow :blur] - :path [:layout :fill :stroke :shadow :blur] - :curve [:layout :fill :stroke :shadow :blur] - :image [:image :layout :shadow :blur] - :text [:layout :text :shadow :blur])] - [:& attributes {:frame frame - :shapes shapes - :options options}]))) -(mf/defc code-panel [] - [:div.element-options]) - -(mf/defc right-sidebar [{:keys [frame]}] - (let [locale (mf/deref i18n/locale) +(mf/defc right-sidebar + [{:keys [frame page-id file-id]}] + (let [expanded (mf/use-state false) + locale (mf/deref i18n/locale) section (mf/use-state :info #_:code) selected-ref (mf/use-memo (make-selected-shapes-iref)) shapes (mf/deref selected-ref)] - [:aside.settings-bar.settings-bar-right + [:aside.settings-bar.settings-bar-right {:class (when @expanded "expanded")} [:div.settings-bar-inside (when (seq shapes) [:div.tool-window @@ -69,11 +52,17 @@ [:span.tool-window-bar-title (->> shapes first :type name (str "handoff.tabs.code.selected.") (t locale))]]) ] [:div.tool-window-content - [:& tab-container {:on-change-tab #(reset! section %) + [:& tab-container {:on-change-tab #(do + (reset! expanded false) + (reset! section %)) :selected @section} [:& tab-element {:id :info :title (t locale "handoff.tabs.info")} - [:& attributes-panel {:frame frame - :shapes shapes}]] + [:& attributes {:page-id page-id + :file-id file-id + :frame frame + :shapes shapes}]] [:& tab-element {:id :code :title (t locale "handoff.tabs.code")} - [:& code-panel]]]]])]])) + [:& code {:frame frame + :shapes shapes + :on-expand #(swap! expanded not)}]]]]])]])) diff --git a/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs b/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs index 9b362a1f34..01bbae2a6d 100644 --- a/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs @@ -10,15 +10,30 @@ (ns app.main.ui.viewer.handoff.selection-feedback (:require [rumext.alpha :as mf] + [cuerdas.core :as str] [okulary.core :as l] + [app.common.data :as d] + [app.common.math :as mth] [app.common.geom.shapes :as gsh] - [app.main.store :as st])) + [app.common.geom.point :as gpt] + [app.main.store :as st] + [app.main.ui.measurements :refer [selection-guides size-display measurement]])) -(def selection-rect-color-normal "#1FDEA7") -(def selection-rect-color-component "#00E0FF") +;; ------------------------------------------------ +;; CONSTANTS +;; ------------------------------------------------ + +(def select-color "#1FDEA7") (def selection-rect-width 1) +(def select-guide-width 1) +(def select-guide-dasharray 5) + +;; ------------------------------------------------ +;; LENSES +;; ------------------------------------------------ (defn make-selected-shapes-iref + "Creates a lens to the current selected shapes" [] (let [selected->shapes (fn [state] @@ -29,38 +44,67 @@ #(l/derived selected->shapes st/state))) (defn make-hover-shapes-iref + "Creates a lens to the shapes the user is making hover" [] (let [hover->shapes (fn [state] (let [hover (get-in state [:viewer-local :hover]) - objects (get-in state [:viewer-data :page :objects]) - resolve-shape #(get objects %)] - (mapv resolve-shape hover)))] + objects (get-in state [:viewer-data :page :objects])] + (get objects hover)))] #(l/derived hover->shapes st/state))) -(mf/defc selection-rect [{:keys [shape]}] - (let [{:keys [x y width height]} (:selrect shape)] - [:rect {:x x - :y y - :width width - :height height - :fill "transparent" - :stroke selection-rect-color-normal - :stroke-width selection-rect-width - :pointer-events "none"}])) +(def selected-zoom + (l/derived (l/in [:viewer-local :zoom]) st/state)) + +;; ------------------------------------------------ +;; HELPERS +;; ------------------------------------------------ + +(defn frame->selrect [frame] + {:x1 0 + :y1 0 + :x2 (:width frame) + :y2 (:height frame) + :width (:width frame) + :height (:height frame)}) + +;; ------------------------------------------------ +;; COMPONENTS +;; ------------------------------------------------ + +(mf/defc selection-rect [{:keys [frame selrect zoom]}] + (let [{:keys [x y width height]} selrect + selection-rect-width (/ selection-rect-width zoom)] + [:g.selection-rect + [:rect {:x x + :y y + :width width + :height height + :style {:fill "transparent" + :stroke select-color + :stroke-width selection-rect-width}}]])) (mf/defc selection-feedback [{:keys [frame]}] - (let [hover-shapes-ref (mf/use-memo (make-hover-shapes-iref)) - hover-shapes (->> (mf/deref hover-shapes-ref) - (map #(gsh/translate-to-frame % frame))) - + (let [zoom (mf/deref selected-zoom) + + hover-shapes-ref (mf/use-memo (make-hover-shapes-iref)) + hover-shape (-> (mf/deref hover-shapes-ref) + (gsh/translate-to-frame frame)) + selected-shapes-ref (mf/use-memo (make-selected-shapes-iref)) selected-shapes (->> (mf/deref selected-shapes-ref) - (map #(gsh/translate-to-frame % frame)))] + (map #(gsh/translate-to-frame % frame))) - [:* - (for [shape hover-shapes] - [:& selection-rect {:shape shape}]) + selrect (gsh/selection-rect selected-shapes)] - (for [shape selected-shapes] - [:& selection-rect {:shape shape}])])) + (when (seq selected-shapes) + [:g.selection-feedback {:pointer-events "none"} + [:g.selected-shapes + [:& selection-guides {:selrect selrect :frame frame :zoom zoom}] + [:& selection-rect {:selrect selrect :zoom zoom}] + [:& size-display {:selrect selrect :zoom zoom}]] + + [:& measurement {:bounds frame + :selected-shapes selected-shapes + :hover-shape hover-shape + :zoom zoom}]]))) diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 7ceb6166c8..8cca3ce1e6 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -21,7 +21,6 @@ [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.group :as group] - [app.main.ui.shapes.icon :as icon] [app.main.ui.shapes.image :as image] [app.main.ui.shapes.path :as path] [app.main.ui.shapes.rect :as rect] @@ -86,10 +85,6 @@ [show-interactions?] (generic-wrapper-factory rect/rect-shape show-interactions?)) -(defn icon-wrapper - [show-interactions?] - (generic-wrapper-factory icon/icon-shape show-interactions?)) - (defn image-wrapper [show-interactions?] (generic-wrapper-factory image/image-shape show-interactions?)) @@ -142,7 +137,6 @@ [objects show-interactions?] (let [path-wrapper (path-wrapper show-interactions?) text-wrapper (text-wrapper show-interactions?) - icon-wrapper (icon-wrapper show-interactions?) rect-wrapper (rect-wrapper show-interactions?) image-wrapper (image-wrapper show-interactions?) circle-wrapper (circle-wrapper show-interactions?)] @@ -160,7 +154,6 @@ (case (:type shape) :curve [:> path-wrapper opts] :text [:> text-wrapper opts] - :icon [:> icon-wrapper opts] :rect [:> rect-wrapper opts] :path [:> path-wrapper opts] :image [:> image-wrapper opts] diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index aa3cb8023e..ea77a68b97 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -99,8 +99,10 @@ i/loader-pencil]) (mf/defc workspace - [{:keys [project-id file-id page-id] :as props}] - (mf/use-effect #(st/emit! dw/initialize-layout)) + [{:keys [project-id file-id page-id layout-name] :as props}] + (mf/use-effect + (mf/deps layout-name) + #(st/emit! (dw/initialize-layout layout-name))) (mf/use-effect (mf/deps project-id file-id) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 00043faea3..5928fc1788 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -75,6 +75,7 @@ (st/emit! (dwl/update-component id)) (st/emit! (dwl/sync-file nil)) (st/emit! dwc/commit-undo-transaction)) + do-show-component #(st/emit! (dw/go-to-layout :assets)) do-navigate-component-file #(st/emit! (dwl/nav-to-component-file (:component-file shape)))] [:* @@ -139,25 +140,38 @@ [:& menu-entry {:title (t locale "workspace.shape.menu.lock") :on-click do-lock-shape}]) - (when (nil? (:shape-ref shape)) + (when (or (nil? (:shape-ref shape)) + (> (count selected) 1)) [:* [:& menu-separator] [:& menu-entry {:title (t locale "workspace.shape.menu.create-component") :shortcut "Ctrl + K" :on-click do-add-component}]]) - (when (:component-id shape) - [:* - [:& menu-separator] - [:& menu-entry {:title (t locale "workspace.shape.menu.detach-instance") - :on-click do-detach-component}] - [:& menu-entry {:title (t locale "workspace.shape.menu.reset-overrides") - :on-click do-reset-component}] - (if (nil? (:component-file shape)) + (when (and (:component-id shape) + (= (count selected) 1)) + ;; WARNING: this menu is the same as the context menu at the sidebar. + ;; If you change it, you must change equally the file + ;; app/main/ui/workspace/sidebar/options/component.cljs + (if (nil? (:component-file shape)) + [:* + [:& menu-separator] + [:& menu-entry {:title (t locale "workspace.shape.menu.detach-instance") + :on-click do-detach-component}] + [:& menu-entry {:title (t locale "workspace.shape.menu.reset-overrides") + :on-click do-reset-component}] [:& menu-entry {:title (t locale "workspace.shape.menu.update-master") :on-click do-update-component}] + [:& menu-entry {:title (t locale "workspace.shape.menu.show-master") + :on-click do-show-component}]] + [:* + [:& menu-separator] + [:& menu-entry {:title (t locale "workspace.shape.menu.detach-instance") + :on-click do-detach-component}] + [:& menu-entry {:title (t locale "workspace.shape.menu.reset-overrides") + :on-click do-reset-component}] [:& menu-entry {:title (t locale "workspace.shape.menu.go-master") - :on-click do-navigate-component-file}])]) + :on-click do-navigate-component-file}]])) [:& menu-separator] [:& menu-entry {:title (t locale "workspace.shape.menu.delete") diff --git a/frontend/src/app/main/ui/workspace/left_toolbar.cljs b/frontend/src/app/main/ui/workspace/left_toolbar.cljs index 8a1cd65c4f..d67f40e14c 100644 --- a/frontend/src/app/main/ui/workspace/left_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/left_toolbar.cljs @@ -105,17 +105,17 @@ [:li.tooltip.tooltip-right {:alt "Layers" :class (when (contains? layout :layers) "selected") - :on-click (st/emitf (dw/ensure-layout :layers))} + :on-click (st/emitf (dw/go-to-layout :layers))} i/layers] [:li.tooltip.tooltip-right {:alt (t locale "workspace.toolbar.assets") :class (when (contains? layout :assets) "selected") - :on-click (st/emitf (dw/ensure-layout :assets))} + :on-click (st/emitf (dw/go-to-layout :assets))} i/library] [:li.tooltip.tooltip-right {:alt "History" :class (when (contains? layout :document-history) "selected") - :on-click (st/emitf (dw/ensure-layout :document-history))} + :on-click (st/emitf (dw/go-to-layout :document-history))} i/undo-history] [:li.tooltip.tooltip-right {:alt (t locale "workspace.toolbar.color-palette") diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index 1b22620817..8138379361 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.libraries (:require + [app.common.data :as d] [rumext.alpha :as mf] [cuerdas.core :as str] [okulary.core :as l] @@ -142,7 +143,6 @@ :value (tr "workspace.libraries.update") :on-click #(update-library (:id library))}]])]])])) - (mf/defc libraries-dialog {::mf/register modal/components ::mf/register-as :libraries-dialog} @@ -152,7 +152,9 @@ locale (mf/deref i18n/locale) project (mf/deref refs/workspace-project) file (mf/deref workspace-file) - libraries (mf/deref refs/workspace-libraries) + libraries (->> (mf/deref refs/workspace-libraries) + (d/removem (fn [[key val]] + (:is-indirect val)))) shared-files (mf/deref refs/workspace-shared-files) change-tab #(reset! selected-tab %) diff --git a/frontend/src/app/main/ui/workspace/selection.cljs b/frontend/src/app/main/ui/workspace/selection.cljs index eda6c18880..4c149c3cbb 100644 --- a/frontend/src/app/main/ui/workspace/selection.cljs +++ b/frontend/src/app/main/ui/workspace/selection.cljs @@ -15,6 +15,8 @@ [potok.core :as ptk] [rumext.alpha :as mf] [rumext.util :refer [map->obj]] + [app.common.uuid :as uuid] + [app.util.data :as d] [app.main.data.workspace :as dw] [app.main.data.workspace.common :as dwc] [app.main.refs :as refs] @@ -28,7 +30,8 @@ [app.common.geom.point :as gpt] [app.common.geom.matrix :as gmt] [app.util.debug :refer [debug?]] - [app.main.ui.workspace.shapes.outline :refer [outline]])) + [app.main.ui.workspace.shapes.outline :refer [outline]] + [app.main.ui.measurements :as msr])) (def rotation-handler-size 25) (def resize-point-radius 4) @@ -138,8 +141,7 @@ [:circle {:r (/ resize-point-radius zoom) :style {:fillOpacity "1" :strokeWidth "1px" - :vectorEffect "non-scaling-stroke" - } + :vectorEffect "non-scaling-stroke"} :fill "#FFFFFF" :stroke (if (and (= position :bottom-right) overflow-text) "red" color) :cx cx' @@ -266,9 +268,16 @@ :fill "transparent"}}]])) (mf/defc multiple-selection-handlers - [{:keys [shapes selected zoom color] :as props}] + [{:keys [shapes selected zoom color show-distances] :as props}] (let [shape (geom/selection-rect shapes) shape-center (geom/center shape) + + hover-id (-> (mf/deref refs/current-hover) first) + hover-id (when-not (d/seek #(= hover-id (:id %)) shapes) hover-id) + hover-shape (mf/deref (refs/object-by-id hover-id)) + + vbox (mf/deref refs/vbox) + on-resize (fn [current-position initial-position event] (dom/stop-propagation event) (st/emit! (dw/start-resize current-position initial-position selected shape))) @@ -282,13 +291,29 @@ :color color :on-resize on-resize :on-rotate on-rotate}] + + (when show-distances + [:& msr/measurement {:bounds vbox + :selected-shapes shapes + :hover-shape hover-shape + :zoom zoom}]) + (when (debug? :selection-center) [:circle {:cx (:x shape-center) :cy (:y shape-center) :r 5 :fill "yellow"}])])) (mf/defc single-selection-handlers - [{:keys [shape zoom color] :as props}] + [{:keys [shape zoom color show-distances] :as props}] (let [shape-id (:id shape) shape (geom/transform-shape shape) + + frame (mf/deref (refs/object-by-id (:frame-id shape))) + frame (when-not (= (:id frame) uuid/zero) frame) + vbox (mf/deref refs/vbox) + + hover-id (-> (mf/deref refs/current-hover) first) + hover-id (when-not (= shape-id hover-id) hover-id) + hover-shape (mf/deref (refs/object-by-id hover-id)) + shape' (if (debug? :simple-selection) (geom/selection-rect [shape]) shape) on-resize (fn [current-position initial-position event] (dom/stop-propagation event) @@ -303,10 +328,17 @@ :zoom zoom :color color :on-rotate on-rotate - :on-resize on-resize}]])) + :on-resize on-resize}] + + (when show-distances + [:& msr/measurement {:bounds vbox + :frame frame + :selected-shapes [shape] + :hover-shape hover-shape + :zoom zoom}])])) (mf/defc selection-handlers - [{:keys [selected edition zoom] :as props}] + [{:keys [selected edition zoom show-distances] :as props}] (let [;; We need remove posible nil values because on shape ;; deletion many shape will reamin selected and deleted ;; in the same time for small instant of time @@ -326,7 +358,8 @@ [:& multiple-selection-handlers {:shapes shapes :selected selected :zoom zoom - :color color}] + :color color + :show-distances show-distances}] (and (= type :text) (= edition (:id shape))) @@ -343,4 +376,5 @@ :else [:& single-selection-handlers {:shape shape :zoom zoom - :color color}]))) + :color color + :show-distances show-distances}]))) diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 69e8fc5fee..f0b8bc2b55 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -18,7 +18,6 @@ [app.main.ui.cursors :as cur] [app.main.ui.shapes.rect :as rect] [app.main.ui.shapes.circle :as circle] - [app.main.ui.shapes.icon :as icon] [app.main.ui.shapes.image :as image] [app.main.data.workspace.selection :as dws] [app.main.store :as st] @@ -40,7 +39,6 @@ (declare frame-wrapper) (def circle-wrapper (common/generic-wrapper-factory circle/circle-shape)) -(def icon-wrapper (common/generic-wrapper-factory icon/icon-shape)) (def image-wrapper (common/generic-wrapper-factory image/image-shape)) (def rect-wrapper (common/generic-wrapper-factory rect/rect-shape)) @@ -113,7 +111,6 @@ :path [:> path/path-wrapper opts] :text [:> text/text-wrapper opts] :group [:> group-wrapper opts] - :icon [:> icon-wrapper opts] :rect [:> rect-wrapper opts] :image [:> image-wrapper opts] :circle [:> circle-wrapper opts] diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 4cf2d11533..42f4678519 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -29,10 +29,10 @@ [app.main.ui.components.context-menu :refer [context-menu]] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.tab-container :refer [tab-container tab-element]] + [app.main.ui.components.editable-label :refer [editable-label]] [app.main.ui.context :as ctx] [app.main.ui.icons :as i] [app.main.ui.keyboard :as kbd] - [app.main.ui.shapes.icon :as icon] [app.main.ui.workspace.sidebar.options.typography :refer [typography-entry]] [app.util.data :refer [matches-search]] [app.util.dom :as dom] @@ -48,9 +48,15 @@ (mf/defc components-box [{:keys [file-id local? components open? on-open on-close] :as props}] (let [state (mf/use-state {:menu-open false + :renaming nil :top nil :left nil :component-id nil}) + on-duplicate + (mf/use-callback + (mf/deps state) + (st/emitf (dwl/duplicate-component {:id (:component-id @state)}))) + on-delete (mf/use-callback (mf/deps state) @@ -58,6 +64,25 @@ (st/emit! (dwl/delete-component {:id (:component-id @state)})) (st/emit! (dwl/sync-file nil)))) + on-rename + (mf/use-callback + (mf/deps state) + (fn [] + (swap! state assoc :renaming (:component-id @state)))) + + do-rename + (mf/use-callback + (mf/deps state) + (fn [new-name] + (st/emit! (dwl/rename-component (:renaming @state) new-name)) + (swap! state assoc :renaming nil))) + + cancel-rename + (mf/use-callback + (mf/deps state) + (fn [] + (swap! state assoc :renaming nil))) + on-context-menu (mf/use-callback (fn [component-id] @@ -86,13 +111,22 @@ (when open? [:div.group-grid.big (for [component components] - [:div.grid-cell {:key (:id component) - :draggable true - :on-context-menu (on-context-menu (:id component)) - :on-drag-start (partial on-drag-start component)} - [:& exports/component-svg {:group (get-in component [:objects (:id component)]) - :objects (:objects component)}] - [:div.cell-name (:name component)]])]) + (let [renaming? (= (:renaming @state)(:id component))] + [:div.grid-cell {:key (:id component) + :draggable true + :on-context-menu (on-context-menu (:id component)) + :on-drag-start (partial on-drag-start component)} + [:& exports/component-svg {:group (get-in component [:objects (:id component)]) + :objects (:objects component)}] + [:& editable-label + {:class-name (dom/classnames + :cell-name true + :editing renaming?) + :value (:name component) + :editing? renaming? + :disable-dbl-click? true + :on-change do-rename + :on-cancel cancel-rename}]]))]) (when local? [:& context-menu @@ -101,7 +135,9 @@ :on-close #(swap! state assoc :menu-open false) :top (:top @state) :left (:left @state) - :options [[(tr "workspace.assets.delete") on-delete]]}])])) + :options [[(tr "workspace.assets.rename") on-rename] + [(tr "workspace.assets.duplicate") on-duplicate] + [(tr "workspace.assets.delete") on-delete]]}])])) (mf/defc graphics-box [{:keys [file-id local? objects open? on-open on-close] :as props}] @@ -595,7 +631,9 @@ (mf/defc assets-toolbox [] - (let [libraries (mf/deref refs/workspace-libraries) + (let [libraries (->> (mf/deref refs/workspace-libraries) + (vals) + (remove :is-indirect)) file (mf/deref refs/workspace-file) locale (mf/deref i18n/locale) team-id (mf/use-ctx ctx/current-team-id) @@ -661,7 +699,7 @@ :open? true :filters @filters}] - (for [file (->> (vals libraries) + (for [file (->> libraries (sort-by #(str/lower (:name %))))] [:& file-library {:key (:id file) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index e488d0d394..2b83cb73f6 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -19,7 +19,6 @@ [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.main.ui.keyboard :as kbd] - [app.main.ui.shapes.icon :as icon] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [t]] [app.util.object :as obj] @@ -35,7 +34,6 @@ [{:keys [shape] :as props}] (case (:type shape) :frame i/artboard - :icon [:& icon/icon-svg {:shape shape}] :image i/image :line i/line :circle i/circle diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/component.cljs new file mode 100644 index 0000000000..a88062f389 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/component.cljs @@ -0,0 +1,84 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.workspace.sidebar.options.component + (:require + [rumext.alpha :as mf] + [app.common.pages-helpers :as cph] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.main.ui.components.context-menu :refer [context-menu]] + [app.main.data.workspace :as dw] + [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.libraries :as dwl] + [app.util.i18n :as i18n :refer [t]] + [app.util.dom :as dom])) + +(def component-attrs [:component-id :component-file :shape-ref]) + +(mf/defc component-menu + [{:keys [ids values] :as props}] + (let [id (first ids) + locale (mf/deref i18n/locale) + local (mf/use-state {:menu-open false}) + + show? (some? (:component-id values)) + local-library (mf/deref refs/workspace-local-library) + libraries (mf/deref refs/workspace-libraries) + component (cph/get-component (:component-id values) + (:component-file values) + local-library + libraries) + + on-menu-click (mf/use-callback + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (swap! local assoc :menu-open true))) + + on-menu-close (mf/use-callback + #(swap! local assoc :menu-open false)) + + do-detach-component #(st/emit! (dwl/detach-component id)) + do-reset-component #(st/emit! (dwl/reset-component id)) + do-update-component #(do + (st/emit! dwc/start-undo-transaction) + (st/emit! (dwl/update-component id)) + (st/emit! (dwl/sync-file nil)) + (st/emit! dwc/commit-undo-transaction)) + do-show-component #(st/emit! (dw/go-to-layout :assets)) + do-navigate-component-file #(st/emit! (dwl/nav-to-component-file + (:component-file values)))] + (when show? + [:div.element-set + [:div.element-set-title + [:span (t locale "workspace.options.component")]] + [:div.element-set-content + [:div.row-flex.component-row + i/component + (:name component) + [:div.row-actions + {:on-click on-menu-click} + i/actions + ;; WARNING: this menu is the same as the shape context menu. + ;; If you change it, you must change equally the file + ;; app/main/ui/workspace/context_menu.cljs + [:& context-menu {:on-close on-menu-close + :show (:menu-open @local) + :options (if (nil? (:component-file values)) + [[(t locale "workspace.shape.menu.detach-instance") do-detach-component] + [(t locale "workspace.shape.menu.reset-overrides") do-reset-component] + [(t locale "workspace.shape.menu.update-master") do-update-component] + [(t locale "workspace.shape.menu.show-master") do-show-component]] + + [[(t locale "workspace.shape.menu.detach-instance") do-detach-component] + [(t locale "workspace.shape.menu.reset-overrides") do-reset-component] + [(t locale "workspace.shape.menu.go-master") do-navigate-component-file]])}]]]]]))) + diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/group.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/group.cljs index 0d6254a878..43711deb7f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/group.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/group.cljs @@ -17,6 +17,7 @@ [app.main.data.workspace.texts :as dwt] [app.main.ui.workspace.sidebar.options.multiple :refer [get-shape-attrs]] [app.main.ui.workspace.sidebar.options.measures :refer [measure-attrs measures-menu]] + [app.main.ui.workspace.sidebar.options.component :refer [component-attrs component-menu]] [app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]] [app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]] @@ -59,6 +60,9 @@ shape-with-children) [:rx :ry])) + component-values + (select-keys shape component-attrs) + fill-values (geom/get-attrs-multi shape-with-children fill-attrs) @@ -136,13 +140,15 @@ :ids-with-children ids-with-children :type type :values measure-values}] + [:& component-menu {:ids [id] + :values component-values}] [:& fill-menu {:ids ids-with-children :type type :values fill-values}] [:& blur-menu {:ids [id] :values (select-keys shape [:blur])}] - + (when-not (empty? other-ids) [:& stroke-menu {:ids other-ids :type type diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rect.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rect.cljs index a5c8c6fb18..e735a8cc31 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rect.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rect.cljs @@ -28,6 +28,7 @@ [:& measures-menu {:ids ids :type type :values measure-values}] + [:& fill-menu {:ids ids :type type :values fill-values}] diff --git a/frontend/src/app/main/ui/workspace/snap_distances.cljs b/frontend/src/app/main/ui/workspace/snap_distances.cljs index fc7f00f3c3..a2559c01b7 100644 --- a/frontend/src/app/main/ui/workspace/snap_distances.cljs +++ b/frontend/src/app/main/ui/workspace/snap_distances.cljs @@ -216,7 +216,7 @@ (filter #(show-distance? (distance-to-selrect %))) (map #(vector selrect (:selrect %)))) - segments-to-display (concat other-shapes-segments selection-segments)] + segments-to-display (d/concat #{} other-shapes-segments selection-segments)] (mf/use-effect (fn [] diff --git a/frontend/src/app/main/ui/workspace/snap_points.cljs b/frontend/src/app/main/ui/workspace/snap_points.cljs index b0934c370f..480d1d57ce 100644 --- a/frontend/src/app/main/ui/workspace/snap_points.cljs +++ b/frontend/src/app/main/ui/workspace/snap_points.cljs @@ -116,8 +116,9 @@ ;; can cause problems with react keys snap-points (into #{} (mapcat add-point-to-snaps) @state) - snap-lines (into (process-snap-lines @state :x) - (process-snap-lines @state :y))] + snap-lines (->> (into (process-snap-lines @state :x) + (process-snap-lines @state :y)) + (into #{}))] (mf/use-effect (fn [] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 8f9d77ae26..402399bd68 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -210,6 +210,7 @@ (assoc :modifiers (:modifiers local)) (gsh/transform-shape)) + alt? (mf/use-state false) viewport-ref (mf/use-ref nil) zoom-view-ref (mf/use-ref nil) last-position (mf/use-var nil) @@ -498,6 +499,7 @@ (timers/schedule #(st/emit! (dw/initialize-viewport size)))))) (mf/use-layout-effect (mf/deps layout) on-resize) + (hooks/use-stream ms/keyboard-alt #(reset! alt? %)) [:* (when picking-color? @@ -513,9 +515,7 @@ :zoom (:zoom local) :drawing drawing :page-id page-id - :file-id (:id file)} - ]) - + :file-id (:id file)}]) [:svg.viewport {:preserveAspectRatio "xMidYMid meet" @@ -569,7 +569,8 @@ (when (seq selected) [:& selection-handlers {:selected selected :zoom zoom - :edition edition}]) + :edition edition + :show-distances (and (not (:transform local)) @alt?)}]) (when (= (count selected) 1) [:& gradient-handlers {:id (first selected) diff --git a/frontend/src/app/util/code_gen.cljs b/frontend/src/app/util/code_gen.cljs new file mode 100644 index 0000000000..f40a4f3722 --- /dev/null +++ b/frontend/src/app/util/code_gen.cljs @@ -0,0 +1,164 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.util.code-gen + (:require + [cuerdas.core :as str] + [app.common.math :as mth] + [app.util.text :as ut] + [app.util.color :as uc])) + +(defn shadow->css [shadow] + (let [{:keys [style offset-x offset-y blur spread]} shadow + css-color (uc/color->background (:color shadow))] + (str + (if (= style :inner-shadow) "inset " "") + (str/fmt "%spx %spx %spx %spx %s" offset-x offset-y blur spread css-color)))) + + +(defn format-fill-color [_ shape] + (let [color {:color (:fill-color shape) + :opacity (:fill-opacity shape) + :gradient (:fill-color-gradient shape) + :id (:fill-ref-id shape) + :file-id (:fill-ref-file-id shape)}] + (uc/color->background color))) + +(defn format-stroke [_ shape] + (let [width (:stroke-width shape) + style (name (:stroke-style shape)) + color {:color (:stroke-color shape) + :opacity (:stroke-opacity shape) + :gradient (:stroke-color-gradient shape) + :id (:stroke-ref-id shape) + :file-id (:stroke-ref-file-id shape)}] + (str/format "%spx %s %s" width style (uc/color->background color)))) + +(def styles-data + {:layout {:props [:width :height :x :y :radius :rx] + :to-prop {:x "left" :y "top" :rotation "transform" :rx "border-radius"} + :format {:rotation #(str/fmt "rotate(%sdeg)" %)}} + :fill {:props [:fill-color :fill-color-gradient] + :to-prop {:fill-color "background" :fill-color-gradient "background"} + :format {:fill-color format-fill-color :fill-color-gradient format-fill-color}} + :stroke {:props [:stroke-color] + :to-prop {:stroke-color "border"} + :format {:stroke-color format-stroke}} + :shadow {:props [:shadow] + :to-prop {:shadow :box-shadow} + :format {:shadow #(str/join ", " (map shadow->css %1))}} + :blur {:props [:blur] + :to-prop {:blur "filter"} + :format {:blur #(str/fmt "blur(%spx)" (:value %))}}}) + +(def style-text + {:props [:fill-color + :font-family + :font-style + :font-size + :line-height + :letter-spacing + :text-decoration + :text-transform] + :to-prop {:fill-color "color"} + :format {:font-family #(str "'" % "'") + :font-style #(str "'" % "'") + :font-size #(str % "px") + :line-height #(str % "px") + :letter-spacing #(str % "px") + :text-decoration name + :text-transform name + :fill-color format-fill-color}}) + + +(defn generate-css-props + ([values properties] + (generate-css-props values properties nil)) + + ([values properties params] + (let [{:keys [to-prop format tab-size] :or {to-prop {} tab-size 0}} params + ;; We allow the :format and :to-prop to be a map for different properties + ;; or just a value for a single property. This code transform a single + ;; property to a uniform one + properties (if-not (coll? properties) [properties] properties) + + format (if (not (map? format)) + (into {} (map #(vector % format) properties)) + format) + + to-prop (if (not (map? to-prop)) + (into {} (map #(vector % to-prop) properties)) + to-prop) + + default-format (fn [value] (str (mth/precision value 2) "px")) + format-property (fn [prop] + (let [css-prop (or (prop to-prop) (name prop)) + format-fn (or (prop format) default-format)] + (str + (str/repeat " " tab-size) + (str/fmt "%s: %s;" css-prop (format-fn (prop values) values)))))] + + (->> properties + (remove #(let [value (get values %)] + (or (nil? value) (= value 0)))) + (map format-property) + (str/join "\n"))))) + +(defn shape->properties [shape] + (let [props (->> styles-data vals (mapcat :props)) + to-prop (->> styles-data vals (map :to-prop) (reduce merge)) + format (->> styles-data vals (map :format) (reduce merge))] + (generate-css-props shape props {:to-prop to-prop + :format format + :tab-size 2}))) +(defn text->properties [shape] + (let [text-shape-style (select-keys styles-data [:layout :shadow :blur]) + + shape-props (->> text-shape-style vals (mapcat :props)) + shape-to-prop (->> text-shape-style vals (map :to-prop) (reduce merge)) + shape-format (->> text-shape-style vals (map :format) (reduce merge)) + + + text-values (->> (ut/search-text-attrs (:content shape) (conj (:props style-text) :fill-color-gradient)) + (merge ut/default-text-attrs))] + + (str/join + "\n" + [(generate-css-props shape + shape-props + {:to-prop shape-to-prop + :format shape-format + :tab-size 2}) + (generate-css-props text-values + (:props style-text) + {:to-prop (:to-prop style-text) + :format (:format style-text) + :tab-size 2})])) + + ) + +(defn generate-css [shape] + (let [name (:name shape) + properties (if (= :text (:type shape)) + (text->properties shape) + (shape->properties shape)) + + selector (str/css-selector name) + selector (if (str/starts-with? selector "-") (subs selector 1) selector)] + (str/join "\n" [(str/fmt "/* %s */" name) + (str/fmt ".%s {" selector) + properties + "}"]))) + +(defn generate-style-code [type shapes] + (let [generate-style-fn (case type + "css" generate-css)] + (->> shapes + (map generate-style-fn) + (str/join "\n\n")))) diff --git a/frontend/src/app/util/text.cljs b/frontend/src/app/util/text.cljs index e5226b04d0..4e1ad7136a 100644 --- a/frontend/src/app/util/text.cljs +++ b/frontend/src/app/util/text.cljs @@ -3,7 +3,9 @@ [cuerdas.core :as str])) (defonce default-text-attrs - {:font-id "sourcesanspro" + {:typography-ref-file nil + :typography-ref-id nil + :font-id "sourcesanspro" :font-family "sourcesanspro" :font-variant-id "regular" :font-size "14" diff --git a/frontend/yarn.lock b/frontend/yarn.lock index cfb999f660..6be49c138a 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -39,6 +39,11 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + ajv@^6.12.3: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -776,6 +781,11 @@ commander@2.15.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== +commander@^2.19.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -801,6 +811,14 @@ concat-stream@^1.6.0, concat-stream@^1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" +config-chain@^1.1.12: + version "1.1.12" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" + integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + console-browserify@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" @@ -1152,6 +1170,16 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +editorconfig@^0.15.3: + version "0.15.3" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" + integrity sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g== + dependencies: + commander "^2.19.0" + lru-cache "^4.1.5" + semver "^5.6.0" + sigmund "^1.0.1" + electron-to-chromium@^1.3.571: version "1.3.583" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.583.tgz#47a9fde74740b1205dba96db2e433132964ba3ee" @@ -1931,6 +1959,11 @@ he@1.1.1: resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= +highlight.js@^10.3.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.3.1.tgz#3ca6bf007377faae347e8135ff25900aac734b9a" + integrity sha512-jeW8rdPdhshYKObedYg5XGbpVgb1/DT4AHvDFXhkU7UnGSIjy9kkJ7zHG7qplhFHMitTSzh5/iClKQk3Kb2RFQ== + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -2291,6 +2324,17 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= +js-beautify@^1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.13.0.tgz#a056d5d3acfd4918549aae3ab039f9f3c51eebb2" + integrity sha512-/Tbp1OVzZjbwzwJQFIlYLm9eWQ+3aYbBXLSaqb1mEJzhcQAfrqMMQYtjb6io+U6KpD0ID4F+Id3/xcjH3l/sqA== + dependencies: + config-chain "^1.1.12" + editorconfig "^0.15.3" + glob "^7.1.3" + mkdirp "^1.0.4" + nopt "^5.0.0" + "js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -2597,6 +2641,14 @@ loose-envify@^1.1.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lru-cache@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -2887,6 +2939,13 @@ node-releases@^1.1.61: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.64.tgz#71b4ae988e9b1dd7c1ffce58dd9e561752dfebc5" integrity sha512-Iec8O9166/x2HRMJyLLLWkd0sFFLrFNy+Xf+JQfSQsdBJzPcHpNl3JQ9gD4j+aJxmCa25jNsIbM4bmACtSbkSg== +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + normalize-package-data@^2.3.2: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -3336,6 +3395,16 @@ progress@^1.1.8: resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + psl@^1.1.28: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -3730,7 +3799,7 @@ semver-greatest-satisfied-range@^1.1.0: dependencies: sver-compat "^1.5.0" -"semver@2 || 3 || 4 || 5", semver@^5.5.0: +"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -3836,6 +3905,11 @@ should@^13.2.3: should-type-adaptors "^1.0.1" should-util "^1.0.0" +sigmund@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= + signal-exit@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -4668,6 +4742,11 @@ y18n@^3.2.1: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + yargs-parser@5.0.0-security.0: version "5.0.0-security.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz#4ff7271d25f90ac15643b86076a2ab499ec9ee24" diff --git a/manage.sh b/manage.sh index 93e3318524..6f29902b75 100755 --- a/manage.sh +++ b/manage.sh @@ -2,12 +2,12 @@ set -e REV=`git log -n 1 --pretty=format:%h -- docker/` -DEVENV_IMGNAME="uxbox-devenv" +DEVENV_IMGNAME="penpot-devenv" function build-devenv { echo "Building development image $DEVENV_IMGNAME:latest with UID $EXTERNAL_UID..." local EXTERNAL_UID=${1:-$(id -u)} - docker-compose -p uxboxdev -f docker/devenv/docker-compose.yaml build \ + docker-compose -p penpotdev -f docker/devenv/docker-compose.yaml build \ --force-rm --build-arg EXTERNAL_UID=$EXTERNAL_UID } @@ -19,39 +19,39 @@ function build-devenv-if-not-exists { function start-devenv { build-devenv-if-not-exists $@; - docker-compose -p uxboxdev -f docker/devenv/docker-compose.yaml up -d; + docker-compose -p penpotdev -f docker/devenv/docker-compose.yaml up -d; } function stop-devenv { - docker-compose -p uxboxdev -f docker/devenv/docker-compose.yaml stop -t 2; + docker-compose -p penpotdev -f docker/devenv/docker-compose.yaml stop -t 2; } function drop-devenv { - docker-compose -p uxboxdev -f docker/devenv/docker-compose.yaml down -t 2 -v; + docker-compose -p penpotdev -f docker/devenv/docker-compose.yaml down -t 2 -v; echo "Clean old development image $DEVENV_IMGNAME..." docker images $DEVENV_IMGNAME -q | awk '{print $3}' | xargs --no-run-if-empty docker rmi } function run-devenv { - if [[ ! $(docker ps -f "name=uxbox-devenv-main" -q) ]]; then + if [[ ! $(docker ps -f "name=penpot-devenv-main" -q) ]]; then start-devenv fi - docker exec -ti uxbox-devenv-main /home/start-tmux.sh + docker exec -ti penpot-devenv-main /home/start-tmux.sh } function build { build-devenv-if-not-exists; local IMAGE=$DEVENV_IMGNAME:latest; - docker volume create uxboxdev_user_data; + docker volume create penpotdev_user_data; echo "Running development image $IMAGE to build frontend." docker run -t --rm \ - --mount source=uxboxdev_user_data,type=volume,target=/home/uxbox/ \ - --mount source=`pwd`,type=bind,target=/home/uxbox/uxbox \ - -w /home/uxbox/uxbox/$1 \ + --mount source=penpotdev_user_data,type=volume,target=/home/penpot/ \ + --mount source=`pwd`,type=bind,target=/home/penpot/penpot \ + -w /home/penpot/penpot/$1 \ $IMAGE ./scripts/build.sh } @@ -79,7 +79,7 @@ function build-bundle { mv ./backend/target/dist ./bundle/backend mv ./exporter/target ./bundle/exporter - NAME="uxbox-$(date '+%Y.%m.%d-%H%M')" + NAME="penpot-$(date '+%Y.%m.%d-%H%M')" pushd bundle/ tar -cvf ../$NAME.tar *; @@ -89,7 +89,7 @@ function build-bundle { } function log-devenv { - docker-compose -p uxboxdev -f docker/devenv/docker-compose.yaml logs -f --tail=50 + docker-compose -p penpotdev -f docker/devenv/docker-compose.yaml logs -f --tail=50 } function build-testenv { @@ -110,18 +110,18 @@ function build-testenv { popd pushd ./docker/testenv; - docker-compose -p uxbox-testenv -f ./docker-compose.yaml build + docker-compose -p penpot-testenv -f ./docker-compose.yaml build popd } function start-testenv { pushd ./docker/testenv; - docker-compose -p uxbox-testenv -f ./docker-compose.yaml up + docker-compose -p penpot-testenv -f ./docker-compose.yaml up popd } function usage { - echo "UXBOX build & release manager v$REV" + echo "PENPOT build & release manager v$REV" echo "USAGE: $0 OPTION" echo "Options:" # echo "- clean Stop and clean up docker containers"