Merge branch 'develop'
@@ -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!
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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 ##
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The UXBOX team.</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -250,7 +250,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -310,7 +310,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://uxbox.io/" target="_blank">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -350,7 +350,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/uxbox/" target="_blank">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -370,7 +370,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://instagram.com/uxbox/" target="_blank">
|
||||
<a href="https://instagram.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -450,7 +450,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">UXBOX © 2020 | Made with <3 and Open Source</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot © 2020 | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The UXBOX team.</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -240,7 +240,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -300,7 +300,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://uxbox.io/" target="_blank">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -340,7 +340,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/uxbox/" target="_blank">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -360,7 +360,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://instagram.com/uxbox/" target="_blank">
|
||||
<a href="https://instagram.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -440,7 +440,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">UXBOX © 2020 | Made with <3 and Open Source</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot © 2020 | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The UXBOX team.</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -245,7 +245,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -305,7 +305,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://uxbox.io/" target="_blank">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -345,7 +345,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/uxbox/" target="_blank">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -365,7 +365,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://instagram.com/uxbox/" target="_blank">
|
||||
<a href="https://instagram.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -445,7 +445,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">UXBOX © 2020 | Made with <3 and Open Source</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot © 2020 | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Thanks for signing up for your UXBOX account! Please verify your email using the link below adn get started building mockups and prototypes today!</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Thanks for signing up for your Penpot account! Please verify your email using the link below adn get started building mockups and prototypes today!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -194,7 +194,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The UXBOX team.</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -240,7 +240,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -300,7 +300,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://uxbox.io/" target="_blank">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -340,7 +340,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/uxbox/" target="_blank">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -360,7 +360,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://instagram.com/uxbox/" target="_blank">
|
||||
<a href="https://instagram.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -440,7 +440,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">UXBOX © 2020 | Made with <3 and Open Source</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot © 2020 | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)})
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <niwi@niwi.nz>
|
||||
;; 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 <niwi@niwi.nz>
|
||||
|
||||
(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)))))))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)})))
|
||||
|
||||
|
||||
@@ -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]}]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -4,7 +4,7 @@ LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
|
||||
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; \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "$@"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
CREATE DATABASE uxbox_test;
|
||||
CREATE DATABASE penpot_test;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
v12.18.3
|
||||
v14.15.0
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -1 +1,3 @@
|
||||
<svg height="500" viewBox="0 0 500.00001 500.00001" width="500" xmlns="http://www.w3.org/2000/svg"><path d="M0 0v134.00391L134.00586 0H0zm166.32227 0v44.453125h289.22461V455.54688H44.453125V164.07617H0V500h500V0H166.32227z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
|
||||
<path d="M0 0v134.004L134.006 0H0zm166.322 0v44.453h289.225v291.47H500V0H166.322zM0 164.076V500h333.678v-44.453H44.453v-291.47H0zm500 201.92L365.994 500H500V365.996z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 238 B After Width: | Height: | Size: 241 B |
@@ -1 +1,4 @@
|
||||
<svg height="500" viewBox="0 0 500.00001 500.00001" width="500" xmlns="http://www.w3.org/2000/svg"><path d="m249.31931.2729147c-15.69615 1.5029742-26.69705 14.2405013-40.37835 20.6766853-67.87617 41.808035-136.946061 81.73313-203.6765704 125.30305-9.7979734 10.4033-4.5687011 29.56561 8.8007004 34.17351 12.602371 8.04566 25.427072 15.73406 38.196241 23.51001-16.393583 11.43727-35.696112 19.05582-49.56621 33.6899-6.4186198 11.00458-.4334501 26.5756 11.481305 30.82486 13.579059 8.62581 27.394945 16.86772 41.047931 25.3742-17.515721 12.14489-38.521119 19.88761-52.9052744 36.02095-6.298844 11.38127 1.0109666 26.14004 12.5755334 30.6441 62.424935 38.1462 125.003964 76.09764 187.530074 114.11259 14.53681 8.27858 28.63502 17.84148 43.71502 24.95088 13.17393 1.68464 23.14993-9.20439 34.04692-14.67699 71.58125-43.33778 143.60086-86.05625 214.19758-130.91883 9.49113-9.60539 6.1847-27.54433-6.03996-33.12787-14.11946-9.67282-29.30676-17.94933-43.88457-27.02845 17.36604-12.04925 38.15336-19.76376 52.72217-35.43031 6.27533-10.83718.37459-25.88574-10.94786-30.63812-12.64571-8.24391-25.75341-15.67813-38.57306-23.67878 16.79008-12.01353 37.65856-19.10426 50.58013-35.74531 5.2897-11.22863-1.9905-25.21401-13.31213-29.17048-75.06639-45.755824-149.64468-92.405653-225.6515-136.5562067-3.14878-1.3018693-6.51972-2.3591773-9.95812-2.3093886zm1.3053 46.8571573c62.71452 36.705137 124.38029 75.164988 186.57614 112.713118-62.00796 38.38703-124.44389 76.07247-186.85768 113.79989-63.14922-37.15304-125.38266-75.82255-188.125578-113.6542 62.626848-37.54678 124.625008-76.11443 187.379408-113.463226l.86889.511014.15882.0934zm-155.994958 182.571628c50.223898 29.67123 99.077888 61.87355 150.303188 89.69799 13.64279 3.57416 24.52116-8.33034 35.65343-13.9659 42.07059-25.25662 83.75029-51.16086 125.90533-76.27153 10.24124 6.00256 20.31329 12.28529 30.33114 18.65229-61.86919 38.57723-124.22364 76.37165-186.83405 113.73492-62.90406-37.02754-124.86064-75.62248-187.194342-113.5971 10.17143-6.44759 20.402352-12.80256 30.807596-18.8669l.864516.51838.163192.0979zm2.787804 89.69405c49.442164 29.10198 97.453674 60.74038 147.690604 88.3533 12.94756 2.70685 23.3524-7.87321 33.97413-13.36043 41.48097-25.20828 82.86065-50.57982 124.46803-75.58351 11.31406 6.87422 22.63641 13.72784 33.89073 20.69984-62.4284 37.67462-124.3127 76.28654-187.15694 113.28604-63.24792-36.623-125.22726-75.38783-187.72198-113.27422 11.224656-6.99285 22.435195-14.01125 33.823781-20.73528l.866115.5157.16553.0986z"/></svg>
|
||||
<svg viewBox="0 0 500 500">
|
||||
<defs/>
|
||||
<path d="M249.218 3.452c-15.697 1.648-26.696 15.61-40.377 22.663-67.877 45.828-136.948 89.59-203.678 137.349-9.798 11.404-4.569 32.408 8.8 37.46 12.603 8.819 25.429 17.246 38.198 25.77l-.031.022 36.37 24.099C60.492 269.537 32.624 288.5 5.163 308.154c-9.798 11.404-4.569 32.408 8.8 37.459 12.603 8.82 25.429 17.247 38.198 25.77l-.031.023 47.146 31.238.365.314c48.431 31.593 95.708 65.527 145.192 94.99 13.643 3.918 24.52-9.133 35.652-15.31 42.07-27.684 83.751-56.078 125.906-83.604l41.168-27.52.008-.006c16.79-13.165 37.653-20.939 50.573-39.176 5.29-12.309-1.99-27.638-13.31-31.974-24.523-16.384-48.998-32.863-73.483-49.324l36.213-24.21.008-.006c16.788-13.164 37.652-20.938 50.572-39.175 5.29-12.309-1.99-27.638-13.31-31.975-75.067-50.156-149.647-101.29-225.653-149.686-3.15-1.427-6.521-2.584-9.96-2.53zm.279 50.699l.867.56.16.103c62.715 40.234 124.38 82.39 186.576 123.549-21.236 14.41-42.535 28.708-63.853 42.971-4.227-3.669-9.538-5.678-15.02-5.68-13.038 0-23.608 11.109-23.609 24.813.006 2.075.26 4.141.756 6.15-28.355 18.866-56.743 37.674-85.131 56.483-28.986-18.692-57.777-37.74-86.516-56.885.069-.75.105-1.504.108-2.258 0-13.705-10.571-24.815-23.61-24.815a22.744 22.744 0 00-11.925 3.432c-22.04-14.723-44.08-29.444-66.184-44.054 62.627-41.155 124.626-83.43 187.38-124.37zm.504 99.136c-13.04-.001-23.612 9.11-23.612 22.816 0 13.707 10.572 22.818 23.612 22.817 13.04 0 23.61-9.11 23.61-22.817 0-13.705-10.57-22.816-23.61-22.816zm-79.135 56.49c.001 13.705 10.571 22.814 23.61 22.814s23.608-9.11 23.609-22.815c-.001-13.705-10.571-22.815-23.61-22.815s-23.609 9.11-23.609 22.816zm108.375 0c.001 13.705 10.571 22.814 23.61 22.814s23.608-9.11 23.609-22.815c-.001-13.705-10.571-22.815-23.61-22.815-13.04 0-23.609 9.11-23.609 22.816zm-149.445 68.5c38.072 25.547 75.865 51.66 115.037 74.983 13.643 3.917 24.52-9.133 35.652-15.31 29.942-19.703 59.69-39.758 89.549-59.611 22.336 14.943 44.668 29.891 67.066 44.714-62.008 42.078-124.443 83.384-186.857 124.74-63.15-40.725-125.384-83.113-188.127-124.58 22.622-14.868 45.156-29.892 67.68-44.937z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -1,3 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
|
||||
<path d="M453 125c0-4-1-7-3-11L402 14a25 25 0 00-23-14H123C93 0 47 20 47 75v350c0 55 46 75 76 75h305c6 0 13-3 18-7 4-5 7-11 7-18zM123 50h240l24 50H123c-11 0-25-5-25-25s14-25 25-25zm279 400H123c-11 0-25-5-25-25V150h152v150l51-25 51 25V150h50z"/>
|
||||
<svg viewBox="0 0 500 500">
|
||||
<defs/>
|
||||
<path d="M250.133 7.895c-87.667 0-145.384.192-181.637.699-18.126.253-30.858.582-39.486 1.021-4.314.22-7.568.455-10.26.787-1.346.166-2.524.337-3.955.684-1.431.347-2.906-.51-7.393 3.976-4.46 4.46-3.843 6.227-4.322 8.077-.479 1.85-.764 3.57-1.03 5.504-.529 3.865-.903 8.475-1.204 13.81C.242 53.124-.027 78.561.002 92.008c.03 13.446.358 26.831 1.008 37.41.325 5.29.717 9.838 1.27 13.656.276 1.91.568 3.606 1.083 5.479.516 1.873-.426 3.908 5.194 8.572 3.737 3.101 5.429 3.11 7.465 3.715 2.035.604 4.1 1.05 6.357 1.45 4.515.799 9.733 1.371 15.127 1.624l4.894.23.645 147.352c.247 56.381.644 96.696 1.252 123.381.555 23.242-.218 32.079 5.55 48.76 5.066 6.102 7.727 5.057 10.169 5.56 2.441.503 4.943.783 8.21 1.047 6.536.529 15.969.883 30.094 1.154 28.252.543 75.089.707 151.604.707 76.515 0 123.352-.164 151.603-.707 14.126-.271 23.557-.625 30.092-1.154 3.268-.264 5.771-.544 8.213-1.047 2.442-.503 5.103.542 10.168-5.56 3.332-4.014 2.79-5.168 3.094-6.44.304-1.272.463-2.32.617-3.492.308-2.342.534-5.082.75-8.58.432-6.996.786-16.905 1.09-30.248.608-26.685 1.005-67 1.252-123.38l.646-147.317 5.403-.239c6.053-.266 11.447-.856 16.166-1.851 4.718-.995 8.542-.052 14.654-7.416 3.555-4.283 3.073-5.739 3.486-7.496.413-1.758.667-3.433.907-5.317.478-3.767.824-8.28 1.105-13.498.563-10.436.832-23.567.83-36.728-.002-13.161-.273-38.292-.838-48.729-.282-5.218-.63-9.73-1.11-13.498-.239-1.883-.496-3.557-.91-5.314-.413-1.758.07-3.216-3.484-7.498-4.667-5.624-7.693-5.108-10.306-5.655-2.614-.546-5.329-.844-8.965-1.127-7.273-.564-18.076-.936-34.918-1.216-33.684-.56-91.307-.703-189.336-.703zM54.086 61.746h391.676v47.797H54.086V91.645zm43.465 101.649h304.744v274.859H97.551v-137.43zm97.088 56.113c-13.421.37-22.541.845-31.184 4.57-15.16 5.576-16.09 28.069-15.377 32.318 1.082 6.372 4.638 12.794 8.197 16.354 1.846 1.845 5.779 4.86 10.006 6.264 4.228 1.403 8.442 1.89 14.434 2.277 11.984.773 31.614.8 69.209.8 47.83 0 68.792 2.427 85.928-4.728 8.568-3.577 15.003-13.1 16.353-21.074.12-5.212 1.89-25.054-15.812-32.21-8.643-3.726-17.763-4.201-31.184-4.571-13.42-.37-31.115-.164-55.285-.164-29.484.346-29.271-1.651-55.285.164z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 317 B After Width: | Height: | Size: 2.1 KiB |
@@ -1 +1,4 @@
|
||||
<svg height="500" viewBox="0 0 500.00001 500.00001" width="500" xmlns="http://www.w3.org/2000/svg"><path d="m250 0c-137.76 0-250 112.24-250 250s112.24 250 250 250c34.33 0 62.735-28.405 62.735-62.735 0-16.003-6.18-31.14-16.454-42.253l.35.388c-1.946-2.197-2.997-4.588-2.997-8.07 0-7.12 5.055-12.174 12.173-12.174h44.07c82.59 0 150.123-67.533 150.123-150.125 0-125.728-113.767-225.031-250-225.031zm0 50.562c111.955 0 199.44 79.474 199.44 174.47 0 55.234-44.33 99.562-99.565 99.562h-44.068c-34.33 0-62.735 28.405-62.735 62.735 0 15.742 5.944 30.58 15.73 41.616l.16.18.157.182c1.962 2.12 3.018 4.458 3.018 7.93 0 7.12-5.054 12.174-12.173 12.174-110.428 0-199.438-89.01-199.438-199.438 0-110.43 89.01-199.44 199.438-199.44zm-62.6 23.01a38.294 38.294 0 0 0 -38.295 38.294 38.294 38.294 0 0 0 38.295 38.294 38.294 38.294 0 0 0 38.294-38.294 38.294 38.294 0 0 0 -38.294-38.296zm125.404 1.682a38.294 38.294 0 0 0 -38.295 38.294 38.294 38.294 0 0 0 38.294 38.295 38.294 38.294 0 0 0 38.294-38.295 38.294 38.294 0 0 0 -38.294-38.294zm74.065 98.47a38.294 38.294 0 0 0 -38.296 38.295 38.294 38.294 0 0 0 38.295 38.293 38.294 38.294 0 0 0 38.293-38.294 38.294 38.294 0 0 0 -38.294-38.296zm-273.533.845a38.294 38.294 0 0 0 -38.295 38.293 38.294 38.294 0 0 0 38.295 38.294 38.294 38.294 0 0 0 38.294-38.294 38.294 38.294 0 0 0 -38.293-38.294z"/></svg>
|
||||
<svg viewBox="0 0 500 500">
|
||||
<defs/>
|
||||
<path d="M249.305 0c-.734 0-13.585 12.562-29.057 28.062-18.243 17.939-39.097 39.116-46.267 47.324-4.144 4.372-7.677 8.154-9.586 10.353-22.157 25.507-40.877 49.176-56.397 71.37l-.755-.691-.01.005c.195.39-.453 1.947-1.704 4.25-28.86 41.905-46.161 78.529-53.309 112.267-7.234 38.304 1.904 81.92 18.77 116.409 19.538 39.693 50.51 70.042 89.482 89.852 19.588 9.83 43.797 17.103 66.522 19.98 5.726.725 13.987.943 22.869.755 8.882.188 17.143-.03 22.868-.755 22.725-2.877 46.934-10.15 66.523-19.98 42.745-23.552 69.12-49.108 89.482-89.852 16.2-36.655 26.877-77.43 18.767-116.41-22.589-79.13-61.168-128.292-112.174-187.2-1.91-2.198-5.443-5.98-9.587-10.353-7.177-8.216-28.069-29.43-46.324-47.38C263.971 12.534 251.152 0 250.418 0c-.097 0-.295.106-.557.278-.263-.172-.461-.278-.558-.278zm.558 68.475c1.515.997 10.578 10.153 21.1 21.343a966.983 966.983 0 009.572 10.031c6.962 7.473 13.557 14.667 17.661 19.363 21.89 25.047 40.87 49.46 56.18 72.15l-.308.282c.071-.052.251-.2.264-.2.035 0 .084.027.128.044 9.603 14.24 17.769 27.805 24.27 40.406 3.462 6.71 6.016 11.818 8.096 16.29 8.21 20.878 12.663 41.448 12.591 58.95-.09 21.806-4.823 40.866-15.45 62.198-21.312 42.779-61.92 72.804-109.49 80.956-6.604 1.131-15.613 1.703-24.614 1.731-9.001-.028-18.01-.6-24.614-1.731-47.57-8.152-88.179-38.177-109.49-80.956-10.628-21.332-15.362-40.392-15.45-62.199-.072-17.497 4.378-38.062 12.584-58.936 2.08-4.474 4.637-9.587 8.102-16.303 6.502-12.6 14.668-26.166 24.27-40.406.045-.017.094-.044.129-.044.012 0 .192.148.263.2l-.307-.282c15.31-22.69 34.288-47.103 56.18-72.15 4.104-4.696 10.698-11.89 17.66-19.363 2.934-3.033 6.101-6.339 9.573-10.031 10.521-11.19 19.584-20.346 21.1-21.343z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1 +1,4 @@
|
||||
<svg height="500" viewBox="0 0 499.99999 500.00001" width="500" xmlns="http://www.w3.org/2000/svg"><path d="m380.465 0-356.053 356.043-.8 4.717-23.612 139.24 143.957-24.412 356.043-356.053zm0 42.426 29.64 29.64-320.312 320.304-29.64-29.64 320.311-320.304zm47.318 47.318 29.79 29.79-320.303 320.316-29.8-29.8 320.314-320.305zm-378.27 297.703 63.038 63.037-51.6 8.75-20.29-19.58 8.854-52.207z"/></svg>
|
||||
<svg viewBox="0 0 500 500">
|
||||
<defs/>
|
||||
<path d="M376.566 0l-3.515 3.516L25.209 351.348l-1.045 6.156L0 500l148.65-25.209L500 123.434 376.566 0zm-.021 55.396c22.767 23.647 44.925 45.018 68.057 68.055L139.744 428.322c-22.682-22.774-45.376-45.388-68.064-68.066l304.865-304.86zM57.582 394.648l47.77 47.77-38.282 6.49-16.062-15.5 6.574-38.76z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 399 B After Width: | Height: | Size: 348 B |
@@ -1 +1,4 @@
|
||||
<svg height="500" viewBox="0 0 500.00001 500.00001" width="500" xmlns="http://www.w3.org/2000/svg"><path d="m267.393 23.48c-88.18.946-172.42 59.717-204.148 141.947-5.39 13.265-9.62 26.997-12.712 40.976-14.833 1.077-31.187-2.467-45.38 3.705-7.584 5.314-6.272 17.125.517 22.538 18.722 23.626 37.06 47.815 58.234 69.276 9.942 5.903 22.776-.814 27.466-10.445 17.49-22.46 36.883-43.903 51.21-68.53 1.854-10.235-9.626-17.808-18.924-16.114-7.377-.535-14.79.508-22.153-.325 16.96-71.22 83.185-127.703 156.316-133.257 61.472-5.76 125.65 23.543 160.772 74.893 34.636 49.37 42.33 116.303 17.738 171.66-22.51 52.993-72.073 93.513-128.673 104.138-53.846 10.76-112.327-5.19-153.14-42.24-6.707-5.932-13.56-13.615-23.355-13.318-14.056-1.012-26.422 12.118-25.292 25.986.046 8.412 5.87 14.975 11.957 20.033 62.64 61.81 163.483 79.47 243.9 44.304 64.723-27.22 115.68-85.82 131.714-154.412 15.942-64.328 2.593-135.65-37.515-188.706-42.335-57.397-112.723-93.999-184.405-92.113-1.377-.01-2.752-.01-4.127.003zm5 74.84c-14.593.034-25.925 15.445-22.93 29.44-.267 43.316-1.854 80.763.57 124.006 0 16.022 12.657 22.41 31.217 22.41 37.45.106 75.065 1.907 112.41-1.264 16.298-4.748 21.17-28.12 9.398-39.846-6.213-7.357-16.492-8.592-25.47-7.432l-79.22-.735c-.82-37.02 1.053-74.177-2.314-111.09-3.368-9.633-13.656-15.794-23.662-15.49z"/></svg>
|
||||
<svg viewBox="0 0 500 500">
|
||||
<defs/>
|
||||
<path d="M214.21 31.25v175.319H500V31.25H262.939zM131.446 42.5C93.461 60.088 60.253 87.886 37.8 123.426a246.777 246.777 0 00-2.293 3.431c-48.258 73.81-46.268 176.505 4.424 248.608 8.026 11.857 17.086 23.01 26.984 33.355-7.355 12.926-19.394 24.546-22.158 39.774.198 9.258 10.741 14.737 19.015 12.105 30.048-2.419 60.351-4.206 89.963-9.867 10.436-4.98 11.992-19.379 6.596-28.633-8.938-27.027-15.972-55.072-28.47-80.676-7.475-7.233-20.155-1.903-23.919 6.766-4.547 5.833-7.804 12.575-12.591 18.23-49.756-53.705-59.863-140.16-23.805-204.025 18.108-33.437 47.848-60.312 82.55-76.185zM262.94 79.979h188.332v77.86H262.94zm-48.728 203.39v48.729H500v-48.729zm0 122.352v48.728H500v-48.728z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 729 B |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 202 KiB |
BIN
frontend/resources/images/penpot-login2.jpg
Normal file
|
After Width: | Height: | Size: 191 KiB |
@@ -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" : {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
@import 'common/dependencies/reset';
|
||||
@import 'common/dependencies/animations';
|
||||
@import 'common/dependencies/z-index';
|
||||
@import 'common/dependencies/highlightjs-theme';
|
||||
|
||||
//#################################################
|
||||
// Layouts
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
li {
|
||||
cursor: pointer;
|
||||
font-size: $fs14;
|
||||
padding: $small $x-small;
|
||||
padding: $small;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))))))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
23
frontend/src/app/main/ui/components/code_block.cljs
Normal file
@@ -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]))
|
||||
|
||||
35
frontend/src/app/main/ui/components/copy_button.cljs
Normal file
@@ -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)]))
|
||||
@@ -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)))
|
||||
|
||||
|
||||
@@ -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])))
|
||||
|
||||
@@ -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}
|
||||
|
||||
256
frontend/src/app/main/ui/measurements.cljs
Normal file
@@ -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}]])])))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]}]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
:y y
|
||||
:fill (if (debug? :group) "red" "transparent")
|
||||
:opacity 0.5
|
||||
:id (str "group-" id)
|
||||
:width width
|
||||
:height height}])])))
|
||||
|
||||
|
||||
@@ -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]))
|
||||
@@ -43,7 +43,6 @@
|
||||
#js {:x x
|
||||
:y y
|
||||
:transform transform
|
||||
:id (str "shape-" id)
|
||||
:width width
|
||||
:height height
|
||||
:preserveAspectRatio "none"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
#js {:x x
|
||||
:y y
|
||||
:transform transform
|
||||
:id (str "shape-" id)
|
||||
:width width
|
||||
:height height}))]
|
||||
|
||||
|
||||
@@ -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]]))
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}])))
|
||||
|
||||
@@ -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}])]))
|
||||
|
||||
@@ -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)}]])])))
|
||||
|
||||
@@ -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])]))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}])])
|
||||
|
||||
@@ -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}]))]])))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))))
|
||||
|
||||
93
frontend/src/app/main/ui/viewer/handoff/code.cljs
Normal file
@@ -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 "<!-- %s -->" (:name shape))
|
||||
(.-outerHTML el)))))]
|
||||
(->> shapes
|
||||
(map markup-shape )
|
||||
(remove nil?)
|
||||
(str/join "\n\n"))))
|
||||
|
||||
(defn format-code [code type]
|
||||
(let [code (-> code
|
||||
(str/replace "<defs></defs>" "")
|
||||
(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}]]]
|
||||
|
||||
]))
|
||||
132
frontend/src/app/main/ui/viewer/handoff/exports.cljs
Normal file
@@ -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"))]])]))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)}]]]]])]]))
|
||||
|
||||
@@ -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}]])))
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 %)
|
||||
|
||||
@@ -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}])))
|
||||
|
||||
@@ -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]
|
||||
|
||||