diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 4085b6d50..d1e91f24b 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -17,6 +17,7 @@ {:exclude-files ["data_readers.clj" "app/util/perf.cljs" + "app/common/logging.cljc" "app/common/exceptions.cljc"]} :linters diff --git a/.gitignore b/.gitignore index d1cc5a7af..d11830e81 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,8 @@ node_modules /deploy /web /_dump -/vendor/svgclean/bundle*.js \ No newline at end of file +/vendor/svgclean/bundle*.js + +.calva +.clj-kondo +.lsp diff --git a/CHANGES.md b/CHANGES.md index 463d3d2ac..28e18de98 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,31 +1,91 @@ # CHANGELOG + ## :rocket: Next ### :boom: Breaking changes ### :sparkles: New features ### :bug: Bugs fixed ### :arrow_up: Deps updates -### :boom: Breaking changes ### :heart: Community contributions by (Thank you!) + +## 1.9.0-alpha + +### :boom: Breaking changes + +- Some stroke-caps can change behaviour. +- Text display bug fix could potentialy make some texts jump a line. + +### :sparkles: New features + +- Add boolean shapes: intersections, unions, difference and exclusions[Taiga #748](https://tree.taiga.io/project/penpot/us/748). +- Add advanced prototyping [Taiga #244](https://tree.taiga.io/project/penpot/us/244). +- Add multiple flows [Taiga #2091](https://tree.taiga.io/project/penpot/us/2091). +- Change order of the teams menu so it's in the joined time order. + +### :bug: Bugs fixed + +- Enhance duplicating prototype connections behaviour [Taiga #2093](https://tree.taiga.io/project/penpot/us/2093). +- Ignore constraints in horizontal or vertical flip [Taiga #2038](https://tree.taiga.io/project/penpot/issue/2038). +- Fix color and typographies refs lost when duplicated file [Taiga #2165](https://tree.taiga.io/project/penpot/issue/2165). +- Fix problem with overflow dropdown on stroke-cap [#1216](https://github.com/penpot/penpot/issues/1216). +- Fix menu context for single element nested in components [#1186](https://github.com/penpot/penpot/issues/1186). +- Fix error screen when operations over comments fail [#1219](https://github.com/penpot/penpot/issues/1219). +- Fix undo problem when changing typography/color from library [#1230](https://github.com/penpot/penpot/issues/1230). +- Fix problem with text margin while rendering [#1231](https://github.com/penpot/penpot/issues/1231). +- Fix problem with masked texts on exporting [Taiga #2116](https://tree.taiga.io/project/penpot/issue/2116). +- Fix text editor enter behaviour with centered texts [Taiga #2126](https://tree.taiga.io/project/penpot/issue/2126). +- Fix residual stroke on imported svg [Taiga #2125](https://tree.taiga.io/project/penpot/issue/2125). +- Add links for terms of service and privacy policy in register checkbox [Taiga #2020](https://tree.taiga.io/project/penpot/issue/2020). +- Allow three character hex and web colors in color picker hex input [#1184](https://github.com/penpot/penpot/issues/1184). +- Allow lowercase search for fonts [#1180](https://github.com/penpot/penpot/issues/1180). +- Fix group renaming problem [Taiga #1969](https://tree.taiga.io/project/penpot/issue/1969). +- Fix export group with shadows on children [Taiga #2036](https://tree.taiga.io/project/penpot/issue/2036). +- Fix zoom context menu in viewer [Taiga #2041](https://tree.taiga.io/project/penpot/issue/2041). +- Fix stroke caps adjustments in relation with stroke size [Taiga #2123](https://tree.taiga.io/project/penpot/issue/2123). +- Fix problem duplicating paths [Taiga #2147](https://tree.taiga.io/project/penpot/issue/2147). +- Fix problem inheriting attributes from SVG root when importing [Taiga #2124](https://tree.taiga.io/project/penpot/issue/2124). +- Fix problem with lines and inside/outside stroke [Taiga #2146](https://tree.taiga.io/project/penpot/issue/2146). +- Add stroke width in selection calculation [Taiga #2146](https://tree.taiga.io/project/penpot/issue/2146). +- Fix shift+wheel to horizontal scrolling in MacOS [#1217](https://github.com/penpot/penpot/issues/1217). +- Fix path stroke is not working properly with high thickness [Taiga #2154](https://tree.taiga.io/project/penpot/issue/2154). +- Fix bug with transformation operations [Taiga #2155](https://tree.taiga.io/project/penpot/issue/2155). +- Fix bug in firefox when a text box is inside a mask [Taiga #2152](https://tree.taiga.io/project/penpot/issue/2152). +- Fix problem with stroke inside/outside [Taiga #2186](https://tree.taiga.io/project/penpot/issue/2186) +- Fix masks export area [Taiga #2189](https://tree.taiga.io/project/penpot/issue/2189) +- Fix paste in place in arboards [Taiga #2188](https://tree.taiga.io/project/penpot/issue/2188) +- Fix font size input stuck on selection change [Taiga #2184](https://tree.taiga.io/project/penpot/issue/2184) +- Fix stroke cut on shapes export [Taiga #2171](https://tree.taiga.io/project/penpot/issue/2171) +- Fix no color when boolean with an SVG [Taiga #2193](https://tree.taiga.io/project/penpot/issue/2193) +- Fix unlink color styles at strokes [Taiga #2206](https://tree.taiga.io/project/penpot/issue/2206). + +### :arrow_up: Deps updates + +### :heart: Community contributions by (Thank you!) + +- To the translation community for the hard work on making penpot + available on so many languages. + + + ## 1.8.4-alpha ### :bug: Bugs fixed -- Fix problem importing components [Taiga #2151](https://tree.taiga.io/project/penpot/issue/2151) +- Fix problem importing components [Taiga #2151](https://tree.taiga.io/project/penpot/issue/2151). ## 1.8.3-alpha ### :sparkles: New features -- Adds progress report to importing process +- Adds progress report to importing process. ## 1.8.2-alpha ### :bug: Bugs fixed -- Fix problem with masking images in viewer [#1238](https://github.com/penpot/penpot/issues/1238) +- Fix problem with masking images in viewer [#1238](https://github.com/penpot/penpot/issues/1238). ## 1.8.1-alpha diff --git a/backend/deps.edn b/backend/deps.edn index 0bee0c0c5..45f6cbb6d 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -12,7 +12,7 @@ org.zeromq/jeromq {:mvn/version "0.5.2"} com.taoensso/nippy {:mvn/version "3.1.1"} - com.github.luben/zstd-jni {:mvn/version "1.4.9-5"} + com.github.luben/zstd-jni {:mvn/version "1.5.0-4"} ;; NOTE: don't upgrade to latest version, breaking change is ;; introduced on 0.10.0 that suffixes counters with _total if they @@ -24,14 +24,14 @@ org.eclipse.jetty/jetty-servlet]} io.prometheus/simpleclient_httpserver {:mvn/version "0.9.0"} - io.lettuce/lettuce-core {:mvn/version "6.1.2.RELEASE"} - java-http-clj/java-http-clj {:mvn/version "0.4.2"} + io.lettuce/lettuce-core {:mvn/version "6.1.5.RELEASE"} + java-http-clj/java-http-clj {:mvn/version "0.4.3"} - info.sunng/ring-jetty9-adapter {:mvn/version "0.15.1"} - com.github.seancorfield/next.jdbc {:mvn/version "1.2.659"} - metosin/reitit-ring {:mvn/version "0.5.13"} - org.postgresql/postgresql {:mvn/version "42.2.20"} - com.zaxxer/HikariCP {:mvn/version "4.0.3"} + info.sunng/ring-jetty9-adapter {:mvn/version "0.15.2"} + com.github.seancorfield/next.jdbc {:mvn/version "1.2.709"} + metosin/reitit-ring {:mvn/version "0.5.15"} + org.postgresql/postgresql {:mvn/version "42.2.23"} + com.zaxxer/HikariCP {:mvn/version "5.0.0"} funcool/datoteka {:mvn/version "2.0.0"} @@ -39,14 +39,20 @@ buddy/buddy-hashers {:mvn/version "1.8.1"} buddy/buddy-sign {:mvn/version "3.4.1"} - org.jsoup/jsoup {:mvn/version "1.13.1"} + org.jsoup/jsoup {:mvn/version "1.14.2"} org.im4java/im4java {:mvn/version "1.4.0"} - org.lz4/lz4-java {:mvn/version "1.7.1"} + org.lz4/lz4-java {:mvn/version "1.8.0"} org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"} integrant/integrant {:mvn/version "0.8.0"} - software.amazon.awssdk/s3 {:mvn/version "2.16.62"}} + io.sentry/sentry {:mvn/version "5.1.2"} + + ;; Pretty Print specs + fipp/fipp {:mvn/version "0.6.24"} + pretty-spec/pretty-spec {:mvn/version "0.1.4"} + + software.amazon.awssdk/s3 {:mvn/version "2.17.40"}} :paths ["src" "resources"] :aliases @@ -55,7 +61,8 @@ {com.bhauman/rebel-readline {:mvn/version "RELEASE"} org.clojure/tools.namespace {:mvn/version "RELEASE"} org.clojure/test.check {:mvn/version "RELEASE"} - com.clojure-goes-fast/clj-async-profiler {:mvn/version "0.5.0"} + org.clojure/data.csv {:mvn/version "1.0.0"} + com.clojure-goes-fast/clj-async-profiler {:mvn/version "0.5.1"} criterium/criterium {:mvn/version "RELEASE"} mockery/mockery {:mvn/version "RELEASE"}} @@ -66,13 +73,13 @@ :args {}} :kaocha - {:extra-deps {lambdaisland/kaocha {:mvn/version "1.0.829"}} + {:extra-deps {lambdaisland/kaocha {:mvn/version "1.0.887"}} :main-opts ["-m" "kaocha.runner"]} :test {:extra-deps {io.github.cognitect-labs/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git" - :sha "705ad25bbf0228b1c38d0244a36001c2987d7337"}} + :git/sha "dd6da11611eeb87f08780a30ac8ea6012d4c05ce"}} :exec-fn cognitect.test-runner.api/test} :outdated diff --git a/backend/resources/api-doc.css b/backend/resources/api-doc.css new file mode 100644 index 000000000..b9b14a889 --- /dev/null +++ b/backend/resources/api-doc.css @@ -0,0 +1,101 @@ +* { + font-family: "JetBrains Mono", monospace; + font-size: 12px; +} + +pre { + margin: 0px; +} + +body { + margin: 0px; + padding: 0px; + padding-top: 20px; + padding-bottom: 20px; + display: flex; + justify-content: center; +} + +main { + display: flex; + flex-direction: column; + align-items: center; + min-width: 900px; + width: 900px; +} + +header { + border-bottom: 1px solid #c0c0c0; + display: flex; + justify-content: center; + width: 100%; +} + +.rpc-doc-content { + margin-top: 20px; + width: 100%; + display: flex; + flex-direction: column; + /* border: 1px solid red; */ + padding: 5px; +} + +.rpc-doc-content > h2:not(:first-child) { + margin-top: 30px; +} + + +.rpc-items { + list-style: none; + padding: 0px; + margin: 0px; +} + +.rpc-item { + /* border: 1px solid red; */ + cursor: pointer; + display: flex; + flex-direction: column; +} + +.rpc-item:not(:last-child) { + margin-bottom: 3px; +} + +.rpc-row-info { + cursor: pointer; + display: flex; + background-color: #eeeeee; + padding: 5px 10px; +} + +.rpc-row-info > *:not(:last-child) { + margin-right: 10px; +} + +.rpc-row-info > * { + /* border: 1px solid green; */ +} + +.rpc-row-info > .type { + font-weight: bold; + width: 70px; +} + +.rpc-row-info > .name { + width: 280px; + /* font-weight: bold; */ +} + +.rpc-row-info > .tags > .tag > span:first-child { + font-weight: bold; +} + +.hidden { + display: none; +} + +.rpc-row-detail { + padding: 5px 10px; + padding-bottom: 20px; +} diff --git a/backend/resources/api-doc.js b/backend/resources/api-doc.js new file mode 100644 index 000000000..90a0e4f92 --- /dev/null +++ b/backend/resources/api-doc.js @@ -0,0 +1,27 @@ +(function() { + document.addEventListener("DOMContentLoaded", function(event) { + const rows = document.querySelectorAll(".rpc-row-info"); + + const onRowClick = (event) => { + const target = event.currentTarget; + for (let node of rows) { + if (node !== target) { + node.nextElementSibling.classList.add("hidden"); + } else { + const sibling = target.nextElementSibling; + + if (sibling.classList.contains("hidden")) { + sibling.classList.remove("hidden"); + } else { + sibling.classList.add("hidden"); + } + } + } + }; + + for (let node of rows) { + node.addEventListener("click", onRowClick); + } + + }); +})(); diff --git a/backend/resources/api-doc.tmpl b/backend/resources/api-doc.tmpl new file mode 100644 index 000000000..f319a4692 --- /dev/null +++ b/backend/resources/api-doc.tmpl @@ -0,0 +1,80 @@ + + + + + + + Builtin API Documentation - Penpot + + + + + +
+
+

Penpot API Documentation

+
+
+ +

RPC QUERY METHODS:

+ + +

RPC MUTATION METHODS:

+ +
+
+ + + diff --git a/backend/resources/error-report.tmpl b/backend/resources/error-report.tmpl index a04f0df6b..452036997 100644 --- a/backend/resources/error-report.tmpl +++ b/backend/resources/error-report.tmpl @@ -2,6 +2,7 @@ + penpot - error report {{id}} diff --git a/backend/scripts/repl b/backend/scripts/repl index cee9d8c5b..f2321e346 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -1,6 +1,6 @@ #!/usr/bin/env bash -export PENPOT_ASSERTS_ENABLED=true +export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS" export OPTIONS="-A:jmx-remote:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-Dlog4j2.configurationFile=log4j2-devenv.xml -J-Djdk.attach.allowAttachSelf -J-XX:+UseZGC -J-XX:ConcGCThreads=1 -J-XX:-OmitStackTraceInFastThrow -J-Xms50m -J-Xmx512m"; # export OPTIONS="$OPTIONS -J-XX:+UnlockDiagnosticVMOptions"; diff --git a/backend/scripts/start-dev b/backend/scripts/start-dev index a25db5350..88cc5c0c0 100755 --- a/backend/scripts/start-dev +++ b/backend/scripts/start-dev @@ -10,6 +10,23 @@ if [ ! -e ~/.fixtures-loaded ]; then touch ~/.fixtures-loaded fi -clojure -A:dev -M -m app.main +if [ "$1" = "--watch" ]; then + echo "Start Watch..." + + clojure -A:dev -M -m app.main & + PID=$! + + npx nodemon \ + --watch src \ + --watch ../common \ + --ext "clj" \ + --signal SIGKILL \ + --exec 'echo "(user/restart)" | nc -N localhost 6062' + + kill -9 $PID +else + clojure -A:dev -M -m app.main +fi + diff --git a/backend/src/app/cli/fixtures.clj b/backend/src/app/cli/fixtures.clj index cd09769cb..4128c9954 100644 --- a/backend/src/app/cli/fixtures.clj +++ b/backend/src/app/cli/fixtures.clj @@ -7,13 +7,13 @@ (ns app.cli.fixtures "A initial fixtures." (:require + [app.common.logging :as l] [app.common.pages :as cp] [app.common.uuid :as uuid] [app.db :as db] [app.main :as main] [app.rpc.mutations.profile :as profile] [app.util.blob :as blob] - [app.util.logging :as l] [buddy.hashers :as hashers] [integrant.core :as ig])) diff --git a/backend/src/app/cli/manage.clj b/backend/src/app/cli/manage.clj index 29975d0a9..d20758196 100644 --- a/backend/src/app/cli/manage.clj +++ b/backend/src/app/cli/manage.clj @@ -7,11 +7,11 @@ (ns app.cli.manage "A manage cli api." (:require + [app.common.logging :as l] [app.db :as db] [app.main :as main] [app.rpc.mutations.profile :as profile] [app.rpc.queries.profile :refer [retrieve-profile-data-by-email]] - [app.util.logging :as l] [clojure.string :as str] [clojure.tools.cli :refer [parse-opts]] [integrant.core :as ig]) diff --git a/backend/src/app/cli/migrate_media.clj b/backend/src/app/cli/migrate_media.clj index 0d627afdc..b940b1a33 100644 --- a/backend/src/app/cli/migrate_media.clj +++ b/backend/src/app/cli/migrate_media.clj @@ -6,12 +6,12 @@ (ns app.cli.migrate-media (:require + [app.common.logging :as l] [app.common.media :as cm] [app.config :as cf] [app.db :as db] [app.main :as main] [app.storage :as sto] - [app.util.logging :as l] [cuerdas.core :as str] [datoteka.core :as fs] [integrant.core :as ig])) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 27df0e778..2c1d77aba 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -10,6 +10,7 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.flags :as flags] [app.common.spec :as us] [app.common.version :as v] [app.util.time :as dt] @@ -50,8 +51,6 @@ :default-blob-version 3 :loggers-zmq-uri "tcp://localhost:45556" - :asserts-enabled false - :public-uri "http://localhost:3449" :redis-uri "redis://redis/0" @@ -61,15 +60,11 @@ :assets-storage-backend :assets-fs :storage-assets-fs-directory "assets" - :feedback-destination "info@example.com" - :feedback-enabled false - :assets-path "/internal/assets/" :rlimits-password 10 :rlimits-image 2 - :smtp-enabled false :smtp-default-reply-to "Penpot " :smtp-default-from "Penpot " @@ -79,10 +74,6 @@ :profile-bounce-max-age (dt/duration {:days 7}) :profile-bounce-threshold 10 - :allow-demo-users true - :registration-enabled true - - :telemetry-enabled false :telemetry-uri "https://telemetry.penpot.app/" :ldap-user-query "(|(uid=:username)(mail=:username))" @@ -92,27 +83,29 @@ :ldap-attrs-photo "jpegPhoto" ;; a server prop key where initial project is stored. - :initial-project-skey "initial-project" - }) + :initial-project-skey "initial-project"}) -(s/def ::audit-enabled ::us/boolean) -(s/def ::audit-archive-enabled ::us/boolean) -(s/def ::audit-archive-uri ::us/string) -(s/def ::audit-archive-gc-enabled ::us/boolean) -(s/def ::audit-archive-gc-max-age ::dt/duration) +(s/def ::flags ::us/words) + +;; DEPRECATED PROPERTIES: should be removed in 1.10 +(s/def ::registration-enabled ::us/boolean) +(s/def ::smtp-enabled ::us/boolean) +(s/def ::telemetry-enabled ::us/boolean) +(s/def ::asserts-enabled ::us/boolean) +;; END DEPRECATED + +(s/def ::audit-log-archive-uri ::us/string) +(s/def ::audit-log-gc-max-age ::dt/duration) (s/def ::secret-key ::us/string) (s/def ::allow-demo-users ::us/boolean) -(s/def ::asserts-enabled ::us/boolean) (s/def ::assets-path ::us/string) (s/def ::database-password (s/nilable ::us/string)) (s/def ::database-uri ::us/string) (s/def ::database-username (s/nilable ::us/string)) (s/def ::default-blob-version ::us/integer) (s/def ::error-report-webhook ::us/string) -(s/def ::feedback-destination ::us/string) -(s/def ::feedback-enabled ::us/boolean) -(s/def ::feedback-token ::us/string) +(s/def ::user-feedback-destination ::us/string) (s/def ::github-client-id ::us/string) (s/def ::github-client-secret ::us/string) (s/def ::gitlab-base-uri ::us/string) @@ -158,12 +151,10 @@ (s/def ::public-uri ::us/string) (s/def ::redis-uri ::us/string) (s/def ::registration-domain-whitelist ::us/set-of-str) -(s/def ::registration-enabled ::us/boolean) (s/def ::rlimits-image ::us/integer) (s/def ::rlimits-password ::us/integer) (s/def ::smtp-default-from ::us/string) (s/def ::smtp-default-reply-to ::us/string) -(s/def ::smtp-enabled ::us/boolean) (s/def ::smtp-host ::us/string) (s/def ::smtp-password (s/nilable ::us/string)) (s/def ::smtp-port ::us/integer) @@ -180,28 +171,27 @@ (s/def ::storage-fdata-s3-bucket ::us/string) (s/def ::storage-fdata-s3-region ::us/keyword) (s/def ::storage-fdata-s3-prefix ::us/string) -(s/def ::telemetry-enabled ::us/boolean) (s/def ::telemetry-uri ::us/string) (s/def ::telemetry-with-taiga ::us/boolean) (s/def ::tenant ::us/string) +(s/def ::sentry-trace-sample-rate ::us/number) +(s/def ::sentry-attach-stack-trace ::us/boolean) +(s/def ::sentry-debug ::us/boolean) +(s/def ::sentry-dsn ::us/string) + (s/def ::config (s/keys :opt-un [::secret-key + ::flags ::allow-demo-users - ::audit-enabled - ::audit-archive-enabled - ::audit-archive-uri - ::audit-archive-gc-enabled - ::audit-archive-gc-max-age - ::asserts-enabled + ::audit-log-archive-uri + ::audit-log-gc-max-age ::database-password ::database-uri ::database-username ::default-blob-version ::error-report-webhook - ::feedback-destination - ::feedback-enabled - ::feedback-token + ::user-feedback-destination ::github-client-id ::github-client-secret ::gitlab-base-uri @@ -249,6 +239,10 @@ ::registration-enabled ::rlimits-image ::rlimits-password + ::sentry-dsn + ::sentry-debug + ::sentry-attach-stack-trace + ::sentry-trace-sample-rate ::smtp-default-from ::smtp-default-reply-to ::smtp-enabled @@ -258,26 +252,27 @@ ::smtp-ssl ::smtp-tls ::smtp-username - ::srepl-host ::srepl-port - ::assets-storage-backend ::storage-assets-fs-directory ::storage-assets-s3-bucket ::storage-assets-s3-region - ::fdata-storage-backend ::storage-fdata-s3-bucket ::storage-fdata-s3-region ::storage-fdata-s3-prefix - ::telemetry-enabled ::telemetry-uri ::telemetry-referer ::telemetry-with-taiga ::tenant])) +(defn- parse-flags + [config] + (-> (:flags config) + (flags/parse flags/default))) + (defn read-env [prefix] (let [prefix (str prefix "-") @@ -304,11 +299,14 @@ (println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"))) (throw e)))) -(def version (v/parse (or (some-> (io/resource "version.txt") - (slurp) - (str/trim)) - "%version%"))) -(def config (atom (read-config))) +(def version + (v/parse (or (some-> (io/resource "version.txt") + (slurp) + (str/trim)) + "%version%"))) + +(def ^:dynamic config (read-config)) +(def ^:dynamic flags (parse-flags config)) (def deletion-delay (dt/duration {:days 7})) @@ -316,9 +314,9 @@ (defn get "A configuration getter. Helps code be more testable." ([key] - (c/get @config key)) + (c/get config key)) ([key default] - (c/get @config key default))) + (c/get config key default))) ;; Set value for all new threads bindings. -(alter-var-root #'*assert* (constantly (get :asserts-enabled))) +(alter-var-root #'*assert* (constantly (contains? flags :backend-asserts))) diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 6f1b940af..8925c12d0 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -9,13 +9,13 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.geom.point :as gpt] + [app.common.logging :as l] [app.common.spec :as us] [app.common.transit :as t] [app.common.uuid :as uuid] [app.db.sql :as sql] [app.metrics :as mtx] [app.util.json :as json] - [app.util.logging :as l] [app.util.migrations :as mg] [app.util.time :as dt] [clojure.java.io :as io] @@ -46,28 +46,26 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (declare instrument-jdbc!) +(declare apply-migrations!) (s/def ::name keyword?) (s/def ::uri ::us/not-empty-string) (s/def ::min-pool-size ::us/integer) (s/def ::max-pool-size ::us/integer) (s/def ::migrations map?) +(s/def ::read-only ::us/boolean) (defmethod ig/pre-init-spec ::pool [_] - (s/keys :req-un [::uri ::name ::min-pool-size ::max-pool-size ::migrations ::mtx/metrics])) + (s/keys :req-un [::uri ::name ::min-pool-size ::max-pool-size] + :opt-un [::migrations ::mtx/metrics ::read-only])) (defmethod ig/init-key ::pool - [_ {:keys [migrations metrics] :as cfg}] - (l/info :action "initialize connection pool" - :name (d/name (:name cfg)) - :uri (:uri cfg)) - (instrument-jdbc! (:registry metrics)) + [_ {:keys [migrations metrics name] :as cfg}] + (l/info :action "initialize connection pool" :name (d/name name) :uri (:uri cfg)) + (some-> metrics :registry instrument-jdbc!) + (let [pool (create-pool cfg)] - (when (seq migrations) - (with-open [conn ^AutoCloseable (open pool)] - (mg/setup! conn) - (doseq [[name steps] migrations] - (mg/migrate! conn {:name (d/name name) :steps steps})))) + (some->> (seq migrations) (apply-migrations! pool)) pool)) (defmethod ig/halt-key! ::pool @@ -84,37 +82,50 @@ :name "database_query_total" :help "An absolute counter of database queries."})) +(defn- apply-migrations! + [pool migrations] + (with-open [conn ^AutoCloseable (open pool)] + (mg/setup! conn) + (doseq [[name steps] migrations] + (mg/migrate! conn {:name (d/name name) :steps steps})))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; API & Impl ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def initsql - (str "SET statement_timeout = 120000;\n" - "SET idle_in_transaction_session_timeout = 120000;")) + (str "SET statement_timeout = 200000;\n" + "SET idle_in_transaction_session_timeout = 200000;")) (defn- create-datasource-config - [{:keys [metrics] :as cfg}] + [{:keys [metrics read-only] :or {read-only false} :as cfg}] (let [dburi (:uri cfg) username (:username cfg) password (:password cfg) - config (HikariConfig.) - mtf (PrometheusMetricsTrackerFactory. (:registry metrics))] + config (HikariConfig.)] (doto config (.setJdbcUrl (str "jdbc:" dburi)) (.setPoolName (d/name (:name cfg))) (.setAutoCommit true) - (.setReadOnly false) - (.setConnectionTimeout 8000) ;; 8seg - (.setValidationTimeout 8000) ;; 8seg - (.setIdleTimeout 120000) ;; 2min - (.setMaxLifetime 1800000) ;; 30min + (.setReadOnly read-only) + (.setConnectionTimeout 10000) ;; 10seg + (.setValidationTimeout 10000) ;; 10seg + (.setIdleTimeout 120000) ;; 2min + (.setMaxLifetime 1800000) ;; 30min (.setMinimumIdle (:min-pool-size cfg 0)) (.setMaximumPoolSize (:max-pool-size cfg 30)) - (.setMetricsTrackerFactory mtf) (.setConnectionInitSql initsql) (.setInitializationFailTimeout -1)) + + ;; When metrics namespace is provided + (when metrics + (->> (:registry metrics) + (PrometheusMetricsTrackerFactory.) + (.setMetricsTrackerFactory config))) + (when username (.setUsername config username)) (when password (.setPassword config password)) + config)) (defn pool? @@ -127,7 +138,7 @@ [pool] (.isClosed ^HikariDataSource pool)) -(defn- create-pool +(defn create-pool [cfg] (let [dsc (create-datasource-config cfg)] (jdbc-dt/read-as-instant) diff --git a/backend/src/app/emails.clj b/backend/src/app/emails.clj index ac55b688b..43b648274 100644 --- a/backend/src/app/emails.clj +++ b/backend/src/app/emails.clj @@ -7,12 +7,12 @@ (ns app.emails "Main api for send emails." (:require + [app.common.logging :as l] [app.common.spec :as us] - [app.config :as cfg] + [app.config :as cf] [app.db :as db] [app.db.sql :as sql] [app.util.emails :as emails] - [app.util.logging :as l] [app.worker :as wrk] [clojure.spec.alpha :as s] [integrant.core :as ig])) @@ -54,10 +54,10 @@ (defn allow-send-emails? [conn profile] (when-not (:is-muted profile false) - (let [complaint-threshold (cfg/get :profile-complaint-threshold) - complaint-max-age (cfg/get :profile-complaint-max-age) - bounce-threshold (cfg/get :profile-bounce-threshold) - bounce-max-age (cfg/get :profile-bounce-max-age) + (let [complaint-threshold (cf/get :profile-complaint-threshold) + complaint-max-age (cf/get :profile-complaint-max-age) + bounce-threshold (cf/get :profile-bounce-threshold) + bounce-max-age (cf/get :profile-bounce-max-age) {:keys [complaints bounces] :as result} (db/exec-one! conn [sql:profile-complaint-report @@ -140,19 +140,17 @@ (declare send-console!) -(s/def ::username ::cfg/smtp-username) -(s/def ::password ::cfg/smtp-password) -(s/def ::tls ::cfg/smtp-tls) -(s/def ::ssl ::cfg/smtp-ssl) -(s/def ::host ::cfg/smtp-host) -(s/def ::port ::cfg/smtp-port) -(s/def ::default-reply-to ::cfg/smtp-default-reply-to) -(s/def ::default-from ::cfg/smtp-default-from) -(s/def ::enabled ::cfg/smtp-enabled) +(s/def ::username ::cf/smtp-username) +(s/def ::password ::cf/smtp-password) +(s/def ::tls ::cf/smtp-tls) +(s/def ::ssl ::cf/smtp-ssl) +(s/def ::host ::cf/smtp-host) +(s/def ::port ::cf/smtp-port) +(s/def ::default-reply-to ::cf/smtp-default-reply-to) +(s/def ::default-from ::cf/smtp-default-from) (defmethod ig/pre-init-spec ::sendmail-handler [_] - (s/keys :req-un [::enabled] - :opt-un [::username + (s/keys :opt-un [::username ::password ::tls ::ssl @@ -164,9 +162,12 @@ (defmethod ig/init-key ::sendmail-handler [_ cfg] (fn [{:keys [props] :as task}] - (if (:enabled cfg) - (emails/send! cfg props) - (send-console! cfg props)))) + (let [enabled? (or (contains? cf/flags :smtp) + (cf/get :smtp-enabled) + (:enabled task))] + (if enabled? + (emails/send! cfg props) + (send-console! cfg props))))) (defn- send-console! [cfg email] diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index 3681118ad..0dc98852b 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -8,11 +8,12 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.logging :as l] [app.common.spec :as us] + [app.http.doc :as doc] [app.http.errors :as errors] [app.http.middleware :as middleware] [app.metrics :as mtx] - [app.util.logging :as l] [clojure.spec.alpha :as s] [integrant.core :as ig] [reitit.ring :as rr] @@ -141,7 +142,8 @@ ["/webhooks" ["/sns" {:post (:sns-webhook cfg)}]] - ["/api" {:middleware [[middleware/etag] + ["/api" {:middleware [[middleware/cors] + [middleware/etag] [middleware/format-response-body] [middleware/params] [middleware/multipart-params] @@ -150,6 +152,8 @@ [middleware/errors errors/handle] [middleware/cookies]]} + ["/_doc" {:get (doc/handler rpc)}] + ["/feedback" {:middleware [(:middleware session)] :post feedback}] ["/auth/oauth/:provider" {:post (:handler oauth)}] diff --git a/backend/src/app/http/awsns.clj b/backend/src/app/http/awsns.clj index d5c29a979..7117c6466 100644 --- a/backend/src/app/http/awsns.clj +++ b/backend/src/app/http/awsns.clj @@ -8,10 +8,10 @@ "AWS SNS webhook handler for bounces." (:require [app.common.exceptions :as ex] + [app.common.logging :as l] [app.db :as db] [app.db.sql :as sql] [app.util.http :as http] - [app.util.logging :as l] [clojure.spec.alpha :as s] [cuerdas.core :as str] [integrant.core :as ig] diff --git a/backend/src/app/http/doc.clj b/backend/src/app/http/doc.clj new file mode 100644 index 000000000..13a6075cc --- /dev/null +++ b/backend/src/app/http/doc.clj @@ -0,0 +1,53 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.http.doc + "API autogenerated documentation." + (:require + [app.common.data :as d] + [app.config :as cf] + [app.util.services :as sv] + [app.util.template :as tmpl] + [clojure.java.io :as io] + [clojure.spec.alpha :as s] + [pretty-spec.core :as ps])) + +(defn get-spec-str + [k] + (with-out-str + (ps/pprint (s/form k) + {:ns-aliases {"clojure.spec.alpha" "s" + "clojure.core.specs.alpha" "score" + "clojure.core" nil}}))) + +(defn prepare-context + [rpc] + (letfn [(gen-doc [type [name f]] + (let [mdata (meta f)] + ;; (prn name mdata) + {:type (d/name type) + :name (d/name name) + :auth (:auth mdata true) + :docs (::sv/docs mdata) + :spec (get-spec-str (::sv/spec mdata))}))] + {:query-methods + (into [] + (map (partial gen-doc :query)) + (->> rpc :methods :query (sort-by first))) + :mutation-methods + (into [] + (map (partial gen-doc :mutation)) + (->> rpc :methods :mutation (sort-by first)))})) + +(defn handler + [rpc] + (let [context (prepare-context rpc)] + (if (contains? cf/flags :api-doc) + (fn [_] + {:status 200 + :body (-> (io/resource "api-doc.tmpl") + (tmpl/render context))}) + (constantly {:status 404 :body ""})))) diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index 753c218d8..ab8f7e1d2 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -7,31 +7,49 @@ (ns app.http.errors "A errors handling for the http server." (:require + [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.logging :as l] [app.common.uuid :as uuid] - [app.util.logging :as l] - [cuerdas.core :as str] - [expound.alpha :as expound])) + [clojure.pprint] + [cuerdas.core :as str])) -(defn- explain-error - [error] - (with-out-str - (expound/printer (:data error)))) +(defn- parse-client-ip + [{:keys [headers] :as request}] + (or (some-> (get headers "x-forwarded-for") (str/split ",") first) + (get headers "x-real-ip") + (get request :remote-addr))) + +(defn- stringify-data + [data] + (binding [clojure.pprint/*print-right-margin* 200] + (let [result (with-out-str (clojure.pprint/pprint data))] + (str/prune result (* 1024 1024) "[...]")))) (defn get-error-context [request error] - (let [edata (ex-data error)] - (merge - {:id (uuid/next) - :path (:uri request) - :method (:request-method request) - :params (:params request) - :data edata} + (let [data (ex-data error)] + (d/without-nils + (merge + {:id (str (uuid/next)) + :path (str (:uri request)) + :method (name (:request-method request)) + :hint (or (:hint data) (ex-message error)) + :params (stringify-data (:params request)) + :data (stringify-data (dissoc data :explain)) + :ip-addr (parse-client-ip request) + :explain (str/prune (:explain data) (* 1024 1024) "[...]")} + + (when-let [id (:profile-id request)] + {:profile-id id}) + (let [headers (:headers request)] {:user-agent (get headers "user-agent") :frontend-version (get headers "x-frontend-version" "unknown")}) - (when (and (map? edata) (:data edata)) - {:explain (explain-error edata)})))) + + (when (map? data) + {:error-type (:type data) + :error-code (:code data)}))))) (defmulti handle-exception (fn [err & _rest] @@ -43,7 +61,6 @@ [err _] {:status 401 :body (ex-data err)}) - (defmethod handle-exception :restriction [err _] {:status 400 :body (ex-data err)}) @@ -57,13 +74,10 @@ {:status 400 :headers {"content-type" "text/html"} :body (str "
"
-                  (explain-error edata)
+                  (:explain edata)
                   "
\n")} {:status 400 - :body (cond-> edata - (map? (:data edata)) - (-> (assoc :explain (explain-error edata)) - (dissoc :data)))}))) + :body (dissoc edata :data)}))) (defmethod handle-exception :assertion [error request] @@ -77,9 +91,7 @@ {:status 500 :body {:type :server-error :code :assertion - :data (-> edata - (assoc :explain (explain-error edata)) - (dissoc :data))}})) + :data (dissoc edata :data)}})) (defmethod handle-exception :not-found [err _] diff --git a/backend/src/app/http/feedback.clj b/backend/src/app/http/feedback.clj index dd78cfd57..e82a93a4e 100644 --- a/backend/src/app/http/feedback.clj +++ b/backend/src/app/http/feedback.clj @@ -10,7 +10,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] - [app.config :as cfg] + [app.config :as cf] [app.db :as db] [app.emails :as eml] [app.rpc.queries.profile :as profile] @@ -24,8 +24,8 @@ (defmethod ig/init-key ::handler [_ {:keys [pool] :as scfg}] - (let [ftoken (cfg/get :feedback-token ::no-token) - enabled (cfg/get :feedback-enabled)] + (let [ftoken (cf/get :feedback-token ::no-token) + enabled (contains? cf/flags :user-feedback)] (fn [{:keys [profile-id] :as request}] (let [token (get-in request [:headers "x-feedback-token"]) params (d/merge (:params request) @@ -58,7 +58,7 @@ (defn send-feedback [pool profile params] (let [params (us/conform ::feedback params) - destination (cfg/get :feedback-destination)] + destination (cf/get :feedback-destination)] (eml/send! {::eml/conn pool ::eml/factory eml/feedback :to destination diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index c6ee09f2e..8c8bf1151 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -6,10 +6,11 @@ (ns app.http.middleware (:require + [app.common.logging :as l] [app.common.transit :as t] + [app.config :as cf] [app.metrics :as mtx] [app.util.json :as json] - [app.util.logging :as l] [buddy.core.codecs :as bc] [buddy.core.hash :as bh] [clojure.java.io :as io] @@ -176,3 +177,29 @@ :uri (str (:uri request) (when qstring (str "?" qstring))) :method (name (:request-method request))) (handler request))))) + +(defn- wrap-cors + [handler] + (if-not (contains? cf/flags :cors) + handler + (letfn [(add-cors-headers [response request] + (-> response + (update + :headers + (fn [headers] + (-> headers + (assoc "access-control-allow-origin" (get-in request [:headers "origin"])) + (assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH") + (assoc "access-control-allow-credentials" "true") + (assoc "access-control-expose-headers" "x-requested-with, content-type, cookie") + (assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width"))))))] + (fn [request] + (if (= (:request-method request) :options) + (-> {:status 200 :body ""} + (add-cors-headers request)) + (let [response (handler request)] + (add-cors-headers response request))))))) + +(def cors + {:name ::cors + :compile (constantly wrap-cors)}) diff --git a/backend/src/app/http/oauth.clj b/backend/src/app/http/oauth.clj index 528d90427..64b1c7036 100644 --- a/backend/src/app/http/oauth.clj +++ b/backend/src/app/http/oauth.clj @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.logging :as l] [app.common.spec :as us] [app.common.uri :as u] [app.config :as cf] @@ -15,7 +16,6 @@ [app.loggers.audit :as audit] [app.rpc.queries.profile :as profile] [app.util.http :as http] - [app.util.logging :as l] [app.util.time :as dt] [clojure.data.json :as json] [clojure.set :as set] @@ -62,6 +62,13 @@ :cause e) nil))) +(defn- qualify-props + [provider props] + (reduce-kv (fn [result k v] + (assoc result (keyword (:name provider) (name k)) v)) + {} + props)) + (defn- retrieve-user-info [{:keys [provider] :as cfg} tdata] (try @@ -76,8 +83,8 @@ {:backend (:name provider) :email (:email info) :fullname (:name info) - :props (dissoc info :name :email)}))) - + :props (->> (dissoc info :name :email) + (qualify-props provider))}))) (catch Exception e (l/error :hint "unexpected exception on retrieve-user-info" :cause e) @@ -138,15 +145,14 @@ ;; --- HTTP HANDLERS -(defn extract-props +(defn extract-utm-props + "Extracts additional data from user params." [params] (reduce-kv (fn [params k v] (let [sk (name k)] (cond-> params - (or (str/starts-with? sk "pm_") - (str/starts-with? sk "pm-") - (str/starts-with? sk "utm_")) - (assoc (-> sk str/kebab keyword) v)))) + (str/starts-with? sk "utm_") + (assoc (->> sk str/kebab (keyword "penpot")) v)))) {} params)) @@ -210,7 +216,7 @@ (defn- auth-handler [{:keys [tokens] :as cfg} {:keys [params] :as request}] (let [invitation (:invitation-token params) - props (extract-props params) + props (extract-utm-props params) state (tokens :generate {:iss :oauth :invitation-token invitation diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index acff16136..6b9a566fd 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -8,11 +8,11 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.logging :as l] [app.config :as cfg] [app.db :as db] [app.metrics :as mtx] [app.util.async :as aa] - [app.util.logging :as l] [app.util.time :as dt] [app.worker :as wrk] [clojure.core.async :as a] @@ -53,7 +53,12 @@ (defn- add-cookies [response {:keys [id] :as session}] - (assoc response :cookies {cookie-name {:path "/" :http-only true :value id}})) + (let [cors? (contains? cfg/flags :cors)] + (assoc response :cookies {cookie-name {:path "/" + :http-only true + :value id + :same-site (if cors? :none :strict) + :secure true}}))) (defn- clear-cookies [response] diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 5f19403de..e312ebba2 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.logging :as l] [app.common.spec :as us] [app.common.transit :as t] [app.common.uuid :as uuid] @@ -16,7 +17,6 @@ [app.db :as db] [app.util.async :as aa] [app.util.http :as http] - [app.util.logging :as l] [app.util.time :as dt] [app.worker :as wrk] [clojure.core.async :as a] @@ -36,6 +36,7 @@ [profile] (-> profile (select-keys [:is-active :is-muted :auth-backend :email :default-team-id :default-project-id :fullname :lang]) + (merge (:props profile)) (d/without-nils))) (defn clean-props @@ -88,9 +89,9 @@ (s/def ::events (s/every ::event)) (defmethod ig/init-key ::http-handler - [_ {:keys [executor enabled] :as cfg}] - (fn [{:keys [params _headers _cookies profile-id] :as request}] - (when enabled + [_ {:keys [executor] :as cfg}] + (fn [{:keys [params profile-id] :as request}] + (when (contains? cf/flags :audit-log) (let [events (->> (:events params) (remove #(not= profile-id (:profile-id %))) (us/conform ::events)) @@ -137,10 +138,9 @@ ;; an external storage and data cleared. (declare persist-events) -(s/def ::enabled ::us/boolean) (defmethod ig/pre-init-spec ::collector [_] - (s/keys :req-un [::db/pool ::wrk/executor ::enabled])) + (s/keys :req-un [::db/pool ::wrk/executor])) (def event-xform (comp @@ -148,9 +148,9 @@ (map clean-props))) (defmethod ig/init-key ::collector - [_ {:keys [enabled] :as cfg}] - (when enabled - (l/info :msg "intializing audit collector") + [_ cfg] + (when (contains? cf/flags :audit-log) + (l/info :msg "intializing audit log collector") (let [input (a/chan 512 event-xform) buffer (aa/batch input {:max-batch-size 100 :max-batch-age (* 10 1000) ; 10s @@ -204,15 +204,16 @@ (s/def ::tokens fn?) (defmethod ig/pre-init-spec ::archive-task [_] - (s/keys :req-un [::db/pool ::tokens ::enabled] + (s/keys :req-un [::db/pool ::tokens] :opt-un [::uri])) (defmethod ig/init-key ::archive-task - [_ {:keys [uri enabled] :as cfg}] + [_ {:keys [uri] :as cfg}] (fn [props] ;; NOTE: this let allows overwrite default configured values from ;; the repl, when manually invoking the task. - (let [enabled (or enabled (:enabled props false)) + (let [enabled (or (contains? cf/flags :audit-log-archive) + (:enabled props false)) uri (or uri (:uri props)) cfg (assoc cfg :uri uri)] (when (and enabled (not uri)) @@ -271,11 +272,12 @@ :headers headers :body body} resp (http/send! params)] - (when (not= (:status resp) 204) - (ex/raise :type :internal - :code :unable-to-send-events - :hint "unable to send events" - :context resp)))) + (if (= (:status resp) 204) + true + (do + (l/warn :hint "unable to archive events" + :resp-status (:status resp)) + false)))) (mark-as-archived [conn rows] (db/exec-one! conn ["update audit_log set archived_at=now() where id = ANY(?)" @@ -290,26 +292,14 @@ events (into [] xform rows)] (when-not (empty? events) (l/debug :action "archive-events" :uri uri :events (count events)) - (send events) - (mark-as-archived conn rows) - :continue))))) + (when (send events) + (mark-as-archived conn rows) + :continue)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; GC Task ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare clean-archived) - -(s/def ::max-age ::cf/audit-archive-gc-max-age) - -(defmethod ig/pre-init-spec ::archive-gc-task [_] - (s/keys :req-un [::db/pool ::enabled ::max-age])) - -(defmethod ig/init-key ::archive-gc-task - [_ cfg] - (fn [_] - (clean-archived cfg))) - (def sql:clean-archived "delete from audit_log where archived_at is not null @@ -322,3 +312,13 @@ result (:next.jdbc/update-count result)] (l/debug :action "clean archived audit log" :removed result) result)) + +(s/def ::max-age ::cf/audit-log-gc-max-age) + +(defmethod ig/pre-init-spec ::gc-task [_] + (s/keys :req-un [::db/pool ::max-age])) + +(defmethod ig/init-key ::gc-task + [_ cfg] + (fn [_] + (clean-archived cfg))) diff --git a/backend/src/app/loggers/database.clj b/backend/src/app/loggers/database.clj new file mode 100644 index 000000000..2685661d8 --- /dev/null +++ b/backend/src/app/loggers/database.clj @@ -0,0 +1,126 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.loggers.database + "A specific logger impl that persists errors on the database." + (:require + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.util.async :as aa] + [app.util.template :as tmpl] + [app.worker :as wrk] + [clojure.core.async :as a] + [clojure.java.io :as io] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [integrant.core :as ig])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Error Listener +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(declare handle-event) + +(defonce enabled (atom true)) + +(defn- persist-on-database! + [{:keys [pool] :as cfg} {:keys [id] :as event}] + (db/with-atomic [conn pool] + (db/insert! conn :server-error-report + {:id id :content (db/tjson event)}))) + +(defn- parse-context + [event] + (reduce-kv + (fn [acc k v] + (cond + (= k :id) (assoc acc k (uuid/uuid v)) + (= k :profile-id) (assoc acc k (uuid/uuid v)) + (str/blank? v) acc + :else (assoc acc k v))) + {} + (:context event))) + +(defn parse-event + [event] + (-> (parse-context event) + (merge (dissoc event :context)) + (assoc :tenant (cf/get :tenant)) + (assoc :host (cf/get :host)) + (assoc :public-uri (cf/get :public-uri)) + (assoc :version (:full cf/version)))) + +(defn handle-event + [{:keys [executor] :as cfg} event] + (aa/with-thread executor + (try + (let [event (parse-event event)] + (persist-on-database! cfg event)) + (catch Exception e + (l/warn :hint "unexpected exception on database error logger" + :cause e))))) + +(defmethod ig/pre-init-spec ::reporter [_] + (s/keys :req-un [::wrk/executor ::db/pool ::receiver])) + +(defmethod ig/init-key ::reporter + [_ {:keys [receiver] :as cfg}] + (l/info :msg "initializing database error persistence") + (let [output (a/chan (a/sliding-buffer 128) + (filter #(= (:level %) "error")))] + (receiver :sub output) + (a/go-loop [] + (let [msg (a/ (io/resource "error-report.tmpl") + (tmpl/render content)))] + + + (fn [request] + (let [result (some-> (parse-id request) + (retrieve-report) + (render-template))] + (if result + {:status 200 + :headers {"content-type" "text/html; charset=utf-8" + "x-robots-tag" "noindex"} + :body result} + {:status 404 + :body "not found"}))))) diff --git a/backend/src/app/loggers/loki.clj b/backend/src/app/loggers/loki.clj index 7344bad1f..5f6980d32 100644 --- a/backend/src/app/loggers/loki.clj +++ b/backend/src/app/loggers/loki.clj @@ -7,12 +7,12 @@ (ns app.loggers.loki "A Loki integration." (:require + [app.common.logging :as l] [app.common.spec :as us] [app.config :as cfg] [app.util.async :as aa] [app.util.http :as http] [app.util.json :as json] - [app.util.logging :as l] [app.worker :as wrk] [clojure.core.async :as a] [clojure.spec.alpha :as s] diff --git a/backend/src/app/loggers/mattermost.clj b/backend/src/app/loggers/mattermost.clj index f4a60e5c1..ca23b9229 100644 --- a/backend/src/app/loggers/mattermost.clj +++ b/backend/src/app/loggers/mattermost.clj @@ -7,32 +7,51 @@ (ns app.loggers.mattermost "A mattermost integration for error reporting." (:require - [app.common.exceptions :as ex] - [app.common.spec :as us] - [app.common.uuid :as uuid] - [app.config :as cfg] + [app.common.logging :as l] + [app.config :as cf] [app.db :as db] + [app.loggers.database :as ldb] [app.util.async :as aa] [app.util.http :as http] [app.util.json :as json] - [app.util.logging :as l] - [app.util.template :as tmpl] [app.worker :as wrk] [clojure.core.async :as a] - [clojure.java.io :as io] [clojure.spec.alpha :as s] - [cuerdas.core :as str] [integrant.core :as ig])) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Error Listener -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defonce enabled (atom true)) -(declare handle-event) +(defn- send-mattermost-notification! + [cfg {:keys [host id public-uri] :as event}] + (try + (let [uri (:uri cfg) + text (str "Exception on (host: " host ", url: " public-uri "/dbg/error-by-id/" id ")\n" + (when-let [pid (:profile-id event)] + (str "- profile-id: #uuid-" pid "\n"))) + rsp (http/send! {:uri uri + :method :post + :headers {"content-type" "application/json"} + :body (json/encode-str {:text text})})] + (when (not= (:status rsp) 200) + (l/error :hint "error on sending data to mattermost" + :response (pr-str rsp)))) -(defonce enabled-mattermost (atom true)) + (catch Exception e + (l/error :hint "unexpected exception on error reporter" + :cause e)))) -(s/def ::uri ::us/string) +(defn handle-event + [{:keys [executor] :as cfg} event] + (aa/with-thread executor + (try + (let [event (ldb/parse-event event)] + (when @enabled + (send-mattermost-notification! cfg event))) + (catch Exception e + (l/warn :hint "unexpected exception on error reporter" :cause e))))) + + +(s/def ::uri ::cf/error-report-webhook) (defmethod ig/pre-init-spec ::reporter [_] (s/keys :req-un [::wrk/executor ::db/pool ::receiver] @@ -58,95 +77,3 @@ [_ output] (when output (a/close! output))) - -(defn- send-mattermost-notification! - [cfg {:keys [host id] :as cdata}] - (try - (let [uri (:uri cfg) - text (str "Unhandled exception (host: " host ", url: " (cfg/get :public-uri) "/dbg/error-by-id/" id "\n" - "- profile-id: #" (:profile-id cdata) "\n") - rsp (http/send! {:uri uri - :method :post - :headers {"content-type" "application/json"} - :body (json/encode-str {:text text})})] - (when (not= (:status rsp) 200) - (l/error :hint "error on sending data to mattermost" - :response (pr-str rsp)))) - - (catch Exception e - (l/error :hint "unexpected exception on error reporter" - :cause e)))) - -(defn- persist-on-database! - [{:keys [pool] :as cfg} {:keys [id] :as cdata}] - (db/with-atomic [conn pool] - (db/insert! conn :server-error-report - {:id id :content (db/tjson cdata)}))) - -(defn- parse-context - [event] - (reduce-kv - (fn [acc k v] - (cond - (= k :id) (assoc acc k (uuid/uuid v)) - (= k :profile-id) (assoc acc k (uuid/uuid v)) - (str/blank? v) acc - :else (assoc acc k v))) - {:id (uuid/next)} - (:context event))) - -(defn- parse-event - [event] - (-> (parse-context event) - (merge (dissoc event :context)) - (assoc :tenant (cfg/get :tenant)) - (assoc :host (cfg/get :host)) - (assoc :public-uri (cfg/get :public-uri)) - (assoc :version (:full cfg/version)))) - -(defn handle-event - [{:keys [executor] :as cfg} event] - (aa/with-thread executor - (try - (let [cdata (parse-event event)] - (when @enabled-mattermost - (send-mattermost-notification! cfg cdata)) - (persist-on-database! cfg cdata)) - (catch Exception e - (l/error :hint "unexpected exception on error reporter" - :cause e))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Http Handler -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::db/pool])) - -(defmethod ig/init-key ::handler - [_ {:keys [pool] :as cfg}] - (letfn [(parse-id [request] - (let [id (get-in request [:path-params :id]) - id (us/uuid-conformer id)] - (when (uuid? id) - id))) - (retrieve-report [id] - (ex/ignoring - (when-let [{:keys [content] :as row} (db/get-by-id pool :server-error-report id)] - (assoc row :content (db/decode-transit-pgobject content))))) - - (render-template [{:keys [content] :as report}] - (some-> (io/resource "error-report.tmpl") - (tmpl/render content)))] - - - (fn [request] - (let [result (some-> (parse-id request) - (retrieve-report) - (render-template))] - (if result - {:status 200 - :headers {"content-type" "text/html; charset=utf-8"} - :body result} - {:status 404 - :body "not found"}))))) diff --git a/backend/src/app/loggers/sentry.clj b/backend/src/app/loggers/sentry.clj new file mode 100644 index 000000000..78ac1ee39 --- /dev/null +++ b/backend/src/app/loggers/sentry.clj @@ -0,0 +1,172 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.loggers.sentry + "A mattermost integration for error reporting." + (:require + [app.common.logging :as l] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.util.async :as aa] + [app.worker :as wrk] + [clojure.core.async :as a] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [integrant.core :as ig]) + (:import + io.sentry.Scope + io.sentry.IHub + io.sentry.Hub + io.sentry.NoOpHub + io.sentry.protocol.User + io.sentry.SentryOptions + io.sentry.SentryLevel + io.sentry.ScopeCallback)) + +(defonce enabled (atom true)) + +(defn- parse-context + [event] + (reduce-kv + (fn [acc k v] + (cond + (= k :id) (assoc acc k (uuid/uuid v)) + (= k :profile-id) (assoc acc k (uuid/uuid v)) + (str/blank? v) acc + :else (assoc acc k v))) + {} + (:context event))) + +(defn- parse-event + [event] + (assoc event :context (parse-context event))) + +(defn- build-sentry-options + [cfg] + (let [version (:base cf/version)] + (doto (SentryOptions.) + (.setDebug (:debug cfg false)) + (.setTracesSampleRate (:traces-sample-rate cfg 1.0)) + (.setDsn (:dsn cfg)) + (.setServerName (cf/get :host)) + (.setEnvironment (cf/get :tenant)) + (.setAttachServerName true) + (.setAttachStacktrace (:attach-stack-trace cfg false)) + (.setRelease (str "backend@" (if (= version "0.0.0") "develop" version)))))) + +(defn handle-event + [^IHub shub event] + (letfn [(set-user! [^Scope scope {:keys [context] :as event}] + (let [user (User.)] + (.setIpAddress ^User user ^String (:ip-addr context)) + (when-let [pid (:profile-id context)] + (.setId ^User user ^String (str pid))) + (.setUser scope ^User user))) + + (set-level! [^Scope scope] + (.setLevel scope SentryLevel/ERROR)) + + (set-context! [^Scope scope {:keys [context] :as event}] + (let [uri (str (cf/get :public-uri) "/dbg/error-by-id/" (:id context))] + (.setContexts scope "detailed_error_uri" ^String uri)) + (when-let [vers (:frontend-version event)] + (.setContexts scope "frontend_version" ^String vers)) + (when-let [puri (:public-uri event)] + (.setContexts scope "public_uri" ^String (str puri))) + (when-let [uagent (:user-agent context)] + (.setContexts scope "user_agent" ^String uagent)) + (when-let [tenant (:tenant event)] + (.setTag scope "tenant" ^String tenant)) + (when-let [type (:error-type context)] + (.setTag scope "error_type" ^String (str type))) + (when-let [code (:error-code context)] + (.setTag scope "error_code" ^String (str code))) + ) + + (capture [^Scope scope {:keys [context error] :as event}] + (let [msg (str (:message error) "\n\n" + + "======================================================\n" + "=================== Params ===========================\n" + "======================================================\n" + + (:params context) "\n" + + (when (:explain context) + (str "======================================================\n" + "=================== Explain ==========================\n" + "======================================================\n" + (:explain context) "\n")) + + (when (:data context) + (str "======================================================\n" + "=================== Error Data =======================\n" + "======================================================\n" + (:data context) "\n")) + + (str "======================================================\n" + "=================== Stack Trace ======================\n" + "======================================================\n" + (:trace error)) + + "\n")] + (set-user! scope event) + (set-level! scope) + (set-context! scope event) + (.captureMessage ^IHub shub msg) + )) + ] + ;; (clojure.pprint/pprint event) + + (when @enabled + (.withScope ^IHub shub (reify ScopeCallback + (run [_ scope] + (->> event + (parse-event) + (capture scope)))))) + + )) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Error Listener +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(s/def ::receiver any?) +(s/def ::dsn ::cf/sentry-dsn) +(s/def ::trace-sample-rate ::cf/sentry-trace-sample-rate) +(s/def ::attach-stack-trace ::cf/sentry-attach-stack-trace) +(s/def ::debug ::cf/sentry-debug) + +(defmethod ig/pre-init-spec ::reporter [_] + (s/keys :req-un [::wrk/executor ::db/pool ::receiver] + :opt-un [::dsn ::trace-sample-rate ::attach-stack-trace])) + +(defmethod ig/init-key ::reporter + [_ {:keys [receiver dsn executor] :as cfg}] + (l/info :msg "initializing sentry reporter" :dsn dsn) + (let [opts (build-sentry-options cfg) + shub (if dsn + (Hub. ^SentryOptions opts) + (NoOpHub/getInstance)) + output (a/chan (a/sliding-buffer 128) + (filter #(= (:level %) "error")))] + (receiver :sub output) + (a/go-loop [] + (let [event (a/ path slurp svg/parse get-basic-info-from-svg)] + (let [info (some-> path slurp svg/pre-process svg/parse get-basic-info-from-svg)] (when-not info (ex/raise :type :validation :code :invalid-svg-file diff --git a/backend/src/app/metrics.clj b/backend/src/app/metrics.clj index 148e9b3a2..b1d0033e6 100644 --- a/backend/src/app/metrics.clj +++ b/backend/src/app/metrics.clj @@ -7,7 +7,7 @@ (ns app.metrics (:require [app.common.exceptions :as ex] - [app.util.logging :as l] + [app.common.logging :as l] [clojure.spec.alpha :as s] [integrant.core :as ig]) (:import diff --git a/backend/src/app/msgbus.clj b/backend/src/app/msgbus.clj index f7a013f72..146da0c83 100644 --- a/backend/src/app/msgbus.clj +++ b/backend/src/app/msgbus.clj @@ -8,10 +8,10 @@ "The msgbus abstraction implemented using redis as underlying backend." (:require [app.common.exceptions :as ex] + [app.common.logging :as l] [app.common.spec :as us] [app.config :as cfg] [app.util.blob :as blob] - [app.util.logging :as l] [app.util.time :as dt] [clojure.core.async :as a] [clojure.spec.alpha :as s] diff --git a/backend/src/app/notifications.clj b/backend/src/app/notifications.clj index f7efa6f3f..62490b212 100644 --- a/backend/src/app/notifications.clj +++ b/backend/src/app/notifications.clj @@ -7,12 +7,12 @@ (ns app.notifications "A websocket based notifications mechanism." (:require + [app.common.logging :as l] [app.common.spec :as us] [app.common.transit :as t] [app.db :as db] [app.metrics :as mtx] [app.util.async :as aa] - [app.util.logging :as l] [app.util.time :as dt] [app.worker :as wrk] [clojure.core.async :as a] @@ -69,6 +69,7 @@ :mtx-messages mtx-messages :mtx-sessions mtx-sessions )] + (-> #(handler cfg %) (wrap-session) (wrap-keyword-params) diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index d77f83e3e..724b527aa 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -8,12 +8,12 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.logging :as l] [app.common.spec :as us] [app.db :as db] [app.loggers.audit :as audit] [app.metrics :as mtx] [app.rlimits :as rlm] - [app.util.logging :as l] [app.util.services :as sv] [clojure.spec.alpha :as s] [cuerdas.core :as str] @@ -97,36 +97,39 @@ auth? (:auth mdata true)] (l/trace :action "register" :name (::sv/name mdata)) - (fn [params] + (with-meta + (fn [params] - ;; Raise authentication error when rpc method requires auth but - ;; no profile-id is found in the request. - (when (and auth? (not (uuid? (:profile-id params)))) - (ex/raise :type :authentication - :code :authentication-required - :hint "authentication required for this endpoint")) + ;; Raise authentication error when rpc method requires auth but + ;; no profile-id is found in the request. + (when (and auth? (not (uuid? (:profile-id params)))) + (ex/raise :type :authentication + :code :authentication-required + :hint "authentication required for this endpoint")) - (let [params' (dissoc params ::request) - params' (us/conform spec params') - result (f cfg params')] + (let [params' (dissoc params ::request) + params' (us/conform spec params') + result (f cfg params')] - ;; When audit log is enabled (default false). - (when (fn? audit) - (let [resultm (meta result) - request (::request params) - profile-id (or (:profile-id params') - (:profile-id result) - (::audit/profile-id resultm)) - props (d/merge params' (::audit/props resultm))] - (audit :cmd :submit - :type (::type cfg) - :name (or (::audit/name resultm) - (::sv/name mdata)) - :profile-id profile-id - :ip-addr (audit/parse-client-ip request) - :props props))) + ;; When audit log is enabled (default false). + (when (fn? audit) + (let [resultm (meta result) + request (::request params) + profile-id (or (:profile-id params') + (:profile-id result) + (::audit/profile-id resultm)) + props (d/merge params' (::audit/props resultm))] + (audit :cmd :submit + :type (or (::audit/type resultm) + (::type cfg)) + :name (or (::audit/name resultm) + (::sv/name mdata)) + :profile-id profile-id + :ip-addr (audit/parse-client-ip request) + :props props))) - result)))) + result)) + mdata))) (defn- process-method [cfg vfn] @@ -148,10 +151,8 @@ 'app.rpc.queries.teams 'app.rpc.queries.comments 'app.rpc.queries.profile - 'app.rpc.queries.recent-files 'app.rpc.queries.viewer - 'app.rpc.queries.fonts - 'app.rpc.queries.svg) + 'app.rpc.queries.fonts) (map (partial process-method cfg)) (into {})))) @@ -170,7 +171,6 @@ 'app.rpc.mutations.files 'app.rpc.mutations.comments 'app.rpc.mutations.projects - 'app.rpc.mutations.viewer 'app.rpc.mutations.teams 'app.rpc.mutations.management 'app.rpc.mutations.ldap diff --git a/backend/src/app/rpc/mutations/demo.clj b/backend/src/app/rpc/mutations/demo.clj index fc7f184bf..cca26dbb4 100644 --- a/backend/src/app/rpc/mutations/demo.clj +++ b/backend/src/app/rpc/mutations/demo.clj @@ -9,7 +9,7 @@ (:require [app.common.exceptions :as ex] [app.common.uuid :as uuid] - [app.config :as cfg] + [app.config :as cf] [app.db :as db] [app.loggers.audit :as audit] [app.rpc.mutations.profile :as profile] @@ -35,11 +35,11 @@ :email email :fullname fullname :is-demo true - :deleted-at (dt/in-future cfg/deletion-delay) + :deleted-at (dt/in-future cf/deletion-delay) :password password :props {:onboarding-viewed true}}] - (when-not (cfg/get :allow-demo-users) + (when-not (contains? cf/flags :demo-users) (ex/raise :type :validation :code :demo-users-not-allowed :hint "Demo users are disabled by config.")) diff --git a/backend/src/app/rpc/mutations/fonts.clj b/backend/src/app/rpc/mutations/fonts.clj index 85c604b15..b9682dad2 100644 --- a/backend/src/app/rpc/mutations/fonts.clj +++ b/backend/src/app/rpc/mutations/fonts.clj @@ -9,14 +9,12 @@ [app.common.exceptions :as ex] [app.common.spec :as us] [app.common.uuid :as uuid] - [app.config :as cf] [app.db :as db] [app.media :as media] [app.rpc.queries.teams :as teams] [app.storage :as sto] [app.util.services :as sv] [app.util.time :as dt] - [app.worker :as wrk] [clojure.spec.alpha :as s])) (declare create-font-variant) @@ -129,13 +127,6 @@ (db/with-atomic [conn pool] (teams/check-edition-permissions! conn profile-id team-id) - ;; Schedule object deletion - (wrk/submit! {::wrk/task :delete-object - ::wrk/delay cf/deletion-delay - ::wrk/conn conn - :id id - :type :team-font-variant}) - (db/update! conn :team-font-variant {:deleted-at (dt/now)} {:id id :team-id team-id}) diff --git a/backend/src/app/rpc/mutations/ldap.clj b/backend/src/app/rpc/mutations/ldap.clj index d982a30b0..0f6675f24 100644 --- a/backend/src/app/rpc/mutations/ldap.clj +++ b/backend/src/app/rpc/mutations/ldap.clj @@ -7,13 +7,13 @@ (ns app.rpc.mutations.ldap (:require [app.common.exceptions :as ex] + [app.common.logging :as l] [app.common.spec :as us] [app.config :as cfg] [app.db :as db] [app.loggers.audit :as audit] [app.rpc.mutations.profile :as profile-m] [app.rpc.queries.profile :as profile-q] - [app.util.logging :as l] [app.util.services :as sv] [clj-ldap.client :as ldap] [clojure.spec.alpha :as s] diff --git a/backend/src/app/rpc/mutations/management.clj b/backend/src/app/rpc/mutations/management.clj index 1cf111729..76d997fee 100644 --- a/backend/src/app/rpc/mutations/management.clj +++ b/backend/src/app/rpc/mutations/management.clj @@ -39,11 +39,23 @@ [file index] (letfn [(process-form [form] (cond-> form - ;; Relink Components + ;; Relink library items (and (map? form) (uuid? (:component-file form))) (update :component-file #(get index % %)) + (and (map? form) + (uuid? (:fill-color-ref-file form))) + (update :fill-color-ref-file #(get index % %)) + + (and (map? form) + (uuid? (:stroke-color-ref-file form))) + (update :stroke-color-ref-file #(get index % %)) + + (and (map? form) + (uuid? (:typography-ref-file form))) + (update :typography-ref-file #(get index % %)) + ;; Relink Image Shapes (and (map? form) (map? (:metadata form)) diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 099092ba5..906ec83ad 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -12,7 +12,7 @@ [app.config :as cf] [app.db :as db] [app.emails :as eml] - [app.http.oauth :refer [extract-props]] + [app.http.oauth :refer [extract-utm-props]] [app.loggers.audit :as audit] [app.media :as media] [app.metrics :as mtx] @@ -100,10 +100,9 @@ (sv/defmethod ::prepare-register-profile {:auth false} [{:keys [pool tokens] :as cfg} params] - (when-not (cf/get :registration-enabled) + (when-not (contains? cf/flags :registration) (ex/raise :type :restriction :code :registration-disabled)) - (when-let [domains (cf/get :registration-domain-whitelist)] (when-not (email-domain-in-whitelist? domains (:email params)) (ex/raise :type :validation @@ -128,23 +127,16 @@ ;; --- MUTATION: Register Profile (s/def ::accept-terms-and-privacy ::us/boolean) -(s/def ::accept-newsletter-subscription ::us/boolean) (s/def ::token ::us/not-empty-string) (s/def ::register-profile - (s/keys :req-un [::token ::fullname - ::accept-terms-and-privacy] - :opt-un [::accept-newsletter-subscription])) + (s/keys :req-un [::token ::fullname])) (sv/defmethod ::register-profile {:auth false :rlimit :password} [{:keys [pool] :as cfg} params] - (when-not (:accept-terms-and-privacy params) - (ex/raise :type :validation - :code :invalid-terms-and-privacy)) - (db/with-atomic [conn pool] - (let [cfg (assoc cfg :conn conn)] - (register-profile cfg params)))) + (-> (assoc cfg :conn conn) + (register-profile params)))) (defn- annotate-profile-register "A helper for properly increase the profile-register metric once the @@ -163,6 +155,7 @@ (create-profile conn) (create-profile-relations conn) (decode-profile-row))] + (sid/load-initial-project! conn profile) (cond @@ -204,7 +197,6 @@ ptoken (tokens :generate-predefined {:iss :profile-identity :profile-id (:id profile)})] - (eml/send! {::eml/conn conn ::eml/factory eml/register :public-uri (:public-uri cfg) @@ -224,18 +216,17 @@ [conn params] (let [id (or (:id params) (uuid/next)) - props (-> (extract-props params) + props (-> (extract-utm-props params) (merge (:props params)) - (assoc :accept-terms-and-privacy (:accept-terms-and-privacy params true)) - (assoc :accept-newsletter-subscription (:accept-newsletter-subscription params false)) (db/tjson)) password (if-let [password (:password params)] (derive-password password) "!") - locale (as-> (:locale params) locale - (and (string? locale) (not (str/blank? locale)) locale)) + locale (:locale params) + locale (when (and (string? locale) (not (str/blank? locale))) + locale) backend (:backend params "penpot") is-demo (:is-demo params false) @@ -359,11 +350,14 @@ (defn- update-profile [conn {:keys [id fullname lang theme] :as params}] - (db/update! conn :profile - {:fullname fullname - :lang lang - :theme theme} - {:id id})) + (let [profile (db/update! conn :profile + {:fullname fullname + :lang lang + :theme theme} + {:id id})] + (-> profile + (profile/decode-profile-row) + (profile/strip-private-attrs)))) (s/def ::update-profile (s/keys :req-un [::id ::fullname] @@ -372,8 +366,9 @@ (sv/defmethod ::update-profile [{:keys [pool] :as cfg} params] (db/with-atomic [conn pool] - (update-profile conn params) - nil)) + (let [profile (update-profile conn params)] + (with-meta profile + {::audit/props (audit/profile->props profile)})))) ;; --- MUTATION: Update Password @@ -458,7 +453,8 @@ params (assoc params :profile profile :email (str/lower email))] - (if (cf/get :smtp-enabled) + (if (or (cf/get :smtp-enabled) + (contains? cf/flags :smtp)) (request-email-change cfg params) (change-email-inmediatelly cfg params))))) @@ -591,11 +587,15 @@ (db/with-atomic [conn pool] (let [profile (profile/retrieve-profile-data conn profile-id) props (reduce-kv (fn [props k v] - (if (nil? v) - (dissoc props k) - (assoc props k v))) + ;; We don't accept namespaced keys + (if (simple-ident? k) + (if (nil? v) + (dissoc props k) + (assoc props k v)) + props)) (:props profile) props)] + (db/update! conn :profile {:props (db/tjson props)} {:id profile-id}) diff --git a/backend/src/app/rpc/mutations/share_link.clj b/backend/src/app/rpc/mutations/share_link.clj index 0e366957f..6079ecf7d 100644 --- a/backend/src/app/rpc/mutations/share_link.clj +++ b/backend/src/app/rpc/mutations/share_link.clj @@ -31,6 +31,11 @@ :opt-un [::pages])) (sv/defmethod ::create-share-link + "Creates a share-link object. + + Share links are resources that allows external users access to + specific files with specific permissions (flags)." + [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id file-id) diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj index 0d385fa2e..625a13af6 100644 --- a/backend/src/app/rpc/mutations/teams.clj +++ b/backend/src/app/rpc/mutations/teams.clj @@ -132,8 +132,8 @@ (sv/defmethod ::delete-team [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] (db/with-atomic [conn pool] - (let [perms (teams/check-edition-permissions! conn profile-id id)] - (when-not (some :is-owner perms) + (let [perms (teams/get-permissions conn profile-id id)] + (when-not (:is-owner perms) (ex/raise :type :validation :code :only-owner-can-delete-team)) @@ -300,7 +300,7 @@ (sv/defmethod ::invite-team-member [{:keys [pool tokens] :as cfg} {:keys [profile-id team-id email role] :as params}] (db/with-atomic [conn pool] - (let [perms (teams/check-edition-permissions! conn profile-id team-id) + (let [perms (teams/get-permissions conn profile-id team-id) profile (db/get-by-id conn :profile profile-id) member (profile/retrieve-profile-data-by-email conn email) team (db/get-by-id conn :team team-id) @@ -316,7 +316,7 @@ {:iss :profile-identity :profile-id (:id profile)})] - (when-not (some :is-admin perms) + (when-not (:is-admin perms) (ex/raise :type :validation :code :insufficient-permissions)) diff --git a/backend/src/app/rpc/mutations/verify_token.clj b/backend/src/app/rpc/mutations/verify_token.clj index 61b5f9abb..1fa32b81d 100644 --- a/backend/src/app/rpc/mutations/verify_token.clj +++ b/backend/src/app/rpc/mutations/verify_token.clj @@ -9,6 +9,7 @@ [app.common.exceptions :as ex] [app.common.spec :as us] [app.db :as db] + [app.loggers.audit :as audit] [app.metrics :as mtx] [app.rpc.mutations.teams :as teams] [app.rpc.queries.profile :as profile] @@ -63,7 +64,10 @@ (with-meta claims {:transform-response ((:create session) profile-id) - :before-complete (annotate-profile-activation metrics)}))) + :before-complete (annotate-profile-activation metrics) + ::audit/name "verify-profile-email" + ::audit/props (audit/profile->props profile) + ::audit/profile-id (:id profile)}))) (defmethod process-token :auth [{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}] @@ -116,8 +120,7 @@ ;; user is already logged in with some account. (and (uuid? profile-id) (uuid? member-id)) - (do - (accept-invitation cfg claims) + (let [profile (accept-invitation cfg claims)] (if (= member-id profile-id) ;; If the current session is already matches the invited ;; member, then just return the token and leave the frontend @@ -131,27 +134,44 @@ ;; account. (with-meta (assoc claims :state :created) - {:transform-response ((:create session) member-id)}))) + {:transform-response ((:create session) member-id) + ::audit/name "accept-team-invitation" + ::audit/props (merge + (audit/profile->props profile) + {:team-id (:team-id claims) + :role (:role claims)}) + ::audit/profile-id profile-id}))) ;; This happens when member-id is not filled in the invitation but ;; the user already has an account (probably with other mail) and ;; is already logged-in. (and (uuid? profile-id) (nil? member-id)) - (do - (accept-invitation cfg (assoc claims :member-id profile-id)) - (assoc claims :state :created)) + (let [profile (accept-invitation cfg (assoc claims :member-id profile-id))] + (with-meta + (assoc claims :state :created) + {::audit/name "accept-team-invitation" + ::audit/props (merge + (audit/profile->props profile) + {:team-id (:team-id claims) + :role (:role claims)}) + ::audit/profile-id profile-id})) ;; This happens when member-id is filled but the accessing user is ;; not logged-in. In this case we proceed to accept invitation and ;; leave the user logged-in. (and (nil? profile-id) (uuid? member-id)) - (do - (accept-invitation cfg claims) + (let [profile (accept-invitation cfg claims)] (with-meta (assoc claims :state :created) - {:transform-response ((:create session) member-id)})) + {:transform-response ((:create session) member-id) + ::audit/name "accept-team-invitation" + ::audit/props (merge + (audit/profile->props profile) + {:team-id (:team-id claims) + :role (:role claims)}) + ::audit/profile-id member-id})) ;; In this case, we wait until frontend app redirect user to ;; registeration page, the user is correctly registered and the diff --git a/backend/src/app/rpc/mutations/viewer.clj b/backend/src/app/rpc/mutations/viewer.clj deleted file mode 100644 index 85beafa96..000000000 --- a/backend/src/app/rpc/mutations/viewer.clj +++ /dev/null @@ -1,49 +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/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.rpc.mutations.viewer - (:require - [app.common.spec :as us] - [app.db :as db] - [app.rpc.queries.files :as files] - [app.util.services :as sv] - [buddy.core.codecs :as bc] - [buddy.core.nonce :as bn] - [clojure.spec.alpha :as s])) - -(s/def ::profile-id ::us/uuid) -(s/def ::file-id ::us/uuid) -(s/def ::page-id ::us/uuid) - -(s/def ::create-file-share-token - (s/keys :req-un [::profile-id ::file-id ::page-id])) - -(sv/defmethod ::create-file-share-token - [{:keys [pool] :as cfg} {:keys [profile-id file-id page-id] :as params}] - (db/with-atomic [conn pool] - (files/check-edition-permissions! conn profile-id file-id) - (let [token (-> (bn/random-bytes 16) - (bc/bytes->b64u) - (bc/bytes->str))] - (db/insert! conn :file-share-token - {:file-id file-id - :page-id page-id - :token token}) - {:token token}))) - - -(s/def ::token ::us/not-empty-string) -(s/def ::delete-file-share-token - (s/keys :req-un [::profile-id ::file-id ::token])) - -(sv/defmethod ::delete-file-share-token - [{:keys [pool] :as cfg} {:keys [profile-id file-id token]}] - (db/with-atomic [conn pool] - (files/check-edition-permissions! conn profile-id file-id) - (db/delete! conn :file-share-token - {:file-id file-id - :token token}) - nil)) diff --git a/backend/src/app/rpc/permissions.clj b/backend/src/app/rpc/permissions.clj index 9481bcd57..363f967e6 100644 --- a/backend/src/app/rpc/permissions.clj +++ b/backend/src/app/rpc/permissions.clj @@ -41,59 +41,24 @@ "A simple factory for edition permission predicate functions." [qfn] (us/assert fn? qfn) - (fn [& args] - (let [rows (apply qfn args)] - (when-not (or (empty? rows) - (not (or (some :can-edit rows) - (some :is-admin rows) - (some :is-owner rows)))) - rows)))) + (fn check + ([perms] (:can-edit perms)) + ([conn & args] (check (apply qfn conn args))))) (defn make-read-predicate-fn "A simple factory for read permission predicate functions." [qfn] (us/assert fn? qfn) - (fn [& args] - (let [rows (apply qfn args)] - (when (seq rows) - rows)))) + (fn check + ([perms] (:can-read perms)) + ([conn & args] (check (apply qfn conn args))))) (defn make-check-fn "Helper that converts a predicate permission function to a check function (function that raises an exception)." [pred] (fn [& args] - (when-not (seq (apply pred args)) + (when-not (apply pred args) (ex/raise :type :not-found :code :object-not-found :hint "not found")))) - - -;; TODO: the following functions are deprecated and replaced with the -;; new ones. Should not be used. - -(defn make-edition-check-fn - "A simple factory for edition permission check functions." - [qfn] - (us/assert fn? qfn) - (fn [& args] - (let [rows (apply qfn args)] - (if (or (empty? rows) - (not (or (some :can-edit rows) - (some :is-admin rows) - (some :is-owner rows)))) - (ex/raise :type :not-found - :code :object-not-found - :hint "not found") - rows)))) - -(defn make-read-check-fn - "A simple factory for read permission check functions." - [qfn] - (us/assert fn? qfn) - (fn [& args] - (let [rows (apply qfn args)] - (if-not (seq rows) - (ex/raise :type :not-found - :code :object-not-found) - rows)))) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 032032216..7caff964b 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -12,6 +12,7 @@ [app.db :as db] [app.rpc.permissions :as perms] [app.rpc.queries.projects :as projects] + [app.rpc.queries.share-link :refer [retrieve-share-link]] [app.rpc.queries.teams :as teams] [app.storage.impl :as simpl] [app.util.blob :as blob] @@ -59,7 +60,7 @@ where f.id = ? and ppr.profile_id = ?") -(defn- retrieve-file-permissions +(defn retrieve-file-permissions [conn profile-id file-id] (when (and profile-id file-id) (db/exec! conn [sql:file-permissions @@ -67,11 +68,37 @@ file-id profile-id file-id profile-id]))) +(defn get-permissions + ([conn profile-id file-id] + (let [rows (retrieve-file-permissions conn profile-id file-id) + is-owner (boolean (some :is-owner rows)) + is-admin (boolean (some :is-admin rows)) + can-edit (boolean (some :can-edit rows))] + (when (seq rows) + {:type :membership + :is-owner is-owner + :is-admin (or is-owner is-admin) + :can-edit (or is-owner is-admin can-edit) + :can-read true}))) + ([conn profile-id file-id share-id] + (let [perms (get-permissions conn profile-id file-id) + ldata (retrieve-share-link conn file-id share-id)] + + ;; NOTE: in a future when share-link becomes more powerfull and + ;; will allow us specify which parts of the app is availabel, we + ;; will probably need to tweak this function in order to expose + ;; this flags to the frontend. + (cond + (some? perms) perms + (some? ldata) {:type :share-link + :can-read true + :flags (:flags ldata)})))) + (def has-edit-permissions? - (perms/make-edition-predicate-fn retrieve-file-permissions)) + (perms/make-edition-predicate-fn get-permissions)) (def has-read-permissions? - (perms/make-read-predicate-fn retrieve-file-permissions)) + (perms/make-read-predicate-fn get-permissions)) (def check-edition-permissions! (perms/make-check-fn has-edit-permissions?)) @@ -79,7 +106,6 @@ (def check-read-permissions! (perms/make-check-fn has-read-permissions?)) - ;; --- Query: Files search ;; TODO: this query need to a good refactor @@ -131,29 +157,6 @@ profile-id team-id search-term]))) - -;; --- Query: Files - -;; DEPRECATED: should be removed probably on 1.6.x - -(def ^:private sql:files - "select f.* - from file as f - where f.project_id = ? - and f.deleted_at is null - order by f.modified_at desc") - -(s/def ::project-id ::us/uuid) -(s/def ::files - (s/keys :req-un [::profile-id ::project-id])) - -(sv/defmethod ::files - [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}] - (with-open [conn (db/open pool)] - (projects/check-read-permissions! conn profile-id project-id) - (into [] decode-row-xf (db/exec! conn [sql:files project-id])))) - - ;; --- Query: Project Files (def ^:private sql:project-files @@ -201,11 +204,15 @@ (s/keys :req-un [::profile-id ::id])) (sv/defmethod ::file + "Retrieve a file by its ID. Only authenticated users." [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] (db/with-atomic [conn pool] - (let [cfg (assoc cfg :conn conn)] - (check-edition-permissions! conn profile-id id) - (retrieve-file cfg id)))) + (let [cfg (assoc cfg :conn conn) + perms (get-permissions conn profile-id id)] + + (check-read-permissions! perms) + (some-> (retrieve-file cfg id) + (assoc :permissions perms))))) (s/def ::page (s/keys :req-un [::profile-id ::file-id])) @@ -240,7 +247,8 @@ (sv/defmethod ::page [{:keys [pool] :as cfg} {:keys [profile-id file-id strip-thumbnails]}] (db/with-atomic [conn pool] - (check-edition-permissions! conn profile-id file-id) + (check-read-permissions! conn profile-id file-id) + (let [cfg (assoc cfg :conn conn) file (retrieve-file cfg file-id) page-id (get-in file [:data :pages 0])] @@ -250,28 +258,6 @@ ;; --- Query: Shared Library Files -;; DEPRECATED: and will be removed on 1.6.x - -(def ^:private sql:shared-files - "select f.* - from file as f - inner join project as p on (p.id = f.project_id) - where f.is_shared = true - and f.deleted_at is null - and p.deleted_at is null - and p.team_id = ? - order by f.modified_at desc") - -(s/def ::shared-files - (s/keys :req-un [::profile-id ::team-id])) - -(sv/defmethod ::shared-files - [{:keys [pool] :as cfg} {:keys [team-id] :as params}] - (into [] decode-row-xf (db/exec! pool [sql:shared-files team-id]))) - - -;; --- Query: Shared Library Files - (def ^:private sql:team-shared-files "select f.id, f.project_id, @@ -336,7 +322,7 @@ [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] (let [cfg (assoc cfg :conn conn)] - (check-edition-permissions! conn profile-id file-id) + (check-read-permissions! conn profile-id file-id) (retrieve-file-libraries cfg false file-id)))) ;; --- QUERY: team-recent-files diff --git a/backend/src/app/rpc/queries/profile.clj b/backend/src/app/rpc/queries/profile.clj index 41f322881..6843a0719 100644 --- a/backend/src/app/rpc/queries/profile.clj +++ b/backend/src/app/rpc/queries/profile.clj @@ -70,6 +70,10 @@ [conn profile] (merge profile (retrieve-additional-data conn (:id profile)))) +(defn- filter-profile-props + [props] + (into {} (filter (fn [[k _]] (simple-ident? k))) props)) + (defn decode-profile-row [{:keys [props] :as row}] (cond-> row @@ -90,7 +94,7 @@ (ex/raise :type :not-found :hint "Object doest not exists.")) - profile)) + (update profile :props filter-profile-props))) (def ^:private sql:profile-by-email "select p.* from profile as p diff --git a/backend/src/app/rpc/queries/projects.clj b/backend/src/app/rpc/queries/projects.clj index 29195ceb0..47886784a 100644 --- a/backend/src/app/rpc/queries/projects.clj +++ b/backend/src/app/rpc/queries/projects.clj @@ -31,18 +31,31 @@ where ppr.project_id = ? and ppr.profile_id = ?") -(defn- retrieve-project-permissions +(defn- get-permissions [conn profile-id project-id] - (db/exec! conn [sql:project-permissions - project-id profile-id - project-id profile-id])) + (let [rows (db/exec! conn [sql:project-permissions + project-id profile-id + project-id profile-id]) + is-owner (boolean (some :is-owner rows)) + is-admin (boolean (some :is-admin rows)) + can-edit (boolean (some :can-edit rows))] + (when (seq rows) + {:is-owner is-owner + :is-admin (or is-owner is-admin) + :can-edit (or is-owner is-admin can-edit) + :can-read true}))) + +(def has-edit-permissions? + (perms/make-edition-predicate-fn get-permissions)) + +(def has-read-permissions? + (perms/make-read-predicate-fn get-permissions)) (def check-edition-permissions! - (perms/make-edition-check-fn retrieve-project-permissions)) + (perms/make-check-fn has-edit-permissions?)) (def check-read-permissions! - (perms/make-read-check-fn retrieve-project-permissions)) - + (perms/make-check-fn has-read-permissions?)) ;; --- Query: Projects diff --git a/backend/src/app/rpc/queries/recent_files.clj b/backend/src/app/rpc/queries/recent_files.clj deleted file mode 100644 index e878d34e6..000000000 --- a/backend/src/app/rpc/queries/recent_files.clj +++ /dev/null @@ -1,42 +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/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.rpc.queries.recent-files - (:require - [app.common.spec :as us] - [app.db :as db] - [app.rpc.queries.files :refer [decode-row-xf]] - [app.rpc.queries.teams :as teams] - [app.util.services :as sv] - [clojure.spec.alpha :as s])) - -;; DEPRECATED: should be removed on 1.6.x - -(def sql:recent-files - "with recent_files as ( - select f.*, row_number() over w as row_num - from file as f - join project as p on (p.id = f.project_id) - where p.team_id = ? - and p.deleted_at is null - and f.deleted_at is null - window w as (partition by f.project_id order by f.modified_at desc) - order by f.modified_at desc - ) - select * from recent_files where row_num <= 10;") - -(s/def ::team-id ::us/uuid) -(s/def ::profile-id ::us/uuid) - -(s/def ::recent-files - (s/keys :req-un [::profile-id ::team-id])) - -(sv/defmethod ::recent-files - [{:keys [pool] :as cfg} {:keys [profile-id team-id]}] - (with-open [conn (db/open pool)] - (teams/check-read-permissions! conn profile-id team-id) - (let [files (db/exec! conn [sql:recent-files team-id])] - (into [] decode-row-xf files)))) diff --git a/backend/src/app/rpc/queries/share_link.clj b/backend/src/app/rpc/queries/share_link.clj new file mode 100644 index 000000000..0bc567364 --- /dev/null +++ b/backend/src/app/rpc/queries/share_link.clj @@ -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/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.rpc.queries.share-link + (:require + [app.db :as db])) + +(defn decode-share-link-row + [row] + (-> row + (update :flags db/decode-pgarray #{}) + (update :pages db/decode-pgarray #{}))) + +(defn retrieve-share-link + [conn file-id share-id] + (some-> (db/get-by-params conn :share-link + {:id share-id :file-id file-id} + {:check-not-found false}) + (decode-share-link-row))) + diff --git a/backend/src/app/rpc/queries/teams.clj b/backend/src/app/rpc/queries/teams.clj index 7bad8ba59..407237237 100644 --- a/backend/src/app/rpc/queries/teams.clj +++ b/backend/src/app/rpc/queries/teams.clj @@ -24,16 +24,29 @@ where tpr.profile_id = ? and tpr.team_id = ?") -(defn- retrieve-team-permissions +(defn get-permissions [conn profile-id team-id] - (db/exec! conn [sql:team-permissions profile-id team-id])) + (let [rows (db/exec! conn [sql:team-permissions profile-id team-id]) + is-owner (boolean (some :is-owner rows)) + is-admin (boolean (some :is-admin rows)) + can-edit (boolean (some :can-edit rows))] + (when (seq rows) + {:is-owner is-owner + :is-admin (or is-owner is-admin) + :can-edit (or is-owner is-admin can-edit) + :can-read true}))) + +(def has-edit-permissions? + (perms/make-edition-predicate-fn get-permissions)) + +(def has-read-permissions? + (perms/make-read-predicate-fn get-permissions)) (def check-edition-permissions! - (perms/make-edition-check-fn retrieve-team-permissions)) + (perms/make-check-fn has-edit-permissions?)) (def check-read-permissions! - (perms/make-read-check-fn retrieve-team-permissions)) - + (perms/make-check-fn has-read-permissions?)) ;; --- Query: Teams @@ -58,12 +71,26 @@ join team as t on (t.id = tp.team_id) where t.deleted_at is null and tp.profile_id = ? - order by t.created_at asc") + order by tp.created_at asc") + +(defn process-permissions + [team] + (let [is-owner (:is-owner team) + is-admin (:is-admin team) + can-edit (:can-edit team) + permissions {:type :membership + :is-owner is-owner + :is-admin (or is-owner is-admin) + :can-edit (or is-owner is-admin can-edit)}] + (-> team + (dissoc :is-owner :is-admin :can-edit) + (assoc :permissions permissions)))) (defn retrieve-teams [conn profile-id] (let [defaults (profile/retrieve-additional-data conn profile-id)] - (db/exec! conn [sql:teams (:default-team-id defaults) profile-id]))) + (->> (db/exec! conn [sql:teams (:default-team-id defaults) profile-id]) + (mapv process-permissions)))) ;; --- Query: Team (by ID) @@ -86,7 +113,7 @@ (when-not result (ex/raise :type :not-found :code :team-does-not-exist)) - result)) + (process-permissions result))) ;; --- Query: Team Members diff --git a/backend/src/app/rpc/queries/viewer.clj b/backend/src/app/rpc/queries/viewer.clj index 2f5334d99..9d35f9e07 100644 --- a/backend/src/app/rpc/queries/viewer.clj +++ b/backend/src/app/rpc/queries/viewer.clj @@ -10,29 +10,17 @@ [app.common.spec :as us] [app.db :as db] [app.rpc.queries.files :as files] + [app.rpc.queries.share-link :as slnk] [app.rpc.queries.teams :as teams] [app.util.services :as sv] [clojure.spec.alpha :as s])) ;; --- Query: View Only Bundle -(defn- decode-share-link-row - [row] - (-> row - (update :flags db/decode-pgarray #{}) - (update :pages db/decode-pgarray #{}))) - (defn- retrieve-project [conn id] (db/get-by-id conn :project id {:columns [:id :name :team-id]})) -(defn- retrieve-share-link - [{:keys [conn]} file-id id] - (some-> (db/get-by-params conn :share-link - {:id id :file-id file-id} - {:check-not-found false}) - (decode-share-link-row))) - (defn- retrieve-bundle [{:keys [conn] :as cfg} file-id] (let [file (files/retrieve-file cfg file-id) @@ -41,7 +29,7 @@ users (teams/retrieve-users conn (:team-id project)) links (->> (db/query conn :share-link {:file-id file-id}) - (mapv decode-share-link-row)) + (mapv slnk/decode-share-link-row)) fonts (db/query conn :team-font-variant {:team-id (:team-id project) @@ -64,8 +52,11 @@ [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}] (db/with-atomic [conn pool] (let [cfg (assoc cfg :conn conn) - bundle (retrieve-bundle cfg file-id) - slink (retrieve-share-link cfg file-id share-id)] + slink (slnk/retrieve-share-link conn file-id share-id) + perms (files/get-permissions conn profile-id file-id share-id) + + bundle (some-> (retrieve-bundle cfg file-id) + (assoc :permissions perms))] ;; When we have neither profile nor share, we just return a not ;; found response to the user. @@ -80,13 +71,6 @@ (files/check-read-permissions! conn profile-id file-id)) (cond-> bundle - ;; If we have current profile, put - (some? profile-id) - (as-> $ (let [edit? (boolean (files/has-edit-permissions? conn profile-id file-id)) - read? (boolean (files/has-read-permissions? conn profile-id file-id))] - (-> (assoc $ :permissions {:read read? :edit edit?}) - (cond-> (not edit?) (dissoc :share-links))))) - (some? slink) (assoc :share slink) @@ -97,61 +81,3 @@ (-> data (update :pages (fn [pages] (filterv #(contains? allowed-pages %) pages))) (update :pages-index (fn [index] (select-keys index allowed-pages))))))))))) - -;; --- Query: Viewer Bundle (by Page ID) - -;; DEPRECATED: should be removed in 1.9.x - -(declare check-shared-token!) -(declare retrieve-shared-token) - -(s/def ::id ::us/uuid) -(s/def ::page-id ::us/uuid) -(s/def ::token ::us/string) - -(s/def ::viewer-bundle - (s/keys :req-un [::file-id ::page-id] - :opt-un [::profile-id ::token])) - -(sv/defmethod ::viewer-bundle {:auth false} - [{:keys [pool] :as cfg} {:keys [profile-id file-id page-id token] :as params}] - (db/with-atomic [conn pool] - (let [cfg (assoc cfg :conn conn) - file (files/retrieve-file cfg file-id) - project (retrieve-project conn (:project-id file)) - page (get-in file [:data :pages-index page-id]) - file (merge (dissoc file :data) - (select-keys (:data file) [:colors :media :typographies])) - libs (files/retrieve-file-libraries cfg false file-id) - users (teams/retrieve-users conn (:team-id project)) - - fonts (db/query conn :team-font-variant - {:team-id (:team-id project) - :deleted-at nil}) - - bundle {:file file - :page page - :users users - :fonts fonts - :project project - :libraries libs}] - - (if (string? token) - (do - (check-shared-token! conn file-id page-id token) - (assoc bundle :token token)) - (let [stoken (retrieve-shared-token conn file-id page-id)] - (files/check-read-permissions! conn profile-id file-id) - (assoc bundle :token (:token stoken))))))) - -(defn check-shared-token! - [conn file-id page-id token] - (let [sql "select exists(select 1 from file_share_token where file_id=? and page_id=? and token=?) as exists"] - (when-not (:exists (db/exec-one! conn [sql file-id page-id token])) - (ex/raise :type :not-found - :code :object-not-found)))) - -(defn retrieve-shared-token - [conn file-id page-id] - (let [sql "select * from file_share_token where file_id=? and page_id=?"] - (db/exec-one! conn [sql file-id page-id]))) diff --git a/backend/src/app/srepl.clj b/backend/src/app/srepl.clj index 9d7c6c825..e71a2ac26 100644 --- a/backend/src/app/srepl.clj +++ b/backend/src/app/srepl.clj @@ -7,9 +7,9 @@ (ns app.srepl "Server Repl." (:require + [app.common.logging :as l] [app.common.spec :as us] [app.srepl.main] - [app.util.logging :as l] [clojure.core.server :as ccs] [clojure.main :as cm] [clojure.spec.alpha :as s] diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj index e6acb0776..a06c228e5 100644 --- a/backend/src/app/storage.clj +++ b/backend/src/app/storage.clj @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.logging :as l] [app.common.spec :as us] [app.common.uuid :as uuid] [app.db :as db] @@ -16,7 +17,6 @@ [app.storage.fs :as sfs] [app.storage.impl :as impl] [app.storage.s3 :as ss3] - [app.util.logging :as l] [app.util.time :as dt] [app.worker :as wrk] [clojure.spec.alpha :as s] diff --git a/backend/src/app/tasks/delete_object.clj b/backend/src/app/tasks/delete_object.clj deleted file mode 100644 index b1b3e701b..000000000 --- a/backend/src/app/tasks/delete_object.clj +++ /dev/null @@ -1,70 +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/. -;; -;; Copyright (c) UXBOX Labs SL - -;; TODO: DEPRECATED -;; Should be removed in the 1.8.x - -(ns app.tasks.delete-object - "Generic task for permanent deletion of objects." - (:require - [app.common.data :as d] - [app.common.spec :as us] - [app.db :as db] - [app.storage :as sto] - [app.util.logging :as l] - [clojure.spec.alpha :as s] - [integrant.core :as ig])) - -(declare handle-deletion) - -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::db/pool ::sto/storage])) - -(defmethod ig/init-key ::handler - [_ {:keys [pool] :as cfg}] - (fn [{:keys [props] :as task}] - (us/verify ::props props) - (db/with-atomic [conn pool] - (let [cfg (assoc cfg :conn conn)] - (handle-deletion cfg props))))) - -(s/def ::type ::us/keyword) -(s/def ::id ::us/uuid) -(s/def ::props (s/keys :req-un [::id ::type])) - -(defmulti handle-deletion - (fn [_ props] (:type props))) - -(defmethod handle-deletion :default - [_cfg {:keys [type]}] - (l/warn :hint "no handler found" - :type (d/name type))) - -(defmethod handle-deletion :file - [{:keys [conn]} {:keys [id] :as props}] - (let [sql "delete from file where id=? and deleted_at is not null"] - (db/exec-one! conn [sql id]))) - -(defmethod handle-deletion :project - [{:keys [conn]} {:keys [id] :as props}] - (let [sql "delete from project where id=? and deleted_at is not null"] - (db/exec-one! conn [sql id]))) - -(defmethod handle-deletion :team - [{:keys [conn]} {:keys [id] :as props}] - (let [sql "delete from team where id=? and deleted_at is not null"] - (db/exec-one! conn [sql id]))) - -(defmethod handle-deletion :team-font-variant - [{:keys [conn storage]} {:keys [id] :as props}] - (let [font (db/get-by-id conn :team-font-variant id {:check-not-found false}) - storage (assoc storage :conn conn)] - (when (:deleted-at font) - (db/delete! conn :team-font-variant {:id id}) - (some->> (:woff1-file-id font) (sto/del-object storage)) - (some->> (:woff2-file-id font) (sto/del-object storage)) - (some->> (:otf-file-id font) (sto/del-object storage)) - (some->> (:ttf-file-id font) (sto/del-object storage))))) diff --git a/backend/src/app/tasks/delete_profile.clj b/backend/src/app/tasks/delete_profile.clj deleted file mode 100644 index 67a1733df..000000000 --- a/backend/src/app/tasks/delete_profile.clj +++ /dev/null @@ -1,79 +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/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.tasks.delete-profile - "Task for permanent deletion of profiles." - (:require - [app.common.spec :as us] - [app.db :as db] - [app.db.sql :as sql] - [app.util.logging :as l] - [clojure.spec.alpha :as s] - [integrant.core :as ig])) - -;; TODO: DEPRECATED -;; Should be removed in the 1.8.x - -(declare delete-profile-data) - -;; --- INIT - -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::db/pool])) - -;; This task is responsible to permanently delete a profile with all -;; the dependent data. As step (1) we delete all owned teams of the -;; profile (that will cause to delete all underlying projects, files, -;; file_media and mark to be deleted storage_object's used by team, -;; profile and files previously deleted. Then, finally as step (2) we -;; proceed to delete the profile row. -;; -;; The storage_objects marked as deleted will be deleted by the -;; corresponding garbage collector task. - -(s/def ::profile-id ::us/uuid) -(s/def ::props (s/keys :req-un [::profile-id])) - -(defmethod ig/init-key ::handler - [_ {:keys [pool] :as cfg}] - (fn [{:keys [props] :as task}] - (us/verify ::props props) - (db/with-atomic [conn pool] - (let [id (:profile-id props) - profile (db/exec-one! conn (sql/select :profile {:id id} {:for-update true}))] - (if (or (:is-demo profile) - (:deleted-at profile)) - (delete-profile-data conn id) - (l/warn :hint "profile does not match constraints for deletion" - :profile-id id)))))) - -;; --- IMPL - -(def ^:private sql:remove-owned-teams - "delete from team - where id in ( - select tpr.team_id - from team_profile_rel as tpr - where tpr.is_owner is true - and tpr.profile_id = ? - )") - -(defn- delete-teams - [conn profile-id] - (db/exec-one! conn [sql:remove-owned-teams profile-id])) - -(defn delete-profile - [conn profile-id] - (db/delete! conn :profile {:id profile-id})) - -(defn- delete-profile-data - [conn profile-id] - (l/debug :action "delete profile" - :profile-id profile-id) - (delete-teams conn profile-id) - (delete-profile conn profile-id) - true) - diff --git a/backend/src/app/tasks/file_media_gc.clj b/backend/src/app/tasks/file_media_gc.clj index aedcbc78b..2b8a13384 100644 --- a/backend/src/app/tasks/file_media_gc.clj +++ b/backend/src/app/tasks/file_media_gc.clj @@ -9,10 +9,10 @@ objects from files. A file is ellegible to be garbage collected after some period of inactivity (the default threshold is 72h)." (:require + [app.common.logging :as l] [app.common.pages.migrations :as pmg] [app.db :as db] [app.util.blob :as blob] - [app.util.logging :as l] [app.util.time :as dt] [clojure.spec.alpha :as s] [integrant.core :as ig])) diff --git a/backend/src/app/tasks/file_offload.clj b/backend/src/app/tasks/file_offload.clj index ee784d02d..a43afb8a9 100644 --- a/backend/src/app/tasks/file_offload.clj +++ b/backend/src/app/tasks/file_offload.clj @@ -7,11 +7,11 @@ (ns app.tasks.file-offload "A maintenance task that offloads file data to an external storage (S3)." (:require + [app.common.logging :as l] [app.common.spec :as us] [app.db :as db] [app.storage :as sto] [app.storage.impl :as simpl] - [app.util.logging :as l] [app.util.time :as dt] [clojure.spec.alpha :as s] [integrant.core :as ig])) diff --git a/backend/src/app/tasks/file_xlog_gc.clj b/backend/src/app/tasks/file_xlog_gc.clj index 3037a7d5b..b8dce2fa5 100644 --- a/backend/src/app/tasks/file_xlog_gc.clj +++ b/backend/src/app/tasks/file_xlog_gc.clj @@ -8,8 +8,8 @@ "A maintenance task that performs a garbage collection of the file change (transaction) log." (:require + [app.common.logging :as l] [app.db :as db] - [app.util.logging :as l] [app.util.time :as dt] [clojure.spec.alpha :as s] [integrant.core :as ig])) diff --git a/backend/src/app/tasks/objects_gc.clj b/backend/src/app/tasks/objects_gc.clj index 112524560..a3d06dcdd 100644 --- a/backend/src/app/tasks/objects_gc.clj +++ b/backend/src/app/tasks/objects_gc.clj @@ -8,11 +8,11 @@ "A maintenance task that performs a general purpose garbage collection of deleted objects." (:require + [app.common.logging :as l] [app.config :as cf] [app.db :as db] [app.storage :as sto] [app.storage.impl :as simpl] - [app.util.logging :as l] [app.util.time :as dt] [clojure.spec.alpha :as s] [cuerdas.core :as str] diff --git a/backend/src/app/tasks/tasks_gc.clj b/backend/src/app/tasks/tasks_gc.clj index a3560f0e1..788e29269 100644 --- a/backend/src/app/tasks/tasks_gc.clj +++ b/backend/src/app/tasks/tasks_gc.clj @@ -8,8 +8,8 @@ "A maintenance task that performs a cleanup of already executed tasks from the database table." (:require + [app.common.logging :as l] [app.db :as db] - [app.util.logging :as l] [app.util.time :as dt] [clojure.spec.alpha :as s] [integrant.core :as ig])) diff --git a/backend/src/app/util/logging.clj b/backend/src/app/util/logging.clj deleted file mode 100644 index 9a08e66b3..000000000 --- a/backend/src/app/util/logging.clj +++ /dev/null @@ -1,110 +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/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.util.logging - (:require - [clojure.pprint :refer [pprint]]) - (:import - org.apache.logging.log4j.Level - org.apache.logging.log4j.LogManager - org.apache.logging.log4j.Logger - org.apache.logging.log4j.ThreadContext - org.apache.logging.log4j.message.MapMessage - org.apache.logging.log4j.spi.LoggerContext)) - -(defn build-map-message - [m] - (let [message (MapMessage. (count m))] - (reduce-kv #(.with ^MapMessage %1 (name %2) %3) message m))) - -(defprotocol ILogger - (-enabled? [logger level]) - (-write! [logger level throwable message])) - -(def logger-context - (LogManager/getContext false)) - -(def logging-agent - (agent nil :error-mode :continue)) - -(defn get-logger - [lname] - (.getLogger ^LoggerContext logger-context ^String lname)) - -(defn get-level - [level] - (case level - :trace Level/TRACE - :debug Level/DEBUG - :info Level/INFO - :warn Level/WARN - :error Level/ERROR - :fatal Level/FATAL)) - -(defn enabled? - [logger level] - (.isEnabled ^Logger logger ^Level level)) - -(defn write-log! - [logger level e msg] - (if e - (.log ^Logger logger - ^Level level - ^Object msg - ^Throwable e) - (.log ^Logger logger - ^Level level - ^Object msg))) - -(defmacro log - [& {:keys [level cause ::logger ::async ::raw] :as props}] - (let [props (dissoc props :level :cause ::logger ::async ::raw) - logger (or logger (str *ns*)) - logger-sym (gensym "log") - level-sym (gensym "log")] - `(let [~logger-sym (get-logger ~logger) - ~level-sym (get-level ~level)] - (if (enabled? ~logger-sym ~level-sym) - ~(if async - `(send-off logging-agent - (fn [_#] - (let [message# (or ~raw (build-map-message ~props))] - (write-log! ~logger-sym ~level-sym ~cause message#)))) - `(let [message# (or ~raw (build-map-message ~props))] - (write-log! ~logger-sym ~level-sym ~cause message#))))))) - -(defmacro info - [& params] - `(log :level :info ~@params)) - -(defmacro error - [& params] - `(log :level :error ~@params)) - -(defmacro warn - [& params] - `(log :level :warn ~@params)) - -(defmacro debug - [& params] - `(log :level :debug ~@params)) - -(defmacro trace - [& params] - `(log :level :trace ~@params)) - -(defn update-thread-context! - [data] - (run! (fn [[key val]] - (ThreadContext/put - (name key) - (cond - (coll? val) - (binding [clojure.pprint/*print-right-margin* 120] - (with-out-str (pprint val))) - (instance? clojure.lang.Named val) (name val) - :else (str val)))) - data)) diff --git a/backend/src/app/util/migrations.clj b/backend/src/app/util/migrations.clj index f79c50b2b..7b7fb8a44 100644 --- a/backend/src/app/util/migrations.clj +++ b/backend/src/app/util/migrations.clj @@ -6,7 +6,7 @@ (ns app.util.migrations (:require - [app.util.logging :as l] + [app.common.logging :as l] [clojure.java.io :as io] [clojure.spec.alpha :as s] [next.jdbc :as jdbc])) diff --git a/backend/src/app/util/services.clj b/backend/src/app/util/services.clj index 1621ad760..9faa8adcb 100644 --- a/backend/src/app/util/services.clj +++ b/backend/src/app/util/services.clj @@ -7,21 +7,34 @@ (ns app.util.services "A helpers and macros for define rpc like registry based services." (:refer-clojure :exclude [defmethod]) - (:require [app.common.data :as d])) + (:require + [app.common.data :as d] + [cuerdas.core :as str])) (defmacro defmethod [sname & body] - (let [[mdata args body] (if (map? (first body)) - [(first body) (first (rest body)) (drop 2 body)] - [nil (first body) (rest body)]) - mdata (assoc mdata - ::spec sname - ::name (name sname)) + (let [[docs body] (if (string? (first body)) + [(first body) (rest body)] + [nil body]) + [mdata body] (if (map? (first body)) + [(first body) (rest body)] + [nil body]) - sym (symbol (str "sm$" (name sname)))] - `(do - (def ~sym (fn ~args ~@body)) - (reset-meta! (var ~sym) ~mdata)))) + [args body] (if (vector? (first body)) + [(first body) (rest body)] + [nil body])] + (when-not args + (throw (IllegalArgumentException. "Missing arguments on `defmethod` macro."))) + + (let [mdata (assoc mdata + ::docs (some-> docs str/<<-) + ::spec sname + ::name (name sname)) + + sym (symbol (str "sm$" (name sname)))] + `(do + (def ~sym (fn ~args ~@body)) + (reset-meta! (var ~sym) ~mdata))))) (def nsym-xf (comp diff --git a/backend/src/app/rpc/queries/svg.clj b/backend/src/app/util/svg.clj similarity index 79% rename from backend/src/app/rpc/queries/svg.clj rename to backend/src/app/util/svg.clj index 63c0b8aeb..9c3aab795 100644 --- a/backend/src/app/rpc/queries/svg.clj +++ b/backend/src/app/util/svg.clj @@ -4,13 +4,10 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.rpc.queries.svg +(ns app.util.svg (:require [app.common.exceptions :as ex] - [app.common.spec :as us] - [app.util.logging :as l] - [app.util.services :as sv] - [clojure.spec.alpha :as s] + [app.common.logging :as l] [clojure.xml :as xml] [cuerdas.core :as str]) (:import @@ -39,14 +36,6 @@ :hint "invalid svg file" :cause e)))) -(declare pre-process) - -(s/def ::data ::us/string) -(s/def ::parsed-svg (s/keys :req-un [::data])) - -(sv/defmethod ::parsed-svg - [_ {:keys [data] :as params}] - (->> data pre-process parse)) ;; --- PROCESSORS diff --git a/backend/src/app/worker.clj b/backend/src/app/worker.clj index ed9d1982f..49370e164 100644 --- a/backend/src/app/worker.clj +++ b/backend/src/app/worker.clj @@ -9,12 +9,12 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.logging :as l] [app.common.spec :as us] [app.common.uuid :as uuid] [app.db :as db] [app.metrics :as mtx] [app.util.async :as aa] - [app.util.logging :as l] [app.util.time :as dt] [clojure.core.async :as a] [clojure.spec.alpha :as s] diff --git a/backend/test/app/services_files_test.clj b/backend/test/app/services_files_test.clj index e22d8afc8..0fb5acc58 100644 --- a/backend/test/app/services_files_test.clj +++ b/backend/test/app/services_files_test.clj @@ -53,21 +53,6 @@ (t/is (= (:id data) (:id result))) (t/is (= (:name data) (:name result)))))) - (t/testing "query files (deprecated)" - (let [data {::th/type :files - :project-id proj-id - :profile-id (:id prof)} - out (th/query! data)] - - ;; (th/print-result! out) - (t/is (nil? (:error out))) - - (let [result (:result out)] - (t/is (= 1 (count result))) - (t/is (= file-id (get-in result [0 :id]))) - (t/is (= "new name" (get-in result [0 :name]))) - (t/is (= 1 (count (get-in result [0 :data :pages]))))))) - (t/testing "query files" (let [data {::th/type :project-files :project-id proj-id @@ -120,7 +105,7 @@ (t/is (= (:type error-data) :not-found))))) (t/testing "query list files after delete" - (let [data {::th/type :files + (let [data {::th/type :project-files :project-id proj-id :profile-id (:id prof)} out (th/query! data)] diff --git a/backend/test/app/services_profile_test.clj b/backend/test/app/services_profile_test.clj index d5e6dac43..b51bcd8c0 100644 --- a/backend/test/app/services_profile_test.clj +++ b/backend/test/app/services_profile_test.clj @@ -89,7 +89,7 @@ ;; (th/print-result! out) (t/is (nil? (:error out))) - (t/is (nil? (:result out))))) + (t/is (map? (:result out))))) (t/testing "query profile after update" (let [data {::th/type :profile @@ -136,7 +136,7 @@ (t/is (nil? (:error out)))) ;; query files after profile soft deletion - (let [params {::th/type :files + (let [params {::th/type :project-files :project-id (:default-project-id prof) :profile-id (:id prof)} out (th/query! params)] @@ -177,17 +177,6 @@ (t/is (string? token)) - ;; try register without accepting terms - (let [data {::th/type :register-profile - :token token - :fullname "foobar" - :accept-terms-and-privacy false} - out (th/mutation! data)] - (let [error (:error out)] - (t/is (th/ex-info? error)) - (t/is (th/ex-of-type? error :validation)) - (t/is (th/ex-of-code? error :invalid-terms-and-privacy)))) - ;; try register without token (let [data {::th/type :register-profile :fullname "foobar" @@ -205,16 +194,11 @@ :accept-terms-and-privacy true :accept-newsletter-subscription true}] (let [{:keys [result error]} (th/mutation! data)] - (t/is (nil? error)) - (t/is (true? (get-in result [:props :accept-newsletter-subscription]))) - (t/is (true? (get-in result [:props :accept-terms-and-privacy]))))) + (t/is (nil? error)))) )) (t/deftest prepare-register-with-registration-disabled - (with-mocks [mock {:target 'app.config/get - :return (th/mock-config-get-with - {:registration-enabled false})}] - + (th/with-mocks {#'app.config/flags nil} (let [data {::th/type :prepare-register-profile :email "user@example.com" :password "foobar"}] diff --git a/backend/test/app/services_teams_test.clj b/backend/test/app/services_teams_test.clj index 2831ea4ee..6dd7470da 100644 --- a/backend/test/app/services_teams_test.clj +++ b/backend/test/app/services_teams_test.clj @@ -33,11 +33,13 @@ :role :editor :profile-id (:id profile1)}] - ;; (th/print-result! out) ;; invite external user without complaints (let [data (assoc data :email "foo@bar.com") out (th/mutation! data)] + + ;; (th/print-result! out) + (t/is (nil? (:result out))) (t/is (= 1 (:call-count (deref mock))))) @@ -111,6 +113,7 @@ :id (:id team) :profile-id (:id profile1)} out (th/mutation! params)] + ;; (th/print-result! out) (t/is (nil? (:error out)))) ;; query the list of teams after soft deletion @@ -133,7 +136,6 @@ :profile-id (:id profile1)} out (th/query! data)] ;; (th/print-result! out) - (t/is (nil? (:error out))) (let [result (:result out)] (t/is (= 0 (count result))))) diff --git a/backend/test/app/test_helpers.clj b/backend/test/app/test_helpers.clj index ba8999123..43334b883 100644 --- a/backend/test/app/test_helpers.clj +++ b/backend/test/app/test_helpers.clj @@ -7,6 +7,7 @@ (ns app.test-helpers (:require [app.common.data :as d] + [app.common.flags :as flags] [app.common.pages :as cp] [app.common.spec :as us] [app.common.uuid :as uuid] @@ -336,9 +337,15 @@ [data] (fn ([key] - (get data key (get @cf/config key))) + (get data key (get cf/config key))) ([key default] - (get data key (get @cf/config key default))))) + (get data key (get cf/config key default))))) + + +(defmacro with-mocks + [rebinds & body] + `(with-redefs-fn ~rebinds + (fn [] ~@body))) (defn reset-mock! [m] diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 3d227cb38..77dcae010 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -69,6 +69,11 @@ (next colls)) (persistent! result)))) +(defn preconj + [coll elem] + (assert (vector? coll)) + (concat [elem] coll)) + (defn enumerate ([items] (enumerate items 0)) ([items start] @@ -154,6 +159,11 @@ ([mfn coll] (into {} (mapm mfn) coll))) +(defn removev + "Returns a vector of the items in coll for which (fn item) returns logical false" + [fn coll] + (filterv (comp not fn) coll)) + (defn filterm "Filter values of a map that satisfy a predicate" [pred coll] diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc index 5818578a9..3371dbbd9 100644 --- a/common/src/app/common/exceptions.cljc +++ b/common/src/app/common/exceptions.cljc @@ -27,7 +27,10 @@ [& {:keys [message hint cause] :as params}] (s/assert ::error-params params) (let [message (or message hint "") - payload (dissoc params :cause)] + payload (-> params + (dissoc :cause) + (dissoc :message) + (assoc :hint message))] (ex-info message payload cause))) (defmacro raise diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index 4a497cda5..0ae1127df 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -278,6 +278,48 @@ (-> file (update :parent-stack pop)))) +(defn add-bool [file data] + (let [frame-id (:current-frame-id file) + name (:name data) + obj (-> {:id (uuid/next) + :type :bool + :name name + :shapes [] + :frame-id frame-id} + (merge data) + (check-name file :bool) + (d/without-nils))] + (-> file + (commit-shape obj) + (assoc :last-id (:id obj)) + (add-name (:name obj)) + (update :parent-stack conjv (:id obj))))) + +(defn close-bool [file] + (let [bool-id (-> file :parent-stack peek) + bool (lookup-shape file bool-id) + children (->> bool :shapes (mapv #(lookup-shape file %))) + + file + (let [objects (lookup-objects file) + bool' (gsh/update-bool-selrect bool children objects)] + (commit-change + file + {:type :mod-obj + :id bool-id + :operations + [{:type :set :attr :selrect :val (:selrect bool')} + {:type :set :attr :points :val (:points bool')} + {:type :set :attr :x :val (-> bool' :selrect :x)} + {:type :set :attr :y :val (-> bool' :selrect :y)} + {:type :set :attr :width :val (-> bool' :selrect :width)} + {:type :set :attr :height :val (-> bool' :selrect :height)}]} + + {:add-container? true}))] + + (-> file + (update :parent-stack pop)))) + (defn create-shape [file type data] (let [frame-id (:current-frame-id file) frame (when-not (= frame-id root-frame) @@ -332,21 +374,76 @@ (-> file (update :parent-stack pop))) +(defn- read-classifier + [interaction-src] + (select-keys interaction-src [:event-type :action-type])) + +(defmulti read-event-opts :event-type) + +(defmethod read-event-opts :after-delay + [interaction-src] + (select-keys interaction-src [:delay])) + +(defmethod read-event-opts :default + [_] + {}) + +(defmulti read-action-opts :action-type) + +(defmethod read-action-opts :navigate + [interaction-src] + (select-keys interaction-src [:destination])) + +(defmethod read-action-opts :open-overlay + [interaction-src] + (select-keys interaction-src [:destination + :overlay-position + :overlay-pos-type + :close-click-outside + :background-overlay])) + +(defmethod read-action-opts :toggle-overlay + [interaction-src] + (select-keys interaction-src [:destination + :overlay-position + :overlay-pos-type + :close-click-outside + :background-overlay])) + +(defmethod read-action-opts :close-overlay + [interaction-src] + (select-keys interaction-src [:destination])) + +(defmethod read-action-opts :prev-screen + [_] + {}) + +(defmethod read-action-opts :open-url + [interaction-src] + (select-keys interaction-src [:url])) + (defn add-interaction - [file from-id {:keys [action-type event-type destination]}] + [file from-id interaction-src] (assert (some? (lookup-shape file from-id)) (str "Cannot locate shape with id " from-id)) - (assert (some? (lookup-shape file destination)) (str "Cannot locate shape with id " destination)) - (let [interactions (->> (lookup-shape file from-id) - :interactions - (filterv #(or (not= (:action-type %) action-type) - (not= (:event-type %) event-type)))) - interactions (-> interactions + (let [{:keys [event-type action-type]} (read-classifier interaction-src) + {:keys [delay]} (read-event-opts interaction-src) + {:keys [destination overlay-pos-type overlay-position url + close-click-outside background-overlay]} (read-action-opts interaction-src) + + interactions (-> (lookup-shape file from-id) + :interactions (conjv - {:action-type action-type - :event-type event-type - :destination destination}))] + (d/without-nils {:event-type event-type + :action-type action-type + :delay delay + :destination destination + :overlay-pos-type overlay-pos-type + :overlay-position overlay-position + :url url + :close-click-outside close-click-outside + :background-overlay background-overlay})))] (commit-change file {:type :mod-obj diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index a5a051f7c..acc1d2c5c 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -9,24 +9,31 @@ (:require [cuerdas.core :as str])) +(def default + #{:backend-asserts + :api-doc + :registration + :demo-users}) + (defn parse - [default flags] - (loop [flags (seq flags) - result default] - (let [item (first flags)] - (if (nil? item) - result - (let [sname (name item)] - (cond - (str/starts-with? sname "enable-") - (recur (rest flags) - (conj result (keyword (subs sname 7)))) + ([flags] (parse flags #{})) + ([flags default] + (loop [flags (seq flags) + result default] + (let [item (first flags)] + (if (nil? item) + result + (let [sname (name item)] + (cond + (str/starts-with? sname "enable-") + (recur (rest flags) + (conj result (keyword (subs sname 7)))) - (str/starts-with? sname "disable-") - (recur (rest flags) - (disj result (keyword (subs sname 8)))) + (str/starts-with? sname "disable-") + (recur (rest flags) + (disj result (keyword (subs sname 8)))) - :else - (recur (rest flags) result))))))) + :else + (recur (rest flags) result)))))))) diff --git a/common/src/app/common/geom/point.cljc b/common/src/app/common/geom/point.cljc index 0d3feeb06..b73a050e6 100644 --- a/common/src/app/common/geom/point.cljc +++ b/common/src/app/common/geom/point.cljc @@ -22,7 +22,8 @@ (defn ^boolean point? "Return true if `v` is Point instance." [v] - (instance? Point v)) + (or (instance? Point v) + (and (map? v) (contains? v :x) (contains? v :y)))) (defn ^boolean point-like? [{:keys [x y] :as v}] @@ -257,15 +258,12 @@ (and (mth/almost-zero? x) (mth/almost-zero? y))) -(defn line-val - "Given a line with two points p1-p2 and a 'percent'. Returns the point in the vector - generated by these two points. For example: for p1=(0,0) p2=(1,1) and v=0.25 will return - the point (0.25, 0.25)" - [p1 p2 v] - (let [v (-> (to-vec p1 p2) - (scale v))] - (add p1 v))) - +(defn lerp + "Calculates a linear interpolation between two points given a tvalue" + [p1 p2 t] + (let [x (mth/lerp (:x p1) (:x p2) t) + y (mth/lerp (:y p1) (:y p2) t)] + (point x y))) (defn rotate "Rotates the point around center with an angle" diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 8771ff2e3..91a7043ba 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -8,11 +8,13 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] + [app.common.geom.shapes.bool :as gsb] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.intersect :as gin] [app.common.geom.shapes.path :as gsp] [app.common.geom.shapes.rect :as gpr] - [app.common.geom.shapes.transforms :as gtr])) + [app.common.geom.shapes.transforms :as gtr] + [app.common.math :as mth])) ;; --- Setup (Initialize) ;; FIXME: Is this the correct place for these functions? @@ -126,6 +128,13 @@ (assoc :selrect selrect :points points)))) +(defn shape-stroke-margin + [shape stroke-width] + (if (= (:type shape) :path) + ;; TODO: Calculate with the stroke offset (not implemented yet + (mth/sqrt (* 2 stroke-width stroke-width)) + (- (mth/sqrt (* 2 stroke-width stroke-width)) stroke-width))) + ;; EXPORTS (d/export gco/center-shape) @@ -133,19 +142,20 @@ (d/export gco/center-rect) (d/export gco/center-points) (d/export gco/make-centered-rect) +(d/export gco/transform-points) (d/export gpr/rect->selrect) (d/export gpr/rect->points) (d/export gpr/points->selrect) (d/export gpr/points->rect) (d/export gpr/center->rect) +(d/export gpr/join-rects) (d/export gtr/move) (d/export gtr/absolute-move) (d/export gtr/transform-matrix) (d/export gtr/inverse-transform-matrix) (d/export gtr/transform-point-center) -(d/export gtr/transform-points) (d/export gtr/transform-rect) (d/export gtr/calculate-adjust-matrix) (d/export gtr/update-group-selrect) @@ -156,12 +166,15 @@ (d/export gtr/calc-child-modifiers) ;; PATHS -(d/export gsp/content->points) (d/export gsp/content->selrect) (d/export gsp/transform-content) +(d/export gsp/open-path?) ;; Intersection (d/export gin/overlaps?) (d/export gin/has-point?) (d/export gin/has-point-rect?) (d/export gin/rect-contains-shape?) + +;; Bool +(d/export gsb/update-bool-selrect) diff --git a/common/src/app/common/geom/shapes/bool.cljc b/common/src/app/common/geom/shapes/bool.cljc new file mode 100644 index 000000000..93b7ccc72 --- /dev/null +++ b/common/src/app/common/geom/shapes/bool.cljc @@ -0,0 +1,32 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.geom.shapes.bool + (:require + [app.common.geom.shapes.path :as gsp] + [app.common.geom.shapes.rect :as gpr] + [app.common.geom.shapes.transforms :as gtr] + [app.common.path.bool :as pb] + [app.common.path.shapes-to-path :as stp])) + +(defn update-bool-selrect + "Calculates the selrect+points for the boolean shape" + [shape children objects] + + (let [content (->> children + (map #(stp/convert-to-path % objects)) + (mapv :content) + (pb/content-bool (:bool-type shape))) + + [points selrect] + (if (empty? content) + (let [selrect (gtr/selection-rect children) + points (gpr/rect->points selrect)] + [points selrect]) + (gsp/content->points+selrect shape content))] + (-> shape + (assoc :selrect selrect) + (assoc :points points)))) diff --git a/common/src/app/common/geom/shapes/common.cljc b/common/src/app/common/geom/shapes/common.cljc index 9b5c6d1b3..00eec4386 100644 --- a/common/src/app/common/geom/shapes/common.cljc +++ b/common/src/app/common/geom/shapes/common.cljc @@ -6,6 +6,7 @@ (ns app.common.geom.shapes.common (:require + [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.math :as mth])) @@ -48,3 +49,14 @@ :y (- (:y center) (/ height 2.0)) :width width :height height}) + +(defn transform-points + ([points matrix] + (transform-points points nil matrix)) + ([points center matrix] + (let [prev (if center (gmt/translate-matrix center) (gmt/matrix)) + post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix)) + + tr-point (fn [point] + (gpt/transform point (gmt/multiply prev matrix post)))] + (mapv tr-point points)))) diff --git a/common/src/app/common/geom/shapes/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc index 4b0593dc1..3cc358965 100644 --- a/common/src/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -284,12 +284,19 @@ (defn overlaps? "General case to check for overlaping between shapes and a rectangle" [shape rect] - (or (not shape) - (let [path? (= :path (:type shape)) - circle? (= :circle (:type shape))] - (and (overlaps-rect-points? rect (:points shape)) - (or (not path?) (overlaps-path? shape rect)) - (or (not circle?) (overlaps-ellipse? shape rect)))))) + (let [stroke-width (/ (or (:stroke-width shape) 0) 2) + rect (-> rect + (update :x - stroke-width) + (update :y - stroke-width) + (update :width + (* 2 stroke-width)) + (update :height + (* 2 stroke-width)) + )] + (or (not shape) + (let [path? (= :path (:type shape)) + circle? (= :circle (:type shape))] + (and (overlaps-rect-points? rect (:points shape)) + (or (not path?) (overlaps-path? shape rect)) + (or (not circle?) (overlaps-ellipse? shape rect))))))) (defn has-point-rect? [rect point] @@ -308,3 +315,4 @@ (->> shape :points (every? (partial has-point-rect? rect)))) + diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index ff39fa7db..d8bd67cfc 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -7,97 +7,276 @@ (ns app.common.geom.shapes.path (:require [app.common.data :as d] + [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gsc] [app.common.geom.shapes.rect :as gpr] - [app.common.math :as mth])) + [app.common.math :as mth] + [app.common.path.commands :as upc] + [app.common.path.subpaths :as sp])) -(defn content->points [content] +(def ^:const curve-curve-precision 0.1) +(def ^:const curve-range-precision 2) + +(defn s= [a b] + (mth/almost-zero? (- a b))) + +(defn calculate-opposite-handler + "Given a point and its handler, gives the symetric handler" + [point handler] + (let [handler-vector (gpt/to-vec point handler)] + (gpt/add point (gpt/negate handler-vector)))) + +(defn opposite-handler + "Calculates the coordinates of the opposite handler" + [point handler] + (let [phv (gpt/to-vec point handler)] + (gpt/add point (gpt/negate phv)))) + +(defn opposite-handler-keep-distance + "Calculates the coordinates of the opposite handler but keeping the old distance" + [point handler old-opposite] + (let [old-distance (gpt/distance point old-opposite) + phv (gpt/to-vec point handler) + phv2 (gpt/multiply + (gpt/unit (gpt/negate phv)) + (gpt/point old-distance))] + (gpt/add point phv2))) + +(defn content->points + "Returns the points in the given content" + [content] (->> content - (map #(when (-> % :params :x) (gpt/point (-> % :params :x) (-> % :params :y)))) + (map #(when (-> % :params :x) + (gpt/point (-> % :params :x) (-> % :params :y)))) (remove nil?) (into []))) +(defn line-values + [[from-p to-p] t] + (let [move-v (-> (gpt/to-vec from-p to-p) + (gpt/scale t))] + (gpt/add from-p move-v))) + +(defn line-windup + [[from-p to-p :as l] t] + (let [p (line-values l t) + cy (:y p) + ay (:y to-p) + by (:y from-p)] + (cond + (and (> (- cy ay) 0) (not (s= cy ay))) 1 + (and (< (- cy ay) 0) (not (s= cy ay))) -1 + (< (- cy by) 0) 1 + (> (- cy by) 0) -1 + :else 0))) + ;; https://medium.com/@Acegikmo/the-ever-so-lovely-b%C3%A9zier-curve-eb27514da3bf ;; https://en.wikipedia.org/wiki/Bernstein_polynomial (defn curve-values "Parametric equation for cubic beziers. Given a start and end and two intermediate points returns points for values of t. If you draw t on a plane you got the bezier cube" - [start end h1 h2 t] + ([[start end h1 h2] t] + (curve-values start end h1 h2 t)) - (let [t2 (* t t) ;; t square - t3 (* t2 t) ;; t cube + ([start end h1 h2 t] + (let [t2 (* t t) ;; t square + t3 (* t2 t) ;; t cube - start-v (+ (- t3) (* 3 t2) (* -3 t) 1) - h1-v (+ (* 3 t3) (* -6 t2) (* 3 t)) - h2-v (+ (* -3 t3) (* 3 t2)) - end-v t3 + start-v (+ (- t3) (* 3 t2) (* -3 t) 1) + h1-v (+ (* 3 t3) (* -6 t2) (* 3 t)) + h2-v (+ (* -3 t3) (* 3 t2)) + end-v t3 - coord-v (fn [coord] - (+ (* (coord start) start-v) - (* (coord h1) h1-v) - (* (coord h2) h2-v) - (* (coord end) end-v)))] + coord-v (fn [coord] + (+ (* (coord start) start-v) + (* (coord h1) h1-v) + (* (coord h2) h2-v) + (* (coord end) end-v)))] - (gpt/point (coord-v :x) (coord-v :y)))) + (gpt/point (coord-v :x) (coord-v :y))))) + +(defn curve-tangent + "Retrieve the tangent vector to the curve in the point `t`" + [[start end h1 h2] t] + + (let [coords [[(:x start) (:x h1) (:x h2) (:x end)] + [(:y start) (:y h1) (:y h2) (:y end)]] + + solve-derivative + (fn [[c0 c1 c2 c3]] + ;; Solve B'(t) given t to retrieve the value for the + ;; first derivative + (let [t2 (* t t)] + (+ (* c0 (+ (* -3 t2) (* 6 t) -3)) + (* c1 (+ (* 9 t2) (* -12 t) 3)) + (* c2 (+ (* -9 t2) (* 6 t))) + (* c3 (* 3 t2))))) + + [x y] (->> coords (mapv solve-derivative)) + + ;; normalize value + d (mth/sqrt (+ (* x x) (* y y)))] + + (gpt/point (/ x d) (/ y d)))) + +(defn curve-windup + [curve t] + + (let [tangent (curve-tangent curve t)] + (cond + (> (:y tangent) 0) -1 + (< (:y tangent) 0) 1 + :else 0))) (defn curve-split "Splits a curve into two at the given parametric value `t`. Calculates the Casteljau's algorithm intermediate points" - [start end h1 h2 t] + ([[start end h1 h2] t] + (curve-split start end h1 h2 t)) - (let [p1 (gpt/line-val start h1 t) - p2 (gpt/line-val h1 h2 t) - p3 (gpt/line-val h2 end t) - p4 (gpt/line-val p1 p2 t) - p5 (gpt/line-val p2 p3 t) - sp (gpt/line-val p4 p5 t)] - [[start sp p1 p4] - [sp end p5 p3]])) + ([start end h1 h2 t] + (let [p1 (gpt/lerp start h1 t) + p2 (gpt/lerp h1 h2 t) + p3 (gpt/lerp h2 end t) + p4 (gpt/lerp p1 p2 t) + p5 (gpt/lerp p2 p3 t) + sp (gpt/lerp p4 p5 t)] + [[start sp p1 p4] + [sp end p5 p3]]))) + +(defn subcurve-range + "Given a curve returns a new curve between the values t1-t2" + ([[start end h1 h2] [t1 t2]] + (subcurve-range start end h1 h2 t1 t2)) + + ([[start end h1 h2] t1 t2] + (subcurve-range start end h1 h2 t1 t2)) + + ([start end h1 h2 t1 t2] + ;; Make sure that t2 is greater than t1 + (let [[t1 t2] (if (< t1 t2) [t1 t2] [t2 t1]) + t2' (/ (- t2 t1) (- 1 t1)) + [_ curve'] (curve-split start end h1 h2 t1)] + (first (curve-split curve' t2'))))) + + +;; https://trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm +(defn- solve-roots + "Solvers a quadratic or cubic equation given by the parameters a b c d" + ([a b c] + (solve-roots a b c 0)) + + ([a b c d] + (let [sqrt-b2-4ac (mth/sqrt (- (* b b) (* 4 a c)))] + (cond + ;; No solutions + (and (mth/almost-zero? d) (mth/almost-zero? a) (mth/almost-zero? b)) + [] + + ;; Linear solution + (and (mth/almost-zero? d) (mth/almost-zero? a)) + [(/ (- c) b)] + + ;; Cuadratic + (mth/almost-zero? d) + [(/ (+ (- b) sqrt-b2-4ac) + (* 2 a)) + (/ (- (- b) sqrt-b2-4ac) + (* 2 a))] + + ;; Cubic + :else + (let [a (/ a d) + b (/ b d) + c (/ c d) + + p (/ (- (* 3 b) (* a a)) 3) + q (/ (+ (* 2 a a a) (* -9 a b) (* 27 c)) 27) + + p3 (/ p 3) + q2 (/ q 2) + discriminant (+ (* q2 q2) (* p3 p3 p3))] + + (cond + (< discriminant 0) + (let [mp3 (/ (- p) 3) + mp33 (* mp3 mp3 mp3) + r (mth/sqrt mp33) + t (/ (- q) (* 2 r)) + cosphi (cond (< t -1) -1 + (> t 1) 1 + :else t) + phi (mth/acos cosphi) + crtr (mth/cubicroot r) + t1 (* 2 crtr) + root1 (- (* t1 (mth/cos (/ phi 3))) (/ a 3)) + root2 (- (* t1 (mth/cos (/ (+ phi (* 2 mth/PI)) 3))) (/ a 3)) + root3 (- (* t1 (mth/cos (/ (+ phi (* 4 mth/PI)) 3))) (/ a 3))] + + [root1 root2 root3]) + + (mth/almost-zero? discriminant) + (let [u1 (if (< q2 0) (mth/cubicroot (- q2)) (- (mth/cubicroot q2))) + root1 (- (* 2 u1) (/ a 3)) + root2 (- (- u1) (/ a 3))] + [root1 root2]) + + :else + (let [sd (mth/sqrt discriminant) + u1 (mth/cubicroot (- sd q2)) + v1 (mth/cubicroot (+ sd q2)) + root (- u1 v1 (/ a 3))] + [root]))))))) ;; https://pomax.github.io/bezierinfo/#extremities (defn curve-extremities - "Given a cubic bezier cube finds its roots in t. This are the extremities - if we calculate its values for x, y we can find a bounding box for the curve." - [start end h1 h2] + "Calculates the extremities by solving the first derivative for a cubic + bezier and then solving the quadratic formula" + ([[start end h1 h2]] + (curve-extremities start end h1 h2)) - (let [coords [[(:x start) (:x h1) (:x h2) (:x end)] - [(:y start) (:y h1) (:y h2) (:y end)]] + ([start end h1 h2] - coord->tvalue - (fn [[c0 c1 c2 c3]] + (let [coords [[(:x start) (:x h1) (:x h2) (:x end)] + [(:y start) (:y h1) (:y h2) (:y end)]] - (let [a (+ (* -3 c0) (* 9 c1) (* -9 c2) (* 3 c3)) - b (+ (* 6 c0) (* -12 c1) (* 6 c2)) - c (+ (* 3 c1) (* -3 c0)) + coord->tvalue + (fn [[c0 c1 c2 c3]] + (let [a (+ (* -3 c0) (* 9 c1) (* -9 c2) (* 3 c3)) + b (+ (* 6 c0) (* -12 c1) (* 6 c2)) + c (+ (* 3 c1) (* -3 c0))] - sqrt-b2-4ac (mth/sqrt (- (* b b) (* 4 a c)))] + (solve-roots a b c)))] + (->> coords + (mapcat coord->tvalue) - (cond - (and (mth/almost-zero? a) - (not (mth/almost-zero? b))) - ;; When the term a is close to zero we have a linear equation - [(/ (- c) b)] + ;; Only values in the range [0, 1] are valid + (filterv #(and (> % 0.01) (< % 0.99))))))) - ;; If a is not close to zero return the two roots for a cuadratic - (not (mth/almost-zero? a)) - [(/ (+ (- b) sqrt-b2-4ac) - (* 2 a)) - (/ (- (- b) sqrt-b2-4ac) - (* 2 a))] +(defn curve-roots + "Uses cardano algorithm to find the roots for a cubic bezier" + ([[start end h1 h2] coord] + (curve-roots start end h1 h2 coord)) - ;; If a and b close to zero we can't find a root for a constant term - :else - [])))] - (->> coords - (mapcat coord->tvalue) + ([start end h1 h2 coord] - ;; Only values in the range [0, 1] are valid - (filter #(and (>= % 0) (<= % 1))) + (let [coords [[(get start coord) (get h1 coord) (get h2 coord) (get end coord)]] - ;; Pass t-values to actual points - (map #(curve-values start end h1 h2 %))) - )) + coord->tvalue + (fn [[pa pb pc pd]] + + (let [a (+ (* 3 pa) (* -6 pb) (* 3 pc)) + b (+ (* -3 pa) (* 3 pb)) + c pa + d (+ (- pa) (* 3 pb) (* -3 pc) pd)] + + (solve-roots a b c d)))] + (->> coords + (mapcat coord->tvalue) + ;; Only values in the range [0, 1] are valid + (filterv #(and (>= % 0) (<= % 1))))))) (defn command->point ([command] (command->point command nil)) @@ -107,7 +286,50 @@ ykey (keyword (str prefix "y")) x (get params xkey) y (get params ykey)] - (gpt/point x y)))) + (when (and (some? x) (some? y)) + (gpt/point x y))))) + +(defn command->line + ([cmd] + (command->line cmd (:prev cmd))) + ([cmd prev] + [prev (command->point cmd)])) + +(defn command->bezier + ([cmd] + (command->bezier cmd (:prev cmd))) + ([cmd prev] + [prev + (command->point cmd) + (gpt/point (-> cmd :params :c1x) (-> cmd :params :c1y)) + (gpt/point (-> cmd :params :c2x) (-> cmd :params :c2y))])) + +(defn command->selrect + ([command] + (command->selrect command (:prev command))) + + ([command prev-point] + (let [points (case (:command command) + :move-to [(command->point command)] + + ;; If it's a line we add the beginning point and endpoint + :line-to [prev-point (command->point command)] + + ;; We return the bezier extremities + :curve-to (d/concat + [prev-point + (command->point command)] + (let [curve [prev-point + (command->point command) + (command->point command :c1) + (command->point command :c2)]] + (->> (curve-extremities curve) + (mapv #(curve-values curve %))))) + []) + selrect (gpr/points->selrect points)] + (-> selrect + (update :width #(if (mth/almost-zero? %) 1 %)) + (update :height #(if (mth/almost-zero? %) 1 %)))))) (defn content->selrect [content] (let [calc-extremities @@ -123,10 +345,12 @@ :curve-to (d/concat [(command->point prev) (command->point command)] - (curve-extremities (command->point prev) - (command->point command) - (command->point command :c1) - (command->point command :c2))) + (let [curve [(command->point prev) + (command->point command) + (command->point command :c1) + (command->point command :c2)]] + (->> (curve-extremities curve) + (mapv #(curve-values curve %))))) [])) extremities (mapcat calc-extremities @@ -154,7 +378,11 @@ (not (nil? c1x)) (set-tr :c1x :c1y) (not (nil? c2x)) (set-tr :c2x :c2y)))] - (mapv #(update % :params transform-params) content))) + (->> content + (mapv (fn [cmd] + (cond-> cmd + (map? cmd) + (update :params transform-params))))))) (defn transform-content [content transform] @@ -302,24 +530,25 @@ "Given a path and a position" [shape position] - (let [point+distance (fn [[cur-cmd prev-cmd]] - (let [from-p (command->point prev-cmd) - to-p (command->point cur-cmd) - h1 (gpt/point (get-in cur-cmd [:params :c1x]) - (get-in cur-cmd [:params :c1y])) - h2 (gpt/point (get-in cur-cmd [:params :c2x]) - (get-in cur-cmd [:params :c2y])) - point - (case (:command cur-cmd) - :line-to - (line-closest-point position from-p to-p) + (let [point+distance + (fn [[cur-cmd prev-cmd]] + (let [from-p (command->point prev-cmd) + to-p (command->point cur-cmd) + h1 (gpt/point (get-in cur-cmd [:params :c1x]) + (get-in cur-cmd [:params :c1y])) + h2 (gpt/point (get-in cur-cmd [:params :c2x]) + (get-in cur-cmd [:params :c2y])) + point + (case (:command cur-cmd) + :line-to + (line-closest-point position from-p to-p) - :curve-to - (curve-closest-point position from-p to-p h1 h2) + :curve-to + (curve-closest-point position from-p to-p h1 h2) - nil)] - (when point - [point (gpt/distance point position)]))) + nil)] + (when point + [point (gpt/distance point position)]))) find-min-point (fn [[min-p min-dist :as acc] [cur-p cur-dist :as cur]] (if (and (some? acc) (or (not cur) (<= min-dist cur-dist))) @@ -331,3 +560,399 @@ (map point+distance) (reduce find-min-point) (first)))) + +(defn- get-line-tval + [[{x1 :x y1 :y} {x2 :x y2 :y}] {:keys [x y]}] + (cond + (and (s= x1 x2) (s= y1 y2)) + ##Inf + + (s= x1 x2) + (/ (- y y1) (- y2 y1)) + + :else + (/ (- x x1) (- x2 x1)))) + +(defn- curve-range->rect + [curve from-t to-t] + + (let [[from-p to-p :as curve] (subcurve-range curve from-t to-t) + extremes (->> (curve-extremities curve) + (mapv #(curve-values curve %)))] + (gpr/points->rect (into [from-p to-p] extremes)))) + +(defn line-has-point? + "Using the line equation we put the x value and check if matches with + the given Y. If it does the point is inside the line" + [point [from-p to-p]] + (let [{x1 :x y1 :y} from-p + {x2 :x y2 :y} to-p + {px :x py :y} point + + m (when-not (s= x1 x2) (/ (- y2 y1) (- x2 x1))) + vy (when (some? m) (+ (* m px) (* (- m) x1) y1))] + + ;; If x1 = x2 there is no slope, to see if the point is in the line + ;; only needs to check the x is the same + (or (and (s= x1 x2) (s= px x1)) + (and (some? vy) (s= py vy))))) + +(defn segment-has-point? + "Using the line equation we put the x value and check if matches with + the given Y. If it does the point is inside the line" + [point line] + + (and (line-has-point? point line) + (let [t (get-line-tval line point)] + (and (or (> t 0) (s= t 0)) + (or (< t 1) (s= t 1)))))) + +(defn curve-has-point? + [point curve] + (letfn [(check-range [from-t to-t] + (let [r (curve-range->rect curve from-t to-t)] + (when (gpr/contains-point? r point) + (if (s= from-t to-t) + (< (gpt/distance (curve-values curve from-t) point) 0.1) + + (let [half-t (+ from-t (/ (- to-t from-t) 2.0))] + (or (check-range from-t half-t) + (check-range half-t to-t)))))))] + + (check-range 0 1))) + +(defn line-line-crossing + [[from-p1 to-p1 :as l1] [from-p2 to-p2 :as l2]] + + (let [{x1 :x y1 :y} from-p1 + {x2 :x y2 :y} to-p1 + + {x3 :x y3 :y} from-p2 + {x4 :x y4 :y} to-p2 + + nx (- (* (- x3 x4) (- (* x1 y2) (* y1 x2))) + (* (- x1 x2) (- (* x3 y4) (* y3 x4)))) + + ny (- (* (- y3 y4) (- (* x1 y2) (* y1 x2))) + (* (- y1 y2) (- (* x3 y4) (* y3 x4)))) + + d (- (* (- x1 x2) (- y3 y4)) + (* (- y1 y2) (- x3 x4)))] + + (cond + (not (mth/almost-zero? d)) + ;; Coordinates in the line. We calculate the tvalue that will + ;; return 0-1 as a percentage in the segment + (let [cross-p (gpt/point (/ nx d) (/ ny d)) + t1 (get-line-tval l1 cross-p) + t2 (get-line-tval l2 cross-p)] + [t1 t2]) + + ;; If they are parallels they could define the same line + (line-has-point? from-p2 l1) [(get-line-tval l1 from-p2) 0] + (line-has-point? to-p2 l1) [(get-line-tval l1 to-p2) 1] + (line-has-point? to-p1 l2) [1 (get-line-tval l2 to-p1)] + (line-has-point? from-p1 l2) [0 (get-line-tval l2 from-p1)] + + :else + nil))) + +(defn line-curve-crossing + [[from-p1 to-p1] + [from-p2 to-p2 h1-p2 h2-p2]] + + (let [theta (-> (mth/atan2 (- (:y to-p1) (:y from-p1)) + (- (:x to-p1) (:x from-p1))) + (mth/degrees)) + + transform (-> (gmt/matrix) + (gmt/rotate (- theta)) + (gmt/translate (gpt/negate from-p1))) + + c2' [(gpt/transform from-p2 transform) + (gpt/transform to-p2 transform) + (gpt/transform h1-p2 transform) + (gpt/transform h2-p2 transform)]] + + (curve-roots c2' :y))) + + + +(defn ray-line-intersect + [point [a b :as line]] + + ;; If the ray is paralell to the line there will be no crossings + (let [ray-line [point (gpt/point (inc (:x point)) (:y point))] + ;; Rays fail when fall just in a vertex so we move a bit upward + ;; because only want to use this for insideness + a (if (and (some? a) (s= (:y a) (:y point))) (update a :y + 10) a) + b (if (and (some? b) (s= (:y b) (:y point))) (update b :y + 10) b) + [ray-t line-t] (line-line-crossing ray-line [a b])] + + (when (and (some? line-t) (some? ray-t) + (> ray-t 0) + (or (> line-t 0) (s= line-t 0)) + (or (< line-t 1) (s= line-t 1))) + [[(line-values line line-t) + (line-windup line line-t)]]))) + +(defn line-line-intersect + [l1 l2] + + (let [[l1-t l2-t] (line-line-crossing l1 l2)] + (when (and (some? l1-t) (some? l2-t) + (or (> l1-t 0) (s= l1-t 0)) + (or (< l1-t 1) (s= l1-t 1)) + (or (> l2-t 0) (s= l2-t 0)) + (or (< l2-t 1) (s= l2-t 1))) + [[l1-t] [l2-t]]))) + +(defn ray-curve-intersect + [ray-line c2] + + (let [;; ray-line [point (gpt/point (inc (:x point)) (:y point))] + curve-ts (->> (line-curve-crossing ray-line c2) + (filterv #(let [curve-v (curve-values c2 %) + curve-tg (curve-tangent c2 %) + curve-tg-angle (gpt/angle curve-tg) + ray-t (get-line-tval ray-line curve-v)] + (and (> ray-t 0) + (> (mth/abs (- curve-tg-angle 180)) 0.01) + (> (mth/abs (- curve-tg-angle 0)) 0.01)) )))] + (->> curve-ts + (mapv #(vector (curve-values c2 %) + (curve-windup c2 %)))))) + +(defn line-curve-intersect + [l1 c2] + + (let [curve-ts (->> (line-curve-crossing l1 c2) + (filterv + (fn [curve-t] + (let [curve-t (if (mth/almost-zero? curve-t) 0 curve-t) + curve-v (curve-values c2 curve-t) + line-t (get-line-tval l1 curve-v)] + (and (>= curve-t 0) (<= curve-t 1) + (>= line-t 0) (<= line-t 1)))))) + + ;; Intersection line-curve points + intersect-ps (->> curve-ts + (mapv #(curve-values c2 %))) + + line-ts (->> intersect-ps + (mapv #(get-line-tval l1 %)))] + + [line-ts curve-ts])) + +(defn curve-curve-intersect + [c1 c2] + + (letfn [(check-range [c1-from c1-to c2-from c2-to] + (let [r1 (curve-range->rect c1 c1-from c1-to) + r2 (curve-range->rect c2 c2-from c2-to)] + + (when (gpr/overlaps-rects? r1 r2) + (let [p1 (curve-values c1 c1-from) + p2 (curve-values c2 c2-from)] + + (if (< (gpt/distance p1 p2) curve-curve-precision) + [{:p1 p1 + :p2 p2 + :d (gpt/distance p1 p2) + :t1 (mth/precision c1-from 4) + :t2 (mth/precision c2-from 4)}] + + (let [c1-half (+ c1-from (/ (- c1-to c1-from) 2)) + c2-half (+ c2-from (/ (- c2-to c2-from) 2)) + + ts-1 (check-range c1-from c1-half c2-from c2-half) + ts-2 (check-range c1-from c1-half c2-half c2-to) + ts-3 (check-range c1-half c1-to c2-from c2-half) + ts-4 (check-range c1-half c1-to c2-half c2-to)] + + (d/concat [] ts-1 ts-2 ts-3 ts-4))))))) + + (remove-close-ts [{cp1 :p1 cp2 :p2}] + (fn [{:keys [p1 p2]}] + (and (>= (gpt/distance p1 cp1) curve-range-precision) + (>= (gpt/distance p2 cp2) curve-range-precision)))) + + (process-ts [ts] + (loop [current (first ts) + pending (rest ts) + c1-ts [] + c2-ts []] + + (if (nil? current) + [c1-ts c2-ts] + + (let [pending (->> pending (filter (remove-close-ts current))) + c1-ts (conj c1-ts (:t1 current)) + c2-ts (conj c2-ts (:t2 current))] + (recur (first pending) + (rest pending) + c1-ts + c2-ts)))))] + + (->> (check-range 0 1 0 1) + (sort-by :d) + (process-ts)))) + +(defn curve->rect + [[from-p to-p :as curve]] + (let [extremes (->> (curve-extremities curve) + (mapv #(curve-values curve %)))] + (gpr/points->rect (into [from-p to-p] extremes)))) + + +(defn is-point-in-border? + [point content] + + (letfn [(inside-border? [cmd] + (case (:command cmd) + :line-to (segment-has-point? point (command->line cmd)) + :curve-to (curve-has-point? point (command->bezier cmd)) + #_:else false))] + + (->> content + (some inside-border?)))) + +(defn is-point-in-content? + [point content] + (let [selrect (content->selrect content) + ray-line [point (gpt/point (inc (:x point)) (:y point))] + + closed-content + (into [] + (comp (filter sp/is-closed?) + (mapcat :data)) + (->> content + (sp/close-subpaths) + (sp/get-subpaths))) + + cast-ray + (fn [cmd] + (case (:command cmd) + :line-to (ray-line-intersect point (command->line cmd)) + :curve-to (ray-curve-intersect ray-line (command->bezier cmd)) + #_:else []))] + + (and (gpr/contains-point? selrect point) + (->> closed-content + (mapcat cast-ray) + (map second) + (reduce +) + (not= 0))))) + +(defn split-line-to + "Given a point and a line-to command will create a two new line-to commands + that will split the original line into two given a value between 0-1" + [from-p cmd t-val] + (let [to-p (upc/command->point cmd) + sp (gpt/lerp from-p to-p t-val)] + [(upc/make-line-to sp) cmd])) + +(defn split-curve-to + "Given the point and a curve-to command will split the curve into two new + curve-to commands given a value between 0-1" + [from-p cmd t-val] + (let [params (:params cmd) + end (gpt/point (:x params) (:y params)) + h1 (gpt/point (:c1x params) (:c1y params)) + h2 (gpt/point (:c2x params) (:c2y params)) + [[_ to1 h11 h21] + [_ to2 h12 h22]] (curve-split from-p end h1 h2 t-val)] + [(upc/make-curve-to to1 h11 h21) + (upc/make-curve-to to2 h12 h22)])) + +(defn split-line-to-ranges + "Splits a line into several lines given the points in `values` + for example (split-line-to-ranges p c [0 0.25 0.5 0.75 1] will split + the line into 4 lines" + [from-p cmd values] + (let [values (->> values (filter #(and (> % 0) (< % 1))))] + (if (empty? values) + [cmd] + (let [to-p (upc/command->point cmd) + values-set (->> (conj values 1) (into (sorted-set)))] + (->> values-set + (mapv (fn [val] + (-> (gpt/lerp from-p to-p val) + #_(gpt/round 2) + (upc/make-line-to))))))))) + +(defn split-curve-to-ranges + "Splits a curve into several curves given the points in `values` + for example (split-curve-to-ranges p c [0 0.25 0.5 0.75 1] will split + the curve into 4 curves that draw the same curve" + [from-p cmd values] + + (let [values (->> values (filter #(and (> % 0) (< % 1))))] + (if (empty? values) + [cmd] + (let [to-p (upc/command->point cmd) + params (:params cmd) + h1 (gpt/point (:c1x params) (:c1y params)) + h2 (gpt/point (:c2x params) (:c2y params)) + + values-set (->> (conj values 0 1) (into (sorted-set)))] + + (->> (d/with-prev values-set) + (rest) + (mapv + (fn [[t1 t0]] + (let [[_ to-p h1' h2'] (subcurve-range from-p to-p h1 h2 t0 t1)] + (upc/make-curve-to (-> to-p #_(gpt/round 2)) h1' h2'))))))))) + +(defn content-center + [content] + (-> content + content->selrect + gsc/center-selrect)) + +(defn content->points+selrect + "Given the content of a shape, calculate its points and selrect" + [shape content] + (let [{:keys [flip-x flip-y]} shape + transform + (cond-> (:transform shape (gmt/matrix)) + flip-x (gmt/scale (gpt/point -1 1)) + flip-y (gmt/scale (gpt/point 1 -1))) + + transform-inverse + (cond-> (gmt/matrix) + flip-x (gmt/scale (gpt/point -1 1)) + flip-y (gmt/scale (gpt/point 1 -1)) + :always (gmt/multiply (:transform-inverse shape (gmt/matrix)))) + + center (or (gsc/center-shape shape) + (content-center content)) + + base-content (transform-content + content + (gmt/transform-in center transform-inverse)) + + ;; Calculates the new selrect with points given the old center + points (-> (content->selrect base-content) + (gpr/rect->points) + (gsc/transform-points center transform)) + + points-center (gsc/center-points points) + + ;; Points is now the selrect but the center is different so we can create the selrect + ;; through points + selrect (-> points + (gsc/transform-points points-center transform-inverse) + (gpr/points->selrect))] + [points selrect])) + + +(defn open-path? + [shape] + + (and (= :path (:type shape)) + (not (->> shape + :content + (sp/close-subpaths) + (sp/get-subpaths) + (every? sp/is-closed?))))) diff --git a/common/src/app/common/geom/shapes/rect.cljc b/common/src/app/common/geom/shapes/rect.cljc index 91e7d18a9..047781a70 100644 --- a/common/src/app/common/geom/shapes/rect.cljc +++ b/common/src/app/common/geom/shapes/rect.cljc @@ -7,7 +7,8 @@ (ns app.common.geom.shapes.rect (:require [app.common.geom.point :as gpt] - [app.common.geom.shapes.common :as gco])) + [app.common.geom.shapes.common :as gco] + [app.common.math :as mth])) (defn rect->points [{:keys [x y width height]}] ;; (assert (number? x)) @@ -47,6 +48,16 @@ (defn rect->selrect [rect] (-> rect rect->points points->selrect)) +(defn join-rects [rects] + (let [minx (transduce (comp (map :x) (remove nil?)) min ##Inf rects) + miny (transduce (comp (map :y) (remove nil?)) min ##Inf rects) + maxx (transduce (comp (map #(+ (:x %) (:width %))) (remove nil?)) max ##-Inf rects) + maxy (transduce (comp (map #(+ (:y %) (:height %))) (remove nil?)) max ##-Inf rects)] + {:x minx + :y miny + :width (- maxx minx) + :height (- maxy miny)})) + (defn join-selrects [selrects] (let [minx (transduce (comp (map :x1) (remove nil?)) min ##Inf selrects) miny (transduce (comp (map :y1) (remove nil?)) min ##Inf selrects) @@ -70,3 +81,43 @@ :y (- (:y center) (/ height 2)) :width width :height height}) + +(defn s= + [a b] + (mth/almost-zero? (- a b))) + +(defn overlaps-rects? + "Check for two rects to overlap. Rects won't overlap only if + one of them is fully to the left or the top" + [rect-a rect-b] + + (let [x1a (:x rect-a) + y1a (:y rect-a) + x2a (+ (:x rect-a) (:width rect-a)) + y2a (+ (:y rect-a) (:height rect-a)) + + x1b (:x rect-b) + y1b (:y rect-b) + x2b (+ (:x rect-b) (:width rect-b)) + y2b (+ (:y rect-b) (:height rect-b))] + + (and (or (> x2a x1b) (s= x2a x1b)) + (or (>= x2b x1a) (s= x2b x1a)) + (or (<= y1b y2a) (s= y1b y2a)) + (or (<= y1a y2b) (s= y1a y2b))))) + +(defn contains-point? + [rect point] + (assert (gpt/point? point)) + (let [x1 (:x rect) + y1 (:y rect) + x2 (+ (:x rect) (:width rect)) + y2 (+ (:y rect) (:height rect)) + + px (:x point) + py (:y point)] + + (and (or (> px x1) (s= px x1)) + (or (< px x2) (s= px x2)) + (or (> py y1) (s= py y1)) + (or (< py y2) (s= py y2))))) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index 97250b361..bfb6a751f 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -18,7 +18,6 @@ [app.common.spec :as us] [app.common.text :as txt])) - ;; --- Relative Movement (defn- move-selrect [selrect {dx :x dy :y}] @@ -161,23 +160,12 @@ matrix (gmt/translate-matrix (gpt/negate center))))) -(defn transform-points - ([points matrix] - (transform-points points nil matrix)) - ([points center matrix] - (let [prev (if center (gmt/translate-matrix center) (gmt/matrix)) - post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix)) - - tr-point (fn [point] - (gpt/transform point (gmt/multiply prev matrix post)))] - (mapv tr-point points)))) - (defn transform-rect "Transform a rectangles and changes its attributes" [rect matrix] (let [points (-> (gpr/rect->points rect) - (transform-points matrix))] + (gco/transform-points matrix))] (gpr/points->rect points))) (defn calculate-adjust-matrix @@ -201,12 +189,12 @@ stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0)) h1 (max 1 (calculate-height points-temp)) - h2 (max 1 (calculate-height (transform-points points-rec center stretch-matrix))) + h2 (max 1 (calculate-height (gco/transform-points points-rec center stretch-matrix))) h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1) h3 (if (mth/nan? h3) 1 h3) w1 (max 1 (calculate-width points-temp)) - w2 (max 1 (calculate-width (transform-points points-rec center stretch-matrix))) + w2 (max 1 (calculate-width (gco/transform-points points-rec center stretch-matrix))) w3 (if-not (mth/almost-zero? w2) (/ w1 w2) 1) w3 (if (mth/nan? w3) 1 w3) @@ -214,7 +202,7 @@ rotation-angle (calculate-rotation center - (transform-points points-rec (gco/center-points points-rec) stretch-matrix) + (gco/transform-points points-rec (gco/center-points points-rec) stretch-matrix) points-temp flip-x flip-y) @@ -232,14 +220,13 @@ "Given a new set of points transformed, set up the rectangle so it keeps its properties. We adjust de x,y,width,height and create a custom transform" [shape transform round-coords?] - ;; - (let [points (-> shape :points (transform-points transform)) + (let [points (-> shape :points (gco/transform-points transform)) center (gco/center-points points) ;; Reverse the current transformation stack to get the base rectangle tr-inverse (:transform-inverse shape (gmt/matrix)) - points-temp (transform-points points center tr-inverse) + points-temp (gco/transform-points points center tr-inverse) points-temp-dim (calculate-dimensions points-temp) ;; This rectangle is the new data for the current rectangle. We want to change our rectangle @@ -305,12 +292,12 @@ points (->> children (mapcat :points)) ;; Invert to get the points minus the transforms applied to the group - base-points (transform-points points shape-center (:transform-inverse group (gmt/matrix))) + base-points (gco/transform-points points shape-center (:transform-inverse group (gmt/matrix))) ;; Defines the new selection rect with its transformations new-points (-> (gpr/points->selrect base-points) (gpr/rect->points) - (transform-points shape-center (:transform group (gmt/matrix)))) + (gco/transform-points shape-center (:transform group (gmt/matrix)))) ;; Calculte the new selrect new-selrect (gpr/points->selrect base-points)] @@ -457,8 +444,10 @@ transform)) (defn- set-flip [shape modifiers] - (let [rx (get-in modifiers [:resize-vector :x]) - ry (get-in modifiers [:resize-vector :y])] + (let [rx (or (get-in modifiers [:resize-vector :x]) + (get-in modifiers [:resize-vector-2 :x])) + ry (or (get-in modifiers [:resize-vector :y]) + (get-in modifiers [:resize-vector-2 :y]))] (cond-> shape (and rx (< rx 0)) (-> (update :flip-x not) (update :rotation -)) @@ -517,7 +506,7 @@ (defn calc-child-modifiers "Given the modifiers to apply to the parent, calculate the corresponding modifiers for the child, depending on the child constraints." - [parent child parent-modifiers] + [parent child parent-modifiers ignore-constraints] (let [parent-rect (:selrect parent) child-rect (:selrect child) @@ -544,15 +533,19 @@ transformed-parent-rect (-> parent-rect (gpr/rect->points) - (transform-points parent-displacement) - (transform-points parent-origin (gmt/scale-matrix parent-vector)) - (transform-points parent-origin-2 (gmt/scale-matrix parent-vector-2)) + (gco/transform-points parent-displacement) + (gco/transform-points parent-origin (gmt/scale-matrix parent-vector)) + (gco/transform-points parent-origin-2 (gmt/scale-matrix parent-vector-2)) (gpr/points->selrect)) ;; Calculate the modifiers in the horizontal and vertical directions ;; depending on the child constraints. - constraints-h (get child :constraints-h (spec/default-constraints-h child)) - constraints-v (get child :constraints-v (spec/default-constraints-v child)) + constraints-h (if-not ignore-constraints + (get child :constraints-h (spec/default-constraints-h child)) + :scale) + constraints-v (if-not ignore-constraints + (get child :constraints-v (spec/default-constraints-v child)) + :scale) modifiers-h (case constraints-h :left @@ -692,3 +685,12 @@ (assoc :resize-transform (:resize-transform parent-modifiers) :resize-transform-inverse (:resize-transform-inverse parent-modifiers))))) + +(defn selection-rect + "Returns a rect that contains all the shapes and is aware of the + rotation of each shape. Mainly used for multiple selection." + [shapes] + (->> shapes + (transform-shape) + (map (comp gpr/points->selrect :points)) + (gpr/join-selrects))) diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc new file mode 100644 index 000000000..a861bcc27 --- /dev/null +++ b/common/src/app/common/logging.cljc @@ -0,0 +1,297 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.logging + (:require + [app.common.exceptions :as ex] + [clojure.pprint :refer [pprint]] + [cuerdas.core :as str] + #?(:cljs [goog.log :as glog])) + #?(:cljs (:require-macros [app.common.logging])) + #?(:clj + (:import + org.apache.logging.log4j.Level + org.apache.logging.log4j.LogManager + org.apache.logging.log4j.Logger + org.apache.logging.log4j.ThreadContext + org.apache.logging.log4j.message.MapMessage + org.apache.logging.log4j.spi.LoggerContext))) + +#?(:clj + (defn build-map-message + [m] + (let [message (MapMessage. (count m))] + (reduce-kv #(.with ^MapMessage %1 (name %2) %3) message m)))) + +#?(:clj + (def logger-context + (LogManager/getContext false))) + +#?(:clj + (def logging-agent + (agent nil :error-mode :continue))) + +(defn get-logger + [lname] + #?(:clj (.getLogger ^LoggerContext logger-context ^String lname) + :cljs + (glog/getLogger + (cond + (string? lname) lname + (= lname :root) "" + (simple-ident? lname) (name lname) + (qualified-ident? lname) (str (namespace lname) "." (name lname)) + :else (str lname))))) + +(defn get-level + [level] + #?(:clj + (case level + :trace Level/TRACE + :debug Level/DEBUG + :info Level/INFO + :warn Level/WARN + :error Level/ERROR + :fatal Level/FATAL) + :cljs + (case level + :off (.-OFF ^js glog/Level) + :shout (.-SHOUT ^js glog/Level) + :error (.-SEVERE ^js glog/Level) + :severe (.-SEVERE ^js glog/Level) + :warning (.-WARNING ^js glog/Level) + :warn (.-WARNING ^js glog/Level) + :info (.-INFO ^js glog/Level) + :config (.-CONFIG ^js glog/Level) + :debug (.-FINE ^js glog/Level) + :fine (.-FINE ^js glog/Level) + :finer (.-FINER ^js glog/Level) + :trace (.-FINER ^js glog/Level) + :finest (.-FINEST ^js glog/Level) + :all (.-ALL ^js glog/Level)))) + +(defn write-log! + [logger level exception message] + #?(:clj + (if exception + (.log ^Logger logger + ^Level level + ^Object message + ^Throwable exception) + (.log ^Logger logger + ^Level level + ^Object message)) + :cljs + (when glog/ENABLED + (when-let [l (get-logger logger)] + (let [level (get-level level) + record (glog/LogRecord. level message (.getName ^js l))] + (when exception (.setException record exception)) + (glog/publishLogRecord l record)))))) + +#?(:clj + (defn enabled? + [logger level] + (.isEnabled ^Logger logger ^Level level))) + +(defmacro log + [& {:keys [level cause ::logger ::async ::raw] :as props}] + (if (:ns &env) ; CLJS + `(write-log! ~(or logger (str *ns*)) + ~level + ~cause + ~(dissoc props :level :cause ::logger ::raw)) + (let [props (dissoc props :level :cause ::logger ::async ::raw) + logger (or logger (str *ns*)) + logger-sym (gensym "log") + level-sym (gensym "log")] + `(let [~logger-sym (get-logger ~logger) + ~level-sym (get-level ~level)] + (if (enabled? ~logger-sym ~level-sym) + ~(if async + `(send-off logging-agent + (fn [_#] + (let [message# (or ~raw (build-map-message ~props))] + (write-log! ~logger-sym ~level-sym ~cause message#)))) + `(let [message# (or ~raw (build-map-message ~props))] + (write-log! ~logger-sym ~level-sym ~cause message#)))))))) + +(defmacro info + [& params] + `(log :level :info ~@params)) + +(defmacro error + [& params] + `(log :level :error ~@params)) + +(defmacro warn + [& params] + `(log :level :warn ~@params)) + +(defmacro debug + [& params] + `(log :level :debug ~@params)) + +(defmacro trace + [& params] + `(log :level :trace ~@params)) + +(defmacro set-level! + ([level] + (when (:ns &env) + `(set-level* ~(str *ns*) ~level))) + ([n level] + (when (:ns &env) + `(set-level* ~n ~level)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; CLJ Specific +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +#?(:clj + (defn update-thread-context! + [data] + (run! (fn [[key val]] + (ThreadContext/put + (name key) + (cond + (coll? val) + (binding [clojure.pprint/*print-right-margin* 120] + (with-out-str (pprint val))) + (instance? clojure.lang.Named val) (name val) + :else (str val)))) + data))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; CLJS Specific +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +#?(:cljs + (def ^:private colors + {:gray3 "#8e908c" + :gray4 "#969896" + :gray5 "#4d4d4c" + :gray6 "#282a2e" + :black "#1d1f21" + :red "#c82829" + :blue "#4271ae" + :orange "#f5871f"})) + +#?(:cljs + (defn- level->color + [level] + (letfn [(get-level-value [l] (.-value ^js (get-level l)))] + (condp <= (get-level-value level) + (get-level-value :error) (get colors :red) + (get-level-value :warn) (get colors :orange) + (get-level-value :info) (get colors :blue) + (get-level-value :debug) (get colors :gray4) + (get-level-value :trace) (get colors :gray3) + (get colors :gray2))))) + +#?(:cljs + (defn- level->short-name + [l] + (case l + :fine "DBG" + :debug "DBG" + :finer "TRC" + :trace "TRC" + :info "INF" + :warn "WRN" + :warning "WRN" + :error "ERR" + (subs (.-name ^js (get-level l)) 0 3)))) + +#?(:cljs + (defn set-level* + "Set the level (a keyword) of the given logger, identified by name." + [name lvl] + (some-> (get-logger name) + (glog/setLevel (get-level lvl))))) + + +#?(:cljs + (defn set-levels! + [lvls] + (doseq [[logger level] lvls + :let [level (if (string? level) (keyword level) level)]] + (set-level* logger level)))) + +#?(:cljs + (defn- prepare-message + [message] + (loop [kvpairs (seq message) + message (array-map) + specials []] + (if (nil? kvpairs) + [message specials] + (let [[k v] (first kvpairs)] + (cond + (= k :err) + (recur (next kvpairs) + message + (conj specials [:error nil v])) + + (and (qualified-ident? k) + (= "js" (namespace k))) + (recur (next kvpairs) + message + (conj specials [:js (name k) (if (object? v) v (clj->js v))])) + + :else + (recur (next kvpairs) + (assoc message k v) + specials))))))) + +#?(:cljs + (defn default-handler + [{:keys [message level logger-name]}] + (let [header-styles (str "font-weight: 600; color: " (level->color level)) + normal-styles (str "font-weight: 300; color: " (get colors :gray6)) + level-name (level->short-name level) + header (str "%c" level-name " [" logger-name "] ")] + + (if (string? message) + (let [message (str header "%c" message)] + (js/console.log message header-styles normal-styles)) + (let [[message specials] (prepare-message message)] + (if (seq specials) + (let [message (str header "%c" (pr-str message))] + (js/console.group message header-styles normal-styles) + (doseq [[type n v] specials] + (case type + :js (js/console.log n v) + :error (if (ex/ex-info? v) + (js/console.error (pr-str v)) + (js/console.error v)))) + (js/console.groupEnd message)) + (let [message (str header "%c" (pr-str message))] + (js/console.log message header-styles normal-styles)))))))) + +#?(:cljs + (defn record->map + [^js record] + {:seqn (.-sequenceNumber_ record) + :time (.-time_ record) + :level (keyword (str/lower (.-name (.-level_ record)))) + :message (.-msg_ record) + :logger-name (.-loggerName_ record) + :exception (.-exception_ record)})) + +#?(:cljs + (defonce default-console-handler + (comp default-handler record->map))) + +#?(:cljs + (defn initialize! + [] + (let [l (get-logger :root)] + (glog/removeHandler l default-console-handler) + (glog/addHandler l default-console-handler) + nil))) + + diff --git a/common/src/app/common/math.cljc b/common/src/app/common/math.cljc index 22ebd97af..67a327da8 100644 --- a/common/src/app/common/math.cljc +++ b/common/src/app/common/math.cljc @@ -72,17 +72,24 @@ [v] (* v v)) +(defn pow + "Returns the base to the exponent power." + [b e] + #?(:cljs (js/Math.pow b e) + :clj (Math/pow b e))) + (defn sqrt "Returns the square root of a number." [v] #?(:cljs (js/Math.sqrt v) :clj (Math/sqrt v))) -(defn pow - "Returns the base to the exponent power." - [b e] - #?(:cljs (js/Math.pow b e) - :clj (Math/pow b e))) +(defn cubicroot + "Returns the cubic root of a number" + [v] + (if (pos? v) + (pow v (/ 1 3)) + (- (pow (- v) (/ 1 3))))) (defn floor "Returns the largest integer less than or @@ -143,7 +150,7 @@ (if (> num to) to num))) (defn almost-zero? [num] - (< (abs num) 1e-8)) + (< (abs (double num)) 1e-5)) (defonce float-equal-precision 0.001) @@ -151,3 +158,9 @@ "Equality for float numbers. Check if the difference is within a range" [num1 num2] (<= (abs (- num1 num2)) float-equal-precision)) + +(defn lerp + "Calculates a the linear interpolation between two values and a given percent" + [v0 v1 t] + (+ (* (- 1 t) v0) + (* t v1))) diff --git a/common/src/app/common/pages.cljc b/common/src/app/common/pages.cljc index fdf02cfa3..00725454a 100644 --- a/common/src/app/common/pages.cljc +++ b/common/src/app/common/pages.cljc @@ -40,9 +40,11 @@ (d/export helpers/get-children) (d/export helpers/get-children-objects) (d/export helpers/get-object-with-children) +(d/export helpers/select-children) (d/export helpers/is-shape-grouped) (d/export helpers/get-parent) (d/export helpers/get-parents) +(d/export helpers/get-frame) (d/export helpers/clean-loops) (d/export helpers/calculate-invalid-targets) (d/export helpers/valid-frame-target) @@ -66,13 +68,14 @@ (d/export helpers/merge-path-item) (d/export helpers/compact-path) (d/export helpers/compact-name) +(d/export helpers/unframed-shape?) ;; Indices (d/export indices/calculate-z-index) (d/export indices/update-z-index) (d/export indices/generate-child-all-parents-index) (d/export indices/generate-child-parent-index) -(d/export indices/create-mask-index) +(d/export indices/create-clip-index) ;; Process changes (d/export changes/process-changes) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 997edc170..a2211b238 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.bool :as gshb] [app.common.pages.common :refer [component-sync-attrs]] [app.common.pages.helpers :as cph] [app.common.pages.init :as init] @@ -156,7 +157,7 @@ (sequence (comp (mapcat #(cons % (cph/get-parents % objects))) (map #(get objects %)) - (filter #(= (:type %) :group)) + (filter #(contains? #{:group :bool} (:type %))) (map :id) (distinct)) shapes))) @@ -177,6 +178,9 @@ (empty? children) group + (= :bool (:type group)) + (gshb/update-bool-selrect group children objects) + (:masked-group? group) (set-mask-selrect group children) diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc new file mode 100644 index 000000000..45fcebd23 --- /dev/null +++ b/common/src/app/common/pages/changes_builder.cljc @@ -0,0 +1,167 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.pages.changes-builder + (:require + [app.common.data :as d] + [app.common.pages :as cp] + [app.common.pages.helpers :as h])) + +;; Auxiliary functions to help create a set of changes (undo + redo) + +(defn empty-changes [origin page-id] + (let [changes {:redo-changes [] + :undo-changes [] + :origin origin}] + (with-meta changes + {::page-id page-id}))) + +(defn with-objects [changes objects] + (vary-meta changes assoc ::objects objects)) + +(defn add-obj + ([changes obj index] + (add-obj changes (assoc obj ::index index))) + + ([changes obj] + (let [add-change + {:type :add-obj + :id (:id obj) + :page-id (::page-id (meta changes)) + :parent-id (:parent-id obj) + :frame-id (:frame-id obj) + :index (::index obj) + :obj (dissoc obj ::index :parent-id)} + + del-change + {:type :del-obj + :id (:id obj) + :page-id (::page-id (meta changes))}] + + (-> changes + (update :redo-changes conj add-change) + (update :undo-changes d/preconj del-change))))) + +(defn change-parent + [changes parent-id shapes] + (assert (contains? (meta changes) ::objects) "Call (with-objects) first to use this function") + + (let [objects (::objects (meta changes)) + set-parent-change + {:type :mov-objects + :parent-id parent-id + :page-id (::page-id (meta changes)) + :shapes (->> shapes (mapv :id))} + + mk-undo-change + (fn [change-set shape] + (d/preconj + change-set + {:type :mov-objects + :page-id (::page-id (meta changes)) + :parent-id (:parent-id shape) + :shapes [(:id shape)] + :index (cp/position-on-parent (:id shape) objects)}))] + + (-> changes + (update :redo-changes conj set-parent-change) + (update :undo-changes #(reduce mk-undo-change % shapes))))) + +(defn- generate-operation + "Given an object old and new versions and an attribute will append into changes + the set and undo operations" + [changes attr old new ignore-geometry?] + (let [old-val (get old attr) + new-val (get new attr)] + (if (= old-val new-val) + changes + (-> changes + (update :rops conj {:type :set :attr attr :val new-val :ignore-geometry ignore-geometry?}) + (update :uops conj {:type :set :attr attr :val old-val :ignore-touched true}))))) + +(defn update-shapes + "Calculate the changes and undos to be done when a function is applied to a + single object" + ([changes ids update-fn] + (update-shapes changes ids update-fn nil)) + + ([changes ids update-fn {:keys [attrs ignore-geometry?] :or {attrs nil ignore-geometry? false}}] + (assert (contains? (meta changes) ::objects) "Call (with-objects) first to use this function") + (let [objects (::objects (meta changes)) + + update-shape + (fn [changes id] + (let [old-obj (get objects id) + new-obj (update-fn old-obj) + + attrs (or attrs (d/concat #{} (keys old-obj) (keys new-obj))) + + {rops :rops uops :uops} + (reduce #(generate-operation %1 %2 old-obj new-obj ignore-geometry?) + {:rops [] :uops []} + attrs) + + uops (cond-> uops + (seq uops) + (conj {:type :set-touched :touched (:touched old-obj)})) + + change {:type :mod-obj + :page-id (::page-id (meta changes)) + :id id}] + + (cond-> changes + (seq rops) + (update :redo-changes conj (assoc change :operations rops)) + + (seq uops) + (update :undo-changes d/preconj (assoc change :operations uops)))))] + + (reduce update-shape changes ids)))) + +(defn remove-objects + [changes ids] + (assert (contains? (meta changes) ::objects) "Call (with-objects) first to use this function") + (let [page-id (::page-id (meta changes)) + objects (::objects (meta changes)) + + add-redo-change + (fn [change-set id] + (conj change-set + {:type :del-obj + :page-id page-id + :id id})) + + add-undo-change-shape + (fn [change-set id] + (let [shape (get objects id)] + (d/preconj + change-set + {:type :add-obj + :page-id page-id + :parent-id (:frame-id shape) + :frame-id (:frame-id shape) + :id id + :obj (cond-> shape + (contains? shape :shapes) + (assoc :shapes []))}))) + + add-undo-change-parent + (fn [change-set id] + (let [shape (get objects id)] + (d/preconj + change-set + {:type :mov-objects + :page-id page-id + :parent-id (:parent-id shape) + :shapes [id] + :index (h/position-on-parent id objects) + :ignore-touched true})))] + + (-> changes + (update :redo-changes #(reduce add-redo-change % ids)) + (update :undo-changes #(as-> % $ + (reduce add-undo-change-parent $ ids) + (reduce add-undo-change-shape $ ids)))))) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 6b30e3a9e..909908820 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.spec :as us] + [app.common.types.interactions :as cti] [app.common.uuid :as uuid] [cuerdas.core :as str])) @@ -138,6 +139,10 @@ [id objects] (mapv #(get objects %) (cons id (get-children id objects)))) +(defn select-children [id objects] + (->> (get-children id objects) + (select-keys objects))) + (defn is-shape-grouped "Checks if a shape is inside a group" [shape-id objects] @@ -161,6 +166,12 @@ (when parent-id (lazy-seq (cons parent-id (get-parents parent-id objects)))))) +(defn get-frame + "Get the frame that contains the shape. If the shape is already a frame, get itself." + [shape objects] + (if (= (:type shape) :frame) + shape + (get objects (:frame-id shape)))) (defn clean-loops "Clean a list of ids from circular references." @@ -466,3 +477,17 @@ (let [path-split (split-path path)] (merge-path-item (first path-split) name))) +(defn connected-frame? + "Check if some frame is origin or destination of any navigate interaction + in the page" + [frame-id objects] + (let [children (get-object-with-children frame-id objects)] + (or (some cti/flow-origin? (map :interactions children)) + (some #(cti/flow-to? % frame-id) (map :interactions (vals objects)))))) + +(defn unframed-shape? + "Checks if it's a non-frame shape in the top level." + [shape] + (and (not= (:type shape) :frame) + (= (:frame-id shape) uuid/zero))) + diff --git a/common/src/app/common/pages/indices.cljc b/common/src/app/common/pages/indices.cljc index c1681fef1..5a5f40595 100644 --- a/common/src/app/common/pages/indices.cljc +++ b/common/src/app/common/pages/indices.cljc @@ -95,16 +95,24 @@ (map #(vector (:id %) (shape->parents %))) (into {}))))) -(defn create-mask-index +(defn create-clip-index "Retrieves the mask information for an object" [objects parents-index] - (let [retrieve-masks + (let [retrieve-clips (fn [_ parents] - ;; TODO: use transducers? - (->> parents - (map #(get objects %)) - (filter #(:masked-group? %)) - ;; Retrieve the masking element - (mapv #(get objects (->> % :shapes first)))))] + (let [lookup-object (fn [id] (get objects id)) + get-clip-parents + (fn [shape] + (cond-> [] + (:masked-group? shape) + (conj (get objects (->> shape :shapes first))) + + (= :bool (:type shape)) + (conj shape)))] + + (into [] + (comp (map lookup-object) + (mapcat get-clip-parents)) + parents)))] (->> parents-index - (d/mapm retrieve-masks)))) + (d/mapm retrieve-clips)))) diff --git a/common/src/app/common/pages/spec.cljc b/common/src/app/common/pages/spec.cljc index aec112016..286148439 100644 --- a/common/src/app/common/pages/spec.cljc +++ b/common/src/app/common/pages/spec.cljc @@ -9,6 +9,8 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.spec :as us] + [app.common.types.interactions :as cti] + [app.common.types.page-options :as cto] [app.common.uuid :as uuid] [clojure.set :as set] [clojure.spec.alpha :as s])) @@ -30,9 +32,6 @@ (s/def ::component-root? boolean?) (s/def ::shape-ref uuid?) -(s/def ::safe-integer ::us/safe-integer) -(s/def ::safe-number ::us/safe-number) - (s/def :internal.matrix/a ::us/safe-number) (s/def :internal.matrix/b ::us/safe-number) (s/def :internal.matrix/c ::us/safe-number) @@ -61,15 +60,15 @@ ;; GRADIENTS (s/def :internal.gradient.stop/color ::string) -(s/def :internal.gradient.stop/opacity ::safe-number) -(s/def :internal.gradient.stop/offset ::safe-number) +(s/def :internal.gradient.stop/opacity ::us/safe-number) +(s/def :internal.gradient.stop/offset ::us/safe-number) (s/def :internal.gradient/type #{:linear :radial}) -(s/def :internal.gradient/start-x ::safe-number) -(s/def :internal.gradient/start-y ::safe-number) -(s/def :internal.gradient/end-x ::safe-number) -(s/def :internal.gradient/end-y ::safe-number) -(s/def :internal.gradient/width ::safe-number) +(s/def :internal.gradient/start-x ::us/safe-number) +(s/def :internal.gradient/start-y ::us/safe-number) +(s/def :internal.gradient/end-x ::us/safe-number) +(s/def :internal.gradient/end-y ::us/safe-number) +(s/def :internal.gradient/width ::us/safe-number) (s/def :internal.gradient/stop (s/keys :req-un [:internal.gradient.stop/color @@ -95,7 +94,7 @@ (s/def :internal.color/path (s/nilable ::string)) (s/def :internal.color/value (s/nilable ::string)) (s/def :internal.color/color (s/nilable ::string)) -(s/def :internal.color/opacity (s/nilable ::safe-number)) +(s/def :internal.color/opacity (s/nilable ::us/safe-number)) (s/def :internal.color/gradient (s/nilable ::gradient)) (s/def ::color @@ -113,10 +112,10 @@ (s/def :internal.shadow/id uuid?) (s/def :internal.shadow/style #{:drop-shadow :inner-shadow}) (s/def :internal.shadow/color ::color) -(s/def :internal.shadow/offset-x ::safe-number) -(s/def :internal.shadow/offset-y ::safe-number) -(s/def :internal.shadow/blur ::safe-number) -(s/def :internal.shadow/spread ::safe-number) +(s/def :internal.shadow/offset-x ::us/safe-number) +(s/def :internal.shadow/offset-y ::us/safe-number) +(s/def :internal.shadow/blur ::us/safe-number) +(s/def :internal.shadow/spread ::us/safe-number) (s/def :internal.shadow/hidden boolean?) (s/def :internal.shadow/shadow @@ -137,7 +136,7 @@ (s/def :internal.blur/id uuid?) (s/def :internal.blur/type #{:layer-blur}) -(s/def :internal.blur/value ::safe-number) +(s/def :internal.blur/value ::us/safe-number) (s/def :internal.blur/hidden boolean?) (s/def ::blur @@ -146,57 +145,6 @@ :internal.blur/value :internal.blur/hidden])) -;; Page Options -(s/def :internal.page.grid.color/value string?) -(s/def :internal.page.grid.color/opacity ::safe-number) - -(s/def :internal.page.grid/size ::safe-integer) -(s/def :internal.page.grid/color - (s/keys :req-un [:internal.page.grid.color/value - :internal.page.grid.color/opacity])) - -(s/def :internal.page.grid/type #{:stretch :left :center :right}) -(s/def :internal.page.grid/item-length (s/nilable ::safe-integer)) -(s/def :internal.page.grid/gutter (s/nilable ::safe-integer)) -(s/def :internal.page.grid/margin (s/nilable ::safe-integer)) - -(s/def :internal.page.grid/square - (s/keys :req-un [:internal.page.grid/size - :internal.page.grid/color])) - -(s/def :internal.page.grid/column - (s/keys :req-un [:internal.page.grid/size - :internal.page.grid/color - :internal.page.grid/type - :internal.page.grid/item-length - :internal.page.grid/gutter - :internal.page.grid/margin])) - -(s/def :internal.page.grid/row :internal.page.grid/column) - -(s/def :internal.page.options/background string?) -(s/def :internal.page.options/saved-grids - (s/keys :req-un [:internal.page.grid/square - :internal.page.grid/row - :internal.page.grid/column])) - -(s/def :internal.page/options - (s/keys :opt-un [:internal.page.options/background])) - -;; Interactions - -(s/def :internal.shape.interaction/event-type #{:click}) ; In the future we will have more options -(s/def :internal.shape.interaction/action-type #{:navigate}) -(s/def :internal.shape.interaction/destination ::uuid) - -(s/def :internal.shape/interaction - (s/keys :req-un [:internal.shape.interaction/event-type - :internal.shape.interaction/action-type - :internal.shape.interaction/destination])) - -(s/def :internal.shape/interactions - (s/coll-of :internal.shape/interaction :kind vector?)) - ;; Size constraints (s/def :internal.shape/constraints-h #{:left :right :leftright :center :scale}) @@ -227,33 +175,33 @@ (s/def :internal.shape/content any?) (s/def :internal.shape/fill-color string?) -(s/def :internal.shape/fill-opacity ::safe-number) +(s/def :internal.shape/fill-opacity ::us/safe-number) (s/def :internal.shape/fill-color-gradient (s/nilable ::gradient)) (s/def :internal.shape/fill-color-ref-file (s/nilable uuid?)) (s/def :internal.shape/fill-color-ref-id (s/nilable uuid?)) (s/def :internal.shape/font-family string?) -(s/def :internal.shape/font-size ::safe-integer) +(s/def :internal.shape/font-size ::us/safe-integer) (s/def :internal.shape/font-style string?) (s/def :internal.shape/font-weight string?) (s/def :internal.shape/hidden boolean?) -(s/def :internal.shape/letter-spacing ::safe-number) -(s/def :internal.shape/line-height ::safe-number) +(s/def :internal.shape/letter-spacing ::us/safe-number) +(s/def :internal.shape/line-height ::us/safe-number) (s/def :internal.shape/locked boolean?) (s/def :internal.shape/page-id uuid?) -(s/def :internal.shape/proportion ::safe-number) +(s/def :internal.shape/proportion ::us/safe-number) (s/def :internal.shape/proportion-lock boolean?) -(s/def :internal.shape/rx ::safe-number) -(s/def :internal.shape/ry ::safe-number) -(s/def :internal.shape/r1 ::safe-number) -(s/def :internal.shape/r2 ::safe-number) -(s/def :internal.shape/r3 ::safe-number) -(s/def :internal.shape/r4 ::safe-number) +(s/def :internal.shape/rx ::us/safe-number) +(s/def :internal.shape/ry ::us/safe-number) +(s/def :internal.shape/r1 ::us/safe-number) +(s/def :internal.shape/r2 ::us/safe-number) +(s/def :internal.shape/r3 ::us/safe-number) +(s/def :internal.shape/r4 ::us/safe-number) (s/def :internal.shape/stroke-color string?) (s/def :internal.shape/stroke-color-gradient (s/nilable ::gradient)) (s/def :internal.shape/stroke-color-ref-file (s/nilable uuid?)) (s/def :internal.shape/stroke-color-ref-id (s/nilable uuid?)) -(s/def :internal.shape/stroke-opacity ::safe-number) +(s/def :internal.shape/stroke-opacity ::us/safe-number) (s/def :internal.shape/stroke-style #{:solid :dotted :dashed :mixed :none :svg}) (def stroke-caps-line #{:round :square}) @@ -266,26 +214,26 @@ [shape] (= (:type shape) :path)) -(s/def :internal.shape/stroke-width ::safe-number) +(s/def :internal.shape/stroke-width ::us/safe-number) (s/def :internal.shape/stroke-alignment #{:center :inner :outer}) (s/def :internal.shape/text-align #{"left" "right" "center" "justify"}) -(s/def :internal.shape/x ::safe-number) -(s/def :internal.shape/y ::safe-number) -(s/def :internal.shape/cx ::safe-number) -(s/def :internal.shape/cy ::safe-number) -(s/def :internal.shape/width ::safe-number) -(s/def :internal.shape/height ::safe-number) +(s/def :internal.shape/x ::us/safe-number) +(s/def :internal.shape/y ::us/safe-number) +(s/def :internal.shape/cx ::us/safe-number) +(s/def :internal.shape/cy ::us/safe-number) +(s/def :internal.shape/width ::us/safe-number) +(s/def :internal.shape/height ::us/safe-number) (s/def :internal.shape/index integer?) (s/def :internal.shape/shadow ::shadow) (s/def :internal.shape/blur ::blur) -(s/def :internal.shape/x1 ::safe-number) -(s/def :internal.shape/y1 ::safe-number) -(s/def :internal.shape/x2 ::safe-number) -(s/def :internal.shape/y2 ::safe-number) +(s/def :internal.shape/x1 ::us/safe-number) +(s/def :internal.shape/y1 ::us/safe-number) +(s/def :internal.shape/x2 ::us/safe-number) +(s/def :internal.shape/y2 ::us/safe-number) (s/def :internal.shape.export/suffix string?) -(s/def :internal.shape.export/scale ::safe-number) +(s/def :internal.shape.export/scale ::us/safe-number) (s/def :internal.shape/export (s/keys :req-un [::type :internal.shape.export/suffix @@ -361,7 +309,7 @@ :internal.shape/transform-inverse :internal.shape/width :internal.shape/height - :internal.shape/interactions + ::cti/interactions :internal.shape/masked-group? :internal.shape/shadow :internal.shape/blur])) @@ -386,7 +334,7 @@ (s/def ::page (s/keys :req-un [::id ::name - :internal.page/options + ::cto/options :internal.page/objects])) @@ -397,8 +345,8 @@ :internal.color/gradient])) (s/def :internal.media-object/name ::string) -(s/def :internal.media-object/width ::safe-integer) -(s/def :internal.media-object/height ::safe-integer) +(s/def :internal.media-object/width ::us/safe-integer) +(s/def :internal.media-object/height ::us/safe-integer) (s/def :internal.media-object/mtype ::string) (s/def ::media-object diff --git a/common/src/app/common/path/bool.cljc b/common/src/app/common/path/bool.cljc new file mode 100644 index 000000000..ddb388091 --- /dev/null +++ b/common/src/app/common/path/bool.cljc @@ -0,0 +1,309 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.path.bool + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as gsp] + [app.common.geom.shapes.rect :as gpr] + [app.common.path.commands :as upc] + [app.common.path.subpaths :as ups])) + +(defn add-previous + ([content] + (add-previous content nil)) + ([content first] + (->> (d/with-prev content) + (mapv (fn [[cmd prev]] + (cond-> cmd + (and (nil? prev) (some? first)) + (assoc :prev first) + + (some? prev) + (assoc :prev (gsp/command->point prev)))))))) + +(defn close-paths + "Removes the :close-path commands and replace them for line-to so we can calculate + the intersections" + [content] + + (loop [head (first content) + content (rest content) + result [] + last-move nil + last-p nil] + + (if (nil? head) + result + (let [head-p (gsp/command->point head) + head (cond + (and (= :close-path (:command head)) + (< (gpt/distance last-p last-move) 0.01)) + nil + + (= :close-path (:command head)) + (upc/make-line-to last-move) + + :else + head)] + + (recur (first content) + (rest content) + (cond-> result (some? head) (conj head)) + (if (= :move-to (:command head)) + head-p + last-move) + head-p))))) + +(defn- split-command + [cmd values] + (case (:command cmd) + :line-to (gsp/split-line-to-ranges (:prev cmd) cmd values) + :curve-to (gsp/split-curve-to-ranges (:prev cmd) cmd values) + [cmd])) + +(defn split-ts [seg-1 seg-2] + (cond + (and (= :line-to (:command seg-1)) + (= :line-to (:command seg-2))) + (gsp/line-line-intersect (gsp/command->line seg-1) (gsp/command->line seg-2)) + + (and (= :line-to (:command seg-1)) + (= :curve-to (:command seg-2))) + (gsp/line-curve-intersect (gsp/command->line seg-1) (gsp/command->bezier seg-2)) + + (and (= :curve-to (:command seg-1)) + (= :line-to (:command seg-2))) + (let [[seg-2' seg-1'] + (gsp/line-curve-intersect (gsp/command->line seg-2) (gsp/command->bezier seg-1))] + ;; Need to reverse because we send the arguments reversed + [seg-1' seg-2']) + + (and (= :curve-to (:command seg-1)) + (= :curve-to (:command seg-2))) + (gsp/curve-curve-intersect (gsp/command->bezier seg-1) (gsp/command->bezier seg-2)) + + :else + [[] []])) + +(defn split + [seg-1 seg-2] + (let [r1 (gsp/command->selrect seg-1) + r2 (gsp/command->selrect seg-2)] + (if (not (gpr/overlaps-rects? r1 r2)) + [[seg-1] [seg-2]] + (let [[ts-seg-1 ts-seg-2] (split-ts seg-1 seg-2)] + [(-> (split-command seg-1 ts-seg-1) (add-previous (:prev seg-1))) + (-> (split-command seg-2 ts-seg-2) (add-previous (:prev seg-2)))])))) + +(defn content-intersect-split + [content-a content-b] + + (let [cache (atom {})] + (letfn [(split-cache [seg-1 seg-2] + (cond + (contains? @cache [seg-1 seg-2]) + (first (get @cache [seg-1 seg-2])) + + (contains? @cache [seg-2 seg-1]) + (second (get @cache [seg-2 seg-1])) + + :else + (let [value (split seg-1 seg-2)] + (swap! cache assoc [seg-1 seg-2] value) + (first value)))) + + (split-segment-on-content + [segment content] + + (loop [current (first content) + content (rest content) + result [segment]] + + (if (nil? current) + result + (let [result (->> result (into [] (mapcat #(split-cache % current))))] + (recur (first content) + (rest content) + result))))) + + (split-content + [content-a content-b] + (into [] + (mapcat #(split-segment-on-content % content-b)) + content-a))] + + [(split-content content-a content-b) + (split-content content-b content-a)]))) + +(defn is-segment? + [cmd] + (and (contains? cmd :prev) + (contains? #{:line-to :curve-to} (:command cmd)))) + +(defn contains-segment? + [segment content] + + (let [point (case (:command segment) + :line-to (-> (gsp/command->line segment) + (gsp/line-values 0.5)) + + :curve-to (-> (gsp/command->bezier segment) + (gsp/curve-values 0.5)))] + + (or (gsp/is-point-in-content? point content) + (gsp/is-point-in-border? point content)))) + +(defn inside-segment? + [segment content] + (let [point (case (:command segment) + :line-to (-> (gsp/command->line segment) + (gsp/line-values 0.5)) + + :curve-to (-> (gsp/command->bezier segment) + (gsp/curve-values 0.5)))] + + (gsp/is-point-in-content? point content))) + +(defn overlap-segment? + "Finds if the current segment is overlapping against other + segment meaning they have the same coordinates" + [segment content] + + (let [overlap-single? + (fn [other] + (when (and (= (:command segment) (:command other)) + (contains? #{:line-to :curve-to} (:command segment))) + + (case (:command segment) + :line-to (let [[p1 q1] (gsp/command->line segment) + [p2 q2] (gsp/command->line other)] + + (when (or (and (< (gpt/distance p1 p2) 0.1) + (< (gpt/distance q1 q2) 0.1)) + (and (< (gpt/distance p1 q2) 0.1) + (< (gpt/distance q1 p2) 0.1))) + [segment other])) + + :curve-to (let [[p1 q1 h11 h21] (gsp/command->bezier segment) + [p2 q2 h12 h22] (gsp/command->bezier other)] + + (when (or (and (< (gpt/distance p1 p2) 0.1) + (< (gpt/distance q1 q2) 0.1) + (< (gpt/distance h11 h12) 0.1) + (< (gpt/distance h21 h22) 0.1)) + + (and (< (gpt/distance p1 q2) 0.1) + (< (gpt/distance q1 p2) 0.1) + (< (gpt/distance h11 h22) 0.1) + (< (gpt/distance h21 h12) 0.1))) + + [segment other])))))] + + (->> content + (d/seek overlap-single?) + (some?)))) + +(defn create-union [content-a content-a-split content-b content-b-split] + ;; Pick all segments in content-a that are not inside content-b + ;; Pick all segments in content-b that are not inside content-a + (let [content + (d/concat + [] + (->> content-a-split (filter #(not (contains-segment? % content-b)))) + (->> content-b-split (filter #(not (contains-segment? % content-a))))) + + ;; Overlapping segments should be added when they are part of the border + border-content + (->> content-b-split + (filterv #(and (contains-segment? % content-a) + (overlap-segment? % content-a-split) + (not (inside-segment? % content)))))] + + (d/concat content border-content))) + +(defn create-difference [content-a content-a-split content-b content-b-split] + ;; Pick all segments in content-a that are not inside content-b + ;; Pick all segments in content b that are inside content-a + ;; removing overlapping + (d/concat + [] + (->> content-a-split (filter #(not (contains-segment? % content-b)))) + + ;; Reverse second content so we can have holes inside other shapes + (->> content-b-split + (filter #(and (contains-segment? % content-a) + (not (overlap-segment? % content-a-split))))))) + +(defn create-intersection [content-a content-a-split content-b content-b-split] + ;; Pick all segments in content-a that are inside content-b + ;; Pick all segments in content-b that are inside content-a + (d/concat + [] + (->> content-a-split (filter #(contains-segment? % content-b))) + (->> content-b-split (filter #(contains-segment? % content-a))))) + + +(defn create-exclusion [content-a content-b] + ;; Pick all segments + (d/concat [] content-a content-b)) + + +(defn fix-move-to + [content] + ;; Remove the field `:prev` and makes the necesaries `move-to` + ;; then clean the subpaths + + (loop [current (first content) + content (rest content) + prev nil + result []] + + (if (nil? current) + result + + (let [result (if (not= (:prev current) prev) + (conj result (upc/make-move-to (:prev current))) + result)] + (recur (first content) + (rest content) + (gsp/command->point current) + (conj result (dissoc current :prev))))))) + +(defn content-bool-pair + [bool-type content-a content-b] + + (let [content-a (-> content-a (close-paths) (add-previous)) + + content-b (-> content-b + (close-paths) + (cond-> (ups/clockwise? content-b) + (ups/reverse-content)) + (add-previous)) + + ;; Split content in new segments in the intersection with the other path + [content-a-split content-b-split] (content-intersect-split content-a content-b) + content-a-split (->> content-a-split add-previous (filter is-segment?)) + content-b-split (->> content-b-split add-previous (filter is-segment?)) + + bool-content + (case bool-type + :union (create-union content-a content-a-split content-b content-b-split) + :difference (create-difference content-a content-a-split content-b content-b-split) + :intersection (create-intersection content-a content-a-split content-b content-b-split) + :exclude (create-exclusion content-a-split content-b-split))] + + (->> (fix-move-to bool-content) + (ups/close-subpaths)))) + +(defn content-bool + [bool-type contents] + ;; We apply the boolean operation in to each pair and the result to the next + ;; element + (->> contents + (reduce (partial content-bool-pair bool-type)) + (into []))) diff --git a/frontend/src/app/util/path/commands.cljs b/common/src/app/common/path/commands.cljc similarity index 99% rename from frontend/src/app/util/path/commands.cljs rename to common/src/app/common/path/commands.cljc index 84a7725ef..80737db8c 100644 --- a/frontend/src/app/util/path/commands.cljs +++ b/common/src/app/common/path/commands.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.util.path.commands +(ns app.common.path.commands (:require [app.common.data :as d] [app.common.geom.point :as gpt])) @@ -199,3 +199,4 @@ (if (= prefix :c1) (command->point (get content (dec index))) (command->point (get content index)))) + diff --git a/common/src/app/common/path/shapes_to_path.cljc b/common/src/app/common/path/shapes_to_path.cljc new file mode 100644 index 000000000..d2f905914 --- /dev/null +++ b/common/src/app/common/path/shapes_to_path.cljc @@ -0,0 +1,227 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.path.shapes-to-path + (:require + [app.common.data :as d] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gsc] + [app.common.geom.shapes.path :as gsp] + [app.common.path.bool :as pb] + [app.common.path.commands :as pc])) + +(def ^:const bezier-circle-c 0.551915024494) + +(def dissoc-attrs + [:x :y :width :height + :rx :ry :r1 :r2 :r3 :r4 + :metadata :shapes]) + +(def allowed-transform-types + #{:rect + :circle + :image}) + +(def style-group-properties + [:shadow + :blur]) + +(def style-properties + (d/concat + style-group-properties + [:fill-color + :fill-opacity + :fill-color-gradient + :fill-color-ref-file + :fill-color-ref-id + :fill-image + :stroke-color + :stroke-color-ref-file + :stroke-color-ref-id + :stroke-opacity + :stroke-style + :stroke-width + :stroke-alignment + :stroke-cap-start + :stroke-cap-end])) + +(defn make-corner-arc + "Creates a curvle corner for border radius" + [from to corner radius] + (let [x (case corner + :top-left (:x from) + :top-right (- (:x from) radius) + :bottom-right (- (:x to) radius) + :bottom-left (:x to)) + + y (case corner + :top-left (- (:y from) radius) + :top-right (:y from) + :bottom-right (- (:y to) (* 2 radius)) + :bottom-left (- (:y to) radius)) + + width (* radius 2) + height (* radius 2) + + c bezier-circle-c + c1x (+ x (* (/ width 2) (- 1 c))) + c2x (+ x (* (/ width 2) (+ 1 c))) + c1y (+ y (* (/ height 2) (- 1 c))) + c2y (+ y (* (/ height 2) (+ 1 c))) + + h1 (case corner + :top-left (assoc from :y c1y) + :top-right (assoc from :x c2x) + :bottom-right (assoc from :y c2y) + :bottom-left (assoc from :x c1x)) + + h2 (case corner + :top-left (assoc to :x c1x) + :top-right (assoc to :y c1y) + :bottom-right (assoc to :x c2x) + :bottom-left (assoc to :y c2y))] + + (pc/make-curve-to to h1 h2))) + +(defn circle->path + "Creates the bezier curves to approximate a circle shape" + [x y width height] + (let [mx (+ x (/ width 2)) + my (+ y (/ height 2)) + ex (+ x width) + ey (+ y height) + + p1 (gpt/point mx y) + p2 (gpt/point ex my) + p3 (gpt/point mx ey) + p4 (gpt/point x my) + + c bezier-circle-c + c1x (+ x (* (/ width 2) (- 1 c))) + c2x (+ x (* (/ width 2) (+ 1 c))) + c1y (+ y (* (/ height 2) (- 1 c))) + c2y (+ y (* (/ height 2) (+ 1 c)))] + + [(pc/make-move-to p1) + (pc/make-curve-to p2 (assoc p1 :x c2x) (assoc p2 :y c1y)) + (pc/make-curve-to p3 (assoc p2 :y c2y) (assoc p3 :x c2x)) + (pc/make-curve-to p4 (assoc p3 :x c1x) (assoc p4 :y c2y)) + (pc/make-curve-to p1 (assoc p4 :y c1y) (assoc p1 :x c1x))])) + +(defn rect->path + "Creates a bezier curve that approximates a rounded corner rectangle" + [x y width height r1 r2 r3 r4 rx] + (let [[r1 r2 r3 r4] (->> [r1 r2 r3 r4] (mapv #(or % rx 0))) + p1 (gpt/point x (+ y r1)) + p2 (gpt/point (+ x r1) y) + + p3 (gpt/point (+ width x (- r2)) y) + p4 (gpt/point (+ width x) (+ y r2)) + + p5 (gpt/point (+ width x) (+ height y (- r3))) + p6 (gpt/point (+ width x (- r3)) (+ height y)) + + p7 (gpt/point (+ x r4) (+ height y)) + p8 (gpt/point x (+ height y (- r4)))] + (-> [] + (conj (pc/make-move-to p1)) + (cond-> (not= p1 p2) + (conj (make-corner-arc p1 p2 :top-left r1))) + (conj (pc/make-line-to p3)) + (cond-> (not= p3 p4) + (conj (make-corner-arc p3 p4 :top-right r2))) + (conj (pc/make-line-to p5)) + (cond-> (not= p5 p6) + (conj (make-corner-arc p5 p6 :bottom-right r3))) + (conj (pc/make-line-to p7)) + (cond-> (not= p7 p8) + (conj (make-corner-arc p7 p8 :bottom-left r4))) + (conj (pc/make-line-to p1))))) + +(declare convert-to-path) + +(defn fix-first-relative + "Fix an issue with the simplify commands not changing the first relative" + [content] + (let [head (first content)] + (cond-> content + (and head (:relative head)) + (update 0 assoc :relative false)))) + +(defn group-to-path + [group objects] + (let [xform (comp (map #(get objects %)) + (map #(-> (convert-to-path % objects)))) + + child-as-paths (into [] xform (:shapes group)) + head (last child-as-paths) + head-data (select-keys head style-properties) + content (into [] + (comp (filter #(= :path (:type %))) + (mapcat #(fix-first-relative (:content %)))) + child-as-paths)] + (-> group + (assoc :type :path) + (assoc :content content) + (merge head-data) + (d/without-keys dissoc-attrs)))) + +(defn bool-to-path + [shape objects] + + (let [children (->> (:shapes shape) + (map #(get objects %)) + (map #(convert-to-path % objects))) + bool-type (:bool-type shape) + head (if (= bool-type :difference) (first children) (last children)) + head (cond-> head + (and (contains? head :svg-attrs) (nil? (:fill-color head))) + (assoc :fill-color "#000000")) + + head-data (select-keys head style-properties) + content (pb/content-bool (:bool-type shape) (mapv :content children))] + + (-> shape + (assoc :type :path) + (assoc :content content) + (merge head-data) + (d/without-keys dissoc-attrs)))) + +(defn convert-to-path + "Transforms the given shape to a path" + ([shape] + (convert-to-path shape {})) + ([{:keys [type x y width height r1 r2 r3 r4 rx metadata] :as shape} objects] + (assert (map? objects)) + (case (:type shape) + :group + (group-to-path shape objects) + + :bool + (bool-to-path shape objects) + + (:rect :circle :image :text) + (let [new-content + (case type + :circle (circle->path x y width height) + #_:else (rect->path x y width height r1 r2 r3 r4 rx)) + + ;; Apply the transforms that had the shape + transform (:transform shape) + new-content (cond-> new-content + (some? transform) + (gsp/transform-content (gmt/transform-in (gsc/center-shape shape) transform)))] + + (-> shape + (assoc :type :path) + (assoc :content new-content) + (cond-> (= :image type) + (assoc :fill-image metadata)) + (d/without-keys dissoc-attrs))) + + ;; For the rest return the plain shape + shape))) diff --git a/frontend/src/app/util/path/subpaths.cljs b/common/src/app/common/path/subpaths.cljc similarity index 66% rename from frontend/src/app/util/path/subpaths.cljs rename to common/src/app/common/path/subpaths.cljc index 010f1343a..202e273b1 100644 --- a/frontend/src/app/util/path/subpaths.cljs +++ b/common/src/app/common/path/subpaths.cljc @@ -4,10 +4,16 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.util.path.subpaths +(ns app.common.path.subpaths (:require [app.common.data :as d] - [app.util.path.commands :as upc])) + [app.common.geom.point :as gpt] + [app.common.path.commands :as upc])) + +(defn pt= + "Check if two points are close" + [p1 p2] + (< (gpt/distance p1 p2) 0.1)) (defn make-subpath "Creates a subpath either from a single command or with all the data" @@ -67,16 +73,22 @@ (fn [subpaths current] (let [is-move? (= :move-to (:command current)) last-idx (dec (count subpaths))] - (if is-move? + (cond + is-move? (conj subpaths (make-subpath current)) - (update subpaths last-idx add-subpath-command current))))] + + (>= last-idx 0) + (update subpaths last-idx add-subpath-command current) + + :else + subpaths)))] (->> content (reduce reduce-subpath [])))) (defn subpaths-join "Join two subpaths together when the first finish where the second starts" [subpath other] - (assert (= (:to subpath) (:from other))) + (assert (pt= (:to subpath) (:from other))) (-> subpath (update :data d/concat (rest (:data other))) (assoc :to (:to other)))) @@ -88,21 +100,31 @@ (let [merge-with-candidate (fn [[candidate result] current] (cond - (= (:to current) (:from current)) + (pt= (:to current) (:from current)) + ;; Subpath is already a closed path [candidate (conj result current)] - (= (:to candidate) (:from current)) + (pt= (:to candidate) (:from current)) [(subpaths-join candidate current) result] - (= (:to candidate) (:to current)) + (pt= (:from candidate) (:to current)) + [(subpaths-join current candidate) result] + + (pt= (:to candidate) (:to current)) [(subpaths-join candidate (reverse-subpath current)) result] + (pt= (:from candidate) (:from current)) + [(subpaths-join (reverse-subpath current) candidate) result] + :else [candidate (conj result current)]))] (->> subpaths (reduce merge-with-candidate [candidate []])))) +(defn is-closed? [subpath] + (pt= (:from subpath) (:to subpath))) + (defn close-subpaths "Searches a path for posible supaths that can create closed loops and merge them" [content] @@ -114,7 +136,7 @@ (if (some? current) (let [[new-current new-subpaths] - (if (= (:from current) (:to current)) + (if (is-closed? current) [current subpaths] (merge-paths current subpaths))] @@ -134,3 +156,38 @@ (->> closed-subpaths (mapcat :data) (into [])))) + +(defn reverse-content + "Given a content reverse the order of the commands" + [content] + + (->> content + (get-subpaths) + (mapv reverse-subpath) + (reverse) + (mapcat :data) + (into []))) + +;; https://mathworld.wolfram.com/PolygonArea.html +(defn clockwise? + "Check whether the first subpath is clockwise or counter-clock wise" + [content] + (let [subpath (->> content get-subpaths first :data)] + (loop [current (first subpath) + subpath (rest subpath) + first-point nil + signed-area 0] + + (if (nil? current) + (> signed-area 0) + + (let [{x1 :x y1 :y :as p} (upc/command->point current) + last? (nil? (first subpath)) + first-point (if (nil? first-point) p first-point) + {x2 :x y2 :y} (if last? first-point (upc/command->point (first subpath))) + signed-area (+ signed-area (- (* x1 y2) (* x2 y1)))] + + (recur (first subpath) + (rest subpath) + first-point + signed-area)))))) diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc index ca369fb51..88f8eab9a 100644 --- a/common/src/app/common/spec.cljc +++ b/common/src/app/common/spec.cljc @@ -111,6 +111,16 @@ (s/def ::point gpt/point?) (s/def ::id ::uuid) +(s/def ::words + (s/conformer + (fn [s] + (cond + (set? s) s + (string? s) (into #{} (map keyword) (str/words s)) + :else ::s/invalid)) + (fn [s] + (str/join " " (map name s))))) + (defn bytes? "Test if a first parameter is a byte array or not." @@ -196,7 +206,7 @@ :name (pr-str spec) :line (:line &env) :file (:file (:meta nsdata))}) - message (str "Spec Assertion: '" (pr-str spec) "'")] + message (str "spec assert: '" (pr-str spec) "'")] `(spec-assert* ~spec ~x ~message ~context)))) (defmacro verify @@ -208,7 +218,7 @@ :name (pr-str spec) :line (:line &env) :file (:file (:meta nsdata))}) - message (str "Spec Assertion: '" (pr-str spec) "'")] + message (str "spec verify: '" (pr-str spec) "'")] `(spec-assert* ~spec ~x ~message ~context))) ;; --- Public Api diff --git a/common/src/app/common/types/interactions.cljc b/common/src/app/common/types/interactions.cljc new file mode 100644 index 000000000..ebbc81e70 --- /dev/null +++ b/common/src/app/common/types/interactions.cljc @@ -0,0 +1,376 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.types.interactions + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.spec :as us] + [clojure.spec.alpha :as s])) + +;; WARNING: options are not deleted when changing event or action type, so it can be +;; restored if the user changes it back later. +;; +;; But that means that an interaction may have for example a delay or +;; destination, even if its type does not require it (but a previous type did). +;; +;; So make sure to use has-delay/has-destination... functions, or similar, +;; before reading them. + +;; -- Options depending on event type + +(s/def ::event-type #{:click + :mouse-press + :mouse-over + :mouse-enter + :mouse-leave + :after-delay}) + +(s/def ::delay ::us/safe-integer) + +(defmulti event-opts-spec :event-type) + +(defmethod event-opts-spec :after-delay [_] + (s/keys :req-un [::delay])) + +(defmethod event-opts-spec :default [_] + (s/keys :req-un [])) + +(s/def ::event-opts + (s/multi-spec event-opts-spec ::event-type)) + +;; -- Options depending on action type + +(s/def ::action-type #{:navigate + :open-overlay + :toggle-overlay + :close-overlay + :prev-screen + :open-url}) + +(s/def ::destination (s/nilable ::us/uuid)) +(s/def ::overlay-pos-type #{:manual + :center + :top-left + :top-right + :top-center + :bottom-left + :bottom-right + :bottom-center}) +(s/def ::overlay-position ::us/point) +(s/def ::url ::us/string) +(s/def ::close-click-outside ::us/boolean) +(s/def ::background-overlay ::us/boolean) + +(defmulti action-opts-spec :action-type) + +(defmethod action-opts-spec :navigate [_] + (s/keys :req-un [::destination])) + +(defmethod action-opts-spec :open-overlay [_] + (s/keys :req-un [::destination + ::overlay-position + ::overlay-pos-type] + :opt-un [::close-click-outside + ::background-overlay])) + +(defmethod action-opts-spec :toggle-overlay [_] + (s/keys :req-un [::destination + ::overlay-position + ::overlay-pos-type] + :opt-un [::close-click-outside + ::background-overlay])) + +(defmethod action-opts-spec :close-overlay [_] + (s/keys :req-un [::destination])) + +(defmethod action-opts-spec :prev-screen [_] + (s/keys :req-un [])) + +(defmethod action-opts-spec :open-url [_] + (s/keys :req-un [::url])) + +(s/def ::action-opts + (s/multi-spec action-opts-spec ::action-type)) + +;; -- Interaction + +(s/def ::classifier + (s/keys :req-un [::event-type + ::action-type])) + +(s/def ::interaction + (s/merge ::classifier + ::event-opts + ::action-opts)) + +(s/def ::interactions + (s/coll-of ::interaction :kind vector?)) + +(def default-interaction + {:event-type :click + :action-type :navigate + :destination nil}) + +(def default-delay 600) + +;; -- Helpers for interaction + +(declare calc-overlay-pos-initial) + +(defn set-event-type + [interaction event-type shape] + (us/verify ::interaction interaction) + (us/verify ::event-type event-type) + (assert (or (not= event-type :after-delay) + (= (:type shape) :frame))) + (if (= (:event-type interaction) event-type) + interaction + (case event-type + + :after-delay + (assoc interaction + :event-type event-type + :delay (get interaction :delay default-delay)) + + (assoc interaction + :event-type event-type)))) + + +(defn set-action-type + [interaction action-type] + (us/verify ::interaction interaction) + (us/verify ::action-type action-type) + (if (= (:action-type interaction) action-type) + interaction + (case action-type + + :navigate + (assoc interaction + :action-type action-type + :destination (get interaction :destination)) + + (:open-overlay :toggle-overlay) + (let [overlay-pos-type (get interaction :overlay-pos-type :center) + overlay-position (get interaction :overlay-position (gpt/point 0 0))] + (assoc interaction + :action-type action-type + :overlay-pos-type overlay-pos-type + :overlay-position overlay-position)) + + :close-overlay + (assoc interaction + :action-type action-type + :destination (get interaction :destination)) + + :prev-screen + (assoc interaction + :action-type action-type) + + :open-url + (assoc interaction + :action-type action-type + :url (get interaction :url ""))))) + +(defn has-delay + [interaction] + (= (:event-type interaction) :after-delay)) + +(defn set-delay + [interaction delay] + (us/verify ::interaction interaction) + (us/verify ::delay delay) + (assert (has-delay interaction)) + (assoc interaction :delay delay)) + +(defn has-destination + [interaction] + (#{:navigate :open-overlay :toggle-overlay :close-overlay} + (:action-type interaction))) + +(defn destination? + [interaction] + (and (has-destination interaction) + (some? (:destination interaction)))) + +(defn set-destination + [interaction destination] + (us/verify ::interaction interaction) + (us/verify ::destination destination) + (assert (has-destination interaction)) + (cond-> interaction + :always + (assoc :destination destination) + + (or (= (:action-type interaction) :open-overlay) + (= (:action-type interaction) :toggle-overlay)) + (assoc :overlay-pos-type :center + :overlay-position (gpt/point 0 0)))) + +(defn has-url + [interaction] + (= (:action-type interaction) :open-url)) + +(defn set-url + [interaction url] + (us/verify ::interaction interaction) + (us/verify ::url url) + (assert (has-url interaction)) + (assoc interaction :url url)) + +(defn has-overlay-opts + [interaction] + (#{:open-overlay :toggle-overlay} (:action-type interaction))) + +(defn set-overlay-pos-type + [interaction overlay-pos-type shape objects] + (us/verify ::interaction interaction) + (us/verify ::overlay-pos-type overlay-pos-type) + (assert (has-overlay-opts interaction)) + (assoc interaction + :overlay-pos-type overlay-pos-type + :overlay-position (calc-overlay-pos-initial (:destination interaction) + shape + objects + overlay-pos-type))) +(defn toggle-overlay-pos-type + [interaction overlay-pos-type shape objects] + (us/verify ::interaction interaction) + (us/verify ::overlay-pos-type overlay-pos-type) + (assert (has-overlay-opts interaction)) + (let [new-pos-type (if (= (:overlay-pos-type interaction) overlay-pos-type) + :manual + overlay-pos-type)] + (assoc interaction + :overlay-pos-type new-pos-type + :overlay-position (calc-overlay-pos-initial (:destination interaction) + shape + objects + new-pos-type)))) +(defn set-overlay-position + [interaction overlay-position] + (us/verify ::interaction interaction) + (us/verify ::overlay-position overlay-position) + (assert (has-overlay-opts interaction)) + (assoc interaction + :overlay-pos-type :manual + :overlay-position overlay-position)) + +(defn set-close-click-outside + [interaction close-click-outside] + (us/verify ::interaction interaction) + (us/verify ::us/boolean close-click-outside) + (assert (has-overlay-opts interaction)) + (assoc interaction :close-click-outside close-click-outside)) + +(defn set-background-overlay + [interaction background-overlay] + (us/verify ::interaction interaction) + (us/verify ::us/boolean background-overlay) + (assert (has-overlay-opts interaction)) + (assoc interaction :background-overlay background-overlay)) + +(defn- calc-overlay-pos-initial + [destination shape objects overlay-pos-type] + (if (= overlay-pos-type :manual) + (let [dest-frame (get objects destination) + overlay-size (:selrect dest-frame) + orig-frame (if (= (:type shape) :frame) + shape + (get objects (:frame-id shape))) + frame-size (:selrect orig-frame)] + (gpt/point (/ (- (:width frame-size) (:width overlay-size)) 2) + (/ (- (:height frame-size) (:height overlay-size)) 2))) + (gpt/point 0 0))) + +(defn calc-overlay-position + [interaction base-frame dest-frame frame-offset] + (us/verify ::interaction interaction) + (assert (has-overlay-opts interaction)) + (if (nil? dest-frame) + (gpt/point 0 0) + (let [overlay-size (:selrect dest-frame) + base-frame-size (:selrect base-frame)] + (case (:overlay-pos-type interaction) + :center + (gpt/point (/ (- (:width base-frame-size) (:width overlay-size)) 2) + (/ (- (:height base-frame-size) (:height overlay-size)) 2)) + + :top-left + (gpt/point 0 0) + + :top-right + (gpt/point (- (:width base-frame-size) (:width overlay-size)) + 0) + + :top-center + (gpt/point (/ (- (:width base-frame-size) (:width overlay-size)) 2) + 0) + + :bottom-left + (gpt/point 0 + (- (:height base-frame-size) (:height overlay-size))) + + :bottom-right + (gpt/point (- (:width base-frame-size) (:width overlay-size)) + (- (:height base-frame-size) (:height overlay-size))) + + :bottom-center + (gpt/point (/ (- (:width base-frame-size) (:width overlay-size)) 2) + (- (:height base-frame-size) (:height overlay-size))) + + :manual + (gpt/add (:overlay-position interaction) frame-offset))))) + +;; -- Helpers for interactions + +(defn add-interaction + [interactions interaction] + (conj (or interactions []) interaction)) + +(defn remove-interaction + [interactions index] + (let [interactions (or interactions [])] + (into (subvec interactions 0 index) + (subvec interactions (inc index))))) + +(defn update-interaction + [interactions index update-fn] + (update interactions index update-fn)) + +(defn remap-interactions + "Update all interactions whose destination points to a shape in the + map to the new id. And remove the ones whose destination does not exist + in the map nor in the objects tree." + [interactions ids-map objects] + (when (some? interactions) + (let [xform (comp (filter (fn [interaction] + (let [destination (:destination interaction)] + (or (nil? destination) + (contains? ids-map destination) + (contains? objects destination))))) + (map (fn [interaction] + (d/update-when interaction :destination #(get ids-map % %)))))] + (into [] xform interactions)))) + +(defn actionable? + "Check if there is any interaction that is clickable by the user" + [interactions] + (some #(= (:event-type %) :click) interactions)) + +(defn flow-origin? + "Check if there is any interaction that is the start or the continuation of a flow" + [interactions] + (some #(and (#{:navigate :open-overlay :toggle-overlay :close-overlay} (:action-type %)) + (some? (:destination %))) + interactions)) + +(defn flow-to? + "Check if there is any interaction that flows into the given frame" + [interactions frame-id] + (some #(and (#{:navigate :open-overlay :toggle-overlay :close-overlay} (:action-type %)) + (= (:destination %) frame-id)) + interactions)) diff --git a/common/src/app/common/types/page_options.cljc b/common/src/app/common/types/page_options.cljc new file mode 100644 index 000000000..c15258eda --- /dev/null +++ b/common/src/app/common/types/page_options.cljc @@ -0,0 +1,94 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.types.page-options + (:require + [app.common.data :as d] + [app.common.spec :as us] + [clojure.spec.alpha :as s])) + +;; --- Grid options + +(s/def :artboard-grid.color/color ::us/string) +(s/def :artboard-grid.color/opacity ::us/safe-number) + +(s/def :artboard-grid/size ::us/safe-integer) +(s/def :artboard-grid/color (s/keys :req-un [:artboard-grid.color/color + :artboard-grid.color/opacity])) +(s/def :artboard-grid/type #{:stretch :left :center :right}) +(s/def :artboard-grid/item-length (s/nilable ::us/safe-integer)) +(s/def :artboard-grid/gutter (s/nilable ::us/safe-integer)) +(s/def :artboard-grid/margin (s/nilable ::us/safe-integer)) + +(s/def :artboard-grid/square + (s/keys :req-un [:artboard-grid/size + :artboard-grid/color])) + +(s/def :artboard-grid/column + (s/keys :req-un [:artboard-grid/size + :artboard-grid/color + :artboard-grid/margin + :artboard-grid/gutter] + :opt-un [:artboard-grid/type + :artboard-grid/item-length])) + +(s/def :artboard-grid/row :artboard-grid/column) + +(s/def ::saved-grids + (s/keys :opt-un [:artboard-grid/square + :artboard-grid/row + :artboard-grid/column])) + +;; --- Background options + +(s/def ::background string?) + +;; --- Flow options + +(s/def :interactions-flow/id ::us/uuid) +(s/def :interactions-flow/name ::us/string) +(s/def :interactions-flow/starting-frame ::us/uuid) + +(s/def ::flow + (s/keys :req-un [:interactions-flow/id + :interactions-flow/name + :interactions-flow/starting-frame])) + +(s/def ::flows + (s/coll-of ::flow :kind vector?)) + +;; --- Options + +(s/def ::options + (s/keys :opt-un [::background + ::saved-grids + ::flows])) + +;; --- Helpers for flow + +(defn rename-flow + [flow name] + (assoc flow :name name)) + +;; --- Helpers for flows + +(defn add-flow + [flows flow] + (conj (or flows []) flow)) + +(defn remove-flow + [flows flow-id] + (d/removev #(= (:id %) flow-id) flows)) + +(defn update-flow + [flows flow-id update-fn] + (let [index (d/index-of-pred flows #(= (:id %) flow-id))] + (update flows index update-fn))) + +(defn get-frame-flow + [flows frame-id] + (d/seek #(= (:starting-frame %) frame-id) flows)) + diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index bdfbd3341..6b8290e3a 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -3,10 +3,10 @@ LABEL maintainer="Andrey Antukh " ARG DEBIAN_FRONTEND=noninteractive -ENV NODE_VERSION=v14.17.5 \ - CLOJURE_VERSION=1.10.3.933 \ - CLJKONDO_VERSION=2021.07.28 \ - BABASHKA_VERSION=0.5.1 \ +ENV NODE_VERSION=v14.17.6 \ + CLOJURE_VERSION=1.10.3.967 \ + CLJKONDO_VERSION=2021.09.15 \ + BABASHKA_VERSION=0.6.1 \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 @@ -28,6 +28,7 @@ RUN set -ex; \ rlwrap \ unzip \ fakeroot \ + netcat \ ; \ echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \ locale-gen; \ @@ -172,9 +173,10 @@ COPY files/vimrc /root/.vimrc COPY files/tmux.conf /root/.tmux.conf COPY files/sudoers /etc/sudoers -COPY files/start-tmux.sh /home/start-tmux.sh -COPY files/entrypoint.sh /home/entrypoint.sh -COPY files/init.sh /home/init.sh +COPY files/start-tmux.sh /home/start-tmux.sh +COPY files/start-tmux-back.sh /home/start-tmux-back.sh +COPY files/entrypoint.sh /home/entrypoint.sh +COPY files/init.sh /home/init.sh ENTRYPOINT ["/home/entrypoint.sh"] CMD ["/home/init.sh"] diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml index c15bd7d00..764b638db 100644 --- a/docker/devenv/docker-compose.yaml +++ b/docker/devenv/docker-compose.yaml @@ -5,7 +5,7 @@ networks: driver: bridge ipam: config: - - subnet: 172.177.09.0/24 + - subnet: 172.177.9.0/24 volumes: postgres_data: @@ -13,6 +13,7 @@ volumes: services: main: + profiles: ["full"] privileged: true image: "penpotapp/devenv:latest" build: @@ -49,6 +50,57 @@ services: - PENPOT_SMTP_PASSWORD= - PENPOT_SMTP_SSL=false - PENPOT_SMTP_TLS=false + - PENPOT_FLAGS="enable-cors" + + # LDAP setup + - PENPOT_LDAP_HOST=ldap + - PENPOT_LDAP_PORT=10389 + - PENPOT_LDAP_SSL=false + - PENPOT_LDAP_STARTTLS=false + - PENPOT_LDAP_BASE_DN=ou=people,dc=planetexpress,dc=com + - PENPOT_LDAP_BIND_DN=cn=admin,dc=planetexpress,dc=com + - PENPOT_LDAP_BIND_PASSWORD=GoodNewsEveryone + - PENPOT_LDAP_ATTRS_USERNAME=uid + - PENPOT_LDAP_ATTRS_EMAIL=mail + - PENPOT_LDAP_ATTRS_FULLNAME=cn + - PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto + + backend: + profiles: ["backend"] + privileged: true + image: "penpotapp/devenv:latest" + build: + context: "." + container_name: "penpot-backend" + stop_signal: SIGINT + + depends_on: + - postgres + - redis + + volumes: + - "user_data:/home/penpot/" + - "${PWD}:/home/penpot/penpot" + + ports: + - 6060:6060 + - 6061:6061 + - 9090:9090 + + environment: + - EXTERNAL_UID=${CURRENT_USER_ID} + - PENPOT_SECRET_KEY=super-secret-devenv-key + # STMP setup + - PENPOT_SMTP_ENABLED=true + - PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com + - PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com + - PENPOT_SMTP_HOST=mailer + - PENPOT_SMTP_PORT=1025 + - PENPOT_SMTP_USERNAME= + - PENPOT_SMTP_PASSWORD= + - PENPOT_SMTP_SSL=false + - PENPOT_SMTP_TLS=false + - PENPOT_FLAGS="enable-cors" # LDAP setup - PENPOT_LDAP_HOST=ldap diff --git a/docker/devenv/files/start-tmux-back.sh b/docker/devenv/files/start-tmux-back.sh new file mode 100755 index 000000000..7e427080f --- /dev/null +++ b/docker/devenv/files/start-tmux-back.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +sudo chown penpot:users /home/penpot + +cd ~; + +source ~/.bashrc + +set -e; + +echo "[start-tmux.sh] Installing node dependencies" +pushd ~/penpot/exporter/ +yarn install +popd + +tmux -2 new-session -d -s penpot + +tmux rename-window -t penpot:0 'exporter' +tmux select-window -t penpot:0 +tmux send-keys -t penpot 'cd penpot/exporter' enter C-l +tmux send-keys -t penpot 'rm -f target/app.js*' enter C-l +tmux send-keys -t penpot 'clojure -M:dev:shadow-cljs watch main' enter + +tmux split-window -v +tmux send-keys -t penpot 'cd penpot/exporter' enter C-l +tmux send-keys -t penpot './scripts/wait-and-start.sh' enter + +tmux new-window -t penpot:1 -n 'backend' +tmux select-window -t penpot:1 +tmux send-keys -t penpot 'cd penpot/backend' enter C-l +tmux send-keys -t penpot './scripts/start-dev' enter + +tmux -2 attach-session -t penpot diff --git a/docker/devenv/files/start-tmux.sh b/docker/devenv/files/start-tmux.sh index 6676425a8..2891f18bc 100755 --- a/docker/devenv/files/start-tmux.sh +++ b/docker/devenv/files/start-tmux.sh @@ -18,6 +18,11 @@ popd tmux -2 new-session -d -s penpot +tmux rename-window -t penpot:0 'gulp' +tmux select-window -t penpot:0 +tmux send-keys -t penpot 'cd penpot/frontend' enter C-l +tmux send-keys -t penpot 'npx gulp watch' enter + tmux new-window -t penpot:1 -n 'shadow watch' tmux select-window -t penpot:1 tmux send-keys -t penpot 'cd penpot/frontend' enter C-l @@ -38,9 +43,4 @@ tmux select-window -t penpot:3 tmux send-keys -t penpot 'cd penpot/backend' enter C-l tmux send-keys -t penpot './scripts/start-dev' enter -tmux rename-window -t penpot:0 'gulp' -tmux select-window -t penpot:0 -tmux send-keys -t penpot 'cd penpot/frontend' enter C-l -tmux send-keys -t penpot 'npx gulp watch' enter - tmux -2 attach-session -t penpot diff --git a/exporter/deps.edn b/exporter/deps.edn index 5c55ace1e..328023c7c 100644 --- a/exporter/deps.edn +++ b/exporter/deps.edn @@ -14,7 +14,7 @@ :dev {:extra-deps - {thheller/shadow-cljs {:mvn/version "2.15.2"}}} + {thheller/shadow-cljs {:mvn/version "2.15.10"}}} :shadow-cljs {:main-opts ["-m" "shadow.cljs.devtools.cli"]} diff --git a/exporter/package.json b/exporter/package.json index 52cfa2501..5f70a41b7 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -9,10 +9,12 @@ "author": "UXBOX LABS SL", "license": "SEE LICENSE IN ", "dependencies": { + "@sentry/node": "^6.13.1", + "@sentry/tracing": "^6.13.1", + "cookies": "^0.8.0", "generic-pool": "^3.8.2", "inflation": "^2.0.0", "jszip": "^3.7.0", - "koa": "^2.13.0", "luxon": "^2.0.1", "puppeteer-core": "^10.1.0", "raw-body": "^2.4.1", @@ -20,7 +22,7 @@ "xregexp": "^5.0.2" }, "devDependencies": { - "shadow-cljs": "^2.15.2", - "source-map-support": "^0.5.19" + "shadow-cljs": "^2.15.10", + "source-map-support": "^0.5.20" } } diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index 35f1b9a0b..a3775a048 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -108,7 +108,6 @@ (p/then (fn [browser] (let [id (deref pool-browser-id)] (log/info :origin "factory" :action "create" :browser-id id) - (unchecked-set browser "__num_use" 0) (unchecked-set browser "__id" id) (swap! pool-browser-id inc) browser)))))) @@ -118,16 +117,9 @@ (.close ^js obj))) (validate [obj] - (let [max-use (cf/get :browser-max-usage 10) - num-use (unchecked-get obj "__num_use") - id (unchecked-get obj "__id")] - - (log/info :origin "factory" :action "validate" :browser-id id :max-use max-use :num-use num-use :obj obj) - (if (> num-use max-use) - (p/resolved false) - (do - (unchecked-set obj "__num_use" (inc num-use)) - (p/resolved (.isConnected ^js obj))))))] + (let [id (unchecked-get obj "__id")] + (log/info :origin "factory" :action "validate" :browser-id id :obj obj) + (p/resolved (.isConnected ^js obj))))] #js {:create create :destroy destroy @@ -139,10 +131,10 @@ (let [opts #js {:max (cf/get :browser-pool-max 3) :min (cf/get :browser-pool-min 0) :testOnBorrow true - :evictionRunIntervalMillis 30000 + :evictionRunIntervalMillis 5000 :numTestsPerEvictionRun 5 :acquireTimeoutMillis 120000 ; 2min - :idleTimeoutMillis 30000}] + :idleTimeoutMillis 10000}] (reset! pool (gp/createPool browser-pool-factory opts)) (p/resolved nil))) @@ -155,19 +147,29 @@ (p/then (fn [] (.clear ^js pool)))))) (defn exec! - [f] - (letfn [(on-acquire [pool browser] - (p/let [ctx (.createIncognitoBrowserContext ^js browser) - page (.newPage ^js ctx)] - (-> (p/do! (f page)) - (p/handle - (fn [result error] - (-> (p/do! (.close ^js ctx) - (.release ^js pool browser)) - (p/handle (fn [_ _] - (if result - (p/resolved result) - (p/rejected error))))))))))] + [callback] + (letfn [(on-release [pool browser ctx result error] + (-> (p/do! (.close ^js ctx)) + (p/handle + (fn [_ _] + (.release ^js pool browser))) + (p/handle + (fn [_ _] + (let [id (unchecked-get browser "__id")] + (log/info :origin "exec" :action "release" :browser-id id)) + (if result + (p/resolved result) + (p/rejected error)))))) + + (on-context [pool browser ctx] + (-> (p/do! (.newPage ^js ctx)) + (p/then callback) + (p/handle #(on-release pool browser ctx %1 %2)))) + + (on-acquire [pool browser] + (-> (.createIncognitoBrowserContext ^js browser) + (p/then #(on-context pool browser %))))] + (when-let [pool (deref pool)] - (-> (.acquire ^js pool) + (-> (p/do! (.acquire ^js pool)) (p/then (partial on-acquire pool)))))) diff --git a/exporter/src/app/config.cljs b/exporter/src/app/config.cljs index 7c79a533c..307019171 100644 --- a/exporter/src/app/config.cljs +++ b/exporter/src/app/config.cljs @@ -7,9 +7,12 @@ (ns app.config (:refer-clojure :exclude [get]) (:require + ["fs" :as fs] ["process" :as process] + [app.common.exceptions :as ex] [app.common.data :as d] [app.common.spec :as us] + [app.common.version :as v] [cljs.core :as c] [cljs.pprint] [cljs.spec.alpha :as s] @@ -18,18 +21,26 @@ (def defaults {:public-uri "http://localhost:3449" + :tenant "dev" + :host "devenv" :http-server-port 6061 :browser-concurrency 5 :browser-strategy :incognito}) -(s/def ::browser-executable-path ::us/string) -(s/def ::public-uri ::us/string) -(s/def ::http-server-port ::us/integer) (s/def ::browser-concurrency ::us/integer) +(s/def ::browser-executable-path ::us/string) (s/def ::browser-strategy ::us/keyword) +(s/def ::http-server-port ::us/integer) +(s/def ::public-uri ::us/string) +(s/def ::sentry-dsn ::us/string) +(s/def ::tenant ::us/string) +(s/def ::host ::us/string) (s/def ::config (s/keys :opt-un [::public-uri + ::sentry-dsn + ::host + ::tenant ::http-server-port ::browser-concurrency ::browser-strategy @@ -59,6 +70,10 @@ (def config (atom (prepare-config))) +(def version + (atom (v/parse (or (some-> (ex/ignoring (fs/readFileSync "version.txt")) + (str/trim)) + "%version%")))) (defn get "A configuration getter." diff --git a/exporter/src/app/core.cljs b/exporter/src/app/core.cljs index 7fafd8393..29eb8865d 100644 --- a/exporter/src/app/core.cljs +++ b/exporter/src/app/core.cljs @@ -10,11 +10,13 @@ [lambdaisland.glogi.console :as glogi-console] [promesa.core :as p] [app.http :as http] + [app.sentry :as sentry] [app.config] [app.browser :as bwr])) (glogi-console/install!) (enable-console-print!) +(sentry/init!) (defonce state (atom nil)) diff --git a/exporter/src/app/http.cljs b/exporter/src/app/http.cljs index c8d3d4168..a36e030d9 100644 --- a/exporter/src/app/http.cljs +++ b/exporter/src/app/http.cljs @@ -10,6 +10,9 @@ [app.http.export :refer [export-handler]] [app.http.export-frames :refer [export-frames-handler]] [app.http.impl :as impl] + [app.util.transit :as t] + [app.sentry :as sentry] + [cuerdas.core :as str] [lambdaisland.glogi :as log] [promesa.core :as p] [reitit.core :as r])) @@ -20,13 +23,48 @@ (def instance (atom nil)) +(defn- on-error + [error request] + (let [{:keys [type message code] :as data} (ex-data error)] + (sentry/capture-exception error {::sentry/request request + :ex-data data}) + + (cond + (= :validation type) + (let [header (get-in request [:headers "accept"])] + (if (and (str/starts-with? header "text/html") + (= :spec-validation (:code data))) + {:status 400 + :headers {"content-type" "text/html"} + :body (str "
" (:explain data) "
\n")} + {:status 400 + :headers {"content-type" "text/html"} + :body (str "
" (:explain data) "
\n")})) + + (and (= :internal type) + (= :browser-not-ready code)) + {:status 503 + :headers {"x-error" (t/encode data)} + :body ""} + + :else + (do + (log/error :msg "Unexpected error" :error error) + (js/console.error error) + {:status 500 + :headers {"x-error" (t/encode data)} + :body ""})))) + (defn init [] (let [router (r/router routes) - handler (impl/handler router) - server (impl/server handler) + handler (impl/router-handler router) + server (impl/server handler on-error) port (cf/get :http-server-port 6061)] (.listen server port) + (log/info :msg "welcome to penpot" + :module "exporter" + :version (:full @cf/version)) (log/info :msg "starting http server" :port port) (reset! instance server))) diff --git a/exporter/src/app/http/impl.cljs b/exporter/src/app/http/impl.cljs index 3401194d4..67d54f6d8 100644 --- a/exporter/src/app/http/impl.cljs +++ b/exporter/src/app/http/impl.cljs @@ -7,8 +7,8 @@ (ns app.http.impl (:require ["http" :as http] + ["cookies" :as Cookies] ["inflation" :as inflate] - ["koa" :as koa] ["raw-body" :as raw-body] [app.util.transit :as t] [cuerdas.core :as str] @@ -17,94 +17,66 @@ [promesa.core :as p] [reitit.core :as r])) +(def methods-with-body + #{"POST" "PUT" "DELETE"}) + (defn- match - [router ctx] - (let [uri (u/uri (unchecked-get ctx "originalUrl"))] - (when-let [match (r/match-by-path router (:path uri))] - (assoc match :query-params (u/query-string->map (:query uri)))))) - -(defn- handle-error - [error request] - (let [{:keys [type message code] :as data} (ex-data error)] - (cond - (= :validation type) - (let [header (get-in request [:headers "accept"])] - (if (and (str/starts-with? header "text/html") - (= :spec-validation (:code data))) - {:status 400 - :headers {"content-type" "text/html"} - :body (str "
" (:explain data) "
\n")} - {:status 400 - :headers {"content-type" "text/html"} - :body (str "
" (:explain data) "
\n")})) - - (and (= :internal type) - (= :browser-not-ready code)) - {:status 503 - :headers {"x-error" (t/encode data)} - :body ""} - - :else - (do - (log/error :msg "Unexpected error" - :error error) - (js/console.error error) - {:status 500 - :headers {"x-error" (t/encode data)} - :body ""})))) + [router {:keys [path query] :as request}] + (when-let [match (r/match-by-path router path)] + (assoc match :query-params (u/query-string->map query)))) (defn- handle-response - [ctx {:keys [body headers status] :or {headers {} status 200}}] - (run! (fn [[k v]] (.set ^js ctx k v)) headers) - (set! (.-body ^js ctx) body) - (set! (.-status ^js ctx) status) - nil) + [req res] + (fn [{:keys [body headers status] :or {headers {} status 200}}] + (.writeHead ^js res status (clj->js headers)) + (.end ^js res body))) (defn- parse-headers - [ctx] - (let [orig (unchecked-get ctx "headers")] + [req] + (let [orig (unchecked-get req "headers")] (persistent! (reduce #(assoc! %1 %2 (unchecked-get orig %2)) (transient {}) (js/Object.keys orig))))) -(def parse-body? #{"POST" "PUT" "DELETE"}) - (defn- parse-body - [ctx] - (let [headers (unchecked-get ctx "headers") - ctype (unchecked-get headers "content-type")] - (when (parse-body? (.-method ^js ctx)) - (-> (inflate (.-req ^js ctx)) - (raw-body #js {:limit "5mb" :encoding "utf8"}) + [req] + (let [headers (unchecked-get req "headers") + method (unchecked-get req "method") + ctype (unchecked-get headers "content-type") + opts #js {:limit "5mb" :encoding "utf8"}] + (when (contains? methods-with-body method) + (-> (raw-body (inflate req) opts) (p/then (fn [data] - (cond-> data - (= ctype "application/transit+json") - (t/decode)))))))) + (cond-> data + (= ctype "application/transit+json") + (t/decode)))))))) -(defn- wrap-handler - [f] - (fn [ctx] - (p/let [cookies (unchecked-get ctx "cookies") - headers (parse-headers ctx) - body (parse-body ctx) - request {:method (str/lower (unchecked-get ctx "method")) - :body body - :ctx ctx - :headers headers - :cookies cookies}] - (-> (p/do! (f request)) - (p/then (fn [rsp] - (when (map? rsp) - (handle-response ctx rsp)))) - (p/catch (fn [err] - (->> (handle-error err request) - (handle-response ctx)))))))) +(defn- handler-adapter + [handler on-error] + (fn [req res] + (let [cookies (new Cookies req res) + headers (parse-headers req) + uri (u/uri (unchecked-get req "url")) + request {:method (str/lower (unchecked-get req "method")) + :path (:path uri) + :query (:query uri) + :url uri + :headers headers + :cookies cookies + :internal-request req + :internal-response res}] + (-> (parse-body req) + (p/then (fn [body] + (let [request (assoc request :body body)] + (handler request)))) + (p/catch (fn [error] (on-error error request))) + (p/then (handle-response req res)))))) -(defn- router-handler +(defn router-handler [router] - (fn [{:keys [ctx body] :as request}] - (let [route (match router ctx) + (fn [{:keys [body] :as request}] + (let [route (match router request) params (merge {} (:query-params route) (:path-params route) @@ -120,16 +92,5 @@ :body "Not found"})))) (defn server - [handler] - (.createServer http @handler)) - -(defn handler - [router] - (let [instance (doto (new koa) - (.use (-> (router-handler router) - (wrap-handler))))] - (specify! instance - cljs.core/IDeref - (-deref [_] - (.callback instance))))) - + [handler on-error] + (.createServer ^js http (handler-adapter handler on-error))) diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 93e0649f4..54544aa24 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -210,11 +210,10 @@ :height (.. attrs -height -value) :colors (.split colors ",")})) - (extract-single-node [node] + (extract-single-node [[shot node]] (log/trace :fn :extract-single-node) - (p/let [attrs (bw/eval! node extract-element-attrs) - shot (bw/screenshot node {:omit-background? true :type "png"})] + (p/let [attrs (bw/eval! node extract-element-attrs)] {:id (unchecked-get attrs "id") :x (unchecked-get attrs "x") :y (unchecked-get attrs "y") @@ -223,13 +222,21 @@ :colors (vec (unchecked-get attrs "colors")) :data shot})) + (resolve-text-node [page node] + (p/let [attrs (bw/eval! node extract-element-attrs) + id (unchecked-get attrs "id") + text-node (bw/select page (str "#screenshot-text-" id " foreignObject")) + shot (bw/screenshot text-node {:omit-background? true :type "png"})] + [shot node])) + (clean-temp-data [{:keys [tempdir] :as node}] (p/do! (sh/rmdir! tempdir) (dissoc node :tempdir))) - (process-text-node [item] + (process-text-node [page item] (-> (p/resolved item) + (p/then (partial resolve-text-node page)) (p/then extract-single-node) (p/then trace-node) (p/then clean-temp-data))) @@ -237,7 +244,7 @@ (process-text-nodes [page] (log/trace :fn :process-text-nodes) (-> (bw/select-all page "#screenshot foreignObject") - (p/then (fn [nodes] (p/all (map process-text-node nodes)))))) + (p/then (fn [nodes] (p/all (map (partial process-text-node page) nodes)))))) (extract-svg [page] (p/let [dom (bw/select page "#screenshot") @@ -271,7 +278,7 @@ (p/let [page (render-in-page page rctx)] (extract-svg page)))] - (let [path (str "/render-object/" file-id "/" page-id "/" object-id) + (let [path (str "/render-object/" file-id "/" page-id "/" object-id "?render-texts=true") uri (-> (u/uri (cf/get :public-uri)) (assoc :path "/") (assoc :fragment path)) diff --git a/exporter/src/app/sentry.cljs b/exporter/src/app/sentry.cljs new file mode 100644 index 000000000..4f7c9be5c --- /dev/null +++ b/exporter/src/app/sentry.cljs @@ -0,0 +1,44 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.sentry + (:require + ["@sentry/node" :as sentry] + ["@sentry/tracing" :as sentry-t] + [app.common.data :as d] + [app.config :as cf])) + +(defn init! + [] + (when-let [dsn (cf/get :sentry-dsn)] + (sentry/init + #js {:dsn dsn + :release (str "frontend@" (:base @cf/version)) + :serverName (cf/get :host) + :environment (cf/get :tenant) + :autoSessionTracking false + :attachStacktrace false + :maxBreadcrumbs 20 + :tracesSampleRate 1.0}))) + +(def parse-request (unchecked-get sentry/Handlers "parseRequest")) + +(defn capture-exception + [error {:keys [::request ::tags] :as context}] + (let [context (-> (dissoc context ::request ::tags) + (d/without-nils))] + (sentry/withScope + (fn [scope] + (.addEventProcessor ^js scope (fn [event] + (let [node-request (:internal-request request)] + (parse-request event node-request)))) + (doseq [[k v] tags] + (.setTag ^js scope (if (keyword? k) (name k) (str k)) (str v))) + + (doseq [[k v] context] + (.setContext ^js scope (if (keyword? k) (name k) (str k)) (clj->js v))) + + (sentry/captureException error))))) diff --git a/exporter/yarn.lock b/exporter/yarn.lock index 62e1b751b..a671fedf5 100644 --- a/exporter/yarn.lock +++ b/exporter/yarn.lock @@ -10,6 +10,74 @@ core-js-pure "^3.16.0" regenerator-runtime "^0.13.4" +"@sentry/core@6.13.1": + version "6.13.1" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.13.1.tgz#158cb7391c1aed4083fe12f40a3aecc951e3d354" + integrity sha512-IGitAHYtsDPhPPcSTDqL/NMOwTbrqPezQOo4rlU1p1lpyZKSKrgzn/8LCrb+eRV2ffaio6+3kwqjCN2SvU8S7A== + dependencies: + "@sentry/hub" "6.13.1" + "@sentry/minimal" "6.13.1" + "@sentry/types" "6.13.1" + "@sentry/utils" "6.13.1" + tslib "^1.9.3" + +"@sentry/hub@6.13.1": + version "6.13.1" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.13.1.tgz#587227fa502e3ed699878ac903f2a5100849cb00" + integrity sha512-O7bR5suyVNTNyr6tm0IjhZ7NvxSHIbHoy5KYbVv05HQ/DmvAbYWq4dtOMvYQuuTD9krGkZdwPg4Gm6KnFaKqoQ== + dependencies: + "@sentry/types" "6.13.1" + "@sentry/utils" "6.13.1" + tslib "^1.9.3" + +"@sentry/minimal@6.13.1": + version "6.13.1" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.13.1.tgz#86865cc0d6090d9c8be2c44ec7f14a9c66910e7d" + integrity sha512-waRLPRFT1G95LsklH25LvfJy4vSe54PPRSeAGNPa4xVLCP56CnbNXGEXGDyPUewtqESwVpRG6GPL1QRV67IixA== + dependencies: + "@sentry/hub" "6.13.1" + "@sentry/types" "6.13.1" + tslib "^1.9.3" + +"@sentry/node@^6.13.1": + version "6.13.1" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.13.1.tgz#9b76d5746a6e851a11bcdc25097177cbb28fcc82" + integrity sha512-uSAsRPGeTNb6pEzHQ4/RkaylO95f6SmS1yzMizlMkgZq5qPDkmKgN+sZ04g58XlZ6nDKTxYoLSZsfxdaIZNTcw== + dependencies: + "@sentry/core" "6.13.1" + "@sentry/hub" "6.13.1" + "@sentry/tracing" "6.13.1" + "@sentry/types" "6.13.1" + "@sentry/utils" "6.13.1" + cookie "^0.4.1" + https-proxy-agent "^5.0.0" + lru_map "^0.3.3" + tslib "^1.9.3" + +"@sentry/tracing@6.13.1", "@sentry/tracing@^6.13.1": + version "6.13.1" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.13.1.tgz#eec84740a2566132c14c151e855f2a8a2dac3716" + integrity sha512-UF0yHtWXi5SfDa5oKCSw463P3AyAf635w6zFMiLV6kt8DDjnOwJRcT7dekRIU8F4Du+2nWJFPoXsmt1sablycw== + dependencies: + "@sentry/hub" "6.13.1" + "@sentry/minimal" "6.13.1" + "@sentry/types" "6.13.1" + "@sentry/utils" "6.13.1" + tslib "^1.9.3" + +"@sentry/types@6.13.1": + version "6.13.1" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.13.1.tgz#650e5e5fe4aa415987e14cfa2ca03e5d5eb3e347" + integrity sha512-dYm8qv/O6QhOCmWi5rlJBX9rjEqvnjnZH1zqhQCWhMmF9aYS151Y41P1C0TS0o17Z0ClonLiYMG1J+zE/Pmtqg== + +"@sentry/utils@6.13.1": + version "6.13.1" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.13.1.tgz#5c974c4235615ac50caed2b25c30b33a6a234425" + integrity sha512-qFDup/nBj2u2rAcQADFG3njVYUCo4XOQkCT7XdA5Flg1a++r6tIBdjiRyyjb8nqwHZ/OQsKr/V/EQaJiW29ETA== + dependencies: + "@sentry/types" "6.13.1" + tslib "^1.9.3" + "@types/node@*": version "16.6.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50" @@ -22,14 +90,6 @@ dependencies: "@types/node" "*" -accepts@^1.3.5: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== - dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" - agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -37,11 +97,6 @@ agent-base@6: dependencies: debug "4" -any-promise@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= - asn1.js@^5.2.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -205,14 +260,6 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -cache-content-type@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" - integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA== - dependencies: - mime-types "^2.1.18" - ylru "^1.2.0" - chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -226,11 +273,6 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -246,19 +288,12 @@ constants-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= -content-disposition@~0.5.2: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== - dependencies: - safe-buffer "5.1.2" +cookie@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== -content-type@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -cookies@~0.8.0: +cookies@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== @@ -338,33 +373,16 @@ debug@4.3.1: dependencies: ms "2.1.2" -debug@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -deep-equal@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" - integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -depd@^2.0.0, depd@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + des.js@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" @@ -373,11 +391,6 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" -destroy@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - devtools-protocol@0.0.901419: version "0.0.901419" resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd" @@ -397,11 +410,6 @@ domain-browser@^1.1.1: resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - elliptic@^6.5.3: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" @@ -415,11 +423,6 @@ elliptic@^6.5.3: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" -encodeurl@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -427,11 +430,6 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -escape-html@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - events@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -471,11 +469,6 @@ find-up@^4.0.0: locate-path "^5.0.0" path-exists "^4.0.0" -fresh@~0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -510,18 +503,6 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -has-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== - -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - hash-base@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" @@ -548,15 +529,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -http-assert@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.4.1.tgz#c5f725d677aa7e873ef736199b89686cceb37878" - integrity sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw== - dependencies: - deep-equal "~1.0.1" - http-errors "~1.7.2" - -http-errors@1.7.3, http-errors@~1.7.2: +http-errors@1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== @@ -567,23 +540,12 @@ http-errors@1.7.3, http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@^1.6.3: - version "1.8.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507" - integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@5.0.0: +https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== @@ -636,13 +598,6 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -670,55 +625,6 @@ keygrip@~1.1.0: dependencies: tsscmp "1.0.6" -koa-compose@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" - integrity sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec= - dependencies: - any-promise "^1.1.0" - -koa-compose@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" - integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== - -koa-convert@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" - integrity sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA= - dependencies: - co "^4.6.0" - koa-compose "^3.0.0" - -koa@^2.13.0: - version "2.13.1" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.1.tgz#6275172875b27bcfe1d454356a5b6b9f5a9b1051" - integrity sha512-Lb2Dloc72auj5vK4X4qqL7B5jyDPQaZucc9sR/71byg7ryoD1NCaCm63CShk9ID9quQvDEi1bGR/iGjCG7As3w== - dependencies: - accepts "^1.3.5" - cache-content-type "^1.0.0" - content-disposition "~0.5.2" - content-type "^1.0.4" - cookies "~0.8.0" - debug "~3.1.0" - delegates "^1.0.0" - depd "^2.0.0" - destroy "^1.0.4" - encodeurl "^1.0.2" - escape-html "^1.0.3" - fresh "~0.5.2" - http-assert "^1.3.0" - http-errors "^1.6.3" - is-generator-function "^1.0.7" - koa-compose "^4.1.0" - koa-convert "^1.2.0" - on-finished "^2.3.0" - only "~0.0.2" - parseurl "^1.3.2" - statuses "^1.5.0" - type-is "^1.6.16" - vary "^1.1.2" - lie@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" @@ -733,6 +639,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lru_map@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" + integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0= + luxon@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.0.2.tgz#11f2cd4a11655fdf92e076b5782d7ede5bcdd133" @@ -747,11 +658,6 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -760,18 +666,6 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.49.0: - version "1.49.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" - integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== - -mime-types@^2.1.18, mime-types@~2.1.24: - version "2.1.32" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" - integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== - dependencies: - mime-db "1.49.0" - minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -801,21 +695,11 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.5" -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - node-fetch@2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" @@ -855,13 +739,6 @@ object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -on-finished@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -869,11 +746,6 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -only@~0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" - integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q= - os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" @@ -914,11 +786,6 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" -parseurl@^1.3.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - path-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" @@ -1107,16 +974,16 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -1142,11 +1009,6 @@ setprototypeof@1.1.1: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - sha.js@^2.4.0, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" @@ -1160,10 +1022,10 @@ shadow-cljs-jar@1.3.2: resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b" integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== -shadow-cljs@^2.15.2: - version "2.15.4" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.15.4.tgz#0d657fc8ab9a02d8980db5c49cb1622e8fc6fa52" - integrity sha512-xn8UsiVpOf2LTsQZLsCa910CcMCYdMRT6STAsgveOEIncC9cunGdqE7cTq69vTmIijVQmzf0A1nALidyzO3Hcw== +shadow-cljs@^2.15.10: + version "2.15.10" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.15.10.tgz#b1df65a51707a7c34dad2c788a12edeb41e0321c" + integrity sha512-r02f7pvZefiaATt3k7oAb8PNQKaA67iFoowS88Jaf8IClRQL3plBazkDAUWX5yQd9QfT4WhGr6kP0GTzTidQSg== dependencies: node-libs-browser "^2.2.1" readline-sync "^1.4.7" @@ -1179,10 +1041,10 @@ source-map-support@^0.4.15: dependencies: source-map "^0.5.6" -source-map-support@^0.5.19: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== +source-map-support@^0.5.20: + version "0.5.20" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" + integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -1197,7 +1059,7 @@ source-map@^0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -"statuses@>= 1.5.0 < 2", statuses@^1.5.0: +"statuses@>= 1.5.0 < 2": version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= @@ -1278,6 +1140,11 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tsscmp@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" @@ -1288,14 +1155,6 @@ tty-browserify@0.0.0: resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= -type-is@^1.6.16: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - unbzip2-stream@1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a" @@ -1336,11 +1195,6 @@ util@^0.11.0: dependencies: inherits "2.0.3" -vary@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - vm-browserify@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" @@ -1394,8 +1248,3 @@ yauzl@^2.10.0: dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" - -ylru@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f" - integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ== diff --git a/frontend/deps.edn b/frontend/deps.edn index 3ba3255a8..b878e722f 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -4,7 +4,7 @@ {:local/root "../common"} binaryage/devtools {:mvn/version "RELEASE"} - metosin/reitit-core {:mvn/version "0.5.13"} + metosin/reitit-core {:mvn/version "0.5.15"} funcool/beicon {:mvn/version "2021.07.05-1"} funcool/okulary {:mvn/version "2020.04.14-0"} @@ -23,7 +23,8 @@ :dev {:extra-deps - {thheller/shadow-cljs {:mvn/version "2.15.2"}}} + {thheller/shadow-cljs {:mvn/version "2.15.9"} + cider/cider-nrepl {:mvn/version "0.26.0"}}} :shadow-cljs {:main-opts ["-m" "shadow.cljs.devtools.cli"]} diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index 03c4f1c27..5a64cd8a3 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -33,7 +33,7 @@ paths.dist = "./target/dist/"; // Templates function readLocales() { - const langs = ["ca", "de", "el", "en", "es", "fr", "tr", "ru", "zh_CN", "pt_BR", "ro"]; + const langs = ["ar", "he", "ca", "de", "el", "en", "es", "fr", "tr", "ru", "zh_CN", "pt_BR", "ro"]; const result = {}; for (let lang of langs) { diff --git a/frontend/package.json b/frontend/package.json index 13261d45a..74b63207e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,15 @@ "defaults" ], "scripts": { - "validate-translations": "node ./scripts/validate-translations.js" + "validate-translations": "node ./scripts/validate-translations.js", + "watch-main": "shadow-cljs watch main", + "watch-gulp": "gulp watch", + "test-watch-compile": "shadow-cljs watch test", + "test-run": "node target/tests.js", + "test-watch-run": "nodemon --signal SIGKILL --watch target --exec npm run test-run", + "test-watch": "npm-run-all --parallel test-watch-compile test-watch-run", + "test": "npm run test-watch", + "start": "npm-run-all --parallel watch-gulp watch-main" }, "devDependencies": { "autoprefixer": "^10.2.4", @@ -27,21 +35,25 @@ "gulp-sourcemaps": "^3.0.0", "gulp-svg-sprite": "^1.5.0", "map-stream": "0.0.7", - "marked": "^2.1.1", + "marked": "^3.0.4", "mkdirp": "^1.0.4", + "nodemon": "^2.0.13", + "npm-run-all": "^4.1.5", "postcss": "^8.3.5", "postcss-clean": "^1.2.2", "rimraf": "^3.0.0", "sass": "^1.35.1", - "shadow-cljs": "2.15.2" + "shadow-cljs": "2.15.9" }, "dependencies": { + "@sentry/browser": "^6.12.0", + "@sentry/tracing": "^6.12.0", "date-fns": "^2.22.1", "draft-js": "^0.11.7", "highlight.js": "^11.0.1", "js-beautify": "^1.14.0", "jszip": "^3.6.0", - "luxon": "^1.26.0", + "luxon": "^2.0.2", "mousetrap": "^1.6.5", "opentype.js": "^1.3.3", "randomcolor": "^0.6.2", diff --git a/frontend/resources/images/features/advanced-proto.gif b/frontend/resources/images/features/advanced-proto.gif new file mode 100644 index 000000000..e8db03218 Binary files /dev/null and b/frontend/resources/images/features/advanced-proto.gif differ diff --git a/frontend/resources/images/features/booleans.gif b/frontend/resources/images/features/booleans.gif new file mode 100644 index 000000000..7b8f488be Binary files /dev/null and b/frontend/resources/images/features/booleans.gif differ diff --git a/frontend/resources/images/features/flows-proto.gif b/frontend/resources/images/features/flows-proto.gif new file mode 100644 index 000000000..eb3dfa6dc Binary files /dev/null and b/frontend/resources/images/features/flows-proto.gif differ diff --git a/frontend/resources/images/features/libraries-feature.gif b/frontend/resources/images/features/libraries-feature.gif new file mode 100644 index 000000000..3e996a6c2 Binary files /dev/null and b/frontend/resources/images/features/libraries-feature.gif differ diff --git a/frontend/resources/images/icons/boolean-difference.svg b/frontend/resources/images/icons/boolean-difference.svg new file mode 100644 index 000000000..4d5c7f6a8 --- /dev/null +++ b/frontend/resources/images/icons/boolean-difference.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/boolean-exclude.svg b/frontend/resources/images/icons/boolean-exclude.svg new file mode 100644 index 000000000..6a3865703 --- /dev/null +++ b/frontend/resources/images/icons/boolean-exclude.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/boolean-flatten.svg b/frontend/resources/images/icons/boolean-flatten.svg new file mode 100644 index 000000000..f7816f8b5 --- /dev/null +++ b/frontend/resources/images/icons/boolean-flatten.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/boolean-intersection.svg b/frontend/resources/images/icons/boolean-intersection.svg new file mode 100644 index 000000000..3480e6366 --- /dev/null +++ b/frontend/resources/images/icons/boolean-intersection.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/boolean-union.svg b/frontend/resources/images/icons/boolean-union.svg new file mode 100644 index 000000000..fdeb117b7 --- /dev/null +++ b/frontend/resources/images/icons/boolean-union.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/uxbox-logo-icon-loader.svg b/frontend/resources/images/icons/penpot-logo-icon-loader.svg similarity index 100% rename from frontend/resources/images/icons/uxbox-logo-icon-loader.svg rename to frontend/resources/images/icons/penpot-logo-icon-loader.svg diff --git a/frontend/resources/images/icons/uxbox-logo-icon.svg b/frontend/resources/images/icons/penpot-logo-icon.svg similarity index 100% rename from frontend/resources/images/icons/uxbox-logo-icon.svg rename to frontend/resources/images/icons/penpot-logo-icon.svg diff --git a/frontend/resources/images/icons/uxbox-logo.svg b/frontend/resources/images/icons/penpot-logo.svg similarity index 100% rename from frontend/resources/images/icons/uxbox-logo.svg rename to frontend/resources/images/icons/penpot-logo.svg diff --git a/frontend/resources/images/icons/position-bottom-center.svg b/frontend/resources/images/icons/position-bottom-center.svg new file mode 100644 index 000000000..002466ead --- /dev/null +++ b/frontend/resources/images/icons/position-bottom-center.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/images/icons/position-bottom-left.svg b/frontend/resources/images/icons/position-bottom-left.svg new file mode 100644 index 000000000..4811b74a9 --- /dev/null +++ b/frontend/resources/images/icons/position-bottom-left.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/images/icons/position-bottom-right.svg b/frontend/resources/images/icons/position-bottom-right.svg new file mode 100644 index 000000000..ebf861dcf --- /dev/null +++ b/frontend/resources/images/icons/position-bottom-right.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/images/icons/position-center.svg b/frontend/resources/images/icons/position-center.svg new file mode 100644 index 000000000..ce6695ba7 --- /dev/null +++ b/frontend/resources/images/icons/position-center.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/images/icons/position-top-center.svg b/frontend/resources/images/icons/position-top-center.svg new file mode 100644 index 000000000..5a971d427 --- /dev/null +++ b/frontend/resources/images/icons/position-top-center.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/images/icons/position-top-left.svg b/frontend/resources/images/icons/position-top-left.svg new file mode 100644 index 000000000..0285e444e --- /dev/null +++ b/frontend/resources/images/icons/position-top-left.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/resources/images/icons/position-top-right.svg b/frontend/resources/images/icons/position-top-right.svg new file mode 100644 index 000000000..838f63602 --- /dev/null +++ b/frontend/resources/images/icons/position-top-right.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/styles/common/base.scss b/frontend/resources/styles/common/base.scss index ddd623792..54032cad6 100644 --- a/frontend/resources/styles/common/base.scss +++ b/frontend/resources/styles/common/base.scss @@ -63,7 +63,7 @@ a { } p { - font-size: $fs13; + font-size: $fs12; margin-bottom: 1rem; line-height: $base-lh-sm; diff --git a/frontend/resources/styles/common/dependencies/fonts.scss b/frontend/resources/styles/common/dependencies/fonts.scss index 19e9b2f9c..75eb65c1f 100644 --- a/frontend/resources/styles/common/dependencies/fonts.scss +++ b/frontend/resources/styles/common/dependencies/fonts.scss @@ -21,7 +21,7 @@ $fs21: 1.315rem; $fs22: 1.375rem; $fs24: 1.5rem; $fs26: 1.625rem; -$fs28: 1.75rem; +$fs24: 1.75rem; $fs30: 1.875rem; $fs32: 2rem; $fs34: 2.125rem; diff --git a/frontend/resources/styles/common/dependencies/helpers.scss b/frontend/resources/styles/common/dependencies/helpers.scss index ed0f805c1..3562e9370 100644 --- a/frontend/resources/styles/common/dependencies/helpers.scss +++ b/frontend/resources/styles/common/dependencies/helpers.scss @@ -6,16 +6,9 @@ // Copyright (c) 2015-2016 Juan de la Cruz // Padding & Margin sizes -$x-small: 4px; -$small: 8px; -$medium: 16px; -$big: 20px; -$x-big: 32px; - -// New sizes $size-1: 0.25rem; $size-2: 0.5rem; -$size-3: 0.8rem; +$size-3: 0.75rem; $size-4: 1rem; $size-5: 1.5rem; $size-6: 2rem; @@ -38,7 +31,7 @@ $br-huge: 12px; .row-flex { align-items: center; display: flex; - margin-bottom: $x-small; + margin-bottom: $size-1; &.column { flex-direction: column; @@ -59,7 +52,7 @@ $br-huge: 12px; } .column-half { - margin-right: $small; + margin-right: $size-2; } // Display diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss index 3f542ceef..baa6526b6 100644 --- a/frontend/resources/styles/common/framework.scss +++ b/frontend/resources/styles/common/framework.scss @@ -14,7 +14,7 @@ border-radius: 3px; cursor: pointer; display: flex; - font-size: $fs13; + font-size: $fs12; height: 30px; justify-content: center; min-width: 25px; @@ -84,7 +84,7 @@ @extend %btn; background: $color-gray-60; color: $color-gray-20; - padding: $x-small; + padding: $size-1; svg { fill: $color-gray-20; } @@ -102,7 +102,7 @@ background: $color-white; border: 1px solid $color-gray-20; color: $color-black; - padding: $x-small; + padding: $size-1; svg { fill: $color-black; } @@ -119,7 +119,7 @@ @extend %btn; background: transparent; color: $color-gray-20; - padding: $x-small; + padding: $size-1; svg { fill: $color-gray-20; } @@ -280,7 +280,7 @@ ul.slider-dots { li { align-items: center; display: flex; - padding: $small 0; + padding: $size-2 0; &::before { background-color: $color-complete; @@ -317,7 +317,7 @@ ul.slider-dots { cursor: pointer; font-size: $fs14; font-weight: bold; - margin: 0 $small $small 0; + margin: 0 $size-2 $size-2 0; padding: 4px 8px; text-transform: uppercase; @@ -392,7 +392,7 @@ ul.slider-dots { font-size: $fs12; height: 20px; position: absolute; - right: $small; + right: $size-2; text-align: right; top: 26%; width: 18px; @@ -480,7 +480,7 @@ select { color: $color-gray-60; font-family: "worksans", sans-serif; font-size: $fs14; - margin-bottom: $medium; + margin-bottom: $size-4; -webkit-appearance: none; -moz-appearance: none; } @@ -515,7 +515,7 @@ input[type="checkbox"]:focus { background-color: $color-white; box-shadow: none; outline: none; - padding: $small $big $small $small; + padding: $size-2 $size-5 $size-2 $size-2; position: relative; @include placeholder { @@ -555,7 +555,7 @@ input.element-name { border: 1px solid $color-gray-40; border-radius: $br-small; color: $color-gray-60; - font-size: $fs13; + font-size: $fs12; margin: 0px; padding: 3px; width: 100%; @@ -572,7 +572,7 @@ input.element-name { cursor: pointer; &.small { - padding: $x-small $big $x-small $x-small; + padding: $size-1 $size-5 $size-1 $size-1; } } @@ -713,7 +713,7 @@ input[type=radio]:checked + label:before{ position: absolute; left: 3.2px; top: 0; - font-size: $fs11; + font-size: $fs12; transition: border 0.2s linear 0s, color 0.2s linear 0s; } @@ -969,9 +969,9 @@ input[type=range]:focus::-ms-fill-upper { border-radius: $br-small; color: $color-gray-60; content: attr(alt); - font-size: $fs11; + font-size: $fs12; font-weight: bold; - padding: $x-small; + padding: $size-1; position: absolute; left: 130%; text-align: center; @@ -1098,7 +1098,7 @@ input[type=range]:focus::-ms-fill-upper { flex-direction: column; .actions { - margin-top: $medium; + margin-top: $size-4; display: flex; justify-content: flex-start; } @@ -1114,7 +1114,7 @@ input[type=range]:focus::-ms-fill-upper { justify-content: flex-start; .btn-secondary { - margin-left: $medium; + margin-left: $size-4; } } } @@ -1167,7 +1167,7 @@ input[type=range]:focus::-ms-fill-upper { padding-right: 64px; .icon { - margin-right: $medium; + margin-right: $size-4; } .content { @@ -1175,7 +1175,7 @@ input[type=range]:focus::-ms-fill-upper { display: flex; align-items: center; justify-content: center; - font-size: $fs15; + font-size: $fs14; } } } @@ -1187,7 +1187,7 @@ input[type=range]:focus::-ms-fill-upper { display: flex; .icon { - padding: $small; + padding: $size-2; width: 48px; height: 48px; justify-content: center; @@ -1198,7 +1198,7 @@ input[type=range]:focus::-ms-fill-upper { color: $color-black; display: flex; font-size: $fs14; - padding: $small; + padding: $size-2; width: 100%; align-items: center; } diff --git a/frontend/resources/styles/main/layouts/login.scss b/frontend/resources/styles/main/layouts/login.scss index 49aee2b0f..627753cf0 100644 --- a/frontend/resources/styles/main/layouts/login.scss +++ b/frontend/resources/styles/main/layouts/login.scss @@ -64,7 +64,7 @@ width: 100%; *:not(:last-child) { - margin-bottom: $medium; + margin-bottom: $size-4; } } @@ -72,7 +72,7 @@ width: 412px; .auth-buttons { - margin-top: $x-big; + margin-top: $size-6; } form { @@ -81,12 +81,12 @@ } .btn-google-auth { - margin-bottom: $medium; + margin-bottom: $size-4; text-decoration: none; } .btn-gitlab-auth { - margin-bottom: $medium; + margin-bottom: $size-4; text-decoration: none; .logo { @@ -97,7 +97,7 @@ } .btn-github-auth { - margin-bottom: $medium; + margin-bottom: $size-4; text-decoration: none; .logo { @@ -119,13 +119,13 @@ font-size: $fs14; flex-direction: column; justify-content: space-between; - margin-top: $medium; - margin-bottom: $medium; + margin-top: $size-4; + margin-bottom: $size-4; &.demo { justify-content: center; - margin-top: $big; + margin-top: $size-5; } .link-entry { @@ -141,11 +141,11 @@ } .terms-login { - bottom: $big; + bottom: $size-5; font-size: $fs14; position: absolute; span { - margin: 0 $small; + margin: 0 $size-2; } } diff --git a/frontend/resources/styles/main/layouts/viewer.scss b/frontend/resources/styles/main/layouts/viewer.scss index 7e05a861b..296322332 100644 --- a/frontend/resources/styles/main/layouts/viewer.scss +++ b/frontend/resources/styles/main/layouts/viewer.scss @@ -43,3 +43,19 @@ grid-row: 1 / span 2; } } + +.viewer-overlay { + position: absolute; +} + +.viewer-overlay-background { + position: absolute; + top: 0; + left: 0; + + &.visible { + background-color: rgb(0, 0, 0, 0.2); + } +} + + diff --git a/frontend/resources/styles/main/partials/activity-bar.scss b/frontend/resources/styles/main/partials/activity-bar.scss index b1fb0dc94..7e116fae1 100644 --- a/frontend/resources/styles/main/partials/activity-bar.scss +++ b/frontend/resources/styles/main/partials/activity-bar.scss @@ -24,7 +24,7 @@ color: $color-gray-40; font-size: $fs16; font-weight: bold; - margin-bottom: $x-small; + margin-bottom: $size-1; } .date-ribbon { @@ -40,15 +40,15 @@ .activity-input { border-bottom: 1px solid $color-gray-10; display: flex; - font-size: $fs13; - padding: $small; + font-size: $fs12; + padding: $size-2; width: 100%; img.activity-author { border-radius: 50%; flex-shrink: 0; height: 30px; - margin-right: $medium; + margin-right: $size-4; width: 30px; } @@ -70,7 +70,7 @@ .activity-time { color: $color-gray-20; - font-size: $fs11; + font-size: $fs12; font-style: italic; } } diff --git a/frontend/resources/styles/main/partials/color-bullet.scss b/frontend/resources/styles/main/partials/color-bullet.scss index eb4882a51..952261776 100644 --- a/frontend/resources/styles/main/partials/color-bullet.scss +++ b/frontend/resources/styles/main/partials/color-bullet.scss @@ -78,7 +78,7 @@ ul.palette-menu .color-bullet { border: 1px solid $color-gray-20; border-radius: 10px; height: 20px; - margin-right: $x-small; + margin-right: $size-1; width: 20px; } diff --git a/frontend/resources/styles/main/partials/colorpicker.scss b/frontend/resources/styles/main/partials/colorpicker.scss index e0b0ed178..f5e8afb3f 100644 --- a/frontend/resources/styles/main/partials/colorpicker.scss +++ b/frontend/resources/styles/main/partials/colorpicker.scss @@ -13,7 +13,7 @@ .colorpicker-content { display: flex; flex-direction: column; - padding: $small; + padding: $size-2; & > * { width: 200px; @@ -21,7 +21,7 @@ .top-actions { display: flex; - margin-bottom: $x-small; + margin-bottom: $size-1; justify-content: space-between; .picker-btn { @@ -50,7 +50,7 @@ margin: 0; border: 1px solid $color-gray-20; border-radius: 2px; - margin-left: $x-small; + margin-left: $size-1; } .active { @@ -69,8 +69,8 @@ .gradient-stops { height: 10px; display: flex; - margin-top: $small; - margin-bottom: $medium; + margin-top: $size-2; + margin-bottom: $size-4; .gradient-background-wrapper { height: 100%; @@ -280,14 +280,14 @@ margin: 0; border: 1px solid $color-gray-10; border-radius: 2px; - font-size: $fs11; + font-size: $fs12; height: 1.5rem; - padding: 0 $x-small; + padding: 0 $size-1; color: $color-gray-40; } label { - font-size: $fs11; + font-size: $fs12; } } @@ -303,9 +303,9 @@ background-position: 95% 48%; background-size: 10px; margin: 0; - margin-bottom: $small; + margin-bottom: $size-2; width: 100%; - padding: $x-small 0.25rem; + padding: $size-1 0.25rem; font-size: 0.75rem; color: $color-gray-40; cursor: pointer; @@ -434,7 +434,7 @@ .input-text { color: $color-gray-60; - font-size: $fs13; + font-size: $fs12; margin: 5px; padding: 5px; width: 100%; @@ -444,7 +444,7 @@ .colorpicker-tabs { display: flex; - margin-bottom: $small; + margin-bottom: $size-2; border-radius: 5px; border: 1px solid $color-gray-10; height: 2rem; @@ -480,11 +480,11 @@ .color-data { align-items: center; display: flex; - margin-bottom: $small; + margin-bottom: $size-2; position: relative; .color-name { - font-size: $fs13; + font-size: $fs12; margin: 5px 6px 0px 6px; } @@ -498,9 +498,9 @@ color: $color-white; height: 20px; margin: 5px 0 0 0; - padding: 0 $x-small; + padding: 0 $size-1; width: 84px; - font-size: $fs13; + font-size: $fs12; &:focus { border-color: $color-primary !important; @@ -523,7 +523,7 @@ .type { color: $color-gray-10; - margin-right: $x-small; + margin-right: $size-1; } .number { diff --git a/frontend/resources/styles/main/partials/comments.scss b/frontend/resources/styles/main/partials/comments.scss index cc6248efa..87c0d1eb2 100644 --- a/frontend/resources/styles/main/partials/comments.scss +++ b/frontend/resources/styles/main/partials/comments.scss @@ -12,7 +12,7 @@ box-sizing: border-box; box-shadow: 0px 4px 4px rgba($color-black, 0.25); - font-size: $fs13; + font-size: $fs12; width: 30px; height: 30px; border-radius: 50%; @@ -69,11 +69,11 @@ textarea { font-family: "worksans", sans-serif; - font-size: $fs13; + font-size: $fs12; min-height: 32px; outline: none; overflow: hidden; - padding: $small; + padding: $size-2; resize: none; width: 100%; border-radius: 2px; @@ -104,7 +104,7 @@ .comment { display: flex; flex-direction: column; - padding: $medium $small; + padding: $size-4 $size-2; .author { display: flex; @@ -120,7 +120,7 @@ .fullname { font-weight: 700; color: $color-gray-60; - font-size: $fs13; + font-size: $fs12; @include text-ellipsis; width: 174px; @@ -183,11 +183,11 @@ } .content { - margin: $medium 0; + margin: $size-4 0; font-size: $fs14; color: $color-black; .text { - margin: 0 $small 0 26px; + margin: 0 $size-2 0 26px; white-space: pre-wrap; display: inline-block; } @@ -295,7 +295,7 @@ .comment { cursor: pointer; .author { - margin-bottom: $medium; + margin-bottom: $size-4; .name { display: flex; @@ -317,7 +317,7 @@ color: $color-white; &.replies { - margin: 0 $small 0 26px; + margin: 0 $size-2 0 26px; display: flex; .total-replies { margin-right: 9px; @@ -455,13 +455,13 @@ display: flex; flex-direction: column; font-size: $fs12; - padding: $big; + padding: $size-5; text-align: center; svg { fill: $color-gray-20; height: 24px; - margin-bottom: $big; + margin-bottom: $size-5; width: 24px; } } diff --git a/frontend/resources/styles/main/partials/context-menu.scss b/frontend/resources/styles/main/partials/context-menu.scss index d052cd349..ce19531bc 100644 --- a/frontend/resources/styles/main/partials/context-menu.scss +++ b/frontend/resources/styles/main/partials/context-menu.scss @@ -82,7 +82,7 @@ height: 10px; width: 10px; transform: rotate(180deg); - margin-right: $small; + margin-right: $size-2; } } } diff --git a/frontend/resources/styles/main/partials/dashboard-fonts.scss b/frontend/resources/styles/main/partials/dashboard-fonts.scss index 2370def96..e1ea817eb 100644 --- a/frontend/resources/styles/main/partials/dashboard-fonts.scss +++ b/frontend/resources/styles/main/partials/dashboard-fonts.scss @@ -7,13 +7,13 @@ max-width: 1000px; width: 100%; display: flex; - margin-top: $big; + margin-top: $size-5; flex-direction: column; h3 { font-size: $fs14; color: $color-gray-30; - margin: $x-small; + margin: $size-1; } .font-item { @@ -28,7 +28,7 @@ font-size: $fs12; background-color: $color-white; align-items: center; - padding: 0px $big; + padding: 0px $size-5; > .family { min-width: 200px; @@ -49,14 +49,14 @@ border: 1px solid $color-gray-30; border-radius: $br-small; width: 130px; - padding: $x-small; + padding: $size-1; margin: 0px; } } } .font-item { - margin-top: $big; + margin-top: $size-5; color: $color-gray-40; font-size: $fs14; background-color: $color-white; @@ -65,7 +65,7 @@ width: 100%; min-height: 97px; align-items: center; - padding: $big; + padding: $size-5; justify-content: space-between; &:not(:first-child) { @@ -76,7 +76,7 @@ border: 1px solid $color-gray-30; border-radius: $br-small; margin: 0px; - padding: $small; + padding: $size-2; font-size: $fs12; } @@ -135,7 +135,7 @@ justify-content: flex-end; .icon { - width: $big; + width: $size-5; cursor: pointer; display: flex; margin-left: 10px; @@ -169,9 +169,9 @@ .dashboard-fonts-hero { font-size: $fs14; - padding: $x-big; + padding: $size-6; background-color: $color-white; - margin-top: $x-big; + margin-top: $size-6; display: flex; justify-content: space-between; @@ -193,7 +193,7 @@ .desc { h2 { - margin-bottom: $medium; + margin-bottom: $size-4; color: $color-black; } width: 80%; diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss index 67aaa4ca5..032bab70c 100644 --- a/frontend/resources/styles/main/partials/dashboard-grid.scss +++ b/frontend/resources/styles/main/partials/dashboard-grid.scss @@ -28,7 +28,7 @@ flex-direction: column; flex-shrink: 0; height: 200px; - margin: $medium; + margin: $size-4; max-width: 260px; min-width: 260px; position: relative; @@ -85,7 +85,7 @@ &.small-item { max-width: 12%; min-width: 190px; - padding: $medium; + padding: $size-4; justify-content: center; } @@ -97,19 +97,19 @@ .item-info { display: flex; flex-direction: column; - padding: $small; + padding: $size-2; text-align: left; width: 100%; h3 { border: 1px solid transparent; color: $color-gray-60; - font-size: $fs15; + font-size: $fs14; font-weight: 400; overflow: hidden; padding: 0; height: 27px; - padding-right: $small; + padding-right: $size-2; text-overflow: ellipsis; width: 100%; white-space: nowrap; @@ -129,7 +129,7 @@ padding: 0px; height: 25px; color: $color-gray-60; - font-size: $fs15; + font-size: $fs14; font-weight: 400; } } @@ -141,8 +141,8 @@ border: 1px solid $color-gray-20; border-radius: 4px; position: absolute; - top: $x-small; - right: $x-small; + top: $size-1; + right: $size-1; height: 32px; width: 32px; display: flex; @@ -163,7 +163,7 @@ span { color: $color-gray-60; - font-size: $fs15; + font-size: $fs14; } &:hover { @@ -205,7 +205,7 @@ .project-th-icon { align-items: center; display: flex; - margin-right: $small; + margin-right: $size-2; &.menu { margin-right: 0; @@ -296,7 +296,7 @@ // STYLES FOR LIBRARIES &.library { - padding: $medium; + padding: $size-4; } .grid-item-th { diff --git a/frontend/resources/styles/main/partials/dashboard-header.scss b/frontend/resources/styles/main/partials/dashboard-header.scss index 3c5c7ab40..b9e7a44f7 100644 --- a/frontend/resources/styles/main/partials/dashboard-header.scss +++ b/frontend/resources/styles/main/partials/dashboard-header.scss @@ -9,13 +9,13 @@ background-color: $color-white; display: flex; height: 63px; - padding: $x-small $medium $x-small $small; + padding: $size-1 $size-4 $size-1 $size-2; position: relative; z-index: 10; justify-content: space-between; .element-name { - margin-right: $small; + margin-right: $size-2; } .btn-secondary { @@ -27,7 +27,7 @@ svg { fill: $color-black; height: 14px; - margin-right: $x-small; + margin-right: $size-1; width: 14px; } @@ -41,7 +41,7 @@ ul { display: flex; align-items: center; - font-size: $fs15; + font-size: $fs14; justify-content: center; } @@ -52,7 +52,7 @@ color: $color-gray-30; display: flex; height: 40px; - padding: $x-small $big; + padding: $size-1 $size-5; flex-basis: 140px; &:hover { @@ -90,7 +90,7 @@ display: flex; align-items: center; cursor: pointer; - margin-left: $small; + margin-left: $size-2; z-index: 10; svg { @@ -109,7 +109,7 @@ } .pin-icon { - margin: 0 $small 0 $big; + margin: 0 $size-2 0 $size-5; svg { fill: $color-gray-20; } diff --git a/frontend/resources/styles/main/partials/dashboard-settings.scss b/frontend/resources/styles/main/partials/dashboard-settings.scss index 7a81add24..8515c28cc 100644 --- a/frontend/resources/styles/main/partials/dashboard-settings.scss +++ b/frontend/resources/styles/main/partials/dashboard-settings.scss @@ -64,7 +64,7 @@ border-radius: 50%; flex-shrink: 0; height: 120px; - margin-right: $medium; + margin-right: $size-4; width: 120px; } diff --git a/frontend/resources/styles/main/partials/dashboard-sidebar.scss b/frontend/resources/styles/main/partials/dashboard-sidebar.scss index 2e9055759..3a68e1ac4 100644 --- a/frontend/resources/styles/main/partials/dashboard-sidebar.scss +++ b/frontend/resources/styles/main/partials/dashboard-sidebar.scss @@ -141,7 +141,7 @@ } } .text { - font-size: $fs13; + font-size: $fs12; } } @@ -188,7 +188,7 @@ border-radius: $br-small; content: ""; height: 26px; - margin-right: $small; + margin-right: $size-2; width: 4px; } @@ -284,7 +284,7 @@ display: flex; height: 22px; margin-left: auto; - padding: 0 $small; + padding: 0 $size-2; width: 32px; svg { @@ -390,7 +390,7 @@ svg { height: 10px; margin-left: auto; - margin-right: $small; + margin-right: $size-2; width: 10px; } } @@ -409,7 +409,7 @@ svg { fill: $color-gray-20; - margin-right: $small; + margin-right: $size-2; height: 12px; width: 12px; diff --git a/frontend/resources/styles/main/partials/dashboard-team.scss b/frontend/resources/styles/main/partials/dashboard-team.scss index 6688ffebe..6efc6f178 100644 --- a/frontend/resources/styles/main/partials/dashboard-team.scss +++ b/frontend/resources/styles/main/partials/dashboard-team.scss @@ -139,7 +139,7 @@ .name { margin-top: 10px; - font-size: $fs28; + font-size: $fs24; color: $color-black; @include text-ellipsis; margin-right: 90px; diff --git a/frontend/resources/styles/main/partials/dashboard.scss b/frontend/resources/styles/main/partials/dashboard.scss index 6c90ee4f5..3a8c0deb5 100644 --- a/frontend/resources/styles/main/partials/dashboard.scss +++ b/frontend/resources/styles/main/partials/dashboard.scss @@ -9,7 +9,7 @@ border-top-right-radius: $br-huge; border-top-left-radius: $br-huge; flex: 1 0 0; - margin-right: $medium; + margin-right: $size-4; overflow-y: auto; user-select: none; @@ -19,7 +19,7 @@ } .dashboard-project-row { - margin-bottom: $medium; + margin-bottom: $size-4; .project { align-items: center; @@ -27,9 +27,9 @@ border-radius: $br-small; display: flex; flex-direction: row; - margin-left: $medium; - margin-top: $medium; - padding: $x-small $x-small $x-small $small; + margin-left: $size-4; + margin-top: $size-4; + padding: $size-1 $size-1 $size-1 $size-2; width: fit-content; height: 40px; @@ -40,19 +40,19 @@ h2 { cursor: pointer; - font-size: 15px; + font-size: $fs14; line-height: 1rem; font-weight: 500; color: $color-black; - margin-right: $medium; + margin-right: $size-4; } .edit-wrapper { - margin-right: $medium; + margin-right: $size-4; } .info { - font-size: 15px; + font-size: $fs14; line-height: 1rem; font-weight: unset; color: $color-gray-30; @@ -78,7 +78,7 @@ } .recent-files-row-title-info { - font-size: $fs15; + font-size: $fs14; } .dashboard-table { @@ -138,7 +138,7 @@ border: 1px solid $color-gray-10; border-radius: $br-small; display: flex; - padding-right: $big; + padding-right: $size-5; position: relative; input.element-title { diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index fb6d3c686..27da8245d 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -48,7 +48,7 @@ justify-content: space-between; > button { - font-size: $fs13; + font-size: $fs12; } > button:not(:first-child) { margin-left: 25px; @@ -562,7 +562,7 @@ .header-item { cursor: pointer; color: $color-gray-40; - font-size: $fs15; + font-size: $fs14; &.active { color: $color-gray-60; @@ -597,7 +597,7 @@ .section-title { color: $color-black; - font-size: $fs15; + font-size: $fs14; padding: 0 $size-4; font-weight: 500; } @@ -664,7 +664,7 @@ .libraries-search { border: 1px solid $color-gray-20; margin: $size-4; - padding: $x-small $small; + padding: $size-1 $size-2; display: flex; align-items: center; @@ -720,7 +720,7 @@ flex-shrink: 0; justify-content: center; overflow: hidden; - padding: $x-big; + padding: $size-6; width: 230px; &.welcome { @@ -733,11 +733,11 @@ border-bottom-right-radius: 5px; display: flex; flex-direction: column; - padding: $x-big; + padding: $size-6; .modal-title h2 { color: $color-black; - font-size: $fs28; + font-size: $fs24; font-weight: 900; } @@ -746,19 +746,19 @@ color: $color-black; font-size: $fs12; font-weight: bold; - margin-top: $small; - padding: 2px $x-small; + margin-top: $size-2; + padding: 2px $size-1; width: max-content; } .modal-content { border: none; - padding: $big 0; + padding: $size-5 0; p { color: $color-black; font-size: 16px; - margin-top: $small; + margin-top: $size-2; } } @@ -770,8 +770,8 @@ .skip { cursor: pointer; font-family: "worksans", sans-serif; - font-size: $fs13; - margin-left: $big; + font-size: $fs12; + margin-left: $size-5; &:hover { color: $color-black; @@ -789,9 +789,9 @@ background-color: $color-gray-10; border-radius: 50%; cursor: pointer; - height: $small; - margin-left: $small; - width: $small; + height: $size-2; + margin-left: $size-2; + width: $size-2; &.current { background-color: $color-primary; @@ -824,7 +824,7 @@ } &.final { - padding: $big 0 0 0; + padding: $size-5 0 0 0; .modal-left, .modal-right { @@ -834,12 +834,12 @@ flex: 1; flex-direction: column; overflow: visible; - padding: $x-big 40px; + padding: $size-6 40px; text-align: center; h2 { font-weight: 900; - margin-bottom: $big; + margin-bottom: $size-5; font-size: $fs24; } @@ -856,7 +856,7 @@ img { box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25); border-radius: $br-medium; - margin-bottom: $x-big; + margin-bottom: $size-6; margin-top: -90px; width: 150px; } @@ -872,7 +872,7 @@ margin-top: auto; .custom-input { - margin-bottom: $medium; + margin-bottom: $size-4; input { width: 200px; diff --git a/frontend/resources/styles/main/partials/project-bar.scss b/frontend/resources/styles/main/partials/project-bar.scss index 709d2b2ea..908dc03f9 100644 --- a/frontend/resources/styles/main/partials/project-bar.scss +++ b/frontend/resources/styles/main/partials/project-bar.scss @@ -30,15 +30,15 @@ border-bottom: 1px solid $color-gray-10; font-size: $fs14; font-weight: bold; - padding: 0 $small; + padding: 0 $size-2; width: 100%; } .btn-primary, .btn-warning { - font-size: $fs13; + font-size: $fs12; margin-bottom: .5rem; - padding: 8px $small; + padding: 8px $size-2; width: 90%; } @@ -53,18 +53,18 @@ align-items: center; cursor: pointer; display: flex; - padding: $x-small $small; + padding: $size-1 $size-2; position: relative; svg { fill: $color-gray-20; height: 12px; - margin-right: $x-small; + margin-right: $size-1; width: 12px; } span { - font-size: $fs13; + font-size: $fs12; } &:hover, @@ -86,7 +86,7 @@ svg { fill: $color-gray-20; height: 12px; - margin-right: $small; + margin-right: $size-2; width: 12px; &:hover { diff --git a/frontend/resources/styles/main/partials/share-link.scss b/frontend/resources/styles/main/partials/share-link.scss index af0dd9a11..bd59474e9 100644 --- a/frontend/resources/styles/main/partials/share-link.scss +++ b/frontend/resources/styles/main/partials/share-link.scss @@ -64,7 +64,7 @@ .share-link-section { margin-top: 12px; label { - font-size: $fs11; + font-size: $fs12; color: $color-black; } diff --git a/frontend/resources/styles/main/partials/sidebar-align-options.scss b/frontend/resources/styles/main/partials/sidebar-align-options.scss index 7b9750426..18ced6518 100644 --- a/frontend/resources/styles/main/partials/sidebar-align-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-align-options.scss @@ -9,12 +9,12 @@ display: flex; border-bottom: solid 1px $color-gray-60; height: 40px; - padding: 0 $x-small; .align-group { + padding: 0 $size-1; display: flex; - justify-content: space-evenly; - width: 100%; + justify-content: flex-start; + width: 50%; &:not(:last-child) { border-right: solid 1px $color-gray-60; @@ -25,7 +25,12 @@ align-items: center; cursor: pointer; display: flex; - padding: $small $x-small; + height: 30px; + justify-content: center; + margin: 5px 0; + padding: $size-2 $size-1; + width: 25%; + svg { height: 16px; width: 16px; @@ -46,5 +51,13 @@ fill: $color-gray-40; } } + + &.selected svg { + fill: $color-primary; + } + + &.selected:hover svg { + fill: $color-white; + } } } diff --git a/frontend/resources/styles/main/partials/sidebar-assets.scss b/frontend/resources/styles/main/partials/sidebar-assets.scss index 122f5e8aa..7fdacb37b 100644 --- a/frontend/resources/styles/main/partials/sidebar-assets.scss +++ b/frontend/resources/styles/main/partials/sidebar-assets.scss @@ -14,7 +14,7 @@ .assets-bar-title { color: $color-gray-10; font-size: $fs14; - margin: $small $small 0 $small; + margin: $size-2 $size-2 0 $size-2; display: flex; align-items: center; cursor: pointer; @@ -43,8 +43,8 @@ .search-block { border: 1px solid $color-gray-30; - margin: $small $small 0 $small; - padding: $x-small $small; + margin: $size-2 $size-2 0 $size-2; + padding: $size-1 $size-2; display: flex; align-items: center; @@ -92,8 +92,8 @@ color: $color-gray-10; border: 1px solid transparent; border-bottom-color: $color-gray-40; - padding: $x-small; - margin: $small $small $medium $small; + padding: $size-1; + margin: $size-2 $size-2 $size-4 $size-2; &:focus { color: lighten($color-gray-10, 8%); @@ -107,7 +107,7 @@ } .collapse-library { - margin-right: $small; + margin-right: $size-2; &.open svg { transform: rotate(90deg); @@ -122,16 +122,16 @@ background-color: $color-gray-60; display: flex; align-items: center; - padding: $medium $small 0 $small; + padding: $size-4 $size-2 0 $size-2; .selected-count { color: $color-primary; - font-size: $fs11; + font-size: $fs12; } .listing-option-btn { cursor: pointer; - margin-left: $small; + margin-left: $size-2; &.first { margin-left: auto; @@ -147,7 +147,7 @@ .asset-section { background-color: $color-gray-60; - padding: $small; + padding: $size-2; font-size: $fs12; color: $color-gray-20; /* TODO: see if this is useful, or is better to leave only @@ -164,7 +164,7 @@ .asset-title { display: flex; cursor: pointer; - font-size: $fs11; + font-size: $fs12; text-transform: uppercase; & .num-assets { @@ -188,8 +188,8 @@ .group-title { display: flex; cursor: pointer; - margin-top: $small; - margin-bottom: $x-small; + margin-top: $size-2; + margin-bottom: $size-1; color: $color-white; & svg { @@ -226,7 +226,7 @@ } .asset-title + .asset-grid { - margin-top: $small; + margin-top: $size-2; } .asset-grid { @@ -241,7 +241,7 @@ grid-auto-rows: 10vh; .grid-cell { - padding: $x-small; + padding: $size-1; & svg { height: 10vh; @@ -258,7 +258,7 @@ display: flex; align-items: center; justify-content: center; - padding: $small; + padding: $size-2; position: relative; cursor: pointer; @@ -291,7 +291,7 @@ .editable-label-input { border: 1px solid $color-gray-20; border-radius: 3px; - font-size: $fs11; + font-size: $fs12; padding: 2px; margin: 0; height: unset; @@ -316,14 +316,14 @@ } .asset-title + .asset-enum { - margin-top: $small; + margin-top: $size-2; } .asset-enum { .enum-item { display: flex; align-items: center; - margin-bottom: $small; + margin-bottom: $size-2; cursor: pointer; & > svg, @@ -333,11 +333,11 @@ border: 2px solid transparent; height: 24px; width: 24px; - margin-right: $small; + margin-right: $size-2; } .item-name { - width: calc(100% - 24px - #{$small}); + width: calc(100% - 24px - #{$size-2}); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -378,14 +378,14 @@ align-items: center; border: 1px solid transparent; border-radius: $br-small; - margin-top: $x-small; + margin-top: $size-1; padding: 2px; font-size: $fs12; color: $color-white; cursor: pointer; & span { - margin-left: $x-small; + margin-left: $size-1; color: $color-gray-30; text-transform: uppercase; } diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index 0380d9303..b3f555d76 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -14,7 +14,7 @@ border: 1px solid $color-gray-60; border-radius: $br-small; display: flex; - margin: $x-small; + margin: $size-1; li { align-items: center; @@ -24,7 +24,7 @@ display: flex; flex: 1; justify-content: center; - padding: $small; + padding: $size-2; svg { fill: $color-gray-20; @@ -60,13 +60,13 @@ .element-set { border-bottom: 1px solid $color-gray-60; color: $color-gray-20; - padding: $small $x-small; + padding: $size-2 $size-1; .element-set-title { color: $color-gray-20; display: flex; - font-size: $fs13; - padding: $x-small; + font-size: $fs14; + padding: $size-1; width: 100%; align-items: center; } @@ -79,14 +79,14 @@ } .element-list { - margin-bottom: $small; + margin-bottom: $size-2; li { align-items: center; border-bottom: 1px solid $color-gray-60; display: flex; flex-direction: row; - padding: $small; + padding: $size-2; width: 100%; .list-icon { @@ -94,7 +94,7 @@ svg { fill: $color-gray-30; height: 15px; - margin-right: $x-small; + margin-right: $size-1; width: 15px; } @@ -120,7 +120,7 @@ svg { fill: $color-gray-60; height: 15px; - margin-left: $x-small; + margin-left: $size-1; width: 15px; &:hover { @@ -184,7 +184,8 @@ .element-set-content { display: flex; flex-direction: column; - padding: $x-small; + padding: $size-1; + width: 100%; .input-text { background-color: $color-gray-50; @@ -192,9 +193,9 @@ border-bottom-color: $color-gray-40; color: $color-white; font-size: $fs12; - margin: $x-small; + margin: $size-1; min-width: 0; - padding: $x-small; + padding: $size-1; width: 100%; &:focus { @@ -219,13 +220,46 @@ color: $color-gray-60; background: $color-white; font-size: $fs12; + + &:disabled { + color: $color-gray-20; + } + } + } + + .input-checkbox { + label { + color: $color-gray-20; + } + + label::before { + background-color: transparent; + width: 16px; + height: 16px; + } + + label::after { + width: 16px; + height: 16px; + } + + input:checked + label::before { + border-width: 1px; + } + + input:checked + label::after { + font-size: 0.8rem; } } .element-set-subtitle { color: $color-gray-20; - font-size: $fs11; + font-size: $fs12; width: 64px; + + &.wide { + min-width: 75px; + } } .lock-size { @@ -271,12 +305,12 @@ border: 1px solid $color-gray-40; border-radius: $br-small; cursor: pointer; - padding: $x-small $big $x-small $x-small; + padding: $size-1 $size-5 $size-1 $size-1; position: relative; .dropdown-button { position: absolute; - right: $x-small; + right: $size-1; top: 7px; svg { @@ -322,7 +356,7 @@ cursor: pointer; font-size: $fs14; display: flex; - padding: $small; + padding: $size-2; span { color: $color-gray-20; @@ -375,7 +409,7 @@ border: 1px solid transparent; position: relative; height: 38px; - margin-right: $small; + margin-right: $size-2; max-height: 30px; position: relative; width: 60%; @@ -412,6 +446,10 @@ color: $color-gray-60; background: $color-white; font-size: $fs12; + + &:disabled { + color: $color-gray-20; + } } } @@ -490,7 +528,7 @@ fill: $color-gray-20; height: 16px; width: 16px; - margin-right: $small; + margin-right: $size-2; } .row-actions { @@ -500,7 +538,7 @@ svg { fill: $color-gray-20; height: 12px; - margin-right: $x-small; + margin-right: $size-1; width: 12px; } @@ -524,7 +562,7 @@ font-size: $fs12; span { - font-size: $fs11; + font-size: $fs12; } } } @@ -539,8 +577,8 @@ display: flex; flex: 1; justify-content: flex-end; - margin: $small 0 $small $small; - padding: 0 $x-small; + margin: $size-2 0 $size-2 $size-2; + padding: 0 $size-1; &:first-child { justify-content: flex-start; @@ -552,7 +590,7 @@ display: flex; height: 20px; justify-content: center; - margin-right: $small; + margin-right: $size-2; position: relative; width: 20px; @@ -641,7 +679,7 @@ } .orientation-icon { - margin-left: $small; + margin-left: $size-2; display: flex; align-items: center; @@ -662,7 +700,7 @@ .navigate-icon { background-color: $color-gray-60; cursor: pointer; - margin-left: $small; + margin-left: $size-2; display: flex; align-items: center; justify-content: center; @@ -688,7 +726,7 @@ width: 100%; &:first-child { - margin-right: $small; + margin-right: $size-2; } .icon-before { @@ -836,7 +874,6 @@ position: relative; top: 2px; width: 100%; - z-index: 20; } .btn-options { @@ -879,12 +916,35 @@ &.selected { border: 1px solid $color-primary; } + + &:not(:first-child) { + margin-top: 7px; + } + + &.open { + &:hover { + background: unset; + } + } +} + +.interactions-options { + &.element-set { + border-bottom: 0; + } + + .element-set-options-group { + flex-wrap: wrap; + } + + &:not(:first-child) { + border-top: 1px solid $color-gray-60; + } } .exports-options, -.shadow-options{ +.shadow-options { .element-set-options-group { - justify-content: space-between; .delete-icon { display: flex; min-width: 40px; @@ -898,10 +958,6 @@ fill: $color-gray-20; } } - - &:not(:first-child) { - margin-top: 7px; - } } .download-button { @@ -941,11 +997,15 @@ width: 12px; height: 12px; fill: $color-gray-20; + stroke: $color-gray-20; } - &:hover svg { + &:hover svg, + &.active svg { fill: $color-primary; + stroke: $color-primary; } + &.actions-inside { position: absolute; right: 0; @@ -953,7 +1013,7 @@ } .element-set-label { - font-size: $fs11; + font-size: $fs12; padding: 0.5rem; color: $color-gray-10; } @@ -1100,7 +1160,7 @@ .multiple-typography-text, .multiple-typography-button { - font-size: $fs13; + font-size: $fs12; display: flex; align-items: center; } @@ -1153,7 +1213,7 @@ position: relative; .backend-filters { - padding: $small $medium; + padding: $size-2 $size-4; // width: 220px; top: 40px; right: 20px; @@ -1161,15 +1221,15 @@ .backend-filter { display: flex; align-items: center; - padding: $small 0; + padding: $size-2 0; cursor: pointer; .checkbox-icon { display: flex; justify-content: center; align-items: center; - width: $medium; - height: $medium; + width: $size-4; + height: $size-4; border: 1px solid $color-gray-30; border-radius: $br-small; @@ -1182,7 +1242,7 @@ } .backend-name { - margin-left: $small; + margin-left: $size-2; color: $color-gray-50; } @@ -1214,7 +1274,7 @@ align-items: center; width: 24px; height: 24px; - margin-left: $small; + margin-left: $size-2; svg { width: 16px; @@ -1246,9 +1306,9 @@ } .font-item { - padding-left: $big; - height: $x-big; - max-height: $x-big; + padding-left: $size-5; + height: $size-6; + max-height: $size-6; width: 100%; display: flex; align-items: center; @@ -1272,7 +1332,7 @@ // justify-content: center; align-items: center; // border: 1px solid red; - width: $big + width: $size-5 } .label { @@ -1400,14 +1460,14 @@ justify-content: flex-start; .input-select { - font-size: $fs11; - margin: 0 $x-small; + font-size: $fs12; + margin: 0 $size-1; } svg { width: 15px; height: 15px; - margin-left: $medium; + margin-left: $size-4; fill: $color-gray-20; } @@ -1420,11 +1480,11 @@ } .fix-when { - font-size: $fs11; + font-size: $fs12; cursor: pointer; span { - margin-left: $small; + margin-left: $size-2; } &:hover, @@ -1444,10 +1504,10 @@ border-bottom-color: $color-gray-40; color: $color-gray-10; cursor: pointer; - font-size: $fs11; - margin: $x-small; + font-size: $fs12; + margin: $size-1; overflow: hidden; - padding: $x-small; + padding: $size-1; padding-right: 20px; position: relative; text-overflow: ellipsis; @@ -1478,7 +1538,8 @@ right: 5px; top: 30px; z-index: 12; - min-width: 200px; + width: 200px; + height: 320px; position: fixed; & li.separator { @@ -1487,6 +1548,6 @@ & li img { width: 16px; - margin-right: $small; + margin-right: $size-2; } } diff --git a/frontend/resources/styles/main/partials/sidebar-icons.scss b/frontend/resources/styles/main/partials/sidebar-icons.scss index 32eab98cc..8af2365f6 100644 --- a/frontend/resources/styles/main/partials/sidebar-icons.scss +++ b/frontend/resources/styles/main/partials/sidebar-icons.scss @@ -7,7 +7,7 @@ .figures-catalog { width: 100%; - padding: $medium $medium 0 $medium; + padding: $size-4 $size-4 0 $size-4; select { color: $color-gray-10; @@ -27,7 +27,7 @@ flex-shrink: 0; height: 54px; justify-content: center; - margin: $medium 0 0 $medium; + margin: $size-4 0 0 $size-4; width: 54px; svg { diff --git a/frontend/resources/styles/main/partials/sidebar-interactions.scss b/frontend/resources/styles/main/partials/sidebar-interactions.scss index bd086d303..568dd76e1 100644 --- a/frontend/resources/styles/main/partials/sidebar-interactions.scss +++ b/frontend/resources/styles/main/partials/sidebar-interactions.scss @@ -7,14 +7,20 @@ .interactions-help { font-size: $fs12; - margin: 0 $medium; + padding: 7px $size-4; + margin: 0 -7px; text-align: center; + + &.separator { + padding-bottom: $size-4; + border-bottom: 1px solid $color-black; + } } .interactions-help-icon { height: 32px; width: 32px; - margin: $medium auto; + margin: $size-4 auto; svg { fill: $color-gray-40; @@ -22,3 +28,131 @@ width: 32px; } } + +.interactions-summary { + cursor: pointer; + flex-basis: 0; + flex-grow: 1; + + .trigger-name { + font-size: $fs12; + color: $color-white; + } + + .action-summary { + font-size: $fs12; + color: $color-gray-20; + } +} + +.interactions-element { + display: flex; + align-items: center; + margin: 0 -7px; + padding: 0 7px; + + .element-label { + color: $color-gray-20; + font-size: $fs12; + width: 64px; + } + + &.separator { + border-top: 1px solid $color-black; + margin-top: $size-1; + } +} + +.interactions-pos-buttons { + margin-top: $size-2; + padding-top: $size-2; + padding-bottom: $size-2; + justify-content: space-between; + + .element-set-actions-button { + min-width: 18px; + min-height: 18px; + } + + svg { + height: 18px; + width: 18px; + } +} + +.flow-element { + display: flex; + align-items: center; + padding: $size-1; + + .element-label { + font-size: $fs11; + } + + .flow-name { + cursor: pointer; + } + + & input.element-name { + background: transparent; + border-color: $color-primary; + color: $color-white; + font-size: $fs11; + } +} + +.flow-button { + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + margin-right: $size-2; + + & svg { + height: 12px; + width: 12px; + fill: $color-gray-20; + } + + &:hover svg { + fill: $color-primary; + } +} + +.flow-badge { + display: flex; + + & .content { + align-items: center; + background-color: $color-gray-50; + border-radius: 4px; + display: flex; + height: 24px; + + & svg { + height: 12px; + margin: 0 $size-2; + width: 12px; + fill: $color-gray-20; + } + + & span { + color: $color-gray-20; + font-size: $fs12; + margin-right: $size-4; + } + } + + &.selected .content { + background-color: $color-primary; + + & svg { + fill: $color-gray-60; + } + + & span { + color: $color-gray-60; + } + } +} + diff --git a/frontend/resources/styles/main/partials/sidebar-layers.scss b/frontend/resources/styles/main/partials/sidebar-layers.scss index d31e5c7b7..19442c608 100644 --- a/frontend/resources/styles/main/partials/sidebar-layers.scss +++ b/frontend/resources/styles/main/partials/sidebar-layers.scss @@ -9,7 +9,7 @@ align-items: center; display: flex; height: 32px; - padding: $x-small $small; + padding: $size-1 $size-2; transition: none; width: 100%; diff --git a/frontend/resources/styles/main/partials/sidebar-sitemap.scss b/frontend/resources/styles/main/partials/sidebar-sitemap.scss index b04d09fe7..57d82086d 100644 --- a/frontend/resources/styles/main/partials/sidebar-sitemap.scss +++ b/frontend/resources/styles/main/partials/sidebar-sitemap.scss @@ -21,7 +21,7 @@ svg { fill: $color-gray-30; height: 13px; - margin-right: $x-small; + margin-right: $size-1; width: 13px; } @@ -46,7 +46,7 @@ svg { fill: $color-gray-60; height: 15px; - margin-left: $x-small; + margin-left: $size-1; width: 15px; } @@ -110,7 +110,7 @@ .element-list-body { align-items: center; display: flex; - padding: $x-small $small; + padding: $size-1 $size-2; transition: none; width: 100%; @@ -146,7 +146,7 @@ display: flex; justify-content: center; margin-left: auto; - padding: $x-small; + padding: $size-1; svg { fill: $color-gray-20; @@ -175,7 +175,7 @@ } .collapse-pages { - margin-left: $small; + margin-left: $size-2; svg { transform: rotate(90deg); diff --git a/frontend/resources/styles/main/partials/sidebar-tools.scss b/frontend/resources/styles/main/partials/sidebar-tools.scss index 7a2b684b7..2254f240b 100644 --- a/frontend/resources/styles/main/partials/sidebar-tools.scss +++ b/frontend/resources/styles/main/partials/sidebar-tools.scss @@ -18,7 +18,7 @@ flex-shrink: 0; height: 54px; justify-content: center; - margin: $medium 0 0 $medium; + margin: $size-4 0 0 $size-4; width: 54px; svg { diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss index d2f630bb5..17a3ef6fb 100644 --- a/frontend/resources/styles/main/partials/sidebar.scss +++ b/frontend/resources/styles/main/partials/sidebar.scss @@ -82,7 +82,7 @@ $width-settings-bar: 16rem; align-items: center; display: flex; flex-shrink: 0; - padding: $small; + padding: $size-2; overflow: hidden; svg { @@ -93,7 +93,7 @@ $width-settings-bar: 16rem; span { color: $color-gray-10; - font-size: $fs13; + font-size: $fs14; } span.tool-badge { @@ -141,7 +141,7 @@ $width-settings-bar: 16rem; } .tool-window-icon { - margin-right: $small; + margin-right: $size-2; display: none; } diff --git a/frontend/resources/styles/main/partials/tool-bar.scss b/frontend/resources/styles/main/partials/tool-bar.scss index 8d66fd882..a95de8606 100644 --- a/frontend/resources/styles/main/partials/tool-bar.scss +++ b/frontend/resources/styles/main/partials/tool-bar.scss @@ -24,7 +24,7 @@ li { cursor: pointer; - margin-bottom: $big; + margin-bottom: $size-5; svg { fill: $color-gray-20; diff --git a/frontend/resources/styles/main/partials/user-settings.scss b/frontend/resources/styles/main/partials/user-settings.scss index a06c5b150..1927291db 100644 --- a/frontend/resources/styles/main/partials/user-settings.scss +++ b/frontend/resources/styles/main/partials/user-settings.scss @@ -165,7 +165,7 @@ border-radius: 50%; flex-shrink: 0; height: 120px; - margin-right: $medium; + margin-right: $size-4; width: 120px; } } @@ -179,7 +179,7 @@ h2 { font-size: $fs14; font-weight: normal; - margin-bottom: $medium; + margin-bottom: $size-4; } } } diff --git a/frontend/resources/styles/main/partials/viewer-header.scss b/frontend/resources/styles/main/partials/viewer-header.scss index 24b9d802b..ff7808da9 100644 --- a/frontend/resources/styles/main/partials/viewer-header.scss +++ b/frontend/resources/styles/main/partials/viewer-header.scss @@ -4,13 +4,13 @@ border-bottom: 1px solid $color-gray-60; display: flex; height: 48px; - padding: 0 $medium 0 55px; + padding: 0 $size-4 0 55px; position: relative; z-index: 12; justify-content: space-between; a { - font-size: $fs13; + font-size: $fs12; } .main-icon { @@ -50,30 +50,23 @@ position: relative; > * { - margin-left: $big; + margin-left: $size-5; } .btn-primary { flex-shrink: 0; } - .zoom-widget { - .dropdown { - top: 45px; - left: 25px; - } - } - .view-options { align-items: center; cursor: pointer; display: flex; - width: 90px; + position: relative; > span { color: $color-gray-10; - font-size: $fs13; - margin-right: $x-small; + font-size: $fs14; + margin-right: $size-1; } > .icon { @@ -96,7 +89,7 @@ } .dropdown { - min-width: 260px; + min-width: 295px; top: 45px; left: -25px; } @@ -107,7 +100,7 @@ align-items: center; cursor: pointer; display: flex; - padding: $x-small; + padding: $size-1; position: relative; .icon { @@ -118,7 +111,7 @@ svg { fill: $color-gray-20; height: 12px; - margin-right: $small; + margin-right: $size-2; width: 12px; } } @@ -129,7 +122,7 @@ > span { color: $color-gray-20; - margin-right: $x-small; + margin-right: $size-1; font-size: $fs14; overflow-x: hidden; text-overflow: ellipsis; @@ -146,7 +139,7 @@ display: flex; span { color: $color-white; - margin-right: $x-small; + margin-right: $size-1; } .counters { @@ -192,7 +185,7 @@ margin: 0; li { - margin-left: $small; + margin-left: $size-2; position: relative; img { diff --git a/frontend/resources/styles/main/partials/viewer-thumbnails.scss b/frontend/resources/styles/main/partials/viewer-thumbnails.scss index b60a92847..d87f765fa 100644 --- a/frontend/resources/styles/main/partials/viewer-thumbnails.scss +++ b/frontend/resources/styles/main/partials/viewer-thumbnails.scss @@ -175,7 +175,7 @@ text-overflow: ellipsis; span { - font-size: $fs13; + font-size: $fs12; } } } diff --git a/frontend/resources/styles/main/partials/workspace-header.scss b/frontend/resources/styles/main/partials/workspace-header.scss index 7c588f358..2b3034974 100644 --- a/frontend/resources/styles/main/partials/workspace-header.scss +++ b/frontend/resources/styles/main/partials/workspace-header.scss @@ -10,7 +10,7 @@ border-bottom: 1px solid $color-gray-60; display: flex; height: 48px; - padding: $x-small $medium $x-small 55px; + padding: $size-1 $size-4 $size-1 55px; position: relative; z-index: 12; justify-content: space-between; @@ -56,7 +56,7 @@ } .shared-badge { - margin-left: $x-small; + margin-left: $size-1; height: 16px; width: 16px; display: flex; @@ -81,7 +81,7 @@ position: relative; > * { - margin-left: $big; + margin-left: $size-5; } .zoom-dropdown { @@ -93,13 +93,13 @@ .project-tree { align-items: center; display: flex; - margin-left: $x-small; - padding: $x-small; + margin-left: $size-1; + padding: $size-1; svg { fill: $color-gray-20; height: 20px; - margin-right: $small; + margin-right: $size-2; width: 20px; } @@ -112,7 +112,7 @@ &.project-name { color: $color-gray-20; - margin-right: $x-small; + margin-right: $size-1; cursor: pointer; &:hover { @@ -147,7 +147,7 @@ li { cursor: pointer; font-size: $fs14; - padding: $small; + padding: $size-2; display: flex; justify-content: space-between; @@ -159,7 +159,7 @@ span { color: $color-gray-60; - margin: 0 $x-small; + margin: 0 $size-1; } .shortcut { @@ -183,7 +183,7 @@ margin: 0; li { - margin-left: $small; + margin-left: $size-2; position: relative; img { diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss index f167e9814..f6d7816d0 100644 --- a/frontend/resources/styles/main/partials/workspace.scss +++ b/frontend/resources/styles/main/partials/workspace.scss @@ -18,12 +18,12 @@ top: 40px; width: 240px; z-index: 12; - padding: $x-small 0; + padding: $size-1 0; li { align-items: center; font-size: $fs14; - padding: $x-small $medium; + padding: $size-1 $size-4; cursor: pointer; display: flex; @@ -47,6 +47,17 @@ &:hover { background-color: $color-primary-lighter; } + + .submenu-icon { + position: absolute; + right: 1rem; + + svg { + width: 10px; + height: 10px; + } + } + } } @@ -227,10 +238,6 @@ font-size: $fs12; } -.selected .workspace-frame-label { - fill: $color-primary-dark; -} - .multiuser-cursor { align-items: center; display: flex; @@ -250,8 +257,8 @@ border-radius: $br-small; color: $color-black; font-size: $fs12; - margin-left: $small; - padding: $x-small; + margin-left: $size-2; + padding: $size-1; } } diff --git a/frontend/resources/styles/main/partials/zoom-widget.scss b/frontend/resources/styles/main/partials/zoom-widget.scss index b25192321..7aa2a646a 100644 --- a/frontend/resources/styles/main/partials/zoom-widget.scss +++ b/frontend/resources/styles/main/partials/zoom-widget.scss @@ -1,11 +1,12 @@ .zoom-widget { cursor: pointer; display: flex; + position: relative; span { color: $color-gray-10; font-size: $fs14; - margin-left: $x-small; + margin-left: $size-1; } .icon svg { @@ -16,8 +17,10 @@ .dropdown { position: absolute; - z-index: 12; + right: 0; + top: 25px; width: 210px; + z-index: 12; background-color: $color-white; border-radius: $br-small; @@ -28,7 +31,7 @@ cursor: pointer; font-size: $fs14; display: flex; - padding: $small; + padding: $size-2; span { color: $color-gray-20; diff --git a/frontend/scripts/build b/frontend/scripts/build index 1931f5431..11f608e7f 100755 --- a/frontend/scripts/build +++ b/frontend/scripts/build @@ -8,7 +8,7 @@ EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS; yarn install || exit 1; npx gulp clean || exit 1; -clojure -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}\"}" $EXTRA_PARAMS || exit 1 +clojure -J-Xms1G -J-Xmx1G -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}\"}" $EXTRA_PARAMS || exit 1 npx gulp build || exit 1; npx gulp dist:clean || exit 1; npx gulp dist:copy || exit 1; diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index 1d9527552..942e2c93d 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -1,6 +1,6 @@ {:deps {:aliases [:dev]} :http {:port 3448} - :nrepl {:port 3447} + :nrepl {:port 3447 :host "0.0.0.0"} :jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"] :dev-http {8888 "classpath:public"} @@ -29,7 +29,9 @@ :warnings {:fn-deprecated false}} :release - {:compiler-options + {:closure-defines {goog.DEBUG false + goog.debug.LOGGING_ENABLED true} + :compiler-options {:fn-invoke-direct true :source-map true :elide-asserts true diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 8e43d15aa..9e308a1ca 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -54,22 +54,11 @@ :browser :webworker)) -(def available-flags - #{:registration - :audit-log - :demo-users - :user-feedback - :demo-warning - :login-with-ldap}) - -(def default-flags - #{:registration :demo-users}) - (defn- parse-flags [global] (let [flags (obj/get global "penpotFlags" "") flags (into #{} (map keyword) (str/words flags))] - (flags/parse default-flags flags))) + (flags/parse flags flags/default))) (defn- parse-version [global] @@ -88,6 +77,7 @@ (def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js")) (def translations (obj/get global "penpotTranslations")) (def themes (obj/get global "penpotThemes")) +(def sentry-dsn (obj/get global "penpotSentryDsn")) (def flags (atom (parse-flags global))) (def version (atom (parse-version global))) @@ -103,7 +93,8 @@ (when (false? registration) (swap! flags disj :registration))) -(def public-uri +(defn get-public-uri + [] (let [uri (u/uri (or (obj/get global "penpotPublicURI") (.-origin ^js location)))] ;; Ensure that the path always ends with "/"; this ensures that @@ -112,9 +103,7 @@ (not (str/ends-with? (:path uri) "/")) (update :path #(str % "/"))))) -(when (= :browser @target) - (js/console.log - (str/format "Welcome to penpot! version='%s' base-uri='%s'." (:full @version) (str public-uri)))) +(def public-uri (get-public-uri)) ;; --- Helper Functions diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 9a6842730..04b6a00f4 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -6,25 +6,23 @@ (ns app.main (:require - [app.common.spec :as us] + [app.common.logging :as log] [app.common.uuid :as uuid] - [app.config :as cfg] + [app.config :as cf] [app.main.data.events :as ev] - [app.main.data.messages :as dm] [app.main.data.users :as du] + [app.main.errors] + [app.main.sentry :as sentry] [app.main.store :as st] [app.main.ui :as ui] [app.main.ui.confirm] [app.main.ui.modal :refer [modal]] + [app.main.ui.routes :as rt] [app.main.worker] [app.util.dom :as dom] [app.util.i18n :as i18n] - [app.util.logging :as log] - [app.util.router :as rt] - [app.util.storage :refer [storage]] [app.util.theme :as theme] [beicon.core :as rx] - [cljs.spec.alpha :as s] [potok.core :as ptk] [rumext.alpha :as mf])) @@ -32,79 +30,38 @@ (log/set-level! :root :warn) (log/set-level! :app :info) +(when (= :browser @cf/target) + (log/info :message "Welcome to penpot" :version (:full @cf/version) :public-uri (str cf/public-uri))) + (declare reinit) -(s/def ::any any?) - -(defn match-path - [router path] - (when-let [match (rt/match router path)] - (if-let [conform (get-in match [:data :conform])] - (let [spath (get conform :path-params ::any) - squery (get conform :query-params ::any)] - (try - (-> (dissoc match :params) - (assoc :path-params (us/conform spath (get match :path-params)) - :query-params (us/conform squery (get match :query-params)))) - (catch :default _ - nil))) - match))) - -(defn on-navigate - [router path] - (let [match (match-path router path) - profile (:profile @storage) - nopath? (or (= path "") (= path "/")) - authed? (and (not (nil? profile)) - (not= (:id profile) uuid/zero))] - - (cond - (and nopath? authed? (nil? match)) - (if (not= uuid/zero profile) - (st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)})) - (st/emit! (rt/nav :auth-login))) - - (and (not authed?) (nil? match)) - (st/emit! (rt/nav :auth-login)) - - (nil? match) - (st/emit! (dm/assign-exception {:type :not-found})) - - :else - (st/emit! (rt/navigated match))))) - (defn init-ui [] (mf/mount (mf/element ui/app) (dom/get-element "app")) (mf/mount (mf/element modal) (dom/get-element "modal"))) - (defn initialize [] - (letfn [(on-profile [_profile] - (rx/of (rt/initialize-router ui/routes) - (rt/initialize-history on-navigate)))] - (ptk/reify ::initialize - ptk/UpdateEvent - (update [_ state] - (assoc state :session-id (uuid/next))) + (ptk/reify ::initialize + ptk/UpdateEvent + (update [_ state] + (assoc state :session-id (uuid/next))) - ptk/WatchEvent - (watch [_ _ stream] - (rx/merge - (rx/of - (ptk/event ::ev/initialize) - (du/initialize-profile)) - (->> stream - (rx/filter (ptk/type? ::du/profile-fetched)) - (rx/take 1) - (rx/map deref) - (rx/mapcat on-profile))))))) + ptk/WatchEvent + (watch [_ _ stream] + (rx/merge + (rx/of (ptk/event ::ev/initialize) + (du/initialize-profile)) + (->> stream + (rx/filter du/profile-fetched?) + (rx/take 1) + (rx/map #(rt/init-routes))))))) (defn ^:export init [] - (i18n/init! cfg/translations) - (theme/init! cfg/themes) + (sentry/init!) + (i18n/init! cf/translations) + (theme/init! cf/themes) (init-ui) (st/emit! (initialize))) @@ -114,11 +71,14 @@ (mf/unmount (dom/get-element "modal")) (init-ui)) -(add-watch i18n/locale "locale" (fn [_ _ o v] - (when (not= o v) - (reinit)))) - (defn ^:dev/after-load after-load [] (reinit)) +;; Reload the UI when the language changes +(add-watch + i18n/locale "locale" + (fn [_ _ old-value current-value] + (when (not= old-value current-value) + (reinit)))) + diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs index 95387f5ec..e3f26495e 100644 --- a/frontend/src/app/main/data/comments.cljs +++ b/frontend/src/app/main/data/comments.cljs @@ -77,7 +77,8 @@ (watch [_ _ _] (->> (rp/mutation :create-comment-thread params) (rx/mapcat #(rp/query :comment-thread {:file-id (:file-id %) :id (:id %)})) - (rx/map #(partial created %))))))) + (rx/map #(partial created %)) + (rx/catch #(rx/throw {:type :comment-error}))))))) (defn update-comment-thread-status [{:keys [id] :as thread}] @@ -87,7 +88,8 @@ (watch [_ _ _] (let [done #(d/update-in-when % [:comment-threads id] assoc :count-unread-comments 0)] (->> (rp/mutation :update-comment-thread-status {:id id}) - (rx/map (constantly done))))))) + (rx/map (constantly done)) + (rx/catch #(rx/throw {:type :comment-error}))))))) (defn update-comment-thread @@ -104,6 +106,7 @@ ptk/WatchEvent (watch [_ _ _] (->> (rp/mutation :update-comment-thread {:id id :is-resolved is-resolved}) + (rx/catch #(rx/throw {:type :comment-error})) (rx/ignore))))) @@ -118,7 +121,8 @@ (watch [_ _ _] (rx/concat (->> (rp/mutation :add-comment {:thread-id (:id thread) :content content}) - (rx/map #(partial created %))) + (rx/map #(partial created %)) + (rx/catch #(rx/throw {:type :comment-error}))) (rx/of (refresh-comment-thread thread))))))) (defn update-comment @@ -132,6 +136,7 @@ ptk/WatchEvent (watch [_ _ _] (->> (rp/mutation :update-comment {:id id :content content}) + (rx/catch #(rx/throw {:type :comment-error})) (rx/ignore))))) (defn delete-comment-thread @@ -147,6 +152,7 @@ ptk/WatchEvent (watch [_ _ _] (->> (rp/mutation :delete-comment-thread {:id id}) + (rx/catch #(rx/throw {:type :comment-error})) (rx/ignore))))) (defn delete-comment @@ -160,6 +166,7 @@ ptk/WatchEvent (watch [_ _ _] (->> (rp/mutation :delete-comment {:id id}) + (rx/catch #(rx/throw {:type :comment-error})) (rx/ignore))))) (defn refresh-comment-thread @@ -171,7 +178,8 @@ ptk/WatchEvent (watch [_ _ _] (->> (rp/query :comment-thread {:file-id file-id :id id}) - (rx/map #(partial fetched %))))))) + (rx/map #(partial fetched %)) + (rx/catch #(rx/throw {:type :comment-error}))))))) (defn retrieve-comment-threads [file-id] @@ -182,7 +190,8 @@ ptk/WatchEvent (watch [_ _ _] (->> (rp/query :comment-threads {:file-id file-id}) - (rx/map #(partial fetched %))))))) + (rx/map #(partial fetched %)) + (rx/catch #(rx/throw {:type :comment-error}))))))) (defn retrieve-comments [thread-id] @@ -193,7 +202,8 @@ ptk/WatchEvent (watch [_ _ _] (->> (rp/query :comments {:thread-id thread-id}) - (rx/map #(partial fetched %))))))) + (rx/map #(partial fetched %)) + (rx/catch #(rx/throw {:type :comment-error}))))))) (defn retrieve-unread-comment-threads "A event used mainly in dashboard for retrieve all unread threads of a team." @@ -204,7 +214,8 @@ (watch [_ _ _] (let [fetched #(assoc %2 :comment-threads (d/index-by :id %1))] (->> (rp/query :unread-comment-threads {:team-id team-id}) - (rx/map #(partial fetched %))))))) + (rx/map #(partial fetched %)) + (rx/catch #(rx/throw {:type :comment-error}))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 6eb812246..7025dc5ea 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -67,6 +67,7 @@ (ptk/reify ::initialize ptk/UpdateEvent (update [_ state] + (du/set-current-team! id) (let [prev-team-id (:current-team-id state)] (cond-> state (not= prev-team-id id) @@ -749,7 +750,6 @@ (ptk/reify ::go-to-projects-1 ptk/WatchEvent (watch [_ _ _] - (du/set-current-team! team-id) (rx/of (rt/nav :dashboard-projects {:team-id team-id})))))) (defn go-to-team-members diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs index 81d00c9e5..96fb4dbba 100644 --- a/frontend/src/app/main/data/fonts.cljs +++ b/frontend/src/app/main/data/fonts.cljs @@ -8,12 +8,12 @@ (:require ["opentype.js" :as ot] [app.common.data :as d] + [app.common.logging :as log] [app.common.media :as cm] [app.common.spec :as us] [app.common.uuid :as uuid] [app.main.fonts :as fonts] [app.main.repo :as rp] - [app.util.logging :as log] [app.util.webapi :as wa] [beicon.core :as rx] [cuerdas.core :as str] diff --git a/frontend/src/app/main/data/messages.cljs b/frontend/src/app/main/data/messages.cljs index acd156430..74c1ddebe 100644 --- a/frontend/src/app/main/data/messages.cljs +++ b/frontend/src/app/main/data/messages.cljs @@ -128,12 +128,3 @@ :controls controls :actions actions :tag tag}))) - -(defn assign-exception - [error] - (ptk/reify ::assign-exception - ptk/UpdateEvent - (update [_ state] - (if (nil? error) - (dissoc state :exception) - (assoc state :exception error))))) diff --git a/frontend/src/app/main/data/shortcuts.cljs b/frontend/src/app/main/data/shortcuts.cljs index 69c9596bc..0494f9913 100644 --- a/frontend/src/app/main/data/shortcuts.cljs +++ b/frontend/src/app/main/data/shortcuts.cljs @@ -8,9 +8,9 @@ (:refer-clojure :exclude [meta reset!]) (:require ["mousetrap" :as mousetrap] + [app.common.logging :as log] [app.common.spec :as us] - [app.config :as cfg] - [app.util.logging :as log] + [app.config :as cf] [cljs.spec.alpha :as s] [potok.core :as ptk])) @@ -37,7 +37,7 @@ "Adds the control/command modifier to a shortcuts depending on the operating system for the user" [shortcut] - (if (cfg/check-platform? :macos) + (if (cf/check-platform? :macos) (str "command+" shortcut) (str "ctrl+" shortcut))) @@ -55,12 +55,12 @@ [key] ;; If the key is "+" we need to surround with quotes ;; otherwise will not be very readable - (let [key (if (and (not (cfg/check-platform? :macos)) + (let [key (if (and (not (cf/check-platform? :macos)) (= key "+")) "\"+\"" key)] (str - (if (cfg/check-platform? :macos) + (if (cf/check-platform? :macos) mac-command "Ctrl+") key))) @@ -68,7 +68,7 @@ (defn shift [key] (str - (if (cfg/check-platform? :macos) + (if (cf/check-platform? :macos) mac-shift "Shift+") key)) @@ -76,7 +76,7 @@ (defn alt [key] (str - (if (cfg/check-platform? :macos) + (if (cf/check-platform? :macos) mac-option "Alt+") key)) @@ -91,19 +91,19 @@ (defn supr [] - (if (cfg/check-platform? :macos) + (if (cf/check-platform? :macos) mac-delete "Supr")) (defn esc [] - (if (cfg/check-platform? :macos) + (if (cf/check-platform? :macos) mac-esc "Escape")) (defn enter [] - (if (cfg/check-platform? :macos) + (if (cf/check-platform? :macos) mac-enter "Enter")) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 07ad91268..3356c0ae9 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -62,14 +62,26 @@ (defn teams-fetched [teams] - (let [teams (d/index-by :id teams)] + (let [teams (d/index-by :id teams) + ids (into #{} (keys teams))] + (ptk/reify ::teams-fetched IDeref (-deref [_] teams) ptk/UpdateEvent (update [_ state] - (assoc state :teams teams))))) + (assoc state :teams teams)) + + ptk/EffectEvent + (effect [_ _ _] + ;; Check if current team-id is part of available teams + ;; if not, dissoc it from storage. + (when-let [ctid (::current-team-id @storage)] + (when-not (contains? ids ctid) + (swap! storage dissoc ::current-team-id))))))) + + (defn fetch-teams [] @@ -81,6 +93,9 @@ ;; --- EVENT: fetch-profile +(def profile-fetched? + (ptk/type? ::profile-fetched)) + (defn profile-fetched [{:keys [id] :as profile}] (us/verify ::profile profile) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index c30ed5bf2..15f39ba1f 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -31,6 +31,7 @@ :comments-show :unresolved :selected #{} :collapsed #{} + :overlays [] :hover nil}) (declare fetch-comment-threads) @@ -110,6 +111,7 @@ (rx/of (df/fonts-fetched fonts) (bundle-fetched (merge bundle params)))))))))) +(declare go-to-frame-auto) (defn bundle-fetched [{:keys [project file share-links libraries users permissions] :as bundle}] @@ -129,7 +131,15 @@ :permissions permissions :project project :pages pages - :file file})))))) + :file file}))) + + ptk/WatchEvent + (watch [_ state _] + (let [route (:route state) + qparams (:query-params route) + index (:index qparams)] + (when (nil? index) + (rx/of (go-to-frame-auto)))))))) (defn fetch-comment-threads [{:keys [file-id page-id] :as params}] @@ -218,6 +228,12 @@ (update [_ state] (update-in state [:viewer-local :show-thumbnails] not)))) +(def close-thumbnails-panel + (ptk/reify ::close-thumbnails-panel + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:viewer-local :show-thumbnails] false)))) + (def select-prev-frame (ptk/reify ::select-prev-frame ptk/WatchEvent @@ -286,11 +302,15 @@ (update [_ state] (assoc-in state [:viewer-local :interactions-show?] false)))) -;; --- Navigation +;; --- Navigation inside page (defn go-to-frame-by-index [index] (ptk/reify ::go-to-frame-by-index + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:viewer-local :overlays] [])) + ptk/WatchEvent (watch [_ state _] (let [route (:route state) @@ -303,6 +323,10 @@ [frame-id] (us/verify ::us/uuid frame-id) (ptk/reify ::go-to-frame + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:viewer-local :overlays] [])) + ptk/WatchEvent (watch [_ state _] (let [route (:route state) @@ -314,9 +338,27 @@ (when index (rx/of (go-to-frame-by-index index))))))) +(defn go-to-frame-auto + [] + (ptk/reify ::go-to-frame-auto + ptk/WatchEvent + (watch [_ state _] + (let [route (:route state) + qparams (:query-params route) + page-id (:page-id qparams) + flows (get-in state [:viewer :pages page-id :options :flows])] + (if (seq flows) + (let [frame-id (:starting-frame (first flows))] + (rx/of (go-to-frame frame-id))) + (rx/of (go-to-frame-by-index 0))))))) + (defn go-to-section [section] (ptk/reify ::go-to-section + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:viewer-local :overlays] [])) + ptk/WatchEvent (watch [_ state _] (let [route (:route state) @@ -324,6 +366,67 @@ qparams (:query-params route)] (rx/of (rt/nav :viewer pparams (assoc qparams :section section))))))) +;; --- Overlays + +(defn open-overlay + [frame-id position close-click-outside background-overlay] + (us/verify ::us/uuid frame-id) + (us/verify ::us/point position) + (us/verify (s/nilable ::us/boolean) close-click-outside) + (us/verify (s/nilable ::us/boolean) background-overlay) + (ptk/reify ::open-overlay + ptk/UpdateEvent + (update [_ state] + (let [route (:route state) + qparams (:query-params route) + page-id (:page-id qparams) + frames (get-in state [:viewer :pages page-id :frames]) + frame (d/seek #(= (:id %) frame-id) frames) + overlays (get-in state [:viewer-local :overlays])] + (if-not (some #(= (:frame %) frame) overlays) + (update-in state [:viewer-local :overlays] conj + {:frame frame + :position position + :close-click-outside close-click-outside + :background-overlay background-overlay}) + state))))) + +(defn toggle-overlay + [frame-id position close-click-outside background-overlay] + (us/verify ::us/uuid frame-id) + (us/verify ::us/point position) + (us/verify (s/nilable ::us/boolean) close-click-outside) + (us/verify (s/nilable ::us/boolean) background-overlay) + (ptk/reify ::toggle-overlay + ptk/UpdateEvent + (update [_ state] + (let [route (:route state) + qparams (:query-params route) + page-id (:page-id qparams) + frames (get-in state [:viewer :pages page-id :frames]) + frame (d/seek #(= (:id %) frame-id) frames) + overlays (get-in state [:viewer-local :overlays])] + (if-not (some #(= (:frame %) frame) overlays) + (update-in state [:viewer-local :overlays] conj + {:frame frame + :position position + :close-click-outside close-click-outside + :background-overlay background-overlay}) + (update-in state [:viewer-local :overlays] + (fn [overlays] + (d/removev #(= (:id (:frame %)) frame-id) overlays)))))))) + +(defn close-overlay + [frame-id] + (ptk/reify ::close-overlay + ptk/UpdateEvent + (update [_ state] + (update-in state [:viewer-local :overlays] + (fn [overlays] + (d/removev #(= (:id (:frame %)) frame-id) overlays)))))) + +;; --- Objects selection + (defn deselect-all [] (ptk/reify ::deselect-all ptk/UpdateEvent @@ -397,7 +500,7 @@ (update [_ state] (assoc-in state [:viewer-local :hover] (when hover? id))))) -;; --- NAV +;; --- Navigation outside page (defn go-to-dashboard [] @@ -411,6 +514,10 @@ (defn go-to-page [page-id] (ptk/reify ::go-to-page + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:viewer-local :overlays] [])) + ptk/WatchEvent (watch [_ state _] (let [route (:route state) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 3411db165..e5e36f6ab 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -22,13 +22,16 @@ [app.config :as cfg] [app.main.data.events :as ev] [app.main.data.messages :as dm] + [app.main.data.workspace.booleans :as dwb] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.groups :as dwg] + [app.main.data.workspace.interactions :as dwi] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.notifications :as dwn] [app.main.data.workspace.path :as dwdp] + [app.main.data.workspace.path.shapes-to-path :as dwps] [app.main.data.workspace.persistence :as dwp] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] @@ -49,8 +52,6 @@ [cuerdas.core :as str] [potok.core :as ptk])) -;; (log/set-level! :trace) - (s/def ::shape-attrs ::cp/shape-attrs) (s/def ::set-of-string (s/every string? :kind set?)) @@ -1096,7 +1097,7 @@ :text (rx/of (dwc/start-edition-mode id)) - :group + (:group :bool) (rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)]))) :svg-raw @@ -1282,8 +1283,7 @@ (watch [_ state _] (let [{:keys [current-file-id current-page-id]} state pparams {:file-id (or file-id current-file-id)} - qparams {:page-id (or page-id current-page-id) - :index 0}] + qparams {:page-id (or page-id current-page-id)}] (rx/of ::dwp/force-persist (rt/nav-new-window* {:rname :viewer :path-params pparams @@ -1322,10 +1322,33 @@ (ptk/reify ::show-context-menu ptk/UpdateEvent (update [_ state] - (let [mdata (cond-> params - (some? shape) - (assoc :selected - (wsh/lookup-selected state)))] + (let [selected (wsh/lookup-selected state) + objects (wsh/lookup-page-objects state) + + selected-with-children + (into [] + (mapcat #(cp/get-object-with-children % objects)) + selected) + + head (get objects (first selected)) + + first-not-group-like? + (and (= (count selected) 1) + (not (contains? #{:group :bool} (:type head)))) + + has-invalid-shapes? (->> selected-with-children + (some (comp #{:frame :text} :type))) + + disable-booleans? (or (empty? selected) has-invalid-shapes? first-not-group-like?) + disable-flatten? (or (empty? selected) has-invalid-shapes?) + + mdata + (-> params + (assoc :disable-booleans? disable-booleans?) + (assoc :disable-flatten? disable-flatten?) + (cond-> (some? shape) + (assoc :selected selected)))] + (assoc-in state [:workspace-local :context-menu] mdata))))) (defn show-shape-context-menu @@ -1534,8 +1557,12 @@ (= :frame (get-in objects [(first selected) :type]))))) (defn- paste-shape - [{:keys [selected objects images] :as data} in-viewport?] ;; TODO: perhaps rename 'objects' to 'shapes', because it contains only - (letfn [;; Given a file-id and img (part generated by the ;; the shapes to paste, not the whole page tree of shapes + [{selected :selected + paste-objects :objects ;; rename this because here comes only the clipboard shapes, + images :images ;; not the whole page tree of shapes. + :as data} + in-viewport?] + (letfn [;; Given a file-id and img (part generated by the ;; copy-selected event), uploads the new media. (upload-media [file-id imgpart] (->> (http/send! {:uri (:file-data imgpart) @@ -1567,7 +1594,7 @@ (calculate-paste-position [state mouse-pos in-viewport?] (let [page-objects (wsh/lookup-page-objects state) - selected-objs (map #(get objects %) selected) + selected-objs (map #(get paste-objects %) selected) has-frame? (d/seek #(= (:type %) :frame) selected-objs) page-selected (wsh/lookup-selected state) wrapper (gsh/selection-rect selected-objs) @@ -1594,12 +1621,12 @@ [frame-id parent-id delta index])))) ;; Change the indexes if the paste is done with an element selected - (change-add-obj-index [objects selected index change] + (change-add-obj-index [paste-objects selected index change] (let [set-index (fn [[result index] id] [(assoc result id index) (inc index)]) map-ids (when index - (->> (vals objects) + (->> (vals paste-objects) (filter #(not (selected (:parent-id %)))) (map :id) (reduce set-index [{} (inc index)]) @@ -1611,8 +1638,8 @@ ;; Check if the shape is an instance whose master is defined in a ;; library that is not linked to the current file - (foreign-instance? [shape objects state] - (let [root (cph/get-root-shape shape objects) + (foreign-instance? [shape paste-objects state] + (let [root (cph/get-root-shape shape paste-objects) root-file-id (:component-file root)] (and (some? root) (not= root-file-id (:current-file-id state)) @@ -1620,34 +1647,37 @@ ;; Procceed with the standard shape paste procediment. (do-paste [it state mouse-pos media] - (let [media-idx (d/index-by :prev-id media) + (let [page-objects (wsh/lookup-page-objects state) + media-idx (d/index-by :prev-id media) ;; Calculate position for the pasted elements [frame-id parent-id delta index] (calculate-paste-position state mouse-pos in-viewport?) - objects (->> objects - (d/mapm (fn [_ shape] - (-> shape - (assoc :frame-id frame-id) - (assoc :parent-id parent-id) + paste-objects (->> paste-objects + (d/mapm (fn [_ shape] + (-> shape + (assoc :frame-id frame-id) + (assoc :parent-id parent-id) - (cond-> - ;; if foreign instance, detach the shape - (foreign-instance? shape objects state) - (dissoc :component-id - :component-file - :component-root? - :remote-synced? - :shape-ref - :touched)))))) + (cond-> + ;; if foreign instance, detach the shape + (foreign-instance? shape paste-objects state) + (dissoc :component-id + :component-file + :component-root? + :remote-synced? + :shape-ref + :touched)))))) + + all-objects (merge page-objects paste-objects) page-id (:current-page-id state) unames (-> (wsh/lookup-page-objects state page-id) (dwc/retrieve-used-names)) ;; TODO: move this calculation inside prepare-duplcate-changes? - rchanges (->> (dws/prepare-duplicate-changes objects page-id unames selected delta) + rchanges (->> (dws/prepare-duplicate-changes all-objects page-id unames selected delta) (mapv (partial process-rchange media-idx)) - (mapv (partial change-add-obj-index objects selected index))) + (mapv (partial change-add-obj-index paste-objects selected index))) uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %)) (reverse rchanges)) @@ -1751,69 +1781,12 @@ ;; Interactions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare move-create-interaction) -(declare finish-create-interaction) - -(defn start-create-interaction - [] - (ptk/reify ::start-create-interaction - ptk/WatchEvent - (watch [_ state stream] - (let [initial-pos @ms/mouse-position - selected (wsh/lookup-selected state) - stopper (rx/filter ms/mouse-up? stream)] - (when (= 1 (count selected)) - (rx/concat - (->> ms/mouse-position - (rx/take-until stopper) - (rx/map #(move-create-interaction initial-pos %))) - (rx/of (finish-create-interaction initial-pos)))))))) - -(defn move-create-interaction - [initial-pos position] - (ptk/reify ::move-create-interaction - ptk/UpdateEvent - (update [_ state] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - selected-shape-id (-> state wsh/lookup-selected first) - selected-shape (get objects selected-shape-id) - selected-shape-frame-id (:frame-id selected-shape) - start-frame (get objects selected-shape-frame-id) - end-frame (dwc/get-frame-at-point objects position)] - (cond-> state - (not= position initial-pos) (assoc-in [:workspace-local :draw-interaction-to] position) - (not= start-frame end-frame) (assoc-in [:workspace-local :draw-interaction-to-frame] end-frame)))))) - -(defn finish-create-interaction - [initial-pos] - (ptk/reify ::finish-create-interaction - ptk/UpdateEvent - (update [_ state] - (-> state - (assoc-in [:workspace-local :draw-interaction-to] nil) - (assoc-in [:workspace-local :draw-interaction-to-frame] nil))) - - ptk/WatchEvent - (watch [_ state _] - (let [position @ms/mouse-position - page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - frame (dwc/get-frame-at-point objects position) - - shape-id (-> state wsh/lookup-selected first) - shape (get objects shape-id)] - - (when-not (= position initial-pos) - (if (and frame shape-id - (not= (:id frame) (:id shape)) - (not= (:id frame) (:frame-id shape))) - (rx/of (update-shape shape-id - {:interactions [{:event-type :click - :action-type :navigate - :destination (:id frame)}]})) - (rx/of (update-shape shape-id - {:interactions []})))))))) +(d/export dwi/start-edit-interaction) +(d/export dwi/move-edit-interaction) +(d/export dwi/finish-edit-interaction) +(d/export dwi/start-move-overlay-pos) +(d/export dwi/move-overlay-pos) +(d/export dwi/finish-move-overlay-pos) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; CANVAS OPTIONS @@ -1887,3 +1860,12 @@ (d/export dwg/unmask-group) (d/export dwg/group-selected) (d/export dwg/ungroup-selected) + +;; Boolean +(d/export dwb/create-bool) +(d/export dwb/group-to-bool) +(d/export dwb/bool-to-group) +(d/export dwb/change-bool-type) + +;; Shapes to path +(d/export dwps/convert-selected-to-path) diff --git a/frontend/src/app/main/data/workspace/booleans.cljs b/frontend/src/app/main/data/workspace/booleans.cljs new file mode 100644 index 000000000..5c3c1af7b --- /dev/null +++ b/frontend/src/app/main/data/workspace/booleans.cljs @@ -0,0 +1,131 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.data.workspace.booleans + (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] + [app.common.pages :as cp] + [app.common.pages.changes-builder :as cb] + [app.common.path.shapes-to-path :as stp] + [app.common.uuid :as uuid] + [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.state-helpers :as wsh] + [beicon.core :as rx] + [cuerdas.core :as str] + [potok.core :as ptk])) + +(defn selected-shapes + [state] + (let [objects (wsh/lookup-page-objects state)] + (->> (wsh/lookup-selected state) + (cp/clean-loops objects) + (map #(get objects %)) + (filter #(not= :frame (:type %))) + (map #(assoc % ::index (cp/position-on-parent (:id %) objects))) + (sort-by ::index)))) + +(defn create-bool-data + [bool-type name shapes objects] + (let [shapes (mapv #(stp/convert-to-path % objects) shapes) + head (if (= bool-type :difference) (first shapes) (last shapes)) + head (cond-> head + (and (contains? head :svg-attrs) (nil? (:fill-color head))) + (assoc :fill-color "#000000")) + + head-data (select-keys head stp/style-properties)] + [(-> {:id (uuid/next) + :type :bool + :bool-type bool-type + :frame-id (:frame-id head) + :parent-id (:parent-id head) + :name name + :shapes []} + (merge head-data) + (gsh/update-bool-selrect shapes objects)) + (cp/position-on-parent (:id head) objects)])) + +(defn group->bool + [group bool-type objects] + + (let [shapes (->> (:shapes group) + (map #(get objects %)) + (mapv #(stp/convert-to-path % objects))) + head (if (= bool-type :difference) (first shapes) (last shapes)) + head (cond-> head + (and (contains? head :svg-attrs) (nil? (:fill-color head))) + (assoc :fill-color "#000000")) + head-data (select-keys head stp/style-properties)] + + (-> group + (assoc :type :bool) + (assoc :bool-type bool-type) + (merge head-data) + (gsh/update-bool-selrect shapes objects)))) + +(defn bool->group + [shape objects] + + (let [children (->> (:shapes shape) + (mapv #(get objects %)))] + (-> shape + (assoc :type :group) + (dissoc :bool-type) + (d/without-keys stp/style-group-properties) + (gsh/update-group-selrect children)))) + +(defn create-bool + [bool-type] + (ptk/reify ::create-bool-union + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state) + base-name (-> bool-type d/name str/capital (str "-1")) + name (-> (dwc/retrieve-used-names objects) + (dwc/generate-unique-name base-name)) + shapes (selected-shapes state)] + + (when-not (empty? shapes) + (let [[boolean-data index] (create-bool-data bool-type name shapes objects) + shape-id (:id boolean-data) + changes (-> (cb/empty-changes it page-id) + (cb/with-objects objects) + (cb/add-obj boolean-data index) + (cb/change-parent shape-id shapes))] + (rx/of (dch/commit-changes changes) + (dwc/select-shapes (d/ordered-set shape-id))))))))) + +(defn group-to-bool + [shape-id bool-type] + (ptk/reify ::group-to-bool + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + change-to-bool + (fn [shape] (group->bool shape bool-type objects))] + (rx/of (dch/update-shapes [shape-id] change-to-bool {:reg-objects? true})))))) + +(defn bool-to-group + [shape-id] + (ptk/reify ::bool-to-group + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + change-to-group + (fn [shape] (bool->group shape objects))] + (rx/of (dch/update-shapes [shape-id] change-to-group {:reg-objects? true})))))) + + +(defn change-bool-type + [shape-id bool-type] + (ptk/reify ::change-bool-type + ptk/WatchEvent + (watch [_ _ _] + (let [change-type + (fn [shape] (assoc shape :bool-type bool-type))] + (rx/of (dch/update-shapes [shape-id] change-type {:reg-objects? true})))))) diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index 4edfa71fd..99c050db6 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -7,6 +7,7 @@ (ns app.main.data.workspace.changes (:require [app.common.data :as d] + [app.common.logging :as log] [app.common.pages :as cp] [app.common.pages.spec :as spec] [app.common.spec :as us] @@ -14,7 +15,6 @@ [app.main.data.workspace.undo :as dwu] [app.main.store :as st] [app.main.worker :as uw] - [app.util.logging :as log] [beicon.core :as rx] [cljs.spec.alpha :as s] [potok.core :as ptk])) diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 0fe0939d7..6cb0af4f7 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -9,15 +9,17 @@ [app.common.data :as d] [app.common.geom.proportions :as gpr] [app.common.geom.shapes :as gsh] + [app.common.logging :as log] [app.common.pages :as cp] [app.common.spec :as us] + [app.common.types.interactions :as cti] + [app.common.types.page-options :as cto] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.streams :as ms] [app.main.worker :as uw] - [app.util.logging :as log] [beicon.core :as rx] [cljs.spec.alpha :as s] [potok.core :as ptk])) @@ -379,8 +381,10 @@ (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) + options (wsh/lookup-page-options state page-id) ids (cp/clean-loops objects ids) + flows (:flows options) groups-to-unmask (reduce (fn [group-ids id] @@ -399,9 +403,14 @@ interacting-shapes (filter (fn [shape] (let [interactions (:interactions shape)] - (some ids (map :destination interactions)))) + (some #(and (cti/has-destination %) + (contains? ids (:destination %))) + interactions))) (vals objects)) + starting-flows + (filter #(contains? ids (:starting-frame %)) flows) + empty-parents-xform (comp (map (fn [id] (get objects id))) @@ -467,7 +476,8 @@ :operations [{:type :set :attr :interactions :val (vec (remove (fn [interaction] - (contains? ids (:destination interaction))) + (and (cti/has-destination interaction) + (contains? ids (:destination interaction)))) (:interactions obj)))}]}))) mk-mod-int-add-xf (comp (filter some?) @@ -479,6 +489,22 @@ :attr :interactions :val (:interactions obj)}]}))) + mk-mod-del-flow-xf + (comp (filter some?) + (map (fn [flow] + {:type :set-option + :page-id page-id + :option :flows + :value (cto/remove-flow flows (:id flow))}))) + + mk-mod-add-flow-xf + (comp (filter some?) + (map (fn [_] + {:type :set-option + :page-id page-id + :option :flows + :value flows}))) + mk-mod-unmask-xf (comp (filter (partial contains? objects)) (map (fn [id] @@ -508,7 +534,8 @@ :page-id page-id :shapes (vec all-parents)}) (into mk-mod-unmask-xf groups-to-unmask) - (into mk-mod-int-del-xf interacting-shapes)) + (into mk-mod-int-del-xf interacting-shapes) + (into mk-mod-del-flow-xf starting-flows)) uchanges (-> [] @@ -520,8 +547,8 @@ :shapes (vec all-parents)}) (into mk-mod-touched-xf (reverse all-parents)) (into mk-mod-mask-xf groups-to-unmask) - (into mk-mod-int-add-xf interacting-shapes)) - ] + (into mk-mod-int-add-xf interacting-shapes) + (into mk-mod-add-flow-xf starting-flows))] ;; (println "================ rchanges") ;; (cljs.pprint/pprint rchanges) diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index f3f92c2b3..11605a13b 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -198,7 +198,7 @@ group-id (first selected) group (get objects group-id)] (when (and (= 1 (count selected)) - (= (:type group) :group)) + (contains? #{:group :bool} (:type group))) (let [[rchanges uchanges] (prepare-remove-group page-id group objects)] (rx/of (dch/commit-changes {:redo-changes rchanges diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs new file mode 100644 index 000000000..bf7ba200f --- /dev/null +++ b/frontend/src/app/main/data/workspace/interactions.cljs @@ -0,0 +1,332 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.data.workspace.interactions + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.pages.helpers :as cph] + [app.common.spec :as us] + [app.common.types.interactions :as cti] + [app.common.types.page-options :as cto] + [app.common.uuid :as uuid] + [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.state-helpers :as wsh] + [app.main.streams :as ms] + [beicon.core :as rx] + [potok.core :as ptk])) + +;; --- Flows + +(defn add-flow + [starting-frame] + (ptk/reify ::add-flow + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + flows (get-in state [:workspace-data + :pages-index + page-id + :options + :flows] []) + + unames (into #{} (map :name flows)) + name (dwc/generate-unique-name unames "Flow-1") + + new-flow {:id (uuid/next) + :name name + :starting-frame starting-frame}] + + (rx/of (dch/commit-changes + {:redo-changes [{:type :set-option + :page-id page-id + :option :flows + :value (cto/add-flow flows new-flow)}] + :undo-changes [{:type :set-option + :page-id page-id + :option :flows + :value flows}] + :origin it})))))) + +(defn add-flow-selected-frame + [] + (ptk/reify ::add-flow-selected-frame + ptk/WatchEvent + (watch [_ state _] + (let [selected (wsh/lookup-selected state)] + (rx/of (add-flow (first selected))))))) + +(defn remove-flow + [flow-id] + (us/verify ::us/uuid flow-id) + (ptk/reify ::remove-flow + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + flows (get-in state [:workspace-data + :pages-index + page-id + :options + :flows] [])] + (rx/of (dch/commit-changes + {:redo-changes [{:type :set-option + :page-id page-id + :option :flows + :value (cto/remove-flow flows flow-id)}] + :undo-changes [{:type :set-option + :page-id page-id + :option :flows + :value flows}] + :origin it})))))) + +(defn rename-flow + [flow-id name] + (us/verify ::us/uuid flow-id) + (us/verify ::us/string name) + (ptk/reify ::rename-flow + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + flows (get-in state [:workspace-data + :pages-index + page-id + :options + :flows] [])] + (rx/of (dch/commit-changes + {:redo-changes [{:type :set-option + :page-id page-id + :option :flows + :value (cto/update-flow flows flow-id + #(cto/rename-flow % name))}] + :undo-changes [{:type :set-option + :page-id page-id + :option :flows + :value flows}] + :origin it})))))) + + +(defn start-rename-flow + [id] + (us/verify ::us/uuid id) + (ptk/reify ::start-rename-flow + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :flow-for-rename] id)))) + +(defn end-rename-flow + [] + (ptk/reify ::end-rename-flow + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local dissoc :flow-for-rename)))) + +;; --- Interactions + +(defn add-new-interaction + ([shape] (add-new-interaction shape nil)) + ([shape destination] + (ptk/reify ::add-new-interaction + ptk/WatchEvent + (watch [_ state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + frame (cph/get-frame shape objects) + flows (get-in state [:workspace-data + :pages-index + page-id + :options + :flows] []) + flow (cto/get-frame-flow flows (:id frame))] + (rx/concat + (rx/of (dch/update-shapes [(:id shape)] + (fn [shape] + (let [new-interaction (cti/set-destination + cti/default-interaction + destination)] + (update shape :interactions + cti/add-interaction new-interaction))))) + (when (and (not (cph/connected-frame? (:id frame) objects)) + (nil? flow)) + (rx/of (add-flow (:id frame)))))))))) + +(defn remove-interaction + [shape index] + (ptk/reify ::remove-interaction + ptk/WatchEvent + (watch [_ _ _] + (rx/of (dch/update-shapes [(:id shape)] + (fn [shape] + (update shape :interactions + cti/remove-interaction index))))))) + +(defn update-interaction + [shape index update-fn] + (ptk/reify ::update-interaction + ptk/WatchEvent + (watch [_ _ _] + (rx/of (dch/update-shapes [(:id shape)] + (fn [shape] + (update shape :interactions + cti/update-interaction index update-fn))))))) + +(declare move-edit-interaction) +(declare finish-edit-interaction) + +(defn start-edit-interaction + [index] + (ptk/reify ::start-edit-interaction + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :editing-interaction-index] index)) + + ptk/WatchEvent + (watch [_ state stream] + (let [initial-pos @ms/mouse-position + selected (wsh/lookup-selected state) + stopper (rx/filter ms/mouse-up? stream)] + (when (= 1 (count selected)) + (rx/concat + (->> ms/mouse-position + (rx/take-until stopper) + (rx/map #(move-edit-interaction initial-pos %))) + (rx/of (finish-edit-interaction index initial-pos)))))))) + +(defn move-edit-interaction + [initial-pos position] + (ptk/reify ::move-edit-interaction + ptk/UpdateEvent + (update [_ state] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + selected-shape-id (-> state wsh/lookup-selected first) + selected-shape (get objects selected-shape-id) + selected-shape-frame-id (:frame-id selected-shape) + start-frame (get objects selected-shape-frame-id) + end-frame (dwc/get-frame-at-point objects position)] + (cond-> state + (not= position initial-pos) (assoc-in [:workspace-local :draw-interaction-to] position) + (not= start-frame end-frame) (assoc-in [:workspace-local :draw-interaction-to-frame] end-frame)))))) + +(defn finish-edit-interaction + [index initial-pos] + (ptk/reify ::finish-edit-interaction + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-local :editing-interaction-index] nil) + (assoc-in [:workspace-local :draw-interaction-to] nil) + (assoc-in [:workspace-local :draw-interaction-to-frame] nil))) + + ptk/WatchEvent + (watch [_ state _] + (let [position @ms/mouse-position + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + frame (dwc/get-frame-at-point objects position) + + shape-id (-> state wsh/lookup-selected first) + shape (get objects shape-id)] + + (when (and shape (not (= position initial-pos))) + (if (nil? frame) + (when index + (rx/of (remove-interaction shape index))) + (let [frame (if (or (= (:id frame) (:id shape)) + (= (:id frame) (:frame-id shape))) + nil ;; Drop onto self frame -> set destination to none + frame)] + (if (nil? index) + (rx/of (add-new-interaction shape (:id frame))) + (rx/of (update-interaction shape index + (fn [interaction] + (cond-> interaction + (not (cti/has-destination interaction)) + (cti/set-action-type :navigate) + + :always + (cti/set-destination (:id frame)))))))))))))) +;; --- Overlays + +(declare move-overlay-pos) +(declare finish-move-overlay-pos) + +(defn start-move-overlay-pos + [index] + (ptk/reify ::start-move-overlay-pos + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-local :move-overlay-to] nil) + (assoc-in [:workspace-local :move-overlay-index] index))) + + ptk/WatchEvent + (watch [_ state stream] + (let [initial-pos @ms/mouse-position + selected (wsh/lookup-selected state) + stopper (rx/filter ms/mouse-up? stream)] + (when (= 1 (count selected)) + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + shape (->> state + wsh/lookup-selected + first + (get objects)) + overlay-pos (-> shape + (get-in [:interactions index]) + :overlay-position) + orig-frame (cph/get-frame shape objects) + frame-pos (gpt/point (:x orig-frame) (:y orig-frame)) + offset (-> initial-pos + (gpt/subtract overlay-pos) + (gpt/subtract frame-pos))] + (rx/concat + (->> ms/mouse-position + (rx/take-until stopper) + (rx/map #(move-overlay-pos % frame-pos offset))) + (rx/of (finish-move-overlay-pos index frame-pos offset))))))))) + +(defn move-overlay-pos + [pos frame-pos offset] + (ptk/reify ::move-overlay-pos + ptk/UpdateEvent + (update [_ state] + (let [pos (-> pos + (gpt/subtract frame-pos) + (gpt/subtract offset))] + (assoc-in state [:workspace-local :move-overlay-to] pos))))) + +(defn finish-move-overlay-pos + [index frame-pos offset] + (ptk/reify ::finish-move-overlay-pos + ptk/UpdateEvent + (update [_ state] + (-> state + (d/dissoc-in [:workspace-local :move-overlay-to]) + (d/dissoc-in [:workspace-local :move-overlay-index]))) + + ptk/WatchEvent + (watch [_ state _] + (let [pos @ms/mouse-position + overlay-pos (-> pos + (gpt/subtract frame-pos) + (gpt/subtract offset)) + + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + shape (->> state + wsh/lookup-selected + first + (get objects)) + + interactions (:interactions shape) + + new-interactions + (update interactions index + #(cti/set-overlay-position % overlay-pos))] + + (rx/of (dch/update-shapes [(:id shape)] #(merge % {:interactions new-interactions}))))))) + diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 995fb674c..9ba3a9a79 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.geom.shapes :as geom] + [app.common.logging :as log] [app.common.pages :as cp] [app.common.spec :as us] [app.common.uuid :as uuid] @@ -18,10 +19,10 @@ [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.libraries-helpers :as dwlh] [app.main.data.workspace.state-helpers :as wsh] + [app.main.data.workspace.undo :as dwu] [app.main.repo :as rp] [app.main.store :as st] [app.util.i18n :refer [tr]] - [app.util.logging :as log] [app.util.router :as rt] [app.util.time :as dt] [beicon.core :as rx] @@ -134,10 +135,12 @@ :color color} uchg {:type :mod-color :color prev}] - (rx/of (dch/commit-changes {:redo-changes [rchg] + (rx/of (dwu/start-undo-transaction) + (dch/commit-changes {:redo-changes [rchg] :undo-changes [uchg] :origin it}) - (sync-file (:current-file-id state) file-id)))))) + (sync-file (:current-file-id state) file-id) + (dwu/commit-undo-transaction)))))) (defn delete-color [{:keys [id] :as params}] @@ -244,10 +247,12 @@ :typography typography} uchg {:type :mod-typography :typography prev}] - (rx/of (dch/commit-changes {:redo-changes [rchg] + (rx/of (dwu/start-undo-transaction) + (dch/commit-changes {:redo-changes [rchg] :undo-changes [uchg] :origin it}) - (sync-file (:current-file-id state) file-id)))))) + (sync-file (:current-file-id state) file-id) + (dwu/commit-undo-transaction)))))) (defn delete-typography [id] @@ -516,12 +521,14 @@ (ptk/reify ::nav-to-component-file ptk/WatchEvent (watch [_ state _] - (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}] - (rx/of (rt/nav-new-window :workspace pparams qparams)))))) + (let [file (get-in state [:workspace-libraries file-id]) + path-params {:project-id (:project-id file) + :file-id (:id file)} + query-params {:page-id (first (get-in file [:data :pages])) + :layout :assets}] + (rx/of (rt/nav-new-window* {:rname :workspace + :path-params path-params + :query-params query-params})))))) (defn ext-library-changed [file-id modified-at revn changes] diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index 226d5b2ba..0f6a4a2e8 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -9,11 +9,11 @@ [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.geom.shapes :as geom] + [app.common.logging :as log] [app.common.pages :as cp] [app.common.spec :as us] [app.common.text :as txt] [app.main.data.workspace.groups :as dwg] - [app.util.logging :as log] [cljs.spec.alpha :as s] [clojure.set :as set])) diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 202266e79..8e7de7cae 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -7,7 +7,10 @@ (ns app.main.data.workspace.path.drawing (:require [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as upg] [app.common.pages :as cp] + [app.common.path.commands :as upc] + [app.common.path.shapes-to-path :as upsp] [app.common.spec :as us] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] @@ -21,9 +24,6 @@ [app.main.data.workspace.path.undo :as undo] [app.main.data.workspace.state-helpers :as wsh] [app.main.streams :as ms] - [app.util.path.commands :as upc] - [app.util.path.geom :as upg] - [app.util.path.shapes-to-path :as upsp] [beicon.core :as rx] [potok.core :as ptk])) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 89331ebd2..9df8b6e9a 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -8,6 +8,10 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as upg] + [app.common.path.commands :as upc] + [app.common.path.shapes-to-path :as upsp] + [app.common.path.subpaths :as ups] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.path.changes :as changes] @@ -19,10 +23,6 @@ [app.main.data.workspace.path.undo :as undo] [app.main.data.workspace.state-helpers :as wsh] [app.main.streams :as ms] - [app.util.path.commands :as upc] - [app.util.path.geom :as upg] - [app.util.path.shapes-to-path :as upsp] - [app.util.path.subpaths :as ups] [app.util.path.tools :as upt] [beicon.core :as rx] [potok.core :as ptk])) diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index 9b36e4099..a7d47d238 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -10,10 +10,10 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] + [app.common.path.commands :as upc] + [app.common.path.subpaths :as ups] [app.main.data.workspace.path.common :as common] [app.main.streams :as ms] - [app.util.path.commands :as upc] - [app.util.path.subpaths :as ups] [potok.core :as ptk])) (defn end-path-event? [event] diff --git a/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs new file mode 100644 index 000000000..eae4dfb91 --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs @@ -0,0 +1,36 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.data.workspace.path.shapes-to-path + (:require + [app.common.pages :as cp] + [app.common.pages.changes-builder :as cb] + [app.common.path.shapes-to-path :as upsp] + [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.state-helpers :as wsh] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn convert-selected-to-path [] + (ptk/reify ::convert-selected-to-path + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state) + selected (wsh/lookup-selected state) + + children-ids + (into #{} + (mapcat #(cp/get-children % objects)) + selected) + + changes + (-> (cb/empty-changes it page-id) + (cb/with-objects objects) + (cb/remove-objects children-ids) + (cb/update-shapes selected #(upsp/convert-to-path % objects)))] + + (rx/of (dch/commit-changes changes)))))) diff --git a/frontend/src/app/main/data/workspace/path/state.cljs b/frontend/src/app/main/data/workspace/path/state.cljs index 229e46256..86ac1731e 100644 --- a/frontend/src/app/main/data/workspace/path/state.cljs +++ b/frontend/src/app/main/data/workspace/path/state.cljs @@ -7,7 +7,7 @@ (ns app.main.data.workspace.path.state (:require [app.common.data :as d] - [app.util.path.shapes-to-path :as upsp])) + [app.common.path.shapes-to-path :as upsp])) (defn get-path-id "Retrieves the currently editing path id" @@ -31,7 +31,8 @@ [state & ks] (let [path-loc (get-path-location state) shape (-> (get-in state path-loc) - (upsp/convert-to-path))] + ;; Empty map because we know the current shape will not have children + (upsp/convert-to-path {}))] (if (empty? ks) shape diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index b67607ae0..8a8f8a59b 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -7,12 +7,12 @@ (ns app.main.data.workspace.path.streams (:require [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as upg] [app.common.math :as mth] [app.main.data.workspace.path.state :as state] [app.main.snap :as snap] [app.main.store :as st] [app.main.streams :as ms] - [app.util.path.geom :as upg] [beicon.core :as rx] [okulary.core :as l] [potok.core :as ptk])) diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 18f262743..fce88f9db 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -6,13 +6,13 @@ (ns app.main.data.workspace.path.tools (:require + [app.common.path.shapes-to-path :as upsp] + [app.common.path.subpaths :as ups] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.state :as st] [app.main.data.workspace.state-helpers :as wsh] - [app.util.path.shapes-to-path :as upsp] - [app.util.path.subpaths :as ups] [app.util.path.tools :as upt] [beicon.core :as rx] [potok.core :as ptk])) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 86208467f..ce5ff4160 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -12,6 +12,7 @@ [app.common.math :as mth] [app.common.pages :as cp] [app.common.spec :as us] + [app.common.types.interactions :as cti] [app.common.uuid :as uuid] [app.main.data.modal :as md] [app.main.data.workspace.changes :as dch] @@ -288,12 +289,17 @@ fit." [objects page-id unames ids delta] (let [unames (volatile! unames) - update-unames! (fn [new-name] (vswap! unames conj new-name))] + update-unames! (fn [new-name] (vswap! unames conj new-name)) + all-ids (reduce (fn [ids-set id] + (into ids-set (cons id (cp/get-children id objects)))) + #{} + ids) + ids-map (into {} (map #(vector % (uuid/next)) all-ids))] (loop [ids (seq ids) chgs []] (if ids (let [id (first ids) - result (prepare-duplicate-change objects page-id unames update-unames! id delta) + result (prepare-duplicate-change objects page-id unames update-unames! ids-map id delta) result (if (vector? result) result [result])] (recur (next ids) @@ -313,22 +319,27 @@ (-> changes (update-indices index-map)))) (defn- prepare-duplicate-change - [objects page-id unames update-unames! id delta] + [objects page-id unames update-unames! ids-map id delta] (let [obj (get objects id)] (if (= :frame (:type obj)) - (prepare-duplicate-frame-change objects page-id unames update-unames! obj delta) - (prepare-duplicate-shape-change objects page-id unames update-unames! obj delta (:frame-id obj) (:parent-id obj))))) + (prepare-duplicate-frame-change objects page-id unames update-unames! ids-map obj delta) + (prepare-duplicate-shape-change objects page-id unames update-unames! ids-map obj delta (:frame-id obj) (:parent-id obj))))) (defn- prepare-duplicate-shape-change - [objects page-id unames update-unames! obj delta frame-id parent-id] + [objects page-id unames update-unames! ids-map obj delta frame-id parent-id] (when (some? obj) - (let [id (uuid/next) + (let [new-id (ids-map (:id obj)) + parent-id (or parent-id frame-id) name (dwc/generate-unique-name @unames (:name obj)) _ (update-unames! name) - renamed-obj (assoc obj :id id :name name) - moved-obj (geom/move renamed-obj delta) - parent-id (or parent-id frame-id) + new-obj (-> obj + (assoc :id new-id + :name name + :frame-id frame-id) + (dissoc :shapes) + (geom/move delta) + (d/update-when :interactions #(cti/remap-interactions % ids-map objects))) children-changes (loop [result [] @@ -337,47 +348,45 @@ (if (nil? cid) result (let [obj (get objects cid) - changes (prepare-duplicate-shape-change objects page-id unames update-unames! obj delta frame-id id)] + changes (prepare-duplicate-shape-change objects page-id unames update-unames! ids-map obj delta frame-id new-id)] (recur (into result changes) (first cids) - (rest cids))))) + (rest cids)))))] - reframed-obj (-> moved-obj - (assoc :frame-id frame-id) - (dissoc :shapes))] (into [{:type :add-obj - :id id + :id new-id :page-id page-id :old-id (:id obj) :frame-id frame-id :parent-id parent-id :ignore-touched true - :obj (dissoc reframed-obj :shapes)}] + :obj new-obj}] children-changes)))) (defn- prepare-duplicate-frame-change - [objects page-id unames update-unames! obj delta] - (let [frame-id (uuid/next) + [objects page-id unames update-unames! ids-map obj delta] + (let [new-id (ids-map (:id obj)) frame-name (dwc/generate-unique-name @unames (:name obj)) _ (update-unames! frame-name) sch (->> (map #(get objects %) (:shapes obj)) - (mapcat #(prepare-duplicate-shape-change objects page-id unames update-unames! % delta frame-id frame-id))) + (mapcat #(prepare-duplicate-shape-change objects page-id unames update-unames! ids-map % delta new-id new-id))) - frame (-> obj - (assoc :id frame-id) - (assoc :name frame-name) - (assoc :frame-id uuid/zero) - (assoc :shapes []) - (geom/move delta)) + new-frame (-> obj + (assoc :id new-id + :name frame-name + :frame-id uuid/zero + :shapes []) + (geom/move delta) + (d/update-when :interactions #(cti/remap-interactions % ids-map objects))) fch {:type :add-obj :old-id (:id obj) :page-id page-id - :id frame-id + :id new-id :frame-id uuid/zero - :obj frame}] + :obj new-frame}] (into [fch] sch))) @@ -420,48 +429,49 @@ (gpt/point (+ (:width obj) 50) 0) (gpt/point 0 0)) - (let [obj-original (get objects id-original) - obj-duplicated (get objects id-duplicated) - distance (gpt/subtract (gpt/point obj-duplicated) - (gpt/point obj-original)) - new-pos (gpt/add (gpt/point obj-duplicated) distance) - delta (gpt/subtract new-pos (gpt/point obj))] - delta)))) + (let [pt-original (-> (get objects id-original) :selrect gpt/point) + pt-duplicated (-> (get objects id-duplicated) :selrect gpt/point) + pt-obj (-> obj :selrect gpt/point) + distance (gpt/subtract pt-duplicated pt-original) + new-pos (gpt/add pt-duplicated distance)] -(def duplicate-selected + (gpt/subtract new-pos pt-obj))))) + +(defn duplicate-selected [move-delta?] (ptk/reify ::duplicate-selected ptk/WatchEvent (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - selected (wsh/lookup-selected state) - delta (if (= (count selected) 1) - (let [obj (get objects (first selected))] - (calc-duplicate-delta obj state objects)) - (gpt/point 0 0)) + (when (nil? (get-in state [:workspace-local :transform])) + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + selected (wsh/lookup-selected state) + delta (if (and move-delta? (= (count selected) 1)) + (let [obj (get objects (first selected))] + (calc-duplicate-delta obj state objects)) + (gpt/point 0 0)) - unames (dwc/retrieve-used-names objects) + unames (dwc/retrieve-used-names objects) - rchanges (->> (prepare-duplicate-changes objects page-id unames selected delta) - (duplicate-changes-update-indices objects selected)) + rchanges (->> (prepare-duplicate-changes objects page-id unames selected delta) + (duplicate-changes-update-indices objects selected)) - uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %)) - (reverse rchanges)) + uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %)) + (reverse rchanges)) - id-original (when (= (count selected) 1) (first selected)) + id-original (when (= (count selected) 1) (first selected)) - selected (->> rchanges - (filter #(selected (:old-id %))) - (map #(get-in % [:obj :id])) - (into (d/ordered-set))) + selected (->> rchanges + (filter #(selected (:old-id %))) + (map #(get-in % [:obj :id])) + (into (d/ordered-set))) - id-duplicated (when (= (count selected) 1) (first selected))] + id-duplicated (when (= (count selected) 1) (first selected))] - (rx/of (dch/commit-changes {:redo-changes rchanges - :undo-changes uchanges - :origin it}) - (select-shapes selected) - (memorize-duplicated id-original id-duplicated)))))) + (rx/of (dch/commit-changes {:redo-changes rchanges + :undo-changes uchanges + :origin it}) + (select-shapes selected) + (memorize-duplicated id-original id-duplicated))))))) (defn change-hover-state [id value] diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index d589df2e8..87d4735a7 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -119,7 +119,7 @@ :duplicate {:tooltip (ds/meta "D") :command (ds/c-mod "d") - :fn #(st/emit! dw/duplicate-selected)} + :fn #(st/emit! (dw/duplicate-selected true))} :undo {:tooltip (ds/meta "Z") :command (ds/c-mod "z") @@ -260,6 +260,23 @@ :command ["alt" "."] :type "keyup" :fn #(st/emit! (dw/toggle-distances-display false))} + + :boolean-union {:tooltip (ds/alt "U") + :command "alt+u" + :fn #(st/emit! (dw/create-bool :union))} + + :boolean-difference {:tooltip (ds/alt "D") + :command "alt+d" + :fn #(st/emit! (dw/create-bool :difference))} + + :boolean-intersection {:tooltip (ds/alt "I") + :command "alt+i" + :fn #(st/emit! (dw/create-bool :intersection))} + + :boolean-exclude {:tooltip (ds/alt "E") + :command "alt+e" + :fn #(st/emit! (dw/create-bool :exclude))} + }) (defn get-tooltip [shortcut] diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 3193b549d..2b253738a 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -178,7 +178,8 @@ :y (+ y offset-y)} (gsh/setup-selrect) (assoc :svg-attrs (-> (:attrs svg-data) - (dissoc :viewBox :xmlns)))))) + (dissoc :viewBox :xmlns) + (d/without-keys usvg/inheritable-props)))))) (defn create-group [name frame-id svg-data {:keys [attrs]}] (let [svg-transform (usvg/parse-transform (:transform attrs)) @@ -368,16 +369,16 @@ ;; SVG graphic elements ;; :circle :ellipse :image :line :path :polygon :polyline :rect :text :use (let [shape (-> (case tag - (:g :a :svg) (create-group name frame-id svg-data element-data) - :rect (create-rect-shape name frame-id svg-data element-data) + (:g :a :svg) (create-group name frame-id svg-data element-data) + :rect (create-rect-shape name frame-id svg-data element-data) (:circle - :ellipse) (create-circle-shape name frame-id svg-data element-data) - :path (create-path-shape name frame-id svg-data element-data) - :polyline (create-path-shape name frame-id svg-data (-> element-data usvg/polyline->path)) - :polygon (create-path-shape name frame-id svg-data (-> element-data usvg/polygon->path)) - :line (create-path-shape name frame-id svg-data (-> element-data usvg/line->path)) - :image (create-image-shape name frame-id svg-data element-data) - #_other (create-raw-svg name frame-id svg-data element-data)) + :ellipse) (create-circle-shape name frame-id svg-data element-data) + :path (create-path-shape name frame-id svg-data element-data) + :polyline (create-path-shape name frame-id svg-data (-> element-data usvg/polyline->path)) + :polygon (create-path-shape name frame-id svg-data (-> element-data usvg/polygon->path)) + :line (create-path-shape name frame-id svg-data (-> element-data usvg/line->path)) + :image (create-image-shape name frame-id svg-data element-data) + #_other (create-raw-svg name frame-id svg-data element-data)) ) shape (when (some? shape) @@ -387,7 +388,7 @@ (setup-stroke))) children (cond->> (:content element-data) - (= tag :g) + (or (= tag :g) (= tag :svg)) (mapv #(usvg/inherit-attributes attrs %)))] [shape children])))) @@ -487,11 +488,15 @@ ;; Creates the root shape changes (dwc/add-shape-changes page-id objects selected root-shape false) + root-attrs (-> (:attrs svg-data) + (usvg/format-styles)) + ;; Reduces the children to create the changes to add the children shapes [_ [rchanges uchanges]] (reduce (partial add-svg-child-changes page-id objects selected frame-id root-id svg-data) [unames changes] - (d/enumerate (:content svg-data))) + (d/enumerate (->> (:content svg-data) + (mapv #(usvg/inherit-attributes root-attrs %))))) reg-objects-action {:type :reg-objects :page-id page-id diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 841bf0181..53e123c16 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -180,12 +180,10 @@ shape (get objects id) merge-fn (fn [node attrs] - (reduce-kv (fn [node k v] - (if (= (get node k) v) - (dissoc node k) - (assoc node k v))) - node - attrs)) + (reduce-kv + (fn [node k v] (assoc node k v)) + node + attrs)) update-fn #(update-shape % txt/is-paragraph-node? merge-fn attrs) shape-ids (cond (= (:type shape) :text) [id] diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 9426033c8..d7574e330 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -70,23 +70,19 @@ (defn- fix-init-point "Fix the initial point so the resizes are accurate" [initial handler shape] - (let [{:keys [x y width height]} (:selrect shape) - {:keys [rotation]} shape - rotation (or rotation 0)] - (if (= rotation 0) - (cond-> initial - (contains? #{:left :top-left :bottom-left} handler) - (assoc :x x) + (let [{:keys [x y width height]} (:selrect shape)] + (cond-> initial + (contains? #{:left :top-left :bottom-left} handler) + (assoc :x x) - (contains? #{:right :top-right :bottom-right} handler) - (assoc :x (+ x width)) + (contains? #{:right :top-right :bottom-right} handler) + (assoc :x (+ x width)) - (contains? #{:top :top-right :top-left} handler) - (assoc :y y) + (contains? #{:top :top-right :top-left} handler) + (assoc :y y) - (contains? #{:bottom :bottom-right :bottom-left} handler) - (assoc :y (+ y height))) - initial))) + (contains? #{:bottom :bottom-right :bottom-left} handler) + (assoc :y (+ y height))))) (defn finish-transform [] (ptk/reify ::finish-transform @@ -117,8 +113,9 @@ (declare clear-local-transform) (defn- set-modifiers - ([ids] (set-modifiers ids nil)) - ([ids modifiers] + ([ids] (set-modifiers ids nil false)) + ([ids modifiers] (set-modifiers ids modifiers false)) + ([ids modifiers ignore-constraints] (us/verify (s/coll-of uuid?) ids) (ptk/reify ::set-modifiers ptk/UpdateEvent @@ -136,7 +133,8 @@ (get objects id) modifiers nil - nil))) + nil + ignore-constraints))) state ids)))))) @@ -201,7 +199,7 @@ (dwu/commit-undo-transaction)))))) (defn- set-modifiers-recursive - [modif-tree objects shape modifiers root transformed-root] + [modif-tree objects shape modifiers root transformed-root ignore-constraints] (let [children (->> (get shape :shapes []) (map #(get objects %))) @@ -215,13 +213,15 @@ set-child (fn [modif-tree child] (let [child-modifiers (gsh/calc-child-modifiers shape child - modifiers)] + modifiers + ignore-constraints)] (set-modifiers-recursive modif-tree objects child child-modifiers root - transformed-root)))] + transformed-root + ignore-constraints)))] (reduce set-child (assoc-in modif-tree [(:id shape) :modifiers] modifiers) children))) @@ -285,10 +285,19 @@ (letfn [(resize [shape initial layout [point lock? center? point-snap]] (let [{:keys [width height]} (:selrect shape) {:keys [rotation]} shape + + shape-center (gsh/center-shape shape) + shape-transform (:transform shape (gmt/matrix)) + shape-transform-inverse (:transform-inverse shape (gmt/matrix)) + rotation (or rotation 0) + initial (gsh/transform-point-center initial shape-center shape-transform-inverse) initial (fix-init-point initial handler shape) + point (gsh/transform-point-center (if (= rotation 0) point-snap point) + shape-center shape-transform-inverse) + shapev (-> (gpt/point width height)) scale-text (:scale-text layout) @@ -300,8 +309,7 @@ handler-mult (let [[x y] (handler-multipliers handler)] (gpt/point x y)) ;; Difference between the origin point in the coordinate system of the rotation - deltav (-> (gpt/to-vec initial (if (= rotation 0) point-snap point)) - (gpt/transform (gmt/rotate-matrix (- rotation))) + deltav (-> (gpt/to-vec initial point) (gpt/multiply handler-mult)) ;; Resize vector @@ -317,26 +325,25 @@ scalev) ;; Resize origin point given the selected handler - origin (handler-resize-origin (:selrect shape) handler) + handler-origin (handler-resize-origin (:selrect shape) handler) - shape-center (gsh/center-shape shape) - shape-transform (:transform shape (gmt/matrix)) - shape-transform-inverse (:transform-inverse shape (gmt/matrix)) ;; If we want resize from center, displace the shape ;; so it is still centered after resize. - displacement (when center? - (-> shape-center - (gpt/subtract origin) - (gpt/multiply scalev) - (gpt/add origin) - (gpt/subtract shape-center) - (gpt/multiply (gpt/point -1 -1)) - (gpt/transform shape-transform))) + displacement + (when center? + (-> shape-center + (gpt/subtract handler-origin) + (gpt/multiply scalev) + (gpt/add handler-origin) + (gpt/subtract shape-center) + (gpt/multiply (gpt/point -1 -1)) + (gpt/transform shape-transform))) - origin (cond-> (gsh/transform-point-center origin shape-center shape-transform) - (some? displacement) - (gpt/add displacement)) + resize-origin + (cond-> (gsh/transform-point-center handler-origin shape-center shape-transform) + (some? displacement) + (gpt/add displacement)) displacement (when (some? displacement) (gmt/translate-matrix displacement))] @@ -344,7 +351,7 @@ (rx/of (set-modifiers ids {:displacement displacement :resize-vector scalev - :resize-origin origin + :resize-origin resize-origin :resize-transform shape-transform :resize-scale-text scale-text :resize-transform-inverse shape-transform-inverse})))) @@ -407,7 +414,8 @@ shape modifiers nil - nil)))) + nil + false)))) state ids))) @@ -505,7 +513,7 @@ (if alt? ;; When alt is down we start a duplicate+move (rx/of (start-move-duplicate initial) - dws/duplicate-selected) + (dws/duplicate-selected false)) ;; Otherwise just plain old move (rx/of (start-move initial selected))))))))))) @@ -712,7 +720,8 @@ (rx/of (set-modifiers selected {:resize-vector (gpt/point -1.0 1.0) :resize-origin origin - :displacement (gmt/translate-matrix (gpt/point (- (:width selrect)) 0))}) + :displacement (gmt/translate-matrix (gpt/point (- (:width selrect)) 0))} + true) (apply-modifiers selected)))))) (defn flip-vertical-selected [] @@ -728,7 +737,8 @@ (rx/of (set-modifiers selected {:resize-vector (gpt/point 1.0 -1.0) :resize-origin origin - :displacement (gmt/translate-matrix (gpt/point 0 (- (:height selrect))))}) + :displacement (gmt/translate-matrix (gpt/point 0 (- (:height selrect))))} + true) (apply-modifiers selected)))))) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs new file mode 100644 index 000000000..39350c180 --- /dev/null +++ b/frontend/src/app/main/errors.cljs @@ -0,0 +1,172 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.errors + "Generic error handling" + (:require + [app.common.exceptions :as ex] + [app.config :as cf] + [app.main.data.messages :as dm] + [app.main.data.users :as du] + [app.main.sentry :as sentry] + [app.main.store :as st] + [app.util.router :as rt] + [app.util.timers :as ts] + [cljs.pprint :refer [pprint]] + [cuerdas.core :as str] + [expound.alpha :as expound] + [potok.core :as ptk])) + +(defn on-error + "A general purpose error handler." + [error] + (cond + (instance? ExceptionInfo error) + (-> error sentry/capture-exception ex-data ptk/handle-error) + + (map? error) + (ptk/handle-error error) + + :else + (let [hint (ex-message error) + msg (str "Internal Error: " hint)] + (sentry/capture-exception error) + (ts/schedule (st/emitf (rt/assign-exception error))) + + (js/console.group msg) + (ex/ignoring (js/console.error error)) + (js/console.groupEnd msg)))) + +;; Set the main potok error handler +(reset! st/on-error on-error) + +;; We receive a explicit authentication error; this explicitly clears +;; all profile data and redirect the user to the login page. This is +;; here and not in app.main.errors because of circular dependency. +(defmethod ptk/handle-error :authentication + [_] + (ts/schedule (st/emitf (du/logout)))) + + +;; That are special case server-errors that should be treated +;; differently. +(derive :not-found ::exceptional-state) +(derive :bad-gateway ::exceptional-state) +(derive :service-unavailable ::exceptional-state) + +(defmethod ptk/handle-error ::exceptional-state + [error] + (ts/schedule + (st/emitf (rt/assign-exception error)))) + +;; Error that happens on an active bussines model validation does not +;; passes an validation (example: profile can't leave a team). From +;; the user perspective a error flash message should be visualized but +;; user can continue operate on the application. +(defmethod ptk/handle-error :validation + [error] + (ts/schedule + (st/emitf + (dm/show {:content "Unexpected validation error." + :type :error + :timeout 3000}))) + + ;; Print to the console some debug info. + (js/console.group "Validation Error:") + (ex/ignoring + (js/console.info + (with-out-str + (pprint (dissoc error :explain)))) + (when-let [explain (:explain error)] + (js/console.error explain))) + (js/console.groupEnd "Validation Error:")) + + +;; Error on parsing an SVG +(defmethod ptk/handle-error :svg-parser + [_] + (ts/schedule + (st/emitf + (dm/show {:content "SVG is invalid or malformed" + :type :error + :timeout 3000})))) + +(defmethod ptk/handle-error :comment-error + [_] + (ts/schedule + (st/emitf + (dm/show {:content "There was an error with the comment" + :type :error + :timeout 3000})))) + +;; This is a pure frontend error that can be caused by an active +;; assertion (assertion that is preserved on production builds). From +;; the user perspective this should be treated as internal error. +(defmethod ptk/handle-error :assertion + [{:keys [data stack message hint context] :as error}] + (let [message (or message hint) + message (str "Internal Assertion Error: " message) + context (str/fmt "ns: '%s'\nname: '%s'\nfile: '%s:%s'" + (:ns context) + (:name context) + (str cf/public-uri "js/cljs-runtime/" (:file context)) + (:line context))] + (ts/schedule + (st/emitf + (dm/show {:content "Internal error: assertion." + :type :error + :timeout 3000}))) + + ;; Print to the console some debugging info + (js/console.group message) + (js/console.info context) + (js/console.groupCollapsed "Stack Trace") + (js/console.info stack) + (js/console.groupEnd "Stack Trace") + (js/console.error (with-out-str (expound/printer data))) + (js/console.groupEnd message))) + +;; This happens when the backed server fails to process the +;; request. This can be caused by an internal assertion or any other +;; uncontrolled error. +(defmethod ptk/handle-error :server-error + [{:keys [data hint] :as error}] + (let [hint (or hint (:hint data) (:message data)) + info (with-out-str (pprint (dissoc data :explain))) + expl (:explain data) + msg (str "Internal Server Error: " hint)] + + (ts/schedule + (st/emitf + (dm/show {:content "Something wrong has happened (on backend)." + :type :error + :timeout 3000}))) + + (js/console.group msg) + (js/console.info info) + (when expl (js/console.error expl)) + (js/console.groupEnd msg))) + +(defn on-unhandled-error + [error] + (if (instance? ExceptionInfo error) + (-> error sentry/capture-exception ex-data ptk/handle-error) + (let [hint (ex-message error) + msg (str "Unhandled Internal Error: " hint)] + (sentry/capture-exception error) + (ts/schedule (st/emitf (rt/assign-exception error))) + (js/console.group msg) + (ex/ignoring (js/console.error error)) + (js/console.groupEnd msg)))) + +(defonce uncaught-error-handler + (letfn [(on-error [event] + (.preventDefault ^js event) + (some-> (unchecked-get event "error") + (on-unhandled-error)))] + (.addEventListener js/window "error" on-error) + (fn [] + (.removeEventListener js/window "error" on-error)))) diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index 5c0217152..726562f4c 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -14,6 +14,7 @@ [app.common.math :as mth] [app.common.pages :as cp] [app.common.uuid :as uuid] + [app.main.ui.shapes.bool :as bool] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.export :as use] @@ -81,6 +82,18 @@ :is-child-selected? true :childs childs}])))) +(defn bool-wrapper-factory + [objects] + (let [shape-wrapper (shape-wrapper-factory objects) + bool-shape (bool/bool-shape shape-wrapper)] + (mf/fnc bool-wrapper + [{:keys [shape frame] :as props}] + (let [childs (->> (cp/get-children (:id shape) objects) + (select-keys objects))] + [:& bool-shape {:frame frame + :shape shape + :childs childs}])))) + (defn svg-raw-wrapper-factory [objects] (let [shape-wrapper (shape-wrapper-factory objects) @@ -104,9 +117,10 @@ [objects] (mf/fnc shape-wrapper [{:keys [frame shape] :as props}] - (let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper-factory objects)) + (let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper-factory objects)) svg-raw-wrapper (mf/use-memo (mf/deps objects) #(svg-raw-wrapper-factory objects)) - frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))] + bool-wrapper (mf/use-memo (mf/deps objects) #(bool-wrapper-factory objects)) + frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))] (when (and shape (not (:hidden shape))) (let [shape (-> (gsh/transform-shape shape) (gsh/translate-to-frame frame)) @@ -122,6 +136,7 @@ :circle [:> circle/circle-shape opts] :frame [:> frame-wrapper {:shape shape}] :group [:> group-wrapper {:shape shape :frame frame}] + :bool [:> bool-wrapper {:shape shape :frame frame}] nil)] ;; Don't wrap svg elements inside a otherwise some can break diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index dbd50cde3..94187a561 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -9,11 +9,11 @@ (:require-macros [app.main.fonts :refer [preload-gfonts]]) (:require [app.common.data :as d] + [app.common.logging :as log] [app.common.text :as txt] [app.config :as cf] [app.util.dom :as dom] [app.util.http :as http] - [app.util.logging :as log] [app.util.object :as obj] [beicon.core :as rx] [clojure.set :as set] @@ -23,7 +23,7 @@ [okulary.core :as l] [promesa.core :as p])) -(log/set-level! :trace) +(log/set-level! :warn) (def google-fonts (preload-gfonts "fonts/gfonts.2020.04.23.json")) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 4dcbac803..3612d8490 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -1,4 +1,3 @@ - ;; 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/. @@ -11,6 +10,7 @@ [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.pages :as cp] + [app.common.path.commands :as upc] [app.main.data.workspace.state-helpers :as wsh] [app.main.store :as st] [okulary.core :as l])) @@ -122,6 +122,10 @@ :show-distances?]) workspace-local =)) +(def local-displacement + (l/derived #(select-keys % [:modifiers :selected]) + workspace-local =)) + (def selected-zoom (l/derived :zoom workspace-local)) @@ -239,16 +243,44 @@ ([ids {:keys [with-modifiers?] :or { with-modifiers? false }}] - (l/derived (fn [state] - (let [objects (wsh/lookup-page-objects state) - modifiers (:workspace-modifiers state) - objects (cond-> objects - with-modifiers? - (gsh/merge-modifiers modifiers)) - xform (comp (map #(get objects %)) - (remove nil?))] - (into [] xform ids))) - st/state =))) + (let [selector + (fn [state] + (let [objects (wsh/lookup-page-objects state) + modifiers (:workspace-modifiers state) + objects (cond-> objects + with-modifiers? + (gsh/merge-modifiers modifiers)) + xform (comp (map #(get objects %)) + (remove nil?))] + (into [] xform ids)))] + (l/derived selector st/state =)))) + +(defn- set-content-modifiers [state] + (fn [id shape] + (let [content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers])] + (if (some? content-modifiers) + (update shape :content upc/apply-content-modifiers content-modifiers) + shape)))) + +(defn select-children [id] + (let [selector + (fn [state] + (let [objects (wsh/lookup-page-objects state) + + modifiers (-> (:workspace-modifiers state)) + {selected :selected disp-modifiers :modifiers} + (-> (:workspace-local state) + (select-keys [:modifiers :selected])) + + modifiers + (d/deep-merge + modifiers + (into {} (map #(vector % {:modifiers disp-modifiers})) selected))] + + (as-> (cp/select-children id objects) $ + (gsh/merge-modifiers $ modifiers) + (d/mapm (set-content-modifiers state) $))))] + (l/derived selector st/state =))) (def selected-data (l/derived #(let [selected (wsh/lookup-selected %) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 7ec498c40..9fa42387c 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -48,6 +48,7 @@ [id params] (->> (http/send! {:method :get :uri (u/join base-uri "api/rpc/query/" (name id)) + :credentials "include" :query params}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response))) @@ -58,6 +59,7 @@ [id params] (->> (http/send! {:method :post :uri (u/join base-uri "api/rpc/mutation/" (name id)) + :credentials "include" :body (http/transit-data params)}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response))) @@ -87,7 +89,10 @@ [_ {:keys [provider] :as params}] (let [uri (u/join base-uri "api/auth/oauth/" (d/name provider)) params (dissoc params :provider)] - (->> (http/send! {:method :post :uri uri :query params}) + (->> (http/send! {:method :post + :uri uri + :credentials "include" + :query params}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response)))) @@ -95,6 +100,7 @@ [_ params] (->> (http/send! {:method :post :uri (u/join base-uri "api/feedback") + :credentials "include" :body (http/transit-data params)}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response))) @@ -104,6 +110,7 @@ (->> (http/send! {:method :post :uri (u/join base-uri "export") :body (http/transit-data params) + :credentials "include" :response-type :blob}) (rx/mapcat handle-response))) @@ -112,6 +119,7 @@ (->> (http/send! {:method :post :uri (u/join base-uri "export-frames") :body (http/transit-data params) + :credentials "include" :response-type :blob}) (rx/mapcat handle-response))) @@ -123,6 +131,7 @@ [id params] (->> (http/send! {:method :post :uri (u/join base-uri "api/rpc/mutation/" (name id)) + :credentials "include" :body (http/form-data params)}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response))) diff --git a/frontend/src/app/main/sentry.cljs b/frontend/src/app/main/sentry.cljs new file mode 100644 index 000000000..d7cdcfd9a --- /dev/null +++ b/frontend/src/app/main/sentry.cljs @@ -0,0 +1,60 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.sentry + "Sentry integration." + (:require + ["@sentry/browser" :as sentry] + [app.common.exceptions :as ex] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.main.refs :as refs])) + +(defn- setup-profile! + [profile] + (if (or (= uuid/zero (:id profile)) + (nil? profile)) + (sentry/setUser nil) + (sentry/setUser #js {:id (str (:id profile))}))) + +(defn init! + [] + (setup-profile! @refs/profile) + (when cf/sentry-dsn + (sentry/init + #js {:dsn cf/sentry-dsn + :autoSessionTracking false + :attachStacktrace false + :release (str "frontend@" (:base @cf/version)) + :maxBreadcrumbs 20 + :beforeBreadcrumb (fn [breadcrumb _hint] + (let [category (.-category ^js breadcrumb)] + (if (= category "navigate") + breadcrumb + nil))) + :tracesSampleRate 1.0}) + + (add-watch refs/profile ::profile + (fn [_ _ _ profile] + (setup-profile! profile))) + + (add-watch refs/route ::route + (fn [_ _ _ route] + (sentry/addBreadcrumb + #js {:category "navigate", + :message (str "path: " (:path route)) + :level (.-Info ^js sentry/Severity)}))))) + +(defn capture-exception + [err] + (when cf/sentry-dsn + (when (ex/ex-info? err) + (sentry/setContext "ex-data", (clj->js (ex-data err)))) + (sentry/captureException err)) + err) + + + diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index 4fd743ecd..78a93c1a8 100644 --- a/frontend/src/app/main/store.cljs +++ b/frontend/src/app/main/store.cljs @@ -17,11 +17,15 @@ (enable-console-print!) -(def ^:dynamic *on-error* identity) - (defonce loader (l/atom false)) -(defonce state (ptk/store {:resolve ptk/resolve})) -(defonce stream (ptk/input-stream state)) +(defonce on-error (l/atom identity)) + +(defonce state + (ptk/store {:resolve ptk/resolve + :on-error (fn [e] (@on-error e))})) + +(defonce stream + (ptk/input-stream state)) (defonce last-events (let [buffer (atom #queue []) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index bcec7a1b7..b005dc11c 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -6,12 +6,6 @@ (ns app.main.ui (:require - [app.common.exceptions :as ex] - [app.common.spec :as us] - [app.config :as cf] - [app.main.data.events :as ev] - [app.main.data.messages :as dm] - [app.main.data.users :as du] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.auth :refer [auth]] @@ -28,77 +22,12 @@ [app.main.ui.static :as static] [app.main.ui.viewer :as viewer] [app.main.ui.workspace :as workspace] - [app.util.timers :as ts] - [cljs.pprint :refer [pprint]] - [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [expound.alpha :as expound] - [potok.core :as ptk] + [app.util.router :as rt] [rumext.alpha :as mf])) -;; --- Routes - -(s/def ::page-id ::us/uuid) -(s/def ::file-id ::us/uuid) -(s/def ::section ::us/keyword) -(s/def ::index ::us/integer) -(s/def ::token (s/nilable ::us/not-empty-string)) -(s/def ::share-id ::us/uuid) - -(s/def ::viewer-path-params - (s/keys :req-un [::file-id])) - -(s/def ::viewer-query-params - (s/keys :req-un [::index] - :opt-un [::share-id ::section ::page-id])) - -(def routes - [["/auth" - ["/login" :auth-login] - (when (contains? @cf/flags :registration) - ["/register" :auth-register]) - (when (contains? @cf/flags :registration) - ["/register/validate" :auth-register-validate]) - (when (contains? @cf/flags :registration) - ["/register/success" :auth-register-success]) - ["/recovery/request" :auth-recovery-request] - ["/recovery" :auth-recovery] - ["/verify-token" :auth-verify-token]] - - ["/settings" - ["/profile" :settings-profile] - ["/password" :settings-password] - ["/feedback" :settings-feedback] - ["/options" :settings-options]] - - ["/view/:file-id" - {:name :viewer - :conform - {:path-params ::viewer-path-params - :query-params ::viewer-query-params}}] - - (when *assert* - ["/debug/icons-preview" :debug-icons-preview]) - - ;; Used for export - ["/render-object/:file-id/:page-id/:object-id" :render-object] - ["/render-sprite/:file-id" :render-sprite] - - ["/dashboard/team/:team-id" - ["/members" :dashboard-team-members] - ["/settings" :dashboard-team-settings] - ["/projects" :dashboard-projects] - ["/search" :dashboard-search] - ["/fonts" :dashboard-fonts] - ["/fonts/providers" :dashboard-font-providers] - ["/libraries" :dashboard-libraries] - ["/projects/:project-id" :dashboard-files]] - - ["/workspace/:project-id/:file-id" :workspace]]) - (mf/defc on-main-error [{:keys [error] :as props}] - (mf/use-effect #(ptk/handle-error error)) + (mf/use-effect (st/emitf (rt/assign-exception error))) [:span "Internal application errror"]) (mf/defc main-page @@ -161,12 +90,14 @@ :render-object (do - (let [file-id (uuid (get-in route [:path-params :file-id])) - page-id (uuid (get-in route [:path-params :page-id])) - object-id (uuid (get-in route [:path-params :object-id]))] + (let [file-id (uuid (get-in route [:path-params :file-id])) + page-id (uuid (get-in route [:path-params :page-id])) + object-id (uuid (get-in route [:path-params :object-id])) + render-texts (get-in route [:query-params :render-texts])] [:& render/render-object {:file-id file-id :page-id page-id - :object-id object-id}])) + :object-id object-id + :render-texts? (and (some? render-texts) (= render-texts "true"))}])) :render-sprite (do @@ -199,143 +130,3 @@ [:& msgs/notifications] (when route [:& main-page {:route route}])])])) - -;; --- Error Handling - -;; That are special case server-errors that should be treated -;; differently. -(derive :not-found ::exceptional-state) -(derive :bad-gateway ::exceptional-state) -(derive :service-unavailable ::exceptional-state) - -(defmethod ptk/handle-error ::exceptional-state - [error] - (ts/schedule - (st/emitf (dm/assign-exception error)))) - -;; We receive a explicit authentication error; this explicitly clears -;; all profile data and redirect the user to the login page. -(defmethod ptk/handle-error :authentication - [_] - (ts/schedule (st/emitf (du/logout)))) - -;; Error that happens on an active bussines model validation does not -;; passes an validation (example: profile can't leave a team). From -;; the user perspective a error flash message should be visualized but -;; user can continue operate on the application. -(defmethod ptk/handle-error :validation - [error] - (ts/schedule - (st/emitf - (dm/show {:content "Unexpected validation error." - :type :error - :timeout 3000}))) - - ;; Print to the console some debug info. - (js/console.group "Validation Error") - (ex/ignoring - (js/console.info - (with-out-str - (pprint (dissoc error :explain)))) - (when-let [explain (:explain error)] - (js/console.error explain))) - (js/console.groupEnd "Validation Error")) - -;; Error on parsing an SVG -(defmethod ptk/handle-error :svg-parser - [_] - (ts/schedule - (st/emitf - (dm/show {:content "SVG is invalid or malformed" - :type :error - :timeout 3000})))) - -;; This is a pure frontend error that can be caused by an active -;; assertion (assertion that is preserved on production builds). From -;; the user perspective this should be treated as internal error. -(defmethod ptk/handle-error :assertion - [{:keys [data stack message hint context] :as error}] - (let [message (or message hint) - context (str/fmt "ns: '%s'\nname: '%s'\nfile: '%s:%s'" - (:ns context) - (:name context) - (str cf/public-uri "js/cljs-runtime/" (:file context)) - (:line context))] - (ts/schedule - (st/emitf - (dm/show {:content "Internal error: assertion." - :type :error - :timeout 3000}) - (ptk/event ::ev/event - {::ev/type "exception" - ::ev/name "assertion-error" - :message message - :context context - :trace stack}))) - - ;; Print to the console some debugging info - (js/console.group message) - (js/console.info context) - (js/console.groupCollapsed "Stack Trace") - (js/console.info stack) - (js/console.groupEnd "Stack Trace") - (js/console.error (with-out-str (expound/printer data))) - (js/console.groupEnd message))) - -;; This happens when the backed server fails to process the -;; request. This can be caused by an internal assertion or any other -;; uncontrolled error. -(defmethod ptk/handle-error :server-error - [{:keys [data hint] :as error}] - (let [hint (or hint (:hint data) (:message data)) - info (with-out-str (pprint (dissoc data :explain))) - expl (:explain data)] - (ts/schedule - (st/emitf - (dm/show {:content "Something wrong has happened (on backend)." - :type :error - :timeout 3000}) - (ptk/event ::ev/event - {::ev/type "exception" - ::ev/name "server-error" - :hint hint - :info info - :explain expl}))) - - (js/console.group "Internal Server Error:") - (js/console.error "hint:" hint) - (js/console.info info) - (when expl (js/console.error expl)) - (js/console.groupEnd "Internal Server Error:"))) - -(defmethod ptk/handle-error :default - [error] - (if (instance? ExceptionInfo error) - (ptk/handle-error (ex-data error)) - (let [stack (.-stack error) - hint (or (ex-message error) - (:hint error) - (:message error))] - (ts/schedule - (st/emitf - (dm/assign-exception error) - (ptk/event ::ev/event - {::ev/type "exception" - ::ev/name "unexpected-error" - :message hint - :trace (.-stack error)}))) - - (js/console.group "Internal error:") - (js/console.log "hint:" hint) - (ex/ignoring - (js/console.error (clj->js error)) - (js/console.error "stack:" stack)) - (js/console.groupEnd "Internal error:")))) - -(defonce uncaught-error-handler - (letfn [(on-error [event] - (ptk/handle-error (unchecked-get event "error")) - (.preventDefault ^js event))] - (.addEventListener js/window "error" on-error) - (fn [] - (.removeEventListener js/window "error" on-error)))) diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index f10181e77..71cc32eeb 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -210,8 +210,13 @@ [:div.fields-row [:& fm/input {:name :accept-terms-and-privacy :class "check-primary" - :label (tr "auth.terms-privacy-agreement") - :type "checkbox"}]] + :type "checkbox"} + [:span + (tr "auth.terms-privacy-agreement") + [:div + [:a {:href "https://penpot.app/terms.html" :target "_blank"} (tr "auth.terms-of-service")] + [:span ",\u00A0"] + [:a {:href "https://penpot.app/privacy.html" :target "_blank"} (tr "auth.privacy-policy")]]]]] ;; (when (contains? @cf/flags :newsletter-registration-check) ;; [:div.fields-row diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index 3ae683238..f65abba07 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -302,21 +302,22 @@ (when-let [node (mf/ref-val ref)] (.scrollIntoViewIfNeeded ^js node)))) - [:div.thread-content - {:style {:top (str pos-y "px") - :left (str pos-x "px")} - :on-click dom/stop-propagation} + (when (some? comment) + [:div.thread-content + {:style {:top (str pos-y "px") + :left (str pos-x "px")} + :on-click dom/stop-propagation} - [:div.comments - [:& comment-item {:comment comment - :users users - :thread thread}] - (for [item (rest comments)] - [:* - [:hr] - [:& comment-item {:comment item :users users}]]) - [:div {:ref ref}]] - [:& reply-form {:thread thread}]])) + [:div.comments + [:& comment-item {:comment comment + :users users + :thread thread}] + (for [item (rest comments)] + [:* + [:hr] + [:& comment-item {:comment item :users users}]]) + [:div {:ref ref}]] + [:& reply-form {:thread thread}]]))) (mf/defc thread-bubble {::mf/wrap [mf/memo]} diff --git a/frontend/src/app/main/ui/components/editable_select.cljs b/frontend/src/app/main/ui/components/editable_select.cljs index d1a2e666f..49cea928f 100644 --- a/frontend/src/app/main/ui/components/editable_select.cljs +++ b/frontend/src/app/main/ui/components/editable_select.cljs @@ -76,6 +76,7 @@ handle-key-down (mf/use-callback + (mf/deps set-value) (fn [event] (when (= type "number") (let [up? (kbd/up-arrow? event) diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index 2827f5a6e..fd15aa757 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -19,7 +19,7 @@ (def use-form fm/use-form) (mf/defc input - [{:keys [label help-icon disabled form hint trim] :as props}] + [{:keys [label help-icon disabled form hint trim children] :as props}] (let [input-type (get props :type "text") input-name (get props :name) more-classes (get props :class) @@ -82,7 +82,7 @@ (swap! form assoc-in [:touched input-name] true))) props (-> props - (dissoc :help-icon :form :trim) + (dissoc :help-icon :form :trim :children) (assoc :id (name input-name) :value value :auto-focus auto-focus? @@ -97,7 +97,13 @@ {:class klass} [:* [:> :input props] - [:label {:for (name input-name)} label] + (cond + (some? label) + [:label {:for (name input-name)} label] + + (some? children) + [:label {:for (name input-name)} children]) + (when help-icon' [:div.help-icon {:style {:cursor "pointer"} diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index e0c03033b..531df875e 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -74,10 +74,12 @@ on-new-tab (fn [_] - (let [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)))) + (let [path-params {:project-id (:project-id file) + :file-id (:id file)} + query-params {:page-id (first (get-in file [:data :pages]))}] + (st/emit! (rt/nav-new-window* {:rname :workspace + :path-params path-params + :query-params query-params})))) on-duplicate (fn [_] diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs index 1b12fc298..dd35cd446 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.cljs +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -18,13 +18,10 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] - [app.util.logging :as log] [beicon.core :as rx] [cuerdas.core :as str] [rumext.alpha :as mf])) -(log/set-level! :trace) - (defn- use-set-page-title [team section] (mf/use-effect diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 1e4202632..9d8083fc9 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.dashboard.import (:require [app.common.data :as d] + [app.common.logging :as log] [app.main.data.events :as ev] [app.main.data.modal :as modal] [app.main.store :as st] @@ -16,12 +17,11 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] - [app.util.logging :as log] [beicon.core :as rx] [potok.core :as ptk] [rumext.alpha :as mf])) -(log/set-level! :debug) +(log/set-level! :warn) (def ^:const emit-delay 1000) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 3f1e7cbd9..827f48265 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -10,6 +10,7 @@ [app.common.spec :as us] [app.config :as cf] [app.main.data.dashboard :as dd] + [app.main.data.events :as ev] [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.data.users :as du] @@ -69,7 +70,8 @@ (mf/use-callback (mf/deps item) (fn [name] - (st/emit! (dd/rename-project (assoc item :name name))) + (st/emit! (-> (dd/rename-project (assoc item :name name)) + (with-meta {::ev/origin "dashboard:sidebar"}))) (swap! local assoc :edition? false))) on-drag-enter diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 4c87aa4d1..bf0a8bf5a 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -26,12 +26,13 @@ (mf/defc header {::mf/wrap [mf/memo]} - [{:keys [section] :as props}] + [{:keys [section team] :as props}] (let [go-members (st/emitf (dd/go-to-team-members)) go-settings (st/emitf (dd/go-to-team-settings)) - invite-member (st/emitf (modal/show {:type ::invite-member})) + invite-member (st/emitf (modal/show {:type ::invite-member :team team})) members-section? (= section :dashboard-team-members) - settings-section? (= section :dashboard-team-settings)] + settings-section? (= section :dashboard-team-settings) + permissions (:permissions team)] [:header.dashboard-header [:div.dashboard-title @@ -46,20 +47,21 @@ [:li {:class (when settings-section? "active")} [:a {:on-click go-settings} (tr "labels.settings")]]]] - (if members-section? + (if (and members-section? (:is-admin permissions)) [:a.btn-secondary.btn-small {:on-click invite-member} (tr "dashboard.invite-profile")] [:div])])) (defn get-available-roles - [] - [{:value "" :label (tr "labels.role")} - {:value "admin" :label (tr "labels.admin")} - {:value "editor" :label (tr "labels.editor")} - ;; Temporarily disabled viewer role - ;; https://tree.taiga.io/project/uxboxproject/issue/1083 - ;; {:value "viewer" :label (tr "labels.viewer")} - ]) + [permissions] + (->> [{:value "editor" :label (tr "labels.editor")} + (when (:is-admin permissions) + {:value "admin" :label (tr "labels.admin")}) + ;; Temporarily disabled viewer role + ;; https://tree.taiga.io/project/uxboxproject/issue/1083 + ;; {:value "viewer" :label (tr "labels.viewer")} + ] + (filterv identity))) (s/def ::email ::us/email) (s/def ::role ::us/keyword) @@ -69,8 +71,9 @@ (mf/defc invite-member-modal {::mf/register modal/components ::mf/register-as ::invite-member} - [] - (let [roles (mf/use-memo get-available-roles) + [{:keys [team]}] + (let [perms (:permissions team) + roles (mf/use-memo (mf/deps perms) #(get-available-roles perms)) initial (mf/use-memo (constantly {:role "editor"})) form (fm/use-form :spec ::invite-member-form :initial initial) @@ -262,6 +265,7 @@ (tr "dashboard.your-penpot") (:name team)))))) + (mf/use-effect (st/emitf (dd/fetch-team-members) (dd/fetch-team-stats))) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index 257b3b239..484974c1d 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -11,13 +11,10 @@ [app.main.store :as st] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] - [app.util.logging :as log] [app.util.timers :as ts] [beicon.core :as rx] [rumext.alpha :as mf])) -(log/set-level! :warn) - (defn use-rxsub [ob] (let [[state reset-state!] (mf/useState @ob)] @@ -101,7 +98,6 @@ subscribe-to-drag-end (fn [] (when (nil? (:subscr @state)) - ;; (js/console.log "subscribing" (:name data)) (swap! state #(assoc % :subscr (rx/sub! global-drag-end cleanup))))) diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index 47738e45a..fce2cdc65 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -9,6 +9,8 @@ (:require-macros [app.main.ui.icons :refer [icon-xref]]) (:require [rumext.alpha :as mf])) +;; Keep the list of icons sorted + (def action (icon-xref :action)) (def actions (icon-xref :actions)) (def align-bottom (icon-xref :align-bottom)) @@ -23,6 +25,11 @@ (def auto-fix (icon-xref :auto-fix)) (def auto-height (icon-xref :auto-height)) (def auto-width (icon-xref :auto-width)) +(def boolean-difference (icon-xref :boolean-difference)) +(def boolean-exclude (icon-xref :boolean-exclude)) +(def boolean-flatten (icon-xref :boolean-flatten)) +(def boolean-intersection (icon-xref :boolean-intersection)) +(def boolean-union (icon-xref :boolean-union)) (def box (icon-xref :box)) (def chain (icon-xref :chain)) (def chat (icon-xref :chat)) @@ -67,8 +74,8 @@ (def listing-thumbs (icon-xref :listing-thumbs)) (def loader (icon-xref :loader)) (def lock (icon-xref :lock)) -(def logo (icon-xref :uxbox-logo)) -(def logo-icon (icon-xref :uxbox-logo-icon)) +(def logo (icon-xref :penpot-logo)) +(def logo-icon (icon-xref :penpot-logo-icon)) (def logout (icon-xref :logout)) (def lowercase (icon-xref :lowercase)) (def mail (icon-xref :mail)) @@ -101,6 +108,13 @@ (def play (icon-xref :play)) (def plus (icon-xref :plus)) (def pointer-inner (icon-xref :pointer-inner)) +(def position-bottom-center (icon-xref :position-bottom-center)) +(def position-bottom-left (icon-xref :position-bottom-left)) +(def position-bottom-right (icon-xref :position-bottom-right)) +(def position-center (icon-xref :position-center)) +(def position-top-center (icon-xref :position-top-center)) +(def position-top-left (icon-xref :position-top-left)) +(def position-top-right (icon-xref :position-top-right)) (def radius (icon-xref :radius)) (def radius-1 (icon-xref :radius-1)) (def radius-4 (icon-xref :radius-4)) @@ -145,6 +159,7 @@ (def uppercase (icon-xref :uppercase)) (def user (icon-xref :user)) + (def loader-pencil (mf/html [:svg diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs index d25c4b0bf..c980f45c0 100644 --- a/frontend/src/app/main/ui/onboarding.cljs +++ b/frontend/src/app/main/ui/onboarding.cljs @@ -20,6 +20,7 @@ [app.main.ui.releases.v1-6] [app.main.ui.releases.v1-7] [app.main.ui.releases.v1-8] + [app.main.ui.releases.v1-9] [app.util.i18n :as i18n :refer [tr]] [app.util.object :as obj] [app.util.router :as rt] @@ -298,5 +299,5 @@ (defmethod rc/render-release-notes "0.0" [params] - (rc/render-release-notes (assoc params :version "1.8"))) + (rc/render-release-notes (assoc params :version "1.9"))) diff --git a/frontend/src/app/main/ui/releases/v1_9.cljs b/frontend/src/app/main/ui/releases/v1_9.cljs new file mode 100644 index 000000000..7bf95fadb --- /dev/null +++ b/frontend/src/app/main/ui/releases/v1_9.cljs @@ -0,0 +1,108 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.releases.v1-9 + (:require + [app.main.ui.releases.common :as c] + [rumext.alpha :as mf])) + +(defmethod c/render-release-notes "1.9" + [{:keys [slide klass next finish navigate version]}] + (mf/html + (case @slide + :start + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Alpha release 1.9"}]] + [:div.modal-right + [:div.modal-title + [:h2 "What's new?"]] + [:span.release "Alpha version " version] + [:div.modal-content + [:p "Penpot continues growing with new features that improve performance, user experience and visual design."] + [:p "We are happy to show you a sneak peak of the most important stuff that the Alpha 1.9 version brings."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"]]] + [:img.deco {:src "images/deco-left.png" :border "0"}] + [:img.deco.right {:src "images/deco-right.png" :border "0"}]]]] + + 0 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/advanced-proto.gif" :border "0" :alt "Advanced interactions"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Prototyping triggers and actions"]] + [:div.modal-content + [:p "Prototyping options at last! Different triggers (like mouse events or time delays) and actions allow you to add complexity to the interactions of your prototypes."] + [:p "Create overlays, back buttons or links to URLs to mimic the behavior of the product you’re designing."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"] + [:& c/navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]] + + 1 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/flows-proto.gif" :border "0" :alt "Multiple flows"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Multiple flows"]] + [:div.modal-content + [:p "Design projects usually need to define multiple casuistics for different devices and user journeys."] + [:p "Flows allow you to define multiple starting points within the same page so you can better organize and present your prototypes."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"] + [:& c/navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]] + + 2 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/booleans.gif" :border "0" :alt "Boolean shapes"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Boolean operations"]] + [:div.modal-content + [:p "Now in Penpot you can combine shapes in different ways. There are five options: Union, difference, intersection, exclusion and flatten."] + [:p "Using boolean operations will lead to countless graphic possibilities for your designs."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"] + [:& c/navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]] + + 3 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/libraries-feature.gif" :border "0" :alt "Libraries & templates"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Libraries & templates"]] + [:div.modal-content + [:p "We’ve created a new space on Penpot where you can share your libraries and templates and download the ones you like. Material Design, Cocomaterial or Penpot’s Design System are among them (and a lot more to come!)."] + [:p [:a {:alt "Explore libraries & templates" :target "_blank" :href "https://penpot.app/libraries-templates.html"} "Explore libraries & templates"]]] + [:div.modal-navigation + [:button.btn-secondary {:on-click finish} "Start!"] + [:& c/navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs index 9cea9c60a..0263a4826 100644 --- a/frontend/src/app/main/ui/render.cljs +++ b/frontend/src/app/main/ui/render.cljs @@ -25,9 +25,34 @@ [cuerdas.core :as str] [rumext.alpha :as mf])) +(defn calc-bounds + [object objects] + + (let [xf-get-bounds (comp (map #(get objects %)) (map #(calc-bounds % objects))) + padding (filters/calculate-padding object) + obj-bounds + (-> (filters/get-filters-bounds object) + (update :x - padding) + (update :y - padding) + (update :width + (* 2 padding)) + (update :height + (* 2 padding)))] + + (cond + (and (= :group (:type object)) + (:masked-group? object)) + (calc-bounds (get objects (first (:shapes object))) objects) + + (= :group (:type object)) + (->> (:shapes object) + (into [obj-bounds] xf-get-bounds) + (gsh/join-rects)) + + :else + obj-bounds))) + (mf/defc object-svg {::mf/wrap [mf/memo]} - [{:keys [objects object-id zoom] :or {zoom 1} :as props}] + [{:keys [objects object-id zoom render-texts?] :or {zoom 1} :as props}] (let [object (get objects object-id) frame-id (if (= :frame (:type object)) (:id object) @@ -47,20 +72,10 @@ objects (reduce updt-fn objects mod-ids) object (get objects object-id) - ;; We need to get the shadows/blurs paddings to create the viewbox properly - {:keys [x y width height]} (filters/get-filters-bounds object) + {:keys [x y width height] :as bs} (calc-bounds object objects) + [_ _ width height :as coords] (->> [x y width height] (map #(* % zoom))) - x (* x zoom) - y (* y zoom) - width (* width zoom) - height (* height zoom) - - padding (* (filters/calculate-padding object) zoom) - - vbox (str/join " " [(- x padding) - (- y padding) - (+ width padding padding) - (+ height padding padding)]) + vbox (str/join " " coords) frame-wrapper (mf/use-memo @@ -76,18 +91,22 @@ (mf/use-memo (mf/deps objects) #(exports/shape-wrapper-factory objects)) - ] + + text-shapes + (->> objects + (filter (fn [[_ shape]] (= :text (:type shape)))) + (mapv second))] (mf/use-effect (mf/deps width height) - #(dom/set-page-style {:size (str (mth/ceil (+ width padding padding)) "px " - (mth/ceil (+ height padding padding)) "px")})) + #(dom/set-page-style {:size (str (mth/ceil width) "px " + (mth/ceil height) "px")})) [:& (mf/provider embed/context) {:value true} [:svg {:id "screenshot" :view-box vbox - :width (+ width padding padding) - :height (+ height padding padding) + :width width + :height height :version "1.1" :xmlns "http://www.w3.org/2000/svg" :xmlnsXlink "http://www.w3.org/1999/xlink" @@ -100,7 +119,19 @@ :frame [:& frame-wrapper {:shape object :view-box vbox}] :group [:> shape-container {:shape object} [:& group-wrapper {:shape object}]] - [:& shape-wrapper {:shape object}])]])) + [:& shape-wrapper {:shape object}])] + + ;; Auxiliary SVG for rendering text-shapes + (when render-texts? + (for [object text-shapes] + [:svg {:id (str "screenshot-text-" (:id object)) + :view-box (str "0 0 " (:width object) " " (:height object)) + :width (:width object) + :height (:height object) + :version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink"} + [:& shape-wrapper {:shape (-> object (assoc :x 0 :y 0))}]]))])) (defn- adapt-root-frame [objects object-id] @@ -120,7 +151,7 @@ ;; backend entry point for download only the data of single page. (mf/defc render-object - [{:keys [file-id page-id object-id] :as props}] + [{:keys [file-id page-id object-id render-texts?] :as props}] (let [objects (mf/use-state nil)] (mf/use-effect (mf/deps file-id page-id object-id) @@ -140,6 +171,7 @@ (when @objects [:& object-svg {:objects @objects :object-id object-id + :render-texts? render-texts? :zoom 1}]))) (mf/defc render-sprite diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs new file mode 100644 index 000000000..28d402c85 --- /dev/null +++ b/frontend/src/app/main/ui/routes.cljs @@ -0,0 +1,122 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.routes + (:require + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.main.data.users :as du] + [app.main.store :as st] + [app.util.router :as rt] + [app.util.storage :refer [storage]] + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [potok.core :as ptk])) + +(s/def ::page-id ::us/uuid) +(s/def ::file-id ::us/uuid) +(s/def ::section ::us/keyword) +(s/def ::index ::us/integer) +(s/def ::token (s/nilable ::us/not-empty-string)) +(s/def ::share-id ::us/uuid) + +(s/def ::viewer-path-params + (s/keys :req-un [::file-id])) + +(s/def ::viewer-query-params + (s/keys :opt-un [::index ::share-id ::section ::page-id])) + +(s/def ::any any?) + +(def routes + [["/auth" + ["/login" :auth-login] + (when (contains? @cf/flags :registration) + ["/register" :auth-register]) + (when (contains? @cf/flags :registration) + ["/register/validate" :auth-register-validate]) + (when (contains? @cf/flags :registration) + ["/register/success" :auth-register-success]) + ["/recovery/request" :auth-recovery-request] + ["/recovery" :auth-recovery] + ["/verify-token" :auth-verify-token]] + + ["/settings" + ["/profile" :settings-profile] + ["/password" :settings-password] + ["/feedback" :settings-feedback] + ["/options" :settings-options]] + + ["/view/:file-id" + {:name :viewer + :conform + {:path-params ::viewer-path-params + :query-params ::viewer-query-params}}] + + (when *assert* + ["/debug/icons-preview" :debug-icons-preview]) + + ;; Used for export + ["/render-object/:file-id/:page-id/:object-id" :render-object] + ["/render-sprite/:file-id" :render-sprite] + + ["/dashboard/team/:team-id" + ["/members" :dashboard-team-members] + ["/settings" :dashboard-team-settings] + ["/projects" :dashboard-projects] + ["/search" :dashboard-search] + ["/fonts" :dashboard-fonts] + ["/fonts/providers" :dashboard-font-providers] + ["/libraries" :dashboard-libraries] + ["/projects/:project-id" :dashboard-files]] + + ["/workspace/:project-id/:file-id" :workspace]]) + +(defn- match-path + [router path] + (when-let [match (rt/match router path)] + (if-let [conform (get-in match [:data :conform])] + (let [spath (get conform :path-params ::any) + squery (get conform :query-params ::any)] + (try + (-> (dissoc match :params) + (assoc :path-params (us/conform spath (get match :path-params)) + :query-params (us/conform squery (get match :query-params)))) + (catch :default _ + nil))) + match))) + +(defn on-navigate + [router path] + (let [match (match-path router path) + profile (:profile @storage) + nopath? (or (= path "") (= path "/")) + authed? (and (not (nil? profile)) + (not= (:id profile) uuid/zero))] + + (cond + (and nopath? authed? (nil? match)) + (if (not= uuid/zero profile) + (st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)})) + (st/emit! (rt/nav :auth-login))) + + (and (not authed?) (nil? match)) + (st/emit! (rt/nav :auth-login)) + + (nil? match) + (st/emit! (rt/assign-exception {:type :not-found})) + + :else + (st/emit! (rt/navigated match))))) + +(defn init-routes + [] + (ptk/reify ::init-routes + ptk/WatchEvent + (watch [_ _ _] + (rx/of (rt/initialize-router routes) + (rt/initialize-history on-navigate))))) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 973a155a4..e9ca5a966 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -14,12 +14,14 @@ [rumext.alpha :as mf])) (defn- stroke-type->dasharray - [style] - (case style - :mixed "5,5,1,5" - :dotted "5,5" - :dashed "10,10" - nil)) + [width style] + (let [values (case style + :mixed [5 5 1 5] + :dotted [5 5] + :dashed [10 10] + nil)] + + (->> values (map #(+ % width)) (str/join ",")))) (defn- truncate-side [shape ra-attr rb-attr dimension-attr] @@ -102,10 +104,11 @@ (defn add-stroke [attrs shape render-id] (let [stroke-style (:stroke-style shape :none) - stroke-color-gradient-id (str "stroke-color-gradient_" render-id)] + stroke-color-gradient-id (str "stroke-color-gradient_" render-id) + stroke-width (:stroke-width shape 1)] (if (not= stroke-style :none) (let [stroke-attrs - (cond-> {:strokeWidth (:stroke-width shape 1)} + (cond-> {:strokeWidth stroke-width} (:stroke-color-gradient shape) (assoc :stroke (str/format "url(#%s)" stroke-color-gradient-id)) @@ -118,7 +121,7 @@ (assoc :strokeOpacity (:stroke-opacity shape nil)) (not= stroke-style :svg) - (assoc :strokeDasharray (stroke-type->dasharray stroke-style)) + (assoc :strokeDasharray (stroke-type->dasharray stroke-width stroke-style)) ;; For simple line caps we use svg stroke-line-cap attribute. This ;; only works if all caps are the same and we are not using the tricks diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs new file mode 100644 index 000000000..fd9b58f94 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -0,0 +1,114 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.shapes.bool + (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.path :as gsp] + [app.common.path.bool :as pb] + [app.common.path.shapes-to-path :as stp] + [app.main.ui.hooks :refer [use-equal-memo]] + [app.main.ui.shapes.export :as use] + [app.main.ui.shapes.path :refer [path-shape]] + [app.util.object :as obj] + [rumext.alpha :as mf])) + +(mf/defc debug-bool + {::mf/wrap-props false} + [props] + + (let [frame (obj/get props "frame") + shape (obj/get props "shape") + childs (obj/get props "childs") + + [content-a content-b] + (mf/use-memo + (mf/deps shape childs) + (fn [] + (let [childs (d/mapm #(-> %2 (gsh/translate-to-frame frame) gsh/transform-shape) childs) + [content-a content-b] + (->> (:shapes shape) + (map #(get childs %)) + (filter #(not (:hidden %))) + (map #(stp/convert-to-path % childs)) + (map :content) + (map pb/close-paths) + (map pb/add-previous))] + (pb/content-intersect-split content-a content-b))))] + [:g.debug-bool + [:g.shape-a + [:& path-shape {:shape (-> shape + (assoc :type :path) + (assoc :stroke-color "blue") + (assoc :stroke-opacity 1) + (assoc :stroke-width 1) + (assoc :stroke-style :solid) + (dissoc :fill-color :fill-opacity) + (assoc :content content-b)) + :frame frame}] + (for [{:keys [x y]} (gsp/content->points (pb/close-paths content-b))] + [:circle {:cx x + :cy y + :r 2.5 + :style {:fill "blue"}}])] + + [:g.shape-b + [:& path-shape {:shape (-> shape + (assoc :type :path) + (assoc :stroke-color "red") + (assoc :stroke-opacity 1) + (assoc :stroke-width 0.5) + (assoc :stroke-style :solid) + (dissoc :fill-color :fill-opacity) + (assoc :content content-a)) + :frame frame}] + (for [{:keys [x y]} (gsp/content->points (pb/close-paths content-a))] + [:circle {:cx x + :cy y + :r 1.25 + :style {:fill "red"}}])]]) + ) + + +(defn bool-shape + [shape-wrapper] + (mf/fnc bool-shape + {::mf/wrap-props false} + [props] + (let [frame (obj/get props "frame") + shape (obj/get props "shape") + childs (obj/get props "childs") + + childs (use-equal-memo childs) + + include-metadata? (mf/use-ctx use/include-metadata-ctx) + + bool-content + (mf/use-memo + (mf/deps shape childs) + (fn [] + (let [childs (d/mapm #(-> %2 gsh/transform-shape (gsh/translate-to-frame frame)) childs)] + (->> (:shapes shape) + (map #(get childs %)) + (filter #(not (:hidden %))) + (map #(stp/convert-to-path % childs)) + (mapv :content) + (pb/content-bool (:bool-type shape))))))] + + [:* + [:& path-shape {:shape (assoc shape :content bool-content)}] + + (when include-metadata? + [:> "penpot:bool" {} + (for [item (->> (:shapes shape) (mapv #(get childs %)))] + [:& shape-wrapper {:frame frame + :shape item + :key (:id item)}])]) + + #_[:& debug-bool {:frame frame + :shape shape + :childs childs}]]))) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 6a5f92dcb..35d7691de 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.shapes.custom-stroke (:require [app.common.data :as d] + [app.common.geom.shapes :as gsh] [app.main.ui.context :as muc] [app.util.object :as obj] [cuerdas.core :as str] @@ -34,8 +35,22 @@ [{:keys [shape render-id]}] (let [stroke-mask-id (str "outer-stroke-" render-id) shape-id (str "stroke-shape-" render-id) - stroke-width (:stroke-width shape 0)] - [:mask {:id stroke-mask-id} + stroke-width (case (:stroke-alignment shape :center) + :center (/ (:stroke-width shape 0) 2) + :outer (:stroke-width shape 0) + 0) + margin (gsh/shape-stroke-margin shape stroke-width) + bounding-box (-> (gsh/points->selrect (:points shape)) + (update :x - (+ stroke-width margin)) + (update :y - (+ stroke-width margin)) + (update :width + (* 2 (+ stroke-width margin))) + (update :height + (* 2 (+ stroke-width margin))))] + [:mask {:id stroke-mask-id + :x (:x bounding-box) + :y (:y bounding-box) + :width (:width bounding-box) + :height (:height bounding-box) + :maskUnits "userSpaceOnUse"} [:use {:xlinkHref (str "#" shape-id) :style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}] @@ -60,8 +75,8 @@ :viewBox "0 0 3 6" :refX "2" :refY "3" - :markerWidth "3" - :markerHeight "6" + :markerWidth "8.5" + :markerHeight "8.5" :orient "auto-start-reverse" :fill stroke-color :fillOpacity stroke-opacity} @@ -72,8 +87,8 @@ :viewBox "0 0 3 6" :refX "2" :refY "3" - :markerWidth "3" - :markerHeight "6" + :markerWidth "8.5" + :markerHeight "8.5" :orient "auto-start-reverse" :fill stroke-color :fillOpacity stroke-opacity} @@ -82,10 +97,10 @@ (when (or (= cap-start :square-marker) (= cap-end :square-marker)) [:marker {:id (str marker-id-prefix "-square-marker") :viewBox "0 0 6 6" - :refX "5" + :refX "3" :refY "3" - :markerWidth "6" - :markerHeight "6" + :markerWidth "4.2426" ;; diagonal length of a 3x3 square + :markerHeight "4.2426" :orient "auto-start-reverse" :fill stroke-color :fillOpacity stroke-opacity} @@ -94,10 +109,10 @@ (when (or (= cap-start :circle-marker) (= cap-end :circle-marker)) [:marker {:id (str marker-id-prefix "-circle-marker") :viewBox "0 0 6 6" - :refX "5" + :refX "3" :refY "3" - :markerWidth "6" - :markerHeight "6" + :markerWidth "4" + :markerHeight "4" :orient "auto-start-reverse" :fill stroke-color :fillOpacity stroke-opacity} @@ -106,7 +121,7 @@ (when (or (= cap-start :diamond-marker) (= cap-end :diamond-marker)) [:marker {:id (str marker-id-prefix "-diamond-marker") :viewBox "0 0 6 6" - :refX "5" + :refX "3" :refY "3" :markerWidth "6" :markerHeight "6" @@ -145,22 +160,24 @@ (mf/defc stroke-defs [{:keys [shape render-id]}] - (cond - (and (= :inner (:stroke-alignment shape :center)) - (> (:stroke-width shape 0) 0)) - [:& inner-stroke-clip-path {:shape shape - :render-id render-id}] + (when (or (not= (:type shape) :path) + (not (gsh/open-path? shape))) + (cond + (and (= :inner (:stroke-alignment shape :center)) + (> (:stroke-width shape 0) 0)) + [:& inner-stroke-clip-path {:shape shape + :render-id render-id}] - (and (= :outer (:stroke-alignment shape :center)) - (> (:stroke-width shape 0) 0)) - [:& outer-stroke-mask {:shape shape - :render-id render-id}] + (and (= :outer (:stroke-alignment shape :center)) + (> (:stroke-width shape 0) 0)) + [:& outer-stroke-mask {:shape shape + :render-id render-id}] - (and (or (some? (:stroke-cap-start shape)) - (some? (:stroke-cap-end shape))) - (= (:stroke-alignment shape) :center)) - [:& cap-markers {:shape shape - :render-id render-id}])) + (and (or (some? (:stroke-cap-start shape)) + (some? (:stroke-cap-end shape))) + (= (:stroke-alignment shape) :center)) + [:& cap-markers {:shape shape + :render-id render-id}]))) ;; Outer alingmnent: display the shape in two layers. One ;; without stroke (only fill), and another one only with stroke @@ -253,15 +270,17 @@ stroke-position (:stroke-alignment shape :center) has-stroke? (and (> stroke-width 0) (not= stroke-style :none)) - inner? (= :inner stroke-position) - outer? (= :outer stroke-position)] + closed? (or (not= :path (:type shape)) + (not (gsh/open-path? shape))) + inner? (= :inner stroke-position) + outer? (= :outer stroke-position)] (cond - (and has-stroke? inner?) + (and has-stroke? inner? closed?) [:& inner-stroke {:shape shape} child] - (and has-stroke? outer?) + (and has-stroke? outer? closed?) [:& outer-stroke {:shape shape} child] diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index 7f1d3bcb9..f20428298 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -64,6 +64,7 @@ text? (= :text (:type shape)) path? (= :path (:type shape)) mask? (and group? (:masked-group? shape)) + bool? (= :bool (:type shape)) center (gsh/center-shape shape)] (-> props (add! :name) @@ -102,7 +103,10 @@ (add! :content (comp json/encode uuid->string)))) (cond-> mask? - (obj/set! "penpot:masked-group" "true"))))) + (obj/set! "penpot:masked-group" "true")) + + (cond-> bool? + (add! :bool-type))))) (defn add-library-refs [props shape] @@ -127,30 +131,41 @@ (mf/defc export-grid-data [{:keys [grids]}] - (when-not (empty? grids) - [:> "penpot:grids" #js {} - (for [{:keys [type display params]} grids] - (let [props (->> (d/without-keys params [:color]) - (prefix-keys) - (clj->js))] - [:> "penpot:grid" - (-> props - (obj/set! "penpot:color" (get-in params [:color :color])) - (obj/set! "penpot:opacity" (get-in params [:color :opacity])) - (obj/set! "penpot:type" (d/name type)) - (cond-> (some? display) - (obj/set! "penpot:display" (str display))))]))])) + [:> "penpot:grids" #js {} + (for [{:keys [type display params]} grids] + (let [props (->> (d/without-keys params [:color]) + (prefix-keys) + (clj->js))] + [:> "penpot:grid" + (-> props + (obj/set! "penpot:color" (get-in params [:color :color])) + (obj/set! "penpot:opacity" (get-in params [:color :opacity])) + (obj/set! "penpot:type" (d/name type)) + (cond-> (some? display) + (obj/set! "penpot:display" (str display))))]))]) + +(mf/defc export-flows + [{:keys [flows]}] + [:> "penpot:flows" #js {} + (for [{:keys [id name starting-frame]} flows] + [:> "penpot:flow" #js {:id id + :name name + :starting-frame starting-frame}])]) (mf/defc export-page [{:keys [options]}] - (let [saved-grids (get options :saved-grids)] - (when-not (empty? saved-grids) - (let [parse-grid - (fn [[type params]] - {:type type :params params}) - grids (->> saved-grids (mapv parse-grid))] - [:> "penpot:page" #js {} - [:& export-grid-data {:grids grids}]])))) + (let [saved-grids (get options :saved-grids) + flows (get options :flows)] + (when (or (seq saved-grids) (seq flows)) + (let [parse-grid + (fn [[type params]] + {:type type :params params}) + grids (->> saved-grids (mapv parse-grid))] + [:> "penpot:page" #js {} + (when (seq saved-grids) + [:& export-grid-data {:grids grids}]) + (when (seq flows) + [:& export-flows {:flows flows}])])))) (mf/defc export-shadow-data [{:keys [shadow]}] @@ -216,11 +231,18 @@ [{:keys [interactions]}] (when-not (empty? interactions) [:> "penpot:interactions" #js {} - (for [{:keys [action-type destination event-type]} interactions] + (for [interaction interactions] [:> "penpot:interaction" - #js {:penpot:action-type (d/name action-type) - :penpot:destination (str destination) - :penpot:event-type (d/name event-type)}])])) + #js {:penpot:event-type (d/name (:event-type interaction)) + :penpot:action-type (d/name (:action-type interaction)) + :penpot:delay ((d/nilf str) (:delay interaction)) + :penpot:destination ((d/nilf str) (:destination interaction)) + :penpot:overlay-pos-type ((d/nilf d/name) (:overlay-pos-type interaction)) + :penpot:overlay-position-x ((d/nilf get-in) interaction [:overlay-position :x]) + :penpot:overlay-position-y ((d/nilf get-in) interaction [:overlay-position :y]) + :penpot:url (:url interaction) + :penpot:close-click-outside ((d/nilf str) (:close-click-outside interaction)) + :penpot:background-overlay ((d/nilf str) (:background-overlay interaction))}])])) (mf/defc export-data [{:keys [shape]}] diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index 25af34796..60ba3fee6 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -202,17 +202,12 @@ :height (- y2 y1)}))))) (defn calculate-padding [shape] - (let [{:keys [stroke-style stroke-alignment stroke-width]} shape] - (cond - (and (not= stroke-style :none) - (= stroke-alignment :outer)) - stroke-width - - (and (not= stroke-style :none) - (= stroke-alignment :center)) - (mth/ceil (/ stroke-width 2)) - - :else 0))) + (let [stroke-width (case (:stroke-alignment shape :center) + :center (/ (:stroke-width shape 0) 2) + :outer (:stroke-width shape 0) + 0) + margin (gsh/shape-stroke-margin shape stroke-width)] + (+ stroke-width margin))) (mf/defc filters [{:keys [filter-id shape]}] @@ -221,9 +216,7 @@ ;; Adds the previous filter as `filter-in` parameter filters (map #(assoc %1 :filter-in %2) filters (cons nil (map :id filters))) - bounds (get-filters-bounds shape filters (or (-> shape :blur :value) 0)) - padding (calculate-padding shape)] [:* diff --git a/frontend/src/app/main/ui/shapes/group.cljs b/frontend/src/app/main/ui/shapes/group.cljs index 2dafc7bff..49a628350 100644 --- a/frontend/src/app/main/ui/shapes/group.cljs +++ b/frontend/src/app/main/ui/shapes/group.cljs @@ -27,21 +27,31 @@ [(first childs) (rest childs)] [nil childs]) + ;; We need to separate mask and clip into two because a bug in Firefox + ;; breaks when the group has clip+mask+foreignObject + ;; Clip and mask separated will work in every platform + ; Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1734805 + [clip-wrapper clip-props] + (if masked-group? + ["g" (-> (obj/new) + (obj/set! "clipPath" (clip-url render-id mask)))] + [mf/Fragment nil]) + [mask-wrapper mask-props] (if masked-group? ["g" (-> (obj/new) - (obj/set! "clipPath" (clip-url render-id mask)) (obj/set! "mask" (mask-url render-id mask)))] [mf/Fragment nil])] - [:> mask-wrapper mask-props - (when masked-group? - [:> render-mask #js {:frame frame :mask mask}]) + [:> clip-wrapper clip-props + [:> mask-wrapper mask-props + (when masked-group? + [:> render-mask #js {:frame frame :mask mask}]) - (for [item childs] - [:& shape-wrapper {:frame frame - :shape item - :key (:id item)}])])))) + (for [item childs] + [:& shape-wrapper {:frame frame + :shape item + :key (:id item)}])]])))) diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index eb26eb5a4..5b68b7ee9 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -72,6 +72,7 @@ [:> wrapper-tag wrapper-props (when include-metadata? [:& ed/export-data {:shape shape}]) + [:defs [:& defs/svg-defs {:shape shape :render-id render-id}] [:& filters/filters {:shape shape :filter-id filter-id}] diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 2dca984ef..1ee53dd4d 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -18,7 +18,8 @@ (let [valign (:vertical-align node "top") width (some-> (:width shape) (+ 1)) base #js {:height (or (:height shape) "100%") - :width (or width "100%")}] + :width (or width "100%") + :fontFamily "sourcesanspro"}] (cond-> base (= valign "top") (obj/set! "justifyContent" "flex-start") (= valign "center") (obj/set! "justifyContent" "center") @@ -40,6 +41,7 @@ :justifyContent "inherit" :minHeight (when-not (or auto-width? auto-height?) "100%") :minWidth (when-not auto-width? "100%") + :marginRight "1px" :verticalAlign "top"})) (defn generate-paragraph-styles diff --git a/frontend/src/app/main/ui/share_link.cljs b/frontend/src/app/main/ui/share_link.cljs index b9a84565b..f8823ddd6 100644 --- a/frontend/src/app/main/ui/share_link.cljs +++ b/frontend/src/app/main/ui/share_link.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.share-link (:require [app.common.data :as d] + [app.common.logging :as log] [app.config :as cf] [app.main.data.common :as dc] [app.main.data.messages :as dm] @@ -16,12 +17,11 @@ [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.logging :as log] [app.util.router :as rt] [app.util.webapi :as wapi] [rumext.alpha :as mf])) -(log/set-level! :debug) +(log/set-level! :warn) (defn prepare-params [{:keys [sections pages pages-mode]}] diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index 6f630ce14..fc1fe6e08 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -6,7 +6,6 @@ (ns app.main.ui.static (:require - [app.main.data.messages :as dm] [app.main.data.users :as du] [app.main.refs :as refs] [app.main.store :as st] @@ -72,7 +71,7 @@ [:div.desc-message (tr "labels.internal-error.desc-message")] [:div.sign-info [:a.btn-primary.btn-small - {:on-click (st/emitf (dm/assign-exception nil))} + {:on-click (st/emitf (rt/assign-exception nil))} (tr "labels.retry")]]]) (mf/defc exception-page diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index c3b54d8bb..a17097db4 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -6,6 +6,8 @@ (ns app.main.ui.viewer (:require + [app.common.exceptions :as ex] + [app.common.geom.point :as gpt] [app.main.data.comments :as dcm] [app.main.data.viewer :as dv] [app.main.data.viewer.shortcuts :as sc] @@ -13,6 +15,7 @@ [app.main.store :as st] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] + [app.main.ui.shapes.filters :as filters] [app.main.ui.share-link] [app.main.ui.static :as static] [app.main.ui.viewer.comments :refer [comments-layer]] @@ -27,22 +30,19 @@ (defn- calculate-size [frame zoom] - {:width (* (:width frame) zoom) - :height (* (:height frame) zoom) - :vbox (str "0 0 " (:width frame 0) " " (:height frame 0))}) + (let [{:keys [_ _ width height]} (filters/get-filters-bounds frame)] + {:width (* width zoom) + :height (* height zoom) + :vbox (str "0 0 " width " " height)})) (mf/defc viewer [{:keys [params data]}] (let [{:keys [page-id section index]} params + {:keys [file users project permissions]} data local (mf/deref refs/viewer-local) - file (:file data) - users (:users data) - project (:project data) - perms (:permissions data) - page-id (or page-id (-> file :data :pages first)) page (mf/use-memo @@ -58,15 +58,26 @@ (mf/deps frame zoom) (fn [] (calculate-size frame zoom))) + interactions-mode + (:interactions-mode local) + on-click (mf/use-callback (mf/deps section) (fn [_] (when (= section :comments) - (st/emit! (dcm/close-thread)))))] + (st/emit! (dcm/close-thread))))) + + close-overlay + (mf/use-callback + (fn [frame] + (st/emit! (dv/close-overlay (:id frame)))))] (hooks/use-shortcuts ::viewer sc/shortcuts) + (when (nil? page) + (ex/raise :type :not-found)) + ;; Set the page title (mf/use-effect (mf/deps (:name file)) @@ -90,8 +101,8 @@ :file file :page page :frame frame - :permissions perms - :zoom (:zoom local) + :permissions permissions + :zoom zoom :section section}] [:div.viewer-content @@ -118,7 +129,6 @@ :section section :local local}] - [:div.viewport-container {:style {:width (:width size) :height (:height size) @@ -133,11 +143,43 @@ [:& interactions/viewport {:frame frame + :base-frame frame + :frame-offset (gpt/point 0 0) :size size :page page :file file :users users - :local local}]]))]]])) + :interactions-mode interactions-mode}] + + (for [overlay (:overlays local)] + (let [size-over (calculate-size (:frame overlay) zoom)] + [:* + (when (or (:close-click-outside overlay) + (:background-overlay overlay)) + [:div.viewer-overlay-background + {:class (dom/classnames + :visible (:background-overlay overlay)) + :style {:width (:width frame) + :height (:height frame) + :position "absolute" + :left 0 + :top 0} + :on-click #(when (:close-click-outside overlay) + (close-overlay (:frame overlay)))}]) + [:div.viewport-container.viewer-overlay + {:style {:width (:width size-over) + :height (:height size-over) + :left (* (:x (:position overlay)) zoom) + :top (* (:y (:position overlay)) zoom)}} + [:& interactions/viewport + {:frame (:frame overlay) + :base-frame frame + :frame-offset (:position overlay) + :size size-over + :page page + :file file + :users users + :interactions-mode interactions-mode}]]]))]))]]])) ;; --- Component: Viewer Page diff --git a/frontend/src/app/main/ui/viewer/handoff/render.cljs b/frontend/src/app/main/ui/viewer/handoff/render.cljs index 093cfc846..0f1854fc1 100644 --- a/frontend/src/app/main/ui/viewer/handoff/render.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/render.cljs @@ -8,8 +8,10 @@ "The main container for a frame in handoff mode" (:require [app.common.geom.shapes :as geom] + [app.common.pages :as cp] [app.main.data.viewer :as dv] [app.main.store :as st] + [app.main.ui.shapes.bool :as bool] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.group :as group] @@ -106,6 +108,22 @@ (obj/merge! #js {:childs childs}))] [:> group-wrapper props])))) +(defn bool-container-factory + [objects] + (let [shape-container (shape-container-factory objects) + bool-shape (bool/bool-shape shape-container) + bool-wrapper (shape-wrapper-factory bool-shape)] + (mf/fnc bool-container + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + children-ids (cp/get-children (:id shape) objects) + childs (select-keys objects children-ids) + props (-> (obj/new) + (obj/merge! props) + (obj/merge! #js {:childs childs}))] + [:> bool-wrapper props])))) + (defn svg-raw-container-factory [objects] (let [shape-container (shape-container-factory objects) @@ -133,12 +151,18 @@ [props] (let [shape (unchecked-get props "shape") frame (unchecked-get props "frame") - group-container (mf/use-memo - (mf/deps objects) - #(group-container-factory objects)) - svg-raw-container (mf/use-memo - (mf/deps objects) - #(svg-raw-container-factory objects))] + + group-container + (mf/use-memo (mf/deps objects) + #(group-container-factory objects)) + + bool-container + (mf/use-memo (mf/deps objects) + #(bool-container-factory objects)) + + svg-raw-container + (mf/use-memo (mf/deps objects) + #(svg-raw-container-factory objects))] (when (and shape (not (:hidden shape))) (let [shape (-> (geom/transform-shape shape) (geom/translate-to-frame frame)) @@ -151,6 +175,7 @@ :image [:> image-wrapper opts] :circle [:> circle-wrapper opts] :group [:> group-container opts] + :bool [:> bool-container opts] :svg-raw [:> svg-raw-container opts]))))))) (mf/defc render-frame-svg diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index b0c869f90..2c838251e 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -13,14 +13,14 @@ [app.main.ui.components.fullscreen :as fs] [app.main.ui.icons :as i] [app.main.ui.viewer.comments :refer [comments-menu]] - [app.main.ui.viewer.interactions :refer [interactions-menu]] + [app.main.ui.viewer.interactions :refer [flows-menu interactions-menu]] [app.main.ui.workspace.header :refer [zoom-widget]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.alpha :as mf])) (mf/defc header-options - [{:keys [section zoom page file permissions]}] + [{:keys [section zoom page file index permissions]}] (let [fullscreen (mf/use-ctx fs/fullscreen-context) toggle-fullscreen @@ -43,7 +43,10 @@ [:div.options-zone (case section - :interactions [:& interactions-menu] + :interactions [:* + (when index + [:& flows-menu {:page page :index index}]) + [:& interactions-menu]] :comments [:& comments-menu] [:div.view-options]) @@ -64,10 +67,10 @@ i/full-screen-off i/full-screen)] - (when (:edit permissions) + (when (:is-admin permissions) [:span.btn-primary {:on-click open-share-dialog} (tr "labels.share-prototype")]) - (when (:edit permissions) + (when (:can-edit permissions) [:span.btn-text-dark {:on-click go-to-workspace} (tr "labels.edit-file")])])) (mf/defc header-sitemap @@ -84,15 +87,23 @@ show-dropdown? (mf/use-state false) + open-dropdown + (fn [] + (reset! show-dropdown? true) + (st/emit! dv/close-thumbnails-panel)) + + close-dropdown + (fn [] + (reset! show-dropdown? false)) + navigate-to (fn [page-id] (st/emit! (dv/go-to-page page-id)) - (reset! show-dropdown? false)) - ] + (reset! show-dropdown? false))] [:div.sitemap-zone {:alt (tr "viewer.header.sitemap")} [:div.breadcrumb - {:on-click #(swap! show-dropdown? not)} + {:on-click open-dropdown} [:span.project-name project-name] [:span "/"] [:span.file-name file-name] @@ -101,7 +112,7 @@ [:span.icon i/arrow-down] [:& dropdown {:show @show-dropdown? - :on-close #(swap! show-dropdown? not)} + :on-close close-dropdown} [:ul.dropdown (for [id (get-in file [:data :pages])] [:li {:id (str id) @@ -125,7 +136,6 @@ (fn [section] (st/emit! (dv/go-to-section section)))] - [:header.viewer-header [:div.main-icon [:a {:on-click go-to-dashboard @@ -141,14 +151,16 @@ :alt (tr "viewer.header.interactions-section")} i/play] - (when (:edit permissions) + (when (:can-edit permissions) [:button.mode-zone-button.tooltip.tooltip-bottom {:on-click #(navigate :comments) :class (dom/classnames :active (= section :comments)) :alt (tr "viewer.header.comments-section")} i/chat]) - (when (:read permissions) + (when (or (= (:type permissions) :membership) + (and (= (:type permissions) :share-link) + (contains? (:flags permissions) :section-handoff))) [:button.mode-zone-button.tooltip.tooltip-bottom {:on-click #(navigate :handoff) :class (dom/classnames :active (= section :handoff)) @@ -159,5 +171,6 @@ :permissions permissions :page page :file file + :index index :zoom zoom}]])) diff --git a/frontend/src/app/main/ui/viewer/interactions.cljs b/frontend/src/app/main/ui/viewer/interactions.cljs index 15846eca9..f4faa97fc 100644 --- a/frontend/src/app/main/ui/viewer/interactions.cljs +++ b/frontend/src/app/main/ui/viewer/interactions.cljs @@ -10,6 +10,7 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.pages :as cp] + [app.common.types.page-options :as cto] [app.main.data.comments :as dcm] [app.main.data.viewer :as dv] [app.main.refs :as refs] @@ -41,25 +42,23 @@ (mf/defc viewport {::mf/wrap [mf/memo]} - [{:keys [local page frame size]}] - (let [interactions? (:interactions-show? local) - - objects (mf/use-memo + [{:keys [page interactions-mode frame base-frame frame-offset size]}] + (let [objects (mf/use-memo (mf/deps page frame) (prepare-objects page frame)) wrapper (mf/use-memo - (mf/deps objects interactions?) - #(shapes/frame-container-factory objects interactions?)) + (mf/deps objects) + #(shapes/frame-container-factory objects)) - ;; Retrieve frame again with correct modifier + ;; Retrieve frames again with correct modifier frame (get objects (:id frame)) + base-frame (get objects (:id base-frame)) on-click (fn [_] - (let [mode (:interactions-mode local)] - (when (= mode :show-on-click) - (st/emit! dv/flash-interactions)))) + (when (= interactions-mode :show-on-click) + (st/emit! dv/flash-interactions))) on-mouse-wheel (fn [event] @@ -77,7 +76,7 @@ (st/emit! (dcm/close-thread))))] (mf/use-effect - (mf/deps local) ;; on-click event depends on local + (mf/deps interactions-mode) ;; on-click event depends on interactions-mode (fn [] ;; bind with passive=false to allow the event to be cancelled ;; https://stackoverflow.com/a/57582286/3219895 @@ -89,15 +88,50 @@ (events/unlistenByKey key2) (events/unlistenByKey key3))))) - [:svg {:view-box (:vbox size) - :width (:width size) - :height (:height size) - :version "1.1" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns "http://www.w3.org/2000/svg"} - [:& wrapper {:shape frame - :show-interactions? interactions? - :view-box (:vbox size)}]])) + [:& (mf/provider shapes/base-frame-ctx) {:value base-frame} + [:& (mf/provider shapes/frame-offset-ctx) {:value frame-offset} + [:svg {:view-box (:vbox size) + :width (:width size) + :height (:height size) + :version "1.1" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns "http://www.w3.org/2000/svg"} + [:& wrapper {:shape frame + :view-box (:vbox size)}]]]])) + + +(mf/defc flows-menu + {::mf/wrap [mf/memo]} + [{:keys [page index]}] + (let [flows (get-in page [:options :flows]) + frames (:frames page) + frame (get frames index) + current-flow (mf/use-state + (cto/get-frame-flow flows (:id frame))) + + show-dropdown? (mf/use-state false) + toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) + hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) + + select-flow + (mf/use-callback + (fn [flow] + (reset! current-flow flow) + (st/emit! (dv/go-to-frame (:starting-frame flow)))))] + + (when (seq flows) + [:div.view-options {:on-click toggle-dropdown} + [:span.icon i/play] + [:span.label (:name @current-flow)] + [:span.icon i/arrow-down] + [:& dropdown {:show @show-dropdown? + :on-close hide-dropdown} + [:ul.dropdown.with-check + (for [flow flows] + [:li {:class (dom/classnames :selected (= (:id flow) (:id @current-flow))) + :on-click #(select-flow flow)} + [:span.icon i/tick] + [:span.label (:name flow)]])]]]))) (mf/defc interactions-menu diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 39d90d175..dc10b4446 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -12,8 +12,11 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as geom] [app.common.pages :as cp] + [app.common.types.interactions :as cti] [app.main.data.viewer :as dv] + [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.shapes.bool :as bool] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.group :as group] @@ -25,21 +28,162 @@ [app.main.ui.shapes.text :as text] [app.util.dom :as dom] [app.util.object :as obj] + [app.util.router :as rt] + [app.util.timers :as tm] + [okulary.core :as l] [rumext.alpha :as mf])) -(defn on-mouse-down - [event interactions] - (let [interaction (first (filter #(= (:event-type %) :click) interactions))] - (case (:action-type interaction) - :navigate - (let [frame-id (:destination interaction)] - (dom/stop-propagation event) - (st/emit! (dv/go-to-frame frame-id))) +(def base-frame-ctx (mf/create-context nil)) +(def frame-offset-ctx (mf/create-context nil)) - nil))) +(def viewer-interactions-show? + (l/derived :interactions-show? refs/viewer-local)) + +(defn activate-interaction + [interaction shape base-frame frame-offset objects] + (case (:action-type interaction) + :navigate + (when-let [frame-id (:destination interaction)] + (st/emit! (dv/go-to-frame frame-id))) + + :open-overlay + (let [dest-frame-id (:destination interaction) + close-click-outside (:close-click-outside interaction) + background-overlay (:background-overlay interaction) + + dest-frame (get objects dest-frame-id) + position (cti/calc-overlay-position interaction + base-frame + dest-frame + frame-offset)] + (when dest-frame-id + (st/emit! (dv/open-overlay dest-frame-id + position + close-click-outside + background-overlay)))) + + :toggle-overlay + (let [frame-id (:destination interaction) + position (:overlay-position interaction) + close-click-outside (:close-click-outside interaction) + background-overlay (:background-overlay interaction)] + (when frame-id + (st/emit! (dv/toggle-overlay frame-id + position + close-click-outside + background-overlay)))) + + :close-overlay + (let [frame-id (or (:destination interaction) + (if (= (:type shape) :frame) + (:id shape) + (:frame-id shape)))] + (st/emit! (dv/close-overlay frame-id))) + + :prev-screen + (st/emit! (rt/nav-back-local)) + + :open-url + (st/emit! (dom/open-new-window (:url interaction))) + + nil)) + +;; Perform the opposite action of an interaction, if possible +(defn deactivate-interaction + [interaction shape base-frame frame-offset objects] + (case (:action-type interaction) + :open-overlay + (let [frame-id (or (:destination interaction) + (if (= (:type shape) :frame) + (:id shape) + (:frame-id shape)))] + (st/emit! (dv/close-overlay frame-id))) + + :toggle-overlay + (let [frame-id (:destination interaction) + position (:overlay-position interaction) + close-click-outside (:close-click-outside interaction) + background-overlay (:background-overlay interaction)] + (when frame-id + (st/emit! (dv/toggle-overlay frame-id + position + close-click-outside + background-overlay)))) + + :close-overlay + (let [dest-frame-id (:destination interaction) + close-click-outside (:close-click-outside interaction) + background-overlay (:background-overlay interaction) + + dest-frame (get objects dest-frame-id) + position (cti/calc-overlay-position interaction + base-frame + dest-frame + frame-offset)] + (when dest-frame-id + (st/emit! (dv/open-overlay dest-frame-id + position + close-click-outside + background-overlay)))) + nil)) + +(defn on-mouse-down + [event shape base-frame frame-offset objects] + (let [interactions (->> (:interactions shape) + (filter #(or (= (:event-type %) :click) + (= (:event-type %) :mouse-press))))] + (when (seq interactions) + (dom/stop-propagation event) + (doseq [interaction interactions] + (activate-interaction interaction shape base-frame frame-offset objects))))) + +(defn on-mouse-up + [event shape base-frame frame-offset objects] + (let [interactions (->> (:interactions shape) + (filter #(= (:event-type %) :mouse-press)))] + (when (seq interactions) + (dom/stop-propagation event) + (doseq [interaction interactions] + (deactivate-interaction interaction shape base-frame frame-offset objects))))) + +(defn on-mouse-enter + [event shape base-frame frame-offset objects] + (let [interactions (->> (:interactions shape) + (filter #(or (= (:event-type %) :mouse-enter) + (= (:event-type %) :mouse-over))))] + (when (seq interactions) + (dom/stop-propagation event) + (doseq [interaction interactions] + (activate-interaction interaction shape base-frame frame-offset objects))))) + +(defn on-mouse-leave + [event shape base-frame frame-offset objects] + (let [interactions (->> (:interactions shape) + (filter #(= (:event-type %) :mouse-leave))) + interactions-inv (->> (:interactions shape) + (filter #(= (:event-type %) :mouse-over)))] + (when (or (seq interactions) (seq interactions-inv)) + (dom/stop-propagation event) + (doseq [interaction interactions] + (activate-interaction interaction shape base-frame frame-offset objects)) + (doseq [interaction interactions-inv] + (deactivate-interaction interaction shape base-frame frame-offset objects))))) + +(defn on-load + [shape base-frame frame-offset objects] + (let [interactions (->> (:interactions shape) + (filter #(= (:event-type %) :after-delay)))] + (loop [interactions (seq interactions) + sems []] + (if-let [interaction (first interactions)] + (let [sem (tm/schedule (:delay interaction) + #(activate-interaction interaction shape base-frame frame-offset objects))] + (recur (next interactions) + (conj sems sem))) + sems)))) (mf/defc interaction - [{:keys [shape interactions show-interactions?]}] + [{:keys [shape interactions interactions-show?]}] (let [{:keys [x y width height]} (:selrect shape) frame? (= :frame (:type shape))] (when-not (empty? interactions) @@ -49,37 +193,44 @@ :height (+ height 2) :fill "#31EFB8" :stroke "#31EFB8" - :stroke-width (if show-interactions? 1 0) - :fill-opacity (if show-interactions? 0.2 0) + :stroke-width (if interactions-show? 1 0) + :fill-opacity (if interactions-show? 0.2 0) :style {:pointer-events (when frame? "none")} :transform (geom/transform-matrix shape)}]))) (defn generic-wrapper-factory "Wrap some svg shape and add interaction controls" - [component show-interactions?] + [component] (mf/fnc generic-wrapper {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - objects (unchecked-get props "objects") childs (unchecked-get props "childs") frame (unchecked-get props "frame") + objects (unchecked-get props "objects") - interactions (->> (:interactions shape) - (filter #(contains? objects (:destination %)))) + base-frame (mf/use-ctx base-frame-ctx) + frame-offset (mf/use-ctx frame-offset-ctx) - on-mouse-down (mf/use-callback - (mf/deps interactions) - (fn [event] - (on-mouse-down event interactions))) + interactions-show? (mf/deref viewer-interactions-show?) + + interactions (:interactions shape) svg-element? (and (= :svg-raw (:type shape)) (not= :svg (get-in shape [:content :tag])))] + (mf/use-effect + (fn [] + (let [sems (on-load shape base-frame frame-offset objects)] + #(run! tm/dispose! sems)))) + (if-not svg-element? [:> shape-container {:shape shape - :cursor (when-not (empty? interactions) "pointer") - :on-mouse-down on-mouse-down} + :cursor (when (cti/actionable? interactions) "pointer") + :on-mouse-down #(on-mouse-down % shape base-frame frame-offset objects) + :on-mouse-up #(on-mouse-up % shape base-frame frame-offset objects) + :on-mouse-enter #(on-mouse-enter % shape base-frame frame-offset objects) + :on-mouse-leave #(on-mouse-leave % shape base-frame frame-offset objects)} [:& component {:shape shape :frame frame @@ -88,7 +239,7 @@ [:& interaction {:shape shape :interactions interactions - :show-interactions? show-interactions?}]] + :interactions-show? interactions-show?}]] ;; Don't wrap svg elements inside a otherwise some can break [:& component {:shape shape @@ -96,60 +247,63 @@ :childs childs}])))) (defn frame-wrapper - [shape-container show-interactions?] - (generic-wrapper-factory (frame/frame-shape shape-container) show-interactions?)) + [shape-container] + (generic-wrapper-factory (frame/frame-shape shape-container))) (defn group-wrapper - [shape-container show-interactions?] - (generic-wrapper-factory (group/group-shape shape-container) show-interactions?)) + [shape-container] + (generic-wrapper-factory (group/group-shape shape-container))) + +(defn bool-wrapper + [shape-container] + (generic-wrapper-factory (bool/bool-shape shape-container))) (defn svg-raw-wrapper - [shape-container show-interactions?] - (generic-wrapper-factory (svg-raw/svg-raw-shape shape-container) show-interactions?)) + [shape-container] + (generic-wrapper-factory (svg-raw/svg-raw-shape shape-container))) (defn rect-wrapper - [show-interactions?] - (generic-wrapper-factory rect/rect-shape show-interactions?)) + [] + (generic-wrapper-factory rect/rect-shape)) (defn image-wrapper - [show-interactions?] - (generic-wrapper-factory image/image-shape show-interactions?)) + [] + (generic-wrapper-factory image/image-shape)) (defn path-wrapper - [show-interactions?] - (generic-wrapper-factory path/path-shape show-interactions?)) + [] + (generic-wrapper-factory path/path-shape)) (defn text-wrapper - [show-interactions?] - (generic-wrapper-factory text/text-shape show-interactions?)) + [] + (generic-wrapper-factory text/text-shape)) (defn circle-wrapper - [show-interactions?] - (generic-wrapper-factory circle/circle-shape show-interactions?)) + [] + (generic-wrapper-factory circle/circle-shape)) (declare shape-container-factory) (defn frame-container-factory - [objects show-interactions?] - (let [shape-container (shape-container-factory objects show-interactions?) - frame-wrapper (frame-wrapper shape-container show-interactions?)] + [objects] + (let [shape-container (shape-container-factory objects) + frame-wrapper (frame-wrapper shape-container)] (mf/fnc frame-container {::mf/wrap-props false} [props] - (let [shape (obj/get props "shape") - childs (mapv #(get objects %) (:shapes shape)) - shape (geom/transform-shape shape) - props (obj/merge! #js {} props - #js {:shape shape - :childs childs - :objects objects - :show-interactions? show-interactions?})] + (let [shape (obj/get props "shape") + childs (mapv #(get objects %) (:shapes shape)) + shape (geom/transform-shape shape) + props (obj/merge! #js {} props + #js {:shape shape + :childs childs + :objects objects})] [:> frame-wrapper props])))) (defn group-container-factory - [objects show-interactions?] - (let [shape-container (shape-container-factory objects show-interactions?) - group-wrapper (group-wrapper shape-container show-interactions?)] + [objects] + (let [shape-container (shape-container-factory objects) + group-wrapper (group-wrapper shape-container)] (mf/fnc group-container {::mf/wrap-props false} [props] @@ -157,14 +311,27 @@ childs (mapv #(get objects %) (:shapes shape)) props (obj/merge! #js {} props #js {:childs childs - :objects objects - :show-interactions? show-interactions?})] + :objects objects})] [:> group-wrapper props])))) +(defn bool-container-factory + [objects] + (let [shape-container (shape-container-factory objects) + bool-wrapper (bool-wrapper shape-container)] + (mf/fnc bool-container + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + childs (select-keys objects (cp/get-children (:id shape) objects)) + props (obj/merge! #js {} props + #js {:childs childs + :objects objects})] + [:> bool-wrapper props])))) + (defn svg-raw-container-factory - [objects show-interactions?] - (let [shape-container (shape-container-factory objects show-interactions?) - svg-raw-wrapper (svg-raw-wrapper shape-container show-interactions?)] + [objects] + (let [shape-container (shape-container-factory objects) + svg-raw-wrapper (svg-raw-wrapper shape-container)] (mf/fnc svg-raw-container {::mf/wrap-props false} [props] @@ -172,26 +339,30 @@ childs (mapv #(get objects %) (:shapes shape)) props (obj/merge! #js {} props #js {:childs childs - :objects objects - :show-interactions? show-interactions?})] + :objects objects})] [:> svg-raw-wrapper props])))) (defn shape-container-factory - [objects show-interactions?] - (let [path-wrapper (path-wrapper show-interactions?) - text-wrapper (text-wrapper show-interactions?) - rect-wrapper (rect-wrapper show-interactions?) - image-wrapper (image-wrapper show-interactions?) - circle-wrapper (circle-wrapper show-interactions?)] + [objects] + (let [path-wrapper (path-wrapper) + text-wrapper (text-wrapper) + rect-wrapper (rect-wrapper) + image-wrapper (image-wrapper) + circle-wrapper (circle-wrapper)] (mf/fnc shape-container {::mf/wrap-props false} [props] - (let [group-container (mf/use-memo - (mf/deps objects) - #(group-container-factory objects show-interactions?)) - svg-raw-container (mf/use-memo - (mf/deps objects) - #(svg-raw-container-factory objects show-interactions?)) + (let [group-container + (mf/use-memo (mf/deps objects) + #(group-container-factory objects)) + + bool-container + (mf/use-memo (mf/deps objects) + #(bool-container-factory objects)) + + svg-raw-container + (mf/use-memo (mf/deps objects) + #(svg-raw-container-factory objects)) shape (unchecked-get props "shape") frame (unchecked-get props "frame")] (when (and shape (not (:hidden shape))) @@ -207,11 +378,12 @@ :image [:> image-wrapper opts] :circle [:> circle-wrapper opts] :group [:> group-container {:shape shape :frame frame :objects objects}] + :bool [:> bool-container {:shape shape :frame frame :objects objects}] :svg-raw [:> svg-raw-container {:shape shape :frame frame :objects objects}]))))))) (mf/defc frame-svg {::mf/wrap [mf/memo]} - [{:keys [objects frame zoom show-interactions?] :or {zoom 1} :as props}] + [{:keys [objects frame zoom] :or {zoom 1} :as props}] (let [modifier (-> (gpt/point (:x frame) (:y frame)) (gpt/negate) (gmt/translate-matrix)) @@ -229,7 +401,7 @@ " " (:height frame 0)) wrapper (mf/use-memo (mf/deps objects) - #(frame-container-factory objects show-interactions?))] + #(frame-container-factory objects))] [:svg {:view-box vbox :width width @@ -238,6 +410,5 @@ :xmlnsXlink "http://www.w3.org/1999/xlink" :xmlns "http://www.w3.org/2000/svg"} [:& wrapper {:shape frame - :show-interactions? show-interactions? :view-box vbox}]])) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index e2eaf6136..1ccc80d5b 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -121,7 +121,6 @@ (fn [] (st/emit! (dw/setup-layout layout-name)))) - (mf/use-effect (mf/deps project-id file-id) (fn [] diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index 7e71d5c4a..e0688ade1 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -36,9 +36,6 @@ (def picked-color-select (l/derived :picked-color-select refs/workspace-local)) -(def picked-shift? - (l/derived :picked-shift? refs/workspace-local)) - (def viewport (l/derived (l/in [:workspace-local :vport]) st/state)) @@ -202,10 +199,10 @@ h (str (* s 100) "%") (str (* l 100) "%")))] - (dom/set-css-property node "--color" (str/join ", " rgb)) - (dom/set-css-property node "--hue-rgb" (str/join ", " hue-rgb)) - (dom/set-css-property node "--saturation-grad-from" (format-hsl hsl-from)) - (dom/set-css-property node "--saturation-grad-to" (format-hsl hsl-to))))) + (dom/set-css-property! node "--color" (str/join ", " rgb)) + (dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb)) + (dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from)) + (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))))) ;; When closing the modal we update the recent-color list (mf/use-effect diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs index ed86777bd..00a0c7851 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs @@ -27,15 +27,27 @@ :v (mf/use-ref nil) :alpha (mf/use-ref nil)} + setup-hex-color + (fn [hex] + (let [[r g b] (uc/hex->rgb hex) + [h s v] (uc/hex->hsv hex)] + (on-change {:hex hex + :h h :s s :v v + :r r :g g :b b}))) on-change-hex (fn [e] (let [val (-> e dom/get-target-val parse-hex)] (when (uc/hex? val) - (let [[r g b] (uc/hex->rgb val) - [h s v] (uc/hex->hsv hex)] - (on-change {:hex val - :h h :s s :v v - :r r :g g :b b}))))) + (setup-hex-color val)))) + + on-blur-hex + (fn [e] + (let [val (-> e dom/get-target-val) + val (cond + (uc/color? val) (uc/parse-color val) + (uc/hex? (parse-hex val)) (parse-hex val))] + (when (some? val) + (setup-hex-color val)))) on-change-property (fn [property max-value] @@ -81,9 +93,10 @@ [:div.color-values {:class (when disable-opacity "disable-opacity")} [:input {:id "hex-value" - :ref (:hex refs) - :default-value hex - :on-change on-change-hex}] + :ref (:hex refs) + :default-value hex + :on-change on-change-hex + :on-blur on-blur-hex}] (if (= type :rgb) [:* diff --git a/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs b/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs index d4a2e1772..b886df4f1 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs @@ -115,7 +115,8 @@ :on-mouse-down #(reset! dragging? true) :on-mouse-up #(reset! dragging? false) :on-pointer-down (partial dom/capture-pointer) - :on-pointer-up (partial dom/release-pointer) + :on-lost-pointer-capture #(do (dom/release-pointer %) + (reset! dragging? false)) :on-click calculate-pos :on-mouse-move #(when @dragging? (calculate-pos %))}] [:div.handler {:style {:pointer-events "none" diff --git a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs index ea79bbd3c..8fbd1e534 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs @@ -26,7 +26,8 @@ {:on-mouse-down #(reset! dragging? true) :on-mouse-up #(reset! dragging? false) :on-pointer-down (partial dom/capture-pointer) - :on-pointer-up (partial dom/release-pointer) + :on-lost-pointer-capture #(do (dom/release-pointer %) + (reset! dragging? false)) :on-click calculate-pos :on-mouse-move #(when @dragging? (calculate-pos %))} [:div.handler {:style {:pointer-events "none" diff --git a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs index e0e630e43..4518decda 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs @@ -35,7 +35,8 @@ :on-mouse-down #(reset! dragging? true) :on-mouse-up #(reset! dragging? false) :on-pointer-down (partial dom/capture-pointer) - :on-pointer-up (partial dom/release-pointer) + :on-lost-pointer-capture #(do (dom/release-pointer %) + (reset! dragging? false)) :on-click calculate-pos :on-mouse-move #(when @dragging? (calculate-pos %))} diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 29180d9a1..9cdae0466 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -7,8 +7,10 @@ (ns app.main.ui.workspace.context-menu "A workspace specific context menu (mouse right click)." (:require + [app.common.types.page-options :as cto] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] + [app.main.data.workspace.interactions :as dwi] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.shortcuts :as sc] [app.main.data.workspace.undo :as dwu] @@ -16,6 +18,7 @@ [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.context :as ctx] + [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :refer [tr] :as i18n] [app.util.timers :as timers] @@ -31,10 +34,52 @@ (dom/stop-propagation event)) (mf/defc menu-entry - [{:keys [title shortcut on-click] :as props}] - [:li {:on-click on-click} - [:span.title title] - [:span.shortcut (or shortcut "")]]) + [{:keys [title shortcut on-click children] :as props}] + (let [submenu-ref (mf/use-ref nil) + hovering? (mf/use-ref false) + + on-pointer-enter + (mf/use-callback + (fn [] + (mf/set-ref-val! hovering? true) + (let [submenu-node (mf/ref-val submenu-ref)] + (when (some? submenu-node) + (dom/set-css-property! submenu-node "display" "block"))))) + + on-pointer-leave + (mf/use-callback + (fn [] + (mf/set-ref-val! hovering? false) + (let [submenu-node (mf/ref-val submenu-ref)] + (when (some? submenu-node) + (timers/schedule + 200 + #(when-not (mf/ref-val hovering?) + (dom/set-css-property! submenu-node "display" "none"))))))) + + set-dom-node + (mf/use-callback + (fn [dom] + (let [submenu-node (mf/ref-val submenu-ref)] + (when (and (some? dom) (some? submenu-node)) + (dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))] + + [:li {:ref set-dom-node + :on-click on-click + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave} + [:span.title title] + [:span.shortcut (or shortcut "")] + + (when (> (count children) 1) + [:span.submenu-icon i/arrow-slide]) + + (when (> (count children) 1) + [:ul.workspace-context-menu + {:ref submenu-ref + :style {:display "none" :left 250} + :on-context-menu prevent-default} + children])])) (mf/defc menu-separator [] @@ -42,16 +87,36 @@ (mf/defc shape-context-menu [{:keys [mdata] :as props}] - (let [{:keys [id] :as shape} (:shape mdata) - selected (:selected mdata) + (let [{:keys [shape selected disable-booleans? disable-flatten?]} mdata + {:keys [id type]} shape single? (= (count selected) 1) multiple? (> (count selected) 1) - editable-shape? (#{:group :text :path} (:type shape)) + editable-shape? (#{:group :text :path} type) + + is-group? (and (some? shape) (= :group type)) + is-bool? (and (some? shape) (= :bool type)) + + options (mf/deref refs/workspace-page-options) + flows (:flows options) + + options-mode (mf/deref refs/options-mode) + + set-bool + (fn [bool-type] + #(cond + (> (count selected) 1) + (st/emit! (dw/create-bool bool-type)) + + (and (= (count selected) 1) is-group?) + (st/emit! (dw/group-to-bool (:id shape) bool-type)) + + (and (= (count selected) 1) is-bool?) + (st/emit! (dw/change-bool-type (:id shape) bool-type)))) current-file-id (mf/use-ctx ctx/current-file-id) - do-duplicate (st/emitf dw/duplicate-selected) + do-duplicate (st/emitf (dw/duplicate-selected false)) do-delete (st/emitf dw/delete-selected) do-copy (st/emitf (dw/copy-selected)) do-cut (st/emitf (dw/copy-selected) dw/delete-selected) @@ -64,6 +129,8 @@ do-hide-shape (st/emitf (dw/update-shape-flags id {:hidden true})) do-lock-shape (st/emitf (dw/update-shape-flags id {:blocked true})) do-unlock-shape (st/emitf (dw/update-shape-flags id {:blocked false})) + do-add-flow (st/emitf (dwi/add-flow-selected-frame)) + do-remove-flow #(st/emitf (dwi/remove-flow (:id %))) do-create-group (st/emitf dw/group-selected) do-remove-group (st/emitf dw/ungroup-selected) do-mask-group (st/emitf dw/mask-group) @@ -98,7 +165,10 @@ :on-accept confirm-update-remote-component})) do-show-component (st/emitf (dw/go-to-layout :assets)) do-navigate-component-file (st/emitf (dwl/nav-to-component-file - (:component-file shape)))] + (:component-file shape))) + + do-transform-to-path (st/emitf (dw/convert-selected-to-path)) + do-flatten (st/emitf (dw/convert-selected-to-path))] [:* [:& menu-entry {:title (tr "workspace.shape.menu.copy") :shortcut (sc/get-tooltip :copy) @@ -147,7 +217,7 @@ :on-click do-flip-horizontal}] [:& menu-separator]]) - (when (and single? (= (:type shape) :group)) + (when (and single? (or is-bool? is-group?)) [:* [:& menu-entry {:title (tr "workspace.shape.menu.ungroup") :shortcut (sc/get-tooltip :ungroup) @@ -165,6 +235,32 @@ :shortcut (sc/get-tooltip :start-editing) :on-click do-start-editing}]) + (when-not disable-flatten? + [:& menu-entry {:title (tr "workspace.shape.menu.transform-to-path") + :on-click do-transform-to-path}]) + + (when (and (not disable-booleans?) + (or multiple? (and single? (or is-group? is-bool?)))) + [:& menu-entry {:title (tr "workspace.shape.menu.path")} + [:& menu-entry {:title (tr "workspace.shape.menu.union") + :shortcut (sc/get-tooltip :boolean-union) + :on-click (set-bool :union)}] + [:& menu-entry {:title (tr "workspace.shape.menu.difference") + :shortcut (sc/get-tooltip :boolean-difference) + :on-click (set-bool :difference)}] + [:& menu-entry {:title (tr "workspace.shape.menu.intersection") + :shortcut (sc/get-tooltip :boolean-intersection) + :on-click (set-bool :intersection)}] + [:& menu-entry {:title (tr "workspace.shape.menu.exclude") + :shortcut (sc/get-tooltip :boolean-exclude) + :on-click (set-bool :exclude)}] + + (when (and single? is-bool? (not disable-flatten?)) + [:* + [:& menu-separator] + [:& menu-entry {:title (tr "workspace.shape.menu.flatten") + :on-click do-flatten}]])]) + (if (:hidden shape) [:& menu-entry {:title (tr "workspace.shape.menu.show") :on-click do-show-shape}] @@ -177,9 +273,15 @@ [:& menu-entry {:title (tr "workspace.shape.menu.lock") :on-click do-lock-shape}]) - (when (and (or (nil? (:shape-ref shape)) - (> (count selected) 1)) - (not= (:type shape) :frame)) + (when (and (= options-mode :prototype) (= (:type shape) :frame)) + (let [flow (cto/get-frame-flow flows (:id shape))] + (if (nil? flow) + [:& menu-entry {:title (tr "workspace.shape.menu.flow-start") + :on-click do-add-flow}] + [:& menu-entry {:title (tr "workspace.shape.menu.delete-flow-start") + :on-click (do-remove-flow flow)}]))) + + (when (not= (:type shape) :frame) [:* [:& menu-separator] [:& menu-entry {:title (tr "workspace.shape.menu.create-component") @@ -240,7 +342,7 @@ (when dropdown (let [bounding-rect (dom/get-bounding-rect dropdown) window-size (dom/get-window-size) - delta-x (max (- (:right bounding-rect) (:width window-size)) 0) + delta-x (max (- (+ (:right bounding-rect) 250) (:width window-size)) 0) delta-y (max (- (:bottom bounding-rect) (:height window-size)) 0) new-style (str "top: " (- top delta-y) "px; " "left: " (- left delta-x) "px;")] diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index b310d2118..70efd1f49 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -20,6 +20,7 @@ [app.main.ui.shapes.image :as image] [app.main.ui.shapes.rect :as rect] [app.main.ui.shapes.text.fontfaces :as ff] + [app.main.ui.workspace.shapes.bool :as bool] [app.main.ui.workspace.shapes.bounding-box :refer [bounding-box]] [app.main.ui.workspace.shapes.common :as common] [app.main.ui.workspace.shapes.frame :as frame] @@ -35,6 +36,7 @@ (declare shape-wrapper) (declare group-wrapper) (declare svg-raw-wrapper) +(declare bool-wrapper) (declare frame-wrapper) (def circle-wrapper (common/generic-wrapper-factory circle/circle-shape)) @@ -92,13 +94,14 @@ [:* (if-not svg-element? (case (:type shape) - :path [:> path/path-wrapper opts] - :text [:> text/text-wrapper opts] - :group [:> group-wrapper opts] - :rect [:> rect-wrapper opts] - :image [:> image-wrapper opts] - :circle [:> circle-wrapper opts] + :path [:> path/path-wrapper opts] + :text [:> text/text-wrapper opts] + :group [:> group-wrapper opts] + :rect [:> rect-wrapper opts] + :image [:> image-wrapper opts] + :circle [:> circle-wrapper opts] :svg-raw [:> svg-raw-wrapper opts] + :bool [:> bool-wrapper opts] ;; Only used when drawing a new frame. :frame [:> frame-wrapper {:shape shape}] @@ -113,5 +116,6 @@ (def group-wrapper (group/group-wrapper-factory shape-wrapper)) (def svg-raw-wrapper (svg-raw/svg-raw-wrapper-factory shape-wrapper)) +(def bool-wrapper (bool/bool-wrapper-factory shape-wrapper)) (def frame-wrapper (frame/frame-wrapper-factory shape-wrapper)) diff --git a/frontend/src/app/main/ui/workspace/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/shapes/bool.cljs new file mode 100644 index 000000000..2cc8f6e00 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/bool.cljs @@ -0,0 +1,47 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.workspace.shapes.bool + (:require + [app.main.data.workspace :as dw] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.streams :as ms] + [app.main.ui.shapes.bool :as bool] + [app.main.ui.shapes.shape :refer [shape-container]] + [app.util.dom :as dom] + [rumext.alpha :as mf])) + +(defn use-double-click [{:keys [id]}] + (mf/use-callback + (mf/deps id) + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + (st/emit! (dw/select-inside-group id @ms/mouse-position))))) + +(defn bool-wrapper-factory + [shape-wrapper] + (let [shape-component (bool/bool-shape shape-wrapper)] + (mf/fnc bool-wrapper + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))] + ::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + frame (unchecked-get props "frame") + + childs-ref (mf/use-memo + (mf/deps (:id shape)) + #(refs/select-children (:id shape))) + + childs (mf/deref childs-ref)] + + [:> shape-container {:shape shape} + [:& shape-component + {:frame frame + :shape shape + :childs childs}]])))) + diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index bae6a5d99..a245d5bb2 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -6,11 +6,11 @@ (ns app.main.ui.workspace.shapes.path (:require + [app.common.path.commands :as upc] [app.main.refs :as refs] [app.main.ui.shapes.path :as path] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.workspace.shapes.path.common :as pc] - [app.util.path.commands :as upc] [rumext.alpha :as mf])) (mf/defc path-wrapper diff --git a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs index aa683f708..0330f101f 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -8,7 +8,9 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] - [app.common.geom.shapes.path :as gshp] + [app.common.geom.shapes.path :as gsp] + [app.common.path.commands :as upc] + [app.common.path.shapes-to-path :as ups] [app.main.data.workspace.path :as drp] [app.main.snap :as snap] [app.main.store :as st] @@ -18,10 +20,7 @@ [app.main.ui.workspace.shapes.path.common :as pc] [app.util.dom :as dom] [app.util.keyboard :as kbd] - [app.util.path.commands :as upc] [app.util.path.format :as upf] - [app.util.path.geom :as upg] - [app.util.path.shapes-to-path :as ups] [clojure.set :refer [map-invert]] [goog.events :as events] [rumext.alpha :as mf]) @@ -217,16 +216,16 @@ shape (cond-> shape (not= :path (:type shape)) - ups/convert-to-path + (ups/convert-to-path {}) :always hooks/use-equal-memo) base-content (:content shape) - base-points (mf/use-memo (mf/deps base-content) #(->> base-content upg/content->points)) + base-points (mf/use-memo (mf/deps base-content) #(->> base-content gsp/content->points)) content (upc/apply-content-modifiers base-content content-modifiers) - content-points (mf/use-memo (mf/deps content) #(->> content upg/content->points)) + content-points (mf/use-memo (mf/deps content) #(->> content gsp/content->points)) point->base (->> (map hash-map content-points base-points) (reduce merge)) base->point (map-invert point->base) @@ -269,10 +268,14 @@ ms/mouse-position (mf/deps shape zoom) (fn [position] - (when-let [point (gshp/path-closest-point shape position)] + (when-let [point (gsp/path-closest-point shape position)] (reset! hover-point (when (< (gpt/distance position point) (/ 10 zoom)) point))))) [:g.path-editor {:ref editor-ref} + [:path {:d (upf/format-path content) + :style {:fill "none" + :stroke pc/primary-color + :strokeWidth (/ 1 zoom)}}] (when (and preview (not drag-handler)) [:& path-preview {:command preview :from last-p diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 6f3b652ce..704920fba 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.shapes.text (:require + [app.common.logging :as log] [app.common.math :as mth] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] @@ -13,7 +14,6 @@ [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.text :as text] [app.util.dom :as dom] - [app.util.logging :as log] [app.util.object :as obj] [app.util.text-editor :as ted] [app.util.timers :as timers] diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index db9d70aad..d00ea37bd 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -174,7 +174,7 @@ handle-return (mf/use-callback (fn [_ state] - (let [style (ted/get-editor-current-inline-styles state) + (let [style (ted/get-editor-current-block-data state) state (-> (ted/insert-text state "\n" style) (handle-change))] (st/emit! (dwt/update-editor-state shape state))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index c759464b9..16889f759 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -230,7 +230,7 @@ on-fold-group (mf/use-callback - (mf/deps group-open?) + (mf/deps file-id box path group-open?) (fn [event] (dom/stop-propagation event) (st/emit! (dwl/set-assets-group-open file-id diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 0f147b1f2..928a3e9d8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -19,6 +19,7 @@ [app.util.keyboard :as kbd] [app.util.object :as obj] [app.util.timers :as ts] + [cuerdas.core :as str] [okulary.core :as l] [rumext.alpha :as mf])) @@ -39,6 +40,11 @@ (if (:masked-group? shape) i/mask i/folder)) + :bool (case (:bool-type shape) + :difference i/boolean-difference + :exclude i/boolean-exclude + :intersection i/boolean-intersection + #_:default i/boolean-union) :svg-raw i/file-svg nil)) @@ -63,7 +69,8 @@ (on-stop-edit) (swap! local assoc :edition false) (st/emit! (dw/end-rename-shape) - (dw/update-shape (:id shape) {:name name})))) + (when-not (str/empty? name) + (dw/update-shape (:id shape) {:name name}))))) cancel-edit (fn [] (on-stop-edit) @@ -292,7 +299,8 @@ :shape-ref :touched :metadata - :masked-group?])) + :masked-group? + :bool-type])) (defn- strip-objects [objects] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 89248b58b..e0a62cbff 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -11,10 +11,12 @@ [app.main.store :as st] [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.context :as ctx] - [app.main.ui.workspace.sidebar.align :refer [align-options]] + [app.main.ui.workspace.sidebar.options.menus.align :refer [align-options]] + [app.main.ui.workspace.sidebar.options.menus.booleans :refer [booleans-options]] [app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-menu]] [app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu]] [app.main.ui.workspace.sidebar.options.page :as page] + [app.main.ui.workspace.sidebar.options.shapes.bool :as bool] [app.main.ui.workspace.sidebar.options.shapes.circle :as circle] [app.main.ui.workspace.sidebar.options.shapes.frame :as frame] [app.main.ui.workspace.sidebar.options.shapes.group :as group] @@ -43,6 +45,7 @@ :path [:& path/options {:shape shape}] :image [:& image/options {:shape shape}] :svg-raw [:& svg-raw/options {:shape shape}] + :bool [:& bool/options {:shape shape}] nil) [:& exports-menu {:shape shape @@ -60,6 +63,7 @@ :title (tr "workspace.options.design")} [:div.element-options [:& align-options] + [:& booleans-options] (case (count selected) 0 [:& page/options] 1 [:& shape-options {:shape (first shapes) diff --git a/frontend/src/app/main/ui/workspace/sidebar/align.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.cljs similarity index 98% rename from frontend/src/app/main/ui/workspace/sidebar/align.cljs rename to frontend/src/app/main/ui/workspace/sidebar/options/menus/align.cljs index acb3c6a42..7b32c969a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/align.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.workspace.sidebar.align +(ns app.main.ui.workspace.sidebar.options.menus.align (:require [app.common.uuid :as uuid] [app.main.data.workspace :as dw] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs new file mode 100644 index 000000000..80b2be852 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs @@ -0,0 +1,87 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.workspace.sidebar.options.menus.booleans + (:require + [app.main.data.workspace :as dw] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) + +(mf/defc booleans-options + [] + (let [selected (mf/deref refs/selected-objects) + selected-with-children (mf/deref refs/selected-objects-with-children) + + has-invalid-shapes? (->> selected-with-children + (some (comp #{:frame :text} :type))) + + first-not-group-like? + (and (= (count selected) 1) + (not (contains? #{:group :bool} (:type (first selected))))) + + disabled-bool-btns (or (empty? selected) has-invalid-shapes? first-not-group-like?) + disabled-flatten (or (empty? selected) has-invalid-shapes?) + + head (first selected) + is-group? (and (some? head) (= :group (:type head))) + is-bool? (and (some? head) (= :bool (:type head))) + head-bool-type (and (some? head) is-bool? (:bool-type head)) + + set-bool + (fn [bool-type] + #(cond + (> (count selected) 1) + (st/emit! (dw/create-bool bool-type)) + + (and (= (count selected) 1) is-group?) + (st/emit! (dw/group-to-bool (:id head) bool-type)) + + (and (= (count selected) 1) is-bool?) + (if (= head-bool-type bool-type) + (st/emit! (dw/bool-to-group (:id head))) + (st/emit! (dw/change-bool-type (:id head) bool-type)))))] + + [:div.align-options + [:div.align-group + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.union") + :class (dom/classnames :disabled disabled-bool-btns + :selected (= head-bool-type :union)) + :on-click (set-bool :union)} + i/boolean-union] + + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.difference") + :class (dom/classnames :disabled disabled-bool-btns + :selected (= head-bool-type :difference)) + :on-click (set-bool :difference)} + i/boolean-difference] + + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.intersection") + :class (dom/classnames :disabled disabled-bool-btns + :selected (= head-bool-type :intersection)) + :on-click (set-bool :intersection)} + i/boolean-intersection] + + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.exclude") + :class (dom/classnames :disabled disabled-bool-btns + :selected (= head-bool-type :exclude)) + :on-click (set-bool :exclude)} + i/boolean-exclude]] + + [:div.align-group + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.flatten") + :class (dom/classnames :disabled disabled-flatten) + :on-click (st/emitf (dw/convert-selected-to-path))} + i/boolean-flatten]]])) + diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index 500b5e038..881b95236 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -8,70 +8,396 @@ (:require [app.common.data :as d] [app.common.pages :as cp] + [app.common.types.interactions :as cti] + [app.common.types.page-options :as cto] + [app.common.uuid :as uuid] [app.main.data.workspace :as dw] + [app.main.data.workspace.interactions :as dwi] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.components.numeric-input :refer [numeric-input]] [app.main.ui.icons :as i] + [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] + [cuerdas.core :as str] + [okulary.core :as l] [rumext.alpha :as mf])) -(mf/defc interactions-menu - [{:keys [shape] :as props}] - (let [objects (deref refs/workspace-page-objects) - interaction (first (:interactions shape)) ; TODO: in the - ; future we may - ; have several - ; interactions in - ; one shape +(defn- event-type-names + [] + {:click (tr "workspace.options.interaction-on-click") + ; TODO: need more UX research + ;; :mouse-over (tr "workspace.options.interaction-while-hovering") + ;; :mouse-press (tr "workspace.options.interaction-while-pressing") + :mouse-enter (tr "workspace.options.interaction-mouse-enter") + :mouse-leave (tr "workspace.options.interaction-mouse-leave") + :after-delay (tr "workspace.options.interaction-after-delay")}) +(defn- event-type-name + [interaction] + (get (event-type-names) (:event-type interaction) "--")) + +(defn- action-type-names + [] + {:navigate (tr "workspace.options.interaction-navigate-to") + :open-overlay (tr "workspace.options.interaction-open-overlay") + :toggle-overlay (tr "workspace.options.interaction-toggle-overlay") + :close-overlay (tr "workspace.options.interaction-close-overlay") + :prev-screen (tr "workspace.options.interaction-prev-screen") + :open-url (tr "workspace.options.interaction-open-url")}) + +(defn- action-summary + [interaction destination] + (case (:action-type interaction) + :navigate (tr "workspace.options.interaction-navigate-to-dest" + (get destination :name (tr "workspace.options.interaction-none"))) + :open-overlay (tr "workspace.options.interaction-open-overlay-dest" + (get destination :name (tr "workspace.options.interaction-none"))) + :toggle-overlay (tr "workspace.options.interaction-toggle-overlay-dest" + (get destination :name (tr "workspace.options.interaction-none"))) + :close-overlay (tr "workspace.options.interaction-close-overlay-dest" + (get destination :name (tr "workspace.options.interaction-self"))) + :prev-screen (tr "workspace.options.interaction-prev-screen") + :open-url (tr "workspace.options.interaction-open-url") + "--")) + +(defn- overlay-pos-type-names + [] + {:manual (tr "workspace.options.interaction-pos-manual") + :center (tr "workspace.options.interaction-pos-center") + :top-left (tr "workspace.options.interaction-pos-top-left") + :top-right (tr "workspace.options.interaction-pos-top-right") + :top-center (tr "workspace.options.interaction-pos-top-center") + :bottom-left (tr "workspace.options.interaction-pos-bottom-left") + :bottom-right (tr "workspace.options.interaction-pos-bottom-right") + :bottom-center (tr "workspace.options.interaction-pos-bottom-center")}) + +(def flow-for-rename-ref + (l/derived (l/in [:workspace-local :flow-for-rename]) st/state)) + +(mf/defc flow-item + [{:keys [flow]}] + (let [editing? (mf/use-state false) + flow-for-rename (mf/deref flow-for-rename-ref) + name-ref (mf/use-ref) + + start-edit (fn [] + (reset! editing? true)) + + accept-edit (fn [] + (let [name-input (mf/ref-val name-ref) + name (dom/get-value name-input)] + (reset! editing? false) + (st/emit! (dwi/end-rename-flow) + (when-not (str/empty? name) + (dwi/rename-flow (:id flow) name))))) + + cancel-edit (fn [] + (reset! editing? false) + (st/emit! (dwi/end-rename-flow))) + + on-key-down (fn [event] + (when (kbd/enter? event) (accept-edit)) + (when (kbd/esc? event) (cancel-edit)))] + + (mf/use-effect + (fn [] + #(when editing? + (cancel-edit)))) + + (mf/use-effect + (mf/deps flow-for-rename) + #(when (and (= flow-for-rename (:id flow)) + (not @editing?)) + (start-edit))) + + (mf/use-effect + (mf/deps @editing?) + #(when @editing? + (let [name-input (mf/ref-val name-ref)] + (dom/select-text! name-input)) + nil)) + + [:div.flow-element + [:div.flow-button {:on-click (st/emitf (dw/select-shape (:starting-frame flow)))} + i/play] + (if @editing? + [:input.element-name + {:type "text" + :ref name-ref + :on-blur accept-edit + :on-key-down on-key-down + :auto-focus true + :default-value (:name flow "")}] + [:span.element-label.flow-name + {:on-double-click (st/emitf (dwi/start-rename-flow (:id flow)))} + (:name flow)]) + [:div.add-page {:on-click (st/emitf (dwi/remove-flow (:id flow)))} + i/minus]])) + +(mf/defc page-flows + [{:keys [flows]}] + (when (seq flows) + [:div.element-set.interactions-options + [:div.element-set-title + [:span (tr "workspace.options.flows.flow-starts")]] + (for [flow flows] + [:& flow-item {:flow flow :key (str (:id flow))}])])) + +(mf/defc shape-flows + [{:keys [flows shape]}] + (when (= (:type shape) :frame) + (let [flow (cto/get-frame-flow flows (:id shape))] + [:div.element-set.interactions-options + [:div.element-set-title + [:span (tr "workspace.options.flows.flow-start")]] + (if (nil? flow) + [:div.flow-element + [:span.element-label (tr "workspace.options.flows.add-flow-start")] + [:div.add-page {:on-click (st/emitf (dwi/add-flow-selected-frame))} + i/plus]] + [:& flow-item {:flow flow :key (str (:id flow))}])]))) + +(mf/defc interaction-entry + [{:keys [index shape interaction update-interaction remove-interaction]}] + (let [objects (deref refs/workspace-page-objects) destination (get objects (:destination interaction)) frames (mf/use-memo (mf/deps objects) #(cp/select-frames objects)) - show-frames-dropdown? (mf/use-state false) + overlay-pos-type (:overlay-pos-type interaction) + close-click-outside? (:close-click-outside interaction false) + background-overlay? (:background-overlay interaction false) - on-set-blur #(reset! show-frames-dropdown? false) - on-navigate #(when destination - (st/emit! (dw/select-shapes (d/ordered-set (:id destination))))) + extended-open? (mf/use-state false) - on-select-destination - (fn [dest] - (if (nil? dest) - (st/emit! (dw/update-shape (:id shape) {:interactions []})) - (st/emit! (dw/update-shape (:id shape) {:interactions [{:event-type :click - :action-type :navigate - :destination dest}]}))))] + ext-delay-ref (mf/use-ref nil) - (if (not shape) - [:* - [:div.interactions-help-icon i/interaction] - [:div.interactions-help (tr "workspace.options.select-a-shape")] - [:div.interactions-help-icon i/play] - [:div.interactions-help (tr "workspace.options.use-play-button")]] + select-text + (fn [ref] (fn [_] (dom/select-text! (mf/ref-val ref)))) - [:div.element-set {:on-blur on-set-blur} - [:div.element-set-title - [:span (tr "workspace.options.navigate-to")]] - [:div.element-set-content - [:div.row-flex - [:div.custom-select.flex-grow {:on-click #(reset! show-frames-dropdown? true)} - (if destination - [:span (:name destination)] - [:span (tr "workspace.options.select-artboard")]) - [:span.dropdown-button i/arrow-down] - [:& dropdown {:show @show-frames-dropdown? - :on-close #(reset! show-frames-dropdown? false)} - [:ul.custom-select-dropdown - [:li.dropdown-separator - {:on-click #(on-select-destination nil)} - (tr "workspace.options.none")] + change-event-type + (fn [event] + (let [value (-> event dom/get-target dom/get-value d/read-string)] + (update-interaction index #(cti/set-event-type % value shape)))) + change-action-type + (fn [event] + (let [value (-> event dom/get-target dom/get-value d/read-string)] + (update-interaction index #(cti/set-action-type % value)))) + + change-delay + (fn [value] + (update-interaction index #(cti/set-delay % value))) + + change-destination + (fn [event] + (let [value (-> event dom/get-target dom/get-value) + value (when (not= value "") (uuid/uuid value))] + (update-interaction index #(cti/set-destination % value)))) + + change-url + (fn [event] + (let [value (-> event dom/get-target dom/get-value)] + (update-interaction index #(cti/set-url % value)))) + + change-overlay-pos-type + (fn [event] + (let [value (-> event dom/get-target dom/get-value d/read-string)] + (update-interaction index #(cti/set-overlay-pos-type % value shape objects)))) + + toggle-overlay-pos-type + (fn [pos-type] + (update-interaction index #(cti/toggle-overlay-pos-type % pos-type shape objects))) + + change-close-click-outside + (fn [event] + (let [value (-> event dom/get-target dom/checked?)] + (update-interaction index #(cti/set-close-click-outside % value)))) + + change-background-overlay + (fn [event] + (let [value (-> event dom/get-target dom/checked?)] + (update-interaction index #(cti/set-background-overlay % value))))] + + [:* + [:div.element-set-options-group {:class (dom/classnames + :open @extended-open?)} + + ; Summary + [:div.element-set-actions-button {:on-click #(swap! extended-open? not)} + i/actions] + [:div.interactions-summary {:on-click #(swap! extended-open? not)} + [:div.trigger-name (event-type-name interaction)] + [:div.action-summary (action-summary interaction destination)]] + [:div.elemen-set-actions {:on-click #(remove-interaction index)} + [:div.element-set-actions-button i/minus]] + + (when @extended-open? + [:div.element-set-content + + ; Trigger select + [:div.interactions-element.separator + [:span.element-set-subtitle.wide (tr "workspace.options.interaction-trigger")] + [:select.input-select + {:value (str (:event-type interaction)) + :on-change change-event-type} + (for [[value name] (event-type-names)] + (when-not (and (= value :after-delay) + (not= (:type shape) :frame)) + [:option {:value (str value)} name]))]] + + ; Delay + (when (cti/has-delay interaction) + [:div.interactions-element + [:span.element-set-subtitle.wide (tr "workspace.options.interaction-delay")] + [:div.input-element + [:> numeric-input {:ref ext-delay-ref + :on-click (select-text ext-delay-ref) + :on-change change-delay + :value (:delay interaction)}] + [:span.after (tr "workspace.options.interaction-ms")]]]) + + ; Action select + [:div.interactions-element.separator + [:span.element-set-subtitle.wide (tr "workspace.options.interaction-action")] + [:select.input-select + {:value (str (:action-type interaction)) + :on-change change-action-type} + (for [[value name] (action-type-names)] + [:option {:value (str value)} name])]] + + ; Destination + (when (cti/has-destination interaction) + [:div.interactions-element + [:span.element-set-subtitle.wide (tr "workspace.options.interaction-destination")] + [:select.input-select + {:value (str (:destination interaction)) + :on-change change-destination} + (if (= (:action-type interaction) :close-overlay) + [:option {:value ""} (tr "workspace.options.interaction-self")] + [:option {:value ""} (tr "workspace.options.interaction-none")]) (for [frame frames] (when (and (not= (:id frame) (:id shape)) ; A frame cannot navigate to itself (not= (:id frame) (:frame-id shape))) ; nor a shape to its container frame - [:li {:key (:id frame) - :on-click #(on-select-destination (:id frame))} - (:name frame)]))]]] - [:span.navigate-icon {:style {:visibility (when (not destination) "hidden")} - :on-click on-navigate} i/navigate]]]]))) + [:option {:value (str (:id frame))} (:name frame)]))]]) + + ; URL + (when (cti/has-url interaction) + [:div.interactions-element + [:span.element-set-subtitle.wide (tr "workspace.options.interaction-url")] + [:input.input-text {:default-value (str (:url interaction)) + :on-blur change-url}]]) + + (when (cti/has-overlay-opts interaction) + [:* + ; Overlay position (select) + [:div.interactions-element + [:span.element-set-subtitle.wide (tr "workspace.options.interaction-position")] + [:select.input-select + {:value (str (:overlay-pos-type interaction)) + :on-change change-overlay-pos-type} + (for [[value name] (overlay-pos-type-names)] + [:option {:value (str value)} name])]] + + ; Overlay position (buttons) + [:div.interactions-element.interactions-pos-buttons + [:div.element-set-actions-button + {:class (dom/classnames :active (= overlay-pos-type :center)) + :on-click #(toggle-overlay-pos-type :center)} + i/position-center] + [:div.element-set-actions-button + {:class (dom/classnames :active (= overlay-pos-type :top-left)) + :on-click #(toggle-overlay-pos-type :top-left)} + i/position-top-left] + [:div.element-set-actions-button + {:class (dom/classnames :active (= overlay-pos-type :top-right)) + :on-click #(toggle-overlay-pos-type :top-right)} + i/position-top-right] + [:div.element-set-actions-button + {:class (dom/classnames :active (= overlay-pos-type :top-center)) + :on-click #(toggle-overlay-pos-type :top-center)} + i/position-top-center] + [:div.element-set-actions-button + {:class (dom/classnames :active (= overlay-pos-type :bottom-left)) + :on-click #(toggle-overlay-pos-type :bottom-left)} + i/position-bottom-left] + [:div.element-set-actions-button + {:class (dom/classnames :active (= overlay-pos-type :bottom-right)) + :on-click #(toggle-overlay-pos-type :bottom-right)} + i/position-bottom-right] + [:div.element-set-actions-button + {:class (dom/classnames :active (= overlay-pos-type :bottom-center)) + :on-click #(toggle-overlay-pos-type :bottom-center)} + i/position-bottom-center]] + + ; Overlay click outside + [:div.interactions-element + [:div.input-checkbox + [:input {:type "checkbox" + :id (str "close-" index) + :checked close-click-outside? + :on-change change-close-click-outside}] + [:label {:for (str "close-" index)} + (tr "workspace.options.interaction-close-outside")]]] + + ; Overlay background + [:div.interactions-element + [:div.input-checkbox + [:input {:type "checkbox" + :id (str "background-" index) + :checked background-overlay? + :on-change change-background-overlay}] + [:label {:for (str "background-" index)} + (tr "workspace.options.interaction-background")]]]])])]])) + +(mf/defc interactions-menu + [{:keys [shape] :as props}] + (let [interactions (get shape :interactions []) + + options (mf/deref refs/workspace-page-options) + flows (:flows options) + + add-interaction + (fn [] + (st/emit! (dwi/add-new-interaction shape))) + + remove-interaction + (fn [index] + (st/emit! (dwi/remove-interaction shape index))) + + update-interaction + (fn [index update-fn] + (st/emit! (dwi/update-interaction shape index update-fn)))] + [:* + (if shape + [:& shape-flows {:flows flows + :shape shape}] + [:& page-flows {:flows flows}]) + + [:div.element-set.interactions-options + (when (and shape (not (cp/unframed-shape? shape))) + [:div.element-set-title + [:span (tr "workspace.options.interactions")] + [:div.add-page {:on-click add-interaction} + i/plus]]) + [:div.element-set-content + (when (= (count interactions) 0) + [:* + (when (and shape (not (cp/unframed-shape? shape))) + [:* + [:div.interactions-help-icon i/plus] + [:div.interactions-help.separator (tr "workspace.options.add-interaction")]]) + [:div.interactions-help-icon i/interaction] + [:div.interactions-help (tr "workspace.options.select-a-shape")] + [:div.interactions-help-icon i/play] + [:div.interactions-help (tr "workspace.options.use-play-button")]])] + [:div.groups + (for [[index interaction] (d/enumerate interactions)] + [:& interaction-entry {:index index + :shape shape + :interaction interaction + :update-interaction update-interaction + :remove-interaction remove-interaction}])]]])) + diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index a5698e4c0..f0bd78126 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -73,7 +73,7 @@ :group (tr "workspace.options.group-stroke") (tr "workspace.options.stroke")) - show-options (not= (:stroke-style values :none) :none) + show-options (not= (or (:stroke-style values) :none) :none) show-caps (and show-caps (not (#{:inner :outer} (:stroke-alignment values)))) @@ -102,7 +102,10 @@ (mf/use-callback (mf/deps ids) (fn [] - (st/emit! (dc/change-stroke ids (dissoc current-stroke-color :id :file-id))))) + (let [remove-multiple (fn [[_ value]] (not= value :multiple)) + current-stroke-color (-> (into {} (filter remove-multiple) current-stroke-color) + (assoc :id nil :file-id nil))] + (st/emit! (dc/change-stroke ids current-stroke-color))))) on-stroke-style-change (fn [event] @@ -141,7 +144,10 @@ target (dom/get-current-target event) rect (dom/get-bounding-rect target) - top (+ (:bottom rect) 5) + top (if (< (+ (:bottom rect) 320) (:height window-size)) + (+ (:bottom rect) 5) + (- (:height window-size) 325)) + left (if (< (+ (:left rect) 200) (:width window-size)) (:left rect) (- (:width window-size) 205))] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 45f313c73..b51955e96 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -74,7 +74,8 @@ (defn filter-fonts [{:keys [term backends]} fonts] - (let [xform (cond-> (map identity) + (let [term (str/lower term) + xform (cond-> (map identity) (seq term) (comp (filter #(str/includes? (str/lower (:name %)) term))) @@ -175,7 +176,7 @@ [:div.font-selector [:div.font-selector-dropdown [:header - [:input {:placeholder "Search font" + [:input {:placeholder (tr "workspace.options.search-font") :value (:term @state) :ref input :spell-check false diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs new file mode 100644 index 000000000..dc5a8fa8c --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs @@ -0,0 +1,45 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.workspace.sidebar.options.shapes.bool + (:require + [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] + [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] + [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] + [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] + [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] + [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] + [rumext.alpha :as mf])) + +(mf/defc options + [{:keys [shape] :as props}] + (let [ids [(:id shape)] + type (:type shape) + measure-values (select-keys shape measure-attrs) + stroke-values (select-keys shape stroke-attrs) + layer-values (select-keys shape layer-attrs) + constraint-values (select-keys shape constraint-attrs)] + [:* + [:& measures-menu {:ids ids + :type type + :values measure-values}] + [:& constraints-menu {:ids ids + :values constraint-values}] + [:& layer-menu {:ids ids + :type type + :values layer-values}] + [:& fill-menu {:ids ids + :type type + :values (select-keys shape fill-attrs)}] + [:& stroke-menu {:ids ids + :type type + :show-caps true + :values stroke-values}] + [:& shadow-menu {:ids ids + :values (select-keys shape [:shadow])}] + [:& blur-menu {:ids ids + :values (select-keys shape [:blur])}]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index 07dbf09fb..bbad1e169 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -93,6 +93,16 @@ :text :ignore} :svg-raw + {:measure :shape + :layer :shape + :constraint :shape + :fill :shape + :shadow :shape + :blur :shape + :stroke :shape + :text :ignore} + + :bool {:measure :shape :layer :shape :constraint :shape diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index c989136b5..53d82208a 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -74,6 +74,7 @@ cursor (mf/use-state (utils/get-cursor :pointer-inner)) hover-ids (mf/use-state nil) hover (mf/use-state nil) + hover-disabled? (mf/use-state false) frame-hover (mf/use-state nil) active-frames (mf/use-state {}) @@ -153,13 +154,14 @@ (hooks/setup-cursor cursor alt? panning drawing-tool drawing-path? node-editing?) (hooks/setup-resize layout viewport-ref) (hooks/setup-keyboard alt? ctrl? space?) - (hooks/setup-hover-shapes page-id move-stream objects transform selected ctrl? hover hover-ids zoom) + (hooks/setup-hover-shapes page-id move-stream objects transform selected ctrl? hover hover-ids @hover-disabled? zoom) (hooks/setup-viewport-modifiers modifiers selected objects render-ref) (hooks/setup-shortcuts node-editing? drawing-path?) (hooks/setup-active-frames objects vbox hover active-frames) [:div.viewport [:div.viewport-overlays + [:& wtr/frame-renderer {:objects objects :background background}] @@ -195,11 +197,12 @@ [:& use/export-page {:options options}] - [:& (mf/provider embed/context) {:value true} - ;; Render root shape - [:& shapes/root-shape {:key page-id - :objects objects - :active-frames @active-frames}]]] + [:& (mf/provider use/include-metadata-ctx) {:value false} + [:& (mf/provider embed/context) {:value true} + ;; Render root shape + [:& shapes/root-shape {:key page-id + :objects objects + :active-frames @active-frames}]]]] [:svg.viewport-controls {:xmlns "http://www.w3.org/2000/svg" @@ -228,7 +231,6 @@ :on-pointer-up on-pointer-up} [:g {:style {:pointer-events (if disable-events? "none" "auto")}} - (when show-outlines? [:& outline/shape-outlines {:objects objects @@ -267,6 +269,17 @@ :on-frame-leave on-frame-leave :on-frame-select on-frame-select}] + (when show-prototypes? + [:& widgets/frame-flows + {:flows (:flows options) + :objects objects + :selected selected + :zoom zoom + :modifiers modifiers + :on-frame-enter on-frame-enter + :on-frame-leave on-frame-leave + :on-frame-select on-frame-select}]) + (when show-gradient-handlers? [:& gradients/gradient-handlers {:id (first selected) @@ -320,7 +333,8 @@ (when show-prototypes? [:& interactions/interactions - {:selected selected}]) + {:selected selected + :hover-disabled? hover-disabled?}]) (when show-selrect? [:& widgets/selection-rect {:data selrect diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 8ee77b05a..b5427fb4e 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -363,12 +363,14 @@ delta-y (-> (.-deltaY ^js event) (* unit) (/ zoom)) + delta-x (-> (.-deltaX ^js event) (* unit) (/ zoom))] (dom/prevent-default event) (dom/stop-propagation event) - (if (kbd/shift? event) + (if (and (not (cfg/check-platform? :macos)) ;; macos sends delta-x automaticaly, don't need to do it + (kbd/shift? event)) (st/emit! (dw/update-viewport-position {:x #(+ % delta-y)})) (st/emit! (dw/update-viewport-position {:x #(+ % delta-x) :y #(+ % delta-y)}))))))))) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 9bebb02bd..b9df6b983 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -89,36 +89,46 @@ (hooks/use-stream ms/keyboard-ctrl #(reset! ctrl? %)) (hooks/use-stream ms/keyboard-space #(reset! space? %))) -(defn setup-hover-shapes [page-id move-stream objects transform selected ctrl? hover hover-ids zoom] +(defn setup-hover-shapes [page-id move-stream objects transform selected ctrl? hover hover-ids hover-disabled? zoom] (let [;; We use ref so we don't recreate the stream on a change zoom-ref (mf/use-ref zoom) + ctrl-ref (mf/use-ref @ctrl?) transform-ref (mf/use-ref nil) selected-ref (mf/use-ref selected) + hover-disabled-ref (mf/use-ref hover-disabled?) query-point (mf/use-callback (mf/deps page-id) (fn [point] (let [zoom (mf/ref-val zoom-ref) + ctrl? (mf/ref-val ctrl-ref) rect (gsh/center->rect point (/ 5 zoom) (/ 5 zoom))] - (uw/ask-buffered! - {:cmd :selection/query - :page-id page-id - :rect rect - :include-frames? true - :reverse? true})))) ;; we want the topmost shape to be selected first + (if (mf/ref-val hover-disabled-ref) + (rx/of nil) + (uw/ask-buffered! + {:cmd :selection/query + :page-id page-id + :rect rect + :include-frames? true + :clip-children? (not ctrl?) + :reverse? true}))))) ;; we want the topmost shape to be selected first over-shapes-stream (mf/use-memo - (fn [] - (->> move-stream - ;; When transforming shapes we stop querying the worker - (rx/filter #(not (some? (mf/ref-val transform-ref)))) - (rx/switch-map query-point)))) - ] + (fn [] + (rx/merge + (->> move-stream + ;; When transforming shapes we stop querying the worker + (rx/filter #(not (some? (mf/ref-val transform-ref)))) + (rx/switch-map query-point)) + + (->> move-stream + ;; When transforming shapes we stop querying the worker + (rx/filter #(some? (mf/ref-val transform-ref))) + (rx/map (constantly nil))))))] ;; Refresh the refs on a value change - (mf/use-effect (mf/deps transform) #(mf/set-ref-val! transform-ref transform)) @@ -127,23 +137,38 @@ (mf/deps zoom) #(mf/set-ref-val! zoom-ref zoom)) + (mf/use-effect + (mf/deps @ctrl?) + #(mf/set-ref-val! ctrl-ref @ctrl?)) + (mf/use-effect (mf/deps selected) #(mf/set-ref-val! selected-ref selected)) + (mf/use-effect + (mf/deps hover-disabled?) + #(mf/set-ref-val! hover-disabled-ref hover-disabled?)) + (hooks/use-stream over-shapes-stream (mf/deps page-id objects @ctrl?) (fn [ids] - (let [selected (mf/ref-val selected-ref) - remove-id? (into #{} (mapcat #(cp/get-parents % objects)) selected) - remove-id? (if @ctrl? - (d/concat remove-id? - (->> ids - (filterv #(= :group (get-in objects [% :type]))))) - remove-id?) - ids (->> ids (filterv (comp not remove-id?)))] - (reset! hover (get objects (first ids))) + (let [is-group? + (fn [id] + (contains? #{:group :bool} (get-in objects [id :type]))) + + selected (mf/ref-val selected-ref) + + remove-xfm (mapcat #(cp/get-parents % objects)) + remove-id? (cond-> (into #{} remove-xfm selected) + @ctrl? + (d/concat (filterv is-group? ids))) + + ids (->> ids (filterv (comp not remove-id?))) + + hover-shape (get objects (first ids))] + + (reset! hover hover-shape) (reset! hover-ids ids)))))) (defn setup-viewport-modifiers [modifiers selected objects render-ref] diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index ca0d1b0e2..ed2300b65 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -7,24 +7,22 @@ (ns app.main.ui.workspace.viewport.interactions "Visually show shape interactions in workspace" (:require + [app.common.data :as d] + [app.common.pages :as cp] + [app.common.types.interactions :as cti] [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.workspace.viewport.outline :refer [outline]] [app.util.dom :as dom] [cuerdas.core :as str] - [rumext.alpha :as mf] - )) - -(defn- get-click-interaction - [shape] - (first (filter #(= (:event-type %) :click) (:interactions shape)))) + [rumext.alpha :as mf])) (defn- on-mouse-down - [event {:keys [id] :as shape}] + [event index {:keys [id] :as shape}] (dom/stop-propagation event) (st/emit! (dw/select-shape id)) - (st/emit! (dw/start-create-interaction))) + (st/emit! (dw/start-edit-interaction index))) (defn connect-to-shape "Calculate the best position to draw an interaction line @@ -84,38 +82,62 @@ (mf/defc interaction-marker - [{:keys [x y arrow-dir zoom] :as props}] - (let [arrow-pdata (case arrow-dir - :right "M -5 0 l 8 0 l -4 -4 m 4 4 l -4 4" - :left "M 5 0 l -8 0 l 4 -4 m -4 4 l 4 4" - []) + [{:keys [x y stroke action-type arrow-dir zoom] :as props}] + (let [icon-pdata (case action-type + :navigate (case arrow-dir + :right "M -6.5 0 l 12 0 l -6 -6 m 6 6 l -6 6" + :left "M 6.5 0 l -12 0 l 6 -6 m -6 6 l 6 6" + nil) + + :open-overlay "M-5 -5 h7 v7 h-7 z M2 -2 h3.5 v7 h-7 v-2.5" + + :toggle-overlay "M-5 -5 h7 v7 h-7 z M2 -2 h3.5 v7 h-7 v-2.5" + + :close-overlay "M -5 -5 L 5 5 M -5 5 L 5 -5" + + :prev-screen (case arrow-dir + :left "M -6.5 0 l 12 0 l -6 -6 m 6 6 l -6 6" + :right "M 6.5 0 l -12 0 l 6 -6 m -6 6 l 6 6" + nil) + + :open-url (str "M1 -5 L 3 -7 L 7 -3 L 1 3 L -1 1" + "M-1 5 L -3 7 L -7 3 L -1 -3 L 1 -1") + + nil) inv-zoom (/ 1 zoom)] [:* [:circle {:cx 0 :cy 0 - :r 8 - :stroke "#31EFB8" - :stroke-width 2 - :fill "#FFFFFF" + :r (if (some? action-type) 11 4) + :fill stroke :transform (str "scale(" inv-zoom ", " inv-zoom ") " "translate(" (* zoom x) ", " (* zoom y) ")")}] - (when arrow-dir - [:path {:stroke "#31EFB8" - :fill "none" + (when icon-pdata + [:path {:fill stroke :stroke-width 2 - :d arrow-pdata + :stroke "#FFFFFF" + :d icon-pdata :transform (str "scale(" inv-zoom ", " inv-zoom ") " "translate(" (* zoom x) ", " (* zoom y) ")")}])])) (mf/defc interaction-path - [{:keys [orig-shape dest-shape dest-point selected? zoom] :as props}] + [{:keys [index level orig-shape dest-shape dest-point selected? action-type zoom] :as props}] (let [[orig-pos orig-x orig-y dest-pos dest-x dest-y] - (if dest-shape + (cond + dest-shape (connect-to-shape orig-shape dest-shape) - (connect-to-point orig-shape dest-point)) + + dest-point + (connect-to-point orig-shape dest-point) + + :else + (connect-to-point orig-shape + {:x (+ (:x2 (:selrect orig-shape)) 100) + :y (+ (- (:y1 (:selrect orig-shape)) 50) + (/ (* level 32) zoom))})) orig-dx (if (= orig-pos :right) 100 -100) dest-dx (if (= dest-pos :right) 100 -100) @@ -126,93 +148,183 @@ arrow-dir (if (= dest-pos :left) :right :left)] (if-not selected? - [:path {:stroke "#B1B2B5" - :fill "none" - :pointer-events "visible" - :stroke-width (/ 2 zoom) - :d pdata - :on-mouse-down #(on-mouse-down % orig-shape)}] + [:g {:on-mouse-down #(on-mouse-down % index orig-shape)} + [:path {:stroke "#B1B2B5" + :fill "none" + :pointer-events "visible" + :stroke-width (/ 2 zoom) + :d pdata}] + (when (not dest-shape) + [:& interaction-marker {:index index + :x dest-x + :y dest-y + :stroke "#B1B2B5" + :action-type action-type + :arrow-dir arrow-dir + :zoom zoom}])] - [:g {:on-mouse-down #(on-mouse-down % orig-shape)} + [:g {:on-mouse-down #(on-mouse-down % index orig-shape)} [:path {:stroke "#31EFB8" :fill "none" :pointer-events "visible" :stroke-width (/ 2 zoom) :d pdata}] - [:& interaction-marker {:x orig-x - :y orig-y - :arrow-dir nil - :zoom zoom}] - [:& interaction-marker {:x dest-x - :y dest-y - :arrow-dir arrow-dir - :zoom zoom}] (when dest-shape [:& outline {:shape dest-shape - :color "#31EFB8"}])]))) + :color "#31EFB8"}]) + + [:& interaction-marker {:index index + :x orig-x + :y orig-y + :stroke "#31EFB8" + :zoom zoom}] + [:& interaction-marker {:index index + :x dest-x + :y dest-y + :stroke "#31EFB8" + :action-type action-type + :arrow-dir arrow-dir + :zoom zoom}]]))) (mf/defc interaction-handle - [{:keys [shape zoom] :as props}] + [{:keys [index shape zoom] :as props}] (let [shape-rect (:selrect shape) handle-x (+ (:x shape-rect) (:width shape-rect)) handle-y (+ (:y shape-rect) (/ (:height shape-rect) 2))] - [:g {:on-mouse-down #(on-mouse-down % shape)} + [:g {:on-mouse-down #(on-mouse-down % index shape)} [:& interaction-marker {:x handle-x :y handle-y + :stroke "#31EFB8" + :action-type :navigate :arrow-dir :right :zoom zoom}]])) +(mf/defc overlay-marker + [{:keys [index orig-shape dest-shape position objects hover-disabled?] :as props}] + (let [start-move-position + (fn [_] + (st/emit! (dw/start-move-overlay-pos index)))] + + (when dest-shape + (let [orig-frame (cp/get-frame orig-shape objects) + marker-x (+ (:x orig-frame) (:x position)) + marker-y (+ (:y orig-frame) (:y position)) + width (:width dest-shape) + height (:height dest-shape)] + [:g {:on-mouse-down start-move-position + :on-mouse-enter #(reset! hover-disabled? true) + :on-mouse-leave #(reset! hover-disabled? false)} + [:path {:stroke "#31EFB8" + :fill "#000000" + :fill-opacity 0.3 + :stroke-width 1 + :d (str "M" marker-x " " marker-y " " + "h " width " " + "v " height " " + "h -" width " z" + "M" marker-x " " marker-y " " + "l " width " " height " " + "M" marker-x " " (+ marker-y height) " " + "l " width " -" height " ")}] + [:circle {:cx (+ marker-x (/ width 2)) + :cy (+ marker-y (/ height 2)) + :r 8 + :fill "#31EFB8"}]])))) + (mf/defc interactions - [{:keys [selected] :as props}] + [{:keys [selected hover-disabled?] :as props}] (let [local (mf/deref refs/workspace-local) zoom (mf/deref refs/selected-zoom) current-transform (:transform local) objects (mf/deref refs/workspace-page-objects) - active-shapes (filter #(first (get-click-interaction %)) (vals objects)) + active-shapes (filter #(seq (:interactions %)) (vals objects)) selected-shapes (map #(get objects %) selected) + editing-interaction-index (:editing-interaction-index local) draw-interaction-to (:draw-interaction-to local) draw-interaction-to-frame (:draw-interaction-to-frame local) - first-selected (first selected-shapes)] + move-overlay-to (:move-overlay-to local) + move-overlay-index (:move-overlay-index local) + first-selected (first selected-shapes) + + calc-level (fn [index interactions] + (->> (subvec interactions 0 index) + (filter #(nil? (:destination %))) + (count)))] [:g.interactions [:g.non-selected (for [shape active-shapes] - (let [interaction (get-click-interaction shape) - dest-shape (get objects (:destination interaction)) - selected? (contains? selected (:id shape))] - (when-not (or selected? (not dest-shape)) - [:& interaction-path {:key (:id shape) - :orig-shape shape - :dest-shape dest-shape - :selected selected - :selected? false - :zoom zoom}])))] + (for [[index interaction] (d/enumerate (:interactions shape))] + (let [dest-shape (when (cti/destination? interaction) + (get objects (:destination interaction))) + selected? (contains? selected (:id shape)) + level (calc-level index (:interactions shape))] + (when-not selected? + [:& interaction-path {:key (str (:id shape) "-" index) + :index index + :level level + :orig-shape shape + :dest-shape dest-shape + :selected selected + :selected? false + :action-type (:action-type interaction) + :zoom zoom}]))))] [:g.selected - (if (and draw-interaction-to first-selected) + (when (and draw-interaction-to first-selected) [:& interaction-path {:key "interactive" + :index nil :orig-shape first-selected :dest-point draw-interaction-to :dest-shape draw-interaction-to-frame :selected? true - :zoom zoom}] - - (for [shape selected-shapes] - (let [interaction (get-click-interaction shape) - dest-shape (get objects (:destination interaction))] - (if dest-shape - [:& interaction-path {:key (:id shape) - :orig-shape shape - :dest-shape dest-shape - :selected selected - :selected? true - :zoom zoom}] - (when (not (#{:move :rotate} current-transform)) - [:& interaction-handle {:key (:id shape) - :shape shape + :action-type :navigate + :zoom zoom}]) + (for [shape selected-shapes] + (if (seq (:interactions shape)) + (for [[index interaction] (d/enumerate (:interactions shape))] + (when-not (= index editing-interaction-index) + (let [dest-shape (when (cti/destination? interaction) + (get objects (:destination interaction))) + level (calc-level index (:interactions shape))] + [:* + [:& interaction-path {:key (str (:id shape) "-" index) + :index index + :level level + :orig-shape shape + :dest-shape dest-shape :selected selected - :zoom zoom}])))))]])) + :selected? true + :action-type (:action-type interaction) + :zoom zoom}] + (when (and (or (= (:action-type interaction) :open-overlay) + (= (:action-type interaction) :toggle-overlay)) + (= (:overlay-pos-type interaction) :manual)) + (if (and (some? move-overlay-to) + (= move-overlay-index index)) + [:& overlay-marker {:key (str "pos" (:id shape) "-" index) + :index index + :orig-shape shape + :dest-shape dest-shape + :position move-overlay-to + :objects objects + :hover-disabled? hover-disabled?}] + [:& overlay-marker {:key (str "pos" (:id shape) "-" index) + :index index + :orig-shape shape + :dest-shape dest-shape + :position (:overlay-position interaction) + :objects objects + :hover-disabled? hover-disabled?}]))]))) + (when (and shape + (not (cp/unframed-shape? shape)) + (not (#{:move :rotate} current-transform))) + [:& interaction-handle {:key (:id shape) + :index nil + :shape shape + :selected selected + :zoom zoom}])))]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs index b8282b5d8..9dc30ad5d 100644 --- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs @@ -229,7 +229,12 @@ current-transform (mf/deref refs/current-transform) selrect (:selrect shape) - transform (geom/transform-matrix shape {:no-flip true})] + transform (geom/transform-matrix shape {:no-flip true}) + + rotation (-> (gpt/point 1 0) + (gpt/transform (:transform shape)) + (gpt/angle) + (mod 360))] (when (not (#{:move :rotate} current-transform)) [:g.controls {:pointer-events (if disable-handlers "none" "visible")} @@ -249,7 +254,7 @@ :on-rotate on-rotate :on-resize (partial on-resize position) :transform transform - :rotation (:rotation shape) + :rotation rotation :color color :overflow-text overflow-text} props (map->obj (merge common-props props))] diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 9ca6c54a1..9d7f54ed4 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -10,10 +10,12 @@ [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.main.data.workspace :as dw] + [app.main.data.workspace.interactions :as dwi] [app.main.refs :as refs] [app.main.store :as st] [app.main.streams :as ms] [app.main.ui.hooks :as hooks] + [app.main.ui.icons :as i] [app.main.ui.workspace.viewport.path-actions :refer [path-actions]] [app.util.dom :as dom] [rumext.alpha :as mf])) @@ -90,7 +92,7 @@ (mf/defc frame-title [{:keys [frame modifiers selected? zoom on-frame-enter on-frame-leave on-frame-select]}] (let [{:keys [width x y]} (gsh/transform-shape frame) - label-pos (gpt/point x (- y (/ 10 zoom))) + label-pos (gpt/point x (- y (/ 10 zoom))) on-mouse-down (mf/use-callback @@ -156,3 +158,75 @@ :on-frame-enter on-frame-enter :on-frame-leave on-frame-leave :on-frame-select on-frame-select}])])) + +(mf/defc frame-flow + [{:keys [flow frame modifiers selected? zoom on-frame-enter on-frame-leave on-frame-select]}] + (let [{:keys [x y]} (gsh/transform-shape frame) + flow-pos (gpt/point x (- y (/ 35 zoom))) + + on-mouse-down + (mf/use-callback + (mf/deps (:id frame) on-frame-select) + (fn [bevent] + (let [event (.-nativeEvent bevent)] + (when (= 1 (.-which event)) + (dom/prevent-default event) + (dom/stop-propagation event) + (on-frame-select event (:id frame)))))) + + on-double-click + (mf/use-callback + (mf/deps (:id frame)) + (st/emitf (dwi/start-rename-flow (:id flow)))) + + on-pointer-enter + (mf/use-callback + (mf/deps (:id frame) on-frame-enter) + (fn [_] + (on-frame-enter (:id frame)))) + + on-pointer-leave + (mf/use-callback + (mf/deps (:id frame) on-frame-leave) + (fn [_] + (on-frame-leave (:id frame))))] + + [:foreignObject {:x 0 + :y -15 + :width 100000 + :height 24 + :transform (str (when (and selected? modifiers) + (str (:displacement modifiers) " " )) + (text-transform flow-pos zoom))} + [:div.flow-badge {:class (dom/classnames :selected selected?)} + [:div.content {:on-mouse-down on-mouse-down + :on-double-click on-double-click + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave} + i/play + [:span (:name flow)]]]])) + +(mf/defc frame-flows + {::mf/wrap-props false} + [props] + (let [flows (unchecked-get props "flows") + objects (unchecked-get props "objects") + zoom (unchecked-get props "zoom") + modifiers (unchecked-get props "modifiers") + selected (or (unchecked-get props "selected") #{}) + + on-frame-enter (unchecked-get props "on-frame-enter") + on-frame-leave (unchecked-get props "on-frame-leave") + on-frame-select (unchecked-get props "on-frame-select")] + [:g.frame-flows + (for [flow flows] + (let [frame (get objects (:starting-frame flow))] + [:& frame-flow {:flow flow + :frame frame + :selected? (contains? selected (:id frame)) + :zoom zoom + :modifiers modifiers + :on-frame-enter on-frame-enter + :on-frame-leave on-frame-leave + :on-frame-select on-frame-select}]))])) + diff --git a/frontend/src/app/util/browser_history.js b/frontend/src/app/util/browser_history.js index 380cb32ab..8663bd684 100644 --- a/frontend/src/app/util/browser_history.js +++ b/frontend/src/app/util/browser_history.js @@ -11,7 +11,6 @@ goog.provide("app.util.browser_history"); goog.require("goog.history.Html5History"); - goog.scope(function() { const self = app.util.browser_history; const Html5History = goog.history.Html5History; diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 5c3074e14..b6c50eb1b 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -281,7 +281,7 @@ (defn set-text! [node text] (set! (.-textContent node) text)) -(defn set-css-property [node property value] +(defn set-css-property! [node property value] (.setProperty (.-style ^js node) property value)) (defn capture-pointer [event] @@ -394,3 +394,16 @@ (defn left-mouse? [bevent] (let [event (.-nativeEvent ^js bevent)] (= 1 (.-which event)))) + +(defn open-new-window + ([uri] + (open-new-window uri "_blank")) + ([uri name] + ;; Warning: need to protect against reverse tabnabbing attack + ;; https://www.comparitech.com/blog/information-security/reverse-tabnabbing/ + (.open js/window (str uri) name "noopener,noreferrer"))) + +(defn browser-back + [] + (.back (.-history js/window))) + diff --git a/frontend/src/app/util/http.cljs b/frontend/src/app/util/http.cljs index fe60147a3..41ac92a38 100644 --- a/frontend/src/app/util/http.cljs +++ b/frontend/src/app/util/http.cljs @@ -54,8 +54,10 @@ {"x-frontend-version" (:full @cfg/version)}) (defn fetch - [{:keys [method uri query headers body mode omit-default-headers] - :or {mode :cors headers {}}}] + [{:keys [method uri query headers body mode omit-default-headers credentials] + :or {mode :cors + headers {} + credentials "same-origin"}}] (rx/Observable.create (fn [subscriber] (let [controller (js/AbortController.) @@ -83,7 +85,7 @@ :body body :mode (d/name mode) :redirect "follow" - :credentials "same-origin" + :credentials credentials :referrerPolicy "no-referrer" :signal signal}] (-> (js/fetch (str uri) params) @@ -165,7 +167,6 @@ :uri uri :response-type :blob :omit-default-headers true}) - (rx/filter #(= 200 (:status %))) (rx/map :body) (rx/mapcat wapi/read-file-as-data-url) diff --git a/frontend/src/app/util/i18n.cljs b/frontend/src/app/util/i18n.cljs index b7e942f94..fda05744b 100644 --- a/frontend/src/app/util/i18n.cljs +++ b/frontend/src/app/util/i18n.cljs @@ -27,6 +27,8 @@ {:label "Rumanian (communit)" :value "ro"} {:label "Portuguese (Brazil, community)" :value "pt_br"} {:label "Ελληνική γλώσσα (community)" :value "el"} + {:label "עִבְרִית (community)" :value "he"} + {:label "عربي/عربى (community)" :value "ar"} {:label "简体中文 (community)" :value "zh_cn"}]) (defn- parse-locale diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index 8481543e2..552ca311b 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.types.interactions :as cti] [app.common.uuid :as uuid] [app.util.color :as uc] [app.util.json :as json] @@ -209,6 +210,13 @@ (->> node :content last))] (merge (add-attrs {} (:attrs svg-node)) node-attrs)) + (= type :bool) + (->> node + (:content) + (filter #(= :path (:tag %))) + (map #(:attrs %)) + (reduce add-attrs node-attrs)) + :else node-attrs))) @@ -443,6 +451,11 @@ mask? (assoc :masked-group? true)))) +(defn add-bool-data + [props node] + (-> props + (assoc :bool-type (get-meta node :bool-type keyword)))) + (defn parse-shadow [node] {:id (uuid/next) :style (get-meta node :shadow-type keyword) @@ -465,7 +478,6 @@ :suffix (get-meta node :suffix) :scale (get-meta node :scale d/parse-double)}) - (defn parse-grid-node [node] (let [attrs (-> node :attrs remove-penpot-prefix) color {:color (:color attrs) @@ -482,8 +494,18 @@ :params params})) (defn parse-grids [node] - (let [grid-node (get-data node :penpot:grids)] - (->> grid-node :content (mapv parse-grid-node)))) + (let [grids-node (get-data node :penpot:grids)] + (->> grids-node :content (mapv parse-grid-node)))) + +(defn parse-flow-node [node] + (let [attrs (-> node :attrs remove-penpot-prefix)] + {:id (uuid/next) + :name (-> attrs :name) + :starting-frame (-> attrs :starting-frame uuid)})) + +(defn parse-flows [node] + (let [flows-node (get-data node :penpot:flows)] + (->> flows-node :content (mapv parse-flow-node)))) (defn extract-from-data ([node tag] @@ -706,28 +728,51 @@ (add-image-data type node)) (cond-> (= :text type) - (add-text-data node)))))) + (add-text-data node)) + + (cond-> (= :bool type) + (add-bool-data node)))))) (defn parse-page-data [node] - (let [style (parse-style (get-in node [:attrs :style])) + (let [style (parse-style (get-in node [:attrs :style])) background (:background style) - grids (->> (parse-grids node) - (group-by :type) - (d/mapm (fn [_ v] (-> v first :params))))] + grids (->> (parse-grids node) + (group-by :type) + (d/mapm (fn [_ v] (-> v first :params)))) + flows (parse-flows node)] (cond-> {} (some? background) (assoc-in [:options :background] background) (d/not-empty? grids) - (assoc-in [:options :saved-grids] grids)))) + (assoc-in [:options :saved-grids] grids) + + (d/not-empty? flows) + (assoc-in [:options :flows] flows)))) (defn parse-interactions [node] (let [interactions-node (get-data node :penpot:interactions)] (->> (find-all-nodes interactions-node :penpot:interaction) (mapv (fn [node] - {:destination (get-meta node :destination uuid/uuid) - :action-type (get-meta node :action-type keyword) - :event-type (get-meta node :event-type keyword)}))))) + (let [interaction {:event-type (get-meta node :event-type keyword) + :action-type (get-meta node :action-type keyword)}] + (cond-> interaction + (cti/has-delay interaction) + (assoc :delay (get-meta node :delay d/parse-double)) + + (cti/has-destination interaction) + (assoc :destination (get-meta node :destination uuid/uuid)) + + (cti/has-url interaction) + (assoc :url (get-meta node :url str)) + + (cti/has-overlay-opts interaction) + (assoc :overlay-pos-type (get-meta node :overlay-pos-type keyword) + :overlay-position (gpt/point + (get-meta node :overlay-position-x d/parse-double) + (get-meta node :overlay-position-y d/parse-double)) + :close-click-outside (get-meta node :close-click-outside str->bool) + :background-overlay (get-meta node :background-overlay str->bool))))))))) diff --git a/frontend/src/app/util/logging.clj b/frontend/src/app/util/logging.clj deleted file mode 100644 index f888f1bc8..000000000 --- a/frontend/src/app/util/logging.clj +++ /dev/null @@ -1,46 +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/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.util.logging) - -(defn- log-expr [_form level keyvals] - (let [keyvals-map (apply array-map keyvals) - ;;formatter (::formatter keyvals-map 'identity) - ] - `(log ~(::logger keyvals-map (str *ns*)) - ~level - ~(-> keyvals-map - (dissoc ::logger) - #_(assoc :line (:line (meta form)))) - ~(:err keyvals-map)))) - -(defmacro set-level! - ([level] - `(set-level* ~(str *ns*) ~level)) - ([n level] - `(set-level* ~n ~level))) - -(defmacro error [& keyvals] - (log-expr &form :error keyvals)) - -(defmacro warn [& keyvals] - (log-expr &form :warn keyvals)) - -(defmacro info [& keyvals] - (log-expr &form :info keyvals)) - -(defmacro debug [& keyvals] - (log-expr &form :debug keyvals)) - -(defmacro trace [& keyvals] - (log-expr &form :trace keyvals)) - -(defmacro spy [form] - (let [res (gensym)] - `(let [~res ~form] - ~(log-expr &form :debug [:spy `'~form - :=> res]) - ~res))) diff --git a/frontend/src/app/util/logging.cljs b/frontend/src/app/util/logging.cljs deleted file mode 100644 index b2bfaaff9..000000000 --- a/frontend/src/app/util/logging.cljs +++ /dev/null @@ -1,189 +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/. -;; -;; Copyright (c) UXBOX Labs SL -;; -;; This code is highly inspired on the lambdaisland/glogi library but -;; adapted and simplified to our needs. The adapted code shares the -;; same license. You can found the origianl source code here: -;; https://github.com/lambdaisland/glogi - -(ns app.util.logging - (:require - [app.common.exceptions :as ex] - [cuerdas.core :as str] - [goog.log :as glog]) - (:require-macros [app.util.logging])) - -(defn- logger-name - [s] - (cond - (string? s) s - (= s :root) "" - (simple-ident? s) (name s) - (qualified-ident? s) (str (namespace s) "." (name s)) - :else (str s))) - -(defn get-logger - [n] - (glog/getLogger (logger-name n))) - -(def levels - {:off (.-OFF ^js glog/Level) - :shout (.-SHOUT ^js glog/Level) - :error (.-SEVERE ^js glog/Level) - :severe (.-SEVERE ^js glog/Level) - :warning (.-WARNING ^js glog/Level) - :warn (.-WARNING ^js glog/Level) - :info (.-INFO ^js glog/Level) - :config (.-CONFIG ^js glog/Level) - :debug (.-FINE ^js glog/Level) - :fine (.-FINE ^js glog/Level) - :finer (.-FINER ^js glog/Level) - :trace (.-FINER ^js glog/Level) - :finest (.-FINEST ^js glog/Level) - :all (.-ALL ^js glog/Level)}) - -(def colors - {:gray3 "#8e908c" - :gray4 "#969896" - :gray5 "#4d4d4c" - :gray6 "#282a2e" - :black "#1d1f21" - :red "#c82829" - :blue "#4271ae" - :orange "#f5871f"}) - -(defn- get-level-value - [level] - (if (instance? glog/Level level) - (.-value ^js level) - (.-value ^js (get levels level)))) - -(defn- level->color - [level] - (condp <= (get-level-value level) - (get-level-value :error) (get colors :red) - (get-level-value :warn) (get colors :orange) - (get-level-value :info) (get colors :blue) - (get-level-value :debug) (get colors :gray4) - (get-level-value :trace) (get colors :gray3) - (get colors :gray2))) - -(defn- level->short-name - [l] - (case l - :fine "DBG" - :debug "DBG" - :finer "TRC" - :trace "TRC" - :info "INF" - :warn "WRN" - :warning "WRN" - :error "ERR" - (subs (.-name ^js (get levels l)) 0 3))) - -(defn- make-log-record - [level message name exception] - (let [record (glog/LogRecord. level message name)] - (when exception (.setException record exception)) - record)) - -(defn log - "Output a log message to the given logger, optionally with an exception to be - logged." - ([name lvl message] - (log name lvl message nil)) - ([name lvl message exception] - (when glog/ENABLED - (when-let [l (get-logger name)] - (glog/publishLogRecord l (make-log-record (get levels lvl) message name exception)))))) - -(defn set-level* - "Set the level (a keyword) of the given logger, identified by name." - [name lvl] - (assert (contains? levels lvl)) - (some-> (get-logger name) - (glog/setLevel (get levels lvl)))) - -(defn set-levels! - [lvls] - (doseq [[logger level] lvls - :let [level (if (string? level) (keyword level) level)]] - (set-level* logger level))) - -(defn record->map - [^js record] - {:seqn (.-sequenceNumber_ record) - :time (.-time_ record) - :level (keyword (str/lower (.-name (.-level_ record)))) - :message (.-msg_ record) - :logger-name (.-loggerName_ record) - :exception (.-exception_ record)}) - -(defn add-handler! - ([handler-fn] - (add-handler! :root handler-fn)) - ([logger-or-name handler-fn] - (when-let [l (get-logger logger-or-name)] - (glog/removeHandler l handler-fn) - (glog/addHandler l handler-fn)))) - -(defn- prepare-message - [message] - (loop [kvpairs (seq message) - message (array-map) - specials []] - (if (nil? kvpairs) - [message specials] - (let [[k v] (first kvpairs)] - (cond - (= k :err) - (recur (next kvpairs) - message - (conj specials [:error nil v])) - - (and (qualified-ident? k) - (= "js" (namespace k))) - (recur (next kvpairs) - message - (conj specials [:js (name k) (if (object? v) v (clj->js v))])) - - :else - (recur (next kvpairs) - (assoc message k v) - specials)))))) - -(defn default-handler - [{:keys [message level logger-name]}] - (let [header-styles (str "font-weight: 600; color: " (level->color level)) - normal-styles (str "font-weight: 300; color: " (get colors :gray6)) - level-name (level->short-name level) - header (str "%c" level-name " [" logger-name "] ")] - - (if (string? message) - (let [message (str header "%c" message)] - (js/console.log message header-styles normal-styles)) - (let [[message specials] (prepare-message message)] - (if (seq specials) - (let [message (str header "%c" (pr-str message))] - (js/console.group message header-styles normal-styles) - (doseq [[type n v] specials] - (case type - :js (js/console.log n v) - :error (if (ex/ex-info? v) - (js/console.error (pr-str v)) - (js/console.error v)))) - (js/console.groupEnd message)) - (let [message (str header "%c" (pr-str message))] - (js/console.log message header-styles normal-styles))))))) - -(defonce default-console-handler - #(default-handler (record->map %))) - -(defn initialize! - [] - (add-handler! :root default-console-handler) - nil) - diff --git a/frontend/src/app/util/path/format.cljs b/frontend/src/app/util/path/format.cljs index 4b0640f4e..80b1f9214 100644 --- a/frontend/src/app/util/path/format.cljs +++ b/frontend/src/app/util/path/format.cljs @@ -6,7 +6,8 @@ (ns app.util.path.format (:require - [app.util.path.commands :as upc] + [app.common.path.commands :as upc] + [app.common.path.subpaths :refer [pt=]] [cuerdas.core :as str])) (defn command->param-list [command] @@ -43,7 +44,9 @@ (:large-arc-flag params) "," (:sweep-flag params) "," (:x params) "," - (:y params))))) + (:y params)) + + ""))) (defn command->string [{:keys [command relative] :as entry}] (let [command-str (case command @@ -56,12 +59,19 @@ :smooth-curve-to "S" :quadratic-bezier-curve-to "Q" :smooth-quadratic-bezier-curve-to "T" - :elliptical-arc "A") + :elliptical-arc "A" + "") command-str (if relative (str/lower command-str) command-str) param-list (command->param-list entry)] (str command-str param-list))) +(defn set-point + [command point] + (-> command + (assoc-in [:params :x] (:x point)) + (assoc-in [:params :y] (:y point)))) + (defn format-path [content] (with-out-str (loop [last-move nil @@ -72,9 +82,12 @@ (let [point (upc/command->point current) current-move? (= :move-to (:command current)) last-move (if current-move? point last-move)] - (print (command->string current)) - (when (and (not current-move?) (= last-move point)) + (if (and (not current-move?) (pt= last-move point)) + (print (command->string (set-point current last-move))) + (print (command->string current))) + + (when (and (not current-move?) (pt= last-move point)) (print "Z")) (recur last-move diff --git a/frontend/src/app/util/path/geom.cljs b/frontend/src/app/util/path/geom.cljs deleted file mode 100644 index 0478fff8c..000000000 --- a/frontend/src/app/util/path/geom.cljs +++ /dev/null @@ -1,55 +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/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.util.path.geom - (:require - [app.common.geom.point :as gpt] - [app.common.geom.shapes.path :as gshp] - [app.util.path.commands :as upc])) - -(defn calculate-opposite-handler - "Given a point and its handler, gives the symetric handler" - [point handler] - (let [handler-vector (gpt/to-vec point handler)] - (gpt/add point (gpt/negate handler-vector)))) - -(defn split-line-to [from-p cmd val] - (let [to-p (upc/command->point cmd) - sp (gpt/line-val from-p to-p val)] - [(upc/make-line-to sp) cmd])) - -(defn split-curve-to [from-p cmd val] - (let [params (:params cmd) - end (gpt/point (:x params) (:y params)) - h1 (gpt/point (:c1x params) (:c1y params)) - h2 (gpt/point (:c2x params) (:c2y params)) - [[_ to1 h11 h21] - [_ to2 h12 h22]] (gshp/curve-split from-p end h1 h2 val)] - [(upc/make-curve-to to1 h11 h21) - (upc/make-curve-to to2 h12 h22)])) - -(defn opposite-handler - "Calculates the coordinates of the opposite handler" - [point handler] - (let [phv (gpt/to-vec point handler)] - (gpt/add point (gpt/negate phv)))) - -(defn opposite-handler-keep-distance - "Calculates the coordinates of the opposite handler but keeping the old distance" - [point handler old-opposite] - (let [old-distance (gpt/distance point old-opposite) - phv (gpt/to-vec point handler) - phv2 (gpt/multiply - (gpt/unit (gpt/negate phv)) - (gpt/point old-distance))] - (gpt/add point phv2))) - -(defn content->points [content] - (->> content - (map #(when (-> % :params :x) (gpt/point (-> % :params :x) (-> % :params :y)))) - (remove nil?) - (into []))) - diff --git a/frontend/src/app/util/path/parser.cljs b/frontend/src/app/util/path/parser.cljs index 7b68caf64..99e6435a5 100644 --- a/frontend/src/app/util/path/parser.cljs +++ b/frontend/src/app/util/path/parser.cljs @@ -8,9 +8,9 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as upg] + [app.common.path.commands :as upc] [app.util.path.arc-to-curve :refer [a2c]] - [app.util.path.commands :as upc] - [app.util.path.geom :as upg] [app.util.svg :as usvg] [cuerdas.core :as str])) @@ -292,6 +292,10 @@ [result next-pos next-start next-cc next-qc])) start (first commands) + start (cond-> start + (:relative start) + (assoc :relative false)) + start-pos (gpt/point (:params start))] (->> (map vector (rest commands) commands) diff --git a/frontend/src/app/util/path/shapes_to_path.cljs b/frontend/src/app/util/path/shapes_to_path.cljs deleted file mode 100644 index 8d7c86cbd..000000000 --- a/frontend/src/app/util/path/shapes_to_path.cljs +++ /dev/null @@ -1,146 +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/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.util.path.shapes-to-path - (:require - [app.common.data :as d] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.path :as gsp] - [app.util.path.commands :as pc])) - -(def bezier-circle-c 0.551915024494) -(def dissoc-attrs [:x :y :width :height - :rx :ry :r1 :r2 :r3 :r4 - :medata]) -(def allowed-transform-types #{:rect - :circle - :image}) - -(defn make-corner-arc - "Creates a curvle corner for border radius" - [from to corner radius] - (let [x (case corner - :top-left (:x from) - :top-right (- (:x from) radius) - :bottom-right (- (:x to) radius) - :bottom-left (:x to)) - - y (case corner - :top-left (- (:y from) radius) - :top-right (:y from) - :bottom-right (- (:y to) (* 2 radius)) - :bottom-left (- (:y to) radius)) - - width (* radius 2) - height (* radius 2) - - c bezier-circle-c - c1x (+ x (* (/ width 2) (- 1 c))) - c2x (+ x (* (/ width 2) (+ 1 c))) - c1y (+ y (* (/ height 2) (- 1 c))) - c2y (+ y (* (/ height 2) (+ 1 c))) - - h1 (case corner - :top-left (assoc from :y c1y) - :top-right (assoc from :x c2x) - :bottom-right (assoc from :y c2y) - :bottom-left (assoc from :x c1x)) - - h2 (case corner - :top-left (assoc to :x c1x) - :top-right (assoc to :y c1y) - :bottom-right (assoc to :x c2x) - :bottom-left (assoc to :y c2y))] - - (pc/make-curve-to to h1 h2))) - -(defn circle->path - "Creates the bezier curves to approximate a circle shape" - [x y width height] - (let [mx (+ x (/ width 2)) - my (+ y (/ height 2)) - ex (+ x width) - ey (+ y height) - - p1 (gpt/point mx y) - p2 (gpt/point ex my) - p3 (gpt/point mx ey) - p4 (gpt/point x my) - - c bezier-circle-c - c1x (+ x (* (/ width 2) (- 1 c))) - c2x (+ x (* (/ width 2) (+ 1 c))) - c1y (+ y (* (/ height 2) (- 1 c))) - c2y (+ y (* (/ height 2) (+ 1 c)))] - - [(pc/make-move-to p1) - (pc/make-curve-to p2 (assoc p1 :x c2x) (assoc p2 :y c1y)) - (pc/make-curve-to p3 (assoc p2 :y c2y) (assoc p3 :x c2x)) - (pc/make-curve-to p4 (assoc p3 :x c1x) (assoc p4 :y c2y)) - (pc/make-curve-to p1 (assoc p4 :y c1y) (assoc p1 :x c1x))])) - -(defn rect->path - "Creates a bezier curve that approximates a rounded corner rectangle" - [x y width height r1 r2 r3 r4] - (let [p1 (gpt/point x (+ y r1)) - p2 (gpt/point (+ x r1) y) - - p3 (gpt/point (+ width x (- r2)) y) - p4 (gpt/point (+ width x) (+ y r2)) - - p5 (gpt/point (+ width x) (+ height y (- r3))) - p6 (gpt/point (+ width x (- r3)) (+ height y)) - - p7 (gpt/point (+ x r4) (+ height y)) - p8 (gpt/point x (+ height y (- r4)))] - (-> [] - (conj (pc/make-move-to p1)) - (cond-> (not= p1 p2) - (conj (make-corner-arc p1 p2 :top-left r1))) - (conj (pc/make-line-to p3)) - (cond-> (not= p3 p4) - (conj (make-corner-arc p3 p4 :top-right r2))) - (conj (pc/make-line-to p5)) - (cond-> (not= p5 p6) - (conj (make-corner-arc p5 p6 :bottom-right r3))) - (conj (pc/make-line-to p7)) - (cond-> (not= p7 p8) - (conj (make-corner-arc p7 p8 :bottom-left r4))) - (conj (pc/make-line-to p1))))) - -(defn convert-to-path - "Transforms the given shape to a path" - [{:keys [type x y width height r1 r2 r3 r4 rx metadata] :as shape}] - - (if (contains? allowed-transform-types type) - (let [r1 (or r1 rx 0) - r2 (or r2 rx 0) - r3 (or r3 rx 0) - r4 (or r4 rx 0) - - new-content - (case type - :circle - (circle->path x y width height) - (rect->path x y width height r1 r2 r3 r4)) - - ;; Apply the transforms that had the shape - transform (:transform shape) - new-content (cond-> new-content - (some? transform) - (gsp/transform-content (gmt/transform-in (gsh/center-shape shape) transform)))] - - (-> shape - (d/without-keys dissoc-attrs) - (assoc :type :path) - (assoc :content new-content) - (cond-> (= :image type) (-> (assoc :fill-image metadata) - (dissoc :metadata))))) - ;; Do nothing if the shape is not of a correct type - shape)) - diff --git a/frontend/src/app/util/path/tools.cljs b/frontend/src/app/util/path/tools.cljs index f6409f221..9f97ab666 100644 --- a/frontend/src/app/util/path/tools.cljs +++ b/frontend/src/app/util/path/tools.cljs @@ -8,9 +8,9 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as upg] [app.common.math :as mth] - [app.util.path.commands :as upc] - [app.util.path.geom :as upg] + [app.common.path.commands :as upc] [clojure.set :as set])) (defn remove-line-curves @@ -210,7 +210,7 @@ (case (:command cmd) :line-to [index (upg/split-line-to start cmd value)] :curve-to [index (upg/split-curve-to start cmd value)] - :close-path [index [(upc/make-line-to (gpt/line-val start end value)) cmd]] + :close-path [index [(upc/make-line-to (gpt/lerp start end value)) cmd]] nil)) cmd-changes diff --git a/frontend/src/app/util/router.cljs b/frontend/src/app/util/router.cljs index 2329af2b6..27bfe4374 100644 --- a/frontend/src/app/util/router.cljs +++ b/frontend/src/app/util/router.cljs @@ -10,6 +10,7 @@ [app.common.uri :as u] [app.config :as cfg] [app.util.browser-history :as bhistory] + [app.util.dom :as dom] [app.util.timers :as ts] [beicon.core :as rx] [goog.events :as e] @@ -20,12 +21,12 @@ (defn resolve ([router id] (resolve router id {} {})) - ([router id params] (resolve router id params {})) - ([router id params qparams] - (when-let [match (r/match-by-name router id params)] - (if (empty? qparams) + ([router id path-params] (resolve router id path-params {})) + ([router id path-params query-params] + (when-let [match (r/match-by-name router id path-params)] + (if (empty? query-params) (r/match->path match) - (let [query (u/map->query-string qparams)] + (let [query (u/map->query-string query-params)] (-> (u/uri (r/match->path match)) (assoc :query query) (str))))))) @@ -47,12 +48,12 @@ [router path] (let [uri (u/uri path)] (when-let [match (r/match-by-path router (:path uri))] - (let [qparams (u/query-string->map (:query uri)) - params {:path (:path-params match) - :query qparams}] + (let [query-params (u/query-string->map (:query uri)) + params {:path (:path-params match) + :query query-params}] (-> match (assoc :params params) - (assoc :query-params qparams)))))) + (assoc :query-params query-params)))))) ;; --- Navigate (Event) @@ -64,60 +65,51 @@ ptk/UpdateEvent (update [_ state] - (assoc state :route match)))) + (-> state + (assoc :route match) + (dissoc :exception))))) (defn navigate* - [id params qparams replace] + [id path-params query-params replace] (ptk/reify ::navigate IDeref (-deref [_] {:id id - :path-params params - :query-params qparams + :path-params path-params + :query-params query-params :replace replace}) - ptk/UpdateEvent - (update [_ state] - (dissoc state :exception)) - ptk/EffectEvent (effect [_ state _] - (ts/asap - #(let [router (:router state) - history (:history state) - path (resolve router id params qparams)] - (if ^boolean replace + (let [router (:router state) + history (:history state) + path (resolve router id path-params query-params)] + (ts/asap + #(if ^boolean replace (bhistory/replace-token! history path) (bhistory/set-token! history path))))))) +(defn assign-exception + [error] + (ptk/reify ::assign-exception + ptk/UpdateEvent + (update [_ state] + (if (nil? error) + (dissoc state :exception) + (assoc state :exception error))))) + (defn nav ([id] (nav id nil nil)) - ([id params] (nav id params nil)) - ([id params qparams] (navigate* id params qparams false))) + ([id path-params] (nav id path-params nil)) + ([id path-params query-params] (navigate* id path-params query-params false))) (defn nav' ([id] (nav id nil nil)) - ([id params] (nav id params nil)) - ([id params qparams] (navigate* id params qparams true))) + ([id path-params] (nav id path-params nil)) + ([id path-params query-params] (navigate* id path-params query-params true))) (def navigate nav) -(deftype NavigateNewWindow [id params qparams] - ptk/EffectEvent - (effect [_ state _] - (let [router (:router state) - path (resolve router id params qparams) - uri (-> (u/uri cfg/public-uri) - (assoc :fragment path)) - name (str (name id) "-" (:file-id params))] - (js/window.open (str uri) name)))) - -(defn nav-new-window - ([id] (nav-new-window id nil nil)) - ([id params] (nav-new-window id params nil)) - ([id params qparams] (NavigateNewWindow. id params qparams))) - - (defn nav-new-window* [{:keys [rname path-params query-params name]}] (ptk/reify ::nav-new-window @@ -127,10 +119,23 @@ path (resolve router rname path-params query-params) uri (-> (u/uri cfg/public-uri) (assoc :fragment path))] + (dom/open-new-window (str uri) name))))) +(defn nav-back + [] + (ptk/reify ::nav-back + ptk/EffectEvent + (effect [_ _ _] + (ts/asap dom/browser-back)))) - - (js/window.open (str uri) name))))) +(defn nav-back-local + "Navigate back only if the previous page is in penpot app." + [] + (let [location (.-location js/document) + referrer (u/uri (.-referrer js/document))] + (when (or (nil? (:host referrer)) + (= (.-hostname location) (:host referrer))) + (nav-back)))) ;; --- History API diff --git a/frontend/src/app/util/time.cljs b/frontend/src/app/util/time.cljs index a32a0e1a4..b6c470799 100644 --- a/frontend/src/app/util/time.cljs +++ b/frontend/src/app/util/time.cljs @@ -7,6 +7,8 @@ (ns app.util.time (:require ["date-fns/formatDistanceToNowStrict" :default dateFnsFormatDistanceToNowStrict] + ["date-fns/locale/ar-SA" :default dateFnsLocalesAr] + ["date-fns/locale/he" :default dateFnsLocalesHe] ["date-fns/locale/ca" :default dateFnsLocalesCa] ["date-fns/locale/de" :default dateFnsLocalesDe] ["date-fns/locale/el" :default dateFnsLocalesEl] @@ -202,6 +204,8 @@ (def ^:private locales #js {:en dateFnsLocalesEnUs + :ar dateFnsLocalesAr + :he dateFnsLocalesHe :fr dateFnsLocalesFr :tr dateFnsLocalesTr :es dateFnsLocalesEs diff --git a/frontend/src/app/util/worker.cljs b/frontend/src/app/util/worker.cljs index 914756e96..497a050d5 100644 --- a/frontend/src/app/util/worker.cljs +++ b/frontend/src/app/util/worker.cljs @@ -9,6 +9,8 @@ (:require [app.common.transit :as t] [app.common.uuid :as uuid] + [app.util.globals :refer [global]] + [app.util.object :as obj] [beicon.core :as rx])) (declare handle-response) @@ -28,11 +30,13 @@ data (t/encode-str message) instance (:instance worker)] - (.postMessage instance data) - (->> (:stream worker) - (rx/filter #(= (:reply-to %) sender-id)) - (take-messages) - (rx/map handle-response))))) + (if (some? instance) + (do (.postMessage instance data) + (->> (:stream worker) + (rx/filter #(= (:reply-to %) sender-id)) + (take-messages) + (rx/map handle-response))) + (rx/empty))))) (defn ask! [worker message] @@ -79,6 +83,11 @@ (.addEventListener instance "message" handle-message) (.addEventListener instance "error" handle-error) + (ask! worker + {:cmd :configure + :params + {"penpotPublicURI" (obj/get global "penpotPublicURI")}}) + worker)) (defn- handle-response diff --git a/frontend/src/app/worker/impl.cljs b/frontend/src/app/worker/impl.cljs index 9dfb3bba2..13b7d167f 100644 --- a/frontend/src/app/worker/impl.cljs +++ b/frontend/src/app/worker/impl.cljs @@ -7,6 +7,8 @@ (ns app.worker.impl (:require [app.common.pages.changes :as ch] + [app.util.globals :refer [global]] + [app.util.object :as obj] [okulary.core :as l])) (enable-console-print!) @@ -50,3 +52,8 @@ (assoc :cmd :selection/update-index))) (handler (-> message (assoc :cmd :snaps/update-index)))))) + +(defmethod handler :configure + [{:keys [params]}] + (doseq [[param-key param-value] params] + (obj/set! global param-key param-value))) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 34c6ae3f5..d36ec13a6 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.file-builder :as fb] + [app.common.logging :as log] [app.common.pages :as cp] [app.common.text :as ct] [app.common.uuid :as uuid] @@ -17,14 +18,13 @@ [app.util.http :as http] [app.util.import.parser :as cip] [app.util.json :as json] - [app.util.logging :as log] [app.util.zip :as uz] [app.worker.impl :as impl] [beicon.core :as rx] [cuerdas.core :as str] [tubax.core :as tubax])) -(log/set-level! :trace) +(log/set-level! :warn) ;; Upload changes batches size (def ^:const change-batch-size 100) @@ -238,6 +238,7 @@ (case type :frame (fb/close-artboard file) :group (fb/close-group file) + :bool (fb/close-bool file) :svg-raw (fb/close-svg-raw file) #_default file) @@ -254,6 +255,7 @@ file (case type :frame (fb/add-artboard file data) :group (fb/add-group file data) + :bool (fb/add-bool file data) :rect (fb/create-rect file data) :circle (fb/create-circle file data) :path (fb/create-path file data) @@ -313,7 +315,10 @@ page-data (-> (cip/parse-page-data content) (assoc :name page-name) (assoc :id (resolve page-id))) - file (-> file (fb/add-page page-data))] + flows (->> (get-in page-data [:options :flows]) + (mapv #(update % :starting-frame resolve))) + page-data (d/assoc-in-when page-data [:options :flows] flows) + file (-> file (fb/add-page page-data))] (->> (rx/from nodes) (rx/filter cip/shape?) (rx/mapcat (partial resolve-media context file-id)) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index d93fcfaf4..64217e014 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -18,13 +18,13 @@ (defonce state (l/atom {})) (defn index-shape - [objects parents-index masks-index] + [objects parents-index clip-parents-index] (fn [index shape] (let [{:keys [x y width height]} (gsh/points->selrect (:points shape)) shape-bound #js {:x x :y y :width width :height height} - parents (get parents-index (:id shape)) - masks (get masks-index (:id shape)) + parents (get parents-index (:id shape)) + clip-parents (get clip-parents-index (:id shape)) frame (when (and (not= :frame (:type shape)) (not= (:frame-id shape) uuid/zero)) @@ -32,19 +32,22 @@ (qdt/insert index (:id shape) shape-bound - (assoc shape :frame frame :masks masks :parents parents))))) + (assoc shape + :frame frame + :clip-parents clip-parents + :parents parents))))) (defn- create-index [objects] - (let [shapes (-> objects (dissoc uuid/zero) (vals)) - parents-index (cp/generate-child-all-parents-index objects) - masks-index (cp/create-mask-index objects parents-index) + (let [shapes (-> objects (dissoc uuid/zero) (vals)) + parents-index (cp/generate-child-all-parents-index objects) + clip-parents-index (cp/create-clip-index objects parents-index) bounds #js {:x (int -0.5e7) :y (int -0.5e7) :width (int 1e7) :height (int 1e7)} - index (reduce (index-shape objects parents-index masks-index) + index (reduce (index-shape objects parents-index clip-parents-index) (qdt/create bounds) shapes) @@ -61,18 +64,19 @@ (get new-objects id))) changed-ids (into #{} - (comp (filter changes?) - (filter #(not= % uuid/zero))) + (comp (filter #(not= % uuid/zero)) + (filter changes?) + (mapcat #(d/concat [%] (cp/get-children % new-objects)))) (set/union (set (keys old-objects)) (set (keys new-objects)))) - shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?))) - parents-index (cp/generate-child-all-parents-index new-objects shapes) - masks-index (cp/create-mask-index new-objects parents-index) + shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?))) + parents-index (cp/generate-child-all-parents-index new-objects shapes) + clip-parents-index (cp/create-clip-index new-objects parents-index) new-index (qdt/remove-all index changed-ids) - index (reduce (index-shape new-objects parents-index masks-index) + index (reduce (index-shape new-objects parents-index clip-parents-index) new-index shapes) @@ -84,7 +88,7 @@ (create-index new-objects))) (defn- query-index - [{index :index z-index :z-index} rect frame-id include-frames? full-frame? include-groups? reverse?] + [{index :index z-index :z-index} rect frame-id full-frame? include-frames? clip-children? reverse?] (let [result (-> (qdt/search index (clj->js rect)) (es6-iterator-seq)) @@ -96,7 +100,6 @@ (or (not frame-id) (= frame-id (:frame-id shape))) (case (:type shape) :frame include-frames? - :group include-groups? true) (or (not full-frame?) @@ -107,11 +110,9 @@ (fn [shape] (gsh/overlaps? shape rect)) - overlaps-masks? - (fn [masks] - (->> masks - (some (comp not overlaps?)) - not)) + overlaps-parent? + (fn [clip-parents] + (->> clip-parents (some (comp not overlaps?)) not)) add-z-index (fn [{:keys [id frame-id] :as shape}] @@ -125,7 +126,9 @@ (filter match-criteria?) (filter overlaps?) (filter (comp overlaps? :frame)) - (filter (comp overlaps-masks? :masks)) + (filter (if clip-children? + (comp overlaps-parent? :clip-parents) + (constantly true))) (map add-z-index)) result) @@ -155,10 +158,10 @@ nil) (defmethod impl/handler :selection/query - [{:keys [page-id rect frame-id include-frames? full-frame? include-groups? reverse?] - :or {include-groups? true reverse? false include-frames? false full-frame? false} :as message}] + [{:keys [page-id rect frame-id reverse? full-frame? include-frames? clip-children?] + :or {reverse? false full-frame? false include-frames? false clip-children? true} :as message}] (when-let [index (get @state page-id)] - (query-index index rect frame-id include-frames? full-frame? include-groups? reverse?))) + (query-index index rect frame-id full-frame? include-frames? clip-children? reverse?))) (defmethod impl/handler :selection/query-z-index [{:keys [page-id objects ids]}] diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index 210dfe31b..606f72ec4 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -7,6 +7,8 @@ (ns app.worker.thumbnails (:require ["react-dom/server" :as rds] + [app.common.uri :as u] + [app.config :as cfg] [app.main.exports :as exports] [app.main.fonts :as fonts] [app.util.http :as http] @@ -29,11 +31,15 @@ (defn- request-page [file-id page-id] - (let [uri "/api/rpc/query/page"] + (let [uri (u/join (cfg/get-public-uri) "api/rpc/query/page") + params {:file-id file-id + :id page-id + :strip-thumbnails true}] (->> (http/send! - {:uri uri - :query {:file-id file-id :id page-id :strip-thumbnails true} - :method :get}) + {:method :get + :uri uri + :credentials "include" + :query params}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response)))) diff --git a/frontend/test/app/components_basic_test.cljs b/frontend/test/app/components_basic_test.cljs index e4ecfca84..30def79e5 100644 --- a/frontend/test/app/components_basic_test.cljs +++ b/frontend/test/app/components_basic_test.cljs @@ -19,356 +19,338 @@ (t/use-fixtures :each {:before thp/reset-idmap!}) -(t/deftest test-add-component-from-single-shape - (t/async done - (try - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}))] +;; Test using potok +#_(t/deftest test-add-component-from-single-shape + (t/testing "test-add-component-from-single-shape" + (t/async + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"})) + store (ptk/store {:state state}) + stream (ptk/input-stream store) + end? (->> stream (rx/filter #(= ::end %)))] - (->> state - (the/do-update (dw/select-shape (thp/id :shape1))) - (the/do-watch-update dwl/add-component) - (rx/do - (fn [new-state] - (let [shape1 (thp/get-shape new-state :shape1) + (->> stream + (rx/take-until end?) + (rx/last) + (rx/do + (fn [] + (let [new-state @store + shape1 (thp/get-shape new-state :shape1) - [[group shape1] [c-group c-shape1] component] - (thl/resolve-instance-and-main + [[group shape1] [c-group c-shape1] component] + (thl/resolve-instance-and-main new-state (:parent-id shape1)) - file (dwlh/get-local-file new-state)] + file (dwlh/get-local-file new-state)] - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name group) "Component-1")) - (t/is (= (:name component) "Component-1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-group) "Component-1")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name group) "Component-1")) + (t/is (= (:name component) "Component-1")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-group) "Component-1")) - (thl/is-from-file group file)))) + (thl/is-from-file group file)))) - (rx/subs - done - #(do - (println (.-stack %)) - (done))))) + (rx/subs done #(throw %))) - (catch :default e - (println (.-stack e)) - (done))))) + (ptk/emit! + store + (dw/select-shape (thp/id :shape1)) + (dwl/add-component) + ::end))))) -(t/deftest test-add-component-from-several-shapes - (t/async done - (try - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}))] +;; FAILING +#_(t/deftest test-add-component-from-single-shape + (t/async + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}))] - (->> state - (the/do-update (dw/select-shapes (lks/set - (thp/id :shape1) - (thp/id :shape2)))) - (the/do-watch-update dwl/add-component) - (rx/do - (fn [new-state] - (let [shape1 (thp/get-shape new-state :shape1) + (->> state + (the/do-update (dw/select-shape (thp/id :shape1))) + (the/do-watch-update dwl/add-component) + (rx/do + (fn [new-state] + (let [shape1 (thp/get-shape new-state :shape1) - [[group shape1 shape2] - [c-group c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - (:parent-id shape1)) + [[group shape1] [c-group c-shape1] component] + (thl/resolve-instance-and-main + new-state + (:parent-id shape1)) - file (dwlh/get-local-file new-state)] + file (dwlh/get-local-file new-state)] - ;; NOTE: the group name depends on having executed - ;; the previous test. - (t/is (= (:name group) "Component-1")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:name component) "Component-1")) - (t/is (= (:name c-group) "Component-1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 2")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name group) "Component-1")) + (t/is (= (:name component) "Component-1")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-group) "Component-1")) - (thl/is-from-file group file)))) + (thl/is-from-file group file)))) - (rx/subs - done - #(do - (println (.-stack %)) - (done))))) + (rx/subs done #(throw %)))))) - (catch :default e - (println (.-stack e)) - (done))))) +;; FAILING +#_(t/deftest test-add-component-from-several-shapes + (t/async + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}))] + + (->> state + (the/do-update (dw/select-shapes (lks/set + (thp/id :shape1) + (thp/id :shape2)))) + (the/do-watch-update dwl/add-component) + (rx/do + (fn [new-state] + (let [shape1 (thp/get-shape new-state :shape1) + + [[group shape1 shape2] + [c-group c-shape1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + (:parent-id shape1)) + + file (dwlh/get-local-file new-state)] + + ;; NOTE: the group name depends on having executed + ;; the previous test. + (t/is (= (:name group) "Component-1")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:name component) "Component-1")) + (t/is (= (:name c-group) "Component-1")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 2")) + + (thl/is-from-file group file)))) + + (rx/subs done #(throw %)))))) -(t/deftest test-add-component-from-group - (t/async done - (try - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/group-shapes :group1 - [(thp/id :shape1) - (thp/id :shape2)]))] +#_(t/deftest test-add-component-from-group + (t/async + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/group-shapes :group1 + [(thp/id :shape1) + (thp/id :shape2)]))] - (->> state - (the/do-update (dw/select-shape (thp/id :group1))) - (the/do-watch-update dwl/add-component) - (rx/do - (fn [new-state] - (let [[[group shape1 shape2] - [c-group c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - (thp/id :group1)) + (->> state + (the/do-update (dw/select-shape (thp/id :group1))) + (the/do-watch-update dwl/add-component) + (rx/do + (fn [new-state] + (let [[[group shape1 shape2] + [c-group c-shape1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + (thp/id :group1)) - file (dwlh/get-local-file new-state)] + file (dwlh/get-local-file new-state)] - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:name group) "Group-1")) - (t/is (= (:name component) "Group-1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 2")) - (t/is (= (:name c-group) "Group-1")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:name group) "Group-1")) + (t/is (= (:name component) "Group-1")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 2")) + (t/is (= (:name c-group) "Group-1")) - (thl/is-from-file group file)))) + (thl/is-from-file group file)))) - (rx/subs - done - #(do - (println (.-stack %)) - (done))))) - - (catch :default e - (println (.-stack e)) - (done))))) + (rx/subs done #(throw %)))))) (t/deftest test-rename-component - (t/async done - (try - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :instance1 - [(thp/id :shape1)])) + (t/async + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :instance1 + [(thp/id :shape1)])) - instance1 (thp/get-shape state :instance1)] + instance1 (thp/get-shape state :instance1)] - (->> state - (the/do-watch-update (dwl/rename-component - (:component-id instance1) - "Renamed component")) - (rx/do - (fn [new-state] - (let [file (dwlh/get-local-file new-state) - component (cph/get-component - (:component-id instance1) - (:component-file instance1) - file - {})] + (->> state + (the/do-watch-update (dwl/rename-component + (:component-id instance1) + "Renamed component")) + (rx/do + (fn [new-state] + (let [file (dwlh/get-local-file new-state) + component (cph/get-component + (:component-id instance1) + (:component-file instance1) + file + {})] - (t/is (= (:name component) - "Renamed component"))))) + (t/is (= (:name component) + "Renamed component"))))) - (rx/subs - done - #(do - (println (.-stack %)) - (done))))) - - (catch :default e - (println (.-stack e)) - (done))))) + (rx/subs done #(throw %)))))) (t/deftest test-duplicate-component - (t/async done - (try - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :instance1 - [(thp/id :shape1)])) + (t/async + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :instance1 + [(thp/id :shape1)])) - instance1 (thp/get-shape state :instance1) - component-id (:component-id instance1)] + instance1 (thp/get-shape state :instance1) + component-id (:component-id instance1)] - (->> state - (the/do-watch-update (dwl/duplicate-component - {:id component-id})) - (rx/do - (fn [new-state] - (let [new-component-id (->> (get-in new-state - [:workspace-data - :components]) - (keys) - (filter #(not= % component-id)) - (first)) + (->> state + (the/do-watch-update (dwl/duplicate-component + {:id component-id})) + (rx/do + (fn [new-state] + (let [new-component-id (->> (get-in new-state + [:workspace-data + :components]) + (keys) + (filter #(not= % component-id)) + (first)) - [[instance1 shape1] - [c-instance1 c-shape1] - component1] - (thl/resolve-instance-and-main - new-state - (:id instance1)) + [[instance1 shape1] + [c-instance1 c-shape1] + component1] + (thl/resolve-instance-and-main + new-state + (:id instance1)) - [[c-component2 c-shape2] - component2] - (thl/resolve-component - new-state - new-component-id)] + [[c-component2 c-shape2] + component2] + (thl/resolve-component + new-state + new-component-id)] - (t/is (= (:name component2) - "Component-2"))))) + (t/is (= (:name component2) + "Component-2"))))) - (rx/subs - done - #(do - (println (.-stack %)) - (done))))) - - (catch :default e - (println (.-stack e)) - (done))))) + (rx/subs done #(throw %)))))) (t/deftest test-delete-component - (t/async done - (try - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :instance1 - [(thp/id :shape1)])) + (t/async + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :instance1 + [(thp/id :shape1)])) - instance1 (thp/get-shape state :instance1) - component-id (:component-id instance1)] + instance1 (thp/get-shape state :instance1) + component-id (:component-id instance1)] - (->> state - (the/do-watch-update (dwl/delete-component - {:id component-id})) - (rx/do - (fn [new-state] - (let [[instance1 shape1] - (thl/resolve-instance - new-state - (:id instance1)) + (->> state + (the/do-watch-update (dwl/delete-component + {:id component-id})) + (rx/do + (fn [new-state] + (let [[instance1 shape1] + (thl/resolve-instance + new-state + (:id instance1)) - file (dwlh/get-local-file new-state) - component (cph/get-component - (:component-id instance1) - (:component-file instance1) - file - {})] + file (dwlh/get-local-file new-state) + component (cph/get-component + (:component-id instance1) + (:component-file instance1) + file + {})] - (t/is (nil? component))))) + (t/is (nil? component))))) - (rx/subs - done - #(do - (println (.-stack %)) - (done))))) - - (catch :default e - (println (.-stack e)) - (done))))) + (rx/subs done #(throw %)))))) (t/deftest test-instantiate-component - (t/async done - (try - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :instance1 - [(thp/id :shape1)])) + (t/async + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :instance1 + [(thp/id :shape1)])) - file (dwlh/get-local-file state) - instance1 (thp/get-shape state :instance1) - component-id (:component-id instance1)] + file (dwlh/get-local-file state) + instance1 (thp/get-shape state :instance1) + component-id (:component-id instance1)] - (->> state - (the/do-watch-update (dwl/instantiate-component - (:id file) - (:component-id instance1) - (gpt/point 100 100))) - (rx/do - (fn [new-state] - (let [new-instance-id (-> new-state - wsh/lookup-selected - first) + (->> state + (the/do-watch-update (dwl/instantiate-component + (:id file) + (:component-id instance1) + (gpt/point 100 100))) + (rx/do + (fn [new-state] + (let [new-instance-id (-> new-state + wsh/lookup-selected + first) - [[instance2 shape2] - [c-instance2 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - new-instance-id)] + [[instance2 shape2] + [c-instance2 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + new-instance-id)] - (t/is (not= (:id instance1) (:id instance2))) - (t/is (= (:id component) component-id)) - (t/is (= (:name instance2) "Component-2")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance2) "Component-1")) - (t/is (= (:name c-shape2) "Rect 1"))))) + (t/is (not= (:id instance1) (:id instance2))) + (t/is (= (:id component) component-id)) + (t/is (= (:name instance2) "Component-2")) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:name c-instance2) "Component-1")) + (t/is (= (:name c-shape2) "Rect 1"))))) - (rx/subs - done - #(do - (println (.-stack %)) - (done))))) - - (catch :default e - (println (.-stack e)) - (done))))) + (rx/subs done #(throw %)))))) (t/deftest test-detach-component - (t/async done - (try - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :instance1 - [(thp/id :shape1)])) + (t/async + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :instance1 + [(thp/id :shape1)])) - instance1 (thp/get-shape state :instance1) - component-id (:component-id instance1)] + instance1 (thp/get-shape state :instance1) + component-id (:component-id instance1)] - (->> state - (the/do-watch-update (dwl/detach-component - (:id instance1))) - (rx/do - (fn [new-state] - (let [[instance1 shape1] - (thl/resolve-noninstance - new-state - (:id instance1))] + (->> state + (the/do-watch-update (dwl/detach-component + (:id instance1))) + (rx/do + (fn [new-state] + (let [[instance1 shape1] + (thl/resolve-noninstance + new-state + (:id instance1))] - (t/is (= (:name "Rect 1")))))) + (t/is (= (:name "Rect 1")))))) - (rx/subs - done - #(do - (println (.-stack %)) - (done))))) - - (catch :default e - (println (.-stack e)) - (done))))) + (rx/subs done #(throw %)))))) diff --git a/frontend/translations/ar.po b/frontend/translations/ar.po index ea4f2ff9e..3027caa6f 100644 --- a/frontend/translations/ar.po +++ b/frontend/translations/ar.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2021-08-10 23:33+0000\n" -"Last-Translator: Mahmoud A. Rabo \n" +"PO-Revision-Date: 2021-09-14 13:35+0000\n" +"Last-Translator: Amine Gdoura \n" "Language-Team: Arabic " "\n" "Language: ar\n" @@ -10,7 +10,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " "&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" -"X-Generator: Weblate 4.8-dev\n" +"X-Generator: Weblate 4.9-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -88,6 +88,10 @@ msgstr "تسجيل الدخول باستخدام OpenID (SSO)" msgid "auth.new-password" msgstr "اكتب كلمة مرور جديدة" +#: src/app/main/ui/auth/register.cljs +msgid "auth.newsletter-subscription" +msgstr "أوافق على الاشتراك في قائمة Penpot البريدية." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.invalid-token-error" msgstr "رمز الاسترداد غير صالح." @@ -160,6 +164,20 @@ msgstr "عند إنشاء حساب جديد ، فإنك توافق على شرو msgid "auth.verification-email-sent" msgstr "لقد أرسلنا رسالة تحقق إلى بريدك الالكتروني" +msgid "common.share-link.confirm-deletion-link-description" +msgstr "" +"هل أنت متأكد أنك تريد إزالة هذا الرابط؟ إذا قمت بذلك ، فلن يكون متاحًا لأي " +"شخص" + +msgid "common.share-link.get-link" +msgstr "خذ رابطا إلكتروني" + +msgid "common.share-link.link-copied-success" +msgstr "تم نسخ الرابط بنجاح" + +msgid "common.share-link.link-deleted-success" +msgstr "تم حذف الرابط بنجاح" + #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "أضف كمكتبة مشتركة" @@ -193,7 +211,7 @@ msgstr "تكرير" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.duplicate-multi" -msgstr "تكرير ٪s الملفات" +msgstr "تكرير %s الملفات" #: src/app/main/ui/dashboard/grid.cljs msgid "dashboard.empty-files" @@ -255,7 +273,7 @@ msgstr "الانتقال إلى" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to-multi" -msgstr "أنقل ٪s الملفات إلى" +msgstr "أنقل %s الملفات إلى" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to-other-team" @@ -279,7 +297,7 @@ msgstr "مشروع جديد" #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.no-matches-for" -msgstr "لم يتم العثور على مطابقات ل \"٪s\"" +msgstr "لم يتم العثور على مطابقات ل \"%s\"" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.no-projects-placeholder" @@ -299,7 +317,7 @@ msgstr "تم حفظ كلمة المرور بنجاح!" #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.num-of-members" -msgstr "٪s الأعضاء" +msgstr "%s الأعضاء" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.open-in-new-tab" @@ -338,7 +356,7 @@ msgstr "بحث…" #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.searching-for" -msgstr "البحث عن \"٪s\"…" +msgstr "البحث عن \"%s\"…" #: src/app/main/ui/settings/options.cljs msgid "dashboard.select-ui-language" @@ -442,7 +460,7 @@ msgstr "هل أنت متأكد؟" #: src/app/main/ui/dashboard/grid.cljs msgid "ds.updated-at" -msgstr "محدث: ٪s" +msgstr "محدث: %s" #: src/app/main/data/workspace.cljs msgid "errors.clipboard-not-implemented" @@ -802,7 +820,7 @@ msgid "handoff.tabs.info" msgstr "معلومات" msgid "history.alert-message" -msgstr "أنت ترى الإصدار٪ s" +msgstr "أنت ترى الإصدار %s" msgid "labels.accept" msgstr "إقبل" @@ -1228,7 +1246,7 @@ msgstr "حذف الملفات" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-multi-confirm.message" -msgstr "هل تريد بالتأكيد حذف ٪s من الملفات؟" +msgstr "هل تريد بالتأكيد حذف %s من الملفات؟" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-multi-confirm.title" @@ -1417,7 +1435,7 @@ msgstr "موفرو الخطوط - %s - Penpot" #: src/app/main/ui/dashboard/fonts.cljs msgid "title.dashboard.fonts" -msgstr "الخطوط -٪ s - Penpot" +msgstr "الخطوط - %s - Penpot" #: src/app/main/ui/dashboard/projects.cljs msgid "title.dashboard.projects" @@ -1425,11 +1443,11 @@ msgstr "المشاريع - %s - Penpot" #: src/app/main/ui/dashboard/search.cljs msgid "title.dashboard.search" -msgstr "بحث - ٪s - بينبوت" +msgstr "بحث - %s - بينبوت" #: src/app/main/ui/dashboard/libraries.cljs msgid "title.dashboard.shared-libraries" -msgstr "المكتبات المشتركة - ٪s - Penpot" +msgstr "المكتبات المشتركة - %s - Penpot" #: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/auth.cljs msgid "title.default" @@ -1453,15 +1471,15 @@ msgstr "الملف الشخصي - Penpot" #: src/app/main/ui/dashboard/team.cljs msgid "title.team-members" -msgstr "الأعضاء - ٪s - Penpot" +msgstr "الأعضاء - %s - Penpot" #: src/app/main/ui/dashboard/team.cljs msgid "title.team-settings" -msgstr "الإعدادات - ٪s - Penpot" +msgstr "الإعدادات - %s - Penpot" #: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs msgid "title.viewer" -msgstr "٪s - وضع العرض - Penpot" +msgstr "%s - وضع العرض - Penpot" #: src/app/main/ui/workspace.cljs msgid "title.workspace" @@ -1782,7 +1800,7 @@ msgstr "التغييرات غير المحفوظة" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.viewer" -msgstr "عرض الوضع (٪s)" +msgstr "عرض الوضع (%s)" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.add" @@ -1790,7 +1808,7 @@ msgstr "أضف" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.colors" -msgstr "٪s الألوان" +msgstr "%s الألوان" #: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.big-thumbnails" @@ -1814,7 +1832,7 @@ msgstr "صور مصغرة صغيرة" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.components" -msgstr "٪s المكونات" +msgstr "%s المكونات" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.file-library" @@ -1822,7 +1840,7 @@ msgstr "مكتبة الملف" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.graphics" -msgstr "٪s الرسومات" +msgstr "%s الرسومات" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.in-this-file" @@ -1866,7 +1884,7 @@ msgstr "قم بفك ارتباط كافة الأنماط المطبعية" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.typography" -msgstr "٪s الطباعة" +msgstr "%s الطباعة" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.update" diff --git a/frontend/translations/ca.po b/frontend/translations/ca.po index 5ab7d6d07..8d59bd3be 100644 --- a/frontend/translations/ca.po +++ b/frontend/translations/ca.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2021-09-04 15:33+0000\n" +"PO-Revision-Date: 2021-09-11 09:32+0000\n" "Last-Translator: Rubén \n" "Language-Team: Catalan " "\n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.8.1-dev\n" +"X-Generator: Weblate 4.9-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -91,6 +91,10 @@ msgstr "Entreu amb OpenID (SSO)" msgid "auth.new-password" msgstr "Escriviu la nova contrasenya" +#: src/app/main/ui/auth/register.cljs +msgid "auth.newsletter-subscription" +msgstr "Accepto subscriure'm a la llista de correu de Penpot." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.invalid-token-error" msgstr "El codi de recuperació no és vàlid." @@ -165,6 +169,44 @@ msgstr "" msgid "auth.verification-email-sent" msgstr "S'ha enviat un correu de verificació a" +msgid "common.share-link.confirm-deletion-link-description" +msgstr "" +"Segur que voleu eliminar l'enllaç? Si ho feu, ja no estarà disponible per a " +"ningú" + +msgid "common.share-link.get-link" +msgstr "Obtén l'enllaç" + +msgid "common.share-link.link-copied-success" +msgstr "S'ha copiat l'enllaç correctament" + +msgid "common.share-link.link-deleted-success" +msgstr "S'ha eliminat l'enllaç correctament" + +msgid "common.share-link.permissions-can-access" +msgstr "Pot accedir" + +msgid "common.share-link.permissions-can-view" +msgstr "Lector" + +msgid "common.share-link.permissions-hint" +msgstr "Qualsevol persona amb l'enllaç hi tindrà accés" + +msgid "common.share-link.remove-link" +msgstr "Elimina l'enllaç" + +msgid "common.share-link.title" +msgstr "Compartiu prototips" + +msgid "common.share-link.view-all-pages" +msgstr "Totes les pàgines" + +msgid "common.share-link.view-current-page" +msgstr "Només aquesta pàgina" + +msgid "common.share-link.view-selected-pages" +msgstr "Pàgines seleccionades" + #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Afegeix com a biblioteca compartida" @@ -204,12 +246,52 @@ msgstr "Duplica %s fitxers" msgid "dashboard.empty-files" msgstr "Encara no teniu cap arxiu aquí" +msgid "dashboard.export-frames" +msgstr "Exporta les taules de treball a PDF..." + msgid "dashboard.export-multi" msgstr "Exporta %s fitxers" msgid "dashboard.export-single" msgstr "Exporta el fitxer" +msgid "dashboard.export.detail" +msgstr "* Pot incloure components, gràfics, colors i/o tipografies." + +msgid "dashboard.export.explain" +msgstr "" +"Un o més fitxers que voleu exportar utilitzen biblioteques compartides. Què " +"voleu fer amb els seus recursos*?" + +msgid "dashboard.export.options.all.message" +msgstr "" +"els fitxers amb biblioteques compartides s’inclouran a l’exportació, " +"mantenint la vinculació." + +msgid "dashboard.export.options.all.title" +msgstr "Exporta les biblioteques compartides" + +msgid "dashboard.export.options.detach.message" +msgstr "" +"Les biblioteques compartides no s'inclouran a l'exportació i no s'afegiran " +"recursos a la biblioteca. " + +msgid "dashboard.export.options.detach.title" +msgstr "Tracta els recursos de la biblioteca compartida com a objectes bàsics" + +msgid "dashboard.export.options.merge.message" +msgstr "" +"El fitxer s'exportarà amb tots els recursos externs fusionats a la " +"biblioteca de fitxers." + +msgid "dashboard.export.options.merge.title" +msgstr "" +"Inclou els recursos de les biblioteques compartides a les biblioteques del " +"fitxer" + +msgid "dashboard.export.title" +msgstr "Exporta els fitxers" + msgid "dashboard.fonts.deleted-placeholder" msgstr "S'ha eliminat el tipus de lletra" @@ -237,6 +319,15 @@ msgstr "" msgid "dashboard.import" msgstr "Importa fitxers" +msgid "dashboard.import.analyze-error" +msgstr "Vaja! No s'ha pogut importar aquest fitxer" + +msgid "dashboard.import.import-error" +msgstr "S'ha produït un problema en importar el fitxer i no s'ha importat." + +msgid "dashboard.import.import-message" +msgstr "S'han importat %s fitxers correctament." + #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.invite-profile" msgstr "Convida a l'equip" @@ -847,6 +938,9 @@ msgstr "Cancel·la" msgid "labels.centered" msgstr "Centrat" +msgid "labels.close" +msgstr "Tanca" + #: src/app/main/ui/dashboard/comments.cljs msgid "labels.comments" msgstr "Comentaris" @@ -858,6 +952,9 @@ msgstr "Confirma la contrasenya" msgid "labels.content" msgstr "Contingut" +msgid "labels.continue" +msgstr "Continua" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "labels.create" msgstr "Crea" @@ -877,6 +974,9 @@ msgstr "Tipus de lletra personalitzats" msgid "labels.dashboard" msgstr "Panell" +msgid "labels.default" +msgstr "predeterminat" + #: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "Elimina" @@ -901,6 +1001,9 @@ msgstr "Esborranys" msgid "labels.edit" msgstr "Edita" +msgid "labels.edit-file" +msgstr "Edita el fitxer" + #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "Editor" @@ -909,6 +1012,9 @@ msgstr "Editor" msgid "labels.email" msgstr "Correu electrònic" +msgid "labels.export" +msgstr "Exporta" + #: src/app/main/ui/settings/feedback.cljs msgid "labels.feedback-disabled" msgstr "Opinions desactivades" @@ -963,6 +1069,9 @@ msgstr "Error intern" msgid "labels.language" msgstr "Llengua" +msgid "labels.link" +msgstr "Enllaç" + #: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.logout" msgstr "Tanca la sessió" @@ -1101,6 +1210,9 @@ msgstr "Servei no disponible" msgid "labels.settings" msgstr "Configuració" +msgid "labels.share-prototype" +msgstr "Comparteix el prototip" + #: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.shared-libraries" msgstr "Biblioteques compartides" @@ -1111,12 +1223,18 @@ msgstr "Mostra tots els comentaris" #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.show-your-comments" -msgstr "Mostra només els vostres comentaris" +msgstr "Mostra només els meus comentaris" #: src/app/main/ui/static.cljs msgid "labels.sign-out" msgstr "Tanca la sessió" +msgid "labels.skip" +msgstr "Omet" + +msgid "labels.start" +msgstr "Inicia" + #: src/app/main/ui/settings/profile.cljs msgid "labels.update" msgstr "Actualitza" @@ -1138,6 +1256,9 @@ msgstr "S'està pujant…" msgid "labels.viewer" msgstr "Espectador" +msgid "labels.workspace" +msgstr "Espai de treball" + #: src/app/main/ui/comments.cljs msgid "labels.write-new-comment" msgstr "Escriu un comentari nou" @@ -1406,6 +1527,120 @@ msgstr "El perfil s'ha desat correctament!" msgid "notifications.validation-email-sent" msgstr "S'ha enviat un correu electrònic de verificació a %s. Reviseu el correu!" +msgid "onboarding.contrib.alt" +msgstr "Codi obert" + +msgid "onboarding.contrib.desc1" +msgstr "" +"Penpot és de codi obert, fet per i per a la comunitat. Si voleu " +"col·laborar, sou més que benvinguts!" + +msgid "onboarding.contrib.desc2.1" +msgstr "Podeu accedir al" + +msgid "onboarding.contrib.desc2.2" +msgstr "i seguir les instruccions de contribució :)" + +msgid "onboarding.contrib.link" +msgstr "projecte a github" + +msgid "onboarding.contrib.title" +msgstr "Ets contribuïdor de codi obert?" + +msgid "onboarding.slide.0.alt" +msgstr "Crea dissenys" + +msgid "onboarding.slide.0.desc1" +msgstr "" +"Creeu interfícies d'usuari boniques en col·laboració amb tots els membres " +"de l'equip." + +msgid "onboarding.slide.0.desc2" +msgstr "" +"Manteniu la consistència a escala amb components, biblioteques i sistemes " +"de disseny." + +msgid "onboarding.slide.1.alt" +msgstr "Prototips interactius" + +msgid "onboarding.slide.1.desc1" +msgstr "Creeu interaccions enriquides per a imitar el comportament del producte." + +msgid "onboarding.slide.1.desc2" +msgstr "" +"Compartiu amb els grups d'interés, presenteu propostes a l'equip i comenceu " +"a provar els usuaris amb els vostres dissenys, tot en un sol lloc." + +msgid "onboarding.slide.1.title" +msgstr "Doneu vida als vostres dissenys amb interaccions" + +msgid "onboarding.slide.2.alt" +msgstr "Obteniu opinions" + +msgid "onboarding.slide.2.desc1" +msgstr "" +"Tot l'equip treballant simultàniament amb disseny en temps real i " +"comentaris, idees i opinions sobre els dissenys de forma centralitzada." + +msgid "onboarding.slide.2.title" +msgstr "Obteniu opinions, presenteu i compartiu el vostre treball" + +msgid "onboarding.slide.3.alt" +msgstr "Lliurament i codi baix" + +msgid "onboarding.slide.3.desc1" +msgstr "" +"Sincronitzeu el disseny i el codi de tots els components i estils i obteniu " +"fragments de codi." + +msgid "onboarding.slide.3.desc2" +msgstr "" +"Obteniu i proporcioneu especificacions de codi d'etiquetatge (SVG, HTML) o " +"d'estils (CSS, Less, Stylus...)." + +msgid "onboarding.slide.3.title" +msgstr "Una font compartida de veritat" + +msgid "onboarding.team.create.button" +msgstr "Crea un equip" + +msgid "onboarding.team.create.desc1" +msgstr "" +"Esteu treballant amb algú? Creeu un equip per a treballar junts en " +"projectes i compartir recursos de disseny." + +msgid "onboarding.team.create.input-placeholder" +msgstr "Introduïu el nom de l'equip nou" + +#, fuzzy +msgid "onboarding.team.create.title" +msgstr "Creeu un equip" + +msgid "onboarding.team.start.button" +msgstr "Comenceu de seguida" + +msgid "onboarding.team.start.desc1" +msgstr "" +"Salteu ara al Penpot i comenceu a dissenyar pel vostre compte. Tindreu " +"l'oportunitat de crear més equips després." + +msgid "onboarding.team.start.title" +msgstr "Comenceu a dissenyar" + +msgid "onboarding.welcome.alt" +msgstr "Penpot" + +msgid "onboarding.welcome.desc1" +msgstr "Estem molt contents de presentar-vos la primera versió alfa." + +msgid "onboarding.welcome.desc2" +msgstr "" +"Penpot encara està en fase de desenvolupament i hi haurà actualitzacions " +"constants. Esperem que gaudiu de la primera versió estable." + +msgid "onboarding.welcome.title" +msgstr "Vos donem la benvinguda a Penpot!" + #: src/app/main/ui/auth/recovery.cljs msgid "profile.recovery.go-to-login" msgstr "Vés a l'inici de sessió" @@ -1483,6 +1718,9 @@ msgstr "No s'ha trobat cap taula de treball a la pàgina." msgid "viewer.frame-not-found" msgstr "No s'ha trobat la taula de treball." +msgid "viewer.header.comments-section" +msgstr "Comentaris" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.dont-show-interactions" msgstr "No mostris les interaccions" @@ -1495,10 +1733,16 @@ msgstr "Edita el fitxer" msgid "viewer.header.fullscreen" msgstr "Pantalla completa" +msgid "viewer.header.handsoff-section" +msgstr "Lliurament" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.interactions" msgstr "Interaccions" +msgid "viewer.header.interactions-section" +msgstr "Interaccions" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.share.copy-link" msgstr "Copia l'enllaç" @@ -1806,10 +2050,22 @@ msgstr "Miniatures grans" msgid "workspace.libraries.colors.file-library" msgstr "Biblioteca del fitxer" +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.hsv" +msgstr "HSV" + #: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "Colors recents" +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.rgb-complementary" +msgstr "Complementari d'RGB" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.rgba" +msgstr "RGBA" + #: src/app/main/ui/workspace/colorpicker.cljs msgid "workspace.libraries.colors.save-color" msgstr "Desa l'estil de color" @@ -2073,8 +2329,9 @@ msgid "workspace.options.group-fill" msgstr "Emplenament de grup" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +#, fuzzy msgid "workspace.options.group-stroke" -msgstr "Vora del grup" +msgstr "Traç del grup" #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.color" @@ -2152,14 +2409,6 @@ msgstr "Agrupa les capes" msgid "workspace.options.layer-options.title.multiple" msgstr "Capes seleccionades" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.navigate-to" -msgstr "Vés a" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.none" -msgstr "Cap" - #: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Posició" @@ -2198,8 +2447,9 @@ msgid "workspace.options.selection-fill" msgstr "Emplenament de selecció" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +#, fuzzy msgid "workspace.options.selection-stroke" -msgstr "Vora de selecció" +msgstr "Traç de la selecció" #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.blur" @@ -2247,11 +2497,44 @@ msgstr "Mides predefinides" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke" -msgstr "Vora" +msgstr "Traç" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker" +msgstr "Marcador circular" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker" +msgstr "Marcador de diamant" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +#, fuzzy +msgid "workspace.options.stroke-cap.line-arrow" +msgstr "Fletxa de línia" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.none" +msgstr "Cap" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.round" +msgstr "Redó" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square" +msgstr "Quadrat" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker" +msgstr "Marcador quadrat" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow" +msgstr "Fletxa triangular" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke.center" -msgstr "Centre" +msgstr "Centrat" #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke.dashed" diff --git a/frontend/translations/de.po b/frontend/translations/de.po index b66d51f92..53dc3c6df 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2021-06-27 08:33+0000\n" -"Last-Translator: nautilusx \n" +"PO-Revision-Date: 2021-10-08 20:58+0000\n" +"Last-Translator: Marius \n" "Language-Team: German " "\n" "Language: de\n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.7.1-dev\n" +"X-Generator: Weblate 4.9-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -91,6 +91,10 @@ msgstr "Einloggen mit OpenID (SSO)" msgid "auth.new-password" msgstr "Geben Sie ein neues Passwort ein" +#: src/app/main/ui/auth/register.cljs +msgid "auth.newsletter-subscription" +msgstr "Ich bin damit einverstanden, die Penpot-Mailingliste zu abonnieren." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.invalid-token-error" msgstr "Der Wiederherstellungscode ist ungültig." @@ -167,6 +171,44 @@ msgstr "" msgid "auth.verification-email-sent" msgstr "Wir haben eine Bestätigungs-E-Mail gesendet an" +msgid "common.share-link.confirm-deletion-link-description" +msgstr "" +"Sind Sie sicher, dass Sie diesen Link löschen möchten? Wenn Sie das tun, " +"wird er für niemanden mehr verfügbar sein" + +msgid "common.share-link.get-link" +msgstr "Link erstellen" + +msgid "common.share-link.link-copied-success" +msgstr "Link wurde erfolgreich kopiert" + +msgid "common.share-link.link-deleted-success" +msgstr "Link wurde erfolgreich gelöscht" + +msgid "common.share-link.permissions-can-access" +msgstr "Freigabe für" + +msgid "common.share-link.permissions-can-view" +msgstr "Sichtbar" + +msgid "common.share-link.permissions-hint" +msgstr "Jeder mit dem Link kann auf die Datei zugreifen" + +msgid "common.share-link.remove-link" +msgstr "Link entfernen" + +msgid "common.share-link.title" +msgstr "Prototypen teilen" + +msgid "common.share-link.view-all-pages" +msgstr "Alle Seiten" + +msgid "common.share-link.view-current-page" +msgstr "Nur diese Seite" + +msgid "common.share-link.view-selected-pages" +msgstr "Ausgewählte Seiten" + #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Hinzufügen als gemeinsam genutzte Bibliothek" @@ -206,12 +248,51 @@ msgstr "%s Dateien duplizieren" msgid "dashboard.empty-files" msgstr "Sie haben hier noch keine Dateien" +msgid "dashboard.export-frames" +msgstr "Zeichenflächen als PDF exportieren…" + msgid "dashboard.export-multi" msgstr "%s Dateien exportieren" msgid "dashboard.export-single" msgstr "Datei exportieren" +msgid "dashboard.export.detail" +msgstr "* kann Komponenten, Grafiken, Farben und/oder Textstile enthalten." + +msgid "dashboard.export.explain" +msgstr "" +"Eine oder mehrere Dateien, die Sie exportieren möchten, verwenden geteilte " +"Bibliotheken. Was möchten Sie mit den Assets* aus diesen Bibliotheken " +"machen?" + +msgid "dashboard.export.options.all.message" +msgstr "" +"Dateien mit geteilten Bibliotheken werden exportiert, und ihre " +"Verknüpfungen bleiben erhalten." + +msgid "dashboard.export.options.all.title" +msgstr "Geteilte Bibliotheken exportieren" + +msgid "dashboard.export.options.detach.message" +msgstr "" +"Geteilte Bibliotheken werden nicht exportiert und der Bibliothek werden " +"keine Assets hinzugefügt. " + +msgid "dashboard.export.options.detach.title" +msgstr "Assets aus geteilten Bibliotheken als gewöhnliche Objekte behandeln" + +msgid "dashboard.export.options.merge.message" +msgstr "" +"Ihre Datei wird exportiert, und alle externen Assets werden der " +"Dateibibliothek hinzugefügt." + +msgid "dashboard.export.options.merge.title" +msgstr "Assets aus geteilten Bibliotheken in die Dateibibliothek aufnehmen" + +msgid "dashboard.export.title" +msgstr "Dateien exportieren" + msgid "dashboard.fonts.deleted-placeholder" msgstr "Schriftart gelöscht" @@ -228,9 +309,29 @@ msgstr "" "den folgenden Formaten hochladen: **TTF, OTF und WOFF** (nur eine wird " "benötigt)." +#, markdown +msgid "dashboard.fonts.hero-text2" +msgstr "" +"Sie sollten nur Schriftarten hochladen, die Sie besitzen oder für die Sie " +"eine Lizenz zur Verwendung in Penpot verfügen. Weitere Informationen finden " +"Sie im Abschnitt über Inhaltsrechte in den [Nutzungsbedingungen von " +"Penpot](https://penpot.app/terms.html). Mehr über die [Lizenzierung von " +"Schriftarten erfahren Sie hier](https://www.typography.com/faq)." + msgid "dashboard.import" msgstr "Dateien importieren" +msgid "dashboard.import.analyze-error" +msgstr "Huch! Wir konnten diese Datei nicht importieren" + +msgid "dashboard.import.import-error" +msgstr "" +"Beim Importieren der Datei ist ein Fehler aufgetreten. Die Datei wurde " +"nicht importiert." + +msgid "dashboard.import.import-message" +msgstr "%s Dateien wurden erfolgreich importiert." + #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.invite-profile" msgstr "Zum Team einladen" @@ -254,6 +355,10 @@ msgstr "laden Ihrer Schriftarten …" msgid "dashboard.move-to" msgstr "Verschieben nach" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-multi" +msgstr "%s Dateien verschieben" + #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to-other-team" msgstr "Zu anderem Team verschieben" @@ -369,6 +474,10 @@ msgstr "Ihr Projekt wurde erfolgreich dupliziert" msgid "dashboard.success-move-file" msgstr "Ihre Datei wurde erfolgreich verschoben" +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-files" +msgstr "Ihre Dateien wurden erfolgreich verschoben" + #: src/app/main/ui/dashboard/project_menu.cljs msgid "dashboard.success-move-project" msgstr "Ihr Projekt wurde erfolgreich verschoben" @@ -522,6 +631,10 @@ msgstr "" "Sie müssen unsere Nutzungsbedingungen und Datenschutzrichtlinien " "akzeptieren." +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.token-expired" +msgstr "Token abgelaufen" + #: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs msgid "errors.unexpected-error" msgstr "Ein unerwarteter Fehler ist aufgetreten." @@ -801,6 +914,9 @@ msgstr "Sie sehen Version %s" msgid "labels.accept" msgstr "Akzeptieren" +msgid "labels.add-custom-font" +msgstr "Eigene Schriftart hinzufügen" + #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" msgstr "Admin" @@ -826,6 +942,9 @@ msgstr "Abbrechen" msgid "labels.centered" msgstr "Zentriert" +msgid "labels.close" +msgstr "Schließen" + #: src/app/main/ui/dashboard/comments.cljs msgid "labels.comments" msgstr "Kommentare" @@ -837,6 +956,9 @@ msgstr "Passwort bestätigen" msgid "labels.content" msgstr "Inhalt" +msgid "labels.continue" +msgstr "Weiter" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "labels.create" msgstr "Erstellen" @@ -849,10 +971,16 @@ msgstr "Neues Team erstellen" msgid "labels.create-team.placeholder" msgstr "Neuen Teamnamen eingeben" +msgid "labels.custom-fonts" +msgstr "Eigene Schriftarten" + #: src/app/main/ui/settings/sidebar.cljs msgid "labels.dashboard" msgstr "Dashboard" +msgid "labels.default" +msgstr "Standardeinstellung" + #: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" msgstr "Löschen" @@ -877,6 +1005,9 @@ msgstr "Entwürfe" msgid "labels.edit" msgstr "Bearbeiten" +msgid "labels.edit-file" +msgstr "Datei bearbeiten" + #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" msgstr "Editor" @@ -885,6 +1016,9 @@ msgstr "Editor" msgid "labels.email" msgstr "E-Mail" +msgid "labels.export" +msgstr "Exportieren" + #: src/app/main/ui/settings/feedback.cljs msgid "labels.feedback-disabled" msgstr "Feedback deaktiviert" @@ -893,6 +1027,12 @@ msgstr "Feedback deaktiviert" msgid "labels.feedback-sent" msgstr "Feedback gesendet" +msgid "labels.font-family" +msgstr "Schriftfamilie" + +msgid "labels.font-providers" +msgstr "Schriftenhersteller" + msgid "labels.font-variants" msgstr "Stile" @@ -933,6 +1073,9 @@ msgstr "Interner Fehler" msgid "labels.language" msgstr "Sprache" +msgid "labels.link" +msgstr "Link" + #: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.logout" msgstr "Abmelden" @@ -976,6 +1119,11 @@ msgid_plural "labels.num-of-files" msgstr[0] "1 Datei" msgstr[1] "%s Dateien" +msgid "labels.num-of-frames" +msgid_plural "labels.num-of-frames" +msgstr[0] "1 Zeichenfläche" +msgstr[1] "%s Zeichenflächen" + #: src/app/main/ui/dashboard/team.cljs msgid "labels.num-of-projects" msgid_plural "labels.num-of-projects" @@ -1086,6 +1234,12 @@ msgstr "Nur eigene Kommentare anzeigen" msgid "labels.sign-out" msgstr "Abmelden" +msgid "labels.skip" +msgstr "Überspringen" + +msgid "labels.start" +msgstr "Start" + #: src/app/main/ui/settings/profile.cljs msgid "labels.update" msgstr "Aktualisieren" @@ -1094,10 +1248,22 @@ msgstr "Aktualisieren" msgid "labels.update-team" msgstr "Team aktualisieren" +msgid "labels.upload" +msgstr "Hochladen" + +msgid "labels.upload-custom-fonts" +msgstr "Eigene Schriftarten hochladen" + +msgid "labels.uploading" +msgstr "Lädt hoch…" + #: src/app/main/ui/dashboard/team.cljs msgid "labels.viewer" msgstr "Zuschauer" +msgid "labels.workspace" +msgstr "Arbeitsbereich" + #: src/app/main/ui/comments.cljs msgid "labels.write-new-comment" msgstr "Neuen Kommentar schreiben" @@ -1198,6 +1364,19 @@ msgstr "Sind Sie sicher, dass Sie %s Dateien löschen möchten?" msgid "modals.delete-file-multi-confirm.title" msgstr "Lösche %s Dateien" +msgid "modals.delete-font-variant.message" +msgstr "" +"Sind Sie sicher, dass Sie diesen Schriftschnitt löschen möchten? Sie wird " +"nicht mehr geladen, wenn sie bereits in einer Datei verwendet wird." + +msgid "modals.delete-font-variant.title" +msgstr "Schriftschnitt löschen" + +msgid "modals.delete-font.message" +msgstr "" +"Sind Sie sicher, dass Sie diese Schriftart löschen möchten? Sie wird nicht " +"mehr geladen, wenn sie bereits in einer Datei verwendet wird." + msgid "modals.delete-font.title" msgstr "Schriftart löschen" @@ -1255,6 +1434,11 @@ msgstr "Einladung senden" msgid "modals.invite-member.title" msgstr "Einladen, dem Team beizutreten" +msgid "modals.leave-and-reassign.forbiden" +msgstr "" +"Sie können das Team nicht verlassen, wenn es kein anderes Mitglied gibt, " +"das Sie zum Besitzer ernennen können. Sie können das Team jedoch löschen." + #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-reassign.hint1" msgstr "Sie sind der Eigentümer von %s." @@ -1352,6 +1536,101 @@ msgstr "Profil erfolgreich gespeichert!" msgid "notifications.validation-email-sent" msgstr "Verifizierungs-E-Mail an %s gesendet. Prüfen Sie Ihren Posteingang!" +msgid "onboarding.contrib.alt" +msgstr "Open Source" + +msgid "onboarding.slide.0.desc1" +msgstr "" +"Erstellen Sie ansprechende Benutzeroberflächen gemeinsam mit anderen " +"Teammitgliedern." + +msgid "onboarding.slide.0.desc2" +msgstr "" +"Schaffen Sie ein einheitliches Erscheinungsbild mit Hilfe von Komponenten, " +"Bibliotheken und Design-Systemen." + +msgid "onboarding.slide.0.title" +msgstr "Design-Bibliotheken, Stile und Komponenten" + +msgid "onboarding.slide.1.alt" +msgstr "Interaktive Prototypen" + +msgid "onboarding.slide.1.desc1" +msgstr "" +"Erstellen Sie umfangreiche Interaktionen, um das reale Verhalten ihres " +"Produktes zu simulieren." + +msgid "onboarding.slide.1.desc2" +msgstr "" +"Teilen Sie Ihre Vorschläge mit allen Beteiligten und testen Sie Entwürfe " +"direkt mit Ihren Nutzern - alles an einem Ort." + +msgid "onboarding.slide.1.title" +msgstr "Hauchen Sie Ihren Entwürfen mit Interaktionen Leben ein" + +msgid "onboarding.slide.2.alt" +msgstr "Erhalten Sie Feedback" + +msgid "onboarding.slide.2.desc1" +msgstr "" +"Alle Teammitglieder können in Echtzeit an Entwürfen arbeiten – und erhalten " +"direktes Feedback über die integrierte Kommentarfunktion." + +msgid "onboarding.slide.2.title" +msgstr "Holen Sie sich Feedback, präsentieren und teilen Sie Ihre Entwürfe" + +msgid "onboarding.slide.3.desc1" +msgstr "" +"Synchronisieren Sie das Design und den Code all Ihrer Komponenten und Stile " +"und nutzen Sie Code-Snippets." + +msgid "onboarding.slide.3.desc2" +msgstr "" +"Erstellen und erhalten Sie Spezifikationen in Auszeichnungs- (SVG, HTML) " +"oder Stylesheet-Sprachen (CSS, Less, Stylus…)." + +msgid "onboarding.slide.3.title" +msgstr "Eine zentrale Anlaufstelle für alle" + +msgid "onboarding.team.create.button" +msgstr "Team erstellen" + +msgid "onboarding.team.create.desc1" +msgstr "" +"Arbeiten Sie mit jemandem zusammen? Erstellen Sie ein Team, um gemeinsam an " +"Projekten zu arbeiten und Design-Assets zu teilen." + +msgid "onboarding.team.create.input-placeholder" +msgstr "Neuen Teamnamen eingeben" + +msgid "onboarding.team.create.title" +msgstr "Team erstellen" + +msgid "onboarding.team.start.button" +msgstr "Gleich loslegen" + +msgid "onboarding.team.start.desc1" +msgstr "" +"Legen Sie gleich los und gestalten Sie erstmal alleine. Sie können später " +"immer noch Teams erstellen." + +msgid "onboarding.team.start.title" +msgstr "Mit der Gestaltung beginnen" + +msgid "onboarding.welcome.alt" +msgstr "Penpot" + +msgid "onboarding.welcome.desc1" +msgstr "Wir freuen uns sehr, Ihnen die erste Alpha-Version vorstellen zu können." + +msgid "onboarding.welcome.desc2" +msgstr "" +"Penpot befindet sich noch in der Entwicklungsphase und wird ständig " +"aktualisiert. Wir hoffen, dass Ihnen die erste stabile Version gefällt." + +msgid "onboarding.welcome.title" +msgstr "Willkommen bei Penpot!" + #: src/app/main/ui/auth/recovery.cljs msgid "profile.recovery.go-to-login" msgstr "Zur Anmeldung" @@ -1360,6 +1639,18 @@ msgstr "Zur Anmeldung" msgid "settings.multiple" msgstr "Mehrere" +#: src/app/main/ui/dashboard/files.cljs +msgid "title.dashboard.files" +msgstr "%s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.font-providers" +msgstr "Schriftenhersteller - %s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.fonts" +msgstr "Schriftarten - %s - Penpot" + #: src/app/main/ui/dashboard/projects.cljs msgid "title.dashboard.projects" msgstr "Projekte - %s - Penpot" @@ -1372,6 +1663,10 @@ msgstr "Suchen - %s - Penpot" msgid "title.dashboard.shared-libraries" msgstr "Gemeinsam genutzte Bibliotheken - %s - Penpot" +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/auth.cljs +msgid "title.default" +msgstr "Penpot - Gestaltungsfreiheit für Teams" + #: src/app/main/ui/settings/feedback.cljs msgid "title.settings.feedback" msgstr "Feedback geben - Penpot" @@ -1400,6 +1695,10 @@ msgstr "Einstellungen - %s - Penpot" msgid "title.viewer" msgstr "%s - Ansichtsmodus - Penpot" +#: src/app/main/ui/workspace.cljs +msgid "title.workspace" +msgstr "%s - Penpot" + #: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs msgid "viewer.empty-state" msgstr "Keine Zeichenflächen auf der Seite gefunden." @@ -1408,6 +1707,9 @@ msgstr "Keine Zeichenflächen auf der Seite gefunden." msgid "viewer.frame-not-found" msgstr "Keine Zeichenfläche gefunden." +msgid "viewer.header.comments-section" +msgstr "Kommentare" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.dont-show-interactions" msgstr "Interaktionen nicht anzeigen" @@ -1420,10 +1722,16 @@ msgstr "Seite bearbeiten" msgid "viewer.header.fullscreen" msgstr "Vollbildmodus" +msgid "viewer.header.handsoff-section" +msgstr "Übergabe" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.interactions" msgstr "Interaktionen" +msgid "viewer.header.interactions-section" +msgstr "Interaktionen" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.share.copy-link" msgstr "Link kopieren" @@ -1507,6 +1815,10 @@ msgstr "Farben" msgid "workspace.assets.components" msgstr "Komponente" +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.create-group" +msgstr "Gruppe erstellen" + #: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "Löschen" @@ -1527,6 +1839,14 @@ msgstr "Dateibibliothek" msgid "workspace.assets.graphics" msgstr "Grafiken" +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group" +msgstr "Gruppieren" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group-name" +msgstr "Name der Gruppe" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.libraries" msgstr "Bibliotheken" @@ -1539,10 +1859,20 @@ msgstr "Keine Assets gefunden" msgid "workspace.assets.rename" msgstr "Umbenennen" +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.rename-group" +msgstr "Gruppe umbenennen" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.search" msgstr "Assets suchen" +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.selected-count" +msgid_plural "workspace.assets.selected-count" +msgstr[0] "%s Element ausgewählt" +msgstr[1] "%s Elemente ausgewählt" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.shared" msgstr "GETEILT" @@ -1583,6 +1913,10 @@ msgstr "Ag" msgid "workspace.assets.typography.text-transform" msgstr "Texttransformation" +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.ungroup" +msgstr "Gruppierung aufheben" + #: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "Linearer Farbverlauf" @@ -1595,6 +1929,10 @@ msgstr "Radialer Farbverlauf" msgid "workspace.header.menu.disable-dynamic-alignment" msgstr "Dynamische Ausrichtung deaktivieren" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-scale-text" +msgstr "Textskalierung ausschalten" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.disable-snap-grid" msgstr "Am Raster ausrichten deaktivieren" @@ -1603,6 +1941,10 @@ msgstr "Am Raster ausrichten deaktivieren" msgid "workspace.header.menu.enable-dynamic-alignment" msgstr "Dynamische Ausrichtung aktivieren" +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-scale-text" +msgstr "Textskalierung einschalten" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.enable-snap-grid" msgstr "Am Raster ausrichten" @@ -1687,10 +2029,18 @@ msgstr "Große Miniaturen" msgid "workspace.libraries.colors.file-library" msgstr "Dateibibliothek" +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.hsv" +msgstr "HSV" + #: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "Aktuelle Farben" +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.rgba" +msgstr "RGBA" + #: src/app/main/ui/workspace/colorpicker.cljs msgid "workspace.libraries.colors.save-color" msgstr "Farbstil speichern" @@ -1803,6 +2153,46 @@ msgstr "Hintergrundfarbe" msgid "workspace.options.component" msgstr "Komponente" +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints" +msgstr "Constraints" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.bottom" +msgstr "Unten" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.center" +msgstr "Mittig" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.fix-when-scrolling" +msgstr "Beim Scrollen fixieren" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.left" +msgstr "Links" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.leftright" +msgstr "Links & Rechts" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.right" +msgstr "Rechts" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.scale" +msgstr "Skalieren" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.top" +msgstr "Oben" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.topbottom" +msgstr "Oben & Unten" + #: src/app/main/ui/workspace/sidebar/options.cljs msgid "workspace.options.design" msgstr "Design" @@ -1995,14 +2385,6 @@ msgstr "Ebenen gruppieren" msgid "workspace.options.layer-options.title.multiple" msgstr "Ausgewählte Ebenen" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.navigate-to" -msgstr "Navigiere zu" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.none" -msgstr "Keine" - #: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Position" @@ -2092,6 +2474,38 @@ msgstr "Größenvoreinstellungen" msgid "workspace.options.stroke" msgstr "Rahmen" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker" +msgstr "Punkt" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker" +msgstr "Diamant" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow" +msgstr "Pfeil" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.none" +msgstr "Keine" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.round" +msgstr "Rund" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square" +msgstr "Rechteckig" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker" +msgstr "Quadrat" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow" +msgstr "Dreieckiger Pfeil" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke.center" msgstr "Zentriert" @@ -2151,6 +2565,14 @@ msgstr "Oben ausrichten" msgid "workspace.options.text-options.decoration" msgstr "Textdekoration" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.direction-ltr" +msgstr "von links nach rechts" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.direction-rtl" +msgstr "von rechts nach links" + #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.google" msgstr "Google" @@ -2227,6 +2649,36 @@ msgstr "" "Verwenden Sie die Wiedergabetaste in der Kopfzeile, um die Prototypansicht " "zu wechseln." +msgid "workspace.path.actions.add-node" +msgstr "Ankerpunkt hinzufügen (%s)" + +msgid "workspace.path.actions.delete-node" +msgstr "Ankerpunkt entfernen (%s)" + +msgid "workspace.path.actions.draw-nodes" +msgstr "Ankerpunkte zeichnen (%s)" + +msgid "workspace.path.actions.join-nodes" +msgstr "Ankerpunkte verbinden (%s)" + +msgid "workspace.path.actions.make-corner" +msgstr "in Ecke umwandeln (%s)" + +msgid "workspace.path.actions.make-curve" +msgstr "in Kurve umwandeln (%s)" + +msgid "workspace.path.actions.merge-nodes" +msgstr "Ankerpunkte zusammenlegen (%s)" + +msgid "workspace.path.actions.move-nodes" +msgstr "Ankerpunkte verschieben (%s)" + +msgid "workspace.path.actions.separate-nodes" +msgstr "Ankerpunkte trennen (%s)" + +msgid "workspace.path.actions.snap-nodes" +msgstr "An Ankerpunkten ausrichten (%s)" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" msgstr "In den Hintergrund" diff --git a/frontend/translations/el.po b/frontend/translations/el.po index 3812b4182..0ede3c579 100644 --- a/frontend/translations/el.po +++ b/frontend/translations/el.po @@ -1832,14 +1832,6 @@ msgstr "στρώματα Ομάδα" msgid "workspace.options.layer-options.title.multiple" msgstr "Επιλεγμένα επίπεδα" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.navigate-to" -msgstr "Μεταβείτε στο" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.none" -msgstr "Κανένας" - #: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Θέση" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 2a92e779b..691cc14e6 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -121,6 +121,9 @@ msgstr "Password" msgid "auth.password-length-hint" msgstr "At least 8 characters" +msgid "auth.privacy-policy" +msgstr "Privacy policy" + #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.recovery-request-submit" msgstr "Recover Password" @@ -157,6 +160,9 @@ msgstr "Create an account" msgid "auth.sidebar-tagline" msgstr "The open-source solution for design and prototyping." +msgid "auth.terms-of-service" +msgstr "Terms of service" + #: src/app/main/ui/auth/register.cljs msgid "auth.terms-privacy-agreement" msgstr "" @@ -323,27 +329,27 @@ msgstr "There was a problem importing the file. The file wasn't imported." msgid "dashboard.import.import-message" msgstr "%s files have been imported succesfully." +msgid "dashboard.import.progress.process-colors" +msgstr "Processing colors" + +msgid "dashboard.import.progress.process-components" +msgstr "Processing components" + +msgid "dashboard.import.progress.process-media" +msgstr "Processing media" + +msgid "dashboard.import.progress.process-page" +msgstr "Processing page: %s" + +msgid "dashboard.import.progress.process-typographies" +msgstr "Processing typographies" + msgid "dashboard.import.progress.upload-data" msgstr "Uploading data to server (%s/%s)" msgid "dashboard.import.progress.upload-media" msgstr "Uploading file: %s" -msgid "dashboard.import.progress.process-page" -msgstr "Processing page: %s" - -msgid "dashboard.import.progress.process-colors" -msgstr "Processing colors" - -msgid "dashboard.import.progress.process-typographies" -msgstr "Processing typographies" - -msgid "dashboard.import.progress.process-media" -msgstr "Processing media" - -msgid "dashboard.import.progress.process-components" -msgstr "Processing components" - #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.invite-profile" msgstr "Invite to team" @@ -1707,6 +1713,14 @@ msgstr "%s - View mode - Penpot" msgid "title.workspace" msgstr "%s - Penpot" +msgid "viewer.breaking-change.description" +msgstr "" +"This shareable link is no longer valid. Create a new one or ask the owner " +"for a new one." + +msgid "viewer.breaking-change.message" +msgstr "Sorry!" + #: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs msgid "viewer.empty-state" msgstr "No artboards found on the page." @@ -2137,6 +2151,10 @@ msgstr "My libraries" msgid "workspace.library.store" msgstr "Store libraries" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.add-interaction" +msgstr "Click the + button to add interactions." + msgid "workspace.options.blur-options.background-blur" msgstr "Background" @@ -2227,6 +2245,18 @@ msgstr "Exporting…" msgid "workspace.options.fill" msgstr "Fill" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.add-flow-start" +msgstr "Add flow start" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow-start" +msgstr "Flow start" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow-starts" +msgstr "Flow starts" + #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.auto" msgstr "Auto" @@ -2319,6 +2349,150 @@ msgstr "Group fill" msgid "workspace.options.group-stroke" msgstr "Group stroke" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-action" +msgstr "Action" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-after-delay" +msgstr "After delay" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-background" +msgstr "Add background overlay" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-outside" +msgstr "Close when clicking outside" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-overlay" +msgstr "Close overlay" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-overlay-dest" +msgstr "Close overlay: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-delay" +msgstr "Delay" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-destination" +msgstr "Destination" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-mouse-enter" +msgstr "Mouse enter" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-mouse-leave" +msgstr "Mouse leave" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-ms" +msgstr "ms" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-navigate-to" +msgstr "Navigate to" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-navigate-to-dest" +msgstr "Navigate to: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-none" +msgstr "(not set)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-on-click" +msgstr "On Click" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-overlay" +msgstr "Open overlay" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-overlay-dest" +msgstr "Open overlay: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-url" +msgstr "Open url" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-center" +msgstr "Bottom center" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-left" +msgstr "Bottom left" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-right" +msgstr "Bottom right" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-center" +msgstr "Center" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-manual" +msgstr "Manual" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-top-center" +msgstr "Top center" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-top-left" +msgstr "Top left" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-top-right" +msgstr "Top right" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-position" +msgstr "Position" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-prev-screen" +msgstr "Previous screen" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-self" +msgstr "self" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-toggle-overlay" +msgstr "Toggle overlay" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-toggle-overlay-dest" +msgstr "Toggle overlay: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-trigger" +msgstr "Trigger" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-url" +msgstr "URL" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-while-hovering" +msgstr "While Hovering" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-while-pressing" +msgstr "While Pressing" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interactions" +msgstr "Interactions" + #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.color" msgstr "Color" @@ -2395,14 +2569,6 @@ msgstr "Group layers" msgid "workspace.options.layer-options.title.multiple" msgstr "Selected layers" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.navigate-to" -msgstr "Navigate to" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.none" -msgstr "None" - #: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Position" @@ -2426,6 +2592,9 @@ msgstr "Single corners" msgid "workspace.options.rotation" msgstr "Rotation" +msgid "workspace.options.search-font" +msgstr "Search font" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.select-a-shape" msgstr "Select a shape, artboard or group to drag a connection to other artboard." @@ -2717,10 +2886,17 @@ msgstr "Cut" msgid "workspace.shape.menu.delete" msgstr "Delete" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.delete-flow-start" +msgstr "Delete flow start" + #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Detach instance" +msgid "workspace.shape.menu.difference" +msgstr "Difference" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.duplicate" msgstr "Duplicate" @@ -2729,6 +2905,12 @@ msgstr "Duplicate" msgid "workspace.shape.menu.edit" msgstr "Edit" +msgid "workspace.shape.menu.exclude" +msgstr "Exclude" + +msgid "workspace.shape.menu.flatten" +msgstr "Flatten" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.flip-horizontal" msgstr "Flip horizontal" @@ -2737,6 +2919,10 @@ msgstr "Flip horizontal" msgid "workspace.shape.menu.flip-vertical" msgstr "Flip vertical" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flow-start" +msgstr "Flow start" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.forward" msgstr "Bring forward" @@ -2757,6 +2943,9 @@ msgstr "Group" msgid "workspace.shape.menu.hide" msgstr "Hide" +msgid "workspace.shape.menu.intersection" +msgstr "Intersection" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.lock" msgstr "Lock" @@ -2769,6 +2958,9 @@ msgstr "Mask" msgid "workspace.shape.menu.paste" msgstr "Paste" +msgid "workspace.shape.menu.path" +msgstr "Path" + #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "Reset overrides" @@ -2781,10 +2973,16 @@ msgstr "Show" msgid "workspace.shape.menu.show-main" msgstr "Show main component" +msgid "workspace.shape.menu.transform-to-path" +msgstr "Transform to path" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.ungroup" msgstr "Ungroup" +msgid "workspace.shape.menu.union" +msgstr "Union" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.unlock" msgstr "Unlock" @@ -2989,11 +3187,4 @@ msgid "workspace.updates.update" msgstr "Update" msgid "workspace.viewport.click-to-close-path" -msgstr "Click to close the path" - -msgid "viewer.breaking-change.message" -msgstr "Sorry!" - -msgid "viewer.breaking-change.description" -msgstr "This shareable link is no longer valid. Create a new one or ask the owner for a new one. - +msgstr "Click to close the path" \ No newline at end of file diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 265eb513c..a7a3afe5f 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2021-09-04 15:33+0000\n" -"Last-Translator: Rubén \n" +"PO-Revision-Date: 2021-09-24 13:39+0000\n" +"Last-Translator: andy \n" "Language-Team: Spanish " "\n" "Language: es\n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.8.1-dev\n" +"X-Generator: Weblate 4.9-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -65,7 +65,7 @@ msgstr "Introduce tus datos aquí" #: src/app/main/ui/auth/login.cljs msgid "auth.login-title" -msgstr "¡Encantados de verte de nuevo!" +msgstr "¡Un placer verte de nuevo!" #: src/app/main/ui/auth/login.cljs msgid "auth.login-with-github-submit" @@ -93,7 +93,7 @@ msgstr "Introduce la nueva contraseña" #: src/app/main/ui/auth/register.cljs msgid "auth.newsletter-subscription" -msgstr "Estoy de acuerdo en suscribirme a la lista de correo de Penpot" +msgstr "Estoy de acuerdo en suscribirme a la lista de correo de Penpot." #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.invalid-token-error" @@ -125,6 +125,9 @@ msgstr "Contraseña" msgid "auth.password-length-hint" msgstr "8 caracteres como mínimo" +msgid "auth.privacy-policy" +msgstr "Política de privacidad" + #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.recovery-request-submit" msgstr "Recuperar contraseña" @@ -161,6 +164,9 @@ msgstr "Crear una cuenta" msgid "auth.sidebar-tagline" msgstr "La solución de código abierto para diseñar y prototipar" +msgid "auth.terms-of-service" +msgstr "Terminos de servicio" + #: src/app/main/ui/auth/register.cljs msgid "auth.terms-privacy-agreement" msgstr "" @@ -174,7 +180,7 @@ msgstr "Hemos enviado un email de verificación a" msgid "common.share-link.confirm-deletion-link-description" msgstr "" "¿Estas seguro que quieres eliminar el enlace? Si lo haces, el enlace dejara " -"de funcionar para todos" +"de funcionar para cualquiera" msgid "common.share-link.get-link" msgstr "Obtener enlace" @@ -276,10 +282,10 @@ msgstr "Exportar librerias compartidas" msgid "dashboard.export.options.detach.message" msgstr "" "Las librerias compartidas no se incluirán en la exportación y ningún " -"recurso será incluido en la librería." +"recurso será incluido en la librería. " msgid "dashboard.export.options.detach.title" -msgstr "Usar los recursos como objetos básicos." +msgstr "Usar los recursos como objetos básicos" msgid "dashboard.export.options.merge.message" msgstr "" @@ -287,7 +293,7 @@ msgstr "" "propio fichero." msgid "dashboard.export.options.merge.title" -msgstr "Incluir librerias compartidas dentro de las librerias del fichero." +msgstr "Incluir librerias compartidas dentro de las librerias del fichero" msgid "dashboard.export.title" msgstr "Exportar ficheros" @@ -316,39 +322,39 @@ msgstr "" "te puede interesar leer más sobre licencias tipográficas: [font " "licensing](2)." +msgid "dashboard.import" +msgstr "Importar archivos" + msgid "dashboard.import.analyze-error" -msgstr "¡Vaya! No hemos podido importar el fichero." +msgstr "¡Vaya! No hemos podido importar el fichero" msgid "dashboard.import.import-error" -msgstr "Hubo un problema importando el fichero. No se ha creado el fichero." - -msgid "dashboard.import.import-message" -msgstr "%s ficheros han sido importados con éxito." +msgstr "Hubo un problema importando el fichero. No ha podido ser importado." msgid "dashboard.import.import-message" msgstr "%s files have been imported succesfully." +msgid "dashboard.import.progress.process-colors" +msgstr "Procesando colores" + +msgid "dashboard.import.progress.process-components" +msgstr "Procesando componentes" + +msgid "dashboard.import.progress.process-media" +msgstr "Procesando media" + +msgid "dashboard.import.progress.process-page" +msgstr "Procesando página: %s" + +msgid "dashboard.import.progress.process-typographies" +msgstr "Procesando tipografías" + msgid "dashboard.import.progress.upload-data" msgstr "Enviando datos al servidor (%s/%s)" msgid "dashboard.import.progress.upload-media" msgstr "Enviando fichero: %s" -msgid "dashboard.import.progress.process-page" -msgstr "Procesando página: %s" - -msgid "dashboard.import.progress.process-colors" -msgstr "Procesando colores" - -msgid "dashboard.import.progress.process-typographies" -msgstr "Procesando tipografías" - -msgid "dashboard.import.progress.process-media" -msgstr "Procesando media" - -msgid "dashboard.import.progress.process-components" -msgstr "Procesando componentes" - #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.invite-profile" msgstr "Invitar al equipo" @@ -424,6 +430,9 @@ msgstr "%s integrantes" msgid "dashboard.open-in-new-tab" msgstr "Abrir en una pestaña nueva" +msgid "dashboard.options" +msgstr "Opciones" + #: src/app/main/ui/settings/password.cljs msgid "dashboard.password-change" msgstr "Cambiar contraseña" @@ -927,7 +936,7 @@ msgid "labels.accept" msgstr "Aceptar" msgid "labels.add-custom-font" -msgstr "Añadir fuentes personalizada" +msgstr "Añadir fuente personalizada" #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.admin" @@ -1244,6 +1253,12 @@ msgstr "Mostrar sólo tus comentarios" msgid "labels.sign-out" msgstr "Salir" +msgid "labels.skip" +msgstr "Omitir" + +msgid "labels.start" +msgstr "Comenzar" + #: src/app/main/ui/settings/profile.cljs msgid "labels.update" msgstr "Actualizar" @@ -1531,6 +1546,118 @@ msgstr "Perfil guardado correctamente!" msgid "notifications.validation-email-sent" msgstr "Verificación de email enviada a %s. Comprueba tu correo." +msgid "onboarding.contrib.alt" +msgstr "Código Abierto" + +msgid "onboarding.contrib.desc1" +msgstr "" +"Penpot es Open Source, hecho por y para la comunidad. Si decides colaborar " +"nos encantará :)" + +msgid "onboarding.contrib.desc2.1" +msgstr "Puedes acceder al" + +msgid "onboarding.contrib.desc2.2" +msgstr "y seguir las instrucciones de contribución :)" + +msgid "onboarding.contrib.link" +msgstr "proyecto en github" + +msgid "onboarding.contrib.title" +msgstr "Contribuyes en Open Source?" + +msgid "onboarding.slide.0.alt" +msgstr "Crea diseños" + +msgid "onboarding.slide.0.desc1" +msgstr "Crea bellos interfaces en colaboración con todas las personas del equipo." + +msgid "onboarding.slide.0.desc2" +msgstr "" +"Mantén consistencia a escala usando componentes, bibliotecas y sistemas de " +"diseño." + +msgid "onboarding.slide.0.title" +msgstr "Bibliotecas de diseño, estilos y componentes" + +msgid "onboarding.slide.1.alt" +msgstr "Prototipos interactivos" + +msgid "onboarding.slide.1.desc1" +msgstr "Crea interacciones completas para imitar el comportamiento del producto." + +msgid "onboarding.slide.1.desc2" +msgstr "" +"Comparte con stakeholders, presenta propuestas a tu equipo y realiza tests " +"de usabilidad sobre tus diseños, todo en uno." + +msgid "onboarding.slide.1.title" +msgstr "Lleva tus diseños a la vida con interacciones" + +msgid "onboarding.slide.2.alt" +msgstr "Obtén feedback" + +msgid "onboarding.slide.2.desc1" +msgstr "" +"Todas las personas del equipo trabajando simultáneamente en archivos de " +"diseño con edición conjunta en tiempo real y herramientas para comentar " +"directamente sobre los diseños." + +msgid "onboarding.slide.2.title" +msgstr "Obtén feedback, presenta y comparte tu trabajo" + +msgid "onboarding.slide.3.alt" +msgstr "Especificaciones de código" + +msgid "onboarding.slide.3.desc1" +msgstr "" +"Sincroniza diseño y código de todos tus estilos y componentes con a " +"snippets de código." + +msgid "onboarding.slide.3.desc2" +msgstr "" +"Obtén y entrega especificaciones de código para marcado (SVG, HTML) y " +"estilos (CSS, Less, Stylus…)." + +msgid "onboarding.slide.3.title" +msgstr "Una \"fuente de la verdad\" compartida" + +msgid "onboarding.team.create.button" +msgstr "Crea un equipo" + +msgid "onboarding.team.create.desc1" +msgstr "" +"¿Trabajando con alguien más? Crea un equipo donde compartir proyectos y " +"elementos de diseño." + +msgid "onboarding.team.create.input-placeholder" +msgstr "Introduce el nombre del equipo" + +msgid "onboarding.team.create.title" +msgstr "Crear equipo" + +msgid "onboarding.team.start.button" +msgstr "Comenzar directamente" + +msgid "onboarding.team.start.desc1" +msgstr "" +"Entra directamente a Penpot y comienza a diseñar individualmente. Siempre " +"tendrás la opción de crear equipos más adelante." + +msgid "onboarding.team.start.title" +msgstr "Comienza a diseñar" + +msgid "onboarding.welcome.desc1" +msgstr "Estamos felices de presentarte la primera Alpha release." + +msgid "onboarding.welcome.desc2" +msgstr "" +"Penpot está en fase de desarrollo y tendrá actualizaciones constantes. " +"Esperamos que disfrutes esta version." + +msgid "onboarding.welcome.title" +msgstr "Te damos la bienvenida a Penpot!" + #: src/app/main/ui/auth/recovery.cljs msgid "profile.recovery.go-to-login" msgstr "Ir al login" @@ -1591,6 +1718,14 @@ msgstr "Configuración - %s - Penpot" msgid "title.viewer" msgstr "%s - Modo de visualización - Penpot" +msgid "viewer.breaking-change.description" +msgstr "" +"Este link compartido ya no funciona. Crea uno nuevo o pídelo a la persona " +"que lo creó." + +msgid "viewer.breaking-change.message" +msgstr "¡Lo sentimos!" + #: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs msgid "viewer.empty-state" msgstr "No se ha encontrado ningún tablero." @@ -1611,7 +1746,7 @@ msgid "viewer.header.fullscreen" msgstr "Pantalla completa" msgid "viewer.header.handsoff-section" -msgstr "Handsoff" +msgstr "Handoff" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.interactions" @@ -2023,6 +2158,10 @@ msgstr "Mis bibliotecas" msgid "workspace.library.store" msgstr "Predefinidas" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.add-interaction" +msgstr "Pulsa el botón + para añadir interacciones." + msgid "workspace.options.blur-options.background-blur" msgstr "Fondo" @@ -2113,6 +2252,18 @@ msgstr "Exportando" msgid "workspace.options.fill" msgstr "Relleno" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.add-flow-start" +msgstr "Añadir inicio de flujo" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow-start" +msgstr "Inicio de flujo" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow-starts" +msgstr "Inicios de flujo" + #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.auto" msgstr "Automático" @@ -2205,6 +2356,150 @@ msgstr "Relleno de grupo" msgid "workspace.options.group-stroke" msgstr "Borde de grupo" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-action" +msgstr "Acción" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-after-delay" +msgstr "Tiempo" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-background" +msgstr "Añadir sombreado de fondo" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-outside" +msgstr "Cerrar al pulsar fuera" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-overlay" +msgstr "Cerrar superposición" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-close-overlay-dest" +msgstr "Cerrar superposición: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-delay" +msgstr "Tiempo" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-destination" +msgstr "Destino" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-mouse-enter" +msgstr "Pasar encima" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-mouse-leave" +msgstr "Retirar encima" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-ms" +msgstr "ms" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-navigate-to" +msgstr "Navegar a" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-navigate-to-dest" +msgstr "Navegar a: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-none" +msgstr "(sin definir)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-on-click" +msgstr "En click" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-overlay" +msgstr "Superposición" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-overlay-dest" +msgstr "Superposición: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-open-url" +msgstr "Abrir url" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-center" +msgstr "Abajo centro" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-left" +msgstr "Abajo izquierda" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-bottom-right" +msgstr "Abajo derecha" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-center" +msgstr "Centro" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-manual" +msgstr "Manual" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-top-center" +msgstr "Arriba centro" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-top-left" +msgstr "Arriba izquierda" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-pos-top-right" +msgstr "Arriba derecha" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-position" +msgstr "Posición" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-prev-screen" +msgstr "Pantalla anterior" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-self" +msgstr "mismo" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-toggle-overlay" +msgstr "Alternar superpos." + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-toggle-overlay-dest" +msgstr "Alternar superpos.: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-trigger" +msgstr "Disparador" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-url" +msgstr "URL" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-while-hovering" +msgstr "Mientras pasa encima" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-while-pressing" +msgstr "Mientras pulsa" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interactions" +msgstr "Interacciones" + #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.color" msgstr "Color" @@ -2281,14 +2576,6 @@ msgstr "Capas de grupo" msgid "workspace.options.layer-options.title.multiple" msgstr "Capas seleccionadas" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.navigate-to" -msgstr "Navegar a" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.none" -msgstr "Ninguno" - #: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Posición" @@ -2312,6 +2599,9 @@ msgstr "Esquinas individuales" msgid "workspace.options.rotation" msgstr "Rotación" +msgid "workspace.options.search-font" +msgstr "Buscar fuente" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.select-a-shape" msgstr "" @@ -2605,10 +2895,17 @@ msgstr "Cortar" msgid "workspace.shape.menu.delete" msgstr "Eliminar" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.delete-flow-start" +msgstr "Eliminar inicio de flujo" + #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Desacoplar instancia" +msgid "workspace.shape.menu.difference" +msgstr "Diferencia" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.duplicate" msgstr "Duplicar" @@ -2617,6 +2914,12 @@ msgstr "Duplicar" msgid "workspace.shape.menu.edit" msgstr "Editar" +msgid "workspace.shape.menu.exclude" +msgstr "Exclusión" + +msgid "workspace.shape.menu.flatten" +msgstr "Aplanar" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.flip-horizontal" msgstr "Voltear horizontal" @@ -2625,6 +2928,10 @@ msgstr "Voltear horizontal" msgid "workspace.shape.menu.flip-vertical" msgstr "Voltear vertical" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flow-start" +msgstr "Inicio de flujo" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.forward" msgstr "Mover hacia delante" @@ -2645,6 +2952,9 @@ msgstr "Grupo" msgid "workspace.shape.menu.hide" msgstr "Ocultar" +msgid "workspace.shape.menu.intersection" +msgstr "Intersección" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.lock" msgstr "Bloquear" @@ -2657,6 +2967,9 @@ msgstr "Máscara" msgid "workspace.shape.menu.paste" msgstr "Pegar" +msgid "workspace.shape.menu.path" +msgstr "Path" + #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.reset-overrides" msgstr "Deshacer modificaciones" @@ -2669,10 +2982,16 @@ msgstr "Mostrar" msgid "workspace.shape.menu.show-main" msgstr "Ver componente principal" +msgid "workspace.shape.menu.transform-to-path" +msgstr "Convertir en vector" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.ungroup" msgstr "Desagrupar" +msgid "workspace.shape.menu.union" +msgstr "Unión" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.unlock" msgstr "Desbloquear" @@ -2877,10 +3196,4 @@ msgid "workspace.updates.update" msgstr "Actualizar" msgid "workspace.viewport.click-to-close-path" -msgstr "Pulsar para cerrar la ruta" - -msgid "viewer.breaking-change.message" -msgstr "¡Lo sentimos!" - -msgid "viewer.breaking-change.description" -msgstr "Este link compartido ya no funciona. Crea uno nuevo o pídelo a la persona que lo creó." +msgstr "Pulsar para cerrar la ruta" \ No newline at end of file diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 3b98ea239..f80abca9d 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -119,6 +119,9 @@ msgstr "Mot de passe" msgid "auth.password-length-hint" msgstr "Au moins 8 caractères" +msgid "auth.privacy-policy" +msgstr "Politique de confidentialité" + #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.recovery-request-submit" msgstr "Récupérer le mot de passe" @@ -155,6 +158,9 @@ msgstr "Créer un compte" msgid "auth.sidebar-tagline" msgstr "La solution Open Source pour la conception et le prototypage." +msgid "auth.terms-of-service" +msgstr "Conditions générales d'utilisation" + #: src/app/main/ui/auth/register.cljs msgid "auth.terms-privacy-agreement" msgstr "" @@ -2157,14 +2163,6 @@ msgstr "Grouper les calques" msgid "workspace.options.layer-options.title.multiple" msgstr "Calques sélectionnés" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.navigate-to" -msgstr "Naviguer vers" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.none" -msgstr "Aucun" - #: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Position" diff --git a/frontend/translations/he.po b/frontend/translations/he.po new file mode 100644 index 000000000..f4c03a8da --- /dev/null +++ b/frontend/translations/he.po @@ -0,0 +1,2959 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2021-10-18 07:35+0000\n" +"Last-Translator: Yaron Shahrabani \n" +"Language-Team: Hebrew " +"\n" +"Language: he\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && " +"n % 10 == 0) ? 2 : 3));\n" +"X-Generator: Weblate 4.9-dev\n" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.already-have-account" +msgstr "כבר יש לך חשבון?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.check-your-email" +msgstr "נא לחפש בדוא״ל הנכנס שלך וללחוץ על הקישור כדי לאמת ולהתחיל להשתמש ב־Penpot." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.confirm-password" +msgstr "אישור הסיסמה" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-account" +msgstr "יצירת חשבון הדגמה" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-profile" +msgstr "מעניין אותך רק להתנסות?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.demo-warning" +msgstr "זה שירות ניסיוני, לא להשתמש בו לעבודה אמתית, המיזמים יימחקו מדי פעם בפעם." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.email" +msgstr "דוא״ל" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.forgot-password" +msgstr "שכחת סיסמה?" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.fullname" +msgstr "שם מלא" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.login-here" +msgstr "כניסה מכאן" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-submit" +msgstr "כניסה" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-subtitle" +msgstr "נא למלא את הפרטים שלך להלן" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-title" +msgstr "שמחים לראות אותך שוב!" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-github-submit" +msgstr "כניסה עם GitHub" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-gitlab-submit" +msgstr "כניסה עם GitLab" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-google-submit" +msgstr "כניסה עם Google" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-ldap-submit" +msgstr "כניסה עם LDAP" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-oidc-submit" +msgstr "כניסה עם OpenID ‏(SSO)" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.new-password" +msgstr "נא להקליד סיסמה חדשה" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.newsletter-subscription" +msgstr "ההרשמה לרשימת הדיוור של Penpot מקובלת עלי." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.invalid-token-error" +msgstr "אסימון השחזור שגוי." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.password-changed-succesfully" +msgstr "הסיסמה הוחלפה בהצלחה" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.profile-not-verified" +msgstr "הפרופיל לא עבר אימות, נא לאמת את הפרופיל לפני המשך התהליך." + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.recovery-token-sent" +msgstr "קישור לשחזור סיסמה נשלח לתיבת הדוא״ל הנכנס שלך." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "auth.notifications.team-invitation-accepted" +msgstr "הצטרפת לצוות בהצלחה" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.password" +msgstr "סיסמה" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-length-hint" +msgstr "8 תווים לפחות" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-submit" +msgstr "שחזור סיסמה" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-subtitle" +msgstr "נשלח לך הודעה בדוא״ל עם ההנחיות" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-title" +msgstr "שכחת סיסמה?" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.recovery-submit" +msgstr "החלפת הסיסמה שלך" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.register" +msgstr "אין לך חשבון עדיין?" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.register-submit" +msgstr "יצירת חשבון" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-subtitle" +msgstr "זה חינם, בקוד פתוח" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-title" +msgstr "יצירת חשבון" + +#: src/app/main/ui/auth.cljs +msgid "auth.sidebar-tagline" +msgstr "הפתרון בקוד פתוח לעיצוב ויצירת אבטיפוס." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.terms-privacy-agreement" +msgstr "יצירת חשבון חדש מהווה את הסכמתך לתנאי השירות ולמדיניות הפרטיות." + +#: src/app/main/ui/auth/register.cljs +msgid "auth.verification-email-sent" +msgstr "שלחנו הודעת דוא״ל לאימות אל" + +msgid "common.share-link.confirm-deletion-link-description" +msgstr "להסיר את הקישור הזה? ביצוע פעולה זו תמנע מכולם לגשת אליו" + +msgid "common.share-link.get-link" +msgstr "קבלת קישור" + +msgid "common.share-link.link-copied-success" +msgstr "הקישור הועתק בהצלחה" + +msgid "common.share-link.link-deleted-success" +msgstr "הקישור נמחק בהצלחה" + +msgid "common.share-link.permissions-can-access" +msgstr "יש גישה" + +msgid "common.share-link.permissions-can-view" +msgstr "אפשר לצפות" + +msgid "common.share-link.permissions-hint" +msgstr "כל מי שיש לו את הקישור יכול לגשת" + +msgid "common.share-link.remove-link" +msgstr "הסרת קישור" + +msgid "common.share-link.title" +msgstr "שיתוף אבות טיפוס" + +msgid "common.share-link.view-all-pages" +msgstr "כל העמודים" + +msgid "common.share-link.view-current-page" +msgstr "רק העמוד הזה" + +msgid "common.share-link.view-selected-pages" +msgstr "עמודים נבחרים" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.add-shared" +msgstr "הוספת ספריה משותפת" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.change-email" +msgstr "החלפת דוא״ל" + +#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs +msgid "dashboard.copy-suffix" +msgstr "(עותק)" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.create-new-team" +msgstr "+ יצירת צוות חדש" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.default-team-name" +msgstr "ה־Penpot שלך" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.delete-team" +msgstr "מחיקת צוות" + +msgid "dashboard.draft-title" +msgstr "טיוטה" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate" +msgstr "שכפול" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate-multi" +msgstr "שכפול %s קבצים" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.empty-files" +msgstr "עדיין אין לך כאן קבצים" + +msgid "dashboard.export-frames" +msgstr "ייצוא לוחות אומנות ל־PDF…" + +msgid "dashboard.export-multi" +msgstr "ייצוא קובצי %s" + +msgid "dashboard.export-single" +msgstr "ייצוא קובץ" + +msgid "dashboard.export.detail" +msgstr "* עשוי לכלול רכיבים, גרפיקה, צבעים ו/או טיפוגרפיות." + +msgid "dashboard.export.explain" +msgstr "" +"אחד או יותר מהקבצים שברצונך לייצא משתמשים בספריות משותפות. מה לעשות עם " +"המשאבים שלהן*?" + +msgid "dashboard.export.options.all.message" +msgstr "קבצים עם ספריות משותפות יצורפו לייצוא, תוך שימור הקישוריות שלהם." + +msgid "dashboard.export.options.all.title" +msgstr "ייצוא ספריות משותפות" + +msgid "dashboard.export.options.detach.message" +msgstr "ספריות משותפות לא יצורפו לייצוא ואף משאב לא יתווסף לספריה. " + +msgid "dashboard.export.options.detach.title" +msgstr "להתייחס למשאבים בספריות משותפות כעצמים בסיסיים" + +msgid "dashboard.export.options.merge.message" +msgstr "הקובץ שלך ייוצא כשכל המשאבים החיצוניים ממוזגים לספריית הקבצים." + +msgid "dashboard.export.options.merge.title" +msgstr "לכלול משאבי ספריה משותפת בספריות הקבצים" + +msgid "dashboard.export.title" +msgstr "ייצוא קבצים" + +msgid "dashboard.fonts.deleted-placeholder" +msgstr "הגופן נמחק" + +msgid "dashboard.fonts.empty-placeholder" +msgstr "עדיין לא מותקנים אצלך גופנים משלך." + +#, markdown +msgid "dashboard.fonts.hero-text1" +msgstr "" +"כל גופן דפדפן שיועלה כאן יתווסף לרשימת משפחת הגופנים שזמין במאפייני הטקסט " +"של הקבצים של הצוות הזה. גופנים מאותו שם של משפחת גופנים יקובצו תחת **משפחת " +"גופנים יחידה**. ניתן להעלות גופנים מהסוגים הבאים: **TTF,‏ OTF ו־WOFF** (אחד " +"הסוגים יספיק)." + +#, markdown +msgid "dashboard.fonts.hero-text2" +msgstr "" +"עליך להעלות גופנים בבעלותך או שיש לך רישיון להשתמש בהם ב־Penpot. ניתן למצוא " +"על כך מידע נוסף בסעיף זכויות התוכן של [תנאי השירות של " +"Penpot](https://penpot.app/terms.html). אפשר גם לקרוא גם על [רישוי " +"גופנים](https://www.typography.com/faq)." + +msgid "dashboard.import" +msgstr "ייבוא קבצים" + +msgid "dashboard.import.analyze-error" +msgstr "אופס! לא הצלחנו לייבא את הקובץ הזה" + +msgid "dashboard.import.import-error" +msgstr "אירעה תקלה בייבוא הקובץ. הוא לא ייובא." + +msgid "dashboard.import.import-message" +msgstr "%s קבצים עברו ייבוא כרגיל." + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.invite-profile" +msgstr "הזמנה לצוות" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.leave-team" +msgstr "עזיבת הצוות" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "dashboard.libraries-title" +msgstr "ספריות משותפות" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.loading-files" +msgstr "הקבצים שלך נטענים…" + +msgid "dashboard.loading-fonts" +msgstr "הגופנים שלך נטענים…" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to" +msgstr "העברה אל" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-multi" +msgstr "העברה של %s קבצים אל" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-other-team" +msgstr "העברה לצוות אחר" + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +msgid "dashboard.new-file" +msgstr "+ קובץ חדש" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-file-prefix" +msgstr "קובץ חדש" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.new-project" +msgstr "+ מיזם חדש" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-project-prefix" +msgstr "מיזם חדש" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.no-matches-for" +msgstr "לא נמצאו תוצאות לחיפוש אחר „%s”" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.no-projects-placeholder" +msgstr "מיזמים נעוצים יופיעו כאן" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-changed-successfully" +msgstr "כתובת הדוא״ל שלך עודכנה בהצלחה" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-verified-successfully" +msgstr "כתובת הדוא״ל שלך אומתה בהצלחה" + +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.notifications.password-saved" +msgstr "הסיסמה נשמרה בהצלחה!" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.num-of-members" +msgstr "%s חברים" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.open-in-new-tab" +msgstr "פתיחת קובץ בלשונית חדשה" + +msgid "dashboard.options" +msgstr "אפשרויות" + +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.password-change" +msgstr "החלפת סיסמה" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.pin-unpin" +msgstr "נעיצה/שחרור" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.projects-title" +msgstr "מיזמים" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.promote-to-owner" +msgstr "קידום לבעלות" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.remove-account" +msgstr "להסיר את החשבון שלך?" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.remove-shared" +msgstr "הסרה כספריה משותפת" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.search-placeholder" +msgstr "חיפוש…" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.searching-for" +msgstr "מתבצע חיפוש אחר „%s”…" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.select-ui-language" +msgstr "בחירת שפת מנשק משתמש" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.select-ui-theme" +msgstr "בחירת ערכת עיצוב" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.show-all-files" +msgstr "הצגת כל הקבצים" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgstr "הקובץ שלך נמחק בהצלחה" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-delete-project" +msgstr "המיזם שלך נמחק בהצלחה" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgstr "הקובץ שלך שוכפל בהצלחה" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-duplicate-project" +msgstr "המיזם שלך שוכפל בהצלחה" + +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-file" +msgstr "הקובץ שלך הועבר בהצלחה" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-files" +msgstr "הקבצים שלך הועברו בהצלחה" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-move-project" +msgstr "המיזם שלך הועבר בהצלחה" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.switch-team" +msgstr "לעבור צוות" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-info" +msgstr "פרטי הצוות" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-members" +msgstr "חברי הצוות" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-projects" +msgstr "מיזמי הצוות" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.theme-change" +msgstr "ערכת עיצוב מנשק משתמש" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.title-search" +msgstr "תוצאות חיפוש" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.type-something" +msgstr "נא להקליד כדי לחפש" + +#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +msgid "dashboard.update-settings" +msgstr "עדכון הגדרות" + +#: src/app/main/ui/settings.cljs +msgid "dashboard.your-account-title" +msgstr "החשבון שלך" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-email" +msgstr "דוא״ל" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-name" +msgstr "שמך" + +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.your-penpot" +msgstr "ה־Penpot שלך" + +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-cancel" +msgstr "ביטול" + +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-ok" +msgstr "אישור" + +#: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs +msgid "ds.confirm-title" +msgstr "להמשיך?" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "ds.updated-at" +msgstr "עדכון: %s" + +#: src/app/main/data/workspace.cljs +msgid "errors.clipboard-not-implemented" +msgstr "הדפדפן שלך לא יכול לבצע את הפעולה הזאת" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +msgid "errors.email-already-exists" +msgstr "כתובת הדוא״ל כבר בשימוש" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.email-already-validated" +msgstr "כתובת הדוא״ל כבר אומתה." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +msgid "errors.email-has-permanent-bounces" +msgstr "לכתובת הדוא״ל „%s” יש יותר מדי דוחות החזרה קבועים." + +#: src/app/main/ui/settings/change_email.cljs +msgid "errors.email-invalid-confirmation" +msgstr "כתובת הדוא״ל לאימות חייבת להיות תואמת" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +msgid "errors.generic" +msgstr "קרה משהו לא טוב." + +#: src/app/main/ui/auth/login.cljs +msgid "errors.google-auth-not-enabled" +msgstr "אימות מול Google הושבת במנגנון" + +#: src/app/main/ui/components/color_input.cljs +msgid "errors.invalid-color" +msgstr "צבע שגוי" + +#: src/app/main/ui/auth/login.cljs +msgid "errors.ldap-disabled" +msgstr "אימות מול LDAP הושבת." + +msgid "errors.media-format-unsupported" +msgstr "סוג התמונה אינו נתמך (חייב להיות svg,‏ jpg או png)." + +#: src/app/main/data/workspace/persistence.cljs +msgid "errors.media-too-large" +msgstr "התמונה גדולה מכדי להוסיף אותה (חייבת להיות פחות מ־5 מ״ב)." + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "errors.media-type-mismatch" +msgstr "נראה כי תוכן התמונה לא תואם לסיומת הקובץ." + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "errors.media-type-not-allowed" +msgstr "נראה כי זאת תמונה שגויה." + +#: src/app/main/ui/dashboard/team.cljs +msgid "errors.member-is-muted" +msgstr "הודעות הדוא״ל לפרופיל שהזמנת מושתקות (דיווחים על דואר זבל או הרבה החזרות)." + +msgid "errors.network" +msgstr "לא ניתן ליצור קשר עם שרת המנגנון." + +#: src/app/main/ui/settings/password.cljs +msgid "errors.password-invalid-confirmation" +msgstr "סיסמת האימות חייבת להיות תואמת" + +#: src/app/main/ui/settings/password.cljs +msgid "errors.password-too-short" +msgstr "הסיסמה חייבת להיות באורך 8 תווים לפחות" + +#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs +msgid "errors.profile-is-muted" +msgstr "הודעות הדוא״ל לפרופיל שלך מושתקות (דיווחי דואר זבל או הרבה החזרות)." + +#: src/app/main/ui/auth/register.cljs +msgid "errors.registration-disabled" +msgstr "ההרשמה מושבתת כרגע." + +msgid "errors.terms-privacy-agreement-invalid" +msgstr "עליך לקבל את תנאי השירות ואת מדיניות הפרטיות." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.token-expired" +msgstr "תוקף האסימון פג" + +#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "errors.unexpected-error" +msgstr "אירעה שגיאה בלתי צפויה." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.unexpected-token" +msgstr "אסימון בלתי ידוע" + +#: src/app/main/ui/auth/login.cljs +msgid "errors.wrong-credentials" +msgstr "כנראה ששם המשתמש והסיסמה שגויים." + +#: src/app/main/ui/settings/password.cljs +msgid "errors.wrong-old-password" +msgstr "הסיסמה הישנה שגויה" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.chat-start" +msgstr "הצטרפות לצ׳אט" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.chat-subtitle" +msgstr "מעניין אותך לדבר? מזמינים אותו ל־Gitter" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.description" +msgstr "תיאור" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-go-to" +msgstr "מעבר לדיונים" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-subtitle1" +msgstr "מזמינים אותך להצטרף לפורום התקשורת של Penpot." + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-subtitle2" +msgstr "" +"ניתן לשאול ולענות על שאלות, לנהל דיונים פתוחים ולעקוב אחר החלטות שמשפיעות " +"על המיזם." + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-title" +msgstr "דיונים צוותיים" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.subject" +msgstr "נושא" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.subtitle" +msgstr "" +"נא לתאר את הסיבה שלך לשליחת ההודעה תוך פירוט האם זאת תקלה, רעיון או בספק. " +"אחד מחברי הצוות שלנו יגיב לך במהירות האפשרית." + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.title" +msgstr "דוא״ל" + +#: src/app/main/ui/settings/password.cljs +msgid "generic.error" +msgstr "אירעה שגיאה" + +#: src/app/main/ui/handoff/attributes/blur.cljs +msgid "handoff.attributes.blur" +msgstr "טשטוש" + +#: src/app/main/ui/handoff/attributes/blur.cljs +msgid "handoff.attributes.blur.value" +msgstr "ערך" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.hex" +msgstr "הקסדצימלי" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.hsla" +msgstr "HSLA" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.rgba" +msgstr "RGBA" + +#: src/app/main/ui/handoff/attributes/fill.cljs +msgid "handoff.attributes.fill" +msgstr "מילוי" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.download" +msgstr "הורדת תמונת מקור" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.height" +msgstr "גובה" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.width" +msgstr "רוחב" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout" +msgstr "פריסה" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.height" +msgstr "גובה" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.left" +msgstr "שמאל" + +#: src/app/main/ui/handoff/attributes/layout.cljs, src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.radius" +msgstr "רדיוס" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.rotation" +msgstr "סיבוב" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.top" +msgstr "עליון" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.width" +msgstr "רוחב" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow" +msgstr "צל" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.blur" +msgstr "ט" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.offset-x" +msgstr "X" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.offset-y" +msgstr "Y" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.spread" +msgstr "פ" + +#: src/app/main/ui/handoff/attributes/stroke.cljs +msgid "handoff.attributes.stroke" +msgstr "מתאר" + +#, permanent +msgid "handoff.attributes.stroke.alignment.center" +msgstr "מרכז" + +#, permanent +msgid "handoff.attributes.stroke.alignment.inner" +msgstr "בפנים" + +#, permanent +msgid "handoff.attributes.stroke.alignment.outer" +msgstr "בחוץ" + +msgid "handoff.attributes.stroke.style.dotted" +msgstr "מנוקד" + +msgid "handoff.attributes.stroke.style.mixed" +msgstr "מעורב" + +msgid "handoff.attributes.stroke.style.none" +msgstr "ללא" + +msgid "handoff.attributes.stroke.style.solid" +msgstr "אחיד" + +#: src/app/main/ui/handoff/attributes/stroke.cljs +msgid "handoff.attributes.stroke.width" +msgstr "רוחב" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography" +msgstr "טיפוגרפיה" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-family" +msgstr "משפחת גופנים" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-size" +msgstr "גודל גופן" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-style" +msgstr "סגנון גופן" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.letter-spacing" +msgstr "ריווח תווים" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.line-height" +msgstr "גובה שורה" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.text-decoration" +msgstr "עיטור טקסט" + +msgid "handoff.attributes.typography.text-decoration.none" +msgstr "ללא" + +msgid "handoff.attributes.typography.text-decoration.strikethrough" +msgstr "קו חוצה" + +msgid "handoff.attributes.typography.text-decoration.underline" +msgstr "קו תחתי" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.text-transform" +msgstr "התמרת טקסט" + +msgid "handoff.attributes.typography.text-transform.lowercase" +msgstr "אותיות קטנות" + +msgid "handoff.attributes.typography.text-transform.none" +msgstr "ללא" + +msgid "handoff.attributes.typography.text-transform.titlecase" +msgstr "ראשונות גדולות" + +msgid "handoff.attributes.typography.text-transform.uppercase" +msgstr "אותיות גדולות" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.code" +msgstr "קוד" + +msgid "handoff.tabs.code.selected.circle" +msgstr "עיגול" + +msgid "handoff.tabs.code.selected.curve" +msgstr "עיקול" + +msgid "handoff.tabs.code.selected.frame" +msgstr "לוח אומנות" + +msgid "handoff.tabs.code.selected.group" +msgstr "קבוצה" + +msgid "handoff.tabs.code.selected.image" +msgstr "תמונה" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.code.selected.multiple" +msgstr "%s נבחרו" + +msgid "handoff.tabs.code.selected.path" +msgstr "נתיב" + +msgid "handoff.tabs.code.selected.rect" +msgstr "ריבוע" + +msgid "handoff.tabs.code.selected.svg-raw" +msgstr "SVG" + +msgid "handoff.tabs.code.selected.text" +msgstr "טקסט" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.info" +msgstr "מידע" + +msgid "history.alert-message" +msgstr "הגרסה שמוצגת היא %s" + +msgid "labels.accept" +msgstr "מקובל" + +msgid "labels.add-custom-font" +msgstr "הוספת גופן משלך" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.admin" +msgstr "ניהול" + +#: src/app/main/ui/workspace/comments.cljs +msgid "labels.all" +msgstr "הכול" + +#: src/app/main/ui/static.cljs +msgid "labels.bad-gateway.desc-message" +msgstr "" +"נראה כאילו עליך להמתין מעט ולנסות שוב, אנו מבצעים עבודות תחזוקה פשוטות " +"בשרתים שלנו." + +#: src/app/main/ui/static.cljs +msgid "labels.bad-gateway.main-message" +msgstr "שער גישה שגוי" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.cancel" +msgstr "ביטול" + +msgid "labels.centered" +msgstr "מרכז" + +msgid "labels.close" +msgstr "סגירה" + +#: src/app/main/ui/dashboard/comments.cljs +msgid "labels.comments" +msgstr "הערות" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.confirm-password" +msgstr "אישור סיסמה" + +msgid "labels.content" +msgstr "תוכן" + +msgid "labels.continue" +msgstr "להמשיך" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "labels.create" +msgstr "יצירה" + +#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team" +msgstr "יצירת צוות חדש" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team.placeholder" +msgstr "נא למלא שם לצוות החדש" + +msgid "labels.custom-fonts" +msgstr "גופנים משלך" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.dashboard" +msgstr "לוח בקרה" + +msgid "labels.default" +msgstr "ברירת מחדל" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete" +msgstr "מחיקה" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment" +msgstr "מחיקת הערה" + +#: src/app/main/ui/comments.cljs +msgid "labels.delete-comment-thread" +msgstr "מחיקת שרשור" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.delete-multi-files" +msgstr "מחיקת %s קבצים" + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.drafts" +msgstr "טיוטות" + +#: src/app/main/ui/comments.cljs +msgid "labels.edit" +msgstr "עריכה" + +msgid "labels.edit-file" +msgstr "עריכת קובץ" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.editor" +msgstr "עורך" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.email" +msgstr "דוא״ל" + +msgid "labels.export" +msgstr "ייצוא" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.feedback-disabled" +msgstr "המשוב הושבת" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.feedback-sent" +msgstr "המשוב נשלח" + +msgid "labels.font-family" +msgstr "משפחת גופנים" + +msgid "labels.font-providers" +msgstr "ספקי גופנים" + +msgid "labels.font-variants" +msgstr "סגנונות" + +msgid "labels.fonts" +msgstr "גופנים" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.give-feedback" +msgstr "הגשת משוב" + +msgid "labels.go-back" +msgstr "חזרה" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.hide-resolved-comments" +msgstr "הסתרת הערות שנפתרו" + +msgid "labels.icons" +msgstr "סמלים" + +msgid "labels.images" +msgstr "תמונות" + +msgid "labels.installed-fonts" +msgstr "גופנים מותקנים" + +#: src/app/main/ui/static.cljs +msgid "labels.internal-error.desc-message" +msgstr "" +"משהו לא טוב קרה. נא לנסות לבצע את הפעולה שוב ואם הבעיה חוזרת, נא ליצור קשר " +"עם התמיכה." + +#: src/app/main/ui/static.cljs +msgid "labels.internal-error.main-message" +msgstr "שגיאה פנימית" + +#: src/app/main/ui/settings/options.cljs +msgid "labels.language" +msgstr "שפה" + +msgid "labels.link" +msgstr "קישור" + +#: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.logout" +msgstr "יציאה" + +msgid "labels.manage-fonts" +msgstr "ניהול גופנים" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.members" +msgstr "חברים" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.name" +msgstr "שם" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.new-password" +msgstr "סיסמה חדשה" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs +msgid "labels.no-comments-available" +msgstr "אין לך התראות ממתינות על הערות" + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.auth-info" +msgstr "נכנסת בשם" + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.desc-message" +msgstr "יכול להיות שהעמוד לא קיים או שאין לך הרשאות לגשת אליו." + +#: src/app/main/ui/static.cljs +msgid "labels.not-found.main-message" +msgstr "אופס!" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.num-of-files" +msgid_plural "labels.num-of-files" +msgstr[0] "קובץ" +msgstr[1] "%s קבצים" +msgstr[2] "%s קבצים" +msgstr[3] "%s קבצים" + +msgid "labels.num-of-frames" +msgid_plural "labels.num-of-frames" +msgstr[0] "לוח אומנות" +msgstr[1] "%s לוחות אומנות" +msgstr[2] "%s לוחות אומנות" +msgstr[3] "%s לוחות אומנות" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.num-of-projects" +msgid_plural "labels.num-of-projects" +msgstr[0] "מיזם" +msgstr[1] "%s מיזמים" +msgstr[2] "%s מיזמים" +msgstr[3] "%s מיזמים" + +#: src/app/main/ui/settings/password.cljs +msgid "labels.old-password" +msgstr "סיסמה ישנה" + +#: src/app/main/ui/workspace/comments.cljs +msgid "labels.only-yours" +msgstr "שלך בלבד" + +msgid "labels.or" +msgstr "או" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.owner" +msgstr "בעלים" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.password" +msgstr "סיסמה" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.permissions" +msgstr "הרשאות" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.profile" +msgstr "פרופיל" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.projects" +msgstr "מיזמים" + +msgid "labels.recent" +msgstr "אחרונים" + +#: src/app/main/ui/settings/sidebar.cljs +msgid "labels.release-notes" +msgstr "הודעות מהדורה" + +#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.remove" +msgstr "הסרה" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "labels.rename" +msgstr "שינוי שם" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.rename-team" +msgstr "שינוי שם לצוות" + +#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs +msgid "labels.retry" +msgstr "ניסיון חוזר" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.role" +msgstr "תפקיד" + +msgid "labels.save" +msgstr "שמירה" + +msgid "labels.search-font" +msgstr "חיפוש גופן" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.send" +msgstr "שליחה" + +#: src/app/main/ui/settings/feedback.cljs +msgid "labels.sending" +msgstr "מתבצעת שליחה…" + +#: src/app/main/ui/static.cljs +msgid "labels.service-unavailable.desc-message" +msgstr "אנחנו בהפוגת תחזוקה מתוכננת של המערכות שלנו." + +#: src/app/main/ui/static.cljs +msgid "labels.service-unavailable.main-message" +msgstr "השירות אינו זמין" + +#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.settings" +msgstr "הגדרות" + +msgid "labels.share-prototype" +msgstr "שיתוף אבטיפוס" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "labels.shared-libraries" +msgstr "ספריות משותפות" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.show-all-comments" +msgstr "הצגת כל ההערות" + +#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs +msgid "labels.show-your-comments" +msgstr "הצגת ההערות שלך בלבד" + +#: src/app/main/ui/static.cljs +msgid "labels.sign-out" +msgstr "יציאה" + +msgid "labels.skip" +msgstr "דילוג" + +msgid "labels.start" +msgstr "התחלה" + +#: src/app/main/ui/settings/profile.cljs +msgid "labels.update" +msgstr "עדכון" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.update-team" +msgstr "עדכון צוות" + +msgid "labels.upload" +msgstr "העלאה" + +msgid "labels.upload-custom-fonts" +msgstr "העלאת גופנים משלך" + +msgid "labels.uploading" +msgstr "מתבצעת העלאה…" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.viewer" +msgstr "מציג" + +msgid "labels.workspace" +msgstr "סביבת עבודה" + +#: src/app/main/ui/comments.cljs +msgid "labels.write-new-comment" +msgstr "כתיבת הערה חדשה" + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "media.loading" +msgstr "התמונה נטענת…" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.accept" +msgstr "הוספה כספריה משותפת" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.hint" +msgstr "" +"לאחר שנוספה כספריה משותפת, המשאבים בספריית הקבצים הזאת יהיו זמינים בנוסף " +"לשאר הקבצים שלך." + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.message" +msgstr "הוספת „%s” כספריה משותפת" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.confirm-email" +msgstr "אימות כתובת דוא״ל חדשה" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.info" +msgstr "נשלח הודעה לכתובת הדוא״ל הנוכחית שלך „%s” כדי לאמת את הזהות שלך." + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.new-email" +msgstr "כתובת דוא״ל חדשה" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.submit" +msgstr "החלפת כתובת דוא״ל" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.title" +msgstr "החלפת כתובת הדוא״ל שלך" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.cancel" +msgstr "ביטול ושמירה על החשבון שלי" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.confirm" +msgstr "כן, למחוק את החשבון שלי" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.info" +msgstr "הסרת החשבון שלך תוביל לאיבוד כל המיזמים והארכיונים הקיימים שלך." + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.title" +msgstr "למחוק את החשבון שלך?" + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.accept" +msgstr "מחיקת דיון" + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.message" +msgstr "למחוק את הדיון הזה? כל התגובות בשרשור תימחקנה." + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.title" +msgstr "מחיקת דיון" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-confirm.accept" +msgstr "מחיקת קובץ" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-confirm.message" +msgstr "למחוק את הקובץ?" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-confirm.title" +msgstr "הקובץ נמחק" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.accept" +msgstr "מחיקת קבצים" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.message" +msgstr "למחוק %s קבצים?" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.title" +msgstr "%s קבצים נמחקים" + +msgid "modals.delete-font-variant.message" +msgstr "למחוק את סגנון הגופן הזה? הוא לא ייטען אם נעשה בו שימוש בקובץ." + +msgid "modals.delete-font-variant.title" +msgstr "סגנון גופן נמחק" + +msgid "modals.delete-font.message" +msgstr "למחוק את הגופן הזה? הוא לא ייטען אם נעשה בו שימוש בקובץ." + +msgid "modals.delete-font.title" +msgstr "גופן נמחק" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "modals.delete-page.body" +msgstr "למחוק את העמוד הזה?" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "modals.delete-page.title" +msgstr "מחיקת עמוד" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.accept" +msgstr "מחיקת מיזם" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.message" +msgstr "למחוק את המיזם הזה?" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.title" +msgstr "מחיקת מיזם" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.delete-team-confirm.accept" +msgstr "מחיקת צוות" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.delete-team-confirm.message" +msgstr "למחוק את הצוות הזה? כל המיזמים והקבצים שמשויכים לצוות יימחקו לצמיתות." + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.delete-team-confirm.title" +msgstr "צוות נמחק" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.accept" +msgstr "מחיקת חבר" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.message" +msgstr "למחוק את החבר הזה מהצוות?" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.title" +msgstr "למחוק חבר בצוות" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.invite-member-confirm.accept" +msgstr "שליחת הזמנה" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.invite-member.title" +msgstr "הזמנה להצטרף לצוות" + +msgid "modals.leave-and-reassign.forbiden" +msgstr "" +"אי אפשר לעזוב צוות אם אין חברים שאפשר לקדם לבעלות עליה. אולי עדיף למחוק את " +"הצוות." + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.hint1" +msgstr "הבעלות על %s בידיך." + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.hint2" +msgstr "נא לבחור חבר אחר לקידום לבעלות בטרם עזיבתך" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.promote-and-leave" +msgstr "קידום ועזיבה" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.select-memeber-to-promote" +msgstr "נא לבחור חבר לקידום" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.title" +msgstr "נא לבחור חבר לקידום" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.accept" +msgstr "עזיבת צוות" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.message" +msgstr "לעזוב את הצוות הזה?" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.title" +msgstr "עזיבת הצוות" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.accept" +msgstr "קידום" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.message" +msgstr "לקדם את המשתמש הזה לבעלים?" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.title" +msgstr "קידום לבעלים" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.accept" +msgstr "הסרה כספריה משותפת" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.hint" +msgstr "" +"לאחר הסרה כספריה משותפת, ספריית הקבצים של הקובץ הזה לא תהיה זמינה עוד " +"לשימוש בקרב שאר הקבצים שלך." + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.message" +msgstr "הסרת „%s” כספריה משותפת" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.accept" +msgstr "עדכון רכיב" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.cancel" +msgstr "ביטול" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.hint" +msgstr "" +"פעולה זו תעדכן רכיב בספריה משותפת. זה עשוי להשפיע על הקבצים האחרים שמשתמשים " +"בה." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.message" +msgstr "עדכון רכיב בספריה משותפת" + +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-email-sent" +msgstr "ההזמנה נשלחה בהצלחה" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "notifications.profile-deletion-not-allowed" +msgstr "" +"אין אפשר למחוק את הפרופיל שלך. יש להקצות את הצוותים שלך מחדש בטרם המשך " +"בתהליך." + +#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/options.cljs +msgid "notifications.profile-saved" +msgstr "הפרופיל נשמר בהצלחה!" + +#: src/app/main/ui/settings/change_email.cljs +msgid "notifications.validation-email-sent" +msgstr "הודעת האימות נשלחה בדוא״ל אל %s. נא לבדוק את הדוא״ל שלך!" + +msgid "onboarding.contrib.alt" +msgstr "קוד פתוח" + +msgid "onboarding.contrib.desc1" +msgstr "" +"Penpot הוא בקוד פתוח, נוצר על ידי הקהילה ועבורה. אנו מזמינים אותך לקחת חלק, " +"בשמחה!" + +msgid "onboarding.contrib.desc2.1" +msgstr "אפשר לגשת אל" + +msgid "onboarding.contrib.desc2.2" +msgstr "ולעקוב אחר הנחיות ההתנדבות :)" + +msgid "onboarding.contrib.link" +msgstr "מיזם ב־github" + +msgid "onboarding.contrib.title" +msgstr "התנדבות בקוד פתוח?" + +msgid "onboarding.slide.0.alt" +msgstr "יצירת עיצובים" + +msgid "onboarding.slide.0.desc1" +msgstr "אפשר ליצור מנשקי משתמש יפהפיים בשיתוף פעולה של כל חברי הצוות." + +msgid "onboarding.slide.0.desc2" +msgstr "שמירה על אחידות גם בעת גדילה מול כל הרכיבים, הספריות ומערכות העיצוב." + +msgid "onboarding.slide.0.title" +msgstr "ספריות עיצוב, סגנונות ורכיבים" + +msgid "onboarding.slide.1.alt" +msgstr "אבות טיפוס אינטראקטיביים" + +msgid "onboarding.slide.1.desc1" +msgstr "יצירת אינטראקציות עשירות לחיקוי התנהגות המוצר." + +msgid "onboarding.slide.1.desc2" +msgstr "" +"שיתוף עם בעלי עניין, הגשת הצעות לצוות והתחלת בדיקות עם העיצובים שלך, הכול " +"באותו המקום." + +msgid "onboarding.slide.1.title" +msgstr "העיצובים שלך קמים לחיים עם אינטראקציות" + +msgid "onboarding.slide.2.alt" +msgstr "קבלת משוב" + +msgid "onboarding.slide.2.desc1" +msgstr "" +"כל חברי הצוות עובדים במקביל עם הערות, רעיונות ממגוון גורמים במקום מרכזי על " +"העיצוב בזמן אמת ישירות על העיצובים." + +msgid "onboarding.slide.2.title" +msgstr "קבלת משוב, הצגה ושיתוף העבודה שלך" + +msgid "onboarding.slide.3.alt" +msgstr "הגשה וקוד מקור" + +msgid "onboarding.slide.3.desc1" +msgstr "ניתן לסנכרן את העיצוב ואת הקוד של כל הרכיבים והעיצובים שלך ולקבל מקטעי קוד." + +msgid "onboarding.slide.3.desc2" +msgstr "" +"לקבל ולספק מפרטי קוד כגון שפת סימון (SVG,‏ HTML) או סגנונות (CSS,‏ Less,‏ " +"Stylus…)." + +msgid "onboarding.slide.3.title" +msgstr "מקור משותף יחיד לאמת" + +msgid "onboarding.team.create.button" +msgstr "יצירת צוות" + +msgid "onboarding.team.create.desc1" +msgstr "" +"יש לך עבודה בשיתוף עם גורם נוסף? ניתן ליצור צוות כדי לעבוד יחד על מיזמים " +"ולשתף משאבי עיצוב." + +msgid "onboarding.team.create.input-placeholder" +msgstr "נא למלא שם לצוות" + +msgid "onboarding.team.create.title" +msgstr "יצירת צוות" + +msgid "onboarding.team.start.button" +msgstr "להתחיל כבר עכשיו" + +msgid "onboarding.team.start.desc1" +msgstr "" +"אפשר לצלול עמוק ישירות אל Penpot ולהתחיל לעצב בעצמך. עדיין תהיה לך הזדמנות " +"ליצור צוותים בהמשך." + +msgid "onboarding.team.start.title" +msgstr "מתחילים לעצב" + +msgid "onboarding.welcome.alt" +msgstr "Penpot" + +msgid "onboarding.welcome.desc1" +msgstr "אנו שמחים להציג בפניך את גרסת האלפא הראשונית." + +msgid "onboarding.welcome.desc2" +msgstr "" +"Penpot עדיין בשלבי פיתוח וייתכנו עדכונים תכופים. אנו מקווים שהגרסה היציבה " +"הראשונה תשרת אותך כראוי." + +msgid "onboarding.welcome.title" +msgstr "ברוך בואך ל־Penpot!" + +#: src/app/main/ui/auth/recovery.cljs +msgid "profile.recovery.go-to-login" +msgstr "מעבר למסך הכניסה" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "settings.multiple" +msgstr "מעורב" + +#: src/app/main/ui/dashboard/files.cljs +msgid "title.dashboard.files" +msgstr "%s‏ - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.font-providers" +msgstr "ספקי גופנים - %s‏ - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.fonts" +msgstr "גופנים - %s‏ - Penpot" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "title.dashboard.projects" +msgstr "מיזמים - %s‏ - Penpot" + +#: src/app/main/ui/dashboard/search.cljs +msgid "title.dashboard.search" +msgstr "חיפוש - %s‏ - Penpot" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "title.dashboard.shared-libraries" +msgstr "ספריות משותפות - %s‏ - Penpot" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/auth.cljs +msgid "title.default" +msgstr "Penpot - חופש עיצובי לצוותים" + +#: src/app/main/ui/settings/feedback.cljs +msgid "title.settings.feedback" +msgstr "הגשת משוב - Penpot" + +#: src/app/main/ui/settings/options.cljs +msgid "title.settings.options" +msgstr "הגדרות - Penpot" + +#: src/app/main/ui/settings/password.cljs +msgid "title.settings.password" +msgstr "סיסמה - Penpot" + +#: src/app/main/ui/settings/profile.cljs +msgid "title.settings.profile" +msgstr "פרופיל - Penpot" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-members" +msgstr "חברים - %s‏ - Penpot" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-settings" +msgstr "הגדרות - %s‏ - Penpot" + +#: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs +msgid "title.viewer" +msgstr "%s - מצב תצוגה - Penpot" + +#: src/app/main/ui/workspace.cljs +msgid "title.workspace" +msgstr "%s‏ - Penpot" + +#: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs +msgid "viewer.empty-state" +msgstr "לא נמצאו לוחות אומנות בעמוד." + +#: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs +msgid "viewer.frame-not-found" +msgstr "לוח האומנות לא נמצא." + +msgid "viewer.header.comments-section" +msgstr "הערות" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.dont-show-interactions" +msgstr "לא להציג אינטראקציות" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.fullscreen" +msgstr "מסך מלא" + +msgid "viewer.header.handsoff-section" +msgstr "מסירות" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.interactions" +msgstr "אינטראקציות" + +msgid "viewer.header.interactions-section" +msgstr "אינטראקציות" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.copy-link" +msgstr "העתקת קישור" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.create-link" +msgstr "יצירת קישור" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.placeholder" +msgstr "הקישור לשיתוף יופיע כאן" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.remove-link" +msgstr "הסרת קישור" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.subtitle" +msgstr "לכל מי שיש את הקישור יש גישה" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.show-interactions" +msgstr "הצגת פעילויות" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.show-interactions-on-click" +msgstr "הצגת פעילויות בקליק" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.sitemap" +msgstr "מפת אתר" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hcenter" +msgstr "יישור למרכז האופקי" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hdistribute" +msgstr "פיזור ריווח אופקי" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hleft" +msgstr "יישור שמאלה" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hright" +msgstr "יישור ימינה" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vbottom" +msgstr "יישור למטה" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vcenter" +msgstr "יישור למרכז אנכית" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vdistribute" +msgstr "פיזור בריווח אנכי" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vtop" +msgstr "יישור למעלה" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.assets" +msgstr "משאבים" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.box-filter-all" +msgstr "כל המשאבים" + +msgid "workspace.assets.box-filter-graphics" +msgstr "גרפיקה" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.colors" +msgstr "צבעים" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.components" +msgstr "רכיבים" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.create-group" +msgstr "יצירת קבוצה" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.create-group-hint" +msgstr "הפריטים שלך יקבלו אוטומטית שם בסגנון „שם קבוצה / שם פריט”" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.delete" +msgstr "מחיקה" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.duplicate" +msgstr "שכפול" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.edit" +msgstr "עריכה" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.file-library" +msgstr "ספריית קבצים" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.graphics" +msgstr "גרפיקה" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group" +msgstr "קבוצה" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group-name" +msgstr "שם קבוצה" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.libraries" +msgstr "ספריות" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.not-found" +msgstr "לא נמצאו משאבים" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.rename" +msgstr "שינוי שם" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.rename-group" +msgstr "שינוי שם קבוצה" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.search" +msgstr "חיפוש במשאבים" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.selected-count" +msgid_plural "workspace.assets.selected-count" +msgstr[0] "פריט נבחר" +msgstr[1] "%s פריטים נבחרו" +msgstr[2] "%s פריטים נבחרו" +msgstr[3] "%s פריטים נבחרו" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.shared" +msgstr "משותף" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.typography" +msgstr "טיפוגרפיות" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-id" +msgstr "גופן" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-size" +msgstr "גודל" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-variant-id" +msgstr "הגוון" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.go-to-edit" +msgstr "מעבר לקובץ ספריית סגנון כדי לערוך" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.letter-spacing" +msgstr "ריווח תווים" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.line-height" +msgstr "גובה שורה" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/handoff/attributes/text.cljs, src/app/main/ui/handoff/attributes/text.cljs +msgid "workspace.assets.typography.sample" +msgstr "שצ" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.text-transform" +msgstr "התמרת טקסט" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.ungroup" +msgstr "פירוק קבוצה" + +#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +msgid "workspace.gradients.linear" +msgstr "מדרג קווי" + +#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +msgid "workspace.gradients.radial" +msgstr "מדרג מעגלי" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-dynamic-alignment" +msgstr "השבתת יישור דינמי" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-scale-text" +msgstr "השבתת שינוי גודל טקסט" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-snap-grid" +msgstr "השבתת הצמדה לרשת" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-dynamic-alignment" +msgstr "הפעלת יישור דינמי" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-scale-text" +msgstr "הפעלת שינוי גודל טקסט" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-snap-grid" +msgstr "הצמדה לרשת" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-assets" +msgstr "הסתרת משאבים" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-grid" +msgstr "הסתרת רשתות" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-layers" +msgstr "הסתרת שכבות" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-palette" +msgstr "הסתרת ערכת צבעים" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-rules" +msgstr "הסתרת כללים" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.select-all" +msgstr "לבחור הכול" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-assets" +msgstr "הצגת משאבים" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-grid" +msgstr "הצגת רשת" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-layers" +msgstr "הצגת שכבות" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-palette" +msgstr "הצגת ערכת צבעים" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-rules" +msgstr "הצגת כללים" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.save-error" +msgstr "שגיאה בשמירה" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.saved" +msgstr "נשמר" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.saving" +msgstr "בשמירה" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.unsaved" +msgstr "שינויים שלא נשמרו" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.viewer" +msgstr "מצב תצוגה (%s)" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.add" +msgstr "הוספה" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.colors" +msgstr "%s צבעים" + +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.big-thumbnails" +msgstr "תצוגה מקדימה גדולה" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.file-library" +msgstr "ספריית קבצים" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.hsv" +msgstr "HSV" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.recent-colors" +msgstr "צבעים אחרונים" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.rgb-complementary" +msgstr "RGB משלים" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.rgba" +msgstr "RGBA" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.save-color" +msgstr "שמירת סגנון צבע" + +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.small-thumbnails" +msgstr "תצוגה מקדימה קטנה" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.components" +msgstr "%s רכיבים" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.file-library" +msgstr "ספריית קבצים" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.graphics" +msgstr "%s גרפיקה" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.in-this-file" +msgstr "ספריות בקובץ הזה" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.libraries" +msgstr "ספריות" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library" +msgstr "ספרייה" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-libraries-need-sync" +msgstr "אין ספריות משותפות שדורשות עדכון" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-matches-for" +msgstr "לא נמצאו תוצאות לחיפוש אחר „%s”" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-shared-libraries-available" +msgstr "אין ספריות משותפות זמינות" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.search-shared-libraries" +msgstr "חיפוש בספריות המשותפות" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.shared-libraries" +msgstr "ספריות משותפות" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.libraries.text.multiple-typography" +msgstr "מגוון טיפוגרפיות" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.libraries.text.multiple-typography-tooltip" +msgstr "ניתוק כל הטיפוגרפיות" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.typography" +msgstr "%s טיפוגרפיות" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.update" +msgstr "עדכון" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.updates" +msgstr "עדכונים" + +msgid "workspace.library.all" +msgstr "כל הספריות" + +msgid "workspace.library.libraries" +msgstr "ספריות" + +msgid "workspace.library.own" +msgstr "הספריות שלי" + +msgid "workspace.library.store" +msgstr "אחסון ספריות" + +msgid "workspace.options.blur-options.background-blur" +msgstr "רקע" + +msgid "workspace.options.blur-options.layer-blur" +msgstr "שכבה" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title" +msgstr "טשטוש" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title.group" +msgstr "טשטוש קבוצה" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title.multiple" +msgstr "טשטוש בחירה" + +#: src/app/main/ui/workspace/sidebar/options/page.cljs +msgid "workspace.options.canvas-background" +msgstr "רקע משטח ציור" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs +msgid "workspace.options.component" +msgstr "רכיב" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints" +msgstr "הגבלות" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.bottom" +msgstr "תחתית" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.center" +msgstr "מרכז" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.fix-when-scrolling" +msgstr "תיקון בעת גלילה" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.left" +msgstr "שמאל" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.leftright" +msgstr "שמאל וימין" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.right" +msgstr "ימין" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.scale" +msgstr "שינוי גודל" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.top" +msgstr "עליון" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.topbottom" +msgstr "עליון ותחתון" + +#: src/app/main/ui/workspace/sidebar/options.cljs +msgid "workspace.options.design" +msgstr "עיצוב" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.export" +msgstr "ייצוא" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.export-object" +msgstr "ייצוא צורה" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs +msgid "workspace.options.export.suffix" +msgstr "סיומת" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.exporting-object" +msgstr "מתבצע ייצוא…" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.fill" +msgstr "מילוי" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.auto" +msgstr "אוטומטי" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.column" +msgstr "עמודות" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.columns" +msgstr "עמודות" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.gutter" +msgstr "מרזב" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.height" +msgstr "גובה" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.margin" +msgstr "שול" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.rows" +msgstr "שורות" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.set-default" +msgstr "הגדרה כבררת מחדל" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.size" +msgstr "גודל" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type" +msgstr "סוג" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.bottom" +msgstr "תחתון" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.center" +msgstr "מרכז" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.left" +msgstr "שמאל" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.right" +msgstr "ימין" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.stretch" +msgstr "מתיחה" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.top" +msgstr "עליון" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.use-default" +msgstr "להשתמש בבררת המחדל" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.width" +msgstr "רוחב" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.row" +msgstr "שורות" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.square" +msgstr "ריבוע" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.title" +msgstr "רשת ופריסות" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.group-fill" +msgstr "מילוי קבוצה" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.group-stroke" +msgstr "מתאר קבוצה" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.color" +msgstr "צבע" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.color-burn" +msgstr "צריבת צבע" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.color-dodge" +msgstr "בריחת צבע" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.darken" +msgstr "החשכה" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.difference" +msgstr "הבדל" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.exclusion" +msgstr "הדרה" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.hard-light" +msgstr "אור קשיח" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.hue" +msgstr "גוון" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.lighten" +msgstr "הבהרה" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.luminosity" +msgstr "תאורה" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.multiply" +msgstr "הכפלה" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.normal" +msgstr "רגיל" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.overlay" +msgstr "שכבת על" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.saturation" +msgstr "רוויה" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.screen" +msgstr "מסך" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.soft-light" +msgstr "אור רך" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title" +msgstr "שכבה" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title.group" +msgstr "קיבוץ שכבות" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title.multiple" +msgstr "שכבות נבחרות" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.navigate-to" +msgstr "ניווט אל" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.none" +msgstr "ללא" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.position" +msgstr "מיקום" + +#: src/app/main/ui/workspace/sidebar/options.cljs +msgid "workspace.options.prototype" +msgstr "אבטיפוס" + +msgid "workspace.options.radius" +msgstr "רדיוס" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius.all-corners" +msgstr "כל הפינות" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius.single-corners" +msgstr "פינות בודדות" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.rotation" +msgstr "סיבוב" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.select-a-shape" +msgstr "נא לבחור צורה, לוח אומנות או קבוצה כדי לגרור חיבור ללוח אומנות אחר." + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.select-artboard" +msgstr "בחירת לוח אומנות" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.selection-fill" +msgstr "מילוי בחירה" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.selection-stroke" +msgstr "מתאר בחירה" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.blur" +msgstr "טשטוש" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.drop-shadow" +msgstr "הטלת צל" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.inner-shadow" +msgstr "צל פנימי" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.offsetx" +msgstr "X" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.offsety" +msgstr "Y" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.spread" +msgstr "פיזור" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title" +msgstr "צל" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title.group" +msgstr "צל של קבוצה" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title.multiple" +msgstr "צללים של בחירה" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.size" +msgstr "גודל" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +msgid "workspace.options.size-presets" +msgstr "תבניות גודל" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke" +msgstr "מתאר" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker" +msgstr "סמן עגול" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker" +msgstr "סמן יהלום" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow" +msgstr "חץ קו" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.none" +msgstr "ללא" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.round" +msgstr "עגול" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square" +msgstr "ריבוע" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker" +msgstr "סמן ריבוע" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow" +msgstr "חץ משולש" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.center" +msgstr "מרכז" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.dashed" +msgstr "מקווקוו" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.dotted" +msgstr "מנוקד" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.inner" +msgstr "בפנים" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.mixed" +msgstr "מעורב" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.outer" +msgstr "בחוץ" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.solid" +msgstr "אחיד" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-bottom" +msgstr "יישור לתחתית" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-center" +msgstr "יישור למרכז" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-justify" +msgstr "יישור לשני הצדדים" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-left" +msgstr "יישור שמאלה" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-middle" +msgstr "יישור לאמצע" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-right" +msgstr "יישור ימינה" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-top" +msgstr "יישור לראש" + +msgid "workspace.options.text-options.decoration" +msgstr "עיטור" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.direction-ltr" +msgstr "משמאל לימין" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.direction-rtl" +msgstr "מימין לשמאל" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.google" +msgstr "Google" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-auto-height" +msgstr "גובה אוטומטי" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-auto-width" +msgstr "רוחב אוטומטי" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-fixed" +msgstr "קבוע" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.letter-spacing" +msgstr "ריווח תווים" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.line-height" +msgstr "גובה שורה" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.lowercase" +msgstr "אותיות קטנות" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.none" +msgstr "ללא" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.preset" +msgstr "תבנית" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.strikethrough" +msgstr "קו חוצה" + +msgid "workspace.options.text-options.text-case" +msgstr "גדול/קטן" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title" +msgstr "טקסט" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title-group" +msgstr "קיבוץ טקסט" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title-selection" +msgstr "טקסט בחירה" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.titlecase" +msgstr "רישיות כותרת" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.underline" +msgstr "קו תחתי" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.uppercase" +msgstr "אותיות גדולות" + +msgid "workspace.options.text-options.vertical-align" +msgstr "יישור אנכי" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.use-play-button" +msgstr "ניתן להשתמש בכפתור הנגינה שבכותרת כדי להריץ את תצוגת האבטיפוס." + +msgid "workspace.path.actions.add-node" +msgstr "הוספת מפרק (%s)" + +msgid "workspace.path.actions.delete-node" +msgstr "מחיקת מפרק (%s)" + +msgid "workspace.path.actions.draw-nodes" +msgstr "ציור מפרקים (%s)" + +msgid "workspace.path.actions.join-nodes" +msgstr "צירוף מפרקים (%s)" + +msgid "workspace.path.actions.make-corner" +msgstr "לפינה (%s)" + +msgid "workspace.path.actions.make-curve" +msgstr "לעיקול (%s)" + +msgid "workspace.path.actions.merge-nodes" +msgstr "מיזוג מפרקים (%s)" + +msgid "workspace.path.actions.move-nodes" +msgstr "העברת מפרקים (%s)" + +msgid "workspace.path.actions.separate-nodes" +msgstr "הפרדת מפרקים (%s)" + +msgid "workspace.path.actions.snap-nodes" +msgstr "הצמדת מפרקים (%s)" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.back" +msgstr "הרחקה" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.backward" +msgstr "הרחקה לאחור" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.copy" +msgstr "העתקה" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.create-component" +msgstr "יצירת רכיב" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.cut" +msgstr "גזירה" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.delete" +msgstr "מחיקה" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.detach-instance" +msgstr "ניתוק מופע" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.duplicate" +msgstr "שכפול" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.edit" +msgstr "עריכה" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flip-horizontal" +msgstr "היפוך אופקי" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flip-vertical" +msgstr "היפוך אנכי" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.forward" +msgstr "קידום" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.front" +msgstr "קידום לחזית" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.go-main" +msgstr "מעבר לקובץ הרכיב הראשי" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.group" +msgstr "קבוצה" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.hide" +msgstr "הסתרה" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.lock" +msgstr "נעילה" + +#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.mask" +msgstr "מסכה" + +#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.paste" +msgstr "הדבקה" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.reset-overrides" +msgstr "איפוס מעקפים" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show" +msgstr "הצגה" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show-main" +msgstr "הצגת הרכיב הראשי" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.ungroup" +msgstr "פירוק קבוצה" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.unlock" +msgstr "שחרור נעילה" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.unmask" +msgstr "ביטול מסכה" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.update-main" +msgstr "עדכון הרכיב הראשי" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.sidebar.history" +msgstr "היסטוריה (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.sidebar.layers" +msgstr "שכבות (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs +msgid "workspace.sidebar.options.svg-attrs.title" +msgstr "מאפייני SVG יובאו" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "workspace.sidebar.sitemap" +msgstr "עמודים" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.sitemap" +msgstr "מפת אתר" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.assets" +msgstr "משאבים (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.color-palette" +msgstr "ערכת צבעים (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.comments" +msgstr "הערות (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.curve" +msgstr "עיקול (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.ellipse" +msgstr "אליפסה (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.frame" +msgstr "לוח אומנות (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.image" +msgstr "תמונה (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.move" +msgstr "העברה" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.path" +msgstr "נתיב (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.rect" +msgstr "ריבוע (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.text" +msgstr "טקסט (%s)" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.empty" +msgstr "אין שינויים היסטוריים עד כה" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.delete" +msgstr "%s נמחק" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.modify" +msgstr "%s נערך" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.move" +msgstr "פריטים שהועברו" + +msgid "workspace.undo.entry.multiple.circle" +msgstr "עיגולים" + +msgid "workspace.undo.entry.multiple.color" +msgstr "משאבי צבע" + +msgid "workspace.undo.entry.multiple.component" +msgstr "רכיבים" + +msgid "workspace.undo.entry.multiple.curve" +msgstr "עיקולים" + +msgid "workspace.undo.entry.multiple.frame" +msgstr "לוח אומנות" + +msgid "workspace.undo.entry.multiple.group" +msgstr "קבוצות" + +msgid "workspace.undo.entry.multiple.media" +msgstr "משאבים גרפיים" + +msgid "workspace.undo.entry.multiple.multiple" +msgstr "פריטים" + +msgid "workspace.undo.entry.multiple.page" +msgstr "עמודים" + +msgid "workspace.undo.entry.multiple.path" +msgstr "נתיבים" + +msgid "workspace.undo.entry.multiple.rect" +msgstr "ריבועים" + +msgid "workspace.undo.entry.multiple.shape" +msgstr "צורות" + +msgid "workspace.undo.entry.multiple.text" +msgstr "טקסטים" + +msgid "workspace.undo.entry.multiple.typography" +msgstr "משאבים טיפוגרפיים" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.new" +msgstr "%s חדש" + +msgid "workspace.undo.entry.single.circle" +msgstr "עיגול" + +msgid "workspace.undo.entry.single.color" +msgstr "משאב צבע" + +msgid "workspace.undo.entry.single.component" +msgstr "רכיב" + +msgid "workspace.undo.entry.single.curve" +msgstr "עיקול" + +msgid "workspace.undo.entry.single.frame" +msgstr "לוח אומנות" + +msgid "workspace.undo.entry.single.group" +msgstr "קבוצה" + +msgid "workspace.undo.entry.single.image" +msgstr "תמונה" + +msgid "workspace.undo.entry.single.media" +msgstr "משאב גרפי" + +msgid "workspace.undo.entry.single.multiple" +msgstr "פריט" + +msgid "workspace.undo.entry.single.page" +msgstr "עמוד" + +msgid "workspace.undo.entry.single.path" +msgstr "נתיב" + +msgid "workspace.undo.entry.single.rect" +msgstr "ריבוע" + +msgid "workspace.undo.entry.single.shape" +msgstr "צורה" + +msgid "workspace.undo.entry.single.text" +msgstr "טקסט" + +msgid "workspace.undo.entry.single.typography" +msgstr "משאב טיפוגרפי" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.unknown" +msgstr "פעולה על %s" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.title" +msgstr "היסטוריה" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.dismiss" +msgstr "התעלמות" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.there-are-updates" +msgstr "יש עדכונים בספריות המשותפות" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.update" +msgstr "עדכון" + +msgid "workspace.viewport.click-to-close-path" +msgstr "לחיצה תסגור את הנתיב" \ No newline at end of file diff --git a/frontend/translations/pt_BR.po b/frontend/translations/pt_BR.po index 8e8046458..6cd4fdd6b 100644 --- a/frontend/translations/pt_BR.po +++ b/frontend/translations/pt_BR.po @@ -1662,14 +1662,6 @@ msgstr "Camadas do grupo" msgid "workspace.options.layer-options.title.multiple" msgstr "Camadas selecionadas" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.navigate-to" -msgstr "Navegar para" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.none" -msgstr "Nenhum" - #: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Posição" diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index 2dd4d6e5d..c9feb0fb1 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -2048,14 +2048,6 @@ msgstr "Grupează layere" msgid "workspace.options.layer-options.title.multiple" msgstr "Layere selectate" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.navigate-to" -msgstr "Navighează la" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.none" -msgstr "Nici unul" - #: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Poziţie" diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index 6f0d9cc98..ad4d9f0f6 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -957,14 +957,6 @@ msgstr "Заливка для группы" msgid "workspace.options.group-stroke" msgstr "Обводка для группы" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.navigate-to" -msgstr "Перейти к" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.none" -msgstr "Не задано" - #: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "Позиция" diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index afaf33288..33f368fb3 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2021-07-05 17:02+0000\n" -"Last-Translator: Çağlar Yeşilyurt \n" +"PO-Revision-Date: 2021-09-10 10:57+0000\n" +"Last-Translator: Oğuz Ersen \n" "Language-Team: Turkish " "\n" "Language: tr\n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.8-dev\n" +"X-Generator: Weblate 4.8.1-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -91,13 +91,17 @@ msgstr "OpenID (SSO) ile Giriş Yap" msgid "auth.new-password" msgstr "Yeni bir parola gir" +#: src/app/main/ui/auth/register.cljs +msgid "auth.newsletter-subscription" +msgstr "Penpot e-posta listesine abone olmayı kabul ediyorum." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.invalid-token-error" -msgstr "Kurtarma jetonu geçerli değil" +msgstr "Kurtarma jetonu geçerli değil." #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.password-changed-succesfully" -msgstr "Parola başarıyla değiştirldi" +msgstr "Parola başarıyla değiştirildi" #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.notifications.profile-not-verified" @@ -165,6 +169,44 @@ msgstr "" msgid "auth.verification-email-sent" msgstr "Onay e-postanı şu adrese gönderdik" +msgid "common.share-link.confirm-deletion-link-description" +msgstr "" +"Bu bağlantıyı kaldırmak istediğinizden emin misiniz? Bunu yaparsanız, artık " +"kimse tarafından kullanılamayacak" + +msgid "common.share-link.get-link" +msgstr "Bağlantıyı al" + +msgid "common.share-link.link-copied-success" +msgstr "Bağlantı başarıyla kopyalandı" + +msgid "common.share-link.link-deleted-success" +msgstr "Bağlantı başarıyla silindi" + +msgid "common.share-link.permissions-can-access" +msgstr "Erişebilir" + +msgid "common.share-link.permissions-can-view" +msgstr "Görüntüleyebilir" + +msgid "common.share-link.permissions-hint" +msgstr "Bağlantıya sahip olan herkes erişebilir" + +msgid "common.share-link.remove-link" +msgstr "Bağlantıyı kaldır" + +msgid "common.share-link.title" +msgstr "Prototipleri paylaş" + +msgid "common.share-link.view-all-pages" +msgstr "Tüm sayfalar" + +msgid "common.share-link.view-current-page" +msgstr "Yalnızca bu sayfa" + +msgid "common.share-link.view-selected-pages" +msgstr "Seçili sayfalar" + #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Paylaşılan Kitaplık olarak ekle" @@ -179,7 +221,7 @@ msgstr "(kopya)" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.create-new-team" -msgstr "Yeni takım oluştur" +msgstr "+ Yeni takım oluştur" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.default-team-name" @@ -204,31 +246,87 @@ msgstr "%s dosyanın kopyasını oluştur" msgid "dashboard.empty-files" msgstr "Burada hiç dosyan yok" +msgid "dashboard.export-frames" +msgstr "Çalışma yüzeylerini PDF olarak dışarı aktar..." + msgid "dashboard.export-multi" -msgstr "%s dosyalarını dışarı aktar" +msgstr "%s dosyayı dışarı aktar" msgid "dashboard.export-single" msgstr "Dosyayı dışarı aktar" +msgid "dashboard.export.detail" +msgstr "* Bileşenleri, grafikleri, renkleri ve/veya tipografileri içerebilir." + +msgid "dashboard.export.explain" +msgstr "" +"Dışarı aktarmak istediğiniz bir veya daha fazla dosya, paylaşılan " +"kütüphaneleri kullanıyor. Bunların varlıklarıyla ne yapmak istiyorsunuz*?" + +msgid "dashboard.export.options.all.message" +msgstr "" +"paylaşılan kütüphanelere sahip dosyalar, bağlantılarını koruyarak dışarı " +"aktarmaya dahil edilecek." + +msgid "dashboard.export.options.all.title" +msgstr "Paylaşılan kütüphaneleri dışarı aktar" + +msgid "dashboard.export.options.detach.message" +msgstr "" +"Paylaşılan kütüphaneler dışarı aktarmaya dahil edilmeyecek ve kütüphaneye " +"hiçbir varlık eklenmeyecek. " + +msgid "dashboard.export.options.detach.title" +msgstr "Paylaşılan kütüphane varlıklarını temel nesneler olarak ele al" + +msgid "dashboard.export.options.merge.message" +msgstr "" +"Dosyanız, tüm harici varlıklar kütüphane dosyasına birleştirilmiş olarak " +"dışarı aktarılacak." + +msgid "dashboard.export.options.merge.title" +msgstr "Dosya kütüphanelerine paylaşılan kütüphane varlıklarını dahil et" + +msgid "dashboard.export.title" +msgstr "Dosyaları dışarı aktar" + msgid "dashboard.fonts.deleted-placeholder" msgstr "Yazı tipi silindi" msgid "dashboard.fonts.empty-placeholder" msgstr "Kurulu özel yazı tipiniz bulunmamaktadır." +#, markdown +msgid "dashboard.fonts.hero-text1" +msgstr "" +"Buraya yüklediğiniz herhangi bir web yazı tipi, bu takımın dosyalarının " +"metin özelliklerinde bulunan yazı tipi ailesi listesine eklenecek. Aynı " +"yazı tipi ailesi adına sahip yazı tipleri, **tek yazı tipi ailesi** olarak " +"gruplandırılacak. Yazı tiplerini şu biçimlerde yükleyebilirsiniz: **TTF, " +"OTF ve WOFF** (yalnızca bir tane gerekli olacak)." + #, markdown msgid "dashboard.fonts.hero-text2" msgstr "" "Sadece kendinize ait veya Penpot'ta kullanılabilecek bir lisansa sahip olan " -"yazi tiplerini yükleyebilirsiniz. [Penpot'un Kullanım Şartları] içindeki " -"İçerik hakları bölümünden detaylı bilgi alabilirsiniz " -"(https://penpot.app/terms.html). Ayrıca [yazı tipi " +"yazi tiplerini yükleyebilirsiniz. [Penpot'un Kullanım " +"Şartları](https://penpot.app/terms.html) içindeki İçerik hakları bölümünden " +"ayrıntılı bilgi alabilirsiniz. Ayrıca [yazı tipi " "lisanslama](https://www.typography.com/faq) hakkında daha fazla bilgi almak " "isteyebilirsiniz." msgid "dashboard.import" msgstr "Dosyaları içeri aktar" +msgid "dashboard.import.analyze-error" +msgstr "Oops! Bu dosyayı içeri aktaramadık" + +msgid "dashboard.import.import-error" +msgstr "Dosya içeri aktarılırken bir sorun oluştu. Dosya içeri aktarılmadı." + +msgid "dashboard.import.import-message" +msgstr "%s dosya başarıyla içeri aktarıldı." + #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.invite-profile" msgstr "Takıma davet et" @@ -239,7 +337,7 @@ msgstr "Takımdan ayrıl" #: src/app/main/ui/dashboard/libraries.cljs msgid "dashboard.libraries-title" -msgstr "Paylaşılan Kitaplıklar" +msgstr "Paylaşılan Kütüphaneler" #: src/app/main/ui/dashboard/grid.cljs msgid "dashboard.loading-files" @@ -258,11 +356,11 @@ msgstr "%s dosyayı şuraya taşı" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.move-to-other-team" -msgstr "Başkta takıma taşı" +msgstr "Başka takıma taşı" #: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" -msgstr "Yeni Dosya" +msgstr "+ Yeni Dosya" #: src/app/main/data/dashboard.cljs msgid "dashboard.new-file-prefix" @@ -270,7 +368,7 @@ msgstr "Yeni Dosya" #: src/app/main/ui/dashboard/projects.cljs msgid "dashboard.new-project" -msgstr "Yeni Proje" +msgstr "+ Yeni Proje" #: src/app/main/data/dashboard.cljs msgid "dashboard.new-project-prefix" @@ -305,7 +403,7 @@ msgid "dashboard.open-in-new-tab" msgstr "Dosyayı yeni sekmede aç" msgid "dashboard.options" -msgstr "Ayarlar" +msgstr "Seçenekler" #: src/app/main/ui/settings/password.cljs msgid "dashboard.password-change" @@ -325,11 +423,11 @@ msgstr "Sahibi olarak belirle" #: src/app/main/ui/settings/profile.cljs msgid "dashboard.remove-account" -msgstr "Hesabını silmek istediğinden emin misin?" +msgstr "Hesabınızı kaldırmak mı istiyorsunuz?" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" -msgstr "Paylaşılan Kitaplık olarak sil" +msgstr "Paylaşılan Kütüphane olarak sil" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.search-placeholder" @@ -337,7 +435,7 @@ msgstr "Ara…" #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.searching-for" -msgstr "%s aranıyor…" +msgstr "“%s“ aranıyor…" #: src/app/main/ui/settings/options.cljs msgid "dashboard.select-ui-language" @@ -397,7 +495,7 @@ msgstr "Takım projeleri" #: src/app/main/ui/settings/options.cljs msgid "dashboard.theme-change" -msgstr "Önyüz teması" +msgstr "Kullanıcı arayüzü teması" #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.title-search" @@ -429,7 +527,7 @@ msgstr "Penpot'un" #: src/app/main/ui/confirm.cljs msgid "ds.confirm-cancel" -msgstr "Vazgeç" +msgstr "İptal" #: src/app/main/ui/confirm.cljs msgid "ds.confirm-ok" @@ -480,7 +578,7 @@ msgid "errors.ldap-disabled" msgstr "LDAP ile oturum açma devre dışı bırakıldı." msgid "errors.media-format-unsupported" -msgstr "Görsel formatı desteklenmiyor (svg, jpg veya png olmalı)." +msgstr "Görsel biçimi desteklenmiyor (svg, jpg veya png olmalı)." #: src/app/main/data/workspace/persistence.cljs msgid "errors.media-too-large" @@ -488,7 +586,7 @@ msgstr "Bu görsel eklemek için çok büyük (5MB altında olmalı)." #: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-mismatch" -msgstr "Dosya içeriği, uzantısı ile eşleşmiyor gibi görünüyor." +msgstr "Görselin içeriği, dosya uzantısı ile eşleşmiyor gibi görünüyor." #: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" @@ -594,7 +692,7 @@ msgstr "Bir hata oluştu" #: src/app/main/ui/handoff/attributes/blur.cljs msgid "handoff.attributes.blur" -msgstr "Bulanıklaştır" +msgstr "Bulanıklık" #: src/app/main/ui/handoff/attributes/blur.cljs msgid "handoff.attributes.blur.value" @@ -628,6 +726,10 @@ msgstr "Yükseklik" msgid "handoff.attributes.image.width" msgstr "Genişlik" +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout" +msgstr "Yerleşim düzeni" + #: src/app/main/ui/handoff/attributes/layout.cljs msgid "handoff.attributes.layout.height" msgstr "Yükseklik" @@ -638,7 +740,7 @@ msgstr "Sol" #: src/app/main/ui/handoff/attributes/layout.cljs, src/app/main/ui/handoff/attributes/layout.cljs msgid "handoff.attributes.layout.radius" -msgstr "Yarı Çap" +msgstr "Yarıçap" #: src/app/main/ui/handoff/attributes/layout.cljs msgid "handoff.attributes.layout.rotation" @@ -670,7 +772,7 @@ msgstr "Y" #: src/app/main/ui/handoff/attributes/shadow.cljs msgid "handoff.attributes.shadow.shorthand.spread" -msgstr "Y" +msgstr "S" #: src/app/main/ui/handoff/attributes/stroke.cljs msgid "handoff.attributes.stroke" @@ -710,15 +812,15 @@ msgstr "Tipografi" #: src/app/main/ui/handoff/attributes/text.cljs msgid "handoff.attributes.typography.font-family" -msgstr "Font Ailesi" +msgstr "Yazı Tipi Ailesi" #: src/app/main/ui/handoff/attributes/text.cljs msgid "handoff.attributes.typography.font-size" -msgstr "Font Boyutu" +msgstr "Yazı Tipi Boyutu" #: src/app/main/ui/handoff/attributes/text.cljs msgid "handoff.attributes.typography.font-style" -msgstr "Font Stili" +msgstr "Yazı Tipi Biçimi" #: src/app/main/ui/handoff/attributes/text.cljs msgid "handoff.attributes.typography.letter-spacing" @@ -830,6 +932,9 @@ msgstr "İptal" msgid "labels.centered" msgstr "Orta" +msgid "labels.close" +msgstr "Kapat" + #: src/app/main/ui/dashboard/comments.cljs msgid "labels.comments" msgstr "Yorumlar" @@ -841,6 +946,9 @@ msgstr "Parolayı onayla" msgid "labels.content" msgstr "İçerik" +msgid "labels.continue" +msgstr "Devam et" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "labels.create" msgstr "Oluştur" @@ -854,11 +962,14 @@ msgid "labels.create-team.placeholder" msgstr "Yeni takım adı gir" msgid "labels.custom-fonts" -msgstr "Özel Fontlar" +msgstr "Özel yazı tipleri" #: src/app/main/ui/settings/sidebar.cljs msgid "labels.dashboard" -msgstr "Kontrol paneli" +msgstr "Denetim paneli" + +msgid "labels.default" +msgstr "varsayılan" #: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "labels.delete" @@ -884,14 +995,20 @@ msgstr "Taslak" msgid "labels.edit" msgstr "Düzenle" +msgid "labels.edit-file" +msgstr "Dosya düzenle" + #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.editor" -msgstr "Editör" +msgstr "Düzenleyici" #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.email" msgstr "E-posta" +msgid "labels.export" +msgstr "Dışarı aktar" + #: src/app/main/ui/settings/feedback.cljs msgid "labels.feedback-disabled" msgstr "Geri bildirim devre dışı bırakıldı" @@ -901,10 +1018,10 @@ msgid "labels.feedback-sent" msgstr "Geri bildirim gönderildi" msgid "labels.font-family" -msgstr "Font Ailesi" +msgstr "Yazı Tipi Ailesi" msgid "labels.font-providers" -msgstr "Font sağlayıcısı" +msgstr "Yazı tipi sağlayıcıları" msgid "labels.font-variant" msgstr "Stil" @@ -913,7 +1030,7 @@ msgid "labels.font-variants" msgstr "Biçimler" msgid "labels.fonts" -msgstr "Fontlar" +msgstr "Yazı tipleri" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.give-feedback" @@ -933,7 +1050,7 @@ msgid "labels.images" msgstr "Görseller" msgid "labels.installed-fonts" -msgstr "Yüklenmiş fontlar" +msgstr "Kurulu yazı tipleri" #: src/app/main/ui/static.cljs msgid "labels.internal-error.desc-message" @@ -949,12 +1066,15 @@ msgstr "İç Hata" msgid "labels.language" msgstr "Dil" +msgid "labels.link" +msgstr "Bağlantı" + #: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.logout" msgstr "Çıkış Yap" msgid "labels.manage-fonts" -msgstr "Fontları yönet" +msgstr "Yazı tiplerini yönet" #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "labels.members" @@ -990,6 +1110,11 @@ msgid_plural "labels.num-of-files" msgstr[0] "1 dosya" msgstr[1] "%s dosya" +msgid "labels.num-of-frames" +msgid_plural "labels.num-of-frames" +msgstr[0] "1 çalışma yüzeyi" +msgstr[1] "%s çalışma yüzeyi" + #: src/app/main/ui/dashboard/team.cljs msgid "labels.num-of-projects" msgid_plural "labels.num-of-projects" @@ -1004,6 +1129,9 @@ msgstr "Eski parola" msgid "labels.only-yours" msgstr "Sadece seninkiler" +msgid "labels.or" +msgstr "veya" + #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.owner" msgstr "Sahip" @@ -1055,7 +1183,7 @@ msgid "labels.save" msgstr "Kaydet" msgid "labels.search-font" -msgstr "Font ara" +msgstr "Yazı tipi ara" #: src/app/main/ui/settings/feedback.cljs msgid "labels.send" @@ -1083,7 +1211,7 @@ msgstr "Prototipi paylaş" #: src/app/main/ui/dashboard/sidebar.cljs msgid "labels.shared-libraries" -msgstr "Paylaşılan Kitaplıklar" +msgstr "Paylaşılan Kütüphaneler" #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.show-all-comments" @@ -1097,6 +1225,12 @@ msgstr "Yalnızca kendi yorumlarımı göster" msgid "labels.sign-out" msgstr "Çıkış yap" +msgid "labels.skip" +msgstr "Atla" + +msgid "labels.start" +msgstr "Başla" + #: src/app/main/ui/settings/profile.cljs msgid "labels.update" msgstr "Güncelle" @@ -1116,7 +1250,10 @@ msgstr "Yükleniyor…" #: src/app/main/ui/dashboard/team.cljs msgid "labels.viewer" -msgstr "Görüntüler" +msgstr "Görüntüleyici" + +msgid "labels.workspace" +msgstr "Çalışma alanı" #: src/app/main/ui/comments.cljs msgid "labels.write-new-comment" @@ -1124,7 +1261,7 @@ msgstr "Yeni yorum yaz" #: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "media.loading" -msgstr "Resim yükleniyor…" +msgstr "Görsel yükleniyor…" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.add-shared-confirm.accept" @@ -1206,7 +1343,7 @@ msgstr "Dosya siliniyor" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-multi-confirm.accept" -msgstr "Dosyalar sil" +msgstr "Dosyaları sil" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-multi-confirm.message" @@ -1214,15 +1351,23 @@ msgstr "%s dosyayı silmek istediğinden emin misin?" #: src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-file-multi-confirm.title" -msgstr "%s dosyanın silinmesi" +msgstr "%s dosyayı sil" + +msgid "modals.delete-font-variant.message" +msgstr "" +"Bu yazı tipi biçimini silmek istediğinizden emin misiniz? Bir dosyada " +"kullanılıyorsa yüklenmeyecektir." + +msgid "modals.delete-font-variant.title" +msgstr "Yazı tipi biçimini sil" msgid "modals.delete-font.message" msgstr "" -"Bu fontu silmek istediğine emin misin? Bir dosyada kullanılıyorsa " +"Bu yazı tipini silmek istediğinize emin misiniz? Bir dosyada kullanılıyorsa " "yüklenmeyecektir." msgid "modals.delete-font.title" -msgstr "Fontu sil" +msgstr "Yazı tipini sil" #: src/app/main/ui/workspace/sidebar/sitemap.cljs msgid "modals.delete-page.body" @@ -1256,7 +1401,7 @@ msgstr "" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.delete-team-confirm.title" -msgstr "Takımın silinmesi" +msgstr "Takımı sil" #: src/app/main/ui/dashboard/team.cljs msgid "modals.delete-team-member-confirm.accept" @@ -1289,19 +1434,19 @@ msgstr "%s sahibisiniz." #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-reassign.hint2" -msgstr "Ayrılmadan önce terfi etmek için başka bir üye seçin" +msgstr "Ayrılmadan önce terfi ettirmek için başka bir üye seçin" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-reassign.promote-and-leave" -msgstr "Terfi et ve ayrıl" +msgstr "Terfi ettir ve ayrıl" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-reassign.select-memeber-to-promote" -msgstr "Terfi etmek için bir üye seçin" +msgstr "Terfi ettirmek için bir üye seçin" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-reassign.title" -msgstr "Terfi etmek için bir üye seçin" +msgstr "Terfi ettirmek için bir üye seçin" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-confirm.accept" @@ -1313,23 +1458,33 @@ msgstr "Bu takımdan ayrılmak istediğinden emin misin?" #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-confirm.title" -msgstr "Takımdan ayrılmak" +msgstr "Takımdan ayrıl" #: src/app/main/ui/dashboard/team.cljs msgid "modals.promote-owner-confirm.accept" -msgstr "Terfi et" +msgstr "Terfi ettir" #: src/app/main/ui/dashboard/team.cljs msgid "modals.promote-owner-confirm.message" -msgstr "Bu kullanıcıyı sahip olarak terfi etmek istediğinden emin misin?" +msgstr "Bu kullanıcıyı sahip olarak terfi ettirmek istediğinden emin misin?" #: src/app/main/ui/dashboard/team.cljs msgid "modals.promote-owner-confirm.title" -msgstr "Sahip olarak terfi et" +msgstr "Sahip olarak terfi ettir" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.remove-shared-confirm.accept" -msgstr "Paylaşılmış Kütüphane olarak kaldır" +msgstr "Paylaşılan Kütüphane olarak kaldır" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.hint" +msgstr "" +"Paylaşılan Kütüphane olarak kaldırıldıktan sonra, bu dosyanın Dosya " +"Kütüphanesi, dosyalarınızın geri kalanında artık kullanılabilir olmayacak." + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.message" +msgstr "“%s” Paylaşılan Kütüphanesini Kaldır" #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs msgid "modals.update-remote-component.accept" @@ -1355,7 +1510,7 @@ msgstr "Davet başarıyla iletildi" #: src/app/main/ui/settings/delete_account.cljs msgid "notifications.profile-deletion-not-allowed" -msgstr "Profilini silemezsin. Önce takımlarını birine atamalsın." +msgstr "Profilini silemezsin. Önce takımlarını birine atamalısın." #: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/options.cljs msgid "notifications.profile-saved" @@ -1363,7 +1518,115 @@ msgstr "Profil başarıyla kaydedildi!" #: src/app/main/ui/settings/change_email.cljs msgid "notifications.validation-email-sent" -msgstr "%s adresine doğrulama e-postası gönderildi. E-postalarınızı kontrol edin!" +msgstr "%s adresine doğrulama e-postası gönderildi. E-posta kutunuza bakın!" + +msgid "onboarding.contrib.alt" +msgstr "Açık Kaynak" + +msgid "onboarding.contrib.desc1" +msgstr "" +"Penpot, topluluk tarafından ve topluluk için yapılan Açık Kaynaklı bir " +"projedir. İş birliği yapmak istiyorsanız hoş geldiniz!" + +msgid "onboarding.contrib.desc2.1" +msgstr "Projeye" + +msgid "onboarding.contrib.desc2.2" +msgstr "ve katkıda bulunma talimatlarını izleyebilirsiniz :)" + +msgid "onboarding.contrib.link" +msgstr "github'da erişebilir" + +msgid "onboarding.contrib.title" +msgstr "Açık Kaynağa Katkıda Bulunan Birisi?" + +msgid "onboarding.slide.0.alt" +msgstr "Tasarımlar oluşturun" + +msgid "onboarding.slide.0.desc1" +msgstr "Tüm takım üyeleriyle iş birliği içinde güzel kullanıcı arayüzleri oluşturun." + +msgid "onboarding.slide.0.desc2" +msgstr "" +"Bileşenler, kütüphaneler ve tasarım sistemleriyle uygun ölçekte tutarlılığı " +"sağlayın." + +msgid "onboarding.slide.0.title" +msgstr "Kütüphaneler, biçimler ve bileşenler tasarlayın" + +msgid "onboarding.slide.1.alt" +msgstr "Etkileşimli prototipler" + +msgid "onboarding.slide.1.desc1" +msgstr "Ürün davranışını taklit etmek için zengin etkileşimler oluşturun." + +msgid "onboarding.slide.1.desc2" +msgstr "" +"Paydaşlarla paylaşın, takımınıza teklifler sunun ve tasarımlarınız için " +"kullanıcı testlerini başlatın, hepsi tek bir yerde." + +msgid "onboarding.slide.1.title" +msgstr "Etkileşimlerle tasarımlarınıza hayat verin" + +msgid "onboarding.slide.2.alt" +msgstr "Geri bildirim alın" + +msgid "onboarding.slide.2.desc1" +msgstr "" +"Tüm takım üyeleri tasarımlar üzerinde gerçek zamanlı tasarım, çok oyunculu " +"ve merkezi yorumlar, fikirler ve geri bildirimler ile aynı anda çalışır." + +msgid "onboarding.slide.2.title" +msgstr "Geri bildirim alın, çalışmanızı sunun ve paylaşın" + +msgid "onboarding.slide.3.desc1" +msgstr "" +"Tüm bileşenlerinizin ve biçimlerinizin tasarımını ve kodunu eşzamanlayın ve " +"kod parçacıkları alın." + +msgid "onboarding.slide.3.desc2" +msgstr "" +"İşaretleme (SVG, HTML) veya biçimler (CSS, Less, Stylus…) gibi kod " +"özellikleri alın ve sağlayın." + +msgid "onboarding.team.create.button" +msgstr "Takım oluştur" + +msgid "onboarding.team.create.desc1" +msgstr "" +"Biriyle mi çalışıyorsunuz? Projelerde birlikte çalışmak ve tasarım " +"varlıklarını paylaşmak için bir takım oluşturun." + +msgid "onboarding.team.create.input-placeholder" +msgstr "Yeni takım adı gir" + +msgid "onboarding.team.create.title" +msgstr "Takım oluştur" + +msgid "onboarding.team.start.button" +msgstr "Hemen başla" + +msgid "onboarding.team.start.desc1" +msgstr "" +"Hemen Penpot'a atlayın ve kendi başınıza tasarlamaya başlayın. Daha sonra " +"takımlar oluşturma şansınız olacak." + +msgid "onboarding.team.start.title" +msgstr "Tasarlamaya başla" + +msgid "onboarding.welcome.alt" +msgstr "Penpot" + +msgid "onboarding.welcome.desc1" +msgstr "Sizi ilk Alfa sürümüyle tanıştırmaktan mutluluk duyuyoruz." + +msgid "onboarding.welcome.desc2" +msgstr "" +"Penpot hala geliştirme aşamasındadır ve sürekli güncellemeler olacaktır. " +"İlk kararlı sürümü beğeneceğinizi umuyoruz." + +msgid "onboarding.welcome.title" +msgstr "Penpot'a hoş geldiniz!" #: src/app/main/ui/auth/recovery.cljs msgid "profile.recovery.go-to-login" @@ -1379,11 +1642,11 @@ msgstr "%s - Penpot" #: src/app/main/ui/dashboard/fonts.cljs msgid "title.dashboard.font-providers" -msgstr "Yazıtipi Sağlayıcıları - %s - Penpot" +msgstr "Yazı Tipi Sağlayıcıları - %s - Penpot" #: src/app/main/ui/dashboard/fonts.cljs msgid "title.dashboard.fonts" -msgstr "Fontlar - %s - Penpot" +msgstr "Yazı Tipleri - %s - Penpot" #: src/app/main/ui/dashboard/projects.cljs msgid "title.dashboard.projects" @@ -1423,7 +1686,7 @@ msgstr "Üyeler - %s - Penpot" #: src/app/main/ui/dashboard/team.cljs msgid "title.team-settings" -msgstr "Ayarlar * %s - Penpot" +msgstr "Ayarlar - %s - Penpot" #: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs msgid "title.viewer" @@ -1435,11 +1698,14 @@ msgstr "%s - Penpot" #: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs msgid "viewer.empty-state" -msgstr "Sayfada çerçeve bulunmuyor." +msgstr "Sayfada çalışma yüzeyi bulunamadı." #: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs msgid "viewer.frame-not-found" -msgstr "Çerçeve bulunmadı." +msgstr "Çalışma yüzeyi bulunamadı." + +msgid "viewer.header.comments-section" +msgstr "Yorumlar" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.dont-show-interactions" @@ -1461,6 +1727,9 @@ msgstr "Tam Ekran" msgid "viewer.header.interactions" msgstr "Etkileşimler" +msgid "viewer.header.interactions-section" +msgstr "Etkileşimler" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.share.copy-link" msgstr "Bağlantıyı kopyala" @@ -1548,6 +1817,10 @@ msgstr "Bileşenler" msgid "workspace.assets.create-group" msgstr "Grup oluştur" +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.create-group-hint" +msgstr "Ögeleriniz otomatik olarak \"grup adı / öge adı\" olarak adlandırılacak" + #: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.delete" msgstr "Sil" @@ -1590,7 +1863,7 @@ msgstr "Yeniden adlandır" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.rename-group" -msgstr "Grubu yeniden isimlendir" +msgstr "Grubu yeniden adlandır" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.search" @@ -1599,7 +1872,7 @@ msgstr "Varlık ara" #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.selected-count" msgid_plural "workspace.assets.selected-count" -msgstr[0] "Tek öge seçildi" +msgstr[0] "%s öge seçildi" msgstr[1] "%s öge seçildi" #: src/app/main/ui/workspace/sidebar/assets.cljs @@ -1634,6 +1907,10 @@ msgstr "Harf Boşluğu" msgid "workspace.assets.typography.line-height" msgstr "Satır Yüksekliği" +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/handoff/attributes/text.cljs, src/app/main/ui/handoff/attributes/text.cljs +msgid "workspace.assets.typography.sample" +msgstr "Ag" + #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.text-transform" msgstr "Metin Dönüşümü" @@ -1720,7 +1997,7 @@ msgstr "Cetveli göster" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.save-error" -msgstr "Kaydetmede hata" +msgstr "Kaydetme hatası" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.saved" @@ -1754,10 +2031,22 @@ msgstr "Büyük önizlemeler" msgid "workspace.libraries.colors.file-library" msgstr "Dosya kütüphanesi" +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.hsv" +msgstr "HSV" + #: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.recent-colors" msgstr "Son renkler" +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.rgb-complementary" +msgstr "RGB Tamamlayıcı" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.rgba" +msgstr "RGBA" + #: src/app/main/ui/workspace/colorpicker.cljs msgid "workspace.libraries.colors.save-color" msgstr "Renk biçimini kaydet" @@ -1839,8 +2128,11 @@ msgstr "Kütüphaneler" msgid "workspace.library.own" msgstr "Kütüphanelerim" +msgid "workspace.library.store" +msgstr "Mağaza kütüphaneleri" + msgid "workspace.options.blur-options.background-blur" -msgstr "Arkaplan" +msgstr "Arka plan" msgid "workspace.options.blur-options.layer-blur" msgstr "Katman" @@ -1849,6 +2141,18 @@ msgstr "Katman" msgid "workspace.options.blur-options.title" msgstr "Bulanıklık" +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title.group" +msgstr "Grup bulanıklığı" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title.multiple" +msgstr "Seçim bulanıklığı" + +#: src/app/main/ui/workspace/sidebar/options/page.cljs +msgid "workspace.options.canvas-background" +msgstr "Tuval arka planı" + #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs msgid "workspace.options.component" msgstr "Bileşen" @@ -1929,6 +2233,10 @@ msgstr "Sütunlar" msgid "workspace.options.grid.params.columns" msgstr "Sütunlar" +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.gutter" +msgstr "Aralık" + #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.height" msgstr "Yükseklik" @@ -1993,6 +2301,10 @@ msgstr "Satırlar" msgid "workspace.options.grid.square" msgstr "Kare" +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.title" +msgstr "Izgara ve Yerleşim Düzenleri" + #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.color" msgstr "Renk" @@ -2001,6 +2313,34 @@ msgstr "Renk" msgid "workspace.options.layer-options.blend-mode.color-burn" msgstr "Renk yanması" +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.darken" +msgstr "Karart" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.difference" +msgstr "Fark" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.hard-light" +msgstr "Sert ışık" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.hue" +msgstr "Ton" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.lighten" +msgstr "Aydınlat" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.luminosity" +msgstr "Parlaklık" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.multiply" +msgstr "Çoğalt" + #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.blend-mode.normal" msgstr "Normal" @@ -2017,6 +2357,10 @@ msgstr "Doygunluk" msgid "workspace.options.layer-options.blend-mode.screen" msgstr "Ekran" +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.soft-light" +msgstr "Yumuşak ışık" + #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs msgid "workspace.options.layer-options.title" msgstr "Katman" @@ -2035,7 +2379,7 @@ msgstr "Git" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.none" -msgstr "Hiç biri" +msgstr "Hiçbiri" #: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" @@ -2046,7 +2390,7 @@ msgid "workspace.options.prototype" msgstr "Prototip" msgid "workspace.options.radius" -msgstr "Yarı çap" +msgstr "Yarıçap" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius.all-corners" @@ -2070,6 +2414,10 @@ msgstr "" msgid "workspace.options.select-artboard" msgstr "Çalışma yüzeyi seç" +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.blur" +msgstr "Bulanıklık" + #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.drop-shadow" msgstr "Kabartı gölgesi" @@ -2106,10 +2454,38 @@ msgstr "Gölge seçimi" msgid "workspace.options.size" msgstr "Boyut" +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +msgid "workspace.options.size-presets" +msgstr "Boyut ön ayarları" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke" msgstr "Kontur" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker" +msgstr "Daire işaretleyici" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker" +msgstr "Elmas işaretleyici" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.none" +msgstr "Hiçbiri" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.round" +msgstr "Yuvarlak" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square" +msgstr "Kare" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker" +msgstr "Kare işaretleyici" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke.center" msgstr "Merkez" @@ -2209,10 +2585,17 @@ msgstr "Küçük harf" msgid "workspace.options.text-options.none" msgstr "Hiçbiri" +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.preset" +msgstr "Ön ayar" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" msgstr "Üstü çizili" +msgid "workspace.options.text-options.text-case" +msgstr "Durum" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" msgstr "Metin" @@ -2238,7 +2621,7 @@ msgid "workspace.options.text-options.uppercase" msgstr "Büyük Harf" msgid "workspace.options.text-options.vertical-align" -msgstr "Düşey hizalama" +msgstr "Dikey hizalama" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.use-play-button" @@ -2276,11 +2659,11 @@ msgstr "Düğümleri tuttur (%s)" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.back" -msgstr "Arkaya gönder" +msgstr "En arkaya gönder" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.backward" -msgstr "En arkaya gönder" +msgstr "Arkaya gönder" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.copy" @@ -2298,6 +2681,10 @@ msgstr "Kes" msgid "workspace.shape.menu.delete" msgstr "Sil" +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.detach-instance" +msgstr "Örneği ayır" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.duplicate" msgstr "Çoğalt" @@ -2346,6 +2733,10 @@ msgstr "Maskele" msgid "workspace.shape.menu.paste" msgstr "Yapıştır" +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.reset-overrides" +msgstr "Geçersiz kılmaları sıfırla" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.show" msgstr "Göster" @@ -2360,7 +2751,11 @@ msgstr "Grubu dağıt" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.unlock" -msgstr "Çöz" +msgstr "Kilidi aç" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.unmask" +msgstr "Maskelemeyi kaldır" #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.update-main" @@ -2376,7 +2771,7 @@ msgstr "Katmanlar (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs msgid "workspace.sidebar.options.svg-attrs.title" -msgstr "SVG Öznitelikleri İçeri Aktarıldı" +msgstr "İçe Aktarılan SVG Öznitelikleri" #: src/app/main/ui/workspace/sidebar/sitemap.cljs msgid "workspace.sidebar.sitemap" @@ -2412,12 +2807,16 @@ msgstr "Çalışma Yüzeyi (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.image" -msgstr "Resim (%s)" +msgstr "Görsel (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.move" msgstr "Taşı" +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.path" +msgstr "Yol (%s)" + #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.rect" msgstr "Dikdörtgen (%s)" @@ -2461,7 +2860,7 @@ msgid "workspace.undo.entry.multiple.group" msgstr "gruplar" msgid "workspace.undo.entry.multiple.media" -msgstr "grafik varlığı" +msgstr "grafik varlıkları" msgid "workspace.undo.entry.multiple.multiple" msgstr "nesneler" @@ -2469,6 +2868,9 @@ msgstr "nesneler" msgid "workspace.undo.entry.multiple.page" msgstr "sayfalar" +msgid "workspace.undo.entry.multiple.path" +msgstr "yollar" + msgid "workspace.undo.entry.multiple.rect" msgstr "dikdörtgenler" @@ -2478,6 +2880,9 @@ msgstr "şekiller" msgid "workspace.undo.entry.multiple.text" msgstr "metinler" +msgid "workspace.undo.entry.multiple.typography" +msgstr "tipografi varlıkları" + #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.entry.new" msgstr "Yeni %s" @@ -2501,7 +2906,7 @@ msgid "workspace.undo.entry.single.group" msgstr "grup" msgid "workspace.undo.entry.single.image" -msgstr "resim" +msgstr "görsel" msgid "workspace.undo.entry.single.media" msgstr "grafik varlığı" @@ -2512,6 +2917,9 @@ msgstr "nesne" msgid "workspace.undo.entry.single.page" msgstr "sayfa" +msgid "workspace.undo.entry.single.path" +msgstr "yol" + msgid "workspace.undo.entry.single.rect" msgstr "dikdörtgen" @@ -2521,6 +2929,9 @@ msgstr "şekil" msgid "workspace.undo.entry.single.text" msgstr "metin" +msgid "workspace.undo.entry.single.typography" +msgstr "tipografi varlığı" + #: src/app/main/ui/workspace/sidebar/history.cljs msgid "workspace.undo.entry.unknown" msgstr "%s üstündeki işlem" @@ -2531,12 +2942,15 @@ msgstr "Geçmiş" #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.dismiss" -msgstr "Gözardı et" +msgstr "Yoksay" #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.there-are-updates" -msgstr "Paylaşılmış kütüphanelerde güncellemeler mevcut" +msgstr "Paylaşılan kütüphaneler için güncellemeler var" #: src/app/main/data/workspace/libraries.cljs msgid "workspace.updates.update" -msgstr "Güncelle" \ No newline at end of file +msgstr "Güncelle" + +msgid "workspace.viewport.click-to-close-path" +msgstr "Yolu kapatmak için tıklayın" \ No newline at end of file diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po index 71d16f01c..2aa70e16e 100644 --- a/frontend/translations/zh_CN.po +++ b/frontend/translations/zh_CN.po @@ -2101,14 +2101,6 @@ msgstr "图层成组" msgid "workspace.options.layer-options.title.multiple" msgstr "已选中的图层" -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.navigate-to" -msgstr "导航到" - -#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs -msgid "workspace.options.none" -msgstr "无" - #: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.position" msgstr "位置" diff --git a/frontend/translations/zh_Hant.po b/frontend/translations/zh_Hant.po new file mode 100644 index 000000000..b1fc23594 --- /dev/null +++ b/frontend/translations/zh_Hant.po @@ -0,0 +1,6 @@ +msgid "" +msgstr "" +"X-Generator: Weblate\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock index ca1890d13..e525c2b5a 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2,12 +2,12 @@ # yarn lockfile v1 -"@babel/runtime-corejs3@^7.12.1": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.14.0.tgz#6bf5fbc0b961f8e3202888cb2cd0fb7a0a9a3f66" - integrity sha512-0R0HTZWHLk6G8jIk0FtoX+AatCtKnswS98VhXwGImFc759PJRp4Tru0PQYZofyijTFUr+gT8Mu7sgXVJLQ0ceg== +"@babel/runtime-corejs3@^7.14.9": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.15.4.tgz#403139af262b9a6e8f9ba04a6fdcebf8de692bf1" + integrity sha512-lWcAqKeB624/twtTc3w6w/2o9RqJPaNBhPGK6DKLSiwuVWC7WFkypWyNg+CpZoyJH0jVzv1uMtXZ/5/lQOLtCg== dependencies: - core-js-pure "^3.0.0" + core-js-pure "^3.16.0" regenerator-runtime "^0.13.4" "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7": @@ -45,6 +45,81 @@ normalize-path "^2.0.1" through2 "^2.0.3" +"@sentry/browser@^6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.12.0.tgz#970cd68fa117a1e1336fdb373e3b1fa76cd63e2d" + integrity sha512-wsJi1NLOmfwtPNYxEC50dpDcVY7sdYckzwfqz1/zHrede1mtxpqSw+7iP4bHADOJXuF+ObYYTHND0v38GSXznQ== + dependencies: + "@sentry/core" "6.12.0" + "@sentry/types" "6.12.0" + "@sentry/utils" "6.12.0" + tslib "^1.9.3" + +"@sentry/core@6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.12.0.tgz#bc7c5f0785b6a392d9ad47bd9b1fae3f5389996c" + integrity sha512-mU/zdjlzFHzdXDZCPZm8OeCw7c9xsbL49Mq0TrY0KJjLt4CJBkiq5SDTGfRsenBLgTedYhe5Z/J8Z+xVVq+MfQ== + dependencies: + "@sentry/hub" "6.12.0" + "@sentry/minimal" "6.12.0" + "@sentry/types" "6.12.0" + "@sentry/utils" "6.12.0" + tslib "^1.9.3" + +"@sentry/hub@6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.12.0.tgz#29e323ab6a95e178fb14fffb684aa0e09707197f" + integrity sha512-yR/UQVU+ukr42bSYpeqvb989SowIXlKBanU0cqLFDmv5LPCnaQB8PGeXwJAwWhQgx44PARhmB82S6Xor8gYNxg== + dependencies: + "@sentry/types" "6.12.0" + "@sentry/utils" "6.12.0" + tslib "^1.9.3" + +"@sentry/minimal@6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.12.0.tgz#cbe20e95056cedb9709d7d5b2119ef95206a9f8c" + integrity sha512-r3C54Q1KN+xIqUvcgX9DlcoWE7ezWvFk2pSu1Ojx9De81hVqR9u5T3sdSAP2Xma+um0zr6coOtDJG4WtYlOtsw== + dependencies: + "@sentry/hub" "6.12.0" + "@sentry/types" "6.12.0" + tslib "^1.9.3" + +"@sentry/tracing@^6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.12.0.tgz#a05c8985ee7fed7310b029b147d8f9f14f2a2e67" + integrity sha512-u10QHNknPBzbWSUUNMkvuH53sQd5NaBo6YdNPj4p5b7sE7445Sh0PwBpRbY3ZiUUiwyxV59fx9UQ4yVnPGxZQA== + dependencies: + "@sentry/hub" "6.12.0" + "@sentry/minimal" "6.12.0" + "@sentry/types" "6.12.0" + "@sentry/utils" "6.12.0" + tslib "^1.9.3" + +"@sentry/types@6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.12.0.tgz#b7395688a79403c6df8d8bb8d81deb8222519853" + integrity sha512-urtgLzE4EDMAYQHYdkgC0Ei9QvLajodK1ntg71bGn0Pm84QUpaqpPDfHRU+i6jLeteyC7kWwa5O5W1m/jrjGXA== + +"@sentry/utils@6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.12.0.tgz#3de261e8d11bdfdc7add64a3065d43517802e975" + integrity sha512-oRHQ7TH5TSsJqoP9Gqq25Jvn9LKexXfAh/OoKwjMhYCGKGhqpDNUIZVgl9DWsGw5A5N5xnQyLOxDfyRV5RshdA== + dependencies: + "@sentry/types" "6.12.0" + tslib "^1.9.3" + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + "@types/q@^1.5.1": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" @@ -70,6 +145,13 @@ ajv@^6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + ansi-colors@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" @@ -99,6 +181,11 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -106,7 +193,7 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -311,13 +398,13 @@ atob@^2.1.2: integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== autoprefixer@^10.2.4: - version "10.3.1" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.3.1.tgz#954214821d3aa06692406c6a0a9e9d401eafbed2" - integrity sha512-L8AmtKzdiRyYg7BUXJTzigmhbQRCXFKz6SA1Lqo0+AR2FBbQ4aTAPFSDlOutnFkjhiz8my4agGXog1xlMjPJ6A== + version "10.3.4" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.3.4.tgz#29efe5d19f51c281953178ddb5b84c5f1ca24c86" + integrity sha512-EKjKDXOq7ug+jagLzmnoTRpTT0q1KVzEJqrJd0hCBa7FiG0WbFOBCcJCy2QkW1OckpO3qgttA1aWjVbeIPAecw== dependencies: - browserslist "^4.16.6" - caniuse-lite "^1.0.30001243" - colorette "^1.2.2" + browserslist "^4.16.8" + caniuse-lite "^1.0.30001252" + colorette "^1.3.0" fraction.js "^4.1.1" normalize-range "^0.1.2" postcss-value-parser "^4.1.0" @@ -414,6 +501,20 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= +boxen@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -516,16 +617,16 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.16.6: - version "4.16.6" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" - integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== +browserslist@^4.16.8: + version "4.17.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.0.tgz#1fcd81ec75b41d6d4994fb0831b92ac18c01649c" + integrity sha512-g2BJ2a0nEYvEFQC208q8mVAhfNwpZ5Mu8BwgtCdZKO3qx98HChmeg448fPdUzld8aFmfLgVh7yymqV+q1lJZ5g== dependencies: - caniuse-lite "^1.0.30001219" - colorette "^1.2.2" - electron-to-chromium "^1.3.723" + caniuse-lite "^1.0.30001254" + colorette "^1.3.0" + electron-to-chromium "^1.3.830" escalade "^3.1.1" - node-releases "^1.1.71" + node-releases "^1.1.75" buffer-crc32@~0.2.3: version "0.2.13" @@ -538,9 +639,9 @@ buffer-equal@^1.0.0: integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== buffer-xor@^1.0.3: version "1.0.3" @@ -581,6 +682,19 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -589,25 +703,6 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= - camelcase@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" @@ -618,10 +713,15 @@ camelcase@^5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001243: - version "1.0.30001244" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001244.tgz#a6dc49ad5fa02d81d04373ec3f5ceabc3da06abf" - integrity sha512-Wb4UFZPkPoJoKKVfELPWytRzpemjP/s0pe22NriANru1NoI+5bGNxzKtk7edYL8rmCWTfQO8eRiF0pn1Dqzx7Q== +camelcase@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" + integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== + +caniuse-lite@^1.0.30001252, caniuse-lite@^1.0.30001254: + version "1.0.30001257" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz#150aaf649a48bee531104cfeda57f92ce587f6e5" + integrity sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA== caseless@~0.12.0: version "0.12.0" @@ -637,6 +737,14 @@ chalk@^2.4.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" @@ -645,7 +753,7 @@ chalk@^4.1.1: ansi-styles "^4.1.0" supports-color "^7.1.0" -"chokidar@>=3.0.0 <4.0.0": +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.2.2: version "3.5.2" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== @@ -679,6 +787,11 @@ chokidar@^2.0.0: optionalDependencies: fsevents "^1.2.7" +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -704,6 +817,11 @@ clean-css@^4.x: dependencies: source-map "~0.6.0" +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + cliui@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" @@ -727,6 +845,13 @@ clone-buffer@^1.0.0: resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= +clone-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= + dependencies: + mimic-response "^1.0.0" + clone-stats@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" @@ -827,10 +952,10 @@ color@3.0.x: color-convert "^1.9.1" color-string "^1.5.2" -colorette@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== +colorette@^1.2.2, colorette@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== colors@^1.2.1: version "1.4.0" @@ -897,6 +1022,18 @@ config-chain@^1.1.12: ini "^1.3.4" proto-list "~1.2.1" +configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + console-browserify@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" @@ -932,30 +1069,25 @@ copy-props@^2.0.1: each-props "^1.3.2" is-plain-object "^5.0.0" -core-js-pure@^3.0.0: - version "3.13.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.13.0.tgz#9d267fb47d1d7046cfbc05e7b67bb235b6735355" - integrity sha512-7VTvXbsMxROvzPAVczLgfizR8CyYnvWPrb1eGrtlZAJfjQWEHLofVfCKljLHdpazTfpaziRORwUH/kfGDKvpdA== +core-js-pure@^3.16.0: + version "3.17.3" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.17.3.tgz#98ea3587188ab7ef4695db6518eeb71aec42604a" + integrity sha512-YusrqwiOTTn8058JDa0cv9unbXdIiIgcgI9gXso0ey4WgkFLd3lYlV9rp9n7nDCsYxXsMDTjA4m1h3T348mdlQ== core-js@^3.6.4: version "3.13.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.13.0.tgz#58ca436bf01d6903aee3d364089868d0d89fe58d" integrity sha512-iWDbiyha1M5vFwPFmQnvRv+tJzGbFAm6XimJUT0NgHYW3xZEs1SkCAcasWSVFxpI2Xb/V1DDJckq3v90+bQnog== -core-util-is@1.0.2, core-util-is@~1.0.0: +core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -cosmiconfig@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== create-ecdh@^4.0.0: version "4.0.4" @@ -995,7 +1127,7 @@ cross-fetch@^3.0.4: dependencies: node-fetch "2.6.1" -cross-spawn@^6.0.0: +cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -1023,6 +1155,11 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + css-select-base-adapter@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" @@ -1111,9 +1248,9 @@ dashdash@^1.12.0: assert-plus "^1.0.0" date-fns@^2.22.1: - version "2.22.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.22.1.tgz#1e5af959831ebb1d82992bf67b765052d8f0efc4" - integrity sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg== + version "2.23.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.23.0.tgz#4e886c941659af0cf7b30fafdd1eaa37e88788a9" + integrity sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA== dateformat@^3.0.3: version "3.0.3" @@ -1136,7 +1273,7 @@ debug@3.1.0: dependencies: ms "2.0.0" -debug@3.X: +debug@3.X, debug@^3.2.6: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -1160,6 +1297,18 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= + dependencies: + mimic-response "^1.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + default-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f" @@ -1172,6 +1321,11 @@ default-resolution@^2.0.0: resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684" integrity sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ= +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -1277,6 +1431,13 @@ domutils@^1.7.0: dom-serializer "0" domelementtype "1" +dot-prop@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + draft-js@^0.11.7: version "0.11.7" resolved "https://registry.yarnpkg.com/draft-js/-/draft-js-0.11.7.tgz#be293aaa255c46d8a6647f3860aa4c178484a206" @@ -1286,6 +1447,11 @@ draft-js@^0.11.7: immutable "~3.7.4" object-assign "^4.1.1" +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + duplexify@^3.6.0: version "3.7.1" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" @@ -1322,10 +1488,10 @@ editorconfig@^0.15.3: semver "^5.6.0" sigmund "^1.0.1" -electron-to-chromium@^1.3.723: - version "1.3.774" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.774.tgz#4d6661a23119e35151646c9543b346bb3beca423" - integrity sha512-Fggh17Q1yyv1uMzq8Qn1Ci58P50qcRXMXd2MBcB9sxo6rJxjUutWcNw8uCm3gFWMdcblBO6mDT5HzX/RVRRECA== +electron-to-chromium@^1.3.830: + version "1.3.840" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.840.tgz#3f2a1df97015d9b1db5d86a4c6bd4cdb920adcbb" + integrity sha512-yRoUmTLDJnkIJx23xLY7GbSvnmDCq++NSuxHDQ0jiyDJ9YZBUGJcrdUqm+ZwZFzMbCciVzfem2N2AWiHJcWlbw== elliptic@^6.5.3: version "6.5.4" @@ -1340,6 +1506,11 @@ elliptic@^6.5.3: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + enabled@2.0.x: version "2.0.0" resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" @@ -1393,6 +1564,32 @@ es-abstract@^1.17.2, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.1" +es-abstract@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" + integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.1" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.1" + is-string "^1.0.7" + is-weakref "^1.0.1" + object-inspect "^1.11.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.1" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -1448,6 +1645,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -1808,7 +2010,7 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== @@ -1817,13 +2019,28 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" -get-stream@^4.0.0: +get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== dependencies: pump "^3.0.0" +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -1914,6 +2131,13 @@ glob@^7.1.1, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +global-dirs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" + integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA== + dependencies: + ini "2.0.0" + global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" @@ -1941,6 +2165,23 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" @@ -2008,13 +2249,13 @@ gulp-mustache@^5.0.0: through2 "^3.0.1" gulp-postcss@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/gulp-postcss/-/gulp-postcss-9.0.0.tgz#2ade18809ab475dae743a88bd6501af0b04ee54e" - integrity sha512-5mSQ9CK8salSagrXgrVyILfEMy6I5rUGPRiR9rVjgJV9m/rwdZYUhekMr+XxDlApfc5ZdEJ8gXNZrU/TsgT5dQ== + version "9.0.1" + resolved "https://registry.yarnpkg.com/gulp-postcss/-/gulp-postcss-9.0.1.tgz#d43caa2f2ce1018f889f7c1296faf82e9928b66f" + integrity sha512-9QUHam5JyXwGUxaaMvoFQVT44tohpEFpM8xBdPfdwTYGM0AItS1iTQz0MpsF8Jroh7GF5Jt2GVPaYgvy8qD2Fw== dependencies: fancy-log "^1.3.3" plugin-error "^1.0.1" - postcss-load-config "^2.1.1" + postcss-load-config "^3.0.0" vinyl-sourcemaps-apply "^0.2.1" gulp-rename@^2.0.0: @@ -2111,6 +2352,13 @@ has-symbols@^1.0.1, has-symbols@^1.0.2: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -2142,6 +2390,11 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -2180,9 +2433,9 @@ he@1.1.1: integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= highlight.js@^11.0.1: - version "11.1.0" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.1.0.tgz#0198f7326e64ddfbea5f76b00e84ab542cf24ae8" - integrity sha512-X9VVhYKHQPPuwffO8jk4bP/FVj+ibNCy3HxZZNDXFtJrq4O5FdcdCDRIkDis5MiMnjh7UwEdHgRZJcHFYdzDdA== + version "11.2.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.2.0.tgz#a7e3b8c1fdc4f0538b93b2dc2ddd53a40c6ab0f0" + integrity sha512-JOySjtOEcyG8s4MLR2MNbLUyaXqUunmSnL2kdV/KuGJOmHZuAR5xC54Ko7goAXBWNhf09Vy3B+U7vR62UZ/0iw== hmac-drbg@^1.0.1: version "1.0.1" @@ -2205,6 +2458,11 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -2231,6 +2489,11 @@ ieee754@^1.1.4: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -2241,27 +2504,29 @@ immutable@~3.7.4: resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b" integrity sha1-E7TTyxK++hVIKib+Gy665kAHHks= -import-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" - integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= +import-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-3.0.0.tgz#20845547718015126ea9b3676b7592fb8bd4cf92" + integrity sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg== dependencies: - import-from "^2.1.0" + import-from "^3.0.0" -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= +import-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-3.0.0.tgz#055cfec38cd5a27d8057ca51376d7d3bf0891966" + integrity sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ== dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" + resolve-from "^5.0.0" -import-from@^2.1.0: +import-lazy@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" - integrity sha1-M1238qev/VOqpHHUuAId7ja387E= - dependencies: - resolve-from "^3.0.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= inflight@^1.0.4: version "1.0.6" @@ -2286,11 +2551,25 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@^1.3.4: +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + +ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + interpret@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -2374,6 +2653,18 @@ is-callable@^1.1.4, is-callable@^1.2.3: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== +is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + is-core-module@^2.2.0: version "2.5.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.5.0.tgz#f754843617c70bfd29b7bd87327400cda5c18491" @@ -2418,11 +2709,6 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= - is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -2452,6 +2738,11 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-glob@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" @@ -2466,6 +2757,14 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-installed-globally@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + is-negated-glob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" @@ -2476,6 +2775,11 @@ is-negative-zero@^2.0.1: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== +is-npm@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8" + integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA== + is-number-object@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb" @@ -2498,6 +2802,16 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -2523,6 +2837,14 @@ is-regex@^1.1.3: call-bind "^1.0.2" has-symbols "^1.0.2" +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-relative@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" @@ -2530,6 +2852,11 @@ is-relative@^1.0.0: dependencies: is-unc-path "^1.0.0" +is-shared-array-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" + integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== + is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -2545,6 +2872,13 @@ is-string@^1.0.5, is-string@^1.0.6: resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f" integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w== +is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" @@ -2552,7 +2886,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typedarray@~1.0.0: +is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= @@ -2574,11 +2908,23 @@ is-valid-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= +is-weakref@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2" + integrity sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ== + dependencies: + call-bind "^1.0.0" + is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -2634,6 +2980,11 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= + json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -2677,9 +3028,9 @@ jsprim@^1.2.2: verror "1.10.0" jszip@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.6.0.tgz#839b72812e3f97819cc13ac4134ffced95dd6af9" - integrity sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ== + version "3.7.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9" + integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg== dependencies: lie "~3.3.0" pako "~1.0.2" @@ -2696,6 +3047,13 @@ kew@^0.7.0: resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" integrity sha1-edk9LTM2PW/dKXCzNdkUGtWR15s= +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -2740,6 +3098,13 @@ last-run@^1.1.0: default-resolution "^2.0.0" es6-weak-map "^2.0.1" +latest-version@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + lazystream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" @@ -2789,6 +3154,11 @@ liftoff@^3.1.0: rechoir "^0.6.2" resolve "^1.1.7" +lilconfig@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd" + integrity sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg== + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -2800,6 +3170,16 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -2936,6 +3316,16 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + lru-cache@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -2944,6 +3334,13 @@ lru-cache@^4.1.5: pseudomap "^1.0.2" yallist "^2.1.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + lru-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" @@ -2951,10 +3348,17 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" -luxon@^1.26.0: - version "1.28.0" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" - integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== +luxon@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.0.2.tgz#11f2cd4a11655fdf92e076b5782d7ede5bcdd133" + integrity sha512-ZRioYLCgRHrtTORaZX1mx+jtxKtKuI5ZDvHNAmqpUzGqSrR+tL4FVLn/CUGMA3h0+AKD1MAxGI5GnCqR5txNqg== + +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" make-iterator@^1.0.0: version "1.0.1" @@ -2987,10 +3391,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753" - integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA== +marked@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/marked/-/marked-3.0.4.tgz#b8a1539e5e05c6ea9e93f15c0bad1d54ce890406" + integrity sha512-jBo8AOayNaEcvBhNobg6/BLhdsK3NvnKWJg33MAAPbvTWiG4QBn9gpW1+7RssrKu4K1dKlN+0goVQwV41xEfOA== matchdep@^2.0.0: version "2.0.0" @@ -3044,6 +3448,11 @@ memoizee@0.4.X: next-tick "^1.1.0" timers-ext "^0.1.7" +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI= + micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -3088,6 +3497,11 @@ mimic-fn@^2.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-response@^1.0.0, mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -3110,7 +3524,7 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -3195,9 +3609,9 @@ nan@^2.12.1: integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== nanoid@^3.1.23: - version "3.1.23" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" - integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== + version "3.1.25" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152" + integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q== nanomatch@^1.2.9: version "1.2.13" @@ -3265,10 +3679,26 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -node-releases@^1.1.71: - version "1.1.73" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20" - integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg== +node-releases@^1.1.75: + version "1.1.75" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.75.tgz#6dd8c876b9897a1b8e5a02de26afa79bb54ebbfe" + integrity sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw== + +nodemon@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.13.tgz#67d40d3a4d5bd840aa785c56587269cfcf5d24aa" + integrity sha512-UMXMpsZsv1UXUttCn6gv8eQPhn6DR4BW+txnL3IN5IHqrCwcrT/yWHfL35UsClGXknTH79r5xbu+6J1zNHuSyA== + dependencies: + chokidar "^3.2.2" + debug "^3.2.6" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.7" + semver "^5.7.1" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.3" + update-notifier "^5.1.0" nopt@^5.0.0: version "5.0.0" @@ -3277,6 +3707,13 @@ nopt@^5.0.0: dependencies: abbrev "1" +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + normalize-package-data@^2.3.2: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -3304,6 +3741,11 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= +normalize-url@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== + now-and-later@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c" @@ -3311,6 +3753,21 @@ now-and-later@^2.0.0: dependencies: once "^1.3.2" +npm-run-all@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba" + integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ== + dependencies: + ansi-styles "^3.2.1" + chalk "^2.4.1" + cross-spawn "^6.0.5" + memorystream "^0.3.1" + minimatch "^3.0.4" + pidtree "^0.3.0" + read-pkg "^3.0.0" + shell-quote "^1.6.1" + string.prototype.padend "^3.0.0" + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -3354,6 +3811,11 @@ object-inspect@^1.10.3: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== +object-inspect@^1.11.0, object-inspect@^1.9.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" + integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== + object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -3477,6 +3939,11 @@ os-locale@^3.0.0: lcid "^2.0.0" mem "^4.0.0" +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -3511,6 +3978,16 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + pako@~1.0.2, pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -3624,6 +4101,13 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + pbkdf2@^3.0.3: version "3.1.2" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" @@ -3665,11 +4149,21 @@ picomatch@^2.0.4, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== +pidtree@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a" + integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -3705,13 +4199,14 @@ postcss-clean@^1.2.2: clean-css "^4.x" postcss "^6.x" -postcss-load-config@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a" - integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw== +postcss-load-config@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.0.tgz#d39c47091c4aec37f50272373a6a648ef5e97829" + integrity sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g== dependencies: - cosmiconfig "^5.0.0" - import-cwd "^2.0.0" + import-cwd "^3.0.0" + lilconfig "^2.0.3" + yaml "^1.10.2" postcss-value-parser@^4.1.0: version "4.1.0" @@ -3737,14 +4232,19 @@ postcss@^7.0.16: supports-color "^6.1.0" postcss@^8.3.5: - version "8.3.5" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.5.tgz#982216b113412bc20a86289e91eb994952a5b709" - integrity sha512-NxTuJocUhYGsMiMFHDUkmjSKT3EdH4/WbGF6GCi1NDGk+vbcUTun4fpbOqaPtD8IIsztA2ilZm2DhYCuyN58gA== + version "8.3.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea" + integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A== dependencies: colorette "^1.2.2" nanoid "^3.1.23" source-map-js "^0.6.2" +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= + pretty-hrtime@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -3801,6 +4301,11 @@ psl@^1.1.28: resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== +pstree.remy@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + public-encrypt@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" @@ -3853,6 +4358,13 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +pupa@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" + integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== + dependencies: + escape-goat "^2.0.0" + q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -3893,6 +4405,16 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" +rc@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + react-dom@~17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" @@ -3949,6 +4471,15 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + "readable-stream@2 || 3", readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -4000,9 +4531,9 @@ rechoir@^0.6.2: resolve "^1.1.6" regenerator-runtime@^0.13.4: - version "0.13.7" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" - integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" @@ -4012,6 +4543,20 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +registry-auth-token@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250" + integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw== + dependencies: + rc "^1.2.8" + +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" + remove-bom-buffer@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53" @@ -4114,10 +4659,10 @@ resolve-dir@^1.0.0, resolve-dir@^1.0.1: expand-tilde "^2.0.0" global-modules "^1.0.0" -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve-options@^1.1.0: version "1.1.0" @@ -4139,6 +4684,13 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.4.0: is-core-module "^2.2.0" path-parse "^1.0.6" +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= + dependencies: + lowercase-keys "^1.0.0" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -4189,9 +4741,9 @@ safe-regex@^1.1.0: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sass@^1.35.1: - version "1.35.2" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.35.2.tgz#b732314fcdaf7ef8d0f1698698adc378043cb821" - integrity sha512-jhO5KAR+AMxCEwIH3v+4zbB2WB0z67V1X0jbapfVwQQdjHZUGUyukpnoM6+iCMfsIUC016w9OPKQ5jrNOS9uXw== + version "1.41.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.41.0.tgz#f7b41dc00336a4c03429c37b9680b86758af61d4" + integrity sha512-wb8nT60cjo9ZZMcHzG7TzdbFtCAmHEKWrH+zAdScPb4ZxL64WQBnGdbp5nwlenW5wJPcHva1JWmVa0h6iqA5eg== dependencies: chokidar ">=3.0.0 <4.0.0" @@ -4208,6 +4760,13 @@ scheduler@^0.20.2: loose-envify "^1.1.0" object-assign "^4.1.1" +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== + dependencies: + semver "^6.3.0" + semver-greatest-satisfied-range@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz#13e8c2658ab9691cb0cd71093240280d36f77a5b" @@ -4215,11 +4774,23 @@ semver-greatest-satisfied-range@^1.1.0: dependencies: sver-compat "^1.5.0" -"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.4: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -4258,10 +4829,10 @@ shadow-cljs-jar@1.3.2: resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b" integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== -shadow-cljs@2.15.2: - version "2.15.2" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.15.2.tgz#195ad2cc45d3334920e629721f06c6d63802b1ac" - integrity sha512-WPlSMkGgbU5b2nrt+Y1A1TsPs5Rip/JvCxGG2t2Pvzo+pLJ+RcpkZgAxjNQNNA7VYWEh5Pqwyvq5KzQ+0LMsxw== +shadow-cljs@2.15.9: + version "2.15.9" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.15.9.tgz#cb256a9af12c3df1f0b2bbef344e365dab74b519" + integrity sha512-t2KrrMvJZtUFf2xIAL+OL76ahYHPT8VAKxhDic3kloTgOYfZpHRm/S/3C0iW982U3ZJdLUZxmBeluH4Q7564dg== dependencies: node-libs-browser "^2.2.1" readline-sync "^1.4.7" @@ -4282,6 +4853,11 @@ shebang-regex@^1.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= +shell-quote@^1.6.1: + version "1.7.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" + integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== + should-equal@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" @@ -4326,6 +4902,15 @@ should@^13.2.3: should-type-adaptors "^1.0.1" should-util "^1.0.0" +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + sigmund@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" @@ -4336,6 +4921,11 @@ signal-exit@^3.0.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +signal-exit@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f" + integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -4405,9 +4995,9 @@ source-map-support@^0.4.15: source-map "^0.5.6" source-map-support@^0.5.16: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + version "0.5.20" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" + integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -4556,11 +5146,29 @@ string-width@^2.0.0, string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.2: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string.prototype.codepointat@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc" integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg== +string.prototype.padend@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.3.tgz#997a6de12c92c7cb34dc8a201a6c53d9bd88a5f1" + integrity sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + string.prototype.trimend@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" @@ -4612,6 +5220,13 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-bom-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" @@ -4624,11 +5239,21 @@ strip-bom@^2.0.0: dependencies: is-utf8 "^0.2.0" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + supports-color@5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" @@ -4636,7 +5261,7 @@ supports-color@5.4.0: dependencies: has-flag "^3.0.0" -supports-color@^5.3.0, supports-color@^5.4.0: +supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -4797,6 +5422,11 @@ to-object-path@^0.3.0: dependencies: kind-of "^3.0.2" +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" @@ -4829,6 +5459,13 @@ to-through@^2.0.0: dependencies: through2 "^2.0.3" +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -4847,6 +5484,11 @@ triple-beam@^1.2.0, triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tslib@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" @@ -4869,6 +5511,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + type@^1.0.1: version "1.2.0" resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" @@ -4879,6 +5526,13 @@ type@^2.0.0: resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d" integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw== +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -4904,6 +5558,11 @@ unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= +undefsafe@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + undertaker-registry@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/undertaker-registry/-/undertaker-registry-1.0.1.tgz#5e4bda308e4a8a2ae584f9b9a4359a499825cc50" @@ -4943,6 +5602,13 @@ unique-stream@^2.0.2: json-stable-stringify-without-jsonify "^1.0.1" through2-filter "^3.0.0" +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + unquote@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" @@ -4961,6 +5627,26 @@ upath@^1.1.1: resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== +update-notifier@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9" + integrity sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw== + dependencies: + boxen "^5.0.0" + chalk "^4.1.0" + configstore "^5.0.1" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.4.0" + is-npm "^5.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.1.0" + pupa "^2.1.1" + semver "^7.3.4" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -4973,6 +5659,13 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= + dependencies: + prepend-http "^2.0.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -5137,6 +5830,13 @@ which@^1.2.10, which@^1.2.14, which@^1.2.9, which@^1.3.1: dependencies: isexe "^2.0.0" +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + winston-transport@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59" @@ -5168,16 +5868,40 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + ws@^7.4.6: version "7.5.0" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.0.tgz#0033bafea031fb9df041b2026fc72a571ca44691" integrity sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw== +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + xmldom@0.1.27: version "0.1.27" resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" @@ -5189,11 +5913,11 @@ xpath@^0.0.27: integrity sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ== xregexp@^5.0.1: - version "5.0.2" - resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-5.0.2.tgz#798aa7757836f39cdbdeeba3daf94d75f7a9dcc1" - integrity sha512-JPNfN40YMNSDxZrahMrmtNH1QqPJp0/qNeEJM2nnOlhcBdfCCjekPYFV2OnwKxwvpEYglH1RBotbpRRaEuCG8Q== + version "5.1.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-5.1.0.tgz#c87e7ae5ffa5fdc520f898a467dcba02b0d391e9" + integrity sha512-PynwUWtXnSZr8tpQlDPMZfPTyv78EYuA4oI959ukxcQ0a9O/lvndLVKy5wpImzzA26eMxpZmnAXJYiQA13AtWA== dependencies: - "@babel/runtime-corejs3" "^7.12.1" + "@babel/runtime-corejs3" "^7.14.9" xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" @@ -5215,6 +5939,16 @@ yallist@^2.1.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yargs-parser@^11.1.1: version "11.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" diff --git a/manage.sh b/manage.sh index 80d9c6a85..2298eb089 100755 --- a/manage.sh +++ b/manage.sh @@ -44,22 +44,39 @@ function pull-devenv-if-not-exists { function start-devenv { pull-devenv-if-not-exists $@; - docker-compose -p $DEVENV_PNAME -f docker/devenv/docker-compose.yaml up -d; + + # Check if the "backend-only" container is running. If it is, we need tot stop it first + if [[ ! $(docker ps -f "name=penpot-backend" -q) ]]; then + docker compose -p $DEVENV_PNAME --profile backend -f docker/devenv/docker-compose.yaml stop -t 2 backend; + fi + + docker compose -p $DEVENV_PNAME --profile full -f docker/devenv/docker-compose.yaml up -d; +} + +function start-backend { + pull-devenv-if-not-exists $@; + + # Check if the "devenv" container is running. If it is, we need tot stop it first because conflicts with the backend + if [[ ! $(docker ps -f "name=penpot-devenv-main" -q) ]]; then + docker compose -p $DEVENV_PNAME --profile full -f docker/devenv/docker-compose.yaml stop -t 2 main; + fi + + docker compose -p $DEVENV_PNAME --profile backend -f docker/devenv/docker-compose.yaml up -d; } function stop-devenv { - docker-compose -p $DEVENV_PNAME -f docker/devenv/docker-compose.yaml stop -t 2; + docker compose -p $DEVENV_PNAME --profile full --profile backend -f docker/devenv/docker-compose.yaml stop -t 2; } function drop-devenv { - docker-compose -p $DEVENV_PNAME -f docker/devenv/docker-compose.yaml down -t 2 -v; + docker compose -p $DEVENV_PNAME --profile full --profile backend -f docker/devenv/docker-compose.yaml down -t 2 -v; echo "Clean old development image $DEVENV_IMGNAME..." docker images $DEVENV_IMGNAME -q | awk '{print $3}' | xargs --no-run-if-empty docker rmi } function log-devenv { - docker-compose -p $DEVENV_PNAME -f docker/devenv/docker-compose.yaml logs -f --tail=50 + docker compose -p $DEVENV_PNAME -f docker/devenv/docker-compose.yaml logs -f --tail=50 } function run-devenv { @@ -70,6 +87,14 @@ function run-devenv { docker exec -ti penpot-devenv-main sudo -EH -u penpot /home/start-tmux.sh } +function run-backend { + if [[ ! $(docker ps -f "name=penpot-backend" -q) ]]; then + start-backend + fi + + docker exec -ti penpot-backend sudo -EH -u penpot /home/start-tmux-back.sh +} + function build { echo ">> build start: $1" local version=$(print-current-version); @@ -171,10 +196,12 @@ function usage { echo "Options:" echo "- pull-devenv Pulls docker development oriented image" echo "- build-devenv Build docker development oriented image" - echo "- start-devenv Start the development oriented docker-compose service." - echo "- stop-devenv Stops the development oriented docker-compose service." - echo "- drop-devenv Remove the development oriented docker-compose containers, volumes and clean images." + echo "- start-devenv Start the development oriented docker compose service." + echo "- stop-devenv Stops the development oriented docker compose service." + echo "- drop-devenv Remove the development oriented docker compose containers, volumes and clean images." echo "- run-devenv Attaches to the running devenv container and starts development environment" + echo "- start-backend Start the backend only service." + echo "- run-backend Starts a backend-only instance and attach tmux to it" echo " based on tmux (frontend at localhost:3449, backend at localhost:6060)." echo "" } @@ -196,9 +223,15 @@ case $1 in start-devenv) start-devenv ${@:2} ;; + start-backend) + start-backend ${@:2} + ;; run-devenv) run-devenv ${@:2} ;; + run-backend) + run-backend ${@:2} + ;; stop-devenv) stop-devenv ${@:2} ;; diff --git a/version.txt b/version.txt index 75997d5e7..27167ba29 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.8.2-alpha +1.9.0-alpha