Merge remote-tracking branch 'origin/develop'

This commit is contained in:
Andrey Antukh
2021-11-11 13:23:49 +01:00
114 changed files with 2128 additions and 1207 deletions

View File

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

View File

@@ -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"]}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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]))
@@ -150,14 +146,15 @@
[{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}]
(let [claims (tokens :verify {:token token :iss :prepared-register})
params (merge params claims)]
(check-profile-existence! conn params)
(let [profile (->> params
(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))]
(sid/load-initial-project! conn profile)
(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)
: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

View File

@@ -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,6 +324,30 @@
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
(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
@@ -345,5 +367,28 @@
:invited-by (:fullname profile)
:team (:name team)
:token itoken
:extra-data ptoken})
nil)))
: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)))

View File

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

View File

@@ -87,13 +87,9 @@
(defn retrieve-profile
[conn id]
(let [profile (some->> (retrieve-profile-data conn id)
(let [profile (->> (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."))
(update profile :props filter-profile-props)))
(def ^:private sql:profile-by-email

View File

@@ -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;")

View File

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

View File

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

View File

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

View File

@@ -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
(let [id (mk-uuid "team" i)]
(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)))
: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))

View File

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

View File

@@ -10,16 +10,14 @@
[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]
[& flags]
(loop [flags (apply concat flags)
result #{}]
(let [item (first flags)]
(if (nil? item)
result
@@ -34,6 +32,6 @@
(disj result (keyword (subs sname 8))))
:else
(recur (rest flags) result))))))))
(recur (rest flags) result)))))))

View File

