diff --git a/CHANGES.md b/CHANGES.md index 28e18de988..65fe3356a9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,11 +4,45 @@ ## :rocket: Next ### :boom: Breaking changes + +- The initial project / data mechanism (not documented) has been + disabled. Is the mechanism used for creating initial project on user + signup. With the new onboarding approach, this subsystem is no + longer needed and is disabled. + ### :sparkles: New features + +- Enhance corner radius behavior [Taiga #2190](https://tree.taiga.io/project/penpot/issue/2190). +- Allow preserve scroll position in interactions [Taiga #2250](https://tree.taiga.io/project/penpot/us/2250). +- Add new onboarding modals. + ### :bug: Bugs fixed + +- Fix problem with exporting before the document is saved [Taiga #2189](https://tree.taiga.io/project/penpot/issue/2189). +- Fix undo stacking when changing color from color-picker [Taiga #2191](https://tree.taiga.io/project/penpot/issue/2191). +- Fix pages dropdown in viewer [Taiga #2087](https://tree.taiga.io/project/penpot/issue/2087). +- Fix problem when exporting texts with gradients or opacity [Taiga #2200](https://tree.taiga.io/project/penpot/issue/2200). +- Fix problem with view mode comments [Taiga #2226](https://tree.taiga.io/project/penpot/issue/2226). +- Disallow to create a component when already has one [Taiga #2237](https://tree.taiga.io/project/penpot/issue/2237). +- Add ellipsis in long labels for input fields [Taiga #2224](https://tree.taiga.io/project/penpot/issue/2224) +- Fix problem with text rendering on export [Taiga #2223](https://tree.taiga.io/project/penpot/issue/2223) +- Fix problem when flattening booleans losing styles [Taiga #2217](https://tree.taiga.io/project/penpot/issue/2217) +- Add shortcuts to boolean icons popups [Taiga #2220](https://tree.taiga.io/project/penpot/issue/2220) +- Fix a worker error when transforming a rectangle into path +- Fix max/min values for opacity fields [Taiga #2183](https://tree.taiga.io/project/penpot/issue/2183) +- Fix viewer comment position when zoom applied [Taiga #2240](https://tree.taiga.io/project/penpot/issue/2240) +- Remove change style on hover for options [Taiga #2172](https://tree.taiga.io/project/penpot/issue/2172) +- Fix problem in viewer with dropdowns when comments active [#1303](https://github.com/penpot/penpot/issues/1303) +- Add placeholder to create shareable link +- Fix project files count not refreshing correctly after import [Taiga #2216](https://tree.taiga.io/project/penpot/issue/2216) +- Remove button after import process finish [Taiga #2215](https://tree.taiga.io/project/penpot/issue/2215) + ### :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.9.0-alpha diff --git a/backend/deps.edn b/backend/deps.edn index 45f6cbb6df..b7f3295baa 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -68,10 +68,6 @@ mockery/mockery {:mvn/version "RELEASE"}} :extra-paths ["test" "dev"]} - :fn-fixtures - {:exec-fn app.cli.fixtures/run - :args {}} - :kaocha {:extra-deps {lambdaisland/kaocha {:mvn/version "1.0.887"}} :main-opts ["-m" "kaocha.runner"]} diff --git a/backend/scripts/start-dev b/backend/scripts/start-dev index 88cc5c0c0e..87f1a78a78 100755 --- a/backend/scripts/start-dev +++ b/backend/scripts/start-dev @@ -1,15 +1,9 @@ -#!/bin/sh +#!/usr/bin/env bash -export PENPOT_ASSERTS_ENABLED=true +export PENPOT_FLAGS="$PENPOT_FLAGS enable-asserts" set -ex -if [ ! -e ~/.fixtures-loaded ]; then - echo "Loading fixtures..." - clojure -Adev -X:fn-fixtures - touch ~/.fixtures-loaded -fi - if [ "$1" = "--watch" ]; then echo "Start Watch..." @@ -27,6 +21,3 @@ if [ "$1" = "--watch" ]; then 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 deleted file mode 100644 index 4128c99544..0000000000 --- a/backend/src/app/cli/fixtures.clj +++ /dev/null @@ -1,258 +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.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] - [buddy.hashers :as hashers] - [integrant.core :as ig])) - -(defn- mk-uuid - [prefix & args] - (uuid/namespaced uuid/zero (apply str prefix (interpose "-" args)))) - -;; --- Profiles creation - -(def password (hashers/derive "123123")) - -(def preset-small - {:num-teams 5 - :num-profiles 5 - :num-profiles-per-team 5 - :num-projects-per-team 5 - :num-files-per-project 5 - :num-draft-files-per-profile 10}) - -(defn- rng-ids - [rng n max] - (let [stream (->> (.longs rng 0 max) - (.iterator) - (iterator-seq))] - (reduce (fn [acc item] - (if (= (count acc) n) - (reduced acc) - (conj acc item))) - #{} - stream))) - -(defn- rng-vec - [rng vdata n] - (let [ids (rng-ids rng n (count vdata))] - (mapv #(nth vdata %) ids))) - -(defn- rng-nth - [rng vdata] - (let [stream (->> (.longs rng 0 (count vdata)) - (.iterator) - (iterator-seq))] - (nth vdata (first stream)))) - -(defn- collect - [f items] - (reduce #(conj %1 (f %2)) [] items)) - -(defn- register-profile - [conn params] - (->> (#'profile/create-profile conn params) - (#'profile/create-profile-relations conn))) - -(defn impl-run - [pool opts] - (let [rng (java.util.Random. 1)] - (letfn [(create-profile [conn index] - (let [id (mk-uuid "profile" index) - _ (l/info :action "create profile" - :index index - :id id) - - prof (register-profile conn - {:id id - :fullname (str "Profile " index) - :password "123123" - :is-demo true - :email (str "profile" index "@example.com")}) - team-id (:default-team-id prof) - owner-id id] - (let [project-ids (collect (partial create-project conn team-id owner-id) - (range (:num-projects-per-team opts)))] - (run! (partial create-files conn owner-id) project-ids)) - prof)) - - (create-profiles [conn] - (l/info :action "create profiles") - (collect (partial create-profile conn) - (range (:num-profiles opts)))) - - (create-team [conn index] - (let [id (mk-uuid "team" index) - name (str "Team" index)] - (l/info :action "create team" - :index index - :id id) - (db/insert! conn :team {:id id - :name name}) - id)) - - (create-teams [conn] - (l/info :action "create teams") - (collect (partial create-team conn) - (range (:num-teams opts)))) - - (create-file [conn owner-id project-id index] - (let [id (mk-uuid "file" project-id index) - name (str "file" index) - data (cp/make-file-data id)] - (l/info :action "create file" - :index index - :id id) - (db/insert! conn :file - {:id id - :data (blob/encode data) - :project-id project-id - :name name}) - (db/insert! conn :file-profile-rel - {:file-id id - :profile-id owner-id - :is-owner true - :is-admin true - :can-edit true}) - id)) - - (create-files [conn owner-id project-id] - (l/info :action "create files") - (run! (partial create-file conn owner-id project-id) - (range (:num-files-per-project opts)))) - - (create-project [conn team-id owner-id index] - (let [id (if index - (mk-uuid "project" team-id index) - (mk-uuid "project" team-id)) - name (if index - (str "project " index) - "Drafts") - is-default (nil? index)] - (l/info :action "create project" - :index index - :id id) - (db/insert! conn :project - {:id id - :team-id team-id - :is-default is-default - :name name}) - (db/insert! conn :project-profile-rel - {:project-id id - :profile-id owner-id - :is-owner true - :is-admin true - :can-edit true}) - id)) - - (create-projects [conn team-id profile-ids] - (l/info :action "create projects") - (let [owner-id (rng-nth rng profile-ids) - project-ids (conj - (collect (partial create-project conn team-id owner-id) - (range (:num-projects-per-team opts))) - (create-project conn team-id owner-id nil))] - (run! (partial create-files conn owner-id) project-ids))) - - (assign-profile-to-team [conn team-id owner? profile-id] - (db/insert! conn :team-profile-rel - {:team-id team-id - :profile-id profile-id - :is-owner owner? - :is-admin true - :can-edit true})) - - (setup-team [conn team-id profile-ids] - (l/info :action "setup team" - :team-id team-id - :profile-ids (pr-str profile-ids)) - (assign-profile-to-team conn team-id true (first profile-ids)) - (run! (partial assign-profile-to-team conn team-id false) - (rest profile-ids)) - (create-projects conn team-id profile-ids)) - - (assign-teams-and-profiles [conn teams profiles] - (l/info :action "assign teams and profiles") - (loop [team-id (first teams) - teams (rest teams)] - (when-not (nil? team-id) - (let [n-profiles-team (:num-profiles-per-team opts) - selected-profiles (rng-vec rng profiles n-profiles-team)] - (setup-team conn team-id selected-profiles) - (recur (first teams) - (rest teams)))))) - - (create-draft-file [conn owner index] - (let [owner-id (:id owner) - id (mk-uuid "file" "draft" owner-id index) - name (str "file" index) - project-id (:default-project-id owner) - data (cp/make-file-data id)] - - (l/info :action "create draft file" - :index index - :id id) - (db/insert! conn :file - {:id id - :data (blob/encode data) - :project-id project-id - :name name}) - (db/insert! conn :file-profile-rel - {:file-id id - :profile-id owner-id - :is-owner true - :is-admin true - :can-edit true}) - id)) - - (create-draft-files [conn profile] - (run! (partial create-draft-file conn profile) - (range (:num-draft-files-per-profile opts)))) - ] - (db/with-atomic [conn pool] - (let [profiles (create-profiles conn) - teams (create-teams conn)] - (assign-teams-and-profiles conn teams (map :id profiles)) - (run! (partial create-draft-files conn) profiles)))))) - -(defn run-in-system - [system preset] - (let [pool (:app.db/pool system) - preset (if (map? preset) - preset - (case preset - (nil "small" :small) preset-small - ;; "medium" preset-medium - ;; "big" preset-big - preset-small))] - (impl-run pool preset))) - -(defn run - [{:keys [preset] :or {preset :small}}] - (let [config (select-keys main/system-config - [:app.db/pool - :app.telemetry/migrations - :app.migrations/migrations - :app.migrations/all - :app.metrics/metrics]) - _ (ig/load-namespaces config) - system (-> (ig/prep config) - (ig/init))] - (try - (run-in-system system preset) - (catch Exception e - (l/error :hint "unhandled exception" :cause e)) - (finally - (ig/halt! system))))) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 2c1d77aba4..fb86129309 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -85,7 +85,7 @@ ;; a server prop key where initial project is stored. :initial-project-skey "initial-project"}) -(s/def ::flags ::us/words) +(s/def ::flags ::us/set-of-keywords) ;; DEPRECATED PROPERTIES: should be removed in 1.10 (s/def ::registration-enabled ::us/boolean) @@ -268,10 +268,16 @@ ::telemetry-with-taiga ::tenant])) +(def default-flags + [:enable-backend-asserts + :enable-backend-api-doc + :enable-secure-session-cookies]) + (defn- parse-flags [config] - (-> (:flags config) - (flags/parse flags/default))) + (flags/parse flags/default + default-flags + (:flags config))) (defn read-env [prefix] diff --git a/backend/src/app/http/doc.clj b/backend/src/app/http/doc.clj index 13a6075cce..29796a1170 100644 --- a/backend/src/app/http/doc.clj +++ b/backend/src/app/http/doc.clj @@ -45,7 +45,7 @@ (defn handler [rpc] (let [context (prepare-context rpc)] - (if (contains? cf/flags :api-doc) + (if (contains? cf/flags :backend-api-doc) (fn [_] {:status 200 :body (-> (io/resource "api-doc.tmpl") diff --git a/backend/src/app/http/oauth.clj b/backend/src/app/http/oauth.clj index 64b1c70367..faa2c247e5 100644 --- a/backend/src/app/http/oauth.clj +++ b/backend/src/app/http/oauth.clj @@ -203,6 +203,7 @@ (sxf request))) (let [info (assoc info :iss :prepared-register + :is-active true :exp (dt/in-future {:hours 48})) token (tokens :generate info) params (d/without-nils diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index 6b9a566fd9..462e86c625 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -53,12 +53,13 @@ (defn- add-cookies [response {:keys [id] :as session}] - (let [cors? (contains? cfg/flags :cors)] + (let [cors? (contains? cfg/flags :cors) + secure? (contains? cfg/flags :secure-session-cookies)] (assoc response :cookies {cookie-name {:path "/" :http-only true :value id :same-site (if cors? :none :strict) - :secure true}}))) + :secure secure?}}))) (defn- clear-cookies [response] diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 724b527aa6..13c9089cce 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -14,6 +14,7 @@ [app.loggers.audit :as audit] [app.metrics :as mtx] [app.rlimits :as rlm] + [app.util.retry :as retry] [app.util.services :as sv] [clojure.spec.alpha :as s] [cuerdas.core :as str] @@ -92,6 +93,7 @@ (defn- wrap-impl [{:keys [audit] :as cfg} f mdata] (let [f (wrap-with-rlimits cfg f mdata) + f (retry/wrap-retry cfg f mdata) f (wrap-with-metrics cfg f mdata) spec (or (::sv/spec mdata) (s/spec any?)) auth? (:auth mdata true)] @@ -99,7 +101,6 @@ (l/trace :action "register" :name (::sv/name mdata)) (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)))) diff --git a/backend/src/app/rpc/mutations/comments.clj b/backend/src/app/rpc/mutations/comments.clj index 033c31ce25..3a6abab4b7 100644 --- a/backend/src/app/rpc/mutations/comments.clj +++ b/backend/src/app/rpc/mutations/comments.clj @@ -12,6 +12,9 @@ [app.rpc.queries.comments :as comments] [app.rpc.queries.files :as files] [app.util.blob :as blob] + #_:clj-kondo/ignore + [app.util.retry :as retry] + [app.util.services :as sv] [app.util.time :as dt] [clojure.spec.alpha :as s])) @@ -32,6 +35,9 @@ (s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id])) (sv/defmethod ::create-comment-thread + {::retry/enabled true + ::retry/max-retries 3 + ::retry/matches retry/conflict-db-insert?} [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] (files/check-read-permissions! conn profile-id file-id) @@ -43,7 +49,7 @@ res (db/exec-one! conn [sql file-id])] (:next-seqn res))) -(defn- create-comment-thread* +(defn- create-comment-thread [conn {:keys [profile-id file-id page-id position content] :as params}] (let [seqn (retrieve-next-seqn conn file-id) now (dt/now) @@ -78,24 +84,6 @@ (select-keys thread [:id :file-id :page-id]))) -(defn- create-comment-thread - [conn params] - (loop [sp (db/savepoint conn) - rc 0] - (let [res (ex/try (create-comment-thread* conn params))] - (cond - (and (instance? Throwable res) - (< rc 3)) - (do - (db/rollback! conn sp) - (recur (db/savepoint conn) - (inc rc))) - - (instance? Throwable res) - (throw res) - - :else res)))) - (defn- retrieve-page-name [conn {:keys [file-id page-id]}] (let [{:keys [data]} (db/get-by-id conn :file file-id) diff --git a/backend/src/app/rpc/mutations/demo.clj b/backend/src/app/rpc/mutations/demo.clj index cca26dbb48..7b069e86d7 100644 --- a/backend/src/app/rpc/mutations/demo.clj +++ b/backend/src/app/rpc/mutations/demo.clj @@ -34,7 +34,7 @@ params {:id id :email email :fullname fullname - :is-demo true + :is-active true :deleted-at (dt/in-future cf/deletion-delay) :password password :props {:onboarding-viewed true}}] @@ -46,8 +46,7 @@ (db/with-atomic [conn pool] (->> (#'profile/create-profile conn params) - (#'profile/create-profile-relations conn) - (sid/load-initial-project! conn)) + (#'profile/create-profile-relations conn)) (with-meta {:email email :password password} diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 906ec83add..45c2bb5bd6 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -16,10 +16,8 @@ [app.loggers.audit :as audit] [app.media :as media] [app.metrics :as mtx] - [app.rpc.mutations.projects :as projects] [app.rpc.mutations.teams :as teams] [app.rpc.queries.profile :as profile] - [app.setup.initial-data :as sid] [app.storage :as sto] [app.util.services :as sv] [app.util.time :as dt] @@ -126,9 +124,7 @@ ;; --- MUTATION: Register Profile -(s/def ::accept-terms-and-privacy ::us/boolean) (s/def ::token ::us/not-empty-string) - (s/def ::register-profile (s/keys :req-un [::token ::fullname])) @@ -148,16 +144,17 @@ (defn register-profile [{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}] - (let [claims (tokens :verify {:token token :iss :prepared-register}) - params (merge params claims)] + (let [claims (tokens :verify {:token token :iss :prepared-register}) + params (merge params claims)] + (check-profile-existence! conn params) - (let [profile (->> params - (create-profile conn) - (create-profile-relations conn) - (decode-profile-row))] - - (sid/load-initial-project! conn profile) + (let [is-active (or (:is-active params) + (contains? cf/flags :insecure-register)) + profile (->> (assoc params :is-active is-active) + (create-profile conn) + (create-profile-relations conn) + (decode-profile-row))] (cond ;; If invitation token comes in params, this is because the ;; user comes from team-invitation process; in this case, @@ -187,6 +184,15 @@ ::audit/props (audit/profile->props profile) ::audit/profile-id (:id profile)}) + ;; If the `:enable-insecure-register` flag is set, we proceed + ;; to sign in the user directly, without email verification. + (true? is-active) + (with-meta (profile/strip-private-attrs profile) + {:transform-response ((:create session) (:id profile)) + :before-complete (annotate-profile-register metrics) + ::audit/props (audit/profile->props profile) + ::audit/profile-id (:id profile)}) + ;; In all other cases, send a verification email. :else (let [vtoken (tokens :generate @@ -231,7 +237,7 @@ backend (:backend params "penpot") is-demo (:is-demo params false) is-muted (:is-muted params false) - is-active (:is-active params (or (not= "penpot" backend) is-demo)) + is-active (:is-active params false) email (str/lower (:email params)) params {:id id @@ -256,28 +262,15 @@ :code :email-already-exists :cause e))))))) - (defn create-profile-relations [conn profile] - (let [team (teams/create-team conn {:profile-id (:id profile) - :name "Default" - :is-default true}) - project (projects/create-project conn {:profile-id (:id profile) - :team-id (:id team) - :name "Drafts" - :is-default true}) - params {:team-id (:id team) - :profile-id (:id profile) - :project-id (:id project) - :role :owner}] - - (teams/create-team-role conn params) - (projects/create-project-role conn params) - + (let [team (teams/create-team conn {:profile-id (:id profile) + :name "Default" + :is-default true})] (-> profile (profile/strip-private-attrs) (assoc :default-team-id (:id team)) - (assoc :default-project-id (:id project))))) + (assoc :default-project-id (:default-project-id team))))) ;; --- MUTATION: Login diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj index 625a13af60..0f44afb171 100644 --- a/backend/src/app/rpc/mutations/teams.clj +++ b/backend/src/app/rpc/mutations/teams.clj @@ -32,6 +32,7 @@ ;; --- Mutation: Create Team (declare create-team) +(declare create-team-entry) (declare create-team-role) (declare create-team-default-project) @@ -42,15 +43,21 @@ (sv/defmethod ::create-team [{:keys [pool] :as cfg} params] (db/with-atomic [conn pool] - (let [team (create-team conn params) - params (assoc params - :team-id (:id team) - :role :owner)] - (create-team-role conn params) - (create-team-default-project conn params) - team))) + (create-team conn params))) (defn create-team + "This is a complete team creation process, it creates the team + object and all related objects (default role and default project)." + [conn params] + (let [team (create-team-entry conn params) + params (assoc params + :team-id (:id team) + :role :owner) + project (create-team-default-project conn params)] + (create-team-role conn params) + (assoc team :default-project-id (:id project)))) + +(defn- create-team-entry [conn {:keys [id name is-default] :as params}] (let [id (or id (uuid/next)) is-default (if (boolean? is-default) is-default false)] @@ -59,23 +66,24 @@ :name name :is-default is-default}))) -(defn create-team-role +(defn- create-team-role [conn {:keys [team-id profile-id role] :as params}] (let [params {:team-id team-id :profile-id profile-id}] (->> (perms/assign-role-flags params role) (db/insert! conn :team-profile-rel)))) -(defn create-team-default-project +(defn- create-team-default-project [conn {:keys [team-id profile-id] :as params}] (let [project {:id (uuid/next) :team-id team-id :name "Drafts" - :is-default true}] - (projects/create-project conn project) + :is-default true} + project (projects/create-project conn project)] (projects/create-project-role conn {:project-id (:id project) :profile-id profile-id - :role :owner}))) + :role :owner}) + project)) ;; --- Mutation: Update Team @@ -293,28 +301,18 @@ ;; --- Mutation: Invite Member +(declare create-team-invitation) + (s/def ::email ::us/email) (s/def ::invite-team-member (s/keys :req-un [::profile-id ::team-id ::email ::role])) (sv/defmethod ::invite-team-member - [{:keys [pool tokens] :as cfg} {:keys [profile-id team-id email role] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}] (db/with-atomic [conn pool] (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) - itoken (tokens :generate - {:iss :team-invitation - :exp (dt/in-future "48h") - :profile-id (:id profile) - :role role - :team-id team-id - :member-email (:email member email) - :member-id (:id member)}) - ptoken (tokens :generate-predefined - {:iss :profile-identity - :profile-id (:id profile)})] + team (db/get-by-id conn :team team-id)] (when-not (:is-admin perms) (ex/raise :type :validation @@ -326,24 +324,71 @@ :code :profile-is-muted :hint "looks like the profile has reported repeatedly as spam or has permanent bounces")) - (when (and member (not (eml/allow-send-emails? conn member))) - (ex/raise :type :validation - :code :member-is-muted - :hint "looks like the profile has reported repeatedly as spam or has permanent bounces")) - - ;; Secondly check if the invited member email is part of the - ;; global spam/bounce report. - (when (eml/has-bounce-reports? conn email) - (ex/raise :type :validation - :code :email-has-permanent-bounces - :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) - - (eml/send! {::eml/conn conn - ::eml/factory eml/invite-to-team - :public-uri (:public-uri cfg) - :to email - :invited-by (:fullname profile) - :team (:name team) - :token itoken - :extra-data ptoken}) + (create-team-invitation + (assoc cfg + :email email + :conn conn + :team team + :profile profile + :role role)) nil))) + +(defn- create-team-invitation + [{:keys [conn tokens team profile role email] :as cfg}] + (let [member (profile/retrieve-profile-data-by-email conn email) + itoken (tokens :generate + {:iss :team-invitation + :exp (dt/in-future "48h") + :profile-id (:id profile) + :role role + :team-id (:id team) + :member-email (:email member email) + :member-id (:id member)}) + ptoken (tokens :generate-predefined + {:iss :profile-identity + :profile-id (:id profile)})] + + (when (and member (not (eml/allow-send-emails? conn member))) + (ex/raise :type :validation + :code :member-is-muted + :hint "looks like the profile has reported repeatedly as spam or has permanent bounces")) + + ;; Secondly check if the invited member email is part of the + ;; global spam/bounce report. + (when (eml/has-bounce-reports? conn email) + (ex/raise :type :validation + :code :email-has-permanent-bounces + :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) + + (eml/send! {::eml/conn conn + ::eml/factory eml/invite-to-team + :public-uri (:public-uri cfg) + :to email + :invited-by (:fullname profile) + :team (:name team) + :token itoken + :extra-data ptoken}))) + + +;; --- Mutation: Create Team & Invite Members + +(s/def ::emails ::us/set-of-emails) +(s/def ::create-team-and-invite-members + (s/and ::create-team (s/keys :req-un [::emails ::role]))) + +(sv/defmethod ::create-team-and-invite-members + [{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}] + (db/with-atomic [conn pool] + (let [team (create-team conn params) + profile (db/get-by-id conn :profile profile-id)] + + ;; Create invitations for all provided emails. + (doseq [email emails] + (create-team-invitation + (assoc cfg + :conn conn + :team team + :profile profile + :email email + :role role))) + team))) diff --git a/backend/src/app/rpc/mutations/verify_token.clj b/backend/src/app/rpc/mutations/verify_token.clj index 1fa32b81dc..97458b5ca9 100644 --- a/backend/src/app/rpc/mutations/verify_token.clj +++ b/backend/src/app/rpc/mutations/verify_token.clj @@ -34,10 +34,15 @@ (when (profile/retrieve-profile-data-by-email conn email) (ex/raise :type :validation :code :email-already-exists)) + (db/update! conn :profile {:email email} {:id profile-id}) - claims) + + (with-meta claims + {::audit/name "update-profile-email" + ::audit/props {:email email} + ::audit/profile-id profile-id})) (defn- annotate-profile-activation "A helper for properly increase the profile-activation metric once the diff --git a/backend/src/app/rpc/queries/profile.clj b/backend/src/app/rpc/queries/profile.clj index 6843a07191..b68c6a5f17 100644 --- a/backend/src/app/rpc/queries/profile.clj +++ b/backend/src/app/rpc/queries/profile.clj @@ -87,13 +87,9 @@ (defn retrieve-profile [conn id] - (let [profile (some->> (retrieve-profile-data conn id) - (strip-private-attrs) - (populate-additional-data conn))] - (when (nil? profile) - (ex/raise :type :not-found - :hint "Object doest not exists.")) - + (let [profile (->> (retrieve-profile-data conn id) + (strip-private-attrs) + (populate-additional-data conn))] (update profile :props filter-profile-props))) (def ^:private sql:profile-by-email diff --git a/backend/src/app/rpc/queries/projects.clj b/backend/src/app/rpc/queries/projects.clj index 47886784a5..1e92869e45 100644 --- a/backend/src/app/rpc/queries/projects.clj +++ b/backend/src/app/rpc/queries/projects.clj @@ -79,12 +79,14 @@ where f.project_id = p.id and deleted_at is null) as count from project as p + inner join team as t on (t.id = p.team_id) left join team_project_profile_rel as tpp on (tpp.project_id = p.id and tpp.team_id = p.team_id and tpp.profile_id = ?) where p.team_id = ? and p.deleted_at is null + and t.deleted_at is null order by p.modified_at desc") (defn retrieve-projects @@ -108,26 +110,26 @@ (def sql:all-projects "select p1.*, t.name as team_name, t.is_default as is_default_team from project as p1 - inner join team as t - on t.id = p1.team_id + inner join team as t on (t.id = p1.team_id) where t.id in (select team_id from team_profile_rel as tpr where tpr.profile_id = ? and (tpr.can_edit = true or tpr.is_owner = true or tpr.is_admin = true)) + and t.deleted_at is null and p1.deleted_at is null union select p2.*, t.name as team_name, t.is_default as is_default_team from project as p2 - inner join team as t - on t.id = p2.team_id + inner join team as t on (t.id = p2.team_id) where p2.id in (select project_id from project_profile_rel as ppr where ppr.profile_id = ? and (ppr.can_edit = true or ppr.is_owner = true or ppr.is_admin = true)) + and t.deleted_at is null and p2.deleted_at is null order by team_name, name;") diff --git a/backend/src/app/util/retry.clj b/backend/src/app/util/retry.clj new file mode 100644 index 0000000000..d0bed166db --- /dev/null +++ b/backend/src/app/util/retry.clj @@ -0,0 +1,43 @@ +;; 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.retry + "A fault tolerance helpers. Allow retry some operations that we know + we can retry." + (:require + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.util.async :as aa] + [app.util.services :as sv])) + +(defn conflict-db-insert? + "Check if exception matches a insertion conflict on postgresql." + [e] + (and (instance? org.postgresql.util.PSQLException e) + (= "23505" (.getSQLState e)))) + +(defn wrap-retry + [_ f {:keys [::max-retries ::matches ::sv/name] + :or {max-retries 3 + matches (constantly false)} + :as mdata}] + (when (::enabled mdata) + (l/debug :hint "wrapping retry" :name name)) + (if (::enabled mdata) + (fn [cfg params] + (loop [retry 1] + (when (> retry 1) + (l/debug :hint "retrying controlled function" :retry retry :name name)) + (let [res (ex/try (f cfg params))] + (if (ex/exception? res) + (if (and (matches res) (< retry max-retries)) + (do + (aa/thread-sleep (* 100 retry)) + (recur (inc retry))) + (throw res)) + res)))) + f)) + diff --git a/backend/test/app/services_projects_test.clj b/backend/test/app/services_projects_test.clj index 5f23577f35..a59991e38c 100644 --- a/backend/test/app/services_projects_test.clj +++ b/backend/test/app/services_projects_test.clj @@ -43,7 +43,7 @@ (t/is (nil? (:error out))) (let [result (:result out)] - (t/is (= 1 (count result))) + (t/is (= 2 (count result))) (t/is project-id (get-in result [0 :id])) (t/is (= "test project" (get-in result [0 :name]))))) @@ -55,15 +55,15 @@ (t/is (nil? (:error out))) (let [result (:result out)] - (t/is (= 2 (count result))) + (t/is (= 3 (count result))) (t/is (not= project-id (get-in result [0 :id]))) (t/is (= "Drafts" (get-in result [0 :name]))) (t/is (= "Default" (get-in result [0 :team-name]))) (t/is (= true (get-in result [0 :is-default-team]))) - (t/is project-id (get-in result [1 :id])) - (t/is (= "test project" (get-in result [1 :name]))) - (t/is (= "team1" (get-in result [1 :team-name]))) - (t/is (= false (get-in result [1 :is-default-team]))))) + (t/is project-id (get-in result [2 :id])) + (t/is (= "test project" (get-in result [2 :name]))) + (t/is (= "team1" (get-in result [2 :team-name]))) + (t/is (= false (get-in result [2 :is-default-team]))))) ;; rename project (let [data {::th/type :rename-project @@ -95,7 +95,7 @@ (t/is (nil? (:error out))) (t/is (nil? (:result out)))) - ;; query a list of projects after delete" + ;; query a list of projects after delete (let [data {::th/type :projects :team-id (:id team) :profile-id (:id profile)} @@ -103,7 +103,7 @@ ;; (th/print-result! out) (t/is (nil? (:error out))) (let [result (:result out)] - (t/is (= 0 (count result))))) + (t/is (= 1 (count result))))) )) (t/deftest permissions-checks-create-project diff --git a/backend/test/app/services_teams_test.clj b/backend/test/app/services_teams_test.clj index 6dd7470da4..6e2aaeea7b 100644 --- a/backend/test/app/services_teams_test.clj +++ b/backend/test/app/services_teams_test.clj @@ -130,7 +130,7 @@ (let [result (task {:max-age (dt/duration {:minutes 1})})] (t/is (nil? result))) - ;; query the list of projects of a after hard deletion + ;; query the list of projects after hard deletion (let [data {::th/type :projects :team-id (:id team) :profile-id (:id profile1)} diff --git a/backend/test/app/test_helpers.clj b/backend/test/app/test_helpers.clj index 43334b883e..f503e5d663 100644 --- a/backend/test/app/test_helpers.clj +++ b/backend/test/app/test_helpers.clj @@ -126,7 +126,8 @@ :password "123123" :is-demo false} params)] - (->> (#'profile/create-profile conn params) + (->> params + (#'profile/create-profile conn) (#'profile/create-profile-relations conn))))) (defn create-project* @@ -159,15 +160,10 @@ ([i params] (create-team* *pool* i params)) ([conn i {:keys [profile-id] :as params}] (us/assert uuid? profile-id) - (let [id (mk-uuid "team" i) - team (#'teams/create-team conn {:id id - :profile-id profile-id - :name (str "team" i)})] - (#'teams/create-team-role conn - {:team-id id - :profile-id profile-id - :role :owner}) - team))) + (let [id (mk-uuid "team" i)] + (teams/create-team conn {:id id + :profile-id profile-id + :name (str "team" i)})))) (defn create-file-media-object* ([params] (create-file-media-object* *pool* params)) @@ -350,3 +346,11 @@ (defn reset-mock! [m] (reset! m @(mk/make-mock {}))) + +(defn pause + [] + (let [^java.io.Console cnsl (System/console)] + (println "[waiting RETURN]") + (.readLine cnsl) + nil)) + diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index 0ae1127dff..4cf883a975 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -392,7 +392,8 @@ (defmethod read-action-opts :navigate [interaction-src] - (select-keys interaction-src [:destination])) + (select-keys interaction-src [:destination + :preserve-scroll])) (defmethod read-action-opts :open-overlay [interaction-src] @@ -430,7 +431,8 @@ (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) + close-click-outside background-overlay preserve-scroll]} + (read-action-opts interaction-src) interactions (-> (lookup-shape file from-id) :interactions @@ -443,7 +445,8 @@ :overlay-position overlay-position :url url :close-click-outside close-click-outside - :background-overlay background-overlay})))] + :background-overlay background-overlay + :preserve-scroll preserve-scroll})))] (commit-change file {:type :mod-obj diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index acc1d2c5c6..149f71c651 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -10,30 +10,28 @@ [cuerdas.core :as str])) (def default - #{:backend-asserts - :api-doc - :registration - :demo-users}) + "A common flags that affects both: backend and frontend." + [:enable-registration + :enable-demo-users]) (defn parse - ([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)))) + [& flags] + (loop [flags (apply concat flags) + result #{}] + (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/shapes/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc index 3cc3589652..14360639ff 100644 --- a/common/src/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -6,6 +6,7 @@ (ns app.common.geom.shapes.intersect (:require + [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes.path :as gpp] @@ -172,22 +173,23 @@ "Checks if the given rect overlaps with the path in any point" [shape rect] - (let [;; If paths are too complex the intersection is too expensive - ;; we fallback to check its bounding box otherwise the performance penalty - ;; is too big - ;; TODO: Look for ways to optimize this operation - simple? (> (count (:content shape)) 100) + (when (d/not-empty? (:content shape)) + (let [ ;; If paths are too complex the intersection is too expensive + ;; we fallback to check its bounding box otherwise the performance penalty + ;; is too big + ;; TODO: Look for ways to optimize this operation + simple? (> (count (:content shape)) 100) - rect-points (gpr/rect->points rect) - rect-lines (points->lines rect-points) - path-lines (if simple? - (points->lines (:points shape)) - (gpp/path->lines shape)) - start-point (-> shape :content (first) :params (gpt/point))] + rect-points (gpr/rect->points rect) + rect-lines (points->lines rect-points) + path-lines (if simple? + (points->lines (:points shape)) + (gpp/path->lines shape)) + start-point (-> shape :content (first) :params (gpt/point))] - (or (is-point-inside-nonzero? (first rect-points) path-lines) - (is-point-inside-nonzero? start-point rect-lines) - (intersects-lines? rect-lines path-lines)))) + (or (is-point-inside-nonzero? (first rect-points) path-lines) + (is-point-inside-nonzero? start-point rect-lines) + (intersects-lines? rect-lines path-lines))))) (defn is-point-inside-ellipse? "checks if a point is inside an ellipse" diff --git a/common/src/app/common/pages/spec.cljc b/common/src/app/common/pages/spec.cljc index 2861484396..8ce65e409e 100644 --- a/common/src/app/common/pages/spec.cljc +++ b/common/src/app/common/pages/spec.cljc @@ -11,6 +11,7 @@ [app.common.spec :as us] [app.common.types.interactions :as cti] [app.common.types.page-options :as cto] + [app.common.types.radius :as ctr] [app.common.uuid :as uuid] [clojure.set :as set] [clojure.spec.alpha :as s])) @@ -191,12 +192,6 @@ (s/def :internal.shape/page-id uuid?) (s/def :internal.shape/proportion ::us/safe-number) (s/def :internal.shape/proportion-lock boolean?) -(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?)) @@ -285,12 +280,12 @@ :internal.shape/constraints-h :internal.shape/constraints-v :internal.shape/fixed-scroll - :internal.shape/rx - :internal.shape/ry - :internal.shape/r1 - :internal.shape/r2 - :internal.shape/r3 - :internal.shape/r4 + ::ctr/rx + ::ctr/ry + ::ctr/r1 + ::ctr/r2 + ::ctr/r3 + ::ctr/r4 :internal.shape/x :internal.shape/y :internal.shape/exports diff --git a/common/src/app/common/path/shapes_to_path.cljc b/common/src/app/common/path/shapes_to_path.cljc index d2f9059141..3a60ebbd54 100644 --- a/common/src/app/common/path/shapes_to_path.cljc +++ b/common/src/app/common/path/shapes_to_path.cljc @@ -177,18 +177,11 @@ (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))] + content (pb/content-bool bool-type (mapv :content children))] (-> shape (assoc :type :path) (assoc :content content) - (merge head-data) (d/without-keys dissoc-attrs)))) (defn convert-to-path diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc index 88f8eab9a8..e7614707ba 100644 --- a/common/src/app/common/spec.cljc +++ b/common/src/app/common/spec.cljc @@ -111,16 +111,6 @@ (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." @@ -134,7 +124,6 @@ (s/def ::bytes bytes?) - (s/def ::safe-integer #(and (int? %) @@ -149,8 +138,28 @@ (<= % max-safe-int))) +;; --- SPEC: set of Keywords + +(s/def ::set-of-keywords + (s/conformer + (fn [s] + (let [xform (comp + (map (fn [s] + (cond + (string? s) (keyword s) + (keyword? s) s + :else nil))) + (filter identity))] + (cond + (set? s) (into #{} xform s) + (string? s) (into #{} xform (str/words s)) + :else ::s/invalid))) + (fn [s] + (str/join " " (map name s))))) + ;; --- SPEC: email -(def email-re #"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+") + +(def email-re #"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+") (s/def ::email (s/conformer @@ -162,6 +171,23 @@ ::s/invalid)) str)) +(s/def ::set-of-emails + (s/conformer + (fn [v] + (cond + (string? v) + (into #{} (re-seq email-re v)) + + (or (set? v) (sequential? v)) + (->> (str/join " " v) + (re-seq email-re) + (into #{})) + + :else ::s/invalid)) + + (fn [v] + (str/join " " v)))) + ;; --- SPEC: set-of-str (s/def ::set-of-str diff --git a/common/src/app/common/types/interactions.cljc b/common/src/app/common/types/interactions.cljc index ebbc81e707..7c56113ad1 100644 --- a/common/src/app/common/types/interactions.cljc +++ b/common/src/app/common/types/interactions.cljc @@ -64,11 +64,13 @@ (s/def ::url ::us/string) (s/def ::close-click-outside ::us/boolean) (s/def ::background-overlay ::us/boolean) +(s/def ::preserve-scroll ::us/boolean) (defmulti action-opts-spec :action-type) (defmethod action-opts-spec :navigate [_] - (s/keys :req-un [::destination])) + (s/keys :req-un [::destination] + :opt-un [::preserve-scroll])) (defmethod action-opts-spec :open-overlay [_] (s/keys :req-un [::destination @@ -151,7 +153,8 @@ :navigate (assoc interaction :action-type action-type - :destination (get interaction :destination)) + :destination (get interaction :destination) + :preserve-scroll false) (:open-overlay :toggle-overlay) (let [overlay-pos-type (get interaction :overlay-pos-type :center) @@ -196,6 +199,10 @@ (and (has-destination interaction) (some? (:destination interaction)))) +(defn has-preserve-scroll + [interaction] + (= (:action-type interaction) :navigate)) + (defn set-destination [interaction destination] (us/verify ::interaction interaction) @@ -210,6 +217,13 @@ (assoc :overlay-pos-type :center :overlay-position (gpt/point 0 0)))) +(defn set-preserve-scroll + [interaction preserve-scroll] + (us/verify ::interaction interaction) + (us/verify ::us/boolean preserve-scroll) + (assert (has-preserve-scroll interaction)) + (assoc interaction :preserve-scroll preserve-scroll)) + (defn has-url [interaction] (= (:action-type interaction) :open-url)) diff --git a/common/src/app/common/types/radius.cljc b/common/src/app/common/types/radius.cljc new file mode 100644 index 0000000000..308de6c2e3 --- /dev/null +++ b/common/src/app/common/types/radius.cljc @@ -0,0 +1,90 @@ +;; 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.radius + (:require + [app.common.spec :as us] + [clojure.spec.alpha :as s])) + +(s/def ::rx ::us/safe-number) +(s/def ::ry ::us/safe-number) +(s/def ::r1 ::us/safe-number) +(s/def ::r2 ::us/safe-number) +(s/def ::r3 ::us/safe-number) +(s/def ::r4 ::us/safe-number) + +;; Rectangle shapes may define the radius of the corners in two modes: +;; - radius-1 all corners have the same radius (although we store two +;; values :rx and :ry because svg uses it this way). +;; - radius-4 each corner (top-left, top-right, bottom-right, bottom-left) +;; has an independent value. SVG does not allow this directly, so we +;; emulate it with paths. + +;; A shape never will have both :rx and :r1 simultaneously + +;; All operations take into account that the shape may not be a rectangle, and so +;; it hasn't :rx nor :r1. In this case operations must leave shape untouched. + +(defn radius-mode + [shape] + (cond (:rx shape) :radius-1 + (:r1 shape) :radius-4 + :else nil)) + +(defn radius-1? + [shape] + (and (:rx shape) (not= (:rx shape) 0))) + +(defn radius-4? + [shape] + (and (:r1 shape) + (or (not= (:r1 shape) 0) + (not= (:r2 shape) 0) + (not= (:r3 shape) 0) + (not= (:r4 shape) 0)))) + +(defn all-equal? + [shape] + (= (:r1 shape) (:r2 shape) (:r3 shape) (:r4 shape))) + +(defn switch-to-radius-1 + [shape] + (let [r (if (all-equal? shape) (:r1 shape) 0)] + (cond-> shape + (:r1 shape) + (-> (assoc :rx r :ry r) + (dissoc :r1 :r2 :r3 :r4))))) + +(defn switch-to-radius-4 + [shape] + (cond-> shape + (:rx shape) + (-> (assoc :r1 (:rx shape) + :r2 (:rx shape) + :r3 (:rx shape) + :r4 (:rx shape)) + (dissoc :rx :ry)))) + +(defn set-radius-1 + [shape value] + (cond-> shape + (:r1 shape) + (-> (dissoc :r1 :r2 :r3 :r4) + (assoc :rx 0 :ry 0)) + + (:rx shape) + (assoc :rx value :ry value))) + +(defn set-radius-4 + [shape attr value] + (cond-> shape + (:rx shape) + (-> (dissoc :rx :rx) + (assoc :r1 0 :r2 0 :r3 0 :r4 0)) + + (attr shape) + (assoc attr value))) + diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 6b8290e3a4..3fc07920a4 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -5,7 +5,7 @@ ARG DEBIAN_FRONTEND=noninteractive ENV NODE_VERSION=v14.17.6 \ CLOJURE_VERSION=1.10.3.967 \ - CLJKONDO_VERSION=2021.09.15 \ + CLJKONDO_VERSION=2021.10.19 \ BABASHKA_VERSION=0.6.1 \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml index 764b638db9..e073eb2ce4 100644 --- a/docker/devenv/docker-compose.yaml +++ b/docker/devenv/docker-compose.yaml @@ -50,7 +50,7 @@ services: - PENPOT_SMTP_PASSWORD= - PENPOT_SMTP_SSL=false - PENPOT_SMTP_TLS=false - - PENPOT_FLAGS="enable-cors" + - PENPOT_FLAGS="enable-cors enable-insecure-register enable-terms-and-privacy-checkbox" # LDAP setup - PENPOT_LDAP_HOST=ldap diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index 1b7530aac9..4aa3c67b1d 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -89,6 +89,16 @@ http { error_page 301 302 307 = @handle_redirect; } + location ~ ^/github/penpot-files/(?[a-zA-Z0-9\-\_\.]+) { + proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file; + proxy_hide_header Access-Control-Allow-Origin; + proxy_set_header User-Agent "curl/7.74.0"; + proxy_set_header Host "raw.githubusercontent.com"; + proxy_set_header Accept "*/*"; + add_header Access-Control-Allow-Origin $http_origin; + proxy_buffering off; + } + location /internal/assets { internal; alias /home/penpot/penpot/backend/assets; diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index a3775a048c..71fb4f965d 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -104,7 +104,7 @@ (def browser-pool-factory (letfn [(create [] (let [path (cf/get :browser-executable-path "/usr/bin/google-chrome")] - (-> (pp/launch #js {:executablePath path :args #js ["--no-sandbox"]}) + (-> (pp/launch #js {:executablePath path :args #js ["--no-sandbox" "--font-render-hinting=none"]}) (p/then (fn [browser] (let [id (deref pool-browser-id)] (log/info :origin "factory" :action "create" :browser-id id) diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 54544aa24c..ca789f2e0e 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -23,7 +23,7 @@ [app.renderer.bitmap :refer [create-cookie]] [promesa.core :as p])) -(log/set-level "app.http.export-svg" :trace) +(log/set-level "app.renderer.svg" :trace) (defn- xml->clj [data] @@ -129,7 +129,7 @@ svgpath (path/join basepath (str basename ".svg"))] (-> (sh/run-cmd! (str "potrace --flat -b svg " pbmpath " -o " svgpath)) (p/then (constantly svgpath))))) - + (generate-color-layer [ppmpath color] (log/trace :fn :generate-color-layer :ppmpath ppmpath :color color) (let [basepath (path/dirname ppmpath) @@ -146,15 +146,61 @@ {:color color :svgdata data})))))) - (join-color-layers [{:keys [x y width height] :as node} layers] - (log/trace :fn :join-color-layers) + (set-path-color [id color mapping node] + (let [color-mapping (get mapping color)] + (cond + (and (some? color-mapping) + (= "transparent" (get color-mapping "type"))) + (update node "attributes" assoc + "fill" (get color-mapping "hex") + "fill-opacity" (get color-mapping "opacity")) + + (and (some? color-mapping) + (= "gradient" (get color-mapping "type"))) + (update node "attributes" assoc + "fill" (str "url(#gradient-" id "-" (subs color 1) ")")) + + :else + (update node "attributes" assoc "fill" color)))) + + (get-stops [data] + (->> (get-in data ["gradient" "stops"]) + (mapv (fn [stop-data] + {"type" "element" + "name" "stop" + "attributes" {"offset" (get stop-data "offset") + "stop-color" (get stop-data "color") + "stop-opacity" (get stop-data "opacity")}})))) + + (data->gradient-def [id [color data]] + (let [id (str "gradient-" id "-" (subs color 1))] + (if (= type "linear") + {"type" "element" + "name" "linearGradient" + "attributes" {"id" id "x1" "0.5" "y1" "1" "x2" "0.5" "y2" "0"} + "elements" (get-stops data)} + + {"type" "element" + "name" "radialGradient" + "attributes" {"id" id "cx" "0.5" "cy" "0.5" "r" "0.5"} + "elements" (get-stops data)} + ))) + + (get-gradients [id mapping] + (->> mapping + (filter (fn [[color data]] + (= (get data "type") "gradient"))) + (mapv (partial data->gradient-def id)))) + + (join-color-layers [{:keys [id x y width height mapping] :as node} layers] + (log/trace :fn :join-color-layers :mapping mapping) (loop [result (-> (:svgdata (first layers)) (assoc "elements" [])) layers (seq layers)] (if-let [{:keys [color svgdata]} (first layers)] (recur (->> (get svgdata "elements") (filter #(= (get % "name") "g")) - (map #(update % "attributes" assoc "fill" color)) + (map (partial set-path-color id color mapping)) (update result "elements" d/concat)) (rest layers)) @@ -166,22 +212,33 @@ (parse-viewbox)) transform (str/fmt "translate(%s, %s) scale(%s, %s)" x y (/ width (:width vbox)) - (/ height (:height vbox)))] + (/ height (:height vbox))) + + gradient-defs (get-gradients id mapping) + + elements + (->> (get result "elements") + (mapv (fn [group] + (let [paths (get group "elements")] + (if (= 1 (count paths)) + (let [path (first paths)] + (update path "attributes" + (fn [attrs] + (-> attrs + (d/merge (get group "attributes")) + (update "transform" #(str transform " " %)))))) + (update-in group ["attributes" "transform"] #(str transform " " %))))))) + + + elements (cond->> elements + (not (empty? gradient-defs)) + (d/concat [{"type" "element" "name" "defs" "attributes" {} + "elements" gradient-defs}]))] + (-> result (assoc "name" "g") (assoc "attributes" {}) - (update "elements" (fn [elements] - (mapv (fn [group] - (let [paths (get group "elements")] - (if (= 1 (count paths)) - (let [path (first paths)] - (update path "attributes" - (fn [attrs] - (-> attrs - (d/merge (get group "attributes")) - (update "transform" #(str transform " " %)))))) - (update-in group ["attributes" "transform"] #(str transform " " %))))) - elements)))))))) + (assoc "elements" elements)))))) (convert-to-svg [ppmpath {:keys [colors] :as node}] (log/trace :fn :convert-to-svg :ppmpath ppmpath :colors colors) @@ -201,25 +258,28 @@ :svgdata svgdata)))) (extract-element-attrs [^js element] - (let [^js attrs (.. element -attributes) - ^js colors (.. element -dataset -colors)] - #js {:id (.. attrs -id -value) - :x (.. attrs -x -value) - :y (.. attrs -y -value) - :width (.. attrs -width -value) - :height (.. attrs -height -value) - :colors (.split colors ",")})) + (let [^js attrs (.. element -attributes) + ^js colors (.. element -dataset -colors) + ^js mapping (.. element -dataset -mapping)] + #js {:id (.. attrs -id -value) + :x (.. attrs -x -value) + :y (.. attrs -y -value) + :width (.. attrs -width -value) + :height (.. attrs -height -value) + :colors (.split colors ",") + :mapping (js/JSON.parse mapping)})) (extract-single-node [[shot node]] (log/trace :fn :extract-single-node) (p/let [attrs (bw/eval! node extract-element-attrs)] - {:id (unchecked-get attrs "id") - :x (unchecked-get attrs "x") - :y (unchecked-get attrs "y") - :width (unchecked-get attrs "width") - :height (unchecked-get attrs "height") - :colors (vec (unchecked-get attrs "colors")) + {:id (unchecked-get attrs "id") + :x (unchecked-get attrs "x") + :y (unchecked-get attrs "y") + :width (unchecked-get attrs "width") + :height (unchecked-get attrs "height") + :colors (vec (unchecked-get attrs "colors")) + :mapping (js->clj (unchecked-get attrs "mapping")) :data shot})) (resolve-text-node [page node] @@ -313,3 +373,4 @@ ".svg")) :length (alength content) :mime-type "image/svg+xml"})) + diff --git a/frontend/deps.edn b/frontend/deps.edn index b878e722f2..5468b8b552 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -23,7 +23,7 @@ :dev {:extra-deps - {thheller/shadow-cljs {:mvn/version "2.15.9"} + {thheller/shadow-cljs {:mvn/version "2.15.12"} cider/cider-nrepl {:mvn/version "0.26.0"}}} :shadow-cljs diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index 5a64cd8a38..ddf283dc65 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -25,6 +25,17 @@ paths.resources = "./resources/"; paths.output = "./resources/public/"; paths.dist = "./target/dist/"; +/*********************************************** + * Marked Extensions + ***********************************************/ + +const renderer = { + link(href, title, text) { + return `${text}`; + } +}; + +marked.use({renderer}); /*********************************************** * Helpers diff --git a/frontend/package.json b/frontend/package.json index 74b63207e6..63d5aed696 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,7 +23,7 @@ "start": "npm-run-all --parallel watch-gulp watch-main" }, "devDependencies": { - "autoprefixer": "^10.2.4", + "autoprefixer": "^10.3.7", "gettext-parser": "^4.0.4", "gulp": "4.0.2", "gulp-concat": "^2.6.1", @@ -35,36 +35,36 @@ "gulp-sourcemaps": "^3.0.0", "gulp-svg-sprite": "^1.5.0", "map-stream": "0.0.7", - "marked": "^3.0.4", + "marked": "^3.0.8", "mkdirp": "^1.0.4", - "nodemon": "^2.0.13", + "nodemon": "^2.0.14", "npm-run-all": "^4.1.5", - "postcss": "^8.3.5", + "postcss": "^8.3.11", "postcss-clean": "^1.2.2", "rimraf": "^3.0.0", - "sass": "^1.35.1", - "shadow-cljs": "2.15.9" + "sass": "^1.43.4", + "shadow-cljs": "2.15.12" }, "dependencies": { - "@sentry/browser": "^6.12.0", - "@sentry/tracing": "^6.12.0", - "date-fns": "^2.22.1", + "@sentry/browser": "^6.13.3", + "@sentry/tracing": "^6.13.3", + "date-fns": "^2.25.0", "draft-js": "^0.11.7", - "highlight.js": "^11.0.1", + "highlight.js": "^11.3.1", "js-beautify": "^1.14.0", "jszip": "^3.6.0", "luxon": "^2.0.2", "mousetrap": "^1.6.5", - "opentype.js": "^1.3.3", + "opentype.js": "^1.3.4", "randomcolor": "^0.6.2", "react": "~17.0.2", "react-dom": "~17.0.2", "react-virtualized": "^9.22.3", - "rxjs": "~7.2.0", + "rxjs": "~7.4.0", "sax": "^1.2.4", "source-map-support": "^0.5.16", "tdigest": "^0.1.1", - "ua-parser-js": "^0.7.28", + "ua-parser-js": "^1.0.2", "xregexp": "^5.0.1" } } diff --git a/frontend/resources/images/on-solo-hover.svg b/frontend/resources/images/on-solo-hover.svg new file mode 100644 index 0000000000..d755531784 --- /dev/null +++ b/frontend/resources/images/on-solo-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/images/on-solo.svg b/frontend/resources/images/on-solo.svg new file mode 100644 index 0000000000..08d76020df --- /dev/null +++ b/frontend/resources/images/on-solo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/images/on-teamup-hover.svg b/frontend/resources/images/on-teamup-hover.svg new file mode 100644 index 0000000000..c012588a0d --- /dev/null +++ b/frontend/resources/images/on-teamup-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/images/on-teamup.svg b/frontend/resources/images/on-teamup.svg new file mode 100644 index 0000000000..10b85bf137 --- /dev/null +++ b/frontend/resources/images/on-teamup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/images/ph-file.svg b/frontend/resources/images/ph-file.svg new file mode 100644 index 0000000000..6859f17f50 --- /dev/null +++ b/frontend/resources/images/ph-file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/images/ph-left.svg b/frontend/resources/images/ph-left.svg new file mode 100644 index 0000000000..7853a1b55f --- /dev/null +++ b/frontend/resources/images/ph-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/images/ph-right.svg b/frontend/resources/images/ph-right.svg new file mode 100644 index 0000000000..6e2852f314 --- /dev/null +++ b/frontend/resources/images/ph-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss index baa6526b6f..adaf7f1383 100644 --- a/frontend/resources/styles/common/framework.scss +++ b/frontend/resources/styles/common/framework.scss @@ -396,6 +396,11 @@ ul.slider-dots { text-align: right; top: 26%; width: 18px; + + pointer-events: none; + max-width: 4rem; + overflow: hidden; + text-overflow: ellipsis; } .after { diff --git a/frontend/resources/styles/main/layouts/handoff.scss b/frontend/resources/styles/main/layouts/handoff.scss index 027bbac0d9..917410eca3 100644 --- a/frontend/resources/styles/main/layouts/handoff.scss +++ b/frontend/resources/styles/main/layouts/handoff.scss @@ -46,7 +46,7 @@ $width-settings-bar: 16rem; } .handoff-layout { - .viewer-preview { + .viewer-section { flex-wrap: nowrap; } .settings-bar { diff --git a/frontend/resources/styles/main/partials/comments.scss b/frontend/resources/styles/main/partials/comments.scss index 87c0d1eb29..04fcc45839 100644 --- a/frontend/resources/styles/main/partials/comments.scss +++ b/frontend/resources/styles/main/partials/comments.scss @@ -336,7 +336,7 @@ .viewer-comments-container { width: 100%; height: 100%; - z-index: 1000; + z-index: 1; position: absolute; top: 0px; left: 0px; diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss index 032bab70c6..bbf80ec676 100644 --- a/frontend/resources/styles/main/partials/dashboard-grid.scss +++ b/frontend/resources/styles/main/partials/dashboard-grid.scss @@ -335,6 +335,20 @@ padding: 3rem; justify-content: center; + &.drafts { + background-image: url("/images/ph-left.svg"), url("/images/ph-right.svg"); + background-position: 15% bottom, 85% top; + background-repeat: no-repeat; + .text { + p { + max-width: 360px; + text-align: center; + font-size: $fs16; + } + } + } + + svg { width: 36px; height: 36px; @@ -346,5 +360,10 @@ color: $color-gray-30; font-size: $fs16; } + + img.ph-files { + height: 150px; + margin-right: calc(100% - 148px); + } } diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index 27da8245df..bff16696cb 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -63,7 +63,7 @@ display: flex; flex-direction: column; width: 448px; - background-color: $color-dashboard; + background-color: $color-white; .modal-header { align-items: center; @@ -705,7 +705,7 @@ background-color: $color-white; box-shadow: 0 10px 10px rgba(0,0,0,.2); display: flex; - min-height: 370px; + min-height: 420px; flex-direction: row; font-family: "sourcesanspro", sans-serif; min-width: 620px; @@ -824,21 +824,93 @@ } &.final { + // TODO: Juan revisa TODA esta parte + padding: $size-5 0 0 0; + flex-direction: column; + + .modal-top { + padding-top: 40px; + color: $color-gray-60; + display: flex; + flex-direction: column; + align-items: center; + + h1 { + font-family: 'worksans', sans-serif; + font-weight: 700; + font-size: 27px; + margin-bottom: $size-3; + } + p { + font-family: 'worksans', sans-serif; + font-weight: 500; + font-size: $fs18; + } + + } + + .modal-columns { + display: flex; + margin: 17px; + + .modal-left { + background-image: url("/images/on-solo.svg"); + background-position: left top; + background-size: 11%; + } + + .modal-left:hover { + background-image: url("/images/on-solo-hover.svg"); + background-size: 15%; + } + + .modal-right { + background-image: url("/images/on-teamup.svg"); + background-position: right top; + background-size: 28%; + } + + .modal-right:hover { + background-image: url("/images/on-teamup-hover.svg"); + background-size: 32%; + } + + .modal-right, + .modal-left { + background-repeat: no-repeat; + border-radius: $br-medium; + transition: all ease .3s; + &:hover { + background-color: $color-primary; + } + } + } + + .modal-left { + margin-right: 35px; + } .modal-left, .modal-right { + justify-content: center; align-items: center; background-color: $color-white; color: $color-black; flex: 1; flex-direction: column; - overflow: visible; - padding: $size-6 40px; + // overflow: visible; + // padding: $size-6 40px; text-align: center; + border: 1px solid $color-gray-10; + border-radius: 2px; + min-height: 180px; + width: 233px; + cursor: pointer; + h2 { - font-weight: 900; + font-weight: 700; margin-bottom: $size-5; font-size: $fs24; } @@ -847,12 +919,6 @@ font-size: $fs14; } - .btn-primary { - margin-bottom: 0; - margin-top: auto; - width: 200px; - } - img { box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25); border-radius: $br-medium; @@ -861,26 +927,6 @@ width: 150px; } } - - .modal-left { - border-right: 1px solid $color-gray-10; - - form { - align-items: center; - display: flex; - flex-direction: column; - margin-top: auto; - - .custom-input { - margin-bottom: $size-4; - - input { - width: 200px; - } - } - } - } - } } @@ -899,3 +945,193 @@ .relnotes .onboarding { height: 420px; } + +.onboarding-templates { + position: fixed; + top: 0; + right: 0; + width: 348px; + height: 100vh; + + .modal-close-button { + width: 34px; + height: 34px; + margin-right: 13px; + margin-top: 13px; + svg { + width: 24px; + height: 24px; + } + } + + .modal-header { + height: unset; + border-radius: unset; + justify-content: flex-end; + } + + .modal-content { + border: 0px; + padding: 0px 25px; + background-color: $color-white; + flex-grow: 1; + + p, h3 { + color: $color-gray-60; + text-align: center; + } + + h3 { + font-size: $fs18; + font-weight: bold; + } + + p { + font-size: $fs16; + } + + + .templates { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 8%; + } + + .template-item { + width: 275px; + border: 1px solid $color-gray-10; + + display: flex; + flex-direction: column; + text-align: left; + border-radius: $br-small; + + &:not(:last-child) { + margin-bottom: 22px; + } + } + + .template-item-content { + // height: 144px; + flex-grow: 1; + + img { + border-radius: $br-small $br-small 0 0; + } + } + + .template-item-title { + padding: 6px 12px; + height: 64px; + border-top: 1px solid $color-gray-10; + + .label { + color: $color-black; + padding: 0px 4px; + font-size: $fs16; + display: flex; + } + + .action { + color: $color-primary-dark; + cursor: pointer; + font-size: $fs14; + font-weight: 600; + display: flex; + justify-content: flex-end; + margin-top: $size-2; + } + + } + } +} + + +.onboarding-team { + display: flex; + min-width: 620px; + min-height: 420px; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + + .title { + display: flex; + flex-direction: column; + align-items: center; + width: 408px; + + color: $color-gray-60; + h2 { + font-weight: 700; + padding-bottom: 10px; + } + + p { + text-align: center; + font-size: $fs18; + } + } + + form { + display: flex; + flex-direction: column; + margin-top: $size-6; + + .buttons { + margin-top: 30px; + display: flex; + justify-content: flex-end; + + > *:not(:last-child) { + margin-right: 13px; + } + + input { margin-bottom: unset; } + input[type=submit] { + } + + .btn-primary { + width: 117px; + } + } + + .team-row { + .custom-input { + width: 459px; + } + } + + .invite-row { + display: flex; + justify-content: space-between; + + > *:not(:last-child) { + margin-right: 13px; + } + + .custom-input { + width: 321px; + } + + .custom-select { + width: 118px; + } + } + + .skip-action { + display: flex; + justify-content: flex-end; + margin-top: 15px; + .action { + color: $color-primary-dark; + font-weight: 500; + font-size: $fs16; + cursor: pointer; + } + } + + } +} diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index 68fab3ded8..ef8fecf0b4 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -70,12 +70,6 @@ width: 100%; align-items: center; } - - &:hover { - .element-set-title { - color: $color-gray-10; - } - } } .element-list { diff --git a/frontend/resources/styles/main/partials/viewer-header.scss b/frontend/resources/styles/main/partials/viewer-header.scss index ff7808da97..3a2b6a6845 100644 --- a/frontend/resources/styles/main/partials/viewer-header.scss +++ b/frontend/resources/styles/main/partials/viewer-header.scss @@ -6,7 +6,6 @@ height: 48px; padding: 0 $size-4 0 55px; position: relative; - z-index: 12; justify-content: space-between; a { diff --git a/frontend/resources/styles/main/partials/viewer-thumbnails.scss b/frontend/resources/styles/main/partials/viewer-thumbnails.scss index d87f765fa3..d37c05bfd5 100644 --- a/frontend/resources/styles/main/partials/viewer-thumbnails.scss +++ b/frontend/resources/styles/main/partials/viewer-thumbnails.scss @@ -6,7 +6,7 @@ overflow: hidden; display: flex; flex-direction: column; - z-index: 12; + z-index: 10; &.invisible { visibility: hidden; diff --git a/frontend/resources/styles/main/partials/viewer.scss b/frontend/resources/styles/main/partials/viewer.scss index 346db1c83c..34e2c5779c 100644 --- a/frontend/resources/styles/main/partials/viewer.scss +++ b/frontend/resources/styles/main/partials/viewer.scss @@ -6,8 +6,8 @@ grid-template-columns: 1fr; } -.viewer-preview { - height: calc(100vh - 40px); +.viewer-section { + height: calc(100vh - 48px); grid-row: 1 / span 2; grid-column: 1 / span 1; diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 9e308a1ca9..3dbe3e1277 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -57,8 +57,8 @@ (defn- parse-flags [global] (let [flags (obj/get global "penpotFlags" "") - flags (into #{} (map keyword) (str/words flags))] - (flags/parse flags flags/default))) + flags (sequence (map keyword) (str/words flags))] + (flags/parse flags/default flags))) (defn- parse-version [global] diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 7025dc5ea8..982e268f3d 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -187,10 +187,12 @@ (ptk/reify ::files-fetched ptk/UpdateEvent (update [_ state] - (update state :dashboard-files - (fn [state] - (let [state (remove-project-files state)] - (reduce #(assoc %1 (:id %2) %2) state files)))))))) + (-> state + (update :dashboard-files + (fn [state] + (let [state (remove-project-files state)] + (reduce #(assoc %1 (:id %2) %2) state files)))) + (assoc-in [:dashboard-projects project-id :count] (count files))))))) (defn fetch-files [{:keys [project-id] :as params}] @@ -300,6 +302,28 @@ (rx/map team-created) (rx/catch on-error)))))) +;; --- EVENT: create-team-with-invitations + +;; NOTE: right now, it only handles a single email, in a near future +;; this will be changed to the ability to specify multiple emails. + +(defn create-team-with-invitations + [{:keys [name email role] :as params}] + (us/assert string? name) + (ptk/reify ::create-team-with-invitations + ptk/WatchEvent + (watch [_ _ _] + (let [{:keys [on-success on-error] + :or {on-success identity + on-error rx/throw}} (meta params) + params {:name name + :emails #{email} + :role role}] + (->> (rp/mutation! :create-team-and-invite-members params) + (rx/tap on-success) + (rx/map team-created) + (rx/catch on-error)))))) + ;; --- EVENT: update-team (defn update-team diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 15f39ba1ff..56ee0c0dfb 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -302,6 +302,21 @@ (update [_ state] (assoc-in state [:viewer-local :interactions-show?] false)))) +(defn set-nav-scroll + [scroll] + (ptk/reify ::set-nav-scroll + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:viewer-local :nav-scroll] scroll)))) + +(defn reset-nav-scroll + [] + (ptk/reify ::reset-nav-scroll + ptk/UpdateEvent + (update [_ state] + (d/dissoc-in state [:viewer-local :nav-scroll])))) + + ;; --- Navigation inside page (defn go-to-frame-by-index diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index 0f6a4a2e83..53d8cf9a26 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -125,70 +125,73 @@ create a group that contains all ids. Then, make a component with it, and link all shapes to their corresponding one in the component." [shapes objects page-id file-id] - (let [[group rchanges uchanges] - (if (and (= (count shapes) 1) - (= (:type (first shapes)) :group)) - [(first shapes) [] []] - (dwg/prepare-create-group objects page-id shapes "Component-1" true)) + (if (and (= (count shapes) 1) + (:component-id (first shapes))) + empty-changes + (let [[group rchanges uchanges] + (if (and (= (count shapes) 1) + (= (:type (first shapes)) :group)) + [(first shapes) [] []] + (dwg/prepare-create-group objects page-id shapes "Component-1" true)) - [new-shape new-shapes updated-shapes] - (make-component-shape group objects file-id) + [new-shape new-shapes updated-shapes] + (make-component-shape group objects file-id) - rchanges (conj rchanges - {:type :add-component - :id (:id new-shape) - :name (:name new-shape) - :shapes new-shapes}) + rchanges (conj rchanges + {:type :add-component + :id (:id new-shape) + :name (:name new-shape) + :shapes new-shapes}) - rchanges (into rchanges - (map (fn [updated-shape] - {:type :mod-obj - :page-id page-id - :id (:id updated-shape) - :operations [{:type :set - :attr :component-id - :val (:component-id updated-shape)} - {:type :set - :attr :component-file - :val (:component-file updated-shape)} - {:type :set - :attr :component-root? - :val (:component-root? updated-shape)} - {:type :set - :attr :shape-ref - :val (:shape-ref updated-shape)} - {:type :set - :attr :touched - :val (:touched updated-shape)}]}) - updated-shapes)) - - uchanges (conj uchanges - {:type :del-component - :id (:id new-shape)}) - - uchanges (into uchanges - (map (fn [updated-shape] - (let [original-shape (get objects (:id updated-shape))] + rchanges (into rchanges + (map (fn [updated-shape] {:type :mod-obj :page-id page-id :id (:id updated-shape) :operations [{:type :set :attr :component-id - :val (:component-id original-shape)} + :val (:component-id updated-shape)} {:type :set :attr :component-file - :val (:component-file original-shape)} + :val (:component-file updated-shape)} {:type :set :attr :component-root? - :val (:component-root? original-shape)} + :val (:component-root? updated-shape)} {:type :set :attr :shape-ref - :val (:shape-ref original-shape)} + :val (:shape-ref updated-shape)} {:type :set :attr :touched - :val (:touched original-shape)}]})) - updated-shapes))] - [group rchanges uchanges])) + :val (:touched updated-shape)}]}) + updated-shapes)) + + uchanges (conj uchanges + {:type :del-component + :id (:id new-shape)}) + + uchanges (into uchanges + (map (fn [updated-shape] + (let [original-shape (get objects (:id updated-shape))] + {:type :mod-obj + :page-id page-id + :id (:id updated-shape) + :operations [{:type :set + :attr :component-id + :val (:component-id original-shape)} + {:type :set + :attr :component-file + :val (:component-file original-shape)} + {:type :set + :attr :component-root? + :val (:component-root? original-shape)} + {:type :set + :attr :shape-ref + :val (:shape-ref original-shape)} + {:type :set + :attr :touched + :val (:touched original-shape)}]})) + updated-shapes))] + [group rchanges uchanges]))) (defn duplicate-component "Clone the root shape of the component and all children. Generate new diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index c6acba6e5a..f24d2a2807 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -201,6 +201,9 @@ (s/def ::shapes-changes-persisted (s/keys :req-un [::revn ::cp/changes])) +(defn shapes-persited-event? [event] + (= (ptk/type event) ::changes-persisted)) + (defn shapes-changes-persisted [file-id {:keys [revn changes] :as params}] (us/verify ::us/uuid file-id) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 3612d8490c..adc3cfaf60 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -61,12 +61,6 @@ (def dashboard-search-result (l/derived :dashboard-search-result st/state)) -(def dashboard-team - (l/derived (fn [state] - (let [team-id (:current-team-id state)] - (get-in state [:teams team-id]))) - st/state)) - (def dashboard-team-stats (l/derived :dashboard-team-stats st/state)) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index b005dc11cd..5454716154 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -72,7 +72,10 @@ :dashboard-team-settings) [:* #_[:div.modal-wrapper - [:& app.main.ui.onboarding/release-notes-modal {:version "1.8"}]] + #_[:& app.main.ui.onboarding/onboarding-templates-modal] + #_[:& app.main.ui.onboarding/onboarding-modal] + #_[:& app.main.ui.onboarding/onboarding-team-modal] + ] [:& dashboard {:route route}]] :viewer diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 71cc32eeb5..84c362fb23 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -169,7 +169,9 @@ (let [token (:invitation-token data)] (st/emit! (rt/nav :auth-verify-token {} {:token token}))) - (not= "penpot" (:auth-backend data)) + ;; The :is-active flag is true, when insecure-register is enabled + ;; or the user used external auth provider. + (:is-active data) (st/emit! (du/login-from-register)) :else @@ -178,9 +180,14 @@ (s/def ::accept-terms-and-privacy (s/and ::us/boolean true?)) (s/def ::accept-newsletter-subscription ::us/boolean) -(s/def ::register-validate-form - (s/keys :req-un [::token ::fullname ::accept-terms-and-privacy] - :opt-un [::accept-newsletter-subscription])) +(if (contains? @cf/flags :terms-and-privacy-checkbox) + (s/def ::register-validate-form + (s/keys :req-un [::token ::fullname ::accept-terms-and-privacy] + :opt-un [::accept-newsletter-subscription])) + (s/def ::register-validate-form + (s/keys :req-un [::token ::fullname] + :opt-un [::accept-terms-and-privacy + ::accept-newsletter-subscription]))) (mf/defc register-validate-form [{:keys [params] :as props}] @@ -207,23 +214,17 @@ :label (tr "auth.fullname") :type "text"}]] - [:div.fields-row - [:& fm/input {:name :accept-terms-and-privacy - :class "check-primary" - :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 - ;; [:& fm/input {:name :accept-newsletter-subscription - ;; :class "check-primary" - ;; :label (tr "auth.newsletter-subscription") - ;; :type "checkbox"}]]) + (when (contains? @cf/flags :terms-and-privacy-checkbox) + [:div.fields-row + [:& fm/input {:name :accept-terms-and-privacy + :class "check-primary" + :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")]]]]]) [:& fm/submit-button {:label (tr "auth.register-submit") diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index 54154c5479..47595beac7 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -24,6 +24,7 @@ max-val-str (obj/get props "max") wrap-value? (obj/get props "data-wrap") on-change (obj/get props "onChange") + title (obj/get props "title") ;; We need a ref pointing to the input dom element, but the user ;; of this component may provide one (that is forwarded here). @@ -33,9 +34,18 @@ value (d/parse-integer value-str 0) - min-val (when (string? min-val-str) + min-val (cond + (number? min-val-str) + min-val-str + + (string? min-val-str) (d/parse-integer min-val-str)) - max-val (when (string? max-val-str) + + max-val (cond + (number? max-val-str) + max-val-str + + (string? max-val-str) (d/parse-integer max-val-str)) num? (fn [val] (and (number? val) @@ -144,6 +154,7 @@ (obj/set! "type" "text") (obj/set! "ref" ref) (obj/set! "defaultValue" value-str) + (obj/set! "title" title) (obj/set! "onWheel" handle-mouse-wheel) (obj/set! "onKeyDown" handle-key-down) (obj/set! "onBlur" handle-blur))] diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index 631b4b6f58..e91e4e82af 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -11,6 +11,10 @@ (def render-ctx (mf/create-context nil)) (def def-ctx (mf/create-context false)) +;; This content is used to replace complex colors to simple ones +;; for text shapes in the export process +(def text-plain-colors-ctx (mf/create-context false)) + (def current-route (mf/create-context nil)) (def current-team-id (mf/create-context nil)) (def current-project-id (mf/create-context nil)) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 9d2f00178f..1292c6eafb 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -104,11 +104,11 @@ (when (and (:onboarding-viewed props) (not= version (:main @cf/version)) (not= "0.0" (:main @cf/version))) - (tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes :version (:main @cf/version)}))))))) + (tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes + :version (:main @cf/version)}))))))) [:& (mf/provider ctx/current-team-id) {:value team-id} [:& (mf/provider ctx/current-project-id) {:value project-id} - ;; NOTE: dashboard events and other related functions assumes ;; that the team is a implicit context variable that is ;; available using react context or accessing diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index 4ce733033c..ef413c78ce 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -120,6 +120,6 @@ [:* [:& header {:team team :project project}] [:section.dashboard-container - [:& grid {:project-id (:id project) + [:& grid {:project project :files files}]]])) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 3bd48a1ceb..f91e1de5c0 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -15,6 +15,7 @@ [app.main.ui.dashboard.file-menu :refer [file-menu]] [app.main.ui.dashboard.import :refer [use-import-file]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] + [app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]] [app.main.ui.icons :as i] [app.main.worker :as wrk] [app.util.dom :as dom] @@ -195,24 +196,10 @@ :on-edit on-edit :on-menu-close on-menu-close}])]]])) -(mf/defc empty-placeholder - [{:keys [dragging?] :as props}] - (if-not dragging? - [:div.grid-empty-placeholder - [:div.icon i/file-html] - [:div.text (tr "dashboard.empty-files")]] - [:div.grid-row.no-wrap - [:div.grid-item]])) - -(mf/defc loading-placeholder - [] - [:div.grid-empty-placeholder - [:div.icon i/loader] - [:div.text (tr "dashboard.loading-files")]]) - (mf/defc grid - [{:keys [files project-id] :as props}] - (let [dragging? (mf/use-state false) + [{:keys [files project] :as props}] + (let [dragging? (mf/use-state false) + project-id (:id project) on-finish-import (mf/use-callback @@ -272,7 +259,7 @@ :navigate? true}])] :else - [:& empty-placeholder])])) + [:& empty-placeholder {:default? (:is-default project)}])])) (mf/defc line-grid-row [{:keys [files selected-files on-load-more dragging?] :as props}] @@ -330,8 +317,11 @@ (tr "dashboard.show-all-files")]])])) (mf/defc line-grid - [{:keys [project-id team-id files on-load-more] :as props}] + [{:keys [project team files on-load-more] :as props}] (let [dragging? (mf/use-state false) + project-id (:id project) + team-id (:id team) + selected-files (mf/deref refs/dashboard-selected-files) selected-project (mf/deref refs/dashboard-selected-project) @@ -413,5 +403,6 @@ :dragging? @dragging?}] :else - [:& empty-placeholder {:dragging? @dragging?}])])) + [:& empty-placeholder {:dragging? @dragging? + :default? (:is-default project)}])])) diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 9d8083fc95..a076763648 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -331,10 +331,11 @@ [:div.modal-footer [:div.action-buttons - [:input.cancel-button - {:type "button" - :value (tr "labels.cancel") - :on-click handle-cancel}] + (when (or (= :analyzing (:status @state)) pending-import?) + [:input.cancel-button + {:type "button" + :value (tr "labels.cancel") + :on-click handle-cancel}]) (when (= :analyzing (:status @state)) [:input.accept-button diff --git a/frontend/src/app/main/ui/dashboard/placeholder.cljs b/frontend/src/app/main/ui/dashboard/placeholder.cljs new file mode 100644 index 0000000000..1df3839bdf --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/placeholder.cljs @@ -0,0 +1,34 @@ +;; 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.dashboard.placeholder + (:require + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) + +(mf/defc empty-placeholder + [{:keys [dragging? default?] :as props}] + (cond + (true? dragging?) + [:div.grid-row.no-wrap + [:div.grid-item]] + + (true? default?) + [:div.grid-empty-placeholder.drafts + [:div.text + [:& i18n/tr-html {:label "dashboard.empty-placeholder-drafts"}]]] + + :else + [:div.grid-empty-placeholder + [:img.ph-files {:src "images/ph-file.svg"}]])) + +(mf/defc loading-placeholder + [] + [:div.grid-empty-placeholder + [:div.icon i/loader] + [:div.text (tr "dashboard.loading-files")]]) + diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs index b3654ff591..e1820cc1a8 100644 --- a/frontend/src/app/main/ui/dashboard/project_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs @@ -73,7 +73,6 @@ :accept-label (tr "modals.delete-project-confirm.accept") :on-accept delete-fn})) - file-input (mf/use-ref nil) on-import-files diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index 7089ba7211..76da2bc5e7 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -33,10 +33,8 @@ (tr "dashboard.new-project")]])) (mf/defc project-item - [{:keys [project first? files] :as props}] + [{:keys [project first? team files] :as props}] (let [locale (mf/deref i18n/locale) - - team-id (:team-id project) file-count (or (:count project) 0) dstate (mf/deref refs/dashboard-local) @@ -100,7 +98,8 @@ on-import (mf/use-callback (fn [] - (st/emit! (dd/fetch-recent-files) + (st/emit! (dd/fetch-files {:project-id (:id project)}) + (dd/fetch-recent-files) (dd/clear-selected-files))))] [:div.dashboard-project-row {:class (when first? "first")} @@ -145,9 +144,8 @@ i/actions]] [:& line-grid - {:project-id (:id project) - :project project - :team-id team-id + {:project project + :team team :on-load-more on-nav :files files}]])) @@ -186,7 +184,8 @@ (filterv #(= id (:project-id %))) (sort-by :modified-at #(compare %2 %1))))] [:& project-item {:project project - :files files + :team team + :files files :first? (= project (first projects)) :key (:id project)}]))]]))) diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs index b07e3dd57f..e4ae3e4bc2 100644 --- a/frontend/src/app/main/ui/modal.cljs +++ b/frontend/src/app/main/ui/modal.cljs @@ -72,8 +72,9 @@ #(doseq [key keys] (events/unlistenByKey key))))) - [:div.modal-wrapper {:ref wrapper-ref} - (mf/element (get components (:type data)) (:props data))])) + (when-let [component (get components (:type data))] + [:div.modal-wrapper {:ref wrapper-ref} + (mf/element component (:props data))]))) (def modal-ref diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs index c980f45c09..e5054e915c 100644 --- a/frontend/src/app/main/ui/onboarding.cljs +++ b/frontend/src/app/main/ui/onboarding.cljs @@ -12,8 +12,10 @@ [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.data.users :as du] + [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.forms :as fm] + [app.main.ui.icons :as i] [app.main.ui.releases.common :as rc] [app.main.ui.releases.v1-4] [app.main.ui.releases.v1-5] @@ -21,10 +23,13 @@ [app.main.ui.releases.v1-7] [app.main.ui.releases.v1-8] [app.main.ui.releases.v1-9] + [app.util.dom :as dom] + [app.util.http :as http] [app.util.i18n :as i18n :refer [tr]] [app.util.object :as obj] [app.util.router :as rt] [app.util.timers :as tm] + [beicon.core :as rx] [cljs.spec.alpha :as s] [rumext.alpha :as mf])) @@ -38,10 +43,11 @@ [:div.modal-right [:div.modal-title [:h2 (tr "onboarding.welcome.title")]] - [:span.release "Alpha version " (:main @cf/version)] + [:span.release "Beta version " (:main @cf/version)] [:div.modal-content [:p (tr "onboarding.welcome.desc1")] - [:p (tr "onboarding.welcome.desc2")]] + [:p (tr "onboarding.welcome.desc2")] + [:p (tr "onboarding.welcome.desc3")]] [:div.modal-navigation [:button.btn-secondary {:on-click next} (tr "labels.continue")]]] [:img.deco {:src "images/deco-left.png" :border "0"}] @@ -159,7 +165,7 @@ skip (mf/use-callback (st/emitf (modal/hide) - (modal/show {:type :onboarding-team}) + (modal/show {:type :onboarding-choice}) (du/mark-onboarding-as-viewed)))] (mf/use-layout-effect @@ -187,57 +193,233 @@ (s/def ::team-form (s/keys :req-un [::name])) +(mf/defc onboarding-choice-modal + {::mf/register modal/components + ::mf/register-as :onboarding-choice} + [] + (let [;; When user choices the option of `fly solo`, we proceed to show + ;; the onboarding templates modal. + on-fly-solo + (fn [] + (tm/schedule 400 #(st/emit! (modal/show {:type :onboarding-templates})))) + + ;; When user choices the option of `team up`, we proceed to show + ;; the team creation modal. + on-team-up + (fn [] + (st/emit! (modal/show {:type :onboarding-team}))) + ] + + [:div.modal-overlay + [:div.modal-container.onboarding.final.animated.fadeInUp + [:div.modal-top + [:h1 (tr "onboarding.choice.title")] + [:p (tr "onboarding.choice.desc")]] + [:div.modal-columns + [:div.modal-left + [:div.content-button {:on-click on-fly-solo} + [:h2 (tr "onboarding.choice.fly-solo")] + [:p (tr "onboarding.choice.fly-solo-desc")]]] + [:div.modal-right + [:div.content-button {:on-click on-team-up} + [:h2 (tr "onboarding.choice.team-up")] + [:p (tr "onboarding.choice.team-up-desc")]]]] + [:img.deco {:src "images/deco-left.png" :border "0"}] + [:img.deco.right {:src "images/deco-right.png" :border "0"}]]])) + (mf/defc onboarding-team-modal {::mf/register modal/components ::mf/register-as :onboarding-team} [] - (let [close (mf/use-fn (st/emitf (modal/hide))) - form (fm/use-form :spec ::team-form + (let [form (fm/use-form :spec ::team-form :initial {}) + on-submit + (mf/use-callback + (fn [form _] + (let [tname (get-in @form [:clean-data :name])] + (st/emit! (modal/show {:type :onboarding-team-invitations :name tname})))))] + + [:div.modal-overlay + [:div.modal-container.onboarding-team + [:div.title + [:h2 (tr "onboarding.choice.team-up")] + [:p (tr "onboarding.choice.team-up-desc")]] + + [:& fm/form {:form form + :on-submit on-submit} + + [:div.team-row + [:& fm/input {:type "text" + :name :name + :label (tr "onboarding.team-input-placeholder")}]] + + [:div.buttons + [:button.btn-secondary.btn-large + {:on-click #(st/emit! (modal/show {:type :onboarding-choice}))} + (tr "labels.cancel")] + [:& fm/submit-button + {:label (tr "labels.next")}]]] + + [:img.deco {:src "images/deco-left.png" :border "0"}] + [:img.deco.right {:src "images/deco-right.png" :border "0"}]]])) + +(defn get-available-roles + [] + [{:value "editor" :label (tr "labels.editor")} + {:value "admin" :label (tr "labels.admin")}]) + +(s/def ::email ::us/email) +(s/def ::role ::us/keyword) +(s/def ::invite-form + (s/keys :req-un [::role ::email])) + +;; This is the final step of team creation, consists in provide a +;; shortcut for invite users. + +(mf/defc onboarding-team-invitations-modal + {::mf/register modal/components + ::mf/register-as :onboarding-team-invitations} + [{:keys [name] :as props}] + (let [initial (mf/use-memo (constantly + {:role "editor" + :name name})) + form (fm/use-form :spec ::invite-form + :initial initial) + + roles (mf/use-memo #(get-available-roles)) + on-success (mf/use-callback (fn [_form response] - (st/emit! (modal/hide) - (rt/nav :dashboard-projects {:team-id (:id response)})))) + (let [project-id (:default-project-id response) + team-id (:id response)] + (st/emit! + (modal/hide) + (rt/nav :dashboard-projects {:team-id team-id})) + (tm/schedule 400 #(st/emit! + (modal/show {:type :onboarding-templates + :project-id project-id})))))) on-error (mf/use-callback (fn [_form _response] (st/emit! (dm/error "Error on creating team.")))) - on-submit + ;; The SKIP branch only creates the team, without invitations + on-skip (mf/use-callback - (fn [form _event] + (fn [_] (let [mdata {:on-success (partial on-success form) :on-error (partial on-error form)} - params {:name (get-in @form [:clean-data :name])}] - (st/emit! (dd/create-team (with-meta params mdata))))))] + params {:name name}] + (st/emit! (dd/create-team (with-meta params mdata)))))) + + ;; The SUBMIT branch creates the team with the invitations + on-submit + (mf/use-callback + (fn [form _] + (let [mdata {:on-success (partial on-success form) + :on-error (partial on-error form)} + params (:clean-data @form)] + (st/emit! (dd/create-team-with-invitations (with-meta params mdata))))))] [:div.modal-overlay - [:div.modal-container.onboarding.final.animated.fadeInUp - [:div.modal-left - [:img {:src "images/onboarding-team.jpg" :border "0" :alt (tr "onboarding.team.create.title")}] - [:h2 (tr "onboarding.team.create.title")] - [:p (tr "onboarding.team.create.desc1")] + [:div.modal-container.onboarding-team + [:div.title + [:h2 (tr "onboarding.choice.team-up")] + [:p (tr "onboarding.choice.team-up-desc")]] - [:& fm/form {:form form - :on-submit on-submit} - [:& fm/input {:type "text" - :name :name - :label (tr "onboarding.team.create.input-placeholder")}] + [:& fm/form {:form form + :on-submit on-submit} + + [:div.invite-row + [:& fm/input {:name :email + :label (tr "labels.email")}] + [:& fm/select {:name :role + :options roles}]] + + [:div.buttons + [:button.btn-secondary.btn-large + {:on-click #(st/emit! (modal/show {:type :onboarding-choice}))} + (tr "labels.cancel")] [:& fm/submit-button - {:label (tr "onboarding.team.create.button")}]]] - - [:div.modal-right - [:img {:src "images/onboarding-start.jpg" :border "0" :alt (tr "onboarding.team.start.title")}] - [:h2 (tr "onboarding.team.start.title")] - [:p (tr "onboarding.team.start.desc1")] - [:button.btn-primary.btn-large {:on-click close} (tr "onboarding.team.start.button")]] - - + {:label (tr "labels.create")}]] + [:div.skip-action + {:on-click on-skip} + [:div.action "Skip and invite later"]]] [:img.deco {:src "images/deco-left.png" :border "0"}] [:img.deco.right {:src "images/deco-right.png" :border "0"}]]])) +(mf/defc template-item + [{:keys [name path image project-id]}] + (let [downloading? (mf/use-state false) + link (str (assoc cf/public-uri :path path)) + + on-finish-import + (fn [] + (st/emit! (dd/fetch-files {:project-id project-id}) + (dd/fetch-recent-files) + (dd/clear-selected-files))) + + open-import-modal + (fn [file] + (st/emit! (modal/show + {:type :import + :project-id project-id + :files [file] + :on-finish-import on-finish-import}))) + on-click + (fn [] + (reset! downloading? true) + (->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors}) + (rx/subs (fn [{:keys [body] :as response}] + (open-import-modal {:name name :uri (dom/create-uri body)})) + (fn [error] + (js/console.log "error" error)) + (fn [] + (reset! downloading? false))))) + ] + + [:div.template-item + [:div.template-item-content + [:img {:src image}]] + [:div.template-item-title + [:div.label name] + (if @downloading? + [:div.action "Fetching..."] + [:div.action {:on-click on-click} "+ Add to drafts"])]])) + +(mf/defc onboarding-templates-modal + {::mf/register modal/components + ::mf/register-as :onboarding-templates} + ;; NOTE: the project usually comes empty, it only comes fullfilled + ;; when a user creates a new team just after signup. + [{:keys [project-id] :as props}] + (let [close-fn (mf/use-callback #(st/emit! (modal/hide))) + profile (mf/deref refs/profile) + project-id (or project-id (:default-project-id profile))] + [:div.modal-overlay + [:div.modal-container.onboarding-templates + [:div.modal-header + [:div.modal-close-button + {:on-click close-fn} i/close]] + + [:div.modal-content + [:h3 (tr "onboarding.templates.title")] + [:p (tr "onboarding.templates.subtitle")] + + [:div.templates + [:& template-item + {:path "/github/penpot-files/Penpot-Design-system.penpot" + :image "https://penpot.app/images/libraries/cover-ds-penpot.jpg" + :name "Penpot Design System" + :project-id project-id}] + [:& template-item + {:path "/github/penpot-files/Material-Design-Kit.penpot" + :image "https://penpot.app/images/libraries/cover-material.jpg" + :name "Material Design Kit" + :project-id project-id}]]]]])) + ;;; --- RELEASE NOTES MODAL @@ -299,5 +481,4 @@ (defmethod rc/render-release-notes "0.0" [params] - (rc/render-release-notes (assoc params :version "1.9"))) - + (rc/render-release-notes (assoc params :version "1.10"))) diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs index 0263a4826f..f7107e7188 100644 --- a/frontend/src/app/main/ui/render.cljs +++ b/frontend/src/app/main/ui/render.cljs @@ -16,6 +16,7 @@ [app.main.exports :as exports] [app.main.repo :as repo] [app.main.store :as st] + [app.main.ui.context :as muc] [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.export :as ed] [app.main.ui.shapes.filters :as filters] @@ -124,14 +125,15 @@ ;; 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))}]]))])) + [:& (mf/provider muc/text-plain-colors-ctx) {:value true} + [: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] diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index e9ca5a9669..9cf576dede 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.shapes.attrs (:require [app.common.pages.spec :as spec] + [app.common.types.radius :as ctr] [app.main.ui.context :as muc] [app.util.object :as obj] [app.util.svg :as usvg] @@ -53,7 +54,13 @@ (min r-bottom-left r-left-bottom)])) (defn add-border-radius [attrs shape] - (if (or (:r1 shape) (:r2 shape) (:r3 shape) (:r4 shape)) + (case (ctr/radius-mode shape) + + :radius-1 + (obj/merge! attrs #js {:rx (:rx shape) + :ry (:ry shape)}) + + :radius-4 (let [[r1 r2 r3 r4] (truncate-radius shape) top (- (:width shape) r1 r2) right (- (:height shape) r2 r3) @@ -69,10 +76,7 @@ "v" (- left) " " "a" r1 "," r1 " 0 0 1 " r1 "," (- r1) " " "z")})) - (if (or (:rx shape) (:ry shape)) - (obj/merge! attrs #js {:rx (:rx shape) - :ry (:ry shape)}) - attrs))) + attrs)) (defn add-fill [attrs shape render-id] (let [fill-attrs (cond diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index f204282980..45f4b48bd7 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -242,7 +242,8 @@ :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))}])])) + :penpot:background-overlay ((d/nilf str) (:background-overlay interaction)) + :penpot:preserve-scroll ((d/nilf str) (:preserve-scroll interaction))}])])) (mf/defc export-data [{:keys [shape]}] diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs index 51a34a84fe..6ca3abbead 100644 --- a/frontend/src/app/main/ui/shapes/gradients.cljs +++ b/frontend/src/app/main/ui/shapes/gradients.cljs @@ -10,24 +10,10 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.main.ui.context :as muc] + [app.main.ui.shapes.export :as ed] [app.util.object :as obj] [rumext.alpha :as mf])) -(mf/defc linear-gradient [{:keys [id gradient shape]}] - (let [transform (when (= :path (:type shape)) (gsh/transform-matrix shape nil (gpt/point 0.5 0.5)))] - [:> :linearGradient #js {:id id - :x1 (:start-x gradient) - :y1 (:start-y gradient) - :x2 (:end-x gradient) - :y2 (:end-y gradient) - :gradientTransform transform - :penpot:gradient "true"} - (for [{:keys [offset color opacity]} (:stops gradient)] - [:stop {:key (str id "-stop-" offset) - :offset (or offset 0) - :stop-color color - :stop-opacity opacity}])])) - (defn add-metadata [props gradient] (-> props (obj/set! "penpot:gradient" "true") @@ -38,6 +24,30 @@ (obj/set! "penpot:end-y" (:end-y gradient)) (obj/set! "penpot:width" (:width gradient)))) +(mf/defc linear-gradient [{:keys [id gradient shape]}] + (let [transform (when (= :path (:type shape)) (gsh/transform-matrix shape nil (gpt/point 0.5 0.5))) + base-props #js {:id id + :x1 (:start-x gradient) + :y1 (:start-y gradient) + :x2 (:end-x gradient) + :y2 (:end-y gradient) + :gradientTransform transform} + + include-metadata? (mf/use-ctx ed/include-metadata-ctx) + + props (cond-> base-props + include-metadata? + (add-metadata gradient))] + + [:> :linearGradient props + (for [{:keys [offset color opacity]} (:stops gradient)] + [:stop {:key (str id "-stop-" offset) + :offset (or offset 0) + :stop-color color + :stop-opacity opacity}])])) + + + (mf/defc radial-gradient [{:keys [id gradient shape]}] (let [{:keys [x y width height]} (:selrect shape) transform (if (= :path (:type shape)) @@ -73,7 +83,11 @@ :gradientUnits "userSpaceOnUse" :gradientTransform transform} - props (-> base-props (add-metadata gradient))] + include-metadata? (mf/use-ctx ed/include-metadata-ctx) + + props (cond-> base-props + include-metadata? + (add-metadata gradient))] [:> :radialGradient props (for [{:keys [offset color opacity]} (:stops gradient)] [:stop {:key (str id "-stop-" offset) diff --git a/frontend/src/app/main/ui/shapes/text.cljs b/frontend/src/app/main/ui/shapes/text.cljs index b6e037dd01..88e48e13b7 100644 --- a/frontend/src/app/main/ui/shapes/text.cljs +++ b/frontend/src/app/main/ui/shapes/text.cljs @@ -8,9 +8,12 @@ (:require [app.common.data :as d] [app.common.geom.shapes :as geom] + [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.text.styles :as sts] + [app.util.color :as uc] [app.util.object :as obj] + [cuerdas.core :as str] [rumext.alpha :as mf])) (mf/defc render-text @@ -73,12 +76,111 @@ (obj/set! "key" index))] [:> render-node props]))]))))) +(defn- next-color + "Given a set of colors try to get a color not yet used" + [colors] + (assert (set? colors)) + (loop [current-rgb [0 0 0]] + (let [current-hex (uc/rgb->hex current-rgb)] + (if (contains? colors current-hex) + (recur (uc/next-rgb current-rgb)) + current-hex)))) + +(defn- remap-colors + "Returns a new content replacing the original colors by their mapped 'simple color'" + [content color-mapping] + + (cond-> content + (and (:fill-opacity content) (< (:fill-opacity content) 1.0)) + (-> (assoc :fill-color (get color-mapping [(:fill-color content) (:fill-opacity content)])) + (assoc :fill-opacity 1.0)) + + (some? (:fill-color-gradient content)) + (-> (assoc :fill-color (get color-mapping (:fill-color-gradient content))) + (assoc :fill-opacity 1.0) + (dissoc :fill-color-gradient)) + + (contains? content :children) + (update :children #(mapv (fn [node] (remap-colors node color-mapping)) %)))) + +(defn- fill->color + "Given a content node returns the information about that node fill color" + [{:keys [fill-color fill-opacity fill-color-gradient]}] + + (cond + (some? fill-color-gradient) + {:type :gradient + :gradient fill-color-gradient} + + (and (string? fill-color) (some? fill-opacity) (not= fill-opacity 1)) + {:type :transparent + :hex fill-color + :opacity fill-opacity} + + (string? fill-color) + {:type :solid + :hex fill-color + :map-to fill-color})) + (defn- retrieve-colors + "Given a text shape returns a triple with the values: + - colors used as fills + - a mapping from simple solid colors to complex ones (transparents/gradients) + - the inverse of the previous mapping (to restore the value in the SVG)" [shape] - (let [colors (->> (:content shape) - (tree-seq map? :children) - (into #{"#000000"} (comp (map :fill-color) (filter string?))))] - (apply str (interpose "," colors)))) + (let [color-data + (->> (:content shape) + (tree-seq map? :children) + (map fill->color) + (filter some?)) + + colors (->> color-data + (into #{"#000000"} + (comp (filter #(= :solid (:type %))) + (map :hex)))) + + [colors color-data] + (loop [colors colors + head (first color-data) + tail (rest color-data) + result []] + + (if (nil? head) + [colors result] + + (if (= :solid (:type head)) + (recur colors + (first tail) + (rest tail) + (conj result head)) + + (let [next-color (next-color colors) + head (assoc head :map-to next-color) + colors (conj colors next-color)] + (recur colors + (first tail) + (rest tail) + (conj result head)))))) + + color-mapping-inverse + (->> color-data + (remove #(= :solid (:type %))) + (group-by :map-to) + (d/mapm #(first %2))) + + color-mapping + (merge + (->> color-data + (filter #(= :transparent (:type %))) + (map #(vector [(:hex %) (:opacity %)] (:map-to %))) + (into {})) + + (->> color-data + (filter #(= :gradient (:type %))) + (map #(vector (:gradient %) (:map-to %))) + (into {})))] + + [colors color-mapping color-mapping-inverse])) (mf/defc text-shape {::mf/wrap-props false @@ -88,11 +190,19 @@ grow-type (obj/get props "grow-type") ;; This is only needed in workspace ;; We add 8px to add a padding for the exporter ;; width (+ width 8) - ] + [colors color-mapping color-mapping-inverse] (retrieve-colors shape) + + plain-colors? (mf/use-ctx muc/text-plain-colors-ctx) + + content (cond-> content + plain-colors? + (remap-colors color-mapping))] + [:foreignObject {:x x :y y :id id - :data-colors (retrieve-colors shape) + :data-colors (->> colors (str/join ",")) + :data-mapping (-> color-mapping-inverse (clj->js) (js/JSON.stringify)) :transform (geom/transform-matrix shape) :width (if (#{:auto-width} grow-type) 100000 width) :height (if (#{:auto-height :auto-width} grow-type) 100000 height) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 1ee53dd4d7..263424b68a 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -46,7 +46,7 @@ (defn generate-paragraph-styles [shape data] - (let [line-height (:line-height data) + (let [line-height (:line-height data 1.2) text-align (:text-align data "start") grow-type (:grow-type shape) @@ -60,10 +60,10 @@ (defn generate-text-styles [data] - (let [letter-spacing (:letter-spacing data) + (let [letter-spacing (:letter-spacing data 0) text-decoration (:text-decoration data) text-transform (:text-transform data) - line-height (:line-height data) + line-height (:line-height data 1.2) font-id (:font-id data (:font-id txt/default-text-attrs)) font-variant-id (:font-variant-id data) diff --git a/frontend/src/app/main/ui/share_link.cljs b/frontend/src/app/main/ui/share_link.cljs index f8823ddd6d..1bb520416a 100644 --- a/frontend/src/app/main/ui/share_link.cljs +++ b/frontend/src/app/main/ui/share_link.cljs @@ -127,10 +127,14 @@ [:div.share-link-section [:label (tr "labels.link")] [:div.custom-input.with-icon - [:input {:type "text" :value (or @link "") :read-only true}] - [:div.help-icon {:title (tr "labels.copy") - :on-click copy-link} - i/copy]] + [:input {:type "text" + :value (or @link "") + :placeholder (tr "common.share-link.placeholder") + :read-only true}] + (when (some? @link) + [:div.help-icon {:title (tr "labels.copy") + :on-click copy-link} + i/copy])] [:div.hint (tr "common.share-link.permissions-hint")]]] diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index a17097db43..18d2485be1 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -43,6 +43,8 @@ local (mf/deref refs/viewer-local) + nav-scroll (:nav-scroll local) + page-id (or page-id (-> file :data :pages first)) page (mf/use-memo @@ -91,6 +93,14 @@ (fn [] (events/unlistenByKey key1))))) + (mf/use-layout-effect + (mf/deps nav-scroll) + (fn [] + (when (number? nav-scroll) + (let [viewer-section (dom/get-element "viewer-section")] + (st/emit! (dv/reset-nav-scroll)) + (dom/set-scroll-pos! viewer-section nav-scroll))))) + [:div {:class (dom/classnames :force-visible (:show-thumbnails local) :viewer-layout (not= section :handoff) @@ -110,7 +120,7 @@ :show? (:show-thumbnails local false) :page page :index index}] - [:section.viewer-preview + [:section.viewer-section {:id "viewer-section"} (cond (empty? frames) [:section.empty-state diff --git a/frontend/src/app/main/ui/viewer/comments.cljs b/frontend/src/app/main/ui/viewer/comments.cljs index 09ebfb384c..7d547383d7 100644 --- a/frontend/src/app/main/ui/viewer/comments.cljs +++ b/frontend/src/app/main/ui/viewer/comments.cljs @@ -6,10 +6,8 @@ (ns app.main.ui.viewer.comments (:require - [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.shapes :as geom] [app.main.data.comments :as dcm] [app.main.data.events :as ev] [app.main.refs :as refs] @@ -80,6 +78,7 @@ (mf/defc comments-layer [{:keys [zoom file users frame page] :as props}] (let [profile (mf/deref refs/profile) + threads-map (mf/deref threads-ref) modifier1 (-> (gpt/point (:x frame) (:y frame)) (gpt/negate) @@ -88,16 +87,12 @@ modifier2 (-> (gpt/point (:x frame) (:y frame)) (gmt/translate-matrix)) - threads-map (->> (mf/deref threads-ref) - (d/mapm #(update %2 :position gpt/transform modifier1))) - cstate (mf/deref refs/comments-local) - mframe (geom/transform-shape frame) threads (->> (vals threads-map) (dcm/apply-filters cstate profile) (filter (fn [{:keys [position]}] - (frame-contains? mframe position)))) + (frame-contains? frame position)))) on-bubble-click (mf/use-callback @@ -110,14 +105,15 @@ on-click (mf/use-callback - (mf/deps cstate frame page file) + (mf/deps cstate frame page file zoom) (fn [event] (dom/stop-propagation event) (if (some? (:open cstate)) (st/emit! (dcm/close-thread)) (let [event (.-nativeEvent ^js event) - position (-> (dom/get-offset-position event) - (gpt/transform modifier2)) + viewport-point (dom/get-offset-position event) + viewport-point (-> viewport-point (update :x #(/ % zoom)) (update :y #(/ % zoom))) + position (gpt/transform viewport-point modifier2) params {:position position :page-id (:id page) :file-id (:id file)}] @@ -140,14 +136,16 @@ [:div.viewer-comments-container [:div.threads (for [item threads] - [:& cmt/thread-bubble {:thread item - :zoom zoom - :on-click on-bubble-click - :open? (= (:id item) (:open cstate)) - :key (:seqn item)}]) + (let [item (update item :position gpt/transform modifier1)] + [:& cmt/thread-bubble {:thread item + :zoom zoom + :on-click on-bubble-click + :open? (= (:id item) (:open cstate)) + :key (:seqn item)}])) (when-let [id (:open cstate)] - (when-let [thread (get threads-map id)] + (when-let [thread (-> (get threads-map id) + (update :position gpt/transform modifier1))] [:& cmt/thread-comments {:thread thread :users users :zoom zoom}])) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs index ea7ca7b526..bc8c485898 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.viewer.handoff.attributes.layout (:require [app.common.math :as mth] + [app.common.types.radius :as ctr] [app.main.ui.components.copy-button :refer [copy-button]] [app.util.code-gen :as cg] [app.util.i18n :refer [t]] @@ -58,20 +59,17 @@ [:div.attributes-value (mth/precision y 2) "px"] [:& copy-button {:data (copy-data selrect :y)}]]) - (when (and (:rx shape) (not= (:rx shape) 0)) + (when (ctr/radius-1? shape) [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.layout.radius")] [:div.attributes-value (mth/precision (:rx shape) 2) "px"] [:& copy-button {:data (copy-data shape :rx)}]]) - (when (and (:r1 shape) - (or (not= (:r1 shape) 0) - (not= (:r2 shape) 0) - (not= (:r3 shape) 0) - (not= (:r4 shape) 0))) + (when (ctr/radius-4? shape) [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.layout.radius")] - [:div.attributes-value (mth/precision (:r1 shape) 2) ", " + [:div.attributes-value + (mth/precision (:r1 shape) 2) ", " (mth/precision (:r2 shape) 2) ", " (mth/precision (:r3 shape) 2) ", " (mth/precision (:r4 shape) 2) "px"] diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 2c838251e6..13b3d03537 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -75,31 +75,33 @@ (mf/defc header-sitemap [{:keys [project file page frame index] :as props}] - (let [project-name (:name project) - file-name (:name file) - page-name (:name page) - frame-name (:name frame) - total (count (:frames page)) - - toggle-thumbnails - (fn [] - (st/emit! dv/toggle-thumbnails-panel)) - + (let [project-name (:name project) + file-name (:name file) + page-name (:name page) + frame-name (:name frame) + total (count (:frames page)) show-dropdown? (mf/use-state false) + toggle-thumbnails + (mf/use-callback + (fn [] + (st/emit! dv/toggle-thumbnails-panel))) + open-dropdown - (fn [] - (reset! show-dropdown? true) - (st/emit! dv/close-thumbnails-panel)) + (mf/use-callback + (fn [] + (reset! show-dropdown? true))) close-dropdown - (fn [] - (reset! show-dropdown? false)) + (mf/use-callback + (fn [] + (reset! show-dropdown? false))) navigate-to - (fn [page-id] - (st/emit! (dv/go-to-page page-id)) - (reset! show-dropdown? false))] + (mf/use-callback + (fn [page-id] + (st/emit! (dv/go-to-page page-id)) + (reset! show-dropdown? false)))] [:div.sitemap-zone {:alt (tr "viewer.header.sitemap")} [:div.breadcrumb @@ -108,6 +110,7 @@ [:span "/"] [:span.file-name file-name] [:span "/"] + [:span.page-name page-name] [:span.icon i/arrow-down] diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index dc10b44462..53a574ea02 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -44,7 +44,12 @@ (case (:action-type interaction) :navigate (when-let [frame-id (:destination interaction)] - (st/emit! (dv/go-to-frame frame-id))) + (let [viewer-section (dom/get-element "viewer-section") + scroll (if (:preserve-scroll interaction) + (dom/get-scroll-pos viewer-section) + 0)] + (st/emit! (dv/set-nav-scroll scroll) + (dv/go-to-frame frame-id)))) :open-overlay (let [dest-frame-id (:destination interaction) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index e0688ade1e..6d4d306421 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -9,6 +9,7 @@ [app.main.data.modal :as modal] [app.main.data.workspace.colors :as dc] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.icons :as i] @@ -137,12 +138,13 @@ editing-stop (update-in [:stops editing-stop] merge changes))) (reset! dirty? true))) - handle-click-picker (fn [] - (if picking-color? - (do (modal/disallow-click-outside!) - (st/emit! (dc/stop-picker))) - (do (modal/allow-click-outside!) - (st/emit! (dc/start-picker))))) + handle-click-picker + (fn [] + (if picking-color? + (do (modal/disallow-click-outside!) + (st/emit! (dc/stop-picker))) + (do (modal/allow-click-outside!) + (st/emit! (dc/start-picker))))) handle-change-stop (fn [offset] @@ -307,13 +309,19 @@ (case @active-tab :ramp [:& ramp-selector {:color current-color :disable-opacity disable-opacity - :on-change handle-change-color}] + :on-change handle-change-color + :on-start-drag #(st/emit! (dwu/start-undo-transaction)) + :on-finish-drag #(st/emit! (dwu/commit-undo-transaction))}] :harmony [:& harmony-selector {:color current-color :disable-opacity disable-opacity - :on-change handle-change-color}] + :on-change handle-change-color + :on-start-drag #(st/emit! (dwu/start-undo-transaction)) + :on-finish-drag #(st/emit! (dwu/commit-undo-transaction))}] :hsva [:& hsva-selector {:color current-color :disable-opacity disable-opacity - :on-change handle-change-color}] + :on-change handle-change-color + :on-start-drag #(st/emit! (dwu/start-undo-transaction)) + :on-finish-drag #(st/emit! (dwu/commit-undo-transaction))}] nil)) [:& color-inputs {:type (if (= @active-tab :hsva) :hsv :rgb) @@ -363,7 +371,7 @@ position (or position :left) style (calculate-position vport position x y) - handle-change (fn [new-data _shift-clicked?] + handle-change (fn [new-data] (reset! dirty? (not= data new-data)) (reset! last-change new-data) (when on-change diff --git a/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs b/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs index b886df4f13..ca17d1ede0 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs @@ -56,7 +56,7 @@ y (+ (/ canvas-side 2) (* comp-y (/ canvas-side 2)))] (gpt/point x y))) -(mf/defc harmony-selector [{:keys [color disable-opacity on-change]}] +(mf/defc harmony-selector [{:keys [color disable-opacity on-change on-start-drag on-finish-drag]}] (let [canvas-ref (mf/use-ref nil) {hue :h saturation :s value :v alpha :alpha} color @@ -84,6 +84,24 @@ :h new-hue :s new-saturation}))) + handle-start-drag + (mf/use-callback + (mf/deps on-start-drag) + (fn [event] + (dom/capture-pointer event) + (reset! dragging? true) + (when on-start-drag + (on-start-drag)))) + + handle-stop-drag + (mf/use-callback + (mf/deps on-finish-drag) + (fn [event] + (dom/release-pointer event) + (reset! dragging? false) + (when on-finish-drag + (on-finish-drag)))) + on-change-value (fn [new-value] (let [hex (uc/hsv->hex [hue saturation new-value]) [r g b] (uc/hex->rgb hex)] @@ -112,11 +130,8 @@ {:ref canvas-ref :width canvas-side :height canvas-side - :on-mouse-down #(reset! dragging? true) - :on-mouse-up #(reset! dragging? false) - :on-pointer-down (partial dom/capture-pointer) - :on-lost-pointer-capture #(do (dom/release-pointer %) - (reset! dragging? false)) + :on-pointer-down handle-start-drag + :on-lost-pointer-capture handle-stop-drag :on-click calculate-pos :on-mouse-move #(when @dragging? (calculate-pos %))}] [:div.handler {:style {:pointer-events "none" @@ -133,11 +148,15 @@ :value value :max-value 255 :vertical true - :on-change on-change-value}] + :on-change on-change-value + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] (when (not disable-opacity) [:& slider-selector {:class "opacity" :vertical? true :value alpha :max-value 1 :vertical true - :on-change on-change-opacity}])]])) + :on-change on-change-opacity + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}])]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs index e9523cf09c..4bba38966e 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs @@ -10,7 +10,7 @@ [app.util.color :as uc] [rumext.alpha :as mf])) -(mf/defc hsva-selector [{:keys [color disable-opacity on-change]}] +(mf/defc hsva-selector [{:keys [color disable-opacity on-change on-start-drag on-finish-drag]}] (let [{hue :h saturation :s value :v alpha :alpha} color handle-change-slider (fn [key] (fn [new-value] @@ -25,18 +25,39 @@ [:div.hsva-selector [:span.hsva-selector-label "H"] [:& slider-selector - {:class "hue" :max-value 360 :value hue :on-change (handle-change-slider :h)}] + {:class "hue" + :max-value 360 + :value hue + :on-change (handle-change-slider :h) + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] [:span.hsva-selector-label "S"] [:& slider-selector - {:class "saturation" :max-value 1 :value saturation :on-change (handle-change-slider :s)}] + {:class "saturation" + :max-value 1 + :value saturation + :on-change (handle-change-slider :s) + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] [:span.hsva-selector-label "V"] [:& slider-selector - {:class "value" :reverse? true :max-value 255 :value value :on-change (handle-change-slider :v)}] + {:class "value" + :reverse? true + :max-value 255 + :value value + :on-change (handle-change-slider :v) + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] (when (not disable-opacity) [:* [:span.hsva-selector-label "A"] [:& slider-selector - {:class "opacity" :max-value 1 :value alpha :on-change on-change-opacity}]])])) + {:class "opacity" + :max-value 1 + :value alpha + :on-change on-change-opacity + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}]])])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs index 8fbd1e5344..e9bd1d91d5 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs @@ -13,7 +13,7 @@ [app.util.dom :as dom] [rumext.alpha :as mf])) -(mf/defc value-saturation-selector [{:keys [saturation value on-change]}] +(mf/defc value-saturation-selector [{:keys [saturation value on-change on-start-drag on-finish-drag]}] (let [dragging? (mf/use-state false) calculate-pos (fn [ev] @@ -21,13 +21,27 @@ {:keys [x y]} (-> ev dom/get-client-position) px (math/clamp (/ (- x left) (- right left)) 0 1) py (* 255 (- 1 (math/clamp (/ (- y top) (- bottom top)) 0 1)))] - (on-change px py)))] + (on-change px py))) + + handle-start-drag + (mf/use-callback + (mf/deps on-start-drag) + (fn [event] + (dom/capture-pointer event) + (reset! dragging? true) + (on-start-drag))) + + handle-stop-drag + (mf/use-callback + (mf/deps on-finish-drag) + (fn [event] + (dom/release-pointer event) + (reset! dragging? false) + (on-finish-drag))) + ] [:div.value-saturation-selector - {:on-mouse-down #(reset! dragging? true) - :on-mouse-up #(reset! dragging? false) - :on-pointer-down (partial dom/capture-pointer) - :on-lost-pointer-capture #(do (dom/release-pointer %) - (reset! dragging? false)) + {:on-pointer-down handle-start-drag + :on-lost-pointer-capture handle-stop-drag :on-click calculate-pos :on-mouse-move #(when @dragging? (calculate-pos %))} [:div.handler {:style {:pointer-events "none" @@ -35,7 +49,7 @@ :top (str (* 100 (- 1 (/ value 255))) "%")}}]])) -(mf/defc ramp-selector [{:keys [color disable-opacity on-change]}] +(mf/defc ramp-selector [{:keys [color disable-opacity on-change on-start-drag on-finish-drag]}] (let [{hex :hex hue :h saturation :s value :v alpha :alpha} color @@ -64,7 +78,9 @@ {:hue hue :saturation saturation :value value - :on-change on-change-value-saturation}] + :on-change on-change-value-saturation + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] [:div.shade-selector [:& color-bullet {:color {:color hex @@ -72,10 +88,14 @@ [:& slider-selector {:class "hue" :max-value 360 :value hue - :on-change on-change-hue}] + :on-change on-change-hue + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] (when (not disable-opacity) [:& slider-selector {:class "opacity" :max-value 1 :value alpha - :on-change on-change-opacity}])]])) + :on-change on-change-opacity + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}])]])) 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 4518decda7..5e72584c4c 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs @@ -12,10 +12,29 @@ [rumext.alpha :as mf])) (mf/defc slider-selector - [{:keys [value class min-value max-value vertical? reverse? on-change]}] + [{:keys [value class min-value max-value vertical? reverse? on-change on-start-drag on-finish-drag]}] (let [min-value (or min-value 0) max-value (or max-value 1) dragging? (mf/use-state false) + + handle-start-drag + (mf/use-callback + (mf/deps on-start-drag) + (fn [event] + (dom/capture-pointer event) + (reset! dragging? true) + (when on-start-drag + (on-start-drag)))) + + handle-stop-drag + (mf/use-callback + (mf/deps on-finish-drag) + (fn [event] + (dom/release-pointer event) + (reset! dragging? false) + (when on-finish-drag + (on-finish-drag)))) + calculate-pos (fn [ev] (when on-change @@ -32,11 +51,8 @@ [:div.slider-selector {:class (str (if vertical? "vertical " "") class) - :on-mouse-down #(reset! dragging? true) - :on-mouse-up #(reset! dragging? false) - :on-pointer-down (partial dom/capture-pointer) - :on-lost-pointer-capture #(do (dom/release-pointer %) - (reset! dragging? false)) + :on-pointer-down handle-start-drag + :on-lost-pointer-capture handle-stop-drag :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 9cdae04669..abe4ed6062 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -281,7 +281,8 @@ [:& menu-entry {:title (tr "workspace.shape.menu.delete-flow-start") :on-click (do-remove-flow flow)}]))) - (when (not= (:type shape) :frame) + (when (and (not= (:type shape) :frame) + (or multiple? (nil? (:component-id shape)))) [:* [:& menu-separator] [:& menu-entry {:title (tr "workspace.shape.menu.create-component") 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 index 80b2be8527..8476844471 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.sidebar.options.menus.booleans (:require [app.main.data.workspace :as dw] + [app.main.data.workspace.shortcuts :as sc] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.icons :as i] @@ -51,28 +52,28 @@ [:div.align-options [:div.align-group [:div.align-button.tooltip.tooltip-bottom - {:alt (tr "workspace.shape.menu.union") + {:alt (str (tr "workspace.shape.menu.union") " (" (sc/get-tooltip :boolean-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") + {:alt (str (tr "workspace.shape.menu.difference") " (" (sc/get-tooltip :boolean-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") + {:alt (str (tr "workspace.shape.menu.intersection") " (" (sc/get-tooltip :boolean-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") + {:alt (str (tr "workspace.shape.menu.exclude") " (" (sc/get-tooltip :boolean-exclude) ")") :class (dom/classnames :disabled disabled-bool-btns :selected (= head-bool-type :exclude)) :on-click (set-bool :exclude)} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs index 7e65db893e..9dbe80aca5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.main.data.messages :as dm] [app.main.data.workspace :as udw] + [app.main.data.workspace.persistence :as dwp] [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.icons :as i] @@ -19,12 +20,15 @@ (defn request-export [shape exports] - (rp/query! :export - {:page-id (:page-id shape) - :file-id (:file-id shape) - :object-id (:id shape) - :name (:name shape) - :exports exports})) + ;; Force a persist before exporting otherwise the exported shape could be outdated + (st/emit! ::dwp/force-persist) + (rp/query! + :export + {:page-id (:page-id shape) + :file-id (:file-id shape) + :object-id (:id shape) + :name (:name shape) + :exports exports})) (mf/defc exports-menu [{:keys [shape page-id file-id] :as props}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index c13fc2b9a7..e1d0ee4b40 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -8,7 +8,6 @@ (:require [app.common.pages :as cp] [app.main.data.workspace.colors :as dc] - [app.main.data.workspace.undo :as dwu] [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] @@ -70,17 +69,7 @@ (assoc :id nil :file-id nil))] (st/emit! (dc/change-fill ids color))))) - on-open-picker - (mf/use-callback - (mf/deps ids) - (fn [_value _opacity _id _file-id] - (st/emit! (dwu/start-undo-transaction)))) - - on-close-picker - (mf/use-callback - (mf/deps ids) - (fn [_value _opacity _id _file-id] - (st/emit! (dwu/commit-undo-transaction))))] + ] (if show? [:div.element-set @@ -90,10 +79,9 @@ [:div.element-set-content [:& color-row {:color color + :title (tr "workspace.options.fill") :on-change on-change - :on-detach on-detach - :on-open on-open-picker - :on-close on-close-picker}]]] + :on-detach on-detach}]]] [:div.element-set [:div.element-set-title diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs index 8aae413444..e5a4cbb29f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs @@ -130,7 +130,7 @@ :on-change handle-change-type}] (if (= type :square) - [:div.input-element.pixels + [:div.input-element.pixels {:title (tr "workspace.options.size")} [:> numeric-input {:min 1 :no-validate true :value (:size params) @@ -214,6 +214,7 @@ :on-change (handle-change :params :margin)}]]) [:& color-row {:color (:color params) + :title (tr "workspace.options.grid.params.color") :disable-gradient true :on-change handle-change-color :on-detach handle-detach-color}] 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 c4e86847e2..53a14ae5f8 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 @@ -169,6 +169,7 @@ overlay-pos-type (:overlay-pos-type interaction) close-click-outside? (:close-click-outside interaction false) background-overlay? (:background-overlay interaction false) + preserve-scroll? (:preserve-scroll interaction false) extended-open? (mf/use-state false) @@ -197,6 +198,11 @@ value (when (not= value "") (uuid/uuid value))] (update-interaction index #(cti/set-destination % value)))) + change-preserve-scroll + (fn [event] + (let [value (-> event dom/get-target dom/checked?)] + (update-interaction index #(cti/set-preserve-scroll % value)))) + change-url (fn [event] (let [target (dom/get-target event) @@ -264,11 +270,12 @@ (when (cti/has-delay interaction) [:div.interactions-element [:span.element-set-subtitle.wide (tr "workspace.options.interaction-delay")] - [:div.input-element + [:div.input-element {:title (tr "workspace.options.interaction-ms")} [:> numeric-input {:ref ext-delay-ref :on-click (select-text ext-delay-ref) :on-change change-delay - :value (:delay interaction)}] + :value (:delay interaction) + :title (tr "workspace.options.interaction-ms")}] [:span.after (tr "workspace.options.interaction-ms")]]]) ; Action select @@ -295,6 +302,17 @@ (not= (:id frame) (:frame-id shape))) ; nor a shape to its container frame [:option {:value (str (:id frame))} (:name frame)]))]]) + ; Preserve scroll + (when (cti/has-preserve-scroll interaction) + [:div.interactions-element + [:div.input-checkbox + [:input {:type "checkbox" + :id (str "preserve-" index) + :checked preserve-scroll? + :on-change change-preserve-scroll}] + [:label {:for (str "preserve-" index)} + (tr "workspace.options.interaction-preserve-scroll")]]]) + ; URL (when (cti/has-url interaction) [:div.interactions-element diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs index 68f2e86650..cbcbaf7d83 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs @@ -112,8 +112,7 @@ [:option {:value "color"} (tr "workspace.options.layer-options.blend-mode.color")] [:option {:value "luminosity"} (tr "workspace.options.layer-options.blend-mode.luminosity")]] - [:div.input-element - {:class "percentail"} + [:div.input-element {:title (tr "workspace.options.opacity") :class "percentail"} [:> numeric-input {:value (-> values :opacity opacity->string) :placeholder (tr "settings.multiple") :on-click select-all diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index 9305404f56..73e29eae9b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.math :as math] + [app.common.types.radius :as ctr] [app.main.data.workspace :as udw] [app.main.data.workspace.changes :as dch] [app.main.refs :as refs] @@ -61,6 +62,11 @@ proportion-lock (:proportion-lock values) + radius-mode (ctr/radius-mode values) + all-equal? (ctr/all-equal? values) + radius-multi? (mf/use-state nil) + radius-input-ref (mf/use-ref nil) + on-size-change (mf/use-callback (mf/deps ids) @@ -97,57 +103,38 @@ (mf/use-callback (mf/deps ids) (fn [_value] - (let [radius-update - (fn [shape] - (cond-> shape - (:r1 shape) - (-> (assoc :rx 0 :ry 0) - (dissoc :r1 :r2 :r3 :r4))))] - (st/emit! (dch/update-shapes ids-with-children radius-update))))) + (if all-equal? + (st/emit! (dch/update-shapes ids-with-children ctr/switch-to-radius-1)) + (reset! radius-multi? true)))) on-switch-to-radius-4 (mf/use-callback (mf/deps ids) (fn [_value] - (let [radius-update - (fn [shape] - (cond-> shape - (:rx shape) - (-> (assoc :r1 0 :r2 0 :r3 0 :r4 0) - (dissoc :rx :ry))))] - (st/emit! (dch/update-shapes ids-with-children radius-update))))) + (st/emit! (dch/update-shapes ids-with-children ctr/switch-to-radius-4)) + (reset! radius-multi? false))) on-radius-1-change (mf/use-callback (mf/deps ids) (fn [value] - (let [radius-update - (fn [shape] - (cond-> shape - (:r1 shape) - (-> (dissoc :r1 :r2 :r3 :r4) - (assoc :rx 0 :ry 0)) + (st/emit! (dch/update-shapes ids-with-children #(ctr/set-radius-1 % value))))) - (or (:rx shape) (:r1 shape)) - (assoc :rx value :ry value)))] - - (st/emit! (dch/update-shapes ids-with-children radius-update))))) + on-radius-multi-change + (mf/use-callback + (mf/deps ids) + (fn [event] + (let [value (-> event dom/get-target dom/get-value d/parse-integer)] + (when (some? value) + (st/emit! (dch/update-shapes ids-with-children ctr/switch-to-radius-1) + (dch/update-shapes ids-with-children #(ctr/set-radius-1 % value))) + (reset! radius-multi? false))))) on-radius-4-change (mf/use-callback (mf/deps ids) (fn [value attr] - (let [radius-update - (fn [shape] - (cond-> shape - (:rx shape) - (-> (dissoc :rx :rx) - (assoc :r1 0 :r2 0 :r3 0 :r4 0)) - - (attr shape) - (assoc attr value)))] - - (st/emit! (dch/update-shapes ids-with-children radius-update))))) + (st/emit! (dch/update-shapes ids-with-children #(ctr/set-radius-4 % attr value))))) on-width-change #(on-size-change % :width) on-height-change #(on-size-change % :height) @@ -160,6 +147,16 @@ select-all #(-> % (dom/get-target) (.select))] + (mf/use-layout-effect + (mf/deps radius-mode @radius-multi?) + (fn [] + (when (and (= radius-mode :radius-1) + (= @radius-multi? false)) + ;; when going back from radius-multi to normal radius-1, + ;; restore focus to the newly created numeric-input + (let [radius-input (mf/ref-val radius-input-ref)] + (dom/focus! radius-input))))) + [:* [:div.element-set [:div.element-set-content @@ -168,7 +165,7 @@ (when (options :size) [:div.row-flex [:span.element-set-subtitle (tr "workspace.options.size")] - [:div.input-element.width + [:div.input-element.width {:title (tr "workspace.options.width")} [:> numeric-input {:min 1 :no-validate true :placeholder "--" @@ -176,7 +173,7 @@ :on-change on-width-change :value (attr->string :width values)}]] - [:div.input-element.height + [:div.input-element.height {:title (tr "workspace.options.height")} [:> numeric-input {:min 1 :no-validate true :placeholder "--" @@ -196,13 +193,13 @@ (when (options :position) [:div.row-flex [:span.element-set-subtitle (tr "workspace.options.position")] - [:div.input-element.Xaxis + [:div.input-element.Xaxis {:title (tr "workspace.options.x")} [:> numeric-input {:no-validate true :placeholder "--" :on-click select-all :on-change on-pos-x-change :value (attr->string :x values)}]] - [:div.input-element.Yaxis + [:div.input-element.Yaxis {:title (tr "workspace.options.y")} [:> numeric-input {:no-validate true :placeholder "--" :on-click select-all @@ -213,7 +210,7 @@ (when (options :rotation) [:div.row-flex [:span.element-set-subtitle (tr "workspace.options.rotation")] - [:div.input-element.degrees + [:div.input-element.degrees {:title (tr "workspace.options.rotation")} [:> numeric-input {:no-validate true :min 0 @@ -233,60 +230,72 @@ :value (attr->string :rotation values)}]]) ;; RADIUS - (let [radius-1? (some? (:rx values)) - radius-4? (some? (:r1 values))] - (when (and (options :radius) (or radius-1? radius-4?)) - [:div.row-flex - [:div.radius-options - [:div.radius-icon.tooltip.tooltip-bottom - {:class (dom/classnames - :selected - (and radius-1? (not radius-4?))) - :alt (tr "workspace.options.radius.all-corners") - :on-click on-switch-to-radius-1} - i/radius-1] - [:div.radius-icon.tooltip.tooltip-bottom - {:class (dom/classnames - :selected - (and radius-4? (not radius-1?))) - :alt (tr "workspace.options.radius.single-corners") - :on-click on-switch-to-radius-4} - i/radius-4]] - (if radius-1? - [:div.input-element.mini - [:> numeric-input - {:placeholder "--" - :min 0 - :on-click select-all - :on-change on-radius-1-change - :value (attr->string :rx values)}]] + (when (and (options :radius) (some? radius-mode)) + [:div.row-flex + [:div.radius-options + [:div.radius-icon.tooltip.tooltip-bottom + {:class (dom/classnames + :selected (or (= radius-mode :radius-1) @radius-multi?)) + :alt (tr "workspace.options.radius.all-corners") + :on-click on-switch-to-radius-1} + i/radius-1] + [:div.radius-icon.tooltip.tooltip-bottom + {:class (dom/classnames + :selected (and (= radius-mode :radius-4) (not @radius-multi?))) + :alt (tr "workspace.options.radius.single-corners") + :on-click on-switch-to-radius-4} + i/radius-4]] - [:* - [:div.input-element.mini - [:> numeric-input - {:placeholder "--" - :min 0 - :on-click select-all - :on-change on-radius-r1-change - :value (attr->string :r1 values)}]] - [:div.input-element.mini - [:> numeric-input - {:placeholder "--" - :min 0 - :on-click select-all - :on-change on-radius-r2-change - :value (attr->string :r2 values)}]] - [:div.input-element.mini - [:> numeric-input - {:placeholder "--" - :min 0 - :on-click select-all - :on-change on-radius-r3-change - :value (attr->string :r3 values)}]] - [:div.input-element.mini - [:> numeric-input - {:placeholder "--" - :min 0 - :on-click select-all - :on-change on-radius-r4-change - :value (attr->string :r4 values)}]]])]))]]])) + (cond + (= radius-mode :radius-1) + [:div.input-element.mini {:title (tr "workspace.options.radius")} + [:> numeric-input + {:placeholder "--" + :ref radius-input-ref + :min 0 + :on-click select-all + :on-change on-radius-1-change + :value (attr->string :rx values)}]] + + @radius-multi? + [:div.input-element.mini {:title (tr "workspace.options.radius")} + [:input.input-text + {:type "number" + :placeholder "--" + :on-click select-all + :on-change on-radius-multi-change + :value ""}]] + + (= radius-mode :radius-4) + [:* + [:div.input-element.mini {:title (tr "workspace.options.radius")} + [:> numeric-input + {:placeholder "--" + :min 0 + :on-click select-all + :on-change on-radius-r1-change + :value (attr->string :r1 values)}]] + + [:div.input-element.mini {:title (tr "workspace.options.radius")} + [:> numeric-input + {:placeholder "--" + :min 0 + :on-click select-all + :on-change on-radius-r2-change + :value (attr->string :r2 values)}]] + + [:div.input-element.mini {:title (tr "workspace.options.radius")} + [:> numeric-input + {:placeholder "--" + :min 0 + :on-click select-all + :on-change on-radius-r3-change + :value (attr->string :r3 values)}]] + + [:div.input-element.mini {:title (tr "workspace.options.radius")} + [:> numeric-input + {:placeholder "--" + :min 0 + :on-click select-all + :on-change on-radius-r4-change + :value (attr->string :r4 values)}]]])])]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs index ee75d4cfcc..c03706b7e0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs @@ -146,7 +146,7 @@ [:option {:value ":inner-shadow"} (tr "workspace.options.shadow-options.inner-shadow")]]] [:div.row-grid-2 - [:div.input-element + [:div.input-element {:title (tr "workspace.options.shadow-options.offsetx")} [:> numeric-input {:ref adv-offset-x-ref :no-validate true :placeholder "--" @@ -155,7 +155,7 @@ :value (:offset-x value)}] [:span.after (tr "workspace.options.shadow-options.offsetx")]] - [:div.input-element + [:div.input-element {:title (tr "workspace.options.shadow-options.offsety")} [:> numeric-input {:ref adv-offset-y-ref :no-validate true :placeholder "--" @@ -165,7 +165,7 @@ [:span.after (tr "workspace.options.shadow-options.offsety")]]] [:div.row-grid-2 - [:div.input-element + [:div.input-element {:title (tr "workspace.options.shadow-options.blur")} [:> numeric-input {:ref adv-blur-ref :no-validate true :placeholder "--" @@ -175,7 +175,7 @@ :value (:blur value)}] [:span.after (tr "workspace.options.shadow-options.blur")]] - [:div.input-element + [:div.input-element {:title (tr "workspace.options.shadow-options.spread")} [:> numeric-input {:ref adv-spread-ref :no-validate true :placeholder "--" @@ -190,6 +190,7 @@ ;; Support for old format colors {:color (:color value) :opacity (:opacity value)} (:color value)) + :title (tr "workspace.options.shadow-options.color") :disable-gradient true :on-change (update-color index) :on-detach (detach-color index) 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 f0bd78126b..ee9560e624 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 @@ -11,7 +11,6 @@ [app.common.pages.spec :as spec] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.colors :as dc] - [app.main.data.workspace.undo :as dwu] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.icons :as i] @@ -188,19 +187,7 @@ on-del-stroke (fn [_] - (st/emit! (dch/update-shapes ids #(assoc % :stroke-style :none)))) - - on-open-picker - (mf/use-callback - (mf/deps ids) - (fn [_value _opacity _id _file-id] - (st/emit! (dwu/start-undo-transaction)))) - - on-close-picker - (mf/use-callback - (mf/deps ids) - (fn [_value _opacity _id _file-id] - (st/emit! (dwu/commit-undo-transaction))))] + (st/emit! (dch/update-shapes ids #(assoc % :stroke-style :none))))] (if show-options [:div.element-set @@ -211,15 +198,15 @@ [:div.element-set-content ;; Stroke Color [:& color-row {:color current-stroke-color + :title (tr "workspace.options.stroke-color") :on-change handle-change-stroke-color - :on-detach handle-detach - :on-open on-open-picker - :on-close on-close-picker}] + :on-detach handle-detach}] ;; Stroke Width, Alignment & Style [:div.row-flex [:div.input-element - {:class (dom/classnames :pixels (not= (:stroke-width values) :multiple))} + {:class (dom/classnames :pixels (not= (:stroke-width values) :multiple)) + :title (tr "workspace.options.stroke-width")} [:input.input-text {:type "number" :min "0" :value (-> (:stroke-width values) width->string) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs index a5859e682f..42115b0b2b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs @@ -37,6 +37,7 @@ [:div.element-set-content [:& color-row {:disable-gradient true :disable-opacity true + :title (tr "workspace.options.canvas-background") :color {:color (get options :background "#E8E9EA") :opacity 1} :on-change on-change diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index af5a3e81ad..b151b7e156 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -61,7 +61,7 @@ (if (= v :multiple) nil v)) (mf/defc color-row - [{:keys [color disable-gradient disable-opacity on-change on-detach on-open on-close]}] + [{:keys [color disable-gradient disable-opacity on-change on-detach on-open on-close title]}] (let [current-file-id (mf/use-ctx ctx/current-file-id) file-colors (mf/deref refs/workspace-file-colors) shared-libs (mf/deref refs/workspace-libraries) @@ -123,7 +123,7 @@ (when (not= prev-color color) (modal/update-props! :colorpicker {:data (parse-color color)})))) - [:div.row-flex.color-data + [:div.row-flex.color-data {:title title} [:& cb/color-bullet {:color color :on-click handle-click-color}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs index c3f5d8a6d3..55435c4e77 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs @@ -87,7 +87,7 @@ ;; WIDTH & HEIGHT [:div.row-flex [:span.element-set-subtitle (tr "workspace.options.size")] - [:div.input-element.pixels + [:div.input-element.pixels {:title (tr "workspace.options.width")} [:> numeric-input {:min 1 :on-click select-all :on-change on-width-change @@ -95,7 +95,7 @@ (math/precision 2) (d/coalesce-str "1"))}]] - [:div.input-element.pixels + [:div.input-element.pixels {:title (tr "workspace.options.height")} [:> numeric-input {:min 1 :on-click select-all :on-change on-height-change @@ -112,14 +112,14 @@ ;; POSITION [:div.row-flex [:span.element-set-subtitle (tr "workspace.options.position")] - [:div.input-element.pixels + [:div.input-element.pixels {:title (tr "workspace.options.x")} [:> numeric-input {:placeholder "x" :on-click select-all :on-change on-pos-x-change :value (-> (:x shape) (math/precision 2) (d/coalesce-str "0"))}]] - [:div.input-element.pixels + [:div.input-element.pixels {:title (tr "workspace.options.y")} [:> numeric-input {:placeholder "y" :on-click select-all :on-change on-pos-y-change diff --git a/frontend/src/app/util/color.cljs b/frontend/src/app/util/color.cljs index 3c540dc3e8..407b339b8b 100644 --- a/frontend/src/app/util/color.cljs +++ b/frontend/src/app/util/color.cljs @@ -7,6 +7,7 @@ (ns app.util.color "Color conversion utils." (:require + [app.common.exceptions :as ex] [app.util.object :as obj] [cuerdas.core :as str] [goog.color :as gcolor])) @@ -155,3 +156,19 @@ (def empty-color (into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity])) + +(defn next-rgb + "Given a color in rgb returns the next color" + [[r g b]] + (cond + (and (= 255 r) (= 255 g) (= 255 b)) + (ex/raise "Cannot get next color") + + (and (= 255 g) (= 255 b)) + [(inc r) 0 0] + + (= 255 b) + [r (inc g) 0] + + :else + [r g (inc b)])) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 309a04b6e5..82a94e0ed6 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -277,7 +277,8 @@ (-> event get-target (.setPointerCapture (.-pointerId event)))) (defn release-pointer [event] - (-> event get-target (.releasePointerCapture (.-pointerId event)))) + (when (.-pointerId event) + (-> event get-target (.releasePointerCapture (.-pointerId event))))) (defn get-root [] (query globals/document "#app")) @@ -339,6 +340,14 @@ (defn remove-attribute [^js node ^string attr] (.removeAttribute node attr)) +(defn get-scroll-pos + [element] + (.-scrollTop ^js element)) + +(defn set-scroll-pos! + [element scroll] + (obj/set! ^js element "scrollTop" scroll)) + (defn scroll-into-view! ([element] (.scrollIntoView ^js element false)) diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index 552ca311ba..85e976167a 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -763,7 +763,8 @@ (assoc :delay (get-meta node :delay d/parse-double)) (cti/has-destination interaction) - (assoc :destination (get-meta node :destination uuid/uuid)) + (assoc :destination (get-meta node :destination uuid/uuid) + :preserve-scroll (get-meta node :preserve-scroll str->bool)) (cti/has-url interaction) (assoc :url (get-meta node :url str)) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index d36ec13a61..04c9521f3a 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -169,7 +169,7 @@ (rx/tap #(do (swap! current inc) (progress! context :upload-data @current total)))))) - + (rx/map first) (rx/tap #(reset! revn (:revn %))) (rx/ignore)) diff --git a/frontend/translations/ca.po b/frontend/translations/ca.po index 8d59bd3bef..827ae5c34b 100644 --- a/frontend/translations/ca.po +++ b/frontend/translations/ca.po @@ -1630,17 +1630,6 @@ 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ó" diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 53dc3c6df3..c1b88c952f 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -1620,17 +1620,6 @@ 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" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 691cc14e63..87c089241e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -196,6 +196,9 @@ msgstr "Can view" msgid "common.share-link.permissions-hint" msgstr "Anyone with link will have access" +msgid "common.share-link.placeholder" +msgstr "Shareable link will appear here" + msgid "common.share-link.remove-link" msgstr "Remove link" @@ -250,6 +253,14 @@ msgstr "Duplicate %s files" msgid "dashboard.empty-files" msgstr "You still have no files here" +#: src/app/main/ui/dashboard/grid.cljs +#, markdown +msgid "dashboard.empty-placeholder-drafts" +msgstr "" +"There are no files here yet. If you want to try some templates you can go " +"to the [Libraries & templates " +"section](https://penpot.app/libraries-templates.html)" + msgid "dashboard.export-frames" msgstr "Export artboards to PDF..." @@ -1104,6 +1115,9 @@ msgstr "Name" msgid "labels.new-password" msgstr "New password" +msgid "labels.next" +msgstr "Next" + #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs msgid "labels.no-comments-available" msgstr "You have no pending comment notifications" @@ -1533,6 +1547,26 @@ msgstr "Profile saved successfully!" msgid "notifications.validation-email-sent" msgstr "Verification email sent to %s. Check your email!" +msgid "onboarding.choice.desc" +msgstr "How do you want to start?" + +msgid "onboarding.choice.fly-solo" +msgstr "Fly solo" + +msgid "onboarding.choice.fly-solo-desc" +msgstr "Jump away into Penpot and start designing by your own." + +msgid "onboarding.choice.team-up" +msgstr "Team up" + +msgid "onboarding.choice.team-up-desc" +msgstr "" +"Are you working with someone? Create a team and invite people to work " +"together on projects and share design assets." + +msgid "onboarding.choice.title" +msgstr "Welcome to Penpot" + msgid "onboarding.contrib.alt" msgstr "Open Source" @@ -1606,44 +1640,37 @@ msgstr "" msgid "onboarding.slide.3.title" msgstr "One shared source of truth" -msgid "onboarding.team.create.button" -msgstr "Create a team" - -msgid "onboarding.team.create.desc1" -msgstr "" -"Are you working with someone? Create a team to work together on projects " -"and share design assets." - -msgid "onboarding.team.create.input-placeholder" +msgid "onboarding.team-input-placeholder" msgstr "Enter new team name" -msgid "onboarding.team.create.title" -msgstr "Create team" +msgid "onboarding.team.skip-and-invite-later" +msgstr "Skip and invite later" -msgid "onboarding.team.start.button" -msgstr "Start right away" +msgid "onboarding.templates.subtitle" +msgstr "Here are some templates." -msgid "onboarding.team.start.desc1" -msgstr "" -"Jump right away into Penpot and start designing by your own. You will still " -"have the chance to create teams later." - -msgid "onboarding.team.start.title" +msgid "onboarding.templates.title" msgstr "Start designing" msgid "onboarding.welcome.alt" msgstr "Penpot" msgid "onboarding.welcome.desc1" -msgstr "We are very happy to introduce you to the very first Alpha release." +msgstr "Hooray! You are already a Penpot user :)" msgid "onboarding.welcome.desc2" msgstr "" -"Penpot is still at development stage and there will be constant updates. We " -"hope you enjoy the first stable version." +"Penpot is in its first beta version thanks to the combination of core " +"features, maturity, stability and the amazing validation from the community " +"as a whole, to which you are more than welcome." + +msgid "onboarding.welcome.desc3" +msgstr "" +"While you enjoy Penpot for what it is we will keep improving it, releasing " +"iterations of our hopeful plans." msgid "onboarding.welcome.title" -msgstr "Welcome to Penpot!" +msgstr "Welcome to Penpot" #: src/app/main/ui/auth/recovery.cljs msgid "profile.recovery.go-to-login" @@ -2265,6 +2292,9 @@ msgstr "Auto" msgid "workspace.options.grid.column" msgstr "Columns" +msgid "workspace.options.grid.params.color" +msgstr "Color" + #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.columns" msgstr "Columns" @@ -2349,6 +2379,9 @@ msgstr "Group fill" msgid "workspace.options.group-stroke" msgstr "Group stroke" +msgid "workspace.options.height" +msgstr "Height" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-action" msgstr "Action" @@ -2457,6 +2490,10 @@ msgstr "Top right" msgid "workspace.options.interaction-position" msgstr "Position" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-preserve-scroll" +msgstr "Preserve scroll position" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-prev-screen" msgstr "Previous screen" @@ -2569,6 +2606,9 @@ msgstr "Group layers" msgid "workspace.options.layer-options.title.multiple" msgstr "Selected layers" +msgid "workspace.options.opacity" +msgstr "Opacity" + #: 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" @@ -2615,6 +2655,9 @@ msgstr "Selection stroke" msgid "workspace.options.shadow-options.blur" msgstr "Blur" +msgid "workspace.options.shadow-options.color" +msgstr "Shadow color" + #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.drop-shadow" msgstr "Drop shadow" @@ -2691,6 +2734,12 @@ msgstr "Square marker" msgid "workspace.options.stroke-cap.triangle-arrow" msgstr "Triangle arrow" +msgid "workspace.options.stroke-color" +msgstr "Stroke color" + +msgid "workspace.options.stroke-width" +msgstr "Stroke width" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke.center" msgstr "Center" @@ -2832,6 +2881,15 @@ msgstr "Vertical align" msgid "workspace.options.use-play-button" msgstr "Use the play button at the header to run the prototype view." +msgid "workspace.options.width" +msgstr "Width" + +msgid "workspace.options.x" +msgstr "X" + +msgid "workspace.options.y" +msgstr "Y" + msgid "workspace.path.actions.add-node" msgstr "Add node (%s)" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index a7a3afe5f9..1ac121f92e 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -200,6 +200,9 @@ msgstr "Puede ver" msgid "common.share-link.permissions-hint" msgstr "Cualquiera con el enlace puede acceder" +msgid "common.share-link.placeholder" +msgstr "El enlace para compartir aparecerá aquí" + msgid "common.share-link.remove-link" msgstr "Eliminar enlace" @@ -254,6 +257,14 @@ msgstr "Duplicar %s archivos" msgid "dashboard.empty-files" msgstr "Todavía no hay ningún archivo aquí" +#: src/app/main/ui/dashboard/grid.cljs +#, markdown +msgid "dashboard.empty-placeholder-drafts" +msgstr "" +"Aún no tienes archivos. Si quieres probar alguna plantilla visita nuestra " +"seccion de [Bibliotecas y " +"plantillas](https://penpot.app/libraries-templates.html)" + msgid "dashboard.export-frames" msgstr "Exportar tableros a PDF..." @@ -318,9 +329,10 @@ msgid "dashboard.fonts.hero-text2" msgstr "" "Sólo deberías cargar fuentes que te pertenecen o de las que tienes una " "licencia que te permita usarlas en Penpot. Encuentra más información en la " -"sección de Derechos de Contenido: [Penpot's Terms of Service](1). También " -"te puede interesar leer más sobre licencias tipográficas: [font " -"licensing](2)." +"sección de Derechos de Contenido: [Penpot's Terms of " +"Service](https://penpot.app/terms.html). También te puede interesar leer " +"más sobre licencias tipográficas: [font " +"licensing](https://www.typography.com/faq)." msgid "dashboard.import" msgstr "Importar archivos" @@ -771,7 +783,7 @@ msgstr "Izquierda" #: src/app/main/ui/handoff/attributes/layout.cljs, src/app/main/ui/handoff/attributes/layout.cljs msgid "handoff.attributes.layout.radius" -msgstr "Derecha" +msgstr "Radio" #: src/app/main/ui/handoff/attributes/layout.cljs msgid "handoff.attributes.layout.rotation" @@ -1546,6 +1558,9 @@ msgstr "Perfil guardado correctamente!" msgid "notifications.validation-email-sent" msgstr "Verificación de email enviada a %s. Comprueba tu correo." +msgid "onboarding.choice.title" +msgstr "Te damos la bienvenida a Penpot" + msgid "onboarding.contrib.alt" msgstr "Código Abierto" @@ -1648,15 +1663,21 @@ msgid "onboarding.team.start.title" msgstr "Comienza a diseñar" msgid "onboarding.welcome.desc1" -msgstr "Estamos felices de presentarte la primera Alpha release." +msgstr "¡Hurra! Ya tienes tu cuenta en Penpot :)" msgid "onboarding.welcome.desc2" msgstr "" -"Penpot está en fase de desarrollo y tendrá actualizaciones constantes. " -"Esperamos que disfrutes esta version." +"Penpot está en su primera versión beta gracias a una combinación de " +"funcionalidades, madurez, estabilidad y la fantástica validación de su " +"comunidad, a la que te damos la bienvenida." + +msgid "onboarding.welcome.desc3" +msgstr "" +"Mientras disfrutas de Penpot seguiremos haciendo mejoras, lanzando " +"iteraciones de nuestros esperanzadores planes." msgid "onboarding.welcome.title" -msgstr "Te damos la bienvenida a Penpot!" +msgstr "Te damos la bienvenida a Penpot" #: src/app/main/ui/auth/recovery.cljs msgid "profile.recovery.go-to-login" @@ -2272,6 +2293,9 @@ msgstr "Automático" msgid "workspace.options.grid.column" msgstr "Columnas" +msgid "workspace.options.grid.params.color" +msgstr "Color" + #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs msgid "workspace.options.grid.params.columns" msgstr "Columnas" @@ -2356,6 +2380,9 @@ msgstr "Relleno de grupo" msgid "workspace.options.group-stroke" msgstr "Borde de grupo" +msgid "workspace.options.height" +msgstr "Altura" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-action" msgstr "Acción" @@ -2464,6 +2491,10 @@ msgstr "Arriba derecha" msgid "workspace.options.interaction-position" msgstr "Posición" +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.interaction-preserve-scroll" +msgstr "Conservar posición de desplazamiento" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-prev-screen" msgstr "Pantalla anterior" @@ -2576,6 +2607,9 @@ msgstr "Capas de grupo" msgid "workspace.options.layer-options.title.multiple" msgstr "Capas seleccionadas" +msgid "workspace.options.opacity" +msgstr "Opacidad" + #: 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" @@ -2624,6 +2658,9 @@ msgstr "Borde de selección" msgid "workspace.options.shadow-options.blur" msgstr "Desenfoque" +msgid "workspace.options.shadow-options.color" +msgstr "Color de sombra" + #: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs msgid "workspace.options.shadow-options.drop-shadow" msgstr "Sombra arrojada" @@ -2700,6 +2737,12 @@ msgstr "Marcador cuadrado" msgid "workspace.options.stroke-cap.triangle-arrow" msgstr "Flecha triángulo" +msgid "workspace.options.stroke-color" +msgstr "Color del trazo" + +msgid "workspace.options.stroke-width" +msgstr "Ancho del trazo" + #: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs msgid "workspace.options.stroke.center" msgstr "Centro" @@ -2841,6 +2884,15 @@ msgstr "Alineación vertical" msgid "workspace.options.use-play-button" msgstr "Usa el botón de play de la cabecera para arrancar la vista de prototipo." +msgid "workspace.options.width" +msgstr "Ancho" + +msgid "workspace.options.x" +msgstr "X" + +msgid "workspace.options.y" +msgstr "Y" + msgid "workspace.path.actions.add-node" msgstr "Añadir nodo (%s)" diff --git a/frontend/translations/he.po b/frontend/translations/he.po index f4c03a8da9..d8a515c80b 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -1593,17 +1593,6 @@ 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 "מעבר למסך הכניסה" diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index c9feb0fb1d..dc7c4a4be6 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -221,7 +221,8 @@ msgstr "" "Ar trebui să urcați doar fonturi la care aveți drept de folosință sau " "fonturi personale. Află mai multe despre Dreptul de conținut la secțiunea " "[Termenii și Condițiile Penpot](https://penpot.app/terms.html). De " -"asemenea, vă recomandăm să citiți și despre [licențierea fonturilor](2)." +"asemenea, vă recomandăm să citiți și despre [licențierea " +"fonturilor](https://www.typography.com/faq)." #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.invite-profile" diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index 33f368fb3e..80e90da9fb 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -1617,17 +1617,6 @@ 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" msgstr "Giriş yap" diff --git a/frontend/yarn.lock b/frontend/yarn.lock index e525c2b5af..119e401319 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -45,67 +45,67 @@ 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== +"@sentry/browser@^6.13.3": + version "6.13.3" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.13.3.tgz#d4511791b1e484ad48785eba3bce291fdf115c1e" + integrity sha512-jwlpsk2/u1cofvfYsjmqcnx50JJtf/T6HTgdW+ih8+rqWC5ABEZf4IiB/H+KAyjJ3wVzCOugMq5irL83XDCfqQ== dependencies: - "@sentry/core" "6.12.0" - "@sentry/types" "6.12.0" - "@sentry/utils" "6.12.0" + "@sentry/core" "6.13.3" + "@sentry/types" "6.13.3" + "@sentry/utils" "6.13.3" 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== +"@sentry/core@6.13.3": + version "6.13.3" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.13.3.tgz#5cbbb995128e793ebebcbf1d3b7514e0e5e8b221" + integrity sha512-obm3SjgCk8A7nB37b2AU1eq1q7gMoJRrGMv9VRIyfcG0Wlz/5lJ9O3ohUk+YZaaVfZMxXn6hFtsBiOWmlv7IIA== dependencies: - "@sentry/hub" "6.12.0" - "@sentry/minimal" "6.12.0" - "@sentry/types" "6.12.0" - "@sentry/utils" "6.12.0" + "@sentry/hub" "6.13.3" + "@sentry/minimal" "6.13.3" + "@sentry/types" "6.13.3" + "@sentry/utils" "6.13.3" 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== +"@sentry/hub@6.13.3": + version "6.13.3" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.13.3.tgz#cc09623a69b5343315fdb61c7fdd0be74b72299f" + integrity sha512-eYppBVqvhs5cvm33snW2sxfcw6G20/74RbBn+E4WDo15hozis89kU7ZCJDOPkXuag3v1h9igns/kM6PNBb41dw== dependencies: - "@sentry/types" "6.12.0" - "@sentry/utils" "6.12.0" + "@sentry/types" "6.13.3" + "@sentry/utils" "6.13.3" 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== +"@sentry/minimal@6.13.3": + version "6.13.3" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.13.3.tgz#a675a79bcc830142e4f95e6198a2efde2cd3901e" + integrity sha512-63MlYYRni3fs5Bh8XBAfVZ+ctDdWg0fapSTP1ydIC37fKvbE+5zhyUqwrEKBIiclEApg1VKX7bkKxVdu/vsFdw== dependencies: - "@sentry/hub" "6.12.0" - "@sentry/types" "6.12.0" + "@sentry/hub" "6.13.3" + "@sentry/types" "6.13.3" 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== +"@sentry/tracing@^6.13.3": + version "6.13.3" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.13.3.tgz#ca657d4afa99c50f15e638fe38405bac33e780ee" + integrity sha512-yyOFIhqlprPM0g4f35Icear3eZk2mwyYcGEzljJfY2iU6pJwj1lzia5PfSwiCW7jFGMmlBJNhOAIpfhlliZi8Q== dependencies: - "@sentry/hub" "6.12.0" - "@sentry/minimal" "6.12.0" - "@sentry/types" "6.12.0" - "@sentry/utils" "6.12.0" + "@sentry/hub" "6.13.3" + "@sentry/minimal" "6.13.3" + "@sentry/types" "6.13.3" + "@sentry/utils" "6.13.3" 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/types@6.13.3": + version "6.13.3" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.13.3.tgz#63ad5b6735b0dfd90b3a256a9f8e77b93f0f66b2" + integrity sha512-Vrz5CdhaTRSvCQjSyIFIaV9PodjAVFkzJkTRxyY7P77RcegMsRSsG1yzlvCtA99zG9+e6MfoJOgbOCwuZids5A== -"@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== +"@sentry/utils@6.13.3": + version "6.13.3" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.13.3.tgz#188754d40afe693c3fcae410f9322531588a9926" + integrity sha512-zYFuFH3MaYtBZTeJ4Yajg7pDf0pM3MWs3+9k5my9Fd+eqNcl7dYQYJbT9gyC0HXK1QI4CAMNNlHNl4YXhF91ag== dependencies: - "@sentry/types" "6.12.0" + "@sentry/types" "6.13.3" tslib "^1.9.3" "@sindresorhus/is@^0.14.0": @@ -397,16 +397,16 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -autoprefixer@^10.2.4: - version "10.3.4" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.3.4.tgz#29efe5d19f51c281953178ddb5b84c5f1ca24c86" - integrity sha512-EKjKDXOq7ug+jagLzmnoTRpTT0q1KVzEJqrJd0hCBa7FiG0WbFOBCcJCy2QkW1OckpO3qgttA1aWjVbeIPAecw== +autoprefixer@^10.3.7: + version "10.3.7" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.3.7.tgz#cef2562058406bd378c94aacda36bb46a97b3186" + integrity sha512-EmGpu0nnQVmMhX8ROoJ7Mx8mKYPlcUHuxkwrRYEYMz85lu7H09v8w6R1P0JPdn/hKU32GjpLBFEOuIlDWCRWvg== dependencies: - browserslist "^4.16.8" - caniuse-lite "^1.0.30001252" - colorette "^1.3.0" + browserslist "^4.17.3" + caniuse-lite "^1.0.30001264" fraction.js "^4.1.1" normalize-range "^0.1.2" + picocolors "^0.2.1" postcss-value-parser "^4.1.0" aws-sign2@~0.7.0: @@ -617,16 +617,16 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.16.8: - version "4.17.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.0.tgz#1fcd81ec75b41d6d4994fb0831b92ac18c01649c" - integrity sha512-g2BJ2a0nEYvEFQC208q8mVAhfNwpZ5Mu8BwgtCdZKO3qx98HChmeg448fPdUzld8aFmfLgVh7yymqV+q1lJZ5g== +browserslist@^4.17.3: + version "4.17.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.5.tgz#c827bbe172a4c22b123f5e337533ceebadfdd559" + integrity sha512-I3ekeB92mmpctWBoLXe0d5wPS2cBuRvvW0JyyJHMrk9/HmP2ZjrTboNAZ8iuGqaEIlKguljbQY32OkOJIRrgoA== dependencies: - caniuse-lite "^1.0.30001254" - colorette "^1.3.0" - electron-to-chromium "^1.3.830" + caniuse-lite "^1.0.30001271" + electron-to-chromium "^1.3.878" escalade "^3.1.1" - node-releases "^1.1.75" + node-releases "^2.0.1" + picocolors "^1.0.0" buffer-crc32@~0.2.3: version "0.2.13" @@ -718,10 +718,10 @@ camelcase@^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== +caniuse-lite@^1.0.30001264, caniuse-lite@^1.0.30001271: + version "1.0.30001272" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001272.tgz#8e9790ff995e9eb6e1f4c45cd07ddaa87cddbb14" + integrity sha512-DV1j9Oot5dydyH1v28g25KoVm7l8MTxazwuiH3utWiAS6iL/9Nh//TGwqFEeqqN8nnWYQ8HHhUq+o4QPt9kvYw== caseless@~0.12.0: version "0.12.0" @@ -952,11 +952,6 @@ color@3.0.x: color-convert "^1.9.1" color-string "^1.5.2" -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" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -1247,10 +1242,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -date-fns@^2.22.1: - version "2.23.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.23.0.tgz#4e886c941659af0cf7b30fafdd1eaa37e88788a9" - integrity sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA== +date-fns@^2.25.0: + version "2.25.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.25.0.tgz#8c5c8f1d958be3809a9a03f4b742eba894fc5680" + integrity sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w== dateformat@^3.0.3: version "3.0.3" @@ -1488,10 +1483,10 @@ editorconfig@^0.15.3: semver "^5.6.0" sigmund "^1.0.1" -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== +electron-to-chromium@^1.3.878: + version "1.3.882" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.882.tgz#ec57bb0c3a97f0ddfe6e39c5a9655ea9566b2db6" + integrity sha512-Kllt2R9+7yEIBbASR0MReJSK9TjPmHoomLbCLRP7r4SVtSy+Y0hYIhQ7LGjnMhlAyWUtGXTiznoGsaKxEH0ttw== elliptic@^6.5.3: version "6.5.4" @@ -2432,10 +2427,10 @@ he@1.1.1: resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= -highlight.js@^11.0.1: - version "11.2.0" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.2.0.tgz#a7e3b8c1fdc4f0538b93b2dc2ddd53a40c6ab0f0" - integrity sha512-JOySjtOEcyG8s4MLR2MNbLUyaXqUunmSnL2kdV/KuGJOmHZuAR5xC54Ko7goAXBWNhf09Vy3B+U7vR62UZ/0iw== +highlight.js@^11.3.1: + version "11.3.1" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.3.1.tgz#813078ef3aa519c61700f84fe9047231c5dc3291" + integrity sha512-PUhCRnPjLtiLHZAQ5A/Dt5F8cWZeMyj9KRsACsWT+OD6OP0x6dp5OmT5jdx0JgEyPxPZZIPQpRN2TciUT7occw== hmac-drbg@^1.0.1: version "1.0.1" @@ -3391,10 +3386,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -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== +marked@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/marked/-/marked-3.0.8.tgz#2785f0dc79cbdc6034be4bb4f0f0a396bd3f8aeb" + integrity sha512-0gVrAjo5m0VZSJb4rpL59K1unJAMb/hm8HRXqasD8VeC8m91ytDPMritgFSlKonfdt+rRYYpP/JfLxgIX8yoSw== matchdep@^2.0.0: version "2.0.0" @@ -3608,10 +3603,10 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== -nanoid@^3.1.23: - version "3.1.25" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152" - integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q== +nanoid@^3.1.30: + version "3.1.30" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362" + integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ== nanomatch@^1.2.9: version "1.2.13" @@ -3679,15 +3674,15 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -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== +node-releases@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" + integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== -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== +nodemon@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.14.tgz#287c7a2f6cd8a18b07e94cd776ecb6a82e4ba439" + integrity sha512-frcpDx+PviKEQRSYzwhckuO2zoHcBYLHI754RE9z5h1RGtrngerc04mLpQQCPWBkH/2ObrX7We9YiwVSYZpFJQ== dependencies: chokidar "^3.2.2" debug "^3.2.6" @@ -3903,10 +3898,10 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" -opentype.js@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-1.3.3.tgz#65b8645b090a1ad444065b784d442fa19d1061f6" - integrity sha512-/qIY/+WnKGlPIIPhbeNjynfD2PO15G9lA/xqlX2bDH+4lc3Xz5GCQ68mqxj3DdUv6AJqCeaPvuAoH8mVL0zcuA== +opentype.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-1.3.4.tgz#1c0e72e46288473cc4a4c6a2dc60fd7fe6020d77" + integrity sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw== dependencies: string.prototype.codepointat "^0.2.1" tiny-inflate "^1.0.3" @@ -4144,6 +4139,16 @@ phantomjs-prebuilt@^2.1.16: request-progress "^2.0.1" which "^1.2.10" +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + picomatch@^2.0.4, picomatch@^2.2.1: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" @@ -4231,13 +4236,13 @@ postcss@^7.0.16: source-map "^0.6.1" supports-color "^6.1.0" -postcss@^8.3.5: - version "8.3.6" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea" - integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A== +postcss@^8.3.11: + version "8.3.11" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.11.tgz#c3beca7ea811cd5e1c4a3ec6d2e7599ef1f8f858" + integrity sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA== dependencies: - colorette "^1.2.2" - nanoid "^3.1.23" + nanoid "^3.1.30" + picocolors "^1.0.0" source-map-js "^0.6.2" prepend-http@^2.0.0: @@ -4711,10 +4716,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rxjs@~7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.2.0.tgz#5cd12409639e9514a71c9f5f9192b2c4ae94de31" - integrity sha512-aX8w9OpKrQmiPKfT1bqETtUr9JygIz6GZ+gql8v7CijClsP0laoFUdKzxFAoWuRdSlOdU2+crss+cMf+cqMTnw== +rxjs@~7.4.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.4.0.tgz#a12a44d7eebf016f5ff2441b87f28c9a51cebc68" + integrity sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w== dependencies: tslib "~2.1.0" @@ -4740,10 +4745,10 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass@^1.35.1: - version "1.41.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.41.0.tgz#f7b41dc00336a4c03429c37b9680b86758af61d4" - integrity sha512-wb8nT60cjo9ZZMcHzG7TzdbFtCAmHEKWrH+zAdScPb4ZxL64WQBnGdbp5nwlenW5wJPcHva1JWmVa0h6iqA5eg== +sass@^1.43.4: + version "1.43.4" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.4.tgz#68c7d6a1b004bef49af0d9caf750e9b252105d1f" + integrity sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg== dependencies: chokidar ">=3.0.0 <4.0.0" @@ -4829,10 +4834,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.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== +shadow-cljs@2.15.12: + version "2.15.12" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.15.12.tgz#056ff707356538aefe8f5f07b7cb01e46758d10a" + integrity sha512-DKkQ9HIkQgV2yeWlwPUU02iw6Ucdw+Ks6r1XxESV/2I5V8ci4EJdZYcYiNS/GHYKDYaikJKSgmocxP+VtbYGJg== dependencies: node-libs-browser "^2.2.1" readline-sync "^1.4.7" @@ -5538,11 +5543,16 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -ua-parser-js@^0.7.18, ua-parser-js@^0.7.28: +ua-parser-js@^0.7.18: version "0.7.28" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== +ua-parser-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775" + integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg== + unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" diff --git a/version.txt b/version.txt index 27167ba29c..5bc7cd46d4 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.9.0-alpha +1.10.0-beta