Merge branch 'develop'

This commit is contained in:
Andrey Antukh 2020-11-11 16:53:41 +01:00
commit 3dc4893cf1
112 changed files with 3124 additions and 1401 deletions

View File

@ -8,6 +8,8 @@
# PENPOT # # PENPOT #
Were excited to share that Uxbox is now Penpot! Were changing the name, but keeping the same project essence. Stay in the loop for more news comming early 2021. Alpha release is close!
![PENPOT](https://raw.githubusercontent.com/penpot/penpot/develop/docs/screenshot.png) ![PENPOT](https://raw.githubusercontent.com/penpot/penpot/develop/docs/screenshot.png)
@ -18,8 +20,6 @@ currently at an early development stage but we are working hard to
bring you the beta version as soon as possible. Follow the project bring you the beta version as soon as possible. Follow the project
progress in Twitter or Github and stay tuned! progress in Twitter or Github and stay tuned!
[See SVG specification](https://www.w3.org/Graphics/SVG/)
## SVG based ## ## 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 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. editable in many other vector tools and easy to use on the web.
[See SVG specification](https://www.w3.org/Graphics/SVG/)
## Contributing ## ## Contributing ##

View File

@ -204,7 +204,7 @@
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <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> </td>
</tr> </tr>
</table> </table>
@ -250,7 +250,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr> <tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <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> </td>
</tr> </tr>
</table> </table>
@ -310,7 +310,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr> <tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;"> <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" /> <img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
</a> </a>
</td> </td>
@ -350,7 +350,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr> <tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;"> <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" /> <img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
</a> </a>
</td> </td>
@ -370,7 +370,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr> <tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;"> <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" /> <img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
</a> </a>
</td> </td>
@ -450,7 +450,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr> <tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <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 &lt;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 &lt;3 and Open Source</div>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -194,7 +194,7 @@
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <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> </td>
</tr> </tr>
</table> </table>
@ -240,7 +240,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr> <tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <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> </td>
</tr> </tr>
</table> </table>
@ -300,7 +300,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr> <tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;"> <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" /> <img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
</a> </a>
</td> </td>
@ -340,7 +340,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr> <tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;"> <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" /> <img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
</a> </a>
</td> </td>
@ -360,7 +360,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr> <tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;"> <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" /> <img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
</a> </a>
</td> </td>
@ -440,7 +440,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr> <tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <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 &lt;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 &lt;3 and Open Source</div>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -199,7 +199,7 @@
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <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> </td>
</tr> </tr>
</table> </table>
@ -245,7 +245,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr> <tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <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> </td>
</tr> </tr>
</table> </table>
@ -305,7 +305,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr> <tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;"> <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" /> <img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
</a> </a>
</td> </td>
@ -345,7 +345,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr> <tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;"> <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" /> <img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
</a> </a>
</td> </td>
@ -365,7 +365,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr> <tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;"> <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" /> <img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
</a> </a>
</td> </td>
@ -445,7 +445,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr> <tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <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 &lt;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 &lt;3 and Open Source</div>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -173,7 +173,7 @@
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <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> </td>
</tr> </tr>
<tr> <tr>
@ -194,7 +194,7 @@
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <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> </td>
</tr> </tr>
</table> </table>
@ -240,7 +240,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr> <tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <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> </td>
</tr> </tr>
</table> </table>
@ -300,7 +300,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr> <tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;"> <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" /> <img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
</a> </a>
</td> </td>
@ -340,7 +340,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr> <tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;"> <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" /> <img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
</a> </a>
</td> </td>
@ -360,7 +360,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr> <tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;"> <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" /> <img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
</a> </a>
</td> </td>
@ -440,7 +440,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr> <tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <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 &lt;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 &lt;3 and Open Source</div>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
CLASSPATH=`(clojure -Spath)` CLASSPATH=`(clojure -Spath)`
NEWCP="./resources:./main:./common" NEWCP="./main:./common"
rm -rf ./target/dist rm -rf ./target/dist
mkdir -p ./target/dist/deps mkdir -p ./target/dist/deps
@ -16,6 +16,7 @@ done
cp ./resources/log4j2-bundle.xml ./target/dist/log4j2.xml cp ./resources/log4j2-bundle.xml ./target/dist/log4j2.xml
cp -r ./src ./target/dist/main cp -r ./src ./target/dist/main
cp -r ./resources/emails ./target/dist/main/
cp -r ../common ./target/dist/common cp -r ../common ./target/dist/common
echo $NEWCP > ./target/dist/classpath; echo $NEWCP > ./target/dist/classpath;

View File

@ -83,7 +83,7 @@
:fullname (str "Profile " index) :fullname (str "Profile " index)
:password "123123" :password "123123"
:demo? true :demo? true
:email (str "profile" index ".test@uxbox.io")}) :email (str "profile" index ".test@penpot.app")})
team-id (:default-team-id prof) team-id (:default-team-id prof)
owner-id id] owner-id id]
(let [project-ids (collect (partial create-project conn team-id owner-id) (let [project-ids (collect (partial create-project conn team-id owner-id)

View File

@ -22,24 +22,24 @@
(def defaults (def defaults
{:http-server-port 6060 {:http-server-port 6060
:http-server-cors "http://localhost:3449" :http-server-cors "http://localhost:3449"
:database-uri "postgresql://127.0.0.1/uxbox" :database-uri "postgresql://127.0.0.1/penpot"
:database-username "uxbox" :database-username "penpot"
:database-password "uxbox" :database-password "penpot"
:secret-key "default" :secret-key "default"
:media-directory "resources/public/media" :media-directory "resources/public/media"
:assets-directory "resources/public/static" :assets-directory "resources/public/static"
:public-uri "http://localhost:3449/" :public-uri "http://localhost:3449/"
:redis-uri "redis://redis/0" :redis-uri "redis://localhost/0"
:media-uri "http://localhost:3449/media/" :media-uri "http://localhost:3449/media/"
:assets-uri "http://localhost:3449/static/" :assets-uri "http://localhost:3449/static/"
:image-process-max-threads 2 :image-process-max-threads 2
:sendmail-backend "console" :smtp-enabled false
:sendmail-reply-to "no-reply@example.com" :smtp-default-reply-to "no-reply@example.com"
:sendmail-from "no-reply@example.com" :smtp-default-from "no-reply@example.com"
:allow-demo-users true :allow-demo-users true
:registration-enabled true :registration-enabled true
@ -79,13 +79,12 @@
(s/def ::media-uri ::us/string) (s/def ::media-uri ::us/string)
(s/def ::media-directory ::us/string) (s/def ::media-directory ::us/string)
(s/def ::secret-key ::us/string) (s/def ::secret-key ::us/string)
(s/def ::sendmail-backend ::us/string) (s/def ::smtp-enabled ::us/boolean)
(s/def ::sendmail-backend-apikey ::us/string) (s/def ::smtp-default-reply-to ::us/email)
(s/def ::sendmail-reply-to ::us/email) (s/def ::smtp-default-from ::us/email)
(s/def ::sendmail-from ::us/email)
(s/def ::smtp-host ::us/string) (s/def ::smtp-host ::us/string)
(s/def ::smtp-port ::us/integer) (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-password (s/nilable ::us/string))
(s/def ::smtp-tls ::us/boolean) (s/def ::smtp-tls ::us/boolean)
(s/def ::smtp-ssl ::us/boolean) (s/def ::smtp-ssl ::us/boolean)
@ -127,6 +126,7 @@
::gitlab-client-id ::gitlab-client-id
::gitlab-client-secret ::gitlab-client-secret
::gitlab-base-uri ::gitlab-base-uri
::redis-uri
::public-uri ::public-uri
::database-username ::database-username
::database-password ::database-password
@ -136,13 +136,12 @@
::media-directory ::media-directory
::media-uri ::media-uri
::secret-key ::secret-key
::sendmail-reply-to ::smtp-default-from
::sendmail-from ::smtp-default-reply-to
::sendmail-backend ::smtp-enabled
::sendmail-backend-apikey
::smtp-host ::smtp-host
::smtp-port ::smtp-port
::smtp-user ::smtp-username
::smtp-password ::smtp-password
::smtp-tls ::smtp-tls
::smtp-ssl ::smtp-ssl
@ -170,8 +169,8 @@
(reduce-kv (reduce-kv
(fn [acc k v] (fn [acc k v]
(cond-> acc (cond-> acc
(str/starts-with? (name k) "uxbox-") (str/starts-with? (name k) "penpot-")
(assoc (keyword (subs (name k) 6)) v) (assoc (keyword (subs (name k) 7)) v)
(str/starts-with? (name k) "app-") (str/starts-with? (name k) "app-")
(assoc (keyword (subs (name k) 4)) v))) (assoc (keyword (subs (name k) 4)) v)))
@ -188,7 +187,7 @@
[env] [env]
(assoc (read-config env) (assoc (read-config env)
:redis-uri "redis://redis/1" :redis-uri "redis://redis/1"
:database-uri "postgresql://postgres/uxbox_test" :database-uri "postgresql://postgres/penpot_test"
:media-directory "/tmp/app/media" :media-directory "/tmp/app/media"
:assets-directory "/tmp/app/static" :assets-directory "/tmp/app/static"
:migrations-verbose false)) :migrations-verbose false))
@ -198,3 +197,15 @@
(def default-deletion-delay (def default-deletion-delay
(dt/duration {:hours 48})) (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)})

View File

@ -29,25 +29,20 @@
;; --- Public API ;; --- Public API
(defn render (defn render
[email context] [email-factory context]
(let [defaults {:from (:sendmail-from cfg/config) (email-factory context))
:reply-to (:sendmail-reply-to cfg/config)}]
(email (merge defaults context))))
(defn send! (defn send!
"Schedule the email for sending." "Schedule the email for sending."
([email context] (send! db/pool email context)) [conn email-factory context]
([conn email-factory context] (us/verify fn? email-factory)
(us/verify fn? email-factory) (us/verify map? context)
(us/verify map? context) (let [email (email-factory context)]
(let [defaults {:from (:sendmail-from cfg/config) (tasks/submit! conn {:name "sendmail"
:reply-to (:sendmail-reply-to cfg/config)} :delay 0
data (merge defaults context) :max-retries 1
email (email-factory data)] :priority 200
(tasks/submit! conn {:name "sendmail" :props email})))
:delay 0
:priority 200
:props email}))))
;; --- Emails ;; --- Emails
@ -57,7 +52,7 @@
(def register (def register
"A new profile registration welcome email." "A new profile registration welcome email."
(emails/build ::register default-context)) (emails/template-factory ::register default-context))
(s/def ::token ::us/string) (s/def ::token ::us/string)
(s/def ::password-recovery (s/def ::password-recovery
@ -65,7 +60,7 @@
(def password-recovery (def password-recovery
"A password recovery notification email." "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 ::pending-email ::us/email)
(s/def ::change-email (s/def ::change-email
@ -73,7 +68,7 @@
(def change-email (def change-email
"Password change confirmation 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/invited-by ::us/string)
(s/def :internal.emails.invite-to-team/team ::us/string) (s/def :internal.emails.invite-to-team/team ::us/string)
@ -86,4 +81,4 @@
(def invite-to-team (def invite-to-team
"Teams member invitation email." "Teams member invitation email."
(emails/build ::invite-to-team default-context)) (emails/template-factory ::invite-to-team default-context))

View File

@ -247,6 +247,7 @@
(def ^:private sql:file-libraries (def ^:private sql:file-libraries
"select fl.*, "select fl.*,
? as is_indirect,
flr.synced_at as synced_at flr.synced_at as synced_at
from file as fl from file as fl
inner join file_library_rel as flr on (flr.library_file_id = fl.id) inner join file_library_rel as flr on (flr.library_file_id = fl.id)
@ -254,8 +255,23 @@
and fl.deleted_at is null") and fl.deleted_at is null")
(defn retrieve-file-libraries (defn retrieve-file-libraries
[conn file-id] [conn is-indirect file-id]
(into [] decode-row-xf (db/exec! conn [sql:file-libraries 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/def ::file-libraries
(s/keys :req-un [::profile-id ::file-id])) (s/keys :req-un [::profile-id ::file-id]))
@ -264,7 +280,7 @@
[{:keys [profile-id file-id] :as params}] [{:keys [profile-id file-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(check-edition-permissions! conn profile-id file-id) (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 ;; --- Query: Single File Library

View File

@ -49,9 +49,12 @@
project (retrieve-project conn (:project-id file)) project (retrieve-project conn (:project-id file))
page (get-in file [:data :pages-index page-id]) 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]) :page (get-in file [:data :pages-index page-id])
:project project}] :project project}
]
(if (string? share-token) (if (string? share-token)
(do (do
(check-shared-token! conn file-id page-id share-token) (check-shared-token! conn file-id page-id share-token)

View File

@ -9,91 +9,32 @@
(ns app.tasks.sendmail (ns app.tasks.sendmail
(:require (:require
[clojure.data.json :as json]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[postal.core :as postal]
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.util.emails :as emails]
[app.config :as cfg] [app.config :as cfg]
[app.metrics :as mtx] [app.metrics :as mtx]))
[app.util.http :as http]))
(defmulti sendmail (fn [config email] (:sendmail-backend config))) (defn- send-console!
(defmethod sendmail "console"
[config email] [config email]
(let [out (with-out-str (let [baos (java.io.ByteArrayOutputStream.)
(println "email console dump:") mesg (emails/smtp-message config email)]
(println "******** start email" (:id email) "**********") (.writeTo mesg baos)
(println " from: " (:from email)) (let [out (with-out-str
(println " to: " (:to email "---")) (println "email console dump:")
(println " reply-to: " (:reply-to email)) (println "******** start email" (:id email) "**********")
(println " subject: " (:subject email)) (println (.toString baos))
(println " content:") (println "******** end email "(:id email) "**********"))]
(doseq [item (:content email)] (log/info out))))
(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))))
(defn handler (defn handler
{:app.tasks/name "sendmail"} {:app.tasks/name "sendmail"}
[{:keys [props] :as task}] [{: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! (mtx/instrument-with-summary!
{:var #'handler {:var #'handler

View File

@ -2,7 +2,10 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; 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/. ;; 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 (ns app.util.emails
(:require (:require
@ -11,27 +14,187 @@
[cuerdas.core :as str] [cuerdas.core :as str]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.exceptions :as ex] [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") (def ^:private email-path "emails/%(id)s/%(lang)s.%(type)s")
(defn- build-base-email (defn- render-email-template-part
[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
[type id context] [type id context]
(let [lang (:lang context :en) (let [lang (:lang context :en)
path (str/format email-path {:id (name id) path (str/format email-path {:id (name id)
@ -40,34 +203,37 @@
(some-> (io/resource path) (some-> (io/resource path)
(tmpl/render context)))) (tmpl/render context))))
(defn- impl-build-email (defn- build-email-template
[id context] [id context]
(let [lang (:lang context :en) (let [lang (:lang context :en)
subj (render-email-part :subj id context) subj (render-email-template-part :subj id context)
html (render-email-part :html id context) text (render-email-template-part :txt id context)
text (render-email-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 {:subject subj
:content (cond-> [] :body [{:type "text/plain"
text (conj {:type "text/plain" :content text}
:value text}) {:type "text/html"
html (conj {:type "text/html" :content html}]}))
:value html}))}))
;; --- Public API
(s/def ::priority #{:high :low}) (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 ::from ::us/email)
(s/def ::reply-to ::us/email) (s/def ::reply-to ::us/email)
(s/def ::lang string?) (s/def ::lang string?)
(s/def ::custom-data ::us/string)
(s/def ::context (s/def ::context
(s/keys :req-un [::to] (s/keys :req-un [::to]
:opt-un [::reply-to ::from ::lang ::priority])) :opt-un [::reply-to ::from ::lang ::priority ::custom-data]))
(defn build (defn template-factory
([id] (build id {})) ([id] (template-factory id {}))
([id extra-context] ([id extra-context]
(s/assert keyword? id) (s/assert keyword? id)
(fn [context] (fn [context]
@ -79,13 +245,21 @@
(extra-context) (extra-context)
extra-context) extra-context)
context) context)
email (impl-build-email id context)] email (build-email-template id context)]
(when-not email (when-not email
(ex/raise :type :internal (ex/raise :type :internal
:code :email-template-does-not-exists :code :email-template-does-not-exists
:hint "seems like the template is wrong or 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)) (cond-> (assoc email :id (name id))
(:to context) (assoc :to [(:to context)]) (:custom-data context)
(:from context) (assoc :from (:from context)) (assoc :custom-data (:custom-data context))
(:reply-to context) (assoc :reply-to (:reply-to context)))))))
(:from context)
(assoc :from (:from context))
(:reply-to context)
(assoc :reply-to (:reply-to context))
(:to context)
(assoc :to (:to context)))))))

View File

@ -136,6 +136,16 @@
[mfn coll] [mfn coll]
(into {} (map (fn [[key val]] [key (mfn key val)]) 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 (defn map-perm
"Maps a function to each pair of values that can be combined inside the "Maps a function to each pair of values that can be combined inside the
function without repetition. function without repetition.

View File

@ -542,6 +542,14 @@
(< ry1 sy2) (< ry1 sy2)
(> ry2 sy1)))) (> 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? (defn has-point?
[shape position] [shape position]
(let [{:keys [x y]} position (let [{:keys [x y]} position
@ -956,3 +964,4 @@
:width width :height height :width width :height height
:x1 x :y1 y :x1 x :y1 y
:x2 (+ x width) :y2 (+ y height)}))) :x2 (+ x width) :y2 (+ y height)})))

View File

@ -523,7 +523,8 @@
(s/keys :req-un [::id ::name :internal.changes.add-component/shapes])) (s/keys :req-un [::id ::name :internal.changes.add-component/shapes]))
(defmethod change-spec :mod-component [_] (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 [_] (defmethod change-spec :del-component [_]
(s/keys :req-un [::id])) (s/keys :req-un [::id]))
@ -962,11 +963,14 @@
:objects (d/index-by :id shapes)})) :objects (d/index-by :id shapes)}))
(defmethod process-change :mod-component (defmethod process-change :mod-component
[data {:keys [id name shapes]}] [data {:keys [id name objects]}]
(update-in data [:components id] (update-in data [:components id]
#(assoc % #(cond-> %
:name name (some? name)
:objects (d/index-by :id shapes)))) (assoc :name name)
(some? objects)
(assoc :objects objects))))
(defmethod process-change :del-component (defmethod process-change :del-component
[data {:keys [id]}] [data {:keys [id]}]

View File

@ -35,13 +35,34 @@
(defn get-root-shape (defn get-root-shape
"Get the root shape linked to a component for this shape, if any" "Get the root shape linked to a component for this shape, if any"
[shape objects] [shape objects]
(if (:component-root? shape) (if (:component-id shape)
shape shape
(if-let [parent-id (:parent-id shape)] (if-let [parent-id (:parent-id shape)]
(get-root-shape (get objects (:parent-id shape)) (get-root-shape (get objects (:parent-id shape))
objects) objects)
nil))) 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 (defn get-children
"Retrieve all children ids recursively for a given object" "Retrieve all children ids recursively for a given object"
[id objects] [id objects]

View File

@ -4,7 +4,7 @@ LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG EXTERNAL_UID=1000 ARG EXTERNAL_UID=1000
ENV NODE_VERSION=v12.19.0 \ ENV NODE_VERSION=v14.15.0 \
CLOJURE_VERSION=1.10.1.727 \ CLOJURE_VERSION=1.10.1.727 \
LANG=en_US.UTF-8 \ LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 LC_ALL=en_US.UTF-8
@ -31,9 +31,9 @@ RUN set -ex; \
rm -rf /var/lib/apt/lists/*; rm -rf /var/lib/apt/lists/*;
RUN set -ex; \ RUN set -ex; \
useradd -m -g users -s /bin/bash -u $EXTERNAL_UID uxbox; \ useradd -m -g users -s /bin/bash -u $EXTERNAL_UID penpot; \
passwd uxbox -d; \ passwd penpot -d; \
echo "uxbox ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers echo "penpot ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
RUN set -ex; \ RUN set -ex; \
apt-get -qq update; \ 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/entrypoint.sh /home/entrypoint.sh
COPY files/init.sh /home/init.sh COPY files/init.sh /home/init.sh
USER uxbox USER penpot
WORKDIR /home/uxbox WORKDIR /home/penpot
RUN set -ex; \ RUN set -ex; \
git clone https://github.com/creationix/nvm.git .nvm; \ git clone https://github.com/creationix/nvm.git .nvm; \

View File

@ -14,20 +14,19 @@ volumes:
services: services:
main: main:
privileged: true privileged: true
image: "uxbox-devenv" image: "penpot-devenv"
build: build:
context: "." context: "."
container_name: "uxbox-devenv-main" container_name: "penpot-devenv-main"
stop_signal: SIGINT stop_signal: SIGINT
depends_on: depends_on:
- postgres - postgres
- smtp
- redis - redis
volumes: volumes:
- "user_data:/home/uxbox/" - "user_data:/home/penpot/"
- "${PWD}:/home/uxbox/uxbox" - "${PWD}:/home/penpot/penpot"
- ./files/nginx.conf:/etc/nginx/nginx.conf - ./files/nginx.conf:/etc/nginx/nginx.conf
ports: ports:
@ -39,32 +38,22 @@ services:
- 9090:9090 - 9090:9090
environment: environment:
- APP_DATABASE_URI=postgresql://postgres/uxbox - APP_DATABASE_URI=postgresql://postgres/penpot
- APP_DATABASE_USERNAME=uxbox - APP_DATABASE_USERNAME=penpot
- APP_DATABASE_PASSWORD=uxbox - APP_DATABASE_PASSWORD=penpot
- APP_SENDMAIL_BACKEND=console - APP_REDIS_URI=redis://redis/0
- 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
postgres: postgres:
image: postgres:13 image: postgres:13
command: postgres -c config_file=/etc/postgresql.conf command: postgres -c config_file=/etc/postgresql.conf
container_name: "uxbox-devenv-postgres" container_name: "penpot-devenv-postgres"
restart: always restart: always
stop_signal: SIGINT stop_signal: SIGINT
environment: environment:
- POSTGRES_INITDB_ARGS=--data-checksums - POSTGRES_INITDB_ARGS=--data-checksums
- POSTGRES_DB=uxbox - POSTGRES_DB=penpot
- POSTGRES_USER=uxbox - POSTGRES_USER=penpot
- POSTGRES_PASSWORD=uxbox - POSTGRES_PASSWORD=penpot
volumes: volumes:
- ./files/postgresql.conf:/etc/postgresql.conf - ./files/postgresql.conf:/etc/postgresql.conf
- ./files/postgresql_init.sql:/docker-entrypoint-initdb.d/init.sql - ./files/postgresql_init.sql:/docker-entrypoint-initdb.d/init.sql
@ -72,6 +61,6 @@ services:
redis: redis:
image: redis:6 image: redis:6
hostname: "uxbox-devenv-redis" hostname: "penpot-devenv-redis"
container_name: "uxbox-devenv-redis" container_name: "penpot-devenv-redis"
restart: always restart: always

View File

@ -2,11 +2,11 @@
set -e set -e
sudo cp /root/.bashrc /home/uxbox/.bashrc sudo cp /root/.bashrc /home/penpot/.bashrc
sudo cp /root/.vimrc /home/uxbox/.vimrc sudo cp /root/.vimrc /home/penpot/.vimrc
sudo cp /root/.tmux.conf /home/uxbox/.tmux.conf sudo cp /root/.tmux.conf /home/penpot/.tmux.conf
source /home/uxbox/.bashrc source /home/penpot/.bashrc
sudo chown uxbox:users /home/uxbox sudo chown penpot:users /home/penpot
exec "$@" exec "$@"

View File

@ -60,7 +60,7 @@ http {
etag off; etag off;
location / { location / {
root /home/uxbox/uxbox/frontend/resources/public; root /home/penpot/penpot/frontend/resources/public;
try_files $uri /index.html; try_files $uri /index.html;
add_header Cache-Control "no-cache, max-age=0"; add_header Cache-Control "no-cache, max-age=0";
} }
@ -74,7 +74,7 @@ http {
} }
location /playground { location /playground {
alias /home/uxbox/uxbox/experiments/; alias /home/penpot/penpot/experiments/;
add_header Cache-Control "no-cache, max-age=0"; add_header Cache-Control "no-cache, max-age=0";
autoindex on; autoindex on;
} }
@ -86,7 +86,7 @@ http {
} }
location /media { location /media {
alias /home/uxbox/uxbox/backend/resources/public/media; alias /home/penpot/penpot/backend/resources/public/media;
} }
} }
} }

View File

@ -1 +1 @@
CREATE DATABASE uxbox_test; CREATE DATABASE penpot_test;

View File

@ -6,36 +6,36 @@ set -e;
source ~/.bashrc source ~/.bashrc
echo "[start-tmux.sh] Installing node dependencies" echo "[start-tmux.sh] Installing node dependencies"
pushd ~/uxbox/frontend/ pushd ~/penpot/frontend/
yarn install yarn install
popd popd
pushd ~/uxbox/exporter/ pushd ~/penpot/exporter/
yarn install yarn install
popd 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 new-window -t penpot:1 -n 'shadow watch'
tmux select-window -t uxbox:1 tmux select-window -t penpot:1
tmux send-keys -t uxbox 'cd uxbox/frontend' enter C-l tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
tmux send-keys -t uxbox 'npx shadow-cljs watch main' enter tmux send-keys -t penpot 'npx shadow-cljs watch main' enter
tmux new-window -t uxbox:2 -n 'exporter' tmux new-window -t penpot:2 -n 'exporter'
tmux select-window -t uxbox:2 tmux select-window -t penpot:2
tmux send-keys -t uxbox 'cd uxbox/exporter' enter C-l tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
tmux send-keys -t uxbox 'npx shadow-cljs watch main' enter tmux send-keys -t penpot 'npx shadow-cljs watch main' enter
tmux split-window -v tmux split-window -v
tmux send-keys -t uxbox 'cd uxbox/exporter' enter C-l tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
tmux send-keys -t uxbox './scripts/wait-and-start.sh' enter tmux send-keys -t penpot './scripts/wait-and-start.sh' enter
tmux new-window -t uxbox:3 -n 'backend' tmux new-window -t penpot:3 -n 'backend'
tmux select-window -t uxbox:3 tmux select-window -t penpot:3
tmux send-keys -t uxbox 'cd uxbox/backend' enter C-l tmux send-keys -t penpot 'cd penpot/backend' enter C-l
tmux send-keys -t uxbox './scripts/start-dev' enter tmux send-keys -t penpot './scripts/start-dev' enter
tmux rename-window -t uxbox:0 'gulp' tmux rename-window -t penpot:0 'gulp'
tmux select-window -t uxbox:0 tmux select-window -t penpot:0
tmux send-keys -t uxbox 'cd uxbox/frontend' enter C-l tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
tmux send-keys -t uxbox 'npx gulp --theme=${UXBOX_THEME} watch' enter tmux send-keys -t penpot 'npx gulp --theme=${PENPOT_THEME} watch' enter
tmux -2 attach-session -t uxbox tmux -2 attach-session -t penpot

View File

@ -1 +1 @@
v12.18.3 v14.15.0

View File

@ -46,7 +46,7 @@ function scssPipeline(options) {
const touch = (_path) => { const touch = (_path) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
return fs.utimes(file.path, new Date(), new Date(), () => { return fs.utimes(_path, new Date(), new Date(), () => {
resolve(_path); resolve(_path);
}); });
}) })
@ -78,7 +78,7 @@ function scssPipeline(options) {
.then(() => render(input)) .then(() => render(input))
.then((res) => postprocess(res, input, output)) .then((res) => postprocess(res, input, output))
.then(async (res) => { .then(async (res) => {
await write(output, res); await write(output, res.css);
await touch(output); await touch(output);
return res; return res;
}) })
@ -158,19 +158,6 @@ function templatePipeline(options) {
const locales = readLocales(); const locales = readLocales();
const manifest = readManifest(); 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({ const tmpl = mustache({
ts: ts, ts: ts,
th: th, th: th,

View File

@ -29,6 +29,8 @@
}, },
"dependencies": { "dependencies": {
"date-fns": "^2.15.0", "date-fns": "^2.15.0",
"highlight.js": "^10.3.1",
"js-beautify": "^1.13.0",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -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

View File

@ -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

View File

@ -1,3 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"> <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"/> <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> </svg>

Before

Width:  |  Height:  |  Size: 317 B

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

View File

@ -978,6 +978,12 @@
"en" : "Left" "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" : { "handoff.attributes.layout.rotation" : {
"used-in" : [ "src/app/main/ui/viewer/handoff/attributes/layout.cljs:60" ], "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/layout.cljs:60" ],
"translations" : { "translations" : {
@ -2118,6 +2124,14 @@
"es" : "Borrar" "es" : "Borrar"
} }
}, },
"workspace.assets.duplicate" : {
"translations" : {
"en" : "Duplicate",
"fr" : "",
"ru" : "",
"es" : "Duplicar"
}
},
"workspace.assets.edit" : { "workspace.assets.edit" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:305", "src/app/main/ui/workspace/sidebar/assets.cljs:433" ], "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:305", "src/app/main/ui/workspace/sidebar/assets.cljs:433" ],
"translations" : { "translations" : {
@ -2679,6 +2693,12 @@
"es" : "Color de fondo" "es" : "Color de fondo"
} }
}, },
"workspace.options.component" : {
"translations" : {
"en" : "Component",
"es" : "Componente"
}
},
"workspace.options.design" : { "workspace.options.design" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options.cljs:69" ], "used-in" : [ "src/app/main/ui/workspace/sidebar/options.cljs:69" ],
"translations" : { "translations" : {
@ -3427,6 +3447,11 @@
"en" : "Go to master component file" "en" : "Go to master component file"
} }
}, },
"workspace.shape.menu.show-master" : {
"translations" : {
"en" : "Show master component"
}
},
"workspace.shape.menu.group" : { "workspace.shape.menu.group" : {
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:110" ], "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:110" ],
"translations" : { "translations" : {

View File

@ -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;
}

View File

@ -19,6 +19,7 @@
@import 'common/dependencies/reset'; @import 'common/dependencies/reset';
@import 'common/dependencies/animations'; @import 'common/dependencies/animations';
@import 'common/dependencies/z-index'; @import 'common/dependencies/z-index';
@import 'common/dependencies/highlightjs-theme';
//################################################# //#################################################
// Layouts // Layouts

View File

@ -1,3 +1,5 @@
$width-settings-bar: 16rem;
.handoff-layout { .handoff-layout {
display: grid; display: grid;
grid-template-rows: 40px auto; grid-template-rows: 40px auto;
@ -28,6 +30,42 @@
} }
} }
.handoff-layout .settings-bar.settings-bar-left { .handoff-layout {
left: 0; .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;
}
} }

View File

@ -25,18 +25,19 @@
background-color:#2C233E; background-color:#2C233E;
background-image: url("/images/penpot-login.jpg"); background-image: url("/images/penpot-login.jpg");
background-size: cover; background-size: cover;
background-position: center;
.tagline { .tagline {
text-align: center; text-align: center;
width: 280px; width: 280px;
font-size: $fs24; font-size: $fs18;
margin-top: 25px; margin-top: 25px;
color: white; color: #2C233E;
} }
.logo { .logo {
svg { svg {
fill: white; fill: #2C233E;
width: 200px; width: 200px;
height: 80px; height: 80px;
} }

View File

@ -17,7 +17,7 @@
background-color: $color-white; background-color: $color-white;
display: grid; display: grid;
grid-template-rows: 50px 1fr; grid-template-rows: 50px 1fr;
grid-template-columns: 40px 180px 1fr; grid-template-columns: 40px 220px 1fr;
height: 100vh; height: 100vh;
.dashboard-sidebar { .dashboard-sidebar {

View File

@ -12,7 +12,7 @@
background-color: $color-white; background-color: $color-white;
display: flex; display: flex;
height: 63px; height: 63px;
padding: $x-small $small; padding: $x-small $medium $x-small $small;
position: relative; position: relative;
z-index: 10; z-index: 10;
justify-content: space-between; justify-content: space-between;

View File

@ -40,11 +40,11 @@
top: 50px; top: 50px;
z-index: 12; z-index: 12;
max-height: 30rem; max-height: 30rem;
min-width: 189px; min-width: 230px;
} }
.options-dropdown { .options-dropdown {
left: 80px; left: 117px;
top: 50px; top: 50px;
z-index: 12; z-index: 12;
max-height: 30rem; max-height: 30rem;
@ -63,7 +63,7 @@
.switch-options { .switch-options {
display: flex; display: flex;
max-width: 22px; max-width: 22px;
min-width: 22px; min-width: 28px;
border-left: 1px solid $color-gray-10; border-left: 1px solid $color-gray-10;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -111,7 +111,7 @@
.team-text { .team-text {
color: $color-gray-60; color: $color-gray-60;
@include text-ellipsis; @include text-ellipsis;
width: 100px; width: 130px;
} }
} }
@ -191,7 +191,7 @@
border-radius: $br-small; border-radius: $br-small;
content: ""; content: "";
height: 26px; height: 26px;
margin-right: 6px; margin-right: $small;
width: 4px; width: 4px;
} }
@ -266,7 +266,7 @@
font-size: $fs14; font-size: $fs14;
padding: 6px; padding: 6px;
margin: 0; margin: 0;
max-width: 170px; max-width: 195px;
width: 100%; width: 100%;
height: 40px; height: 40px;
} }
@ -368,7 +368,7 @@
color: $color-black; color: $color-black;
margin: 10px 5px; margin: 10px 5px;
font-size: $fs14; font-size: $fs14;
max-width: 135px; max-width: 160px;
} }
img { img {
@ -378,6 +378,13 @@
width: 25px; width: 25px;
} }
svg {
height: 10px;
margin-left: auto;
margin-right: $small;
width: 10px;
}
.dropdown { .dropdown {
left: 15px; left: 15px;
bottom: 45px; bottom: 45px;

View File

@ -71,7 +71,7 @@
z-index: 12; z-index: 12;
top: 30px; top: 30px;
left: 0px; left: 0px;
width: 125px; width: 168px;
hr { hr {
margin: 0; margin: 0;
@ -83,7 +83,7 @@
align-items: center; align-items: center;
color: $color-gray-60; color: $color-gray-60;
cursor: pointer; cursor: pointer;
font-size: $fs12; font-size: $fs14;
height: 31px; height: 31px;
padding: 5px 16px; padding: 5px 16px;
@ -123,7 +123,6 @@
display: flex; display: flex;
max-width: 324px; max-width: 324px;
width: 324px; width: 324px;
height: 100px;
background-color: $color-white; background-color: $color-white;
flex-direction: column; flex-direction: column;
padding: 12px; padding: 12px;
@ -139,7 +138,7 @@
.name { .name {
margin-top: 10px; margin-top: 10px;
font-size: $fs32; font-size: $fs28;
color: $color-black; color: $color-black;
@include text-ellipsis; @include text-ellipsis;
margin-right: 90px; margin-right: 90px;

View File

@ -12,7 +12,7 @@
border-top-right-radius: $br-huge; border-top-right-radius: $br-huge;
border-top-left-radius: $br-huge; border-top-left-radius: $br-huge;
flex: 1 0 0; flex: 1 0 0;
margin-right: $small; margin-right: $medium;
overflow-y: auto; overflow-y: auto;
&.search { &.search {

View File

@ -15,6 +15,42 @@
justify-content: center; 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 { .attributes-block {
user-select: text; user-select: text;
@ -30,30 +66,6 @@
border-bottom: none; 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 { .attributes-label {
color: $color-gray-20; color: $color-gray-20;
} }
@ -68,7 +80,7 @@
padding: 0.5rem; padding: 0.5rem;
font-size: $fs14; font-size: $fs14;
.attributes-copy-button { .copy-button {
padding: 0.5rem; padding: 0.5rem;
margin-top: 0.25rem; margin-top: 0.25rem;
} }
@ -84,7 +96,7 @@
.attributes-value { .attributes-value {
width: 50%; width: 50%;
} }
.attributes-copy-button { .copy-button {
padding: 1rem 0.5rem; padding: 1rem 0.5rem;
margin-top: 0.25rem; margin-top: 0.25rem;
} }
@ -92,9 +104,32 @@
.attributes-color-row { .attributes-color-row {
display: flex; display: flex;
flex-direction: column;
padding: 1rem 0; padding: 1rem 0;
position: relative; 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 { .color-text {
width: 3rem; width: 3rem;
@ -108,20 +143,18 @@
.color-bullet { .color-bullet {
width: 24px; width: 24px;
height: 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; padding: 1rem 0.5rem;
margin-top: 0.25rem; margin-top: 0.25rem;
} }
& > * {
margin: 0 0.5rem;
}
& :last-child {
margin-right: 0;
}
select { select {
font-size: $fs12; font-size: $fs12;
margin: 0; margin: 0;
@ -160,7 +193,7 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
.attributes-copy-button { .copy-button {
padding: 0.5rem; padding: 0.5rem;
margin-top: 0.25rem; margin-top: 0.25rem;
} }
@ -212,6 +245,22 @@
justify-content: space-between; 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 { .attributes-typography-row {
position: relative; position: relative;
margin: 0.5rem; margin: 0.5rem;
@ -220,6 +269,7 @@
.typography-sample { .typography-sample {
font-size: $fs16; font-size: $fs16;
} }
} }
.download-button { .download-button {
@ -246,11 +296,90 @@
.attributes-shadow-row, .attributes-shadow-row,
.attributes-stroke-row, .attributes-stroke-row,
.attributes-typography-row, .attributes-typography-row,
.attributes-content-row { .attributes-content-row,
&:hover .attributes-copy-button { .attributes-typography-name-row {
visibility: visible; &:hover {
opacity: 1; .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;
} }

View File

@ -100,8 +100,8 @@
} }
option { option {
color: $color-gray-60;
background: $color-white; background: $color-white;
color: $color-gray-60;
font-size: $fs12; font-size: $fs12;
} }
} }
@ -213,9 +213,28 @@
left: 0; left: 0;
bottom: 0; bottom: 0;
width: 100%; width: 100%;
padding: 3px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; 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 { .grid-cell:hover {

View File

@ -295,6 +295,7 @@
} }
} }
.custom-select-dropdown { .custom-select-dropdown {
background-color: $color-white; background-color: $color-white;
border-radius: $br-small; border-radius: $br-small;
@ -307,7 +308,6 @@
top: 30px; top: 30px;
z-index: 12; z-index: 12;
.presets { .presets {
width: 200px; 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 { .grid-option .custom-select {
margin-bottom: 0; margin-bottom: 0;
} }
@ -921,8 +952,6 @@
border-right: none; border-right: none;
} }
.size-option .custom-select-dropdown { .size-option .custom-select-dropdown {
cursor: pointer; cursor: pointer;
max-height: 16rem; max-height: 16rem;

View File

@ -154,11 +154,11 @@
border-right: 1px solid $color-gray-40; border-right: 1px solid $color-gray-40;
border-top: 1px solid $color-gray-40; border-top: 1px solid $color-gray-40;
position: absolute; position: absolute;
width: 0.5rem; width: 6px;
height: 0.5rem; height: 6px;
transform: rotate(-45deg); transform: rotate(-45deg);
top: -1px; top: -1px;
left: -5px; left: -4px;
} }
} }

View File

@ -16,6 +16,11 @@ $width-settings-bar: 16rem;
position: fixed; position: fixed;
right: 0; right: 0;
width: $width-settings-bar; width: $width-settings-bar;
&.expanded {
width: $width-settings-bar * 3;
}
z-index: 10; z-index: 10;
overflow-y: auto; overflow-y: auto;

View File

@ -247,8 +247,7 @@
position: absolute; position: absolute;
left: 0; left: 0;
z-index: 12; z-index: 12;
width: 200px; max-height: 31rem;
max-height: 30rem;
min-width: 7rem; min-width: 7rem;
overflow-y: auto; overflow-y: auto;
@ -261,7 +260,7 @@
cursor: pointer; cursor: pointer;
font-size: $fs14; font-size: $fs14;
display: flex; display: flex;
padding: $small $small $small 25px; padding: $small $medium $small 25px;
&.selected { &.selected {
background-image: url(/images/icons/tick.svg); background-image: url(/images/icons/tick.svg);

View File

@ -149,7 +149,7 @@
li { li {
cursor: pointer; cursor: pointer;
font-size: $fs14; font-size: $fs14;
padding: $small $x-small; padding: $small;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@ -20,18 +20,20 @@
(defn show (defn show
([props] ([props]
(show (uuid/next) (:type props) props)) (show (uuid/next) (:type props) props))
([type props] (show (uuid/next) type props)) ([type props]
(show (uuid/next) type props))
([id type props] ([id type props]
(ptk/reify ::show-modal (ptk/reify ::show-modal
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc state ::modal {:id id (assoc state ::modal {:id id
:type type :type type
:props props :props props
:allow-click-outside false}))))) :allow-click-outside false})))))
(defn update-props (defn update-props
([type props] ([type props]
(ptk/reify ::show-modal (ptk/reify ::update-modal-props
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(cond-> state (cond-> state

View File

@ -56,7 +56,7 @@
:selected #{} :selected #{}
:collapsed #{} :collapsed #{}
:hover #{}})) :hover nil}))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
@ -72,8 +72,10 @@
(let [params (cond-> {:page-id page-id (let [params (cond-> {:page-id page-id
:file-id file-id} :file-id file-id}
(string? token) (assoc :share-token token))] (string? token) (assoc :share-token token))]
(->> (rp/query :viewer-bundle params) (->> (rx/zip (rp/query :viewer-bundle params)
(rx/map bundle-fetched) (rp/query :file-libraries {:file-id file-id}))
(rx/first)
(rx/map #(apply bundle-fetched %))
#_(rx/catch (fn [error-data] #_(rx/catch (fn [error-data]
(rx/of (rt/nav :not-found))))))))) (rx/of (rt/nav :not-found)))))))))
@ -87,7 +89,7 @@
(vec)))) (vec))))
(defn bundle-fetched (defn bundle-fetched
[{:keys [project file page share-token] :as bundle}] [{:keys [project file page share-token] :as bundle} libraries]
(us/verify ::bundle bundle) (us/verify ::bundle bundle)
(ptk/reify ::file-fetched (ptk/reify ::file-fetched
ptk/UpdateEvent ptk/UpdateEvent
@ -95,7 +97,8 @@
(let [objects (:objects page) (let [objects (:objects page)
frames (extract-frames objects)] frames (extract-frames objects)]
(-> state (-> state
(assoc :viewer-data {:project project (assoc :viewer-libraries (into {} (map #(vector (:id %) %) libraries))
:viewer-data {:project project
:objects objects :objects objects
:file file :file file
:page page :page page
@ -317,8 +320,7 @@
(ptk/reify ::hover-shape (ptk/reify ::hover-shape
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update-in state [:viewer-local :hover] (if hover? conj disj) id)))) (assoc-in state [:viewer-local :hover] (when hover? id)))))
;; --- Shortcuts ;; --- Shortcuts

View File

@ -43,6 +43,7 @@
[clojure.set :as set] [clojure.set :as set]
[clojure.set :as set] [clojure.set :as set]
[cuerdas.core :as str] [cuerdas.core :as str]
;; [cljs.pprint :refer [pprint]]
[potok.core :as ptk])) [potok.core :as ptk]))
;; (log/set-level! :trace) ;; (log/set-level! :trace)
@ -84,6 +85,19 @@
:snap-grid :snap-grid
:dynamic-alignment}) :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}) (s/def ::options-mode #{:design :prototype})
(def workspace-local-default (def workspace-local-default
@ -103,11 +117,23 @@
:picked-color nil :picked-color nil
:picked-color-select false}) :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/reify ::initialize-layout
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (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 (defn initialize-file
[project-id file-id] [project-id file-id]
@ -388,35 +414,22 @@
;; --- Toggle layout flag ;; --- Toggle layout flag
(def layout-flags (defn ensure-layout
{:assets [layout-name]
{:del #{:sitemap :layers :document-history } (assert (contains? layout-names layout-name)
:add #{:assets}} (str "unexpected layout name: " layout-name))
: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))
(ptk/reify ::ensure-layout (ptk/reify ::ensure-layout
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update state :workspace-layout (update state :workspace-layout
(fn [stored] (fn [stored]
(let [todel (get-in layout-flags [layout :del] #{}) (let [todel (get-in layout-names [layout-name :del] #{})
toadd (get-in layout-flags [layout :add] #{})] toadd (get-in layout-names [layout-name :add] #{})]
(-> stored (-> stored
(set/difference todel) (set/difference todel)
(set/union toadd)))))))) (set/union toadd))))))))
(defn- toggle-layout-flags (defn toggle-layout-flags
[& flags] [& flags]
(ptk/reify ::toggle-layout-flags (ptk/reify ::toggle-layout-flags
ptk/UpdateEvent ptk/UpdateEvent
@ -1187,6 +1200,18 @@
qparams {:page-id page-id}] qparams {:page-id page-id}]
(rx/of (rt/nav :workspace pparams qparams)))))) (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 (def go-to-file
(ptk/reify ::go-to-file (ptk/reify ::go-to-file

View File

@ -27,10 +27,13 @@
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.time :as dt] [app.util.time :as dt]
[app.util.logging :as log]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[potok.core :as ptk])) [potok.core :as ptk]))
(log/set-level! :warn)
(declare sync-file) (declare sync-file)
(defn default-color-name [color] (defn default-color-name [color]
@ -125,423 +128,6 @@
:object prev}] :object prev}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) (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 (defn add-typography
([typography] (add-typography typography true)) ([typography] (add-typography typography true))
([typography edit?] ([typography edit?]
@ -586,3 +172,470 @@
uchg {:type :add-typography uchg {:type :add-typography
:typography prev}] :typography prev}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) (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))))))

View File

@ -15,8 +15,11 @@
[app.common.pages-helpers :as cph] [app.common.pages-helpers :as cph]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.util.logging :as log]
[app.util.text :as ut])) [app.util.text :as ut]))
(log/set-level! :warn)
(defonce empty-changes [[] []]) (defonce empty-changes [[] []])
(defonce color-sync-attrs (defonce color-sync-attrs
@ -33,9 +36,12 @@
(declare has-asset-reference-fn) (declare has-asset-reference-fn)
(declare get-assets) (declare get-assets)
(declare resolve-shapes-and-components)
(declare generate-sync-shape-and-children-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)
(declare generate-sync-shape-inverse-normal)
(declare generate-sync-shape-inverse-nested)
(declare generate-sync-shape<-component) (declare generate-sync-shape<-component)
(declare generate-sync-shape->component) (declare generate-sync-shape->component)
(declare remove-component-and-ref) (declare remove-component-and-ref)
@ -55,23 +61,25 @@
(assert (nil? (:component-id shape))) (assert (nil? (:component-id shape)))
(assert (nil? (:component-file shape))) (assert (nil? (:component-file shape)))
(assert (nil? (:shape-ref 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 (cond-> new-shape
true true
(assoc :frame-id nil) (-> (assoc :frame-id nil)
(dissoc :component-root?))
(nil? (:parent-id new-shape)) (nil? (:parent-id new-shape))
(dissoc :component-id (dissoc :component-id
:component-file :component-file
:component-root?
:shape-ref))) :shape-ref)))
;; Make the original shape an instance of the new component. ;; Make the original shape an instance of the new component.
;; If one of the original shape children already was a 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] update-original-shape (fn [original-shape new-shape]
(cond-> original-shape (cond-> original-shape
true (nil? (:shape-ref original-shape))
(-> (assoc :shape-ref (:id new-shape)) (-> (assoc :shape-ref (:id new-shape))
(dissoc :touched)) (dissoc :touched))
@ -85,6 +93,16 @@
(cph/clone-object shape nil objects update-new-shape update-original-shape))) (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 ---- ;; ---- General library synchronization functions ----
@ -92,10 +110,13 @@
"Generate changes to synchronize all shapes in all pages of the current file, "Generate changes to synchronize all shapes in all pages of the current file,
with the given asset of the given library." with the given asset of the given library."
[asset-type library-id state] [asset-type library-id state]
(s/assert #{:colors :components :typographies} asset-type) (s/assert #{:colors :components :typographies} asset-type)
(s/assert (s/nilable ::us/uuid) library-id) (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 (let [library-items
(if (nil? library-id) (if (nil? library-id)
(get-in state [:workspace-data asset-type]) (get-in state [:workspace-data asset-type])
@ -124,6 +145,11 @@
"Generate changes to synchronize all shapes inside components of the current "Generate changes to synchronize all shapes inside components of the current
file library, that use the given type of asset of the given library." file library, that use the given type of asset of the given library."
[asset-type library-id state] [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 (let [library-items
(if (nil? library-id) (if (nil? library-id)
(get-in state [:workspace-data asset-type]) (get-in state [:workspace-data asset-type])
@ -151,6 +177,11 @@
"Generate changes to synchronize all shapes in a particular container "Generate changes to synchronize all shapes in a particular container
(a page or a component) that are linked to the given library." (a page or a component) that are linked to the given library."
[asset-type library-id state container page-id component-id] [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) (let [has-asset-reference? (has-asset-reference-fn asset-type library-id)
linked-shapes (cph/select-objects has-asset-reference? container)] linked-shapes (cph/select-objects has-asset-reference? container)]
(loop [shapes (seq linked-shapes) (loop [shapes (seq linked-shapes)
@ -176,7 +207,7 @@
[asset-type library-id] [asset-type library-id]
(case asset-type (case asset-type
:components :components
(fn [shape] (and (:component-root? shape) (fn [shape] (and (:component-id shape)
(= (:component-file shape) library-id))) (= (:component-file shape) library-id)))
:colors :colors
@ -214,19 +245,12 @@
(defmethod generate-sync-shape :components (defmethod generate-sync-shape :components
[_ library-id state objects page-id component-id shape] [_ library-id state objects page-id component-id shape]
(let [[all-shapes component root-component] (generate-sync-shape-and-children-components page-id
(resolve-shapes-and-components shape component-id
objects (:id shape)
state (get state :workspace-data)
false)] (get state :workspace-libraries)
false))
(generate-sync-shape-and-children-components shape
all-shapes
component
root-component
page-id
component-id
false)))
(defn- generate-sync-text-shape [shape page-id component-id update-node] (defn- generate-sync-text-shape [shape page-id component-id update-node]
(let [old-content (:content shape) (let [old-content (:content shape)
@ -321,44 +345,6 @@
(get-in state [:workspace-data asset-type]) (get-in state [:workspace-data asset-type])
(get-in state [:workspace-libraries library-id :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 (defn generate-sync-shape-and-children-components
"Generate changes to synchronize one shape that the root of a component "Generate changes to synchronize one shape that the root of a component
instance, and all its children, from the given component. instance, and all its children, from the given component.
@ -367,25 +353,121 @@
be copied to this one. be copied to this one.
If reset? is true, all changed attributes will be copied and the 'touched' If reset? is true, all changed attributes will be copied and the 'touched'
flags in the instance shape will be cleared." flags in the instance shape will be cleared."
[root-shape all-shapes component root-component page-id component-id reset?] [page-id component-id shape-id local-file libraries reset?]
(loop [shapes (seq all-shapes) (log/debug :msg "Sync shape and children" :shape (str shape-id) :reset? reset?)
rchanges [] (let [container (cph/get-container page-id component-id local-file)
uchanges []] shape (cph/get-shape container shape-id)
(let [shape (first shapes)] component (cph/get-component (:component-id shape)
(if (nil? 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] [rchanges uchanges]
(let [[shape-rchanges shape-uchanges] (update-attrs shape
(generate-sync-shape<-component component-shape
shape root-shape
root-shape root-component
root-component page-id
component component-id
page-id {:omit-touched? false
component-id :reset-touched? false
reset?)] :set-touched? false
(recur (next shapes) :copy-touched? true})
(d/concat rchanges shape-rchanges)
(d/concat uchanges shape-uchanges))))))) 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 (defn- generate-sync-shape-inverse
"Generate changes to update the component a shape is linked to, from "Generate changes to update the component a shape is linked to, from
@ -395,23 +477,107 @@
shapes. shapes.
And if the component shapes are, in turn, instances of a second component, And if the component shapes are, in turn, instances of a second component,
their 'touched' flags will be set accordingly." their 'touched' flags will be set accordingly."
[root-shape all-shapes component root-component page-id] [page-id shape-id local-file libraries]
(loop [shapes (seq all-shapes) (log/debug :msg "Sync inverse shape and children" :shape (str shape-id))
rchanges [] (let [page (cph/get-container page-id nil local-file)
uchanges []] shape (cph/get-shape page shape-id)
(let [shape (first shapes)] component (cph/get-component (:component-id shape)
(if (nil? 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] [rchanges uchanges]
(let [[shape-rchanges shape-uchanges] (update-attrs component-shape
(generate-sync-shape->component shape
shape root-component
root-shape root-shape
root-component nil
component (:id component)
page-id)] {:omit-touched? false
(recur (next shapes) :reset-touched? false
(d/concat rchanges shape-rchanges) :set-touched? false
(d/concat uchanges shape-uchanges))))))) :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 (defn- generate-sync-shape<-component
"Generate changes to synchronize one shape that is linked to other shape "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 "Generate changes to synchronize one shape inside a component, with other
shape that is linked to it." shape that is linked to it."
[shape root-shape root-component component page-id] [shape root-shape root-component component page-id]
;; ===== Uncomment this to debug =====
;; (js/console.log "component" (clj->js component))
(if (nil? component) (if (nil? component)
empty-changes empty-changes
(let [component-shape (get (:objects component) (:shape-ref shape))] (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) (if (nil? component-shape)
empty-changes empty-changes
(let [;; ===== Uncomment this to debug ===== (let [[rchanges1 uchanges1]
;; _(js/console.info "update" (:name shape) "->" (:name component-shape))
[rchanges1 uchanges1]
(update-attrs component-shape (update-attrs component-shape
shape shape
root-component root-component
@ -552,15 +712,17 @@
If set-touched? is true, the corresponding 'touched' flags will be If set-touched? is true, the corresponding 'touched' flags will be
set in dest shape if they are different than their current values." set in dest shape if they are different than their current values."
[dest-shape origin-shape dest-root origin-root page-id component-id [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 === (log/info :msg (str "SYNC "
;; (println "SYNC" (:name origin-shape)
;; "[C]" (:name origin-shape) " -> "
;; "->" (if page-id "[W] " "[C] ")
;; (if page-id "[W]" ["C"]) (:name dest-shape)))
;; (:name dest-shape)
;; (str options))
(let [; The position attributes need a special sync algorith, because we do (let [; The position attributes need a special sync algorith, because we do
; not synchronize the absolute position, but the position relative of ; not synchronize the absolute position, but the position relative of
@ -582,16 +744,24 @@
(let [attr (first attrs)] (let [attr (first attrs)]
(if (nil? attr) (if (nil? attr)
(let [roperations (if reset-touched? (let [roperations (cond
reset-touched?
(conj roperations (conj roperations
{:type :set-touched {:type :set-touched
:touched nil}) :touched nil})
copy-touched?
(conj roperations
{:type :set-touched
:touched (:touched origin-shape)})
:else
roperations) roperations)
uoperations (if reset-touched? uoperations (cond
(or reset-touched? copy-touched?)
(conj uoperations (conj uoperations
{:type :set-touched {:type :set-touched
:touched (:touched dest-shape)}) :touched (:touched dest-shape)})
:else
uoperations) uoperations)
rchanges [(d/without-nils {:type :mod-obj rchanges [(d/without-nils {:type :mod-obj

View File

@ -21,7 +21,6 @@
[app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.circle :as circle] [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.image :as image]
[app.main.ui.shapes.path :as path] [app.main.ui.shapes.path :as path]
[app.main.ui.shapes.rect :as rect] [app.main.ui.shapes.rect :as rect]
@ -85,7 +84,6 @@
(case (:type shape) (case (:type shape)
:curve [:> path/path-shape opts] :curve [:> path/path-shape opts]
:text [:> text/text-shape opts] :text [:> text/text-shape opts]
:icon [:> icon/icon-shape opts]
:rect [:> rect/rect-shape opts] :rect [:> rect/rect-shape opts]
:path [:> path/path-shape opts] :path [:> path/path-shape opts]
:image [:> image/image-shape opts] :image [:> image/image-shape opts]

View File

@ -39,7 +39,6 @@
;; ---- Workspace refs ;; ---- Workspace refs
(def workspace-local (def workspace-local
(l/derived :workspace-local st/state)) (l/derived :workspace-local st/state))
@ -56,7 +55,6 @@
(def selected-zoom (def selected-zoom
(l/derived :zoom workspace-local)) (l/derived :zoom workspace-local))
(def selected-drawing-tool (def selected-drawing-tool
(l/derived :tool workspace-drawing)) (l/derived :tool workspace-drawing))
@ -89,7 +87,6 @@
(assoc :pages (get-in file [:data :pages]))))) (assoc :pages (get-in file [:data :pages])))))
st/state =)) st/state =))
(def workspace-file-colors (def workspace-file-colors
(l/derived (fn [state] (l/derived (fn [state]
(when-let [file (:workspace-file state)] (when-let [file (:workspace-file state)]
@ -113,6 +110,15 @@
(def workspace-shared-files (def workspace-shared-files
(l/derived :workspace-shared-files st/state)) (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 (def workspace-libraries
(l/derived :workspace-libraries st/state)) (l/derived :workspace-libraries st/state))

View File

@ -85,8 +85,9 @@
(logjs "state"))))) (logjs "state")))))
(defn ^:export dump-tree (defn ^:export dump-tree
([] (dump-tree false)) ([] (dump-tree false false))
([show-touched] ([show-ids] (dump-tree show-ids false))
([show-ids show-touched]
(let [page-id (get @state :current-page-id) (let [page-id (get @state :current-page-id)
objects (get-in @state [:workspace-data :pages-index page-id :objects]) objects (get-in @state [:workspace-data :pages-index page-id :objects])
components (get-in @state [:workspace-data :components]) components (get-in @state [:workspace-data :components])
@ -98,6 +99,7 @@
(println (str/pad (str (str/repeat " " level) (println (str/pad (str (str/repeat " " level)
(:name shape) (:name shape)
(when (seq (:touched shape)) "*") (when (seq (:touched shape)) "*")
(when show-ids (str/format " <%s>" (:id shape))))
{:length 20 {:length 20
:type :right}) :type :right})
(show-component shape objects)) (show-component shape objects))
@ -107,7 +109,7 @@
(str (:touched shape))))) (str (:touched shape)))))
(when (:shapes shape) (when (:shapes shape)
(dorun (for [shape-id (: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] (show-component [shape objects]
(if (nil? (:shape-ref shape)) (if (nil? (:shape-ref shape))
@ -129,7 +131,8 @@
(when component-file (str/format "<%s> " (:name component-file))) (when component-file (str/format "<%s> " (:name component-file)))
(:name component-shape) (:name component-shape)
(if (or (:component-root? shape) (if (or (:component-root? shape)
(nil? (:component-id shape))) (nil? (:component-id shape))
true)
"" ""
(let [component-id (:component-id shape) (let [component-id (:component-id shape)
component-file-id (:component-file shape) component-file-id (:component-file shape)

View File

@ -149,10 +149,12 @@
:workspace :workspace
(let [project-id (uuid (get-in route [:params :path :project-id])) (let [project-id (uuid (get-in route [:params :path :project-id]))
file-id (uuid (get-in route [:params :path :file-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 [:& workspace/workspace {:project-id project-id
:file-id file-id :file-id file-id
:page-id page-id :page-id page-id
:layout-name (keyword layout-name)
:key file-id}]) :key file-id}])
:not-authorized :not-authorized

View 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]))

View 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)]))

View File

@ -20,7 +20,7 @@
(if ref (if ref
(let [target (dom/get-target event) (let [target (dom/get-target event)
parent (mf/ref-val ref)] parent (mf/ref-val ref)]
(when-not (.contains parent target) (when-not (or (not parent) (.contains parent target))
(on-close))) (on-close)))
(on-close))) (on-close)))

View File

@ -17,35 +17,43 @@
[app.util.data :refer [classnames]])) [app.util.data :refer [classnames]]))
(mf/defc editable-label (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) (let [input (mf/use-ref nil)
state (mf/use-state (:editing false)) state (mf/use-state (:editing false))
is-editing (or edit (:editing @state)) is-editing (:editing @state)
start-editing (fn [] start-editing (fn []
(swap! state assoc :editing true) (swap! state assoc :editing true)
(timers/schedule 100 #(dom/focus! (mf/ref-val input)))) (timers/schedule 100 #(dom/focus! (mf/ref-val input))))
stop-editing (fn [] (swap! state assoc :editing false)) 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 [] cancel-editing (fn []
(stop-editing) (stop-editing)
(when on-cancel (on-cancel))) (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] on-key-up (fn [e]
(cond (cond
(kbd/esc? e) (kbd/esc? e)
(cancel-editing) (cancel-editing)
(kbd/enter? e) (kbd/enter? e)
(let [value (-> e dom/get-target dom/get-value)] (accept-editing)))]
(on-change value)
(stop-editing)))) (mf/use-effect
] (mf/deps editing?)
(fn []
(when (and editing? (not (:editing @state)))
(start-editing))))
(if is-editing (if is-editing
[:div.editable-label {:class class-name} [:div.editable-label {:class class-name}
[:input.editable-label-input {:ref input [:input.editable-label-input {:ref input
:default-value value :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-close {:on-click cancel-editing} i/close]]
[:span.editable-label {:class class-name [:span.editable-label {:class class-name
:on-double-click on-dbl-click} value] :on-double-click on-dbl-click} value])))
)))

View File

@ -393,18 +393,15 @@
[:li.recent-projects [:li.recent-projects
{:on-click go-projects {:on-click go-projects
:class-name (when projects? "current")} :class-name (when projects? "current")}
i/recent
[:span.element-title (t locale "labels.projects")]] [:span.element-title (t locale "labels.projects")]]
[:li {:on-click go-drafts [:li {:on-click go-drafts
:class-name (when drafts? "current")} :class-name (when drafts? "current")}
i/file-html
[:span.element-title (t locale "labels.drafts")]] [:span.element-title (t locale "labels.drafts")]]
[:li {:on-click go-libs [:li {:on-click go-libs
:class-name (when libs? "current")} :class-name (when libs? "current")}
i/library
[:span.element-title (t locale "labels.shared-libraries")]]]] [:span.element-title (t locale "labels.shared-libraries")]]]]
[:hr] [:hr]
@ -442,6 +439,7 @@
[:div.profile-section {:on-click #(reset! show true)} [:div.profile-section {:on-click #(reset! show true)}
[:img {:src photo}] [:img {:src photo}]
[:span (:fullname profile)] [:span (:fullname profile)]
i/arrow-down
[:& dropdown {:on-close #(reset! show false) [:& dropdown {:on-close #(reset! show false)
:show @show} :show @show}

View 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}]])])))

View File

@ -33,8 +33,7 @@
:cy cy :cy cy
:rx rx :rx rx
:ry ry :ry ry
:transform transform :transform transform}))]
:id (str "shape-" id)}))]
[:& shape-custom-stroke {:shape shape [:& shape-custom-stroke {:shape shape
:base-props props :base-props props

View File

@ -97,7 +97,7 @@
(mf/defc layer-blur-filter (mf/defc layer-blur-filter
[{:keys [filter-id params]}] [{:keys [filter-id params]}]
[:feGaussianBlur {:stdDeviation (/ (:value params) 2) [:feGaussianBlur {:stdDeviation (:value params)
:result filter-id}]) :result filter-id}])
(mf/defc image-fix-filter [{:keys [filter-id]}] (mf/defc image-fix-filter [{:keys [filter-id]}]

View File

@ -31,7 +31,6 @@
(obj/merge! (obj/merge!
#js {:x 0 #js {:x 0
:y 0 :y 0
:id (str "shape-" id)
:width width :width width
:height height}))] :height height}))]
[:svg {:x x :y y :width width :height height [:svg {:x x :y y :width width :height height

View File

@ -54,7 +54,6 @@
:y y :y y
:fill (if (debug? :group) "red" "transparent") :fill (if (debug? :group) "red" "transparent")
:opacity 0.5 :opacity 0.5
:id (str "group-" id)
:width width :width width
:height height}])]))) :height height}])])))

View File

@ -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]))

View File

@ -43,7 +43,6 @@
#js {:x x #js {:x x
:y y :y y
:transform transform :transform transform
:id (str "shape-" id)
:width width :width width
:height height :height height
:preserveAspectRatio "none" :preserveAspectRatio "none"

View File

@ -52,7 +52,6 @@
props (-> (attrs/extract-style-attrs shape) props (-> (attrs/extract-style-attrs shape)
(obj/merge! (obj/merge!
#js {:transform transform #js {:transform transform
:id (str "shape-" id)
:d pdata}))] :d pdata}))]
(if background? (if background?
[:g {:mask mask-id} [:g {:mask mask-id}

View File

@ -32,7 +32,6 @@
#js {:x x #js {:x x
:y y :y y
:transform transform :transform transform
:id (str "shape-" id)
:width width :width width
:height height}))] :height height}))]

View File

@ -18,27 +18,6 @@
[app.main.ui.shapes.gradients :as grad] [app.main.ui.shapes.gradients :as grad]
[app.main.ui.context :as muc])) [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/defc shape-container
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
@ -50,24 +29,14 @@
group-props (-> props group-props (-> props
(obj/clone) (obj/clone)
(obj/without ["shape" "children"]) (obj/without ["shape" "children"])
(obj/set! "id" (str "shape-" (:id shape)))
(obj/set! "className" "shape") (obj/set! "className" "shape")
(obj/set! "data-type" (:type shape)) (obj/set! "filter" (filters/filter-str filter-id 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)
]
[:& (mf/provider muc/render-ctx) {:value render-id} [:& (mf/provider muc/render-ctx) {:value render-id}
[:> :g group-props [:> :g group-props
[:defs [:defs
[:& filters/filters {:shape shape :filter-id filter-id}] [:& filters/filters {:shape shape :filter-id filter-id}]
[:& grad/gradient {:shape shape :attr :fill-color-gradient}] [:& grad/gradient {:shape shape :attr :fill-color-gradient}]
[:& grad/gradient {:shape shape :attr :stroke-color-gradient}] [:& grad/gradient {:shape shape :attr :stroke-color-gradient}]]
#_(when (:blur shape)
[:clipPath {:id (str "blur_" render-id)}
children])]
[:& background-blur {:shape shape}]
children]])) children]]))

View File

@ -231,7 +231,6 @@
:y y :y y
:data-colors (retrieve-colors shape) :data-colors (retrieve-colors shape)
:transform (geom/transform-matrix shape) :transform (geom/transform-matrix shape)
:id (str id)
:width width :width width
:height height :height height
:mask mask-id} :mask mask-id}

View File

@ -37,7 +37,7 @@
(st/emit! (dv/select-shape (:id frame))))) (st/emit! (dv/select-shape (:id frame)))))
(mf/defc render-panel (mf/defc render-panel
[{:keys [data local index]}] [{:keys [data local index page-id file-id]}]
(let [locale (mf/deref i18n/locale) (let [locale (mf/deref i18n/locale)
frames (:frames data []) frames (:frames data [])
objects (:objects data) objects (:objects data)
@ -63,15 +63,18 @@
[:* [:*
[:& left-sidebar {:frame frame}] [:& left-sidebar {:frame frame}]
[:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)} [:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)}
[:& render-frame-svg {:frame-id (:id frame) [:div.handoff-svg-container
:zoom (:zoom local) [:& render-frame-svg {:frame-id (:id frame)
:objects objects}]] :zoom (:zoom local)
[:& right-sidebar {:frame frame}]])])) :objects objects}]]]
[:& right-sidebar {:frame frame
:page-id page-id
:file-id file-id}]])]))
(mf/defc handoff-content (mf/defc handoff-content
[{:keys [data local index] :as props}] [{:keys [data local index page-id file-id] :as props}]
(let [container (mf/use-ref)
(let [container (mf/use-ref)
[toggle-fullscreen fullscreen?] (hooks/use-fullscreen container) [toggle-fullscreen fullscreen?] (hooks/use-fullscreen container)
on-mouse-wheel on-mouse-wheel
@ -110,7 +113,9 @@
:screen :handoff}]) :screen :handoff}])
[:& render-panel {:data data [:& render-panel {:data data
:local local :local local
:index index}]]])) :index index
:page-id page-id
:file-id file-id}]]]))
(mf/defc handoff (mf/defc handoff
[{:keys [file-id page-id index] :as props}] [{:keys [file-id page-id index] :as props}]
@ -122,6 +127,8 @@
(let [data (mf/deref refs/viewer-data) (let [data (mf/deref refs/viewer-data)
local (mf/deref refs/viewer-local)] local (mf/deref refs/viewer-local)]
(when data (when data
[:& handoff-content {:index index [:& handoff-content {:file-id file-id
:page-id page-id
:index index
:local local :local local
:data data}]))) :data data}])))

View File

@ -12,6 +12,7 @@
[rumext.alpha :as mf] [rumext.alpha :as mf]
[app.util.i18n :as i18n] [app.util.i18n :as i18n]
[app.common.geom.shapes :as gsh] [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.layout :refer [layout-panel]]
[app.main.ui.viewer.handoff.attributes.fill :refer [fill-panel]] [app.main.ui.viewer.handoff.attributes.fill :refer [fill-panel]]
[app.main.ui.viewer.handoff.attributes.stroke :refer [stroke-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.image :refer [image-panel]]
[app.main.ui.viewer.handoff.attributes.text :refer [text-panel]])) [app.main.ui.viewer.handoff.attributes.text :refer [text-panel]]))
(mf/defc attributes (def type->options
[{:keys [shapes frame options]}] {:multiple [:fill :stroke :image :text :shadow :blur]
(let [locale (mf/deref i18n/locale) :frame [:layout :fill]
shapes (->> shapes :group [:layout]
(map #(gsh/translate-to-frame % frame))) :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 [:div.element-options
(for [option options] (for [option options]
[:> (case option [:> (case option
@ -39,5 +50,9 @@
:text text-panel) :text text-panel)
{:shapes shapes {:shapes shapes
:frame frame :frame frame
:locale locale}])])) :locale locale}])
(when-not (= :multiple type)
[:& exports
{:shape (first shapes)
:page-id page-id
:file-id file-id}])]))

View File

@ -13,29 +13,30 @@
[cuerdas.core :as str] [cuerdas.core :as str]
[app.util.i18n :refer [t]] [app.util.i18n :refer [t]]
[app.main.ui.icons :as i] [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] (defn has-blur? [shape]
(:blur shape)) (:blur shape))
(defn copy-blur [shape] (defn copy-data [shape]
(copy-cb shape (cg/generate-css-props
:blur shape
:to-prop "filter" :blur
:format #(str/fmt "blur(%spx)" (:value %)))) {:to-prop "filter"
:format #(str/fmt "blur(%spx)" (:value %))}))
(mf/defc blur-panel [{:keys [shapes locale]}] (mf/defc blur-panel [{:keys [shapes locale]}]
(let [shapes (->> shapes (filter has-blur?)) (let [shapes (->> shapes (filter has-blur?))]
handle-copy (when (= (count shapes) 1) (copy-blur (first shapes)))]
(when (seq shapes) (when (seq shapes)
[:div.attributes-block [:div.attributes-block
[:div.attributes-block-title [:div.attributes-block-title
[:div.attributes-block-title-text (t locale "handoff.attributes.blur")] [:div.attributes-block-title-text (t locale "handoff.attributes.blur")]
(when handle-copy (when (= (count shapes) 1)
[:button.attributes-copy-button {:on-click handle-copy} i/copy])] [:& copy-button {:data (copy-data (first shapes))}])]
(for [shape shapes] (for [shape shapes]
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.blur.value")] [:div.attributes-label (t locale "handoff.attributes.blur.value")]
[:div.attributes-value (-> shape :blur :value) "px"] [:div.attributes-value (-> shape :blur :value) "px"]
[:button.attributes-copy-button {:on-click (copy-blur shape)} i/copy]])]))) [:& copy-button {:data (copy-data shape)}]])])))

View File

@ -11,71 +11,69 @@
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]
[okulary.core :as l]
[app.common.math :as mth]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [t] :as i18n] [app.util.i18n :refer [t] :as i18n]
[app.util.color :as uc] [app.util.color :as uc]
[app.common.math :as mth] [app.util.code-gen :as cg]
[app.main.ui.icons :as i]
[app.util.webapi :as wapi] [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]])) [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)) (def file-colors-ref
(into {} (map #(vector % format) properties)) (l/derived (l/in [:viewer-data :file :colors]) st/state))
format)
to-prop (if (not (map? to-prop)) (defn make-colors-library-ref [file-id]
(into {} (map #(vector % to-prop) properties)) (let [get-library
to-prop) (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")) (mf/defc color-row [{:keys [color format copy-data on-change-format]}]
format-property (fn [prop] (let [locale (mf/deref i18n/locale)
(let [css-prop (or (prop to-prop) (name prop))]
(str/fmt " %s: %s;" css-prop ((or (prop format) default-format) (prop values) values))))
text-props (->> properties colors-library-ref (mf/use-memo
(remove #(let [value (get values %)] (mf/deps (:file-id color))
(or (nil? value) (= value 0)))) (make-colors-library-ref (:file-id color)))
(map format-property) colors-library (mf/deref colors-library-ref)
(str/join "\n"))
result (str/fmt "{\n%s\n}" text-props)] file-colors (mf/deref file-colors-ref)
(wapi/write-to-clipboard result)))) color-library-name (get-in (or colors-library file-colors) [(:id color) :name])]
(mf/defc color-row [{:keys [color format on-copy on-change-format]}]
(let [locale (mf/deref i18n/locale)]
[:div.attributes-color-row [: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) [:div.attributes-color-value {:class (when color-library-name "hide-color")}
[:& color-name {:color color}] [:& color-bullet {: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)) "%")])]))
(when-not (and on-change-format (:gradient color)) (if (:gradient color)
[:select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)} [:& color-name {:color color}]
[:option {:value "hex"} (case format
(t locale "handoff.attributes.color.hex")] :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"} (when-not (and on-change-format (:gradient color))
(t locale "handoff.attributes.color.rgba")] [:select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)}
[:option {:value "hex"}
(t locale "handoff.attributes.color.hex")]
[:option {:value "hsla"} [:option {:value "rgba"}
(t locale "handoff.attributes.color.hsla")]]) (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])]))

View File

@ -13,7 +13,9 @@
[app.util.i18n :refer [t]] [app.util.i18n :refer [t]]
[app.util.color :as uc] [app.util.color :as uc]
[app.main.ui.icons :as i] [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]) (def fill-attributes [:fill-color :fill-color-gradient])
@ -21,8 +23,8 @@
{:color (:fill-color shape) {:color (:fill-color shape)
:opacity (:fill-opacity shape) :opacity (:fill-opacity shape)
:gradient (:fill-color-gradient shape) :gradient (:fill-color-gradient shape)
:id (:fill-ref-id shape) :id (:fill-color-ref-id shape)
:file-id (:fill-ref-file-id shape)}) :file-id (:fill-color-ref-file shape)})
(defn has-color? [shape] (defn has-color? [shape]
(and (and
@ -30,36 +32,31 @@
(or (:fill-color shape) (or (:fill-color shape)
(:fill-color-gradient 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]}] (mf/defc fill-block [{:keys [shape locale]}]
(let [color-format (mf/use-state :hex) (let [color-format (mf/use-state :hex)
color (shape->color shape) color (shape->color shape)]
handle-copy (copy-cb shape
fill-attributes
:to-prop "background"
:format #(uc/color->background color))]
[:& color-row {:color color [:& color-row {:color color
:format @color-format :format @color-format
:on-change-format #(reset! color-format %) :on-change-format #(reset! color-format %)
:on-copy handle-copy}])) :copy-data (copy-data shape)}]))
(mf/defc fill-panel (mf/defc fill-panel
[{:keys [shapes locale]}] [{:keys [shapes locale]}]
(let [shapes (->> shapes (filter has-color?)) (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)))]
(when (seq shapes) (when (seq shapes)
[:div.attributes-block [:div.attributes-block
[:div.attributes-block-title [:div.attributes-block-title
[:div.attributes-block-title-text (t locale "handoff.attributes.fill")] [:div.attributes-block-title-text (t locale "handoff.attributes.fill")]
(when handle-copy (when (= (count shapes) 1)
[:button.attributes-copy-button [:& copy-button {:data (copy-data (first shapes))}])]
{:on-click handle-copy}
i/copy])]
(for [shape shapes] (for [shape shapes]
[:& fill-block {:key (str "fill-block-" (:id shape)) [:& fill-block {:key (str "fill-block-" (:id shape))

View File

@ -14,7 +14,8 @@
[app.config :as cfg] [app.config :as cfg]
[app.util.i18n :refer [t]] [app.util.i18n :refer [t]]
[app.main.ui.icons :as i] [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] (defn has-image? [shape]
(and (= (:type shape) :image))) (and (= (:type shape) :image)))
@ -30,12 +31,12 @@
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.image.width")] [:div.attributes-label (t locale "handoff.attributes.image.width")]
[:div.attributes-value (-> shape :metadata :width) "px"] [: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-unit-row
[:div.attributes-label (t locale "handoff.attributes.image.height")] [:div.attributes-label (t locale "handoff.attributes.image.height")]
[:div.attributes-value (-> shape :metadata :height) "px"] [: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) "/"))] (let [filename (last (str/split (-> shape :metadata :path) "/"))]
[:a.download-button {:target "_blank" [:a.download-button {:target "_blank"

View File

@ -14,13 +14,22 @@
[app.util.i18n :refer [t]] [app.util.i18n :refer [t]]
[app.common.math :as mth] [app.common.math :as mth]
[app.main.ui.icons :as i] [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] (def properties [:width :height :x :y :radius :rx])
(copy-cb shape (def params
[:width :height :x :y :rotation] {:to-prop {:x "left"
:to-prop {:x "left" :y "top" :rotation "transform"} :y "top"
:format {:rotation #(str/fmt "rotate(%sdeg)" %)})) :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 (mf/defc layout-block
[{:keys [shape locale]}] [{:keys [shape locale]}]
@ -28,57 +37,46 @@
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.width")] [:div.attributes-label (t locale "handoff.attributes.layout.width")]
[:div.attributes-value (mth/precision (:width shape) 2) "px"] [:div.attributes-value (mth/precision (:width shape) 2) "px"]
[:button.attributes-copy-button [:& copy-button {:data (copy-data shape :width)}]]
{:on-click (copy-cb shape :width)}
i/copy]]
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.height")] [:div.attributes-label (t locale "handoff.attributes.layout.height")]
[:div.attributes-value (mth/precision (:height shape) 2) "px"] [:div.attributes-value (mth/precision (:height shape) 2) "px"]
[:button.attributes-copy-button [:& copy-button {:data (copy-data shape :height)}]]
{:on-click (copy-cb shape :height)}
i/copy]]
(when (not= (:x shape) 0) (when (not= (:x shape) 0)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.left")] [:div.attributes-label (t locale "handoff.attributes.layout.left")]
[:div.attributes-value (mth/precision (:x shape) 2) "px"] [:div.attributes-value (mth/precision (:x shape) 2) "px"]
[:button.attributes-copy-button [:& copy-button {:data (copy-data shape :x)}]])
{:on-click (copy-cb shape :x :to-prop "left")}
i/copy]])
(when (not= (:y shape) 0) (when (not= (:y shape) 0)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.top")] [:div.attributes-label (t locale "handoff.attributes.layout.top")]
[:div.attributes-value (mth/precision (:y shape) 2) "px"] [:div.attributes-value (mth/precision (:y shape) 2) "px"]
[:button.attributes-copy-button [:& copy-button {:data (copy-data shape :y)}]])
{:on-click (copy-cb shape :y :to-prop "top")}
i/copy]]) (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) (when (not= (:rotation shape 0) 0)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.rotation")] [:div.attributes-label (t locale "handoff.attributes.layout.rotation")]
[:div.attributes-value (mth/precision (:rotation shape) 2) "deg"] [:div.attributes-value (mth/precision (:rotation shape) 2) "deg"]
[:button.attributes-copy-button [:& copy-button {:data (copy-data shape :rotation)}]])])
{:on-click (copy-cb shape
:rotation
:to-prop "transform"
:format #(str/fmt "rotate(%sdeg)" %))}
i/copy]])])
(mf/defc layout-panel (mf/defc layout-panel
[{:keys [shapes locale]}] [{:keys [shapes locale]}]
(let [handle-copy (when (= (count shapes) 1) [:div.attributes-block
(copy-layout (first shapes)))] [:div.attributes-block-title
[:div.attributes-block [:div.attributes-block-title-text (t locale "handoff.attributes.layout")]
[:div.attributes-block-title (when (= (count shapes) 1)
[:div.attributes-block-title-text (t locale "handoff.attributes.layout")] [:& copy-button {:data (copy-data (first shapes))}])]
(when handle-copy
[:button.attributes-copy-button
{:on-click handle-copy}
i/copy])]
(for [shape shapes] (for [shape shapes]
[:& layout-block {:shape shape [:& layout-block {:shape shape
:locale locale}])])) :locale locale}])])

View File

@ -12,23 +12,32 @@
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]
[app.util.i18n :refer [t]] [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.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] (defn has-shadow? [shape]
(:shadow shape)) (:shadow shape))
(defn shadow->css [shadow] (defn shape-copy-data [shape]
(let [{:keys [style offset-x offset-y blur spread]} shadow (cg/generate-css-props
css-color (uc/color->background (:color shadow))] shape
(str :shadow
(if (= style :inner-shadow) "inset " "") {:to-prop "box-shadow"
(str/fmt "%spx %spx %spx %spx %s" offset-x offset-y blur spread css-color)))) :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]}] (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-block
[:div.attributes-shadow-row [:div.attributes-shadow-row
[:div.attributes-label (->> shadow :style name (str "handoff.attributes.shadow.style.") (t locale))] [: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-label (t locale "handoff.attributes.shadow.shorthand.spread")]
[:div.attributes-value (str (:spread shadow))]] [:div.attributes-value (str (:spread shadow))]]
[:button.attributes-copy-button [:& copy-button {:data (shadow-copy-data shadow)}]]
{:on-click (copy-cb shadow
:style
:to-prop "box-shadow"
:format #(shadow->css shadow))}
i/copy]]
[:& color-row {:color (:color shadow) [:& color-row {:color (:color shadow)
:format @color-format :format @color-format
:on-change-format #(reset! color-format %)}]])) :on-change-format #(reset! color-format %)}]]))
(mf/defc shadow-panel [{:keys [shapes locale]}] (mf/defc shadow-panel [{:keys [shapes locale]}]
(let [shapes (->> shapes (filter has-shadow?)) (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))))))]
(when (seq shapes) (when (seq shapes)
[:div.attributes-block [:div.attributes-block
[:div.attributes-block-title [:div.attributes-block-title
[:div.attributes-block-title-text (t locale "handoff.attributes.shadow")] [:div.attributes-block-title-text (t locale "handoff.attributes.shadow")]
(when handle-copy-shadow (when (= (count shapes) 1)
[:button.attributes-copy-button {:on-click handle-copy-shadow} i/copy])] [:& copy-button {:data (shape-copy-data (first shapes))}])]
(for [shape shapes] [:div.attributes-shadow-blocks
(for [shadow (:shadow shape)] (for [shape shapes]
[:& shadow-block {:shape shape (for [shadow (:shadow shape)]
:locale locale [:& shadow-block {:shape shape
:shadow shadow}]))]))) :locale locale
:shadow shadow}]))]])))

View File

@ -14,14 +14,16 @@
[app.util.i18n :refer [t]] [app.util.i18n :refer [t]]
[app.util.color :as uc] [app.util.color :as uc]
[app.main.ui.icons :as i] [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] (defn shape->color [shape]
{:color (:stroke-color shape) {:color (:stroke-color shape)
:opacity (:stroke-opacity shape) :opacity (:stroke-opacity shape)
:gradient (:stroke-color-gradient shape) :gradient (:stroke-color-gradient shape)
:id (:stroke-ref-id shape) :id (:stroke-color-ref-id shape)
:file-id (:stroke-ref-file-id shape)}) :file-id (:stroke-color-ref-file shape)})
(defn format-stroke [shape] (defn format-stroke [shape]
(let [width (:stroke-width shape) (let [width (:stroke-width shape)
@ -33,49 +35,46 @@
(and (:stroke-style shape) (and (:stroke-style shape)
(not= (:stroke-style shape) :none))) (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 (mf/defc stroke-block
[{:keys [shape locale]}] [{:keys [shape locale]}]
(let [color-format (mf/use-state :hex) (let [color-format (mf/use-state :hex)
color (shape->color shape) 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-row {:color color [:& color-row {:color color
:format @color-format :format @color-format
:on-change-format #(reset! color-format %) :copy-data (copy-color-data shape)
:on-copy handle-copy-color}] :on-change-format #(reset! color-format %)}]
[:div.attributes-stroke-row [:div.attributes-stroke-row
[:div.attributes-label (t locale "handoff.attributes.stroke.width")] [:div.attributes-label (t locale "handoff.attributes.stroke.width")]
[:div.attributes-value (:stroke-width shape) "px"] [:div.attributes-value (:stroke-width shape) "px"]
[:div.attributes-value (->> shape :stroke-style name (str "handoff.attributes.stroke.style.") (t locale))] [: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))] [: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 (mf/defc stroke-panel
[{:keys [shapes locale]}] [{:keys [shapes locale]}]
(let [shapes (->> shapes (filter has-stroke?)) (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))))]
(when (seq shapes) (when (seq shapes)
[:div.attributes-block [:div.attributes-block
[:div.attributes-block-title [:div.attributes-block-title
[:div.attributes-block-title-text (t locale "handoff.attributes.stroke")] [:div.attributes-block-title-text (t locale "handoff.attributes.stroke")]
(when handle-copy (when (= (count shapes) 1)
[:button.attributes-copy-button [:& copy-button {:data (copy-stroke-data (first shapes))}])]
{:on-click handle-copy} i/copy])]
(for [shape shapes] (for [shape shapes]
[:& stroke-block {:key (str "stroke-color-" (:id shape)) [:& stroke-block {:key (str "stroke-color-" (:id shape))

View File

@ -11,6 +11,7 @@
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]
[okulary.core :as l]
[app.util.data :as d] [app.util.data :as d]
[app.util.i18n :refer [t]] [app.util.i18n :refer [t]]
[app.util.color :as uc] [app.util.color :as uc]
@ -18,11 +19,24 @@
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.webapi :as wapi] [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] (defn has-text? [shape]
(:content 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 (def properties [:fill-color
:fill-color-gradient :fill-color-gradient
:font-family :font-family
@ -37,100 +51,117 @@
{:color (:fill-color shape) {:color (:fill-color shape)
:opacity (:fill-opacity shape) :opacity (:fill-opacity shape)
:gradient (:fill-color-gradient shape) :gradient (:fill-color-gradient shape)
:id (:fill-ref-id shape) :id (:fill-color-ref-id shape)
:file-id (:fill-ref-file-id shape)}) :file-id (:fill-color-ref-file shape)})
(defn format-style [color] (def params
{:font-family #(str "'" % "'") {:to-prop {:fill-color "color"
:font-style #(str "'" % "'") :fill-color-gradient "color"}
:font-size #(str % "px") :format {:font-family #(str "'" % "'")
:line-height #(str % "px") :font-style #(str "'" % "'")
:letter-spacing #(str % "px") :font-size #(str % "px")
:text-decoration name :line-height #(str % "px")
:text-transform name :letter-spacing #(str % "px")
:fill-color #(uc/color->background color) :text-decoration name
:fill-color-gradient #(uc/color->background color)}) :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]}] (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) 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-text-block
[:div.attributes-typography-row (if (:typography-ref-id style)
[:div.typography-sample [:div.attributes-typography-name-row
{:style {:font-family (:font-family full-style) [:div.typography-entry
:font-weight (:font-weight full-style) [:div.typography-sample
:font-style (:font-style full-style)}} {:style {:font-family (:font-family typography)
(t locale "workspace.assets.typography.sample")] :font-weight (:font-weight typography)
[:button.attributes-copy-button :font-style (:font-style typography)}}
{:on-click (copy-cb style properties (t locale "workspace.assets.typography.sample")]]
:to-prop to-prop [:div.typography-entry-name (:name typography)]
:format (format-style color))} [:& copy-button {:data (copy-style-data typography)}]]
i/copy]]
[: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 [:div.attributes-content-row
[:pre.attributes-content (str/trim text)] [:pre.attributes-content (str/trim text)]
[:button.attributes-copy-button [:& copy-button {:data (str/trim text)}]]
{:on-click #(wapi/write-to-clipboard (str/trim text))}
i/copy]]
(when (or (:fill-color style) (:fill-color-gradient style)) (when (or (:fill-color style) (:fill-color-gradient style))
[:& color-row {:format @color-format [:& color-row {:format @color-format
:on-change-format #(reset! color-format %)
:color (shape->color style) :color (shape->color style)
:on-copy (copy-cb style :copy-data (copy-style-data style :fill-color :fill-color-gradient)
[:fill-color :fill-color-gradient] :on-change-format #(reset! color-format %)}])
:to-prop to-prop
:format (format-style color))}])
(when (:font-id style) (when (:font-id style)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.typography.font-family")] [:div.attributes-label (t locale "handoff.attributes.typography.font-family")]
[:div.attributes-value (-> style :font-id fonts/get-font-data :name)] [: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) (when (:font-style style)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.typography.font-style")] [:div.attributes-label (t locale "handoff.attributes.typography.font-style")]
[:div.attributes-value (str (:font-style 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) (when (:font-size style)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.typography.font-size")] [:div.attributes-label (t locale "handoff.attributes.typography.font-size")]
[:div.attributes-value (str (:font-size style)) "px"] [: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) (when (:line-height style)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.typography.line-height")] [:div.attributes-label (t locale "handoff.attributes.typography.line-height")]
[:div.attributes-value (str (:line-height style)) "px"] [: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) (when (:letter-spacing style)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.typography.letter-spacing")] [:div.attributes-label (t locale "handoff.attributes.typography.letter-spacing")]
[:div.attributes-value (str (:letter-spacing style)) "px"] [: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) (when (:text-decoration style)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.typography.text-decoration")] [:div.attributes-label (t locale "handoff.attributes.typography.text-decoration")]
[:div.attributes-value (->> style :text-decoration (str "handoff.attributes.typography.text-decoration.") (t locale))] [: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) (when (:text-transform style)
[:div.attributes-unit-row [:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.typography.text-transform")] [:div.attributes-label (t locale "handoff.attributes.typography.text-transform")]
[:div.attributes-value (->> style :text-transform (str "handoff.attributes.typography.text-transform.") (t locale))] [: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]}] (mf/defc text-block [{:keys [shape locale]}]
(let [font (ut/search-text-attrs (:content shape) (let [font (ut/search-text-attrs (:content shape)
(keys ut/default-text-attrs)) (keys ut/default-text-attrs))
style-text-blocks (->> (keys ut/default-text-attrs) style-text-blocks (->> (keys ut/default-text-attrs)
(ut/parse-style-text-blocks (:content shape)) (ut/parse-style-text-blocks (:content shape))
(remove (fn [[style text]] (str/empty? (str/trim text)))) (remove (fn [[style text]] (str/empty? (str/trim text))))

View 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}]]]
]))

View 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"))]])]))

View File

@ -26,7 +26,6 @@
[app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.circle :as circle]
[app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.group :as group] [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.image :as image]
[app.main.ui.shapes.path :as path] [app.main.ui.shapes.path :as path]
[app.main.ui.shapes.rect :as rect] [app.main.ui.shapes.rect :as rect]
@ -111,7 +110,6 @@
[objects show-interactions?] [objects show-interactions?]
(let [path-wrapper (shape-wrapper-factory path/path-shape) (let [path-wrapper (shape-wrapper-factory path/path-shape)
text-wrapper (shape-wrapper-factory text/text-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) rect-wrapper (shape-wrapper-factory rect/rect-shape)
image-wrapper (shape-wrapper-factory image/image-shape) image-wrapper (shape-wrapper-factory image/image-shape)
circle-wrapper (shape-wrapper-factory circle/circle-shape)] circle-wrapper (shape-wrapper-factory circle/circle-shape)]
@ -130,7 +128,6 @@
(case (:type shape) (case (:type shape)
:curve [:> path-wrapper opts] :curve [:> path-wrapper opts]
:text [:> text-wrapper opts] :text [:> text-wrapper opts]
:icon [:> icon-wrapper opts]
:rect [:> rect-wrapper opts] :rect [:> rect-wrapper opts]
:path [:> path-wrapper opts] :path [:> path-wrapper opts]
:image [:> image-wrapper opts] :image [:> image-wrapper opts]
@ -163,7 +160,8 @@
(mf/deps objects) (mf/deps objects)
#(frame-container-factory objects))] #(frame-container-factory objects))]
[:svg {:view-box vbox [:svg {:id "svg-frame"
:view-box vbox
:width width :width width
:height height :height height
:version "1.1" :version "1.1"

View File

@ -15,8 +15,9 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.components.tab-container :refer [tab-container tab-element]] [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.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 (defn make-selected-shapes-iref
[] []
@ -28,33 +29,15 @@
(mapv resolve-shape selected)))] (mapv resolve-shape selected)))]
#(l/derived selected->shapes st/state))) #(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 [] (mf/defc right-sidebar
[:div.element-options]) [{:keys [frame page-id file-id]}]
(let [expanded (mf/use-state false)
(mf/defc right-sidebar [{:keys [frame]}] locale (mf/deref i18n/locale)
(let [locale (mf/deref i18n/locale)
section (mf/use-state :info #_:code) section (mf/use-state :info #_:code)
selected-ref (mf/use-memo (make-selected-shapes-iref)) selected-ref (mf/use-memo (make-selected-shapes-iref))
shapes (mf/deref selected-ref)] 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 [:div.settings-bar-inside
(when (seq shapes) (when (seq shapes)
[:div.tool-window [:div.tool-window
@ -69,11 +52,17 @@
[:span.tool-window-bar-title (->> shapes first :type name (str "handoff.tabs.code.selected.") (t locale))]]) [:span.tool-window-bar-title (->> shapes first :type name (str "handoff.tabs.code.selected.") (t locale))]])
] ]
[:div.tool-window-content [:div.tool-window-content
[:& tab-container {:on-change-tab #(reset! section %) [:& tab-container {:on-change-tab #(do
(reset! expanded false)
(reset! section %))
:selected @section} :selected @section}
[:& tab-element {:id :info :title (t locale "handoff.tabs.info")} [:& tab-element {:id :info :title (t locale "handoff.tabs.info")}
[:& attributes-panel {:frame frame [:& attributes {:page-id page-id
:shapes shapes}]] :file-id file-id
:frame frame
:shapes shapes}]]
[:& tab-element {:id :code :title (t locale "handoff.tabs.code")} [:& tab-element {:id :code :title (t locale "handoff.tabs.code")}
[:& code-panel]]]]])]])) [:& code {:frame frame
:shapes shapes
:on-expand #(swap! expanded not)}]]]]])]]))

View File

@ -10,15 +10,30 @@
(ns app.main.ui.viewer.handoff.selection-feedback (ns app.main.ui.viewer.handoff.selection-feedback
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
[app.common.data :as d]
[app.common.math :as mth]
[app.common.geom.shapes :as gsh] [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 selection-rect-width 1)
(def select-guide-width 1)
(def select-guide-dasharray 5)
;; ------------------------------------------------
;; LENSES
;; ------------------------------------------------
(defn make-selected-shapes-iref (defn make-selected-shapes-iref
"Creates a lens to the current selected shapes"
[] []
(let [selected->shapes (let [selected->shapes
(fn [state] (fn [state]
@ -29,38 +44,67 @@
#(l/derived selected->shapes st/state))) #(l/derived selected->shapes st/state)))
(defn make-hover-shapes-iref (defn make-hover-shapes-iref
"Creates a lens to the shapes the user is making hover"
[] []
(let [hover->shapes (let [hover->shapes
(fn [state] (fn [state]
(let [hover (get-in state [:viewer-local :hover]) (let [hover (get-in state [:viewer-local :hover])
objects (get-in state [:viewer-data :page :objects]) objects (get-in state [:viewer-data :page :objects])]
resolve-shape #(get objects %)] (get objects hover)))]
(mapv resolve-shape hover)))]
#(l/derived hover->shapes st/state))) #(l/derived hover->shapes st/state)))
(mf/defc selection-rect [{:keys [shape]}] (def selected-zoom
(let [{:keys [x y width height]} (:selrect shape)] (l/derived (l/in [:viewer-local :zoom]) st/state))
[:rect {:x x
:y y ;; ------------------------------------------------
:width width ;; HELPERS
:height height ;; ------------------------------------------------
:fill "transparent"
:stroke selection-rect-color-normal (defn frame->selrect [frame]
:stroke-width selection-rect-width {:x1 0
:pointer-events "none"}])) :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]}] (mf/defc selection-feedback [{:keys [frame]}]
(let [hover-shapes-ref (mf/use-memo (make-hover-shapes-iref)) (let [zoom (mf/deref selected-zoom)
hover-shapes (->> (mf/deref hover-shapes-ref)
(map #(gsh/translate-to-frame % frame))) 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-ref (mf/use-memo (make-selected-shapes-iref))
selected-shapes (->> (mf/deref selected-shapes-ref) selected-shapes (->> (mf/deref selected-shapes-ref)
(map #(gsh/translate-to-frame % frame)))] (map #(gsh/translate-to-frame % frame)))
[:* selrect (gsh/selection-rect selected-shapes)]
(for [shape hover-shapes]
[:& selection-rect {:shape shape}])
(for [shape selected-shapes] (when (seq selected-shapes)
[:& selection-rect {:shape shape}])])) [: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}]])))

View File

@ -21,7 +21,6 @@
[app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.circle :as circle]
[app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.group :as group] [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.image :as image]
[app.main.ui.shapes.path :as path] [app.main.ui.shapes.path :as path]
[app.main.ui.shapes.rect :as rect] [app.main.ui.shapes.rect :as rect]
@ -86,10 +85,6 @@
[show-interactions?] [show-interactions?]
(generic-wrapper-factory rect/rect-shape 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 (defn image-wrapper
[show-interactions?] [show-interactions?]
(generic-wrapper-factory image/image-shape show-interactions?)) (generic-wrapper-factory image/image-shape show-interactions?))
@ -142,7 +137,6 @@
[objects show-interactions?] [objects show-interactions?]
(let [path-wrapper (path-wrapper show-interactions?) (let [path-wrapper (path-wrapper show-interactions?)
text-wrapper (text-wrapper show-interactions?) text-wrapper (text-wrapper show-interactions?)
icon-wrapper (icon-wrapper show-interactions?)
rect-wrapper (rect-wrapper show-interactions?) rect-wrapper (rect-wrapper show-interactions?)
image-wrapper (image-wrapper show-interactions?) image-wrapper (image-wrapper show-interactions?)
circle-wrapper (circle-wrapper show-interactions?)] circle-wrapper (circle-wrapper show-interactions?)]
@ -160,7 +154,6 @@
(case (:type shape) (case (:type shape)
:curve [:> path-wrapper opts] :curve [:> path-wrapper opts]
:text [:> text-wrapper opts] :text [:> text-wrapper opts]
:icon [:> icon-wrapper opts]
:rect [:> rect-wrapper opts] :rect [:> rect-wrapper opts]
:path [:> path-wrapper opts] :path [:> path-wrapper opts]
:image [:> image-wrapper opts] :image [:> image-wrapper opts]

View File

@ -99,8 +99,10 @@
i/loader-pencil]) i/loader-pencil])
(mf/defc workspace (mf/defc workspace
[{:keys [project-id file-id page-id] :as props}] [{:keys [project-id file-id page-id layout-name] :as props}]
(mf/use-effect #(st/emit! dw/initialize-layout)) (mf/use-effect
(mf/deps layout-name)
#(st/emit! (dw/initialize-layout layout-name)))
(mf/use-effect (mf/use-effect
(mf/deps project-id file-id) (mf/deps project-id file-id)

View File

@ -75,6 +75,7 @@
(st/emit! (dwl/update-component id)) (st/emit! (dwl/update-component id))
(st/emit! (dwl/sync-file nil)) (st/emit! (dwl/sync-file nil))
(st/emit! dwc/commit-undo-transaction)) (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 do-navigate-component-file #(st/emit! (dwl/nav-to-component-file
(:component-file shape)))] (:component-file shape)))]
[:* [:*
@ -139,25 +140,38 @@
[:& menu-entry {:title (t locale "workspace.shape.menu.lock") [:& menu-entry {:title (t locale "workspace.shape.menu.lock")
:on-click do-lock-shape}]) :on-click do-lock-shape}])
(when (nil? (:shape-ref shape)) (when (or (nil? (:shape-ref shape))
(> (count selected) 1))
[:* [:*
[:& menu-separator] [:& menu-separator]
[:& menu-entry {:title (t locale "workspace.shape.menu.create-component") [:& menu-entry {:title (t locale "workspace.shape.menu.create-component")
:shortcut "Ctrl + K" :shortcut "Ctrl + K"
:on-click do-add-component}]]) :on-click do-add-component}]])
(when (:component-id shape) (when (and (:component-id shape)
[:* (= (count selected) 1))
[:& menu-separator] ;; WARNING: this menu is the same as the context menu at the sidebar.
[:& menu-entry {:title (t locale "workspace.shape.menu.detach-instance") ;; If you change it, you must change equally the file
:on-click do-detach-component}] ;; app/main/ui/workspace/sidebar/options/component.cljs
[:& menu-entry {:title (t locale "workspace.shape.menu.reset-overrides") (if (nil? (:component-file shape))
:on-click do-reset-component}] [:*
(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") [:& menu-entry {:title (t locale "workspace.shape.menu.update-master")
:on-click do-update-component}] :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") [:& 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-separator]
[:& menu-entry {:title (t locale "workspace.shape.menu.delete") [:& menu-entry {:title (t locale "workspace.shape.menu.delete")

View File

@ -105,17 +105,17 @@
[:li.tooltip.tooltip-right [:li.tooltip.tooltip-right
{:alt "Layers" {:alt "Layers"
:class (when (contains? layout :layers) "selected") :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] i/layers]
[:li.tooltip.tooltip-right [:li.tooltip.tooltip-right
{:alt (t locale "workspace.toolbar.assets") {:alt (t locale "workspace.toolbar.assets")
:class (when (contains? layout :assets) "selected") :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] i/library]
[:li.tooltip.tooltip-right [:li.tooltip.tooltip-right
{:alt "History" {:alt "History"
:class (when (contains? layout :document-history) "selected") :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] i/undo-history]
[:li.tooltip.tooltip-right [:li.tooltip.tooltip-right
{:alt (t locale "workspace.toolbar.color-palette") {:alt (t locale "workspace.toolbar.color-palette")

View File

@ -7,6 +7,7 @@
(ns app.main.ui.workspace.libraries (ns app.main.ui.workspace.libraries
(:require (:require
[app.common.data :as d]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
@ -142,7 +143,6 @@
:value (tr "workspace.libraries.update") :value (tr "workspace.libraries.update")
:on-click #(update-library (:id library))}]])]])])) :on-click #(update-library (:id library))}]])]])]))
(mf/defc libraries-dialog (mf/defc libraries-dialog
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :libraries-dialog} ::mf/register-as :libraries-dialog}
@ -152,7 +152,9 @@
locale (mf/deref i18n/locale) locale (mf/deref i18n/locale)
project (mf/deref refs/workspace-project) project (mf/deref refs/workspace-project)
file (mf/deref workspace-file) 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) shared-files (mf/deref refs/workspace-shared-files)
change-tab #(reset! selected-tab %) change-tab #(reset! selected-tab %)

View File

@ -15,6 +15,8 @@
[potok.core :as ptk] [potok.core :as ptk]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[rumext.util :refer [map->obj]] [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 :as dw]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.main.refs :as refs] [app.main.refs :as refs]
@ -28,7 +30,8 @@
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.util.debug :refer [debug?]] [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 rotation-handler-size 25)
(def resize-point-radius 4) (def resize-point-radius 4)
@ -138,8 +141,7 @@
[:circle {:r (/ resize-point-radius zoom) [:circle {:r (/ resize-point-radius zoom)
:style {:fillOpacity "1" :style {:fillOpacity "1"
:strokeWidth "1px" :strokeWidth "1px"
:vectorEffect "non-scaling-stroke" :vectorEffect "non-scaling-stroke"}
}
:fill "#FFFFFF" :fill "#FFFFFF"
:stroke (if (and (= position :bottom-right) overflow-text) "red" color) :stroke (if (and (= position :bottom-right) overflow-text) "red" color)
:cx cx' :cx cx'
@ -266,9 +268,16 @@
:fill "transparent"}}]])) :fill "transparent"}}]]))
(mf/defc multiple-selection-handlers (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) (let [shape (geom/selection-rect shapes)
shape-center (geom/center shape) 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] on-resize (fn [current-position initial-position event]
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! (dw/start-resize current-position initial-position selected shape))) (st/emit! (dw/start-resize current-position initial-position selected shape)))
@ -282,13 +291,29 @@
:color color :color color
:on-resize on-resize :on-resize on-resize
:on-rotate on-rotate}] :on-rotate on-rotate}]
(when show-distances
[:& msr/measurement {:bounds vbox
:selected-shapes shapes
:hover-shape hover-shape
:zoom zoom}])
(when (debug? :selection-center) (when (debug? :selection-center)
[:circle {:cx (:x shape-center) :cy (:y shape-center) :r 5 :fill "yellow"}])])) [:circle {:cx (:x shape-center) :cy (:y shape-center) :r 5 :fill "yellow"}])]))
(mf/defc single-selection-handlers (mf/defc single-selection-handlers
[{:keys [shape zoom color] :as props}] [{:keys [shape zoom color show-distances] :as props}]
(let [shape-id (:id shape) (let [shape-id (:id shape)
shape (geom/transform-shape 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) shape' (if (debug? :simple-selection) (geom/selection-rect [shape]) shape)
on-resize (fn [current-position initial-position event] on-resize (fn [current-position initial-position event]
(dom/stop-propagation event) (dom/stop-propagation event)
@ -303,10 +328,17 @@
:zoom zoom :zoom zoom
:color color :color color
:on-rotate on-rotate :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 (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 (let [;; We need remove posible nil values because on shape
;; deletion many shape will reamin selected and deleted ;; deletion many shape will reamin selected and deleted
;; in the same time for small instant of time ;; in the same time for small instant of time
@ -326,7 +358,8 @@
[:& multiple-selection-handlers {:shapes shapes [:& multiple-selection-handlers {:shapes shapes
:selected selected :selected selected
:zoom zoom :zoom zoom
:color color}] :color color
:show-distances show-distances}]
(and (= type :text) (and (= type :text)
(= edition (:id shape))) (= edition (:id shape)))
@ -343,4 +376,5 @@
:else :else
[:& single-selection-handlers {:shape shape [:& single-selection-handlers {:shape shape
:zoom zoom :zoom zoom
:color color}]))) :color color
:show-distances show-distances}])))

View File

@ -18,7 +18,6 @@
[app.main.ui.cursors :as cur] [app.main.ui.cursors :as cur]
[app.main.ui.shapes.rect :as rect] [app.main.ui.shapes.rect :as rect]
[app.main.ui.shapes.circle :as circle] [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.image :as image]
[app.main.data.workspace.selection :as dws] [app.main.data.workspace.selection :as dws]
[app.main.store :as st] [app.main.store :as st]
@ -40,7 +39,6 @@
(declare frame-wrapper) (declare frame-wrapper)
(def circle-wrapper (common/generic-wrapper-factory circle/circle-shape)) (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 image-wrapper (common/generic-wrapper-factory image/image-shape))
(def rect-wrapper (common/generic-wrapper-factory rect/rect-shape)) (def rect-wrapper (common/generic-wrapper-factory rect/rect-shape))
@ -113,7 +111,6 @@
:path [:> path/path-wrapper opts] :path [:> path/path-wrapper opts]
:text [:> text/text-wrapper opts] :text [:> text/text-wrapper opts]
:group [:> group-wrapper opts] :group [:> group-wrapper opts]
:icon [:> icon-wrapper opts]
:rect [:> rect-wrapper opts] :rect [:> rect-wrapper opts]
:image [:> image-wrapper opts] :image [:> image-wrapper opts]
:circle [:> circle-wrapper opts] :circle [:> circle-wrapper opts]

Some files were not shown because too many files have changed in this diff Show More