@@ -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,7 +173,8 @@
"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
(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
@@ -187,7 +189,7 @@
(or (is-point-inside-nonzero? (first rect-points) path-lines)
(is-point-inside-nonzero? start-point rect-lines)
(intersects-lines? rect-lines path-lines))))
(intersects-lines? rect-lines path-lines)))))
(defn is-point-inside-ellipse?
"checks if a point is inside an ellipse"

View File

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

View File

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

View File

@@ -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,7 +138,27 @@
(<= % 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-.]+")
(s/def ::email
@@ -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

View File

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

View File

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

View File

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

View File

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

View File

@@ -89,6 +89,16 @@ http {
error_page 301 302 307 = @handle_redirect;
}
location ~ ^/github/penpot-files/(?<template_file>[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;

View File

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

View File

@@ -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]
@@ -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,11 +212,12 @@
(parse-viewbox))
transform (str/fmt "translate(%s, %s) scale(%s, %s)" x y
(/ width (:width vbox))
(/ height (:height vbox)))]
(-> result
(assoc "name" "g")
(assoc "attributes" {})
(update "elements" (fn [elements]
(/ 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))
@@ -180,8 +227,18 @@
(-> attrs
(d/merge (get group "attributes"))
(update "transform" #(str transform " " %))))))
(update-in group ["attributes" "transform"] #(str transform " " %)))))
elements))))))))
(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" {})
(assoc "elements" elements))))))
(convert-to-svg [ppmpath {:keys [colors] :as node}]
(log/trace :fn :convert-to-svg :ppmpath ppmpath :colors colors)
@@ -202,13 +259,15 @@
(extract-element-attrs [^js element]
(let [^js attrs (.. element -attributes)
^js colors (.. element -dataset -colors)]
^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 ",")}))
:colors (.split colors ",")
:mapping (js/JSON.parse mapping)}))
(extract-single-node [[shot node]]
(log/trace :fn :extract-single-node)
@@ -220,6 +279,7 @@
: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"}))

View File

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

View File

@@ -25,6 +25,17 @@ paths.resources = "./resources/";
paths.output = "./resources/public/";
paths.dist = "./target/dist/";
/***********************************************
* Marked Extensions
***********************************************/
const renderer = {
link(href, title, text) {
return `<a href="${href}" target="_blank">${text}</a>`;
}
};
marked.use({renderer});
/***********************************************
* Helpers

View File

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

View File

@@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="24.47353642154394" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="10.276508697997087 -10.666460420698968 24.47353642154394 45.833972589785844" height="45.833972589785844" style="-webkit-print-color-adjust: exact;"><g id="shape-68c61310-2040-11ec-9082-1597698bcafa" width="133" height="70" fill="none"><g id="shape-68c61311-2040-11ec-9082-1597698bcafa"><path d="M34.15111509349208,-6.042576477324019C32.728545198285246,-6.330271155262835,31.92939323677365,-3.376021339875024,31.573755956590503,-2.4511143742279273C31.03134089353898,-4.792719923592813,32.95730466396617,-10.268928805500309,31.43334627304739,-10.431571776022338C29.909377991244583,-10.594250374600506,28.25067219538414,-7.913146659964696,27.49821990591863,-6.170389430908017C26.74573283532027,-4.427587759031667,25.891378375591557,-1.7644073640558418,25.713212854031553,-2.2272258928269366C25.535081233357232,-2.6900095615483224,27.15263561710435,-6.757552580227184,25.941143250794084,-7.1652527066157745C24.72968451746874,-7.572893837298125,23.954342086512952,-5.090038220419956,23.25052490786311,-3.268314742871553C21.417817942427064,1.4754114884185583,10.659070475242515,0.4358613513782075,10.659070475242515,0.4358613513782075L10.492289692115264,34.903869918353394C10.950455754343238,34.90895547471155,28.014759237585167,30.129601494621056,32.24939430527593,19.872789654186818C35.25119864070166,13.145361698193483,32.406657583041124,6.514941655593702,33.309134176787666,1.1371823352346837C33.78537439733827,-1.58885192629441,35.57368117762144,-5.754847357929975,34.15111509349208,-6.042576477324019ZZ" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g><g id="shape-68c61312-2040-11ec-9082-1597698bcafa"><path d="M22.85523475939408,17.21695965203071C22.427431361919844,16.01479714589459,23.05111296555242,14.69085378511636,24.24829081777807,14.259819026323385C25.445434386402667,13.828783886987821,26.762725789724755,14.45390600122937,27.19052918719808,15.656068507365035C27.618298301069444,16.85823063295993,26.994616697437777,18.18217399373725,25.79747312881318,18.61320913307327C24.60029527658753,19.044243891866245,23.283003873265443,18.41912177762515,22.85523475939408,17.21695965203071ZZ" style="stroke-width: 1; stroke: rgb(49, 239, 184); stroke-opacity: 1;"/></g><g id="shape-68c61313-2040-11ec-9082-1597698bcafa"><path d="M17.110277419337763,8.332660542501344C14.855684713780647,9.317767677909615,11.627251872240777,12.863450146366631,13.47442592751213,18.054309980863763C15.321633118238424,23.24527363442894,19.819776888222805,23.267580687635473,22.247438584220617,22.768891367604738" style="stroke-width: 1; stroke: rgb(49, 239, 184); stroke-opacity: 1;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="24.47353642154485" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="10.276508697995268 -10.66646042069624 24.47353642154485 45.833972589782206" height="45.833972589782206" style="-webkit-print-color-adjust: exact;"><g id="shape-b0db2ad1-203d-11ec-9082-1597698bcafa" width="133" height="70" fill="none"><g id="shape-b0db2ad2-203d-11ec-9082-1597698bcafa"><path d="M34.15111509349208,-6.042576477324474C32.728545198285246,-6.3302711552623805,31.92939323677365,-3.376021339874569,31.573755956590503,-2.4511143742274726C31.03134089353898,-4.792719923592813,32.95730466396617,-10.268928805500764,31.43334627304739,-10.431571776021883C29.909377991244583,-10.59425037460096,28.25067219538414,-7.913146659964696,27.49821990591863,-6.170389430908472C26.74573283532027,-4.427587759031667,25.891378375591557,-1.7644073640558418,25.713212854031553,-2.2272258928269366C25.535081233357232,-2.6900095615483224,27.15263561710435,-6.757552580227639,25.941143250794084,-7.165252706616229C24.72968451746874,-7.57289383729767,23.954342086512952,-5.090038220419956,23.25052490786311,-3.268314742871553C21.417817942427064,1.4754114884181035,10.659070475242515,0.4358613513777527,10.659070475242515,0.4358613513777527L10.492289692115264,34.90386991835294C10.950455754343238,34.90895547471155,28.014759237585167,30.129601494621056,32.24939430527593,19.872789654186818C35.25119864070166,13.145361698193483,32.406657583041124,6.514941655594157,33.309134176787666,1.1371823352346837C33.78537439733827,-1.58885192629441,35.57368117762144,-5.754847357929975,34.15111509349208,-6.042576477324474ZZ" style="fill: rgb(250, 181, 245);"/></g><g id="shape-b0db51e0-203d-11ec-9082-1597698bcafa"><path d="M22.85523475939408,17.21695965203071C22.427431361919844,16.01479714589459,23.05111296555242,14.690853785115905,24.24829081777807,14.259819026323385C25.445434386402667,13.828783886987367,26.762725789724755,14.45390600122937,27.19052918719808,15.65606850736549C27.618298301069444,16.85823063295993,26.994616697437777,18.182173993736797,25.79747312881318,18.613209133072814C24.60029527658753,19.044243891866245,23.283003873265443,18.41912177762515,22.85523475939408,17.21695965203071ZZ" style="stroke-width: 1; stroke: white;"/></g><g id="shape-b0db51e1-203d-11ec-9082-1597698bcafa"><path d="M17.110277419337763,8.33266054250089C14.855684713780647,9.317767677909615,11.627251872240777,12.863450146366631,13.47442592751213,18.054309980863763C15.321633118238424,23.245273634429395,19.819776888222805,23.267580687635927,22.247438584220617,22.768891367604738" style="stroke-width: 1; stroke: white;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="206.95680443620125" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-1.9573043775739962 -2.7988168413976156 206.95680443620125 158.15679986166066" height="158.15679986166066" style="-webkit-print-color-adjust: exact;"><g id="shape-575c9093-25c2-11ec-9877-05429cda5971"><g id="shape-575c9094-25c2-11ec-9877-05429cda5971"><defs><mask id="outer-stroke-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" x="-1.9573043775739962" y="-2.7988168413976156" width="206.95680443620125" height="158.15679986166066" maskUnits="userSpaceOnUse"><use xlink:href="#stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" style="fill: none; stroke: white; stroke-width: 4;"/><use xlink:href="#stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" style="fill: black;"/></mask></defs><g class="outer-stroke-shape"><defs><rect width="201" height="152" x="1" id="stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" transform="matrix(0.9999999978661108,-1.745329248269963e-8,1.7453292541199837e-7,1.0000000006480119,-0.000013047912574393195,0.0000017222602934907627)" ry="3" rx="3" y="0" data-style="fill:none;stroke-width:2;stroke:#E3E3E3;stroke-opacity:1;stroke-dasharray:"/></defs><use xlink:href="#stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" mask="url(#outer-stroke-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35)" style="stroke-width: 4; stroke: rgb(227, 227, 227); stroke-opacity: 1; fill: none;"/><use xlink:href="#stroke-shape-ddd0b3b0-3bbe-11ec-880d-a73a7db30a35" style="fill: none;"/></g></g><g id="shape-575c9095-25c2-11ec-9877-05429cda5971"><path d="M0,91 h202 a0,0 0 0 1 0,0 v59 a3,3 0 0 1 -3,3 h-196 a3,3 0 0 1 -3,-3 v-59 a0,0 0 0 1 0,0 z" x="0" y="91" transform="matrix(1.0000000024760176,-1.0471975568525078e-7,3.490658523073093e-8,1.0000000103484785,-0.000004508681172410434,0.000009314180957176177)" width="202" height="62" style="fill: none; stroke-width: 2; stroke: rgb(227, 227, 227); stroke-opacity: 1;"/></g><g id="shape-575c9096-25c2-11ec-9877-05429cda5971"><rect rx="3" ry="3" x="10" y="103" transform="matrix(0.9999999995870766,-4.810081334728633e-16,-4.810081268554315e-16,1.000000030940994,3.1795153176972235e-8,-0.0000034653912877047333)" width="134" height="18" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g><g id="shape-575c9097-25c2-11ec-9877-05429cda5971"><rect rx="3" ry="3" x="10" y="131" transform="matrix(1.0000000022017677,6.948317137937551e-23,6.617444834426139e-23,1.0000000667740845,-2.234794180822064e-7,-0.000009114662532283546)" width="183" height="11" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="81.5877720738863" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.43500565160593396 0.14985215967590193 81.5877720738863 128.8502825397186" height="128.8502825397186" style="-webkit-print-color-adjust: exact;"><g id="shape-305b39c3-25c6-11ec-9877-05429cda5971" width="90" height="140" fill="none"><g id="shape-305b39c4-25c6-11ec-9877-05429cda5971"><path d="M18.180074555850297,34.13571661820788C16.328443710368447,37.802933917027985,15.363633970810042,41.85364142381468,15.363541785663983,45.96187246179579C15.363449600517924,49.407753224720636,16.04230101673238,52.81989422415609,17.360917347203213,56.00341606115035C18.679625862816465,59.187030083290665,20.612379636906553,62.07970778419531,23.049017419827578,64.51634556711724C25.485655202748603,66.95298335003963,28.378332903652336,68.88573712412926,31.561946925794473,70.20444563974297C34.74556094793297,71.52306197021062,38.157794132515846,72.20182120128084,41.603674895442964,72.20182120128084C48.56282376312447,72.20172901613478,55.23693615892262,69.43709648322647,60.15777926017472,64.51616119682512C65.07871454658016,59.59522591042378,67.84316270919953,52.92102132947775,67.8430705240462,45.96187246179579C67.84316270919953,41.85373360896074,66.87872171022173,37.802933917027985,65.027275235032,34.13571661820788L18.180074555850297,34.13571661820788ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g><g id="shape-305b39c5-25c6-11ec-9877-05429cda5971"><path d="M40.358622311596264,76.81790020937888C29.539497004501754,76.81799239452494,19.163413509784732,80.76582127821257,11.513152601448382,87.79300278375013C3.862873256086459,94.81972336355693,-0.4349826275210944,104.35074562355658,-0.43500383010177757,114.28922622951995C-0.4349070356984157,119.34558149560962,0.6793145450319571,124.34939122839569,2.840650607504358,129.00013185144462L77.87714712656452,129.00013185144462C80.03842787794929,124.34939122839569,81.15276192455713,119.34558149560962,81.15276192455713,114.28922622951995C81.15276192455713,109.36838312826512,80.09761074177368,104.49547630300549,78.04750527664146,99.94890489510135C75.99739981150924,95.40325533865871,72.99244060259662,91.27234675472573,69.20436857717868,87.79281841345801C65.41629655176075,84.31329007218983,60.919228567337996,81.55317461144296,55.96990025592095,79.67010863112546C51.02047975936148,77.78704265080796,45.715777709559916,76.81790020937888,40.358622311596264,76.81790020937888ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g><g id="shape-305b39c6-25c6-11ec-9877-05429cda5971"><path d="M22.63741074228892,55.77654841648564C22.63741074228892,55.77654841648564,28.038261899520876,66.05546876716517,39.71092947477882,65.88123884095012C51.38359705003313,65.70700891473462,57.30695361562539,56.473468121347196,57.30695361562539,56.473468121347196" style="stroke-width: 2; stroke: white;"/></g><g id="shape-305b39c7-25c6-11ec-9877-05429cda5971"><path d="M33.141447219852125,53.60300704066367C36.667529059923254,53.60300704066367,39.52591388646397,50.77716357071449,39.52591388646397,47.29136663950885C39.52591388646397,43.805569708302755,36.667529059923254,40.97972623835358,33.141447219852125,40.97972623835358C29.615457564927056,40.97972623835358,26.75698055324392,43.805569708302755,26.75698055324392,47.29136663950885C26.75698055324392,50.77716357071449,29.615457564927056,53.60300704066367,33.141447219852125,53.60300704066367ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1; stroke-width: 2; stroke: white;"/></g><g id="shape-305b39c8-25c6-11ec-9877-05429cda5971"><path d="M52.22921139562823,5.9902455968044706C47.897615748537646,2.9522656685448965,42.82503589672706,1.0223696339853632,37.493231413951435,0.383799439228369C32.16142693117581,-0.2547744429339218,26.74730111289682,0.4191478335183092,21.76469396372704,2.341603604932061L64.9989743951628,32.66471824116161C65.42634473269572,27.547336408323645,64.47748302341643,22.41059569479512,62.24254634026147,17.742063338540447C60.007609657106514,13.073623167432288,56.56080704272608,9.028299273180437,52.22921139562823,5.9902455968044706ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="102.12322708597458" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.2830548330293823 0.00013327541864782688 102.12322708597458 54.02153523420793" height="54.02153523420793" style="-webkit-print-color-adjust: exact;"><g id="shape-305b39ca-25c6-11ec-9877-05429cda5971" width="133" height="70" fill="none"><g id="shape-305b39cb-25c6-11ec-9877-05429cda5971"><path d="M92.06001883709541,52.780900512351764C92.66931429164106,49.56773051235177,86.02942338254798,47.840623239624165,83.94800520073477,47.06227823962445C89.18168701891409,45.781289603260575,101.50092247346038,49.98474823962442,101.82747470073446,46.54621233053331C102.1541065189158,43.10765323962414,96.1049138370945,39.43658051235161,92.18095065527814,37.78441233053354C88.2568870189134,36.13216687598788,82.26786429164349,34.273371421442334,83.30053701891302,33.860271421442576C84.33313247346086,33.4472486941695,93.48809610982426,36.99089869416957,94.37155520073429,34.2504214214423C95.25488292800583,31.510021421442616,89.671905200732,29.82478051235148,85.57220065527872,28.284194148715414C74.89666429164208,24.27258051235185,76.95853247345804,0.00013505780589184724,76.95853247345804,0.00013505780589184724L-0.28305389017623384,0.486489603260452C-0.28305389017623384,1.519162330533618,10.851173382550769,39.85655778507862,33.94041883709724,49.143364148715136C49.09020065527875,55.740090512351344,63.87718701891572,49.16363278507879,75.95035520072997,51.06297323962417C82.07081883709907,52.068059603260735,91.45064610982081,55.99406278507877,92.06001883709541,52.780900512351764ZZ" style="fill: rgb(227, 227, 227); fill-opacity: 1;"/></g><g id="shape-305b39cc-25c6-11ec-9877-05429cda5971"><path d="M39.658059746187064,27.905789603260928C42.341277928004274,26.911598694169697,45.32354156436486,28.28403960326068,46.31920065527811,30.971275966897338C47.314859746184084,33.658435057805946,45.946823382550065,36.64278505780612,43.263605200732854,37.6369759668969C40.580387018912006,38.631089603260534,37.59812338255142,37.258648694170006,36.60246429163817,34.57148960326049C35.6068052007322,31.884253239624286,36.974841564369854,28.89990323962411,39.658059746187064,27.905789603260928ZZ" style="stroke-width: 2; stroke: white;"/></g><g id="shape-305b39cd-25c6-11ec-9877-05429cda5971"><path d="M59.423496109822736,14.73643051235149C57.15994610982307,9.680012330533373,49.1343233825537,2.492953239624512,37.54835974618618,6.785685057806404C25.962164291642694,11.078494148715436,26.02405974618887,21.21628960326052,27.201927928006626,26.674912330533516" style="stroke-width: 2; stroke: white;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

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

View File

@@ -46,7 +46,7 @@ $width-settings-bar: 16rem;
}
.handoff-layout {
.viewer-preview {
.viewer-section {
flex-wrap: nowrap;
}
.settings-bar {

View File

@@ -336,7 +336,7 @@
.viewer-comments-container {
width: 100%;
height: 100%;
z-index: 1000;
z-index: 1;
position: absolute;
top: 0px;
left: 0px;

View File

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

View File

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

View File

@@ -70,12 +70,6 @@
width: 100%;
align-items: center;
}
&:hover {
.element-set-title {
color: $color-gray-10;
}
}
}
.element-list {

View File

@@ -6,7 +6,6 @@
height: 48px;
padding: 0 $size-4 0 55px;
position: relative;
z-index: 12;
justify-content: space-between;
a {

View File

@@ -6,7 +6,7 @@
overflow: hidden;
display: flex;
flex-direction: column;
z-index: 12;
z-index: 10;
&.invisible {
visibility: hidden;

View File

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

View File

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

View File

@@ -187,10 +187,12 @@
(ptk/reify ::files-fetched
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-files
(-> state
(update :dashboard-files
(fn [state]
(let [state (remove-project-files state)]
(reduce #(assoc %1 (:id %2) %2) state files))))))))
(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

View File

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

View File

@@ -125,6 +125,9 @@
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]
(if (and (= (count shapes) 1)
(:component-id (first shapes)))
empty-changes
(let [[group rchanges uchanges]
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
@@ -188,7 +191,7 @@
:attr :touched
:val (:touched original-shape)}]}))
updated-shapes))]
[group rchanges uchanges]))
[group rchanges uchanges])))
(defn duplicate-component
"Clone the root shape of the component and all children. Generate new

View File

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

View File

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

View File

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

View File

@@ -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
(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,6 +214,7 @@
:label (tr "auth.fullname")
:type "text"}]]
(when (contains? @cf/flags :terms-and-privacy-checkbox)
[:div.fields-row
[:& fm/input {:name :accept-terms-and-privacy
:class "check-primary"
@@ -216,14 +224,7 @@
[: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"}]])
[:a {:href "https://penpot.app/privacy.html" :target "_blank"} (tr "auth.privacy-policy")]]]]])
[:& fm/submit-button
{:label (tr "auth.register-submit")

View File

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

View File

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

View File

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

View File

@@ -120,6 +120,6 @@
[:*
[:& header {:team team :project project}]
[:section.dashboard-container
[:& grid {:project-id (:id project)
[:& grid {:project project
:files files}]]]))

View File

@@ -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}]
[{: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)}])]))

View File

@@ -331,10 +331,11 @@
[:div.modal-footer
[:div.action-buttons
(when (or (= :analyzing (:status @state)) pending-import?)
[:input.cancel-button
{:type "button"
:value (tr "labels.cancel")
:on-click handle-cancel}]
:on-click handle-cancel}])
(when (= :analyzing (:status @state))
[:input.accept-button

View File

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

View File

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

View File

@@ -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,6 +184,7 @@
(filterv #(= id (:project-id %)))
(sort-by :modified-at #(compare %2 %1))))]
[:& project-item {:project project
:team team
:files files
:first? (= project (first projects))
:key (:id project)}]))]])))

View File

@@ -72,8 +72,9 @@
#(doseq [key keys]
(events/unlistenByKey key)))))
(when-let [component (get components (:type data))]
[:div.modal-wrapper {:ref wrapper-ref}
(mf/element (get components (:type data)) (:props data))]))
(mf/element component (:props data))])))
(def modal-ref

View File

@@ -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")}]
[: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")))

View File

@@ -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,6 +125,7 @@
;; Auxiliary SVG for rendering text-shapes
(when render-texts?
(for [object text-shapes]
[:& (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)
@@ -131,7 +133,7 @@
: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))}]]))]))
[:& shape-wrapper {:shape (-> object (assoc :x 0 :y 0))}]]]))]))
(defn- adapt-root-frame
[objects object-id]

View File

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

View File

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

View File

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

View File

@@ -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)
(let [color-data
(->> (:content shape)
(tree-seq map? :children)
(into #{"#000000"} (comp (map :fill-color) (filter string?))))]
(apply str (interpose "," colors))))
(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)

View File

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

View File

@@ -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}]
[: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]]
i/copy])]
[:div.hint (tr "common.share-link.permissions-hint")]]]

View File

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

View File

@@ -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]
(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)}])
: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}]))

View File

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

View File

@@ -80,26 +80,28 @@
page-name (:name page)
frame-name (:name frame)
total (count (:frames page))
toggle-thumbnails
(fn []
(st/emit! dv/toggle-thumbnails-panel))
show-dropdown? (mf/use-state false)
open-dropdown
toggle-thumbnails
(mf/use-callback
(fn []
(reset! show-dropdown? true)
(st/emit! dv/close-thumbnails-panel))
(st/emit! dv/toggle-thumbnails-panel)))
open-dropdown
(mf/use-callback
(fn []
(reset! show-dropdown? true)))
close-dropdown
(mf/use-callback
(fn []
(reset! show-dropdown? false))
(reset! show-dropdown? false)))
navigate-to
(mf/use-callback
(fn [page-id]
(st/emit! (dv/go-to-page page-id))
(reset! show-dropdown? false))]
(reset! show-dropdown? false)))]
[:div.sitemap-zone {:alt (tr "viewer.header.sitemap")}
[:div.breadcrumb
@@ -108,6 +110,7 @@
[:span "/"]
[:span.file-name file-name]
[:span "/"]
[:span.page-name page-name]
[:span.icon i/arrow-down]

View File

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

View File

@@ -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,7 +138,8 @@
editing-stop (update-in [:stops editing-stop] merge changes)))
(reset! dirty? true)))
handle-click-picker (fn []
handle-click-picker
(fn []
(if picking-color?
(do (modal/disallow-click-outside!)
(st/emit! (dc/stop-picker)))
@@ -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

View File

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

View File

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

View File

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

View File

@@ -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 %))}

View File

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

View File

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

View File

@@ -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,7 +20,10 @@
(defn request-export
[shape exports]
(rp/query! :export
;; 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)

View File

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

View File

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

View File

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

View File

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

View File

@@ -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?))
(when (and (options :radius) (some? radius-mode))
[:div.row-flex
[:div.radius-options
[:div.radius-icon.tooltip.tooltip-bottom
{:class (dom/classnames
:selected
(and radius-1? (not radius-4?)))
: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-4? (not radius-1?)))
: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]]
(if radius-1?
[:div.input-element.mini
(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
[: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
[: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
[: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
[: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)}]]])]))]]]))
:value (attr->string :r4 values)}]]])])]]]))

View File

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

View File

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

View File

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

View File

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

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