Compare commits

..

38 Commits

Author SHA1 Message Date
Alejandro
16cf16c422 Merge pull request #5114 from penpot/superalex-fix-fetch-profile-exception
🐛 Fix fetch profile exception
2024-09-24 14:24:56 +02:00
Alejandro Alonso
4e1eee197e 🐛 Fix fetch profile exception 2024-09-24 14:13:46 +02:00
Alejandro
91c8af9e38 Merge pull request #5108 from penpot/palba-fix-login-redirect
🐛 Fix bad redirect on new oops page with penpot login
2024-09-24 11:27:04 +02:00
Pablo Alba
58593a9428 🐛 Fix bad redirect on new oops page with penpot login 2024-09-24 10:32:57 +02:00
Eva Marco
17cf57f7ca Merge pull request #5113 from penpot/superalex-fix-show-in-assets-panel
🐛 Fix show in assets panel
2024-09-24 09:38:32 +02:00
Alejandro Alonso
f7cfe36f37 🐛 Fix show in assets panel 2024-09-24 08:49:52 +02:00
Alejandro
c26f909565 Merge pull request #5110 from penpot/eva-fix-code-block-height
🐛 Fix code block height
2024-09-24 06:54:44 +02:00
Eva Marco
6db7fe5f7b 🐛 Fix code block height 2024-09-23 17:03:25 +02:00
Pablo Alba
a207114d95 Merge pull request #5109 from penpot/hiru-fix-swap-inside-group
🐛 Fix error when swapping a copy that is the only child of a group
2024-09-23 15:12:55 +02:00
Eva Marco
b8299a5ea5 🐛 Fix create team without invitations on onboarding 2024-09-23 15:09:43 +02:00
Andrés Moya
1fa461e996 🐛 Fix error when swapping a copy that is the only child of a group 2024-09-23 15:01:01 +02:00
Alejandro
2e3745099b Merge pull request #5107 from penpot/alotor-remove-export-option
🐛 Removed "merge assets" option from export
2024-09-23 13:22:56 +02:00
alonso.torres
6892cffe54 🐛 Removed "merge assets" option from export 2024-09-23 12:13:33 +02:00
Alejandro Alonso
e0034dc205 🐛 Fix onboarding edn urls 2024-09-23 06:30:19 +02:00
Alejandro
bd9eab08b7 Merge pull request #5101 from penpot/palba-migrate-templates-v2
 Update templates links to binary v2
2024-09-23 06:12:15 +02:00
Pablo Alba
b5121657ee Update templates links to binary v2 2024-09-18 16:23:12 +02:00
Aitor Moreno
ca257d1caf Merge pull request #5097 from penpot/eva-fix-arrow-keys-tabs
🐛  Fix arrow key movement on tabs
2024-09-17 17:23:32 +02:00
Aitor Moreno
e164692391 Merge pull request #5098 from penpot/eva-fix-desing-panel
🐛  Fix path side panel options
2024-09-17 17:22:21 +02:00
Eva Marco
b58edea544 🐛 Fix path side panel options 2024-09-17 17:00:40 +02:00
Eva Marco
9a587c91a8 🐛 Fix arrow key movement on tabs 2024-09-17 16:36:44 +02:00
Aitor Moreno
aae1571a5c Merge pull request #5096 from penpot/alotor-bugfixes
Bugfixes
2024-09-17 16:14:59 +02:00
alonso.torres
ebaf30727c 🐛 Fix copy/paste images in Safari 2024-09-17 15:38:18 +02:00
alonso.torres
f5f255e2d5 🐛 Fix problem with comments max length 2024-09-17 14:18:51 +02:00
Alejandro
e65c0d9f48 Merge pull request #5088 from penpot/niwinz-bugfix-2
🐛 Fix issues related to invalid colors inserted on shape shadow
2024-09-17 13:58:43 +02:00
Andrey Antukh
86c5ca4213 🐛 Fix incorrect redirect handling on request-access go-home button 2024-09-16 18:53:56 +02:00
Andrey Antukh
179d534237 🐛 Fix several issues related to invalid colors inserted on shadows 2024-09-16 18:32:40 +02:00
Andrey Antukh
162507264c 🐛 Reexecute file migration 26 again for shapes that has transform prop as nil 2024-09-16 18:32:40 +02:00
Andrey Antukh
7e0a8b6227 Merge pull request #5092 from penpot/palbs-fix-request-acces-dont-go-your-team
🐛 Fix request access to the Team don't go to Your Penpot team
2024-09-16 18:32:19 +02:00
Pablo Alba
475d14edf4 🐛 Fix request access to the Team don't go to Your Penpot team 2024-09-16 14:42:52 +02:00
alonso.torres
979828ffe3 🐛 Fix issue when exporting libraries when merging libraries 2024-09-16 09:08:51 +02:00
alonso.torres
65bb795199 🐛 Fix visual problem with stroke cap menu 2024-09-16 09:07:57 +02:00
Alejandro
a0546b2e63 Merge pull request #5086 from penpot/niwinz-bugfix-1
🐛 Ignore object thumbnail requests if file is already marked as deleted
2024-09-13 12:30:27 +02:00
Andrey Antukh
f291125377 🐛 Add migration for invalid value on layout-wrap-type on shape prop 2024-09-12 21:33:01 +02:00
Andrey Antukh
0ce981a68c 🐛 Add missing ref-id unsassign on srepl helpers for process file 2024-09-12 21:32:34 +02:00
Andrey Antukh
a8814dcaba 🐛 Add missing fields on file-gc libraries fetching sql 2024-09-12 21:07:19 +02:00
Pablo Alba
229eeae6db 🐛 Fix bad redirect on new oops page with social login 2024-09-12 16:35:49 +02:00
Andrey Antukh
d03788af93 🐛 Ignore object thumbnail requests if file is already marked as deleted 2024-09-12 15:23:31 +02:00
Alejandro Alonso
017aad6454 🐛 Fix export failed error when exporting multiple shapes 2024-09-12 12:55:32 +02:00
77 changed files with 3138 additions and 3416 deletions

View File

@@ -1,23 +1,15 @@
# CHANGELOG
## 2.3.0
### :rocket: Epics and highlights
### :boom: Breaking changes & Deprecations
### :heart: Community contributions (Thank you!)
### :sparkles: New features
### :bug: Bugs fixed
## 2.2.0
### :rocket: Epics and highlights
### :boom: Breaking changes & Deprecations
- Removed "merge assets" option when exporting ".svg + .json" files. After the components changes the option wasn't
working properly and we're planning to change the format soon. We think it's better to deprecate the option for the
time being.
### :heart: Community contributions (Thank you!)
- Set proper default tenant on exporter (by @june128) [#4946](https://github.com/penpot/penpot/pull/4946)
@@ -93,6 +85,11 @@
- Fix layer panel overflowing [Taiga #8665](https://tree.taiga.io/project/penpot/issue/8665)
- Fix problem when creating a component instance from grid layout [Github #4881](https://github.com/penpot/penpot/issues/4881)
- Fix problem when dismissing shared library update [Taiga #8669](https://tree.taiga.io/project/penpot/issue/8669)
- Fix visual problem with stroke cap menu [Taiga #8730](https://tree.taiga.io/project/penpot/issue/8730)
- Fix issue when exporting libraries when merging libraries [Taiga #8758](https://tree.taiga.io/project/penpot/issue/8758)
- Fix problem with comments max length [Taiga #8778](https://tree.taiga.io/project/penpot/issue/8778)
- Fix copy/paste images in Safari [Taiga #8771](https://tree.taiga.io/project/penpot/issue/8771)
- Fix swap when the copy is the only child of a group [#5075](https://github.com/penpot/penpot/issues/5075)
## 2.1.5

View File

@@ -1,42 +1,42 @@
[{:id "wireframing-kit"
:name "Wireframe library"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/wireframing-kit.penpot"}
{:id "prototype-examples"
:name "Prototype template"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/prototype-examples.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/prototype-examples.penpot"}
{:id "plants-app"
:name "UI mockup example"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Plants-app.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Plants-app.penpot"}
{:id "penpot-design-system"
:name "Design system example"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Penpot-Design-system.penpot"}
{:id "tutorial-for-beginners"
:name "Tutorial for beginners"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/tutorial-for-beginners.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/tutorial-for-beginners.penpot"}
{:id "lucide-icons"
:name "Lucide Icons"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Lucide-icons.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Lucide-icons.penpot"}
{:id "font-awesome"
:name "Font Awesome"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Font-Awesome.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/FontAwesome.penpot"}
{:id "black-white-mobile-templates"
:name "Black & White Mobile Templates"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Black-White-Mobile-Templates.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Black-&-White-Mobile-Templates.penpot"}
{:id "avataaars"
:name "Avataaars"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Avataaars-by-Pablo-Stanley.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Avataaars-by-Pablo-Stanley.penpot"}
{:id "ux-notes"
:name "UX Notes"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/UX-Notes.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/UX-Notes.penpot"}
{:id "whiteboarding-kit"
:name "Whiteboarding Kit"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Whiteboarding-mapping-kit.penpot"}
{:id "open-color-scheme"
:name "Open Color Scheme"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Open-Color-Scheme.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Open%20Color%20Scheme%20(v1.9.1).penpot"}
{:id "flex-layout-playground"
:name "Flex Layout Playground"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Flex%20Layout%20Playground.penpot"}
{:id "welcome"
:name "Welcome"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/welcome.penpot"}]
:file-uri "https://github.com/penpot/penpot-files/raw/main/welcome.penpot"}]

View File

@@ -292,7 +292,7 @@
[:map {:title "create-comment-thread"}
[:file-id ::sm/uuid]
[:position ::gpt/point]
[:content [:string {:max 250}]]
[:content [:string {:max 750}]]
[:page-id ::sm/uuid]
[:frame-id ::sm/uuid]
[:share-id {:optional true} [:maybe ::sm/uuid]]])

View File

@@ -273,7 +273,7 @@
(defn get-minimal-file
[cfg id & {:as opts}]
(let [opts (assoc opts ::sql/columns [:id :modified-at :revn :data-ref-id :data-backend])]
(let [opts (assoc opts ::sql/columns [:id :modified-at :deleted-at :revn :data-ref-id :data-backend])]
(db/get cfg :file {:id id} opts)))
(defn get-file-etag
@@ -487,7 +487,7 @@
[:file-id ::sm/uuid]
[:page-id {:optional true} ::sm/uuid]
[:share-id {:optional true} ::sm/uuid]
[:object-id {:optional true} [:or ::sm/uuid ::sm/coll-of-uuid]]
[:object-id {:optional true} [:or ::sm/uuid [::sm/set ::sm/uuid]]]
[:features {:optional true} ::cfeat/features]])
(sv/defmethod ::get-page

View File

@@ -233,7 +233,7 @@
"INSERT INTO file_tagged_object_thumbnail (file_id, object_id, tag, media_id)
VALUES (?, ?, ?, ?)
ON CONFLICT (file_id, object_id, tag)
DO UPDATE SET updated_at=?, media_id=?, deleted_at=null
DO UPDATE SET updated_at=?, media_id=?, deleted_at=?
RETURNING *")
(defn- persist-thumbnail!
@@ -251,17 +251,19 @@
:content-type mtype
:bucket "file-object-thumbnail"})))
(defn- create-file-object-thumbnail!
[{:keys [::sto/storage] :as cfg} file-id object-id media tag]
(let [tsnow (dt/now)
media (persist-thumbnail! storage media tsnow)
[{:keys [::sto/storage] :as cfg} file object-id media tag]
(let [file-id (:id file)
timestamp (dt/now)
media (persist-thumbnail! storage media timestamp)
[th1 th2] (db/tx-run! cfg (fn [{:keys [::db/conn]}]
(let [th1 (db/exec-one! conn [sql:get-file-object-thumbnail file-id object-id tag])
th2 (db/exec-one! conn [sql:create-file-object-thumbnail
file-id object-id tag (:id media)
tsnow (:id media)])]
file-id object-id tag
(:id media)
timestamp
(:id media)
(:deleted-at file)])]
[th1 th2])))]
(when (and (some? th1)
@@ -294,8 +296,8 @@
(media/validate-media-size! media)
(db/run! cfg files/check-edition-permissions! profile-id file-id)
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame")))
(when-let [file (files/get-minimal-file cfg file-id {::db/check-deleted false})]
(create-file-object-thumbnail! cfg file object-id media (or tag "frame"))))
;; --- MUTATION COMMAND: delete-file-object-thumbnail

View File

@@ -75,6 +75,7 @@
:created-at (:created-at file)
:modified-at (:modified-at file)
:data-backend nil
:data-ref-id nil
:has-media-trimmed false}
{:id (:id file)})))

View File

@@ -133,7 +133,13 @@
file))
(def ^:private sql:get-files-for-library
"SELECT f.id, f.data, f.modified_at, f.features, f.version
"SELECT f.id,
f.data,
f.modified_at,
f.features,
f.version,
f.data_backend,
f.data_ref_id
FROM file AS f
LEFT JOIN file_library_rel AS fl ON (fl.file_id = f.id)
WHERE fl.library_file_id = ?

View File

@@ -6,8 +6,6 @@
(ns user
(:require
[app.common.data :as d]
[app.common.fressian :as fres]
[app.common.json :as json]
[app.common.pprint :as pp]
[app.common.schema :as sm]

View File

@@ -44,8 +44,8 @@
(defn ordered-map
([] lkm/empty-linked-map)
([k a] (assoc lkm/empty-linked-map k a))
([k a & xs] (apply assoc lkm/empty-linked-map k a xs)))
([a] (conj lkm/empty-linked-map a))
([a & xs] (apply conj lkm/empty-linked-map a xs)))
(defn ordered-set?
[o]
@@ -564,41 +564,6 @@
new-elems
(remove p? after))))
(defn addm-at-index
"Insert an element in an ordered map at an arbitrary index"
[coll index key element]
(assert (ordered-map? coll))
(-> (ordered-map)
(into (take index coll))
(assoc key element)
(into (drop index coll))))
(defn insertm-at-index
"Insert a map {k v} of elements in an ordered map at an arbitrary index"
[coll index new-elems]
(assert (ordered-map? coll))
(-> (ordered-map)
(into (take index coll))
(into new-elems)
(into (drop index coll))))
(defn adds-at-index
"Insert an element in an ordered set at an arbitrary index"
[coll index element]
(assert (ordered-set? coll))
(-> (ordered-set)
(into (take index coll))
(conj element)
(into (drop index coll))))
(defn inserts-at-index
"Insert a list of elements in an ordered set at an arbitrary index"
[coll index new-elems]
(assert (ordered-set? coll))
(-> (ordered-set)
(into (take index coll))
(into new-elems)
(into (drop index coll))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Parsing / Conversion

View File

@@ -545,6 +545,7 @@
(d/update-in-when [pid :shapes] d/without-obj sid)
(d/update-in-when [pid :shapes] d/vec-without-nils)
(cond-> component? (d/update-when pid #(dissoc % :remote-synced))))))))
(update-parent-id [objects id]
(-> objects
(d/update-when id assoc :parent-id parent-id)))

View File

@@ -6,4 +6,4 @@
(ns app.common.files.defaults)
(def version 51)
(def version 54)

View File

@@ -863,11 +863,9 @@
(assoc shadow :color color)))
(update-object [object]
(d/update-when object :shadow
#(into []
(comp (map fix-shadow)
(filter valid-shadow?))
%)))
(let [xform (comp (map fix-shadow)
(filter valid-shadow?))]
(d/update-when object :shadow #(into [] xform %))))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
@@ -1010,13 +1008,44 @@
(defn migrate-up-51
"This migration fixes library invalid colors"
[data]
(let [update-colors
(fn [colors]
(into {} (filter #(-> % val valid-color?) colors)))]
(update data :colors update-colors)))
(defn migrate-up-52
"Fixes incorrect value on `layout-wrap-type` prop"
[data]
(letfn [(update-shape [shape]
(if (= :no-wrap (:layout-wrap-type shape))
(assoc shape :layout-wrap-type :nowrap)
shape))
(update-page [page]
(d/update-when page :objects update-vals update-shape))]
(update data :pages-index update-vals update-page)))
(defn migrate-up-54
"Fixes shapes with invalid colors in shadow: it first tries a non
destructive fix, and if it is not possible, then, shadow is removed"
[data]
(letfn [(fix-shadow [shadow]
(update shadow :color d/without-nils))
(update-shape [shape]
(let [xform (comp (map fix-shadow)
(filter valid-shadow?))]
(d/update-when shape :shadow #(into [] xform %))))
(update-container [container]
(d/update-when container :objects update-vals update-shape))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(def migrations
"A vector of all applicable migrations"
[{:id 2 :migrate-up migrate-up-2}
@@ -1059,4 +1088,7 @@
{:id 48 :migrate-up migrate-up-48}
{:id 49 :migrate-up migrate-up-49}
{:id 50 :migrate-up migrate-up-50}
{:id 51 :migrate-up migrate-up-51}])
{:id 51 :migrate-up migrate-up-51}
{:id 52 :migrate-up migrate-up-52}
{:id 53 :migrate-up migrate-up-26}
{:id 54 :migrate-up migrate-up-54}])

View File

@@ -15,7 +15,6 @@
java.time.Instant
java.time.OffsetDateTime
java.util.List
linked.map.LinkedMap
org.fressian.Reader
org.fressian.StreamingWriter
org.fressian.Writer
@@ -110,13 +109,6 @@
(clojure.lang.PersistentArrayMap. (.toArray kvs))
(clojure.lang.PersistentHashMap/create (seq kvs)))))
(defn read-ordered-map
[^Reader rdr]
(let [kvs ^java.util.List (read-object! rdr)]
(reduce #(assoc %1 (first %2) (second %2))
(d/ordered-map)
(partition-all 2 (seq kvs)))))
(def ^:dynamic *write-handler-lookup* nil)
(def ^:dynamic *read-handler-lookup* nil)
@@ -233,11 +225,6 @@
:wfn write-map-like
:rfn read-map-like}
{:name "linked/map"
:class LinkedMap
:wfn write-map-like
:rfn read-ordered-map}
{:name "clj/keyword"
:class clojure.lang.Keyword
:wfn write-named

View File

@@ -158,7 +158,11 @@
empty-parents
;; Any parent whose children are all deleted, must be deleted too.
(into (d/ordered-set) (find-all-empty-parents #{}))
;; Unless we are during a component swap: in this case we are replacing a shape by
;; other one, so must not delete empty parents.
(if-not component-swap
(into (d/ordered-set) (find-all-empty-parents #{}))
#{})
components-to-delete
(if components-v2

View File

@@ -12,7 +12,7 @@
[app.common.uri :as uri]
[cognitect.transit :as t]
[lambdaisland.uri :as luri]
[linked.map :as lkm]
[linked.core :as lk]
[linked.set :as lks])
#?(:clj
(:import
@@ -24,7 +24,6 @@
java.time.Instant
java.time.OffsetDateTime
lambdaisland.uri.URI
linked.map.LinkedMap
linked.set.LinkedSet)))
(def write-handlers (atom nil))
@@ -119,15 +118,10 @@
{:id "u"
:rfn parse-uuid})
{:id "ordered-map"
:class #?(:clj LinkedMap :cljs lkm/LinkedMap)
:wfn vec
:rfn #(into lkm/empty-linked-map %)}
{:id "ordered-set"
:class #?(:clj LinkedSet :cljs lks/LinkedSet)
:wfn vec
:rfn #(into lks/empty-linked-set %)}
:rfn #(into (lk/set) %)}
{:id "duration"
:class #?(:clj Duration :cljs lxn/Duration)

View File

@@ -80,21 +80,23 @@
[:opacity {:optional true} [:maybe ::sm/safe-number]]
[:offset ::sm/safe-number]]]]])
(def schema:color-attrs
[:map {:title "ColorAttrs"}
[:id {:optional true} ::sm/uuid]
[:name {:optional true} :string]
[:path {:optional true} [:maybe :string]]
[:value {:optional true} [:maybe :string]]
[:color {:optional true} [:maybe ::rgb-color]]
[:opacity {:optional true} [:maybe ::sm/safe-number]]
[:modified-at {:optional true} ::sm/inst]
[:ref-id {:optional true} ::sm/uuid]
[:ref-file {:optional true} ::sm/uuid]
[:gradient {:optional true} [:maybe schema:gradient]]
[:image {:optional true} [:maybe schema:image-color]]
[:plugin-data {:optional true} ::ctpg/plugin-data]])
(def schema:color
[:and
[:map {:title "Color"}
[:id {:optional true} ::sm/uuid]
[:name {:optional true} :string]
[:path {:optional true} [:maybe :string]]
[:value {:optional true} [:maybe :string]]
[:color {:optional true} [:maybe ::rgb-color]]
[:opacity {:optional true} [:maybe ::sm/safe-number]]
[:modified-at {:optional true} ::sm/inst]
[:ref-id {:optional true} ::sm/uuid]
[:ref-file {:optional true} ::sm/uuid]
[:gradient {:optional true} [:maybe schema:gradient]]
[:image {:optional true} [:maybe schema:image-color]]
[:plugin-data {:optional true} ::ctpg/plugin-data]]
[:and schema:color-attrs
[::sm/contains-any {:strict true} [:color :gradient :image]]])
(def schema:recent-color
@@ -111,12 +113,13 @@
(sm/register! ::gradient schema:gradient)
(sm/register! ::image-color schema:image-color)
(sm/register! ::recent-color schema:recent-color)
(sm/register! ::color-attrs schema:color-attrs)
(def valid-color?
(sm/lazy-validator schema:color))
(def check-color!
(sm/check-fn schema:color))
(def valid-recent-color?
(sm/lazy-validator schema:recent-color))
(def check-recent-color!
(sm/check-fn schema:recent-color))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS

View File

@@ -125,6 +125,9 @@
(sm/register! ::stroke schema:stroke)
(def check-stroke!
(sm/check-fn schema:stroke))
(def schema:shape-base-attrs
[:map {:title "ShapeMinimalRecord"}
[:id ::sm/uuid]

View File

@@ -27,3 +27,6 @@
[:color ::ctc/color]])
(sm/register! ::shadow schema:shadow)
(def check-shadow!
(sm/check-fn schema:shadow))

View File

@@ -16,27 +16,27 @@ RUN set -ex; \
echo "nameserver 8.8.8.8" > /etc/resolvconf/resolv.conf.d/tail; \
apt-get -qq update; \
apt-get -qqy install --no-install-recommends \
bash \
build-essential \
ca-certificates \
fakeroot \
file \
git \
gnupg2 \
jq \
less \
locales \
nginx \
openssh-client \
redis-tools \
rlwrap \
rsync \
locales \
gnupg2 \
ca-certificates \
wget \
sudo \
tmux \
unzip \
url \
vim \
wget \
curl \
bash \
git \
rlwrap \
unzip \
rsync \
fakeroot \
file \
less \
jq \
nginx \
; \
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
locale-gen; \
@@ -50,28 +50,32 @@ RUN set -ex; \
RUN set -ex; \
apt-get -qq update; \
apt-get -qqy install --no-install-recommends \
fontforge \
fonts-liberation \
gconf-service \
ghostscript \
python3 \
python3-tabulate \
imagemagick \
libappindicator1 \
ghostscript \
netpbm \
poppler-utils \
potrace \
webp \
woff-tools \
woff2 \
fontforge \
gconf-service \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgbm1 \
libgcc1 \
libgconf-2-4 \
libgdk-pixbuf2.0-0 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libx11-6 \
@@ -88,14 +92,10 @@ RUN set -ex; \
libxshmfence1 \
libxss1 \
libxtst6 \
netpbm \
poppler-utils \
potrace \
python3 \
python3-tabulate \
webp \
woff-tools \
woff2 \
fonts-liberation \
libappindicator1 \
libnss3 \
libgbm1 \
xvfb \
; \
rm -rf /var/lib/apt/lists/*;

View File

@@ -1,6 +1,6 @@
FROM debian:bookworm
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
FROM ubuntu:22.04
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ENV LANG='en_US.UTF-8' \
LC_ALL='en_US.UTF-8' \
JAVA_HOME="/opt/jdk" \
@@ -13,16 +13,20 @@ RUN set -ex; \
apt-get -qq update; \
apt-get -qq upgrade; \
apt-get -qqy --no-install-recommends install \
ca-certificates \
fontforge \
imagemagick \
nano \
curl \
tzdata \
locales \
python3 \
python3-tabulate \
rlwrap \
ca-certificates \
imagemagick \
webp \
rlwrap \
fontconfig \
woff-tools \
woff2 \
python3 \
python3-tabulate \
fontforge \
; \
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
locale-gen; \

View File

@@ -1,4 +1,4 @@
FROM debian:bookworm
FROM ubuntu:22.04
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ENV LANG=en_US.UTF-8 \
@@ -13,9 +13,11 @@ RUN set -ex; \
echo "nameserver 127.0.0.11" > /etc/resolvconf/resolv.conf.d/tail; \
apt-get -qq update; \
apt-get -qqy --no-install-recommends install \
ca-certificates \
locales \
curl \
tzdata \
locales \
ca-certificates \
fontconfig \
xz-utils \
; \
rm -rf /var/lib/apt/lists/*; \
@@ -25,32 +27,33 @@ RUN set -ex; \
RUN set -ex; \
apt-get -qq update; \
apt-get -qqy install \
fonts-liberation \
gconf-service \
ghostscript \
imagemagick \
ghostscript \
netpbm \
poppler-utils \
potrace \
gconf-service \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libatomic1 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgbm1 \
libgcc1 \
libgconf-2-4 \
libgdk-pixbuf2.0-0 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcb-dri3-0 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
@@ -62,9 +65,9 @@ RUN set -ex; \
libxshmfence1 \
libxss1 \
libxtst6 \
netpbm \
poppler-utils \
potrace \
fonts-liberation \
libnss3 \
libgbm1 \
; \
rm -rf /var/lib/apt/lists/*;

View File

@@ -177,3 +177,15 @@ test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard-
),
).toBeVisible();
});
test("Bug 8784 - Use keyboard arrow to move inside a text input does not change tabs", async ({
page,
}) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.pageName.click();
await page.keyboard.press("ArrowLeft");
await expect(workspacePage.pageName).toHaveText("Page 1");
});

View File

@@ -117,7 +117,7 @@ test("User goes to the Viewer Inspect code, code tab", async ({ page }) => {
});
await viewerPage.showCode();
await viewerPage.page.getByRole("tab", { name: "code" }).click();
await viewerPage.page.getByTestId("code").click();
await expect(
viewerPage.page.getByRole("button", { name: "Copy all code" }),

View File

File diff suppressed because it is too large Load Diff

View File

@@ -115,30 +115,20 @@ export async function compileSassAll(worker) {
return path.startsWith("app/main/ui/ds/");
};
const isOldComponentSystemFile = (path) => {
return path.startsWith("app/main/ui/components/");
};
let files = (await fs.readdir(sourceDir, { recursive: true })).filter(
isSassFile,
);
const appFiles = files
.filter((path) => !isDesignSystemFile(path))
.filter((path) => !isOldComponentSystemFile(path))
.map((path) => ph.join(sourceDir, path));
const dsFiles = files
.filter(isDesignSystemFile)
.map((path) => ph.join(sourceDir, path));
const oldComponentsFiles = files
.filter(isOldComponentSystemFile)
.map((path) => ph.join(sourceDir, path));
const procs = [compileSass(worker, "resources/styles/main-default.scss", {})];
for (let path of [...oldComponentsFiles, ...dsFiles, ...appFiles]) {
for (let path of [...dsFiles, ...appFiles]) {
const proc = limitFn(() => compileSass(worker, path, { modules: true }));
procs.push(proc);
}

View File

@@ -110,7 +110,7 @@
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" "https://penpot.app/privacy"))
(def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
(def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
(def plugins-list-uri (obj/get global "penpotPluginsListUri" "https://penpot-docs-plugins.pages.dev/plugins/getting-started/#examples"))
(def plugins-list-uri (obj/get global "penpotPluginsListUri" "https://penpot-docs-plugins.pages.dev/technical-guide/plugins/getting-started/#examples"))
(defn- normalize-uri
[uri-str]

View File

@@ -143,4 +143,3 @@
(reinit))))
(set! (.-stackTraceLimit js/Error) 50)

View File

@@ -19,7 +19,6 @@
[app.main.data.websocket :as ws]
[app.main.features :as features]
[app.main.repo :as rp]
[app.plugins.register :as register]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.storage :as s]
@@ -138,9 +137,7 @@
(swap! s/storage assoc :profile profile)
(i18n/set-locale! (:lang profile))
(when (not= previous-email email)
(set-current-team! nil))
(register/init))))))
(set-current-team! nil)))))))
(defn- on-fetch-profile-exception
[cause]
@@ -151,7 +148,7 @@
href (->> path
(js/encodeURIComponent)
(str "/challenge.html?redirect="))]
(rx/of (rt/nav-raw href)))
(rx/of (rt/nav-raw :href href)))
(rx/throw cause))))
(defn fetch-profile
@@ -171,13 +168,25 @@
accepting invitation, or third party auth signup or singin."
[profile]
(letfn [(get-redirect-events []
(let [team-id (get-current-team-id profile)
welcome-file-id (get-in profile [:props :welcome-file-id])]
(if (some? welcome-file-id)
(rx/of
(rt/nav' :workspace {:project-id (:default-project-id profile)
:file-id welcome-file-id})
(update-profile-props {:welcome-file-id nil}))
(let [team-id (get-current-team-id profile)
welcome-file-id (dm/get-in profile [:props :welcome-file-id])
redirect-href (:login-redirect @s/session)
current-href (rt/get-current-href)]
(cond
(some? redirect-href)
(binding [s/*sync* true]
(swap! s/session dissoc :login-redirect)
(if (= current-href redirect-href)
(rx/of (rt/reload true))
(rx/of (rt/nav-raw :href redirect-href))))
(some? welcome-file-id)
(rx/of (rt/nav' :workspace {:project-id (:default-project-id profile)
:file-id welcome-file-id})
(update-profile-props {:welcome-file-id nil}))
:else
(rx/of (rt/nav' :dashboard-projects {:team-id team-id})))))]
(ptk/reify ::logged-in

View File

@@ -85,7 +85,8 @@
[beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
[potok.v2.core :as ptk]
[promesa.core :as p]))
(def default-workspace-local {:zoom 1})
(log/set-level! :debug)
@@ -1551,15 +1552,40 @@
shapes (->> (cfh/selected-with-children objects selected)
(keep (d/getf objects)))]
(->> (rx/from shapes)
(rx/merge-map (partial prepare-object objects frame-id))
(rx/reduce collect-data initial)
(rx/map (partial sort-selected state))
(rx/map (partial advance-copies state selected))
(rx/map #(t/encode-str % {:type :json-verbose}))
(rx/map wapi/write-to-clipboard)
(rx/catch on-copy-error)
(rx/ignore)))))))))
;; The clipboard API doesn't handle well asynchronous calls because it expects to use
;; the clipboard in an user interaction. If you do an async call the callback is outside
;; the thread of the UI and so Safari blocks the copying event.
;; We use the API `ClipboardItem` that allows promises to be passed and so the event
;; will wait for the promise to resolve and everything should work as expected.
;; This only works in the current versions of the browsers.
(if (some? (unchecked-get ug/global "ClipboardItem"))
(let [resolve-data-promise
(p/create
(fn [resolve reject]
(->> (rx/from shapes)
(rx/merge-map (partial prepare-object objects frame-id))
(rx/reduce collect-data initial)
(rx/map (partial sort-selected state))
(rx/map (partial advance-copies state selected))
(rx/map #(t/encode-str % {:type :json-verbose}))
(rx/map #(wapi/create-blob % "text/plain"))
(rx/subs! resolve reject))))]
(->> (rx/from (wapi/write-to-clipboard-promise "text/plain" resolve-data-promise))
(rx/catch on-copy-error)
(rx/ignore)))
;; FIXME: this is to support Firefox versions below 116 that don't support `ClipboardItem`
;; after the version 116 is less common we could remove this.
;; https://caniuse.com/?search=ClipboardItem
(->> (rx/from shapes)
(rx/merge-map (partial prepare-object objects frame-id))
(rx/reduce collect-data initial)
(rx/map (partial sort-selected state))
(rx/map (partial advance-copies state selected))
(rx/map #(t/encode-str % {:type :json-verbose}))
(rx/map wapi/write-to-clipboard)
(rx/catch on-copy-error)
(rx/ignore))))))))))
(declare ^:private paste-transit)
(declare ^:private paste-text)

View File

@@ -12,6 +12,9 @@
[app.common.files.helpers :as cfh]
[app.common.schema :as sm]
[app.common.text :as txt]
[app.common.types.color :as ctc]
[app.common.types.shape :refer [check-stroke!]]
[app.common.types.shape.shadow :refer [check-shadow!]]
[app.main.broadcast :as mbc]
[app.main.data.events :as ev]
[app.main.data.modal :as md]
@@ -21,7 +24,6 @@
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.texts :as dwt]
[app.main.data.workspace.undo :as dwu]
[app.util.color :as uc]
[app.util.storage :refer [storage]]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
@@ -165,6 +167,15 @@
(defn add-fill
[ids color]
(dm/assert!
"expected a valid color struct"
(ctc/check-color! color))
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::add-fill
ptk/WatchEvent
(watch [_ state _]
@@ -175,6 +186,15 @@
(defn remove-fill
[ids color position]
(dm/assert!
"expected a valid color struct"
(ctc/check-color! color))
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::remove-fill
ptk/WatchEvent
(watch [_ state _]
@@ -187,13 +207,21 @@
(defn remove-all-fills
[ids color]
(dm/assert!
"expected a valid color struct"
(ctc/check-color! color))
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::remove-all-fills
ptk/WatchEvent
(watch [_ state _]
(let [remove-all (fn [shape _] (assoc shape :fills []))]
(transform-fill state ids color remove-all)))))
(defn change-hide-fill-on-export
[ids hide-fill-on-export]
(ptk/reify ::change-hide-fill-on-export
@@ -272,17 +300,25 @@
;; example using the color selection from
;; multiple shapes) let's use the first stop
;; color
attrs (cond-> attrs
(:gradient attrs) (get-in [:gradient :stops 0]))
new-attrs (-> (merge (get-in shape [:shadow index :color]) attrs)
(d/without-nils))]
(assoc-in shape [:shadow index :color] new-attrs))))))))
attrs (cond-> attrs
(:gradient attrs)
(dm/get-in [:gradient :stops 0]))
attrs' (-> (dm/get-in shape [:shadow index :color])
(merge attrs)
(d/without-nils))]
(assoc-in shape [:shadow index :color] attrs'))))))))
(defn add-shadow
[ids shadow]
(dm/assert!
"expected a valid shadow struct"
(check-shadow! shadow))
(dm/assert!
"expected a valid coll of uuid's"
(sm/check-coll-of-uuid! ids))
(every? uuid? ids))
(ptk/reify ::add-shadow
ptk/WatchEvent
@@ -293,6 +329,15 @@
(defn add-stroke
[ids stroke]
(dm/assert!
"expected a valid stroke struct"
(check-stroke! stroke))
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::add-stroke
ptk/WatchEvent
(watch [_ _ _]
@@ -301,6 +346,11 @@
(defn remove-stroke
[ids position]
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::remove-stroke
ptk/WatchEvent
(watch [_ _ _]
@@ -314,6 +364,11 @@
(defn remove-all-strokes
[ids]
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::remove-all-strokes
ptk/WatchEvent
(watch [_ _ _]
@@ -376,7 +431,7 @@
:on-change handle-change-color}
:allow-click-outside true})))))))
(defn color-att->text
(defn- color-att->text
[color]
{:fill-color (when (:color color) (str/lower (:color color)))
:fill-opacity (:opacity color)
@@ -395,26 +450,57 @@
(some? has-color?)
(assoc-in [:fills index] parsed-new-color))))
(def ^:private schema:change-color-operation
[:map
[:prop [:enum :fill :stroke :shadow :content]]
[:shape-id ::sm/uuid]
[:index :int]])
(def ^:private schema:change-color-operations
[:vector schema:change-color-operation])
(def ^:private check-change-color-operations!
(sm/check-fn schema:change-color-operations))
(defn change-color-in-selected
[new-color shapes-by-color old-color]
[operations new-color old-color]
(dm/verify!
"expected valid change color operations"
(check-change-color-operations! operations))
(dm/verify!
"expected a valid color struct for new-color param"
(ctc/check-color! new-color))
(dm/verify!
"expected a valid color struct for old-color param"
(ctc/check-color! old-color))
(ptk/reify ::change-color-in-selected
ptk/WatchEvent
(watch [_ _ _]
(let [undo-id (js/Symbol)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
(->> (rx/from shapes-by-color)
(rx/map (fn [shape] (case (:prop shape)
:fill (change-fill [(:shape-id shape)] new-color (:index shape))
:stroke (change-stroke [(:shape-id shape)] new-color (:index shape))
:shadow (change-shadow [(:shape-id shape)] new-color (:index shape))
:content (dwt/update-text-with-function
(:shape-id shape)
(partial change-text-color old-color new-color (:index shape)))))))
(->> (rx/from operations)
(rx/map (fn [{:keys [shape-id index] :as operation}]
(case (:prop operation)
:fill (change-fill [shape-id] new-color index)
:stroke (change-stroke [shape-id] new-color index)
:shadow (change-shadow [shape-id] new-color index)
:content (dwt/update-text-with-function
shape-id
(partial change-text-color old-color new-color index))))))
(rx/of (dwu/commit-undo-transaction undo-id)))))))
(defn apply-color-from-palette
[color stroke?]
(dm/assert!
"should be a valid color"
(ctc/check-color! color))
(ptk/reify ::apply-color-from-palette
ptk/WatchEvent
(watch [_ state _]
@@ -437,9 +523,10 @@
result (cond-> result (not group?) (conj cur))]
(recur (rest pending) result))))]
(if stroke?
(rx/of (change-stroke ids (merge uc/empty-color color) 0))
(rx/of (change-fill ids (merge uc/empty-color color) 0)))))))
(rx/of (change-stroke ids color 0))
(rx/of (change-fill ids color 0)))))))
(declare activate-colorpicker-color)
(declare activate-colorpicker-gradient)
@@ -448,15 +535,22 @@
(defn apply-color-from-colorpicker
[color]
(dm/assert!
"expected valid color structure"
(ctc/check-color! color))
(ptk/reify ::apply-color-from-colorpicker
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(cond
(:image color) (activate-colorpicker-image)
(:color color) (activate-colorpicker-color)
(= :linear (get-in color [:gradient :type])) (activate-colorpicker-gradient :linear-gradient)
(= :radial (get-in color [:gradient :type])) (activate-colorpicker-gradient :radial-gradient))))))
;; FIXME: revisit this
(let [gradient-type (dm/get-in color [:gradient :type])]
(rx/of
(cond
(:image color) (activate-colorpicker-image)
(:color color) (activate-colorpicker-color)
(= :linear gradient-type) (activate-colorpicker-gradient :linear-gradient)
(= :radial gradient-type) (activate-colorpicker-gradient :radial-gradient)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -596,7 +690,8 @@
(update :current-color merge changes)
(update :current-color materialize-color-components)
(update :current-color #(if (not= type :image) (dissoc % :image) %))
;; current color can be a library one I'm changing via colorpicker
;; current color can be a library one
;; I'm changing via colorpicker
(d/dissoc-in [:current-color :id])
(d/dissoc-in [:current-color :file-id]))]
(if-let [stop (:editing-stop state)]
@@ -614,7 +709,8 @@
:colorpicker
:type)
formated-color (get-color-from-colorpicker-state (:colorpicker state))
;; Type is set to color on closing the colorpicker, but we can can close it while still uploading an image fill
;; Type is set to color on closing the colorpicker, but we
;; can can close it while still uploading an image fill
ignore-color? (and (= selected-type :color) (nil? (:color formated-color)))]
(when (and add-recent? (not ignore-color?))
(rx/of (dwl/add-recent-color formated-color)))))))
@@ -686,6 +782,7 @@
(defn select-color
[position add-color]
;; FIXME: revisit
(ptk/reify ::select-color
ptk/WatchEvent
(watch [_ state _]

View File

@@ -116,8 +116,13 @@
(update :id #(or % (uuid/next)))
(assoc :name (or (get-in color [:image :name])
(:color color)
(uc/gradient-type->string (get-in color [:gradient :type])))))]
(dm/assert! ::ctc/color color)
(uc/gradient-type->string (get-in color [:gradient :type]))))
(d/without-nils))]
(dm/assert!
"expect valid color structure"
(ctc/check-color! color))
(ptk/reify ::add-color
ev/Event
(-data [_] color)
@@ -135,8 +140,8 @@
[color]
(dm/assert!
"expected valid recent color map"
(ctc/valid-recent-color? color))
"expected valid recent color structure"
(ctc/check-recent-color! color))
(ptk/reify ::add-recent-color
ptk/UpdateEvent
@@ -155,7 +160,7 @@
(update [_ state]
(assoc-in state [:workspace-local :color-for-rename] nil))))
(defn- do-update-color
(defn- update-color*
[it state color file-id]
(let [data (get state :workspace-data)
[path name] (cfh/parse-path-name (:name color))
@@ -171,19 +176,20 @@
(defn update-color
[color file-id]
(let [color (d/without-nils color)]
(dm/assert!
"expected valid parameters"
(ctc/valid-color? color))
(dm/assert!
"expected valid color data structure"
(ctc/check-color! color))
(dm/assert!
"expected file-id"
(uuid? file-id))
(dm/assert!
"expected file-id"
(uuid? file-id))
(ptk/reify ::update-color
ptk/WatchEvent
(watch [it state _]
(do-update-color it state color file-id))))
(ptk/reify ::update-color
ptk/WatchEvent
(watch [it state _]
(update-color* it state color file-id)))))
(defn rename-color
[file-id id new-name]
@@ -198,9 +204,10 @@
(if (str/empty? new-name)
(rx/empty)
(let [data (get state :workspace-data)
object (get-in data [:colors id])
object (assoc object :name new-name)]
(do-update-color it state object file-id)))))))
color (get-in data [:colors id])
color (assoc color :name new-name)
color (d/without-nils color)]
(update-color* it state color file-id)))))))
(defn delete-color
[{:keys [id] :as params}]

View File

@@ -42,7 +42,8 @@
(mf/lazy-component app.main.ui.workspace/workspace))
(mf/defc main-page
{::mf/props :obj}
{::mf/props :obj
::mf/private true}
[{:keys [route profile]}]
(let [{:keys [data params]} route
props (get profile :props)
@@ -68,6 +69,7 @@
(:onboarding-viewed props)
(not= (:release-notes-viewed props) (:main cf/version))
(not= "0.0" (:main cf/version)))]
[:& (mf/provider ctx/current-route) {:value route}
(case (:name data)
(:auth-login

View File

@@ -23,6 +23,7 @@
[app.util.i18n :refer [tr]]
[app.util.keyboard :as k]
[app.util.router :as rt]
[app.util.storage :as s]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
@@ -44,13 +45,28 @@
[]
(st/emit! (du/create-demo-profile)))
(defn- store-login-redirect
[save-login-redirect]
(binding [s/*sync* true]
(if (some? save-login-redirect)
;; Save the current login raw uri for later redirect user back to
;; the same page, we need it to be synchronous because the user is
;; going to be redirected instantly to the oidc provider uri
(swap! s/session assoc :login-redirect (rt/get-current-href))
;; Clean the login redirect
(swap! s/session dissoc :login-redirect))))
(defn- login-with-oidc
[event provider params]
(dom/prevent-default event)
(store-login-redirect (:save-login-redirect params))
;; FIXME: this code should be probably moved outside of the UI
(->> (rp/cmd! :login-with-oidc (assoc params :provider provider))
(rx/subs! (fn [{:keys [redirect-uri] :as rsp}]
(if redirect-uri
(.replace js/location redirect-uri)
(st/emit! (rt/nav-raw :uri redirect-uri))
(log/error :hint "unexpected response from OIDC method"
:resp (pr-str rsp))))
(fn [cause]
@@ -119,6 +135,7 @@
on-submit
(mf/use-callback
(fn [form _event]
(store-login-redirect (:save-login-redirect params))
(reset! error nil)
(let [params (with-meta (:clean-data @form)
{:on-error on-error

View File

@@ -35,6 +35,7 @@
on-focus (unchecked-get props "on-focus")
on-blur (unchecked-get props "on-blur")
placeholder (unchecked-get props "placeholder")
max-length (unchecked-get props "max-length")
on-change (unchecked-get props "on-change")
on-esc (unchecked-get props "on-esc")
on-ctrl-enter (unchecked-get props "on-ctrl-enter")
@@ -88,7 +89,8 @@
:on-blur on-blur
:value value
:placeholder placeholder
:on-change on-change*}]))
:on-change on-change*
:max-length max-length}]))
(mf/defc reply-form
[{:keys [thread] :as props}]
@@ -128,7 +130,8 @@
:on-focus on-focus
:select-on-focus? false
:on-ctrl-enter on-submit
:on-change on-change}]
:on-change on-change
:max-length 750}]
(when (or @show-buttons? (seq @content))
[:div {:class (stl/css :buttons-wrapper)}
[:input.btn-secondary
@@ -196,7 +199,8 @@
:select-on-focus? false
:on-esc on-esc
:on-change on-change
:on-ctrl-enter on-submit}]
:on-ctrl-enter on-submit
:max-length 750}]
[:div {:class (stl/css :buttons-wrapper)}
[:input {:on-click on-esc
@@ -233,7 +237,8 @@
:select-on-focus true
:select-on-focus? false
:on-ctrl-enter on-submit*
:on-change on-change}]
:on-change on-change
:max-length 750}]
[:div {:class (stl/css :buttons-wrapper)}
[:input {:type "button"
:value "Cancel"

View File

@@ -42,9 +42,7 @@
(let [search-term (get-in route [:params :query :search-term])
team-id (get-in route [:params :path :team-id])
project-id (get-in route [:params :path :project-id])]
(cond->
{:search-term search-term}
(cond-> {:search-term search-term}
(uuid-str? team-id)
(assoc :team-id (uuid team-id))
@@ -84,10 +82,10 @@
(mf/use-effect on-resize)
[:div {:class (stl/css :dashboard-content)
:style {:pointer-events (when file-menu-open? "none")}
:on-click clear-selected-fn :ref container}
:on-click clear-selected-fn
:ref container}
(case section
:dashboard-projects
[:*
@@ -146,7 +144,8 @@
(l/derived :current-team-id st/state))
(mf/defc dashboard
[{:keys [route profile] :as props}]
{::mf/props :obj}
[{:keys [route profile]}]
(let [section (get-in route [:data :name])
params (parse-params route)
@@ -181,13 +180,13 @@
[:& (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
;; the :current-team-id on the state. We set the key to the
;; team-id because we want to completely refresh all the
;; components on team change. Many components assumes that the
;; team is already set so don't put the team into mf/deps.
;; NOTE: dashboard events and other related functions assumes
;; that the team is a implicit context variable that is
;; available using react context or accessing
;; the :current-team-id on the state. We set the key to the
;; team-id because we want to completely refresh all the
;; components on team change. Many components assumes that the
;; team is already set so don't put the team into mf/deps.
(when (and team initialized?)
[:main {:class (stl/css :dashboard)
:key (:id team)}

View File

@@ -519,8 +519,10 @@
@include bodySmallTypography;
color: var(--modal-title-foreground-color);
}
.custom-input-checkbox {
// TODO: This fix is temporary, the error is caused by the
// cascading order of the compiled css files.
// https://tree.taiga.io/project/penpot/task/8658
.custom-input-checkbox.custom-input-checkbox {
align-items: flex-start;
}

View File

@@ -9,8 +9,7 @@
[app.config :as cf]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.ds.forms.input :refer [input*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg* raw-svg-list]]
[app.main.ui.ds.foundations.typography :refer [typography-list]]
@@ -34,7 +33,6 @@
:Input input*
:Loader loader*
:RawSvg raw-svg*
:Select select*
:Text text*
:TabSwitcher tab-switcher*
:Toast toast*

View File

@@ -9,6 +9,4 @@
// TODO: create actual tokens once we have them from design
$sz-16: px2rem(16);
$sz-32: px2rem(32);
$sz-36: px2rem(36);
$sz-224: px2rem(224);
$sz-400: px2rem(400);

View File

@@ -1,245 +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) KALEIDOS INC
(ns app.main.ui.ds.controls.select
(:require-macros
[app.common.data.macros :as dm]
[app.main.style :as stl])
(:require
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i]
[app.util.array :as array]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[rumext.v2 :as mf]))
(mf/defc option*
{::mf/props :obj
::mf/private true}
[{:keys [id label icon aria-label on-click selected set-ref focused] :rest props}]
[:> :li {:value id
:class (stl/css-case :option true
:option-with-icon (some? icon)
:option-current focused)
:aria-selected selected
:ref (fn [node]
(set-ref node id))
:role "option"
:id id
:on-click on-click
:data-id id}
(when (some? icon)
[:> icon*
{:id icon
:size "s"
:class (stl/css :option-icon)
:aria-hidden (when label true)
:aria-label (when (not label) aria-label)}])
[:span {:class (stl/css :option-text)} label]
(when selected
[:> icon*
{:id i/tick
:size "s"
:class (stl/css :option-check)
:aria-hidden (when label true)}])])
(mf/defc options-dropdown*
{::mf/props :obj
::mf/private true}
[{:keys [set-ref on-click options selected focused] :rest props}]
(let [props (mf/spread-props props
{:class (stl/css :option-list)
:tab-index "-1"
:role "listbox"})]
[:> "ul" props
(for [option ^js options]
(let [id (obj/get option "id")
label (obj/get option "label")
aria-label (obj/get option "aria-label")
icon (obj/get option "icon")]
[:> option* {:selected (= id selected)
:key id
:id id
:label label
:icon icon
:aria-label aria-label
:set-ref set-ref
:focused (= id focused)
:on-click on-click}]))]))
(def ^:private schema:select-option
[:and
[:map {:title "option"}
[:id :string]
[:icon {:optional true}
[:and :string [:fn #(contains? icon-list %)]]]
[:label {:optional true} :string]
[:aria-label {:optional true} :string]]
[:fn {:error/message "invalid data: missing required props"}
(fn [option]
(or (and (contains? option :icon)
(or (contains? option :label)
(contains? option :aria-label)))
(contains? option :label)))]])
(def ^:private schema:select
[:map
[:disabled {:optional true} :boolean]
[:class {:optional true} :string]
[:icon {:optional true}
[:and :string [:fn #(contains? icon-list %)]]]
[:default-selected {:optional true} :string]
[:options [:vector {:min 1} schema:select-option]]])
(defn- get-option
[options id]
(or (array/find #(= id (obj/get % "id")) options)
(aget options 0)))
(defn- get-selected-option-id
[options default]
(let [option (get-option options default)]
(obj/get option "id")))
(defn- handle-focus-change
[options focused* new-index options-nodes-refs]
(let [option (aget options new-index)
id (obj/get option "id")
nodes (mf/ref-val options-nodes-refs)
node (obj/get nodes id)]
(reset! focused* id)
(dom/scroll-into-view-if-needed! node)))
(defn- handle-selection
[focused* selected* open*]
(when-let [focused (deref focused*)]
(reset! selected* focused))
(reset! open* false)
(reset! focused* nil))
(mf/defc select*
{::mf/props :obj
::mf/schema schema:select}
[{:keys [disabled default-selected on-change options class] :rest props}]
(let [open* (mf/use-state false)
open (deref open*)
on-click
(mf/use-fn
(mf/deps disabled)
(fn [event]
(dom/stop-propagation event)
(when-not disabled
(swap! open* not))))
selected* (mf/use-state #(get-selected-option-id options default-selected))
selected (deref selected*)
focused* (mf/use-state nil)
focused (deref focused*)
on-option-click
(mf/use-fn
(mf/deps on-change)
(fn [event]
(let [node (dom/get-current-target event)
id (dom/get-data node "id")]
(reset! selected* id)
(reset! focused* nil)
(reset! open* false)
(when (fn? on-change)
(on-change id)))))
options-nodes-refs (mf/use-ref nil)
options-ref (mf/use-ref nil)
set-ref
(mf/use-fn
(fn [node id]
(let [refs (or (mf/ref-val options-nodes-refs) #js {})
refs (if node
(obj/set! refs id node)
(obj/unset! refs id))]
(mf/set-ref-val! options-nodes-refs refs))))
on-blur
(mf/use-fn
(fn [event]
(let [click-outside (nil? (.-relatedTarget event))]
(when click-outside
(reset! focused* nil)
(reset! open* false)))))
on-key-down
(mf/use-fn
(mf/deps focused disabled)
(fn [event]
(when-not disabled
(let [options (mf/ref-val options-ref)
len (alength options)
index (array/find-index #(= (deref focused*) (obj/get % "id")) options)]
(dom/stop-propagation event)
(cond
(kbd/home? event)
(handle-focus-change options focused* 0 options-nodes-refs)
(kbd/up-arrow? event)
(handle-focus-change options focused* (mod (- index 1) len) options-nodes-refs)
(kbd/down-arrow? event)
(handle-focus-change options focused* (mod (+ index 1) len) options-nodes-refs)
(or (kbd/space? event) (kbd/enter? event))
(when (deref open*)
(dom/prevent-default event)
(handle-selection focused* selected* open*))
(kbd/esc? event)
(do (reset! open* false)
(reset! focused* nil)))))))
class (dm/str class " " (stl/css :select))
props (mf/spread-props props {:class class
:role "combobox"
:aria-controls "listbox"
:aria-haspopup "listbox"
:aria-activedescendant focused
:aria-expanded open
:on-key-down on-key-down
:disabled disabled
:on-click on-click
:on-blur on-blur})
selected-option (get-option options selected)
label (obj/get selected-option "label")
icon (obj/get selected-option "icon")]
(mf/with-effect [options]
(mf/set-ref-val! options-ref options))
[:div {:class (stl/css :select-wrapper)}
[:> :button props
[:span {:class (stl/css-case :select-header true
:header-icon (some? icon))}
(when icon
[:> icon* {:id icon
:size "s"
:aria-hidden true}])
[:span {:class (stl/css :header-label)}
label]]
[:> icon* {:id i/arrow
:class (stl/css :arrow)
:size "s"
:aria-hidden true}]]
(when open
[:> options-dropdown* {:on-click on-option-click
:options options
:selected selected
:focused focused
:set-ref set-ref}])]))

View File

@@ -1,63 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as SelectStories from "./select.stories";
<Meta title="Controls/Select" />
# Select
Select lets users choose one option from an options menu.
## Variants
**Text**: We will use this variant when there are enough space and icons don't add any useful context.
<Canvas of={SelectStories.Default} />
**Icon and text**: We will use this variant when there are enough space and icons add any useful context.
<Canvas of={SelectStories.WithIcons} />
## Technical notes
### Icons
Each option of `select*` may accept an `icon`, which must contain an [icon ID](../foundations/assets/icon.mdx).
These are available in the `app.main.ds.foundations.assets.icon` namespace.
```clj
(ns app.main.ui.foo
(:require
[app.main.ui.ds.foundations.assets.icon :as i]))
```
```clj
[:> select*
{:options [{ :label "Code"
:id "option-code"
:icon i/fill-content }
{ :label "Design"
:id "option-design"
:icon i/pentool }
{ :label "Menu"
:id "option-menu" }
]}]
```
<Canvas of={SelectStories.WithIcons} />
## Usage guidelines (design)
### Where to use
Used in a wide range of applications in the app,
to select among available text-based options,
sometimes with icons that offers additional context.
### When to use
Consider using select when you have 5 or more options to choose from.
### Interaction / Behavior
When the user clicks on the clickable area, a list of
options appears. When an option is chosen, the list is closed.

View File

@@ -1,147 +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) KALEIDOS INC
@use "../_borders.scss" as *;
@use "../_sizes.scss" as *;
@use "../typography.scss" as *;
.select-wrapper {
--select-icon-fg-color: var(--color-foreground-secondary);
--select-fg-color: var(--color-foreground-primary);
--select-bg-color: var(--color-background-tertiary);
--select-outline-color: none;
--select-border-color: none;
--select-dropdown-border-color: var(--color-background-quaternary);
&:hover {
--select-bg-color: var(--color-background-quaternary);
}
@include use-typography("body-small");
position: relative;
display: grid;
grid-template-rows: auto;
gap: var(--sp-xxs);
width: 100%;
}
.select {
&:focus-visible {
--select-outline-color: var(--color-accent-primary);
}
&:disabled {
--select-bg-color: var(--color-background-primary);
--select-border-color: var(--color-background-quaternary);
--select-fg-color: var(--color-foreground-secondary);
}
display: grid;
grid-template-columns: 1fr auto;
gap: var(--sp-xs);
height: $sz-32;
width: 100%;
padding: var(--sp-s);
border: none;
border-radius: $br-8;
outline: $b-1 solid var(--select-outline-color);
border: $b-1 solid var(--select-border-color);
background: var(--select-bg-color);
color: var(--select-fg-color);
appearance: none;
}
.arrow {
color: var(--select-icon-fg-color);
transform: rotate(90deg);
}
.select-header {
display: grid;
justify-items: start;
gap: var(--sp-xs);
}
.header-label {
@include use-typography("body-small");
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
min-width: 0;
padding-inline-start: var(--sp-xxs);
text-align: left;
color: var(--select-fg-color);
}
.header-icon {
grid-template-columns: auto 1fr;
color: var(--select-icon-fg-color);
}
.option-list {
--options-dropdown-bg-color: var(--color-background-tertiary);
position: absolute;
right: 0;
top: $sz-36;
width: 100%;
background-color: var(--options-dropdown-bg-color);
border-radius: $br-8;
border: $b-1 solid var(--select-dropdown-border-color);
padding-block: var(--sp-xs);
margin-block-end: 0;
max-height: $sz-400;
overflow-y: auto;
overflow-x: hidden;
}
.option {
--select-option-fg-color: var(--color-foreground-primary);
--select-option-bg-color: unset;
&:hover {
--select-option-bg-color: var(--color-background-quaternary);
}
&[aria-selected="true"] {
--select-option-bg-color: var(--color-background-quaternary);
}
display: grid;
align-items: center;
justify-items: start;
grid-template-columns: 1fr auto;
gap: var(--sp-xs);
width: 100%;
height: $sz-32;
padding: var(--sp-s);
border-radius: $br-8;
outline: $b-1 solid var(--select-outline-color);
outline-offset: -1px;
background-color: var(--select-option-bg-color);
}
.option-with-icon {
grid-template-columns: auto 1fr auto;
}
.option-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
min-width: 0;
padding-inline-start: var(--sp-xxs);
}
.option-icon {
color: var(--select-icon-fg-color);
}
.option-current {
--select-option-outline-color: var(--color-accent-primary);
outline: $b-1 solid var(--select-option-outline-color);
}

View File

@@ -1,65 +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) KALEIDOS INC
import * as React from "react";
import Components from "@target/components";
const { Select } = Components;
export default {
title: "Controls/Select",
component: Select,
argTypes: {
disabled: { control: "boolean" },
},
args: {
disabled: false,
options: [
{
label: "Code",
id: "option-code",
},
{
label: "Design",
id: "option-design",
},
{
label: "Menu",
id: "opeion-menu",
},
],
defaultSelected: "option-code",
},
parameters: {
controls: {
exclude: ["options", "defaultSelected"],
},
},
render: ({ ...args }) => <Select {...args} />,
};
export const Default = {};
export const WithIcons = {
args: {
options: [
{
label: "Code",
id: "option-code",
icon: "fill-content",
},
{
label: "Design",
id: "option-design",
icon: "pentool",
},
{
label: "Menu",
id: "option-menu",
},
],
},
};

View File

@@ -4,7 +4,7 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.ds.controls.input
(ns app.main.ui.ds.forms.input
(:require-macros
[app.common.data.macros :as dm]
[app.main.style :as stl])

View File

@@ -1,7 +1,7 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as InputStories from "./input.stories";
<Meta title="Controls/Input" />
<Meta title="Forms/Input" />
# Input

View File

@@ -1,9 +1,3 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "../_borders.scss" as *;
@use "../_sizes.scss" as *;
@use "../typography.scss" as *;

View File

@@ -11,7 +11,7 @@ const { Input } = Components;
const { icons } = Components.meta;
export default {
title: "Controls/Input",
title: "Forms/Input",
component: Components.Input,
argTypes: {
icon: {

View File

@@ -116,9 +116,9 @@
(mf/defc tab-switcher*
{::mf/props :obj
::mf/schema schema:tab-switcher}
[{:keys [class tabs on-change-tab default-selected action-button-position action-button] :rest props}]
(let [selected* (mf/use-state #(get-selected-tab-id tabs default-selected))
selected (deref selected*)
[{:keys [class tabs on-change-tab default-selected selected action-button-position action-button] :rest props}]
(let [selected* (mf/use-state #(or selected (get-selected-tab-id tabs default-selected)))
selected (or selected (deref selected*))
tabs-nodes-refs (mf/use-ref nil)
tabs-ref (mf/use-ref nil)
@@ -175,8 +175,7 @@
class (dm/str class " " (stl/css :tabs))
props (mf/spread-props props {:class class
:on-key-down on-key-down})]
props (mf/spread-props props {:class class})]
(mf/with-effect [tabs]
(mf/set-ref-val! tabs-ref tabs))
@@ -188,6 +187,7 @@
:tabs tabs
:on-ref on-ref
:selected selected
:on-key-down on-key-down
:on-click on-click}]]
(let [active-tab (get-tab tabs selected)

View File

@@ -101,7 +101,17 @@
}
.tab-panel {
--tab-panel-outline-color: none;
&:focus {
outline: none;
}
&:focus-visible {
--tab-panel-outline-color: var(--color-accent-primary);
}
display: grid;
width: 100%;
height: 100%;
outline: $b-1 solid var(--tab-panel-outline-color);
}

View File

@@ -369,7 +369,9 @@
selected (:selected state)
status (:status state)
;; We've deprecated the merge option on non-binary files because it wasn't working
;; and we're planning to remove this export in future releases.
export-types (if binary? export-types [:all :detach])
start-export
(mf/use-fn

View File

@@ -260,6 +260,14 @@
(when ^boolean obj
(apply (.-f obj) args)))))))
(defn use-ref-value
"Returns a ref that will be automatically updated when the value is changed"
[v]
(let [ref (mf/use-ref v)]
(mf/with-effect [v]
(mf/set-ref-val! ref v))
ref))
(defn use-equal-memo
[val]
(let [ref (mf/use-ref nil)]

View File

@@ -372,6 +372,7 @@
[:& fm/image-radio-buttons {:options start-options
:img-width "159px"
:img-height "120px"
:class (stl/css :image-radio)
:name :start-with
:on-change on-start-change}]

View File

@@ -57,7 +57,7 @@
(def ^:private schema:invite-form
[:map {:title "InviteForm"}
[:role :keyword]
[:emails [::sm/set {:kind ::sm/email}]]])
[:emails {:optional true} [::sm/set {:kind ::sm/email}]]])
(defn- get-available-roles
[]

View File

@@ -106,9 +106,9 @@
(st/emit! (rt/navigated match))
:else
;; We just recheck with an additional profile request; this avoids
;; some race conditions that causes unexpected redirects on
;; invitations workflows (and probably other cases).
;; We just recheck with an additional profile request; this
;; avoids some race conditions that causes unexpected redirects
;; on invitations workflows (and probably other cases).
(->> (rp/cmd! :get-profile)
(rx/subs! (fn [{:keys [id] :as profile}]
(cond

View File

@@ -7,19 +7,21 @@
(ns app.main.ui.static
(:require-macros [app.main.style :as stl])
(:require
["rxjs" :as rxjs]
[app.common.data :as d]
[app.common.pprint :as pp]
[app.common.uri :as u]
[app.main.data.common :as dc]
[app.main.data.events :as ev]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.auth.login :refer [login-methods]]
[app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]]
[app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page terms-register]]
[app.main.ui.auth.register :as register]
[app.main.ui.dashboard.sidebar :refer [sidebar]]
[app.main.ui.icons :as i]
[app.main.ui.viewer.header :as header]
[app.main.ui.viewer.header :as viewer.header]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
@@ -29,10 +31,14 @@
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
;; FIXME: this is a workaround until we export this class on beicon library
(def TimeoutError rxjs/TimeoutError)
(mf/defc error-container*
{::mf/props :obj}
[{:keys [children]}]
(let [profile-id (:profile-id @st/state)]
(let [profile-id (:profile-id @st/state)
on-nav-root (mf/use-fn #(st/emit! (rt/nav-root)))]
[:section {:class (stl/css :exception-layout)}
[:button
{:class (stl/css :exception-header)
@@ -44,7 +50,7 @@
[:div {:class (stl/css :deco-before)} i/logo-error-screen]
(when-not profile-id
[:button {:class (stl/css :login-header)
:on-click rt/nav-root}
:on-click on-nav-root}
(tr "labels.login")])
[:div {:class (stl/css :exception-content)}
@@ -65,8 +71,8 @@
{::mf/props :obj}
[{:keys [show-dialog]}]
(let [current-section (mf/use-state :login)
user-email (mf/use-state "")
register-token (mf/use-state "")
user-email (mf/use-state "")
register-token (mf/use-state "")
set-section
(mf/use-fn
@@ -85,29 +91,37 @@
#(reset! current-section :login))
success-login
(fn []
(reset! show-dialog false)
(.reload js/window.location true))
(mf/use-fn
(fn []
(reset! show-dialog false)
(st/emit! (rt/reload true))))
success-register
(fn [data]
(reset! register-token (:token data))
(reset! current-section :register-validate))
(mf/use-fn
(fn [data]
(reset! register-token (:token data))
(reset! current-section :register-validate)))
register-email-sent
(fn [email]
(reset! user-email email)
(reset! current-section :register-email-sent))
(mf/use-fn
(fn [email]
(reset! user-email email)
(reset! current-section :register-email-sent)))
recovery-email-sent
(fn [email]
(reset! user-email email)
(reset! current-section :recovery-email-sent))]
(mf/use-fn
(fn [email]
(reset! user-email email)
(reset! current-section :recovery-email-sent)))
on-nav-root
(mf/use-fn #(st/emit! (rt/nav-root)))]
[:div {:class (stl/css :overlay)}
[:div {:class (stl/css :dialog-login)}
[:div {:class (stl/css :modal-close)}
[:button {:class (stl/css :modal-close-button) :on-click rt/nav-root}
[:button {:class (stl/css :modal-close-button)
:on-click on-nav-root}
i/close]]
[:div {:class (stl/css :login)}
[:div {:class (stl/css :logo)} i/logo]
@@ -118,19 +132,21 @@
[:div {:class (stl/css :logo-title)} (tr "labels.login")]
[:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.free")]
[:& login-methods {:on-recovery-request set-section-recovery
:on-success-callback success-login}]
:on-success-callback success-login
:params {:save-login-redirect true}}]
[:hr {:class (stl/css :separator)}]
[:div {:class (stl/css :change-section)}
(tr "auth.register")
" "
[:a {:data-section "register"
:on-click set-section} (tr "auth.register-submit")]]]
:on-click set-section}
(tr "auth.register-submit")]]]
:register
[:*
[:div {:class (stl/css :logo-title)} (tr "not-found.login.signup-free")]
[:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.start-using")]
[:& register-methods {:on-success-callback success-register :hide-separator true}]
[:& register/register-methods {:on-success-callback success-register :hide-separator true}]
#_[:hr {:class (stl/css :separator)}]
[:div {:class (stl/css :separator)}]
[:div {:class (stl/css :change-section)}
@@ -140,12 +156,13 @@
:on-click set-section} (tr "auth.login-here")]]
[:div {:class (stl/css :links)}
[:hr {:class (stl/css :separator)}]
[:& terms-register]]]
[:& register/terms-register]]]
:register-validate
[:div {:class (stl/css :form-container)}
[:& register-validate-form {:params {:token @register-token}
:on-success-callback register-email-sent}]
[:& register/register-validate-form
{:params {:token @register-token}
:on-success-callback register-email-sent}]
[:div {:class (stl/css :links)}
[:div {:class (stl/css :register)}
[:a {:data-section "register"
@@ -154,7 +171,7 @@
:register-email-sent
[:div {:class (stl/css :form-container)}
[:& register-success-page {:params {:email @user-email :hide-logo true}}]]
[:& register/register-success-page {:params {:email @user-email :hide-logo true}}]]
:recovery-request
[:& recovery-request-page {:go-back-callback set-section-login
@@ -166,43 +183,57 @@
(mf/defc request-dialog
{::mf/props :obj}
[{:keys [title content button-text on-button-click cancel-text]}]
(let [on-click (or on-button-click rt/nav-root)]
[{:keys [title content button-text on-button-click cancel-text on-close]}]
(let [on-click (or on-button-click on-close)]
[:div {:class (stl/css :overlay)}
[:div {:class (stl/css :dialog)}
[:div {:class (stl/css :modal-close)}
[:button {:class (stl/css :modal-close-button) :on-click rt/nav-root}
[:button {:class (stl/css :modal-close-button) :on-click on-close}
i/close]]
[:div {:class (stl/css :dialog-title)} title]
(for [txt content]
[:div txt])
(for [[index content] (d/enumerate content)]
[:div {:key index} content])
[:div {:class (stl/css :sign-info)}
(when cancel-text
[:button {:class (stl/css :cancel-button) :on-click rt/nav-root} cancel-text])
[:button {:class (stl/css :cancel-button)
:on-click on-close}
cancel-text])
[:button {:on-click on-click} button-text]]]]))
(mf/defc request-access
{::mf/props :obj}
[{:keys [file-id team-id is-default workspace?]}]
(let [profile (:profile @st/state)
(let [profile (mf/deref refs/profile)
requested* (mf/use-state {:sent false :already-requested false})
requested (deref requested*)
show-dialog (mf/use-state true)
on-close
(mf/use-fn
(mf/deps profile)
(fn []
(st/emit! (rt/nav :dashboard-projects {:team-id (:default-team-id profile)}))))
on-success
(mf/use-fn
#(reset! requested* {:sent true :already-requested false}))
on-error
(mf/use-fn
#(reset! requested* {:sent true :already-requested true}))
on-request-access
(mf/use-fn
(mf/deps file-id team-id workspace?)
(fn []
(let [params (if (some? file-id) {:file-id file-id :is-viewer (not workspace?)} {:team-id team-id})
mdata {:on-success on-success :on-error on-error}]
(st/emit! (dc/create-team-access-request (with-meta params mdata))))))]
(let [params (if (some? file-id)
{:file-id file-id
:is-viewer (not workspace?)}
{:team-id team-id})
mdata {:on-success on-success
:on-error on-error}]
(st/emit! (dc/create-team-access-request
(with-meta params mdata))))))]
[:*
(if (some? file-id)
@@ -214,17 +245,24 @@
[:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")]
[:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]]
[:div {:class (stl/css :workspace-right)}]]
[:div {:class (stl/css :viewer)}
[:& header/header {:project {:name (tr "not-found.no-permission.project-name")}
:index 0
:file {:name (tr "not-found.no-permission.penpot-file")}
:page nil
:frame nil
:permissions {:is-logged true}
:zoom 1
:section :interactions
:shown-thumbnails false
:interactions-mode nil}]])
;; FIXME: the viewer header was never designed to be reused
;; from other parts of the application, and this code looks
;; like a fast workaround reusing it as-is without a proper
;; component adaptation for be able to use it easily it on
;; viewer context or static error page context
[:& viewer.header/header {:project
{:name (tr "not-found.no-permission.project-name")}
:index 0
:file {:name (tr "not-found.no-permission.penpot-file")}
:page nil
:frame nil
:permissions {:is-logged true}
:zoom 1
:section :interactions
:shown-thumbnails false
:interactions-mode nil}]])
[:div {:class (stl/css :dashboard)}
[:div {:class (stl/css :dashboard-sidebar)}
@@ -242,22 +280,27 @@
[:& login-dialog {:show-dialog show-dialog}]
is-default
[:& request-dialog {:title (tr "not-found.no-permission.project") :button-text (tr "not-found.no-permission.go-dashboard")}]
[:& request-dialog {:title (tr "not-found.no-permission.project")
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(and (some? file-id) (:already-requested requested))
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.file")
:content [(tr "not-found.no-permission.already-requested.or-others.file")]
:button-text (tr "not-found.no-permission.go-dashboard")}]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(:already-requested requested)
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.project")
:content [(tr "not-found.no-permission.already-requested.or-others.project")]
:button-text (tr "not-found.no-permission.go-dashboard")}]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(:sent requested)
[:& request-dialog {:title (tr "not-found.no-permission.done.success")
:content [(tr "not-found.no-permission.done.remember")]
:button-text (tr "not-found.no-permission.go-dashboard")}]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(some? file-id)
[:& request-dialog {:title (tr "not-found.no-permission.file")
@@ -265,7 +308,8 @@
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")}]
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(some? team-id)
[:& request-dialog {:title (tr "not-found.no-permission.project")
@@ -273,7 +317,8 @@
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")}]))]))
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]))]))
(mf/defc not-found*
[]
@@ -379,64 +424,92 @@
[:div {:class (stl/css :sign-info)}
[:button {:on-click on-reset} (tr "labels.retry")]]]))
(defn- load-info
"Load exception page info"
[path-params]
(let [default {:loaded true}
stream (cond
(:file-id path-params)
(->> (rp/cmd! :get-file-info {:id (:file-id path-params)})
(rx/map (fn [info]
{:loaded true
:file-id (:id info)})))
(:team-id path-params)
(->> (rp/cmd! :get-team-info {:id (:team-id path-params)})
(rx/map (fn [info]
{:loaded true
:team-id (:id info)
:team-default (:is-default info)})))
:else
(rx/of default))]
(->> stream
(rx/timeout 3000)
(rx/catch (fn [cause]
(if (instance? TimeoutError cause)
(rx/of default)
(rx/throw cause)))))))
(mf/defc exception-page*
{::mf/props :obj}
[{:keys [data route] :as props}]
(let [file-info (mf/use-state {:pending true})
team-info (mf/use-state {:pending true})
type (:type data)
path (:path route)
(let [type (:type data)
path (:path route)
workspace? (str/includes? path "workspace")
dashboard? (str/includes? path "dashboard")
view? (str/includes? path "view")
query-params (:query-params route)
path-params (:path-params route)
request-access? (and
(or workspace? dashboard? view?)
(or (not (str/empty? (:file-id @file-info))) (not (str/empty? (:team-id @team-info)))))
workspace? (str/includes? path "workspace")
dashboard? (str/includes? path "dashboard")
view? (str/includes? path "view")
query-params (u/map->query-string (:query-params route))
pparams (:path-params route)
on-file-info (mf/use-fn
(fn [info]
(reset! file-info {:file-id (:id info)})))
on-team-info (mf/use-fn
(fn [info]
(reset! team-info {:team-id (:id info) :is-default (:is-default info)})))]
;; We stora the request access info int this state
info* (mf/use-state nil)
info (deref info*)
(mf/with-effect [type path query-params pparams @file-info @team-info]
(st/emit! (ptk/event ::ev/event {::ev/name "exception-page" :type type :path path :query-params query-params}))
loaded? (get info :loaded false)
(when (and (:file-id pparams) (:pending @file-info))
(->> (rp/cmd! :get-file-info {:id (:file-id pparams)})
(rx/subs! on-file-info)))
request-access?
(and
(or workspace? dashboard? view?)
(or (:file-id info)
(:team-id info)))]
(when (and (:team-id pparams) (:pending @team-info))
(->> (rp/cmd! :get-team-info {:id (:team-id pparams)})
(rx/subs! on-team-info))))
(mf/with-effect [type path query-params path-params]
(let [query-params (u/map->query-string query-params)
event-params {::ev/name "exception-page"
:type type
:path path
:query-params query-params}]
(st/emit! (ptk/event ::ev/event event-params))))
(case (:type data)
:not-found
(mf/with-effect [path-params info]
(when-not (:loaded info)
(->> (load-info path-params)
(rx/subs! (partial reset! info*)))))
(when loaded?
(if request-access?
[:& request-access {:file-id (:file-id @file-info)
:team-id (:team-id @team-info)
:is-default (:is-default @team-info)
[:& request-access {:file-id (:file-id info)
:team-id (:team-id info)
:is-default (:team-default info)
:workspace? workspace?}]
[:> not-found* {}])
:authentication
(if request-access?
[:& request-access {:file-id (:file-id @file-info)
:team-id (:team-id @team-info)
:is-default (:is-default @team-info)
:workspace? workspace?}]
[:> not-found* {}])
(case (:type data)
:not-found
[:> not-found* {}]
:bad-gateway
[:> bad-gateway* props]
:authentication
[:> not-found* {}]
:service-unavailable
[:& service-unavailable]
:bad-gateway
[:> bad-gateway* props]
[:> internal-error* props])))
:service-unavailable
[:& service-unavailable]
[:> internal-error* props])))))

View File

@@ -244,7 +244,8 @@
(fn [result]
(reset! images-data* result)))))
[:div {:class (stl/css :element-options)}
[:div {:class (stl/css-case :element-options true
:viewer-code-block (= :viewer from))}
[:div {:class (stl/css :attributes-block)}
[:button {:class (stl/css :download-button)
:on-click handle-copy-all-code}

View File

@@ -9,7 +9,7 @@
.element-options {
display: flex;
flex-direction: column;
height: calc(100vh - #{$s-128}); // TODO: Fix this hardcoded value
height: calc(100vh - #{$s-160}); // TODO: Fix this hardcoded value
overflow: hidden;
padding-bottom: $s-16;
overflow-y: auto;
@@ -17,6 +17,10 @@
scrollbar-gutter: stable;
}
.viewer-code-block {
height: calc(100vh - #{$s-108}); // TODO: Fix this hardcoded value
}
.download-button {
@extend .button-secondary;
@include uppercaseTitleTipography;

View File

@@ -136,7 +136,8 @@
:else
[:div {:class (stl/css :settings-bar-content)}
[:> tab-switcher* {:tabs tabs
:default-selected (dm/str section)
:default-selected "layers"
:selected (name section)
:on-change-tab on-tab-change
:class (stl/css :left-sidebar-tabs)
:action-button-position "start"

View File

@@ -42,7 +42,7 @@
(cond-> color
(:value color) (assoc :color (:value color) :opacity 1)
(:value color) (dissoc :value)
true (assoc :file-id file-id)))
:always (assoc :file-id file-id)))
color-id (:id color)
@@ -70,7 +70,7 @@
(fn [event]
(st/emit!
(dwl/add-recent-color color)
(dc/apply-color-from-palette (merge uc/empty-color color) (kbd/alt? event)))))
(dc/apply-color-from-palette color (kbd/alt? event)))))
rename-color
(mf/use-fn

View File

@@ -14,6 +14,7 @@
[app.main.data.workspace.selection :as dws]
[app.main.store :as st]
[app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.hooks :as h]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
@@ -21,82 +22,96 @@
(defn- prepare-colors
[shapes file-id shared-libs]
(let [data (into [] (remove nil? (ctc/extract-all-colors shapes file-id shared-libs)))
grouped-colors (group-by :attrs data)
groups (d/group-by :attrs #(dissoc % :attrs) data)
all-colors (distinct (mapv :attrs data))
tmp (group-by #(some? (:id %)) all-colors)
library-colors (get tmp true)
colors (get tmp false)]
{:grouped-colors grouped-colors
{:groups groups
:all-colors all-colors
:colors colors
:library-colors library-colors}))
(def xf:map-shape-id
(map :shape-id))
(mf/defc color-selection-menu
{::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]
::mf/wrap-props false}
[{:keys [shapes file-id shared-libs]}]
(let [{:keys [grouped-colors library-colors colors]} (mf/with-memo [shapes file-id shared-libs]
(prepare-colors shapes file-id shared-libs))
(let [{:keys [groups library-colors colors]} (mf/with-memo [shapes file-id shared-libs]
(prepare-colors shapes file-id shared-libs))
state* (mf/use-state true)
open? (deref state*)
state* (mf/use-state true)
open? (deref state*)
has-colors? (or (some? (seq colors)) (some? (seq library-colors)))
has-colors? (or (some? (seq colors)) (some? (seq library-colors)))
toggle-content (mf/use-fn #(swap! state* not))
toggle-content (mf/use-fn #(swap! state* not))
expand-lib-color (mf/use-state false)
expand-color (mf/use-state false)
grouped-colors* (mf/use-var nil)
prev-colors* (mf/use-var [])
groups-ref (h/use-ref-value groups)
prev-colors-ref (mf/use-ref nil)
;; grouped-colors* (mf/use-var nil)
;; prev-colors* (mf/use-var [])
on-change
(mf/use-fn
(fn [new-color old-color from-picker?]
(let [old-color (-> old-color (dissoc :name :path) d/without-nils)
(prn "new-color" new-color)
(prn "old-color" old-color)
(let [old-color (-> old-color
(dissoc :name :path)
(d/without-nils))
;; When dragging on the color picker sometimes all
;; the shapes hasn't updated the color to the prev
;; value so we need this extra calculation
shapes-by-old-color (get @grouped-colors* old-color)
prev-color (d/seek #(get @grouped-colors* %) @prev-colors*)
shapes-by-prev-color (get @grouped-colors* prev-color)
shapes-by-color (or shapes-by-prev-color shapes-by-old-color)]
groups (mf/ref-val groups-ref)
prev-colors (mf/ref-val prev-colors-ref)
prev-color (d/seek (partial get groups) prev-colors)
cops-old (get groups old-color)
cops-prev (get groups prev-colors)
cops (or cops-prev cops-old)
old-color (or prev-color old-color)]
(when from-picker?
(swap! prev-colors* conj (-> new-color (dissoc :name :path) d/without-nils)))
(let [color (-> new-color
(dissoc :name :path)
(d/without-nils))]
(mf/set-ref-val! prev-colors-ref
(conj prev-colors color))))
(st/emit! (dc/change-color-in-selected new-color shapes-by-color (or prev-color old-color))))))
(st/emit! (dc/change-color-in-selected cops new-color old-color)))))
on-open
(mf/use-fn
(fn []
(reset! prev-colors* [])))
(mf/use-fn #(mf/set-ref-val! prev-colors-ref []))
on-close
(mf/use-fn
(fn []
(reset! prev-colors* [])))
(mf/use-fn #(mf/set-ref-val! prev-colors-ref []))
on-detach
(mf/use-fn
(fn [color]
(let [shapes-by-color (get @grouped-colors* color)
new-color (assoc color :id nil :file-id nil)]
(st/emit! (dc/change-color-in-selected new-color shapes-by-color color)))))
(let [groups (mf/ref-val groups-ref)
cops (get groups color)
color' (dissoc color :id :file-id)]
(st/emit! (dc/change-color-in-selected cops color' color)))))
select-only
(mf/use-fn
(fn [color]
(let [shapes-by-color (get @grouped-colors* color)
ids (into (d/ordered-set) (map :shape-id) shapes-by-color)]
(let [groups (mf/ref-val groups-ref)
cops (get groups color)
ids (into (d/ordered-set) xf:map-shape-id cops)]
(st/emit! (dws/select-shapes ids)))))]
(mf/with-effect [grouped-colors]
(reset! grouped-colors* grouped-colors))
[:div {:class (stl/css :element-set)}
[:div {:class (stl/css :element-title)}
[:& title-bar {:collapsable has-colors?

View File

@@ -113,9 +113,9 @@
handle-value-change
(mf/use-fn
(mf/deps color on-change)
(fn [new-value]
(fn [value]
(let [color (-> color
(assoc :color new-value)
(assoc :color value)
(dissoc :gradient))]
(st/emit! (dwl/add-recent-color color)
(on-change color)))))
@@ -146,7 +146,9 @@
:else
color)
{:keys [x y]} (dom/get-client-position event)
cpos (dom/get-client-position event)
x (dm/get-prop cpos :x)
y (dm/get-prop cpos :y)
props {:x x
:y y
@@ -154,14 +156,14 @@
:disable-opacity disable-opacity
:disable-image disable-image
;; on-change second parameter means if the source is the color-picker
:on-change #(on-change (merge uc/empty-color %) true)
:on-change #(on-change % true)
:on-close (fn [value opacity id file-id]
(when on-close
(on-close value opacity id file-id)))
:data color}]
(when on-open
(on-open (merge uc/empty-color color)))
(when (fn? on-open)
(on-open color))
(modal/show! :colorpicker props))))

View File

@@ -18,8 +18,9 @@
}
.stroke-options {
@include flexRow;
display: grid;
align-items: center;
gap: $s-4;
grid-template-columns: 1fr 2fr 2fr;
.stroke-width-input-element {
@@ -28,11 +29,14 @@
}
}
.stroke-caps-options {
@include flexRow;
display: grid;
align-items: center;
gap: $s-4;
grid-template-columns: 1fr auto 1fr;
}
.cap-select {
width: $s-124;
width: 100%;
}
.stroke-cap-dropdown,
.stroke-cap-dropdown-start {

View File

@@ -10,12 +10,8 @@
[app.main.features :as features]
[app.main.store :as st]
[app.plugins.api :as api]
[app.plugins.flex :as flex]
[app.plugins.format :as format]
[app.plugins.grid :as grid]
[app.plugins.library :as library]
[app.plugins.public-utils]
[app.plugins.shape :as shape]
[app.plugins.register :as register]
[app.util.globals :refer [global]]
[app.util.object :as obj]
[beicon.v2.core :as rx]
@@ -24,6 +20,7 @@
(defn init-plugins-runtime!
[]
(when-let [init-runtime (obj/get global "initPluginsRuntime")]
(register/init)
(init-runtime (fn [plugin-id] (api/create-context plugin-id)))))
(defn initialize
@@ -39,10 +36,3 @@
(rx/tap init-plugins-runtime!)
(rx/ignore)))))
;; Prevent circular dependency
(set! flex/shape-proxy? shape/shape-proxy?)
(set! grid/shape-proxy? shape/shape-proxy?)
(set! format/shape-proxy shape/shape-proxy)
(set! shape/lib-typography-proxy? library/lib-typography-proxy?)
(set! shape/lib-component-proxy library/lib-component-proxy)

View File

@@ -227,7 +227,7 @@
ids (into #{} (map #(obj/get % "$id")) shapes)]
(st/emit! (dwg/ungroup-shapes ids)))))
(createBoard
(createFrame
[_]
(create-shape $plugin :frame))

View File

@@ -31,22 +31,14 @@
"mixed"
value))
;; export type Point = { x: number; y: number };
;; export type PenpotPoint = { x: number; y: number };
(defn format-point
[{:keys [x y] :as point}]
(when (some? point)
(obj/without-empty
#js {:x x :y y})))
(defn shape-type
[type]
(case type
:frame "board"
:rect "rectangle"
:circle "ellipse"
(d/name type)))
;;export type Bounds = {
;;export type PenpotBounds = {
;; x: number;
;; y: number;
;; width: number;
@@ -58,7 +50,7 @@
(obj/without-empty
#js {:x x :y y :width width :height height})))
;; export interface ColorShapeInfoEntry {
;; export interface PenpotColorShapeInfoEntry {
;; readonly property: string;
;; readonly index?: number;
;; readonly shapeId: string;
@@ -71,7 +63,7 @@
:index index
:shapeId (dm/str shape-id)})))
;; export type Gradient = {
;; export type PenpotGradient = {
;; type: 'linear' | 'radial';
;; startX: number;
;; startY: number;
@@ -97,7 +89,7 @@
:width width
:stops (format-array format-stop stops)})))
;; export type ImageData = {
;; export type PenpotImageData = {
;; name?: string;
;; width: number;
;; height: number;
@@ -116,7 +108,7 @@
:id (format-id id)
:keepAspectRatio keep-aspect-ratio})))
;; export interface Color {
;; export interface PenpotColor {
;; id?: string;
;; name?: string;
;; path?: string;
@@ -124,8 +116,8 @@
;; opacity?: number;
;; refId?: string;
;; refFile?: string;
;; gradient?: Gradient;
;; image?: ImageData;
;; gradient?: PenpotGradient;
;; image?: PenpotImageData;
;; }
(defn format-color
[{:keys [id name path color opacity ref-id ref-file gradient image] :as color-data}]
@@ -141,7 +133,7 @@
:gradient (format-gradient gradient)
:image (format-image image)})))
;; Color & ColorShapeInfo
;; PenpotColor & PenpotColorShapeInfo
(defn format-color-result
[[color attrs]]
(let [shapes-info (apply array (map format-shape-info attrs))
@@ -150,7 +142,7 @@
color))
;; export interface Shadow {
;; export interface PenpotShadow {
;; id?: string;
;; style?: 'drop-shadow' | 'inner-shadow';
;; offsetX?: number;
@@ -158,7 +150,7 @@
;; blur?: number;
;; spread?: number;
;; hidden?: boolean;
;; color?: Color;
;; color?: PenpotColor;
;; }
(defn format-shadow
[{:keys [id style offset-x offset-y blur spread hidden color] :as shadow}]
@@ -178,13 +170,13 @@
(when (some? shadows)
(format-array format-shadow shadows)))
;;export interface Fill {
;;export interface PenpotFill {
;; fillColor?: string;
;; fillOpacity?: number;
;; fillColorGradient?: Gradient;
;; fillColorGradient?: PenpotGradient;
;; fillColorRefFile?: string;
;; fillColorRefId?: string;
;; fillImage?: ImageData;
;; fillImage?: PenpotImageData;
;;}
(defn format-fill
[{:keys [fill-color fill-opacity fill-color-gradient fill-color-ref-file fill-color-ref-id fill-image] :as fill}]
@@ -209,7 +201,7 @@
(some? fills)
(format-array format-fill fills)))
;; export interface Stroke {
;; export interface PenpotStroke {
;; strokeColor?: string;
;; strokeColorRefFile?: string;
;; strokeColorRefId?: string;
@@ -217,9 +209,9 @@
;; strokeStyle?: 'solid' | 'dotted' | 'dashed' | 'mixed' | 'none' | 'svg';
;; strokeWidth?: number;
;; strokeAlignment?: 'center' | 'inner' | 'outer';
;; strokeCapStart?: StrokeCap;
;; strokeCapEnd?: StrokeCap;
;; strokeColorGradient?: Gradient;
;; strokeCapStart?: PenpotStrokeCap;
;; strokeCapEnd?: PenpotStrokeCap;
;; strokeColorGradient?: PenpotGradient;
;; }
(defn format-stroke
[{:keys [stroke-color stroke-color-ref-file stroke-color-ref-id
@@ -244,7 +236,7 @@
(when (some? strokes)
(format-array format-stroke strokes)))
;; export interface Blur {
;; export interface PenpotBlur {
;; id?: string;
;; type?: 'layer-blur';
;; value?: number;
@@ -259,7 +251,7 @@
:value value
:hidden hidden})))
;; export interface Export {
;; export interface PenpotExport {
;; type: 'png' | 'jpeg' | 'svg' | 'pdf';
;; scale: number;
;; suffix: string;
@@ -277,7 +269,7 @@
(when (some? exports)
(format-array format-export exports)))
;; export interface GuideColumnParams {
;; export interface PenpotFrameGuideColumnParams {
;; color: { color: string; opacity: number };
;; type?: 'stretch' | 'left' | 'center' | 'right';
;; size?: number;
@@ -296,10 +288,10 @@
:itemLength item-length
:gutter gutter})))
;; export interface GuideColumn {
;; export interface PenpotFrameGuideColumn {
;; type: 'column';
;; display: boolean;
;; params: GuideColumnParams;
;; params: PenpotFrameGuideColumnParams;
;; }
(defn format-frame-guide-column
[{:keys [type display params] :as guide}]
@@ -309,10 +301,10 @@
:display display
:params (format-frame-guide-column-params params)})))
;; export interface GuideRow {
;; export interface PenpotFrameGuideRow {
;; type: 'row';
;; display: boolean;
;; params: GuideColumnParams;
;; params: PenpotFrameGuideColumnParams;
;; }
(defn format-frame-guide-row
[{:keys [type display params] :as guide}]
@@ -322,7 +314,7 @@
:display display
:params (format-frame-guide-column-params params)})))
;;export interface GuideSquareParams {
;;export interface PenpotFrameGuideSquareParams {
;; color: { color: string; opacity: number };
;; size?: number;
;;}
@@ -333,10 +325,10 @@
#js {:color (format-color color)
:size size})))
;; export interface GuideSquare {
;; export interface PenpotFrameGuideSquare {
;; type: 'square';
;; display: boolean;
;; params: GuideSquareParams;
;; params: PenpotFrameGuideSquareParams;
;; }
(defn format-frame-guide-square
@@ -360,7 +352,7 @@
(when (some? guides)
(format-array format-frame-guide guides)))
;;interface PathCommand {
;;interface PenpotPathCommand {
;; command:
;; | 'M' | 'move-to'
;; | 'Z' | 'close-path'
@@ -415,10 +407,10 @@
(when (some? content)
(format-array format-command content)))
;; export type TrackType = 'flex' | 'fixed' | 'percent' | 'auto';
;; export type PenpotTrackType = 'flex' | 'fixed' | 'percent' | 'auto';
;;
;; export interface Track {
;; type: TrackType;
;; export interface PenpotTrack {
;; type: PenpotTrackType;
;; value: number | null;
;; }
(defn format-track
@@ -434,13 +426,13 @@
(format-array format-track tracks)))
;; export interface Dissolve {
;; export interface PenpotDissolve {
;; type: 'dissolve';
;; duration: number;
;; easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
;; }
;;
;; export interface Slide {
;; export interface PenpotSlide {
;; type: 'slide';
;; way: 'in' | 'out';
;; direction?:
@@ -453,7 +445,7 @@
;; easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
;; }
;;
;; export interface Push {
;; export interface PenpotPush {
;; type: 'push';
;; direction?:
;; | 'right'
@@ -465,7 +457,7 @@
;; easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
;; }
;;
;; export type Animation = Dissolve | Slide | Push;
;; export type PenpotAnimation = PenpotDissolve | PenpotSlide | PenpotPush;
(defn format-animation
[animation]
@@ -493,24 +485,24 @@
:easing (format-key (:easing animation))}
nil))))
;;export type Action =
;; | NavigateTo
;; | OpenOverlay
;; | ToggleOverlay
;; | CloseOverlay
;; | PreviousScreen
;; | OpenUrl;
;;export type PenpotAction =
;; | PenpotNavigateTo
;; | PenpotOpenOverlay
;; | PenpotToggleOverlay
;; | PenpotCloseOverlay
;; | PenpotPreviousScreen
;; | PenpotOpenUrl;
;;
;;export interface NavigateTo {
;;export interface PenpotNavigateTo {
;; type: 'navigate-to';
;; destination: Board;
;; destination: PenpotFrame;
;; preserveScrollPosition?: boolean;
;; animation: Animation;
;; animation: PenpotAnimation;
;;}
;;
;;export interface OverlayAction {
;; destination: Board;
;; relativeTo?: Shape;
;;export interface PenpotOverlayAction {
;; destination: PenpotFrame;
;; relativeTo?: PenpotShape;
;; position?:
;; | 'manual'
;; | 'center'
@@ -520,31 +512,31 @@
;; | 'bottom-left'
;; | 'bottom-right'
;; | 'bottom-center';
;; manualPositionLocation?: Point;
;; manualPositionLocation?: PenpotPoint;
;; closeWhenClickOutside?: boolean;
;; addBackgroundOverlay?: boolean;
;; animation: Animation;
;; animation: PenpotAnimation;
;;}
;;
;;export interface OpenOverlay extends OverlayAction {
;;export interface PenpotOpenOverlay extends PenpotOverlayAction {
;; type: 'open-overlay';
;;}
;;
;;export interface ToggleOverlay extends OverlayAction {
;;export interface PenpotToggleOverlay extends PenpotOverlayAction {
;; type: 'toggle-overlay';
;;}
;;
;;export interface CloseOverlay {
;;export interface PenpotCloseOverlay {
;; type: 'close-overlay';
;; destination?: Board;
;; animation: Animation;
;; destination?: PenpotFrame;
;; animation: PenpotAnimation;
;;}
;;
;;export interface PreviousScreen {
;;export interface PenpotPreviousScreen {
;; type: 'previous-screen';
;;}
;;
;;export interface OpenUrl {
;;export interface PenpotOpenUrl {
;; type: 'open-url';
;; url: string;
;;}

View File

@@ -413,6 +413,8 @@
(defn lib-typography-proxy? [p]
(instance? LibraryTypographyProxy p))
(set! shape/lib-typography-proxy? lib-typography-proxy?)
(defn lib-typography-proxy
[plugin-id file-id id]
(assert (uuid? file-id))
@@ -756,6 +758,8 @@
value (dm/str value " / " (:name component))]
(st/emit! (dwl/rename-component id value)))))}))
(set! shape/lib-component-proxy lib-component-proxy)
(deftype Library [$plugin $id]
Object

View File

@@ -29,26 +29,17 @@
{:x (obj/get point "x")
:y (obj/get point "y")}))
(defn parse-shape-type
[type]
(case type
"board" :frame
"boolean" :bool
"rectangle" :rect
"ellipse" :circle
(parse-keyword type)))
;; {
;; name?: string;
;; nameLike?: string;
;; type?:
;; | 'board'
;; | 'frame'
;; | 'group'
;; | 'boolean'
;; | 'rectangle'
;; | 'bool'
;; | 'rect'
;; | 'path'
;; | 'text'
;; | 'ellipse'
;; | 'circle'
;; | 'svg-raw'
;; | 'image';
;; }
@@ -58,9 +49,9 @@
(d/without-nils
{:name (obj/get criteria "name")
:name-like (obj/get criteria "nameLike")
:type (-> (obj/get criteria "type") parse-shape-type)})))
:type (-> (obj/get criteria "type") parse-keyword)})))
;;export type ImageData = {
;;export type PenpotImageData = {
;; name?: string;
;; width: number;
;; height: number;
@@ -79,7 +70,7 @@
:mtype (obj/get image-data "mtype")
:keep-aspect-ratio (obj/get image-data "keepApectRatio")})))
;; export type Gradient = {
;; export type PenpotGradient = {
;; type: 'linear' | 'radial';
;; startX: number;
;; startY: number;
@@ -109,7 +100,7 @@
:stops (->> (obj/get gradient "stops")
(mapv parse-gradient-stop))})))
;; export interface Color {
;; export interface PenpotColor {
;; id?: string;
;; name?: string;
;; path?: string;
@@ -117,8 +108,8 @@
;; opacity?: number;
;; refId?: string;
;; refFile?: string;
;; gradient?: Gradient;
;; image?: ImageData;
;; gradient?: PenpotGradient;
;; image?: PenpotImageData;
;; }
(defn parse-color
[^js color]
@@ -134,7 +125,7 @@
:gradient (-> (obj/get color "gradient") parse-gradient)
:image (-> (obj/get color "image") parse-image-data)})))
;; export interface Shadow {
;; export interface PenpotShadow {
;; id?: string;
;; style?: 'drop-shadow' | 'inner-shadow';
;; offsetX?: number;
@@ -142,7 +133,7 @@
;; blur?: number;
;; spread?: number;
;; hidden?: boolean;
;; color?: Color;
;; color?: PenpotColor;
;; }
(defn parse-shadow
[^js shadow]
@@ -162,13 +153,13 @@
(when (some? shadows)
(into [] (map parse-shadow) shadows)))
;;export interface Fill {
;;export interface PenpotFill {
;; fillColor?: string;
;; fillOpacity?: number;
;; fillColorGradient?: Gradient;
;; fillColorGradient?: PenpotGradient;
;; fillColorRefFile?: string;
;; fillColorRefId?: string;
;; fillImage?: ImageData;
;; fillImage?: PenpotImageData;
;;}
(defn parse-fill
[^js fill]
@@ -186,7 +177,7 @@
(when (some? fills)
(into [] (map parse-fill) fills)))
;; export interface Stroke {
;; export interface PenpotStroke {
;; strokeColor?: string;
;; strokeColorRefFile?: string;
;; strokeColorRefId?: string;
@@ -194,9 +185,9 @@
;; strokeStyle?: 'solid' | 'dotted' | 'dashed' | 'mixed' | 'none' | 'svg';
;; strokeWidth?: number;
;; strokeAlignment?: 'center' | 'inner' | 'outer';
;; strokeCapStart?: StrokeCap;
;; strokeCapEnd?: StrokeCap;
;; strokeColorGradient?: Gradient;
;; strokeCapStart?: PenpotStrokeCap;
;; strokeCapEnd?: PenpotStrokeCap;
;; strokeColorGradient?: PenpotGradient;
;; }
(defn parse-stroke
[^js stroke]
@@ -218,7 +209,7 @@
(when (some? strokes)
(into [] (map parse-stroke) strokes)))
;; export interface Blur {
;; export interface PenpotBlur {
;; id?: string;
;; type?: 'layer-blur';
;; value?: number;
@@ -234,7 +225,7 @@
:hidden (obj/get blur "hidden")})))
;; export interface Export {
;; export interface PenpotExport {
;; type: 'png' | 'jpeg' | 'svg' | 'pdf';
;; scale: number;
;; suffix: string;
@@ -252,7 +243,7 @@
(when (some? exports)
(into [] (map parse-export) exports)))
;; export interface GuideColumnParams {
;; export interface PenpotFrameGuideColumnParams {
;; color: { color: string; opacity: number };
;; type?: 'stretch' | 'left' | 'center' | 'right';
;; size?: number;
@@ -271,10 +262,10 @@
:item-length (obj/get params "itemLength")
:gutter (obj/get params "gutter")})))
;; export interface GuideColumn {
;; export interface PenpotFrameGuideColumn {
;; type: 'column';
;; display: boolean;
;; params: GuideColumnParams;
;; params: PenpotFrameGuideColumnParams;
;; }
(defn parse-frame-guide-column
[^js guide]
@@ -284,10 +275,10 @@
:display (obj/get guide "display")
:params (-> (obj/get guide "params") parse-frame-guide-column-params)})))
;; export interface GuideRow {
;; export interface PenpotFrameGuideRow {
;; type: 'row';
;; display: boolean;
;; params: GuideColumnParams;
;; params: PenpotFrameGuideColumnParams;
;; }
(defn parse-frame-guide-row
@@ -298,7 +289,7 @@
:display (obj/get guide "display")
:params (-> (obj/get guide "params") parse-frame-guide-column-params)})))
;;export interface GuideSquareParams {
;;export interface PenpotFrameGuideSquareParams {
;; color: { color: string; opacity: number };
;; size?: number;
;;}
@@ -309,10 +300,10 @@
{:color (-> (obj/get params "color") parse-color)
:size (obj/get params "size")})))
;; export interface GuideSquare {
;; export interface PenpotFrameGuideSquare {
;; type: 'square';
;; display: boolean;
;; params: GuideSquareParams;
;; params: PenpotFrameGuideSquareParams;
;; }
(defn parse-frame-guide-square
[^js guide]
@@ -340,7 +331,7 @@
(when (some? guides)
(into [] (map parse-frame-guide) guides)))
;;interface PathCommand {
;;interface PenpotPathCommand {
;; command:
;; | 'M' | 'move-to'
;; | 'Z' | 'close-path'
@@ -410,13 +401,13 @@
(when (some? content)
(into [] (map parse-command) content)))
;; export interface Dissolve {
;; export interface PenpotDissolve {
;; type: 'dissolve';
;; duration: number;
;; easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
;; }
;;
;; export interface Slide {
;; export interface PenpotSlide {
;; type: 'slide';
;; way: 'in' | 'out';
;; direction?:
@@ -429,7 +420,7 @@
;; easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
;; }
;;
;; export interface Push {
;; export interface PenpotPush {
;; type: 'push';
;; direction?:
;; | 'right'
@@ -441,7 +432,7 @@
;; easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
;; }
;;
;; export type Animation = Dissolve | Slide | Push;
;; export type PenpotAnimation = PenpotDissolve | PenpotSlide | PenpotPush;
(defn parse-animation
[^js animation]
@@ -450,44 +441,44 @@
(d/without-nils
(case animation-type
:dissolve
{:animation-type animation-type
{:type animation-type
:duration (obj/get animation "duration")
:easing (-> (obj/get animation "easing") parse-keyword)}
:slide
{:animation-type animation-type
{:type animation-type
:way (-> (obj/get animation "way") parse-keyword)
:direction (-> (obj/get animation "direction") parse-keyword)
:duration (obj/get animation "duration")
:easing (-> (obj/get animation "easing") parse-keyword)
:offset-effect (boolean (obj/get animation "offsetEffect"))}
:offset-effect (obj/get animation "offsetEffect")}
:push
{:animation-type animation-type
{:type animation-type
:direction (-> (obj/get animation "direction") parse-keyword)
:duration (obj/get animation "duration")
:easing (-> (obj/get animation "easing") parse-keyword)}
nil)))))
;;export type Action =
;; | NavigateTo
;; | OpenOverlay
;; | ToggleOverlay
;; | CloseOverlay
;; | PreviousScreen
;; | OpenUrl;
;;export type PenpotAction =
;; | PenpotNavigateTo
;; | PenpotOpenOverlay
;; | PenpotToggleOverlay
;; | PenpotCloseOverlay
;; | PenpotPreviousScreen
;; | PenpotOpenUrl;
;;
;;export interface NavigateTo {
;;export interface PenpotNavigateTo {
;; type: 'navigate-to';
;; destination: Board;
;; destination: PenpotFrame;
;; preserveScrollPosition?: boolean;
;; animation: Animation;
;; animation: PenpotAnimation;
;;}
;;
;;export interface OverlayAction {
;; destination: Board;
;; relativeTo?: Shape;
;;export interface PenpotOverlayAction {
;; destination: PenpotFrame;
;; relativeTo?: PenpotShape;
;; position?:
;; | 'manual'
;; | 'center'
@@ -497,31 +488,31 @@
;; | 'bottom-left'
;; | 'bottom-right'
;; | 'bottom-center';
;; manualPositionLocation?: Point;
;; manualPositionLocation?: PenpotPoint;
;; closeWhenClickOutside?: boolean;
;; addBackgroundOverlay?: boolean;
;; animation: Animation;
;; animation: PenpotAnimation;
;;}
;;
;;export interface OpenOverlay extends OverlayAction {
;;export interface PenpotOpenOverlay extends PenpotOverlayAction {
;; type: 'open-overlay';
;;}
;;
;;export interface ToggleOverlay extends OverlayAction {
;;export interface PenpotToggleOverlay extends PenpotOverlayAction {
;; type: 'toggle-overlay';
;;}
;;
;;export interface CloseOverlay {
;;export interface PenpotCloseOverlay {
;; type: 'close-overlay';
;; destination?: Board;
;; animation: Animation;
;; destination?: PenpotFrame;
;; animation: PenpotAnimation;
;;}
;;
;;export interface PreviousScreen {
;;export interface PenpotPreviousScreen {
;; type: 'previous-screen';
;;}
;;
;;export interface OpenUrl {
;;export interface PenpotOpenUrl {
;; type: 'open-url';
;; url: string;
;;}
@@ -563,9 +554,10 @@
nil)))))
(defn parse-interaction
[trigger ^js action delay]
(when (and (string? trigger) (some? action))
(let [trigger (parse-keyword trigger)
action (parse-action action)]
[^js interaction]
(when interaction
(let [trigger (-> (obj/get interaction "trigger") parse-keyword)
delay (obj/get interaction "trigger")
action (-> (obj/get interaction "action") parse-action)]
(d/without-nils
(d/patch-object {:event-type trigger :delay delay} action)))))

View File

@@ -10,10 +10,8 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.uuid :as uuid]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.object :as obj]
[beicon.v2.core :as rx]))
[app.util.storage :refer [storage]]))
;; Stores the installed plugins information
(defonce ^:private registry (atom {}))
@@ -60,14 +58,40 @@
:icon icon
:permissions (into #{} (map str) permissions)}))
;; FIXME: LEGACY version of the load from store
;; can be removed before deploying plugins to production
;; Needs to be preserved for the beta users
(defn legacy-load-from-store
[]
(let [parse-plugin-data
(fn [^js data]
{:plugin-id (obj/get data "plugin-id")
:name (obj/get data "name")
:description (obj/get data "description")
:host (obj/get data "host")
:code (obj/get data "code")
:icon (obj/get data "icon")
:permissions (into #{} (obj/get data "permissions"))})
ls (.-localStorage js/window)
plugins-val (.getItem ls "plugins")]
(when plugins-val
(let [stored (->> (.parse js/JSON plugins-val)
(map parse-plugin-data))]
(reset! registry
{:ids (->> stored (map :plugin-id))
:data (d/index-by :plugin-id stored)})))))
(defn save-to-store
[]
(->> (rp/cmd! :update-profile-props {:props {:plugins @registry}})
(rx/subs! identity)))
(swap! storage assoc :plugins @registry))
(defn load-from-store
[]
(reset! registry (get-in @st/state [:profile :props :plugins] {})))
(if (:plugins @storage)
(reset! registry (:plugins @storage))
(do (legacy-load-from-store)
(save-to-store))))
(defn init
[]

View File

@@ -551,10 +551,10 @@
;; Interactions
(addInteraction
[self trigger action delay]
[self interaction]
(let [interaction
(-> ctsi/default-interaction
(d/patch-object (parser/parse-interaction trigger action delay)))]
(d/patch-object (parser/parse-interaction interaction)))]
(cond
(not (sm/validate ::ctsi/interaction interaction))
(u/display-not-valid :addInteraction interaction)
@@ -576,6 +576,12 @@
(defn shape-proxy? [p]
(instance? ShapeProxy p))
;; Prevent circular dependency
(do (set! flex/shape-proxy? shape-proxy?)
(set! grid/shape-proxy? shape-proxy?))
(set! format/shape-proxy shape-proxy)
(crc/define-properties!
ShapeProxy
{:name js/Symbol.toStringTag
@@ -605,7 +611,7 @@
:get #(-> % u/proxy->shape :id str)}
{:name "type"
:get #(-> % u/proxy->shape :type format/shape-type)}
:get #(-> % u/proxy->shape :type d/name)}
{:name "name"
:get #(-> % u/proxy->shape :name)
@@ -656,21 +662,6 @@
(let [id (obj/get self "$id")]
(st/emit! (dwsh/update-shapes [id] #(assoc % :hidden value))))))}
{:name "visible"
:get #(-> % u/proxy->shape :hidden boolean not)
:set
(fn [self value]
(cond
(not (boolean? value))
(u/display-not-valid :visible value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :visible "Plugin doesn't have 'content:write' permission")
:else
(let [id (obj/get self "$id")]
(st/emit! (dwsh/update-shapes [id] #(assoc % :hidden (not value)))))))}
{:name "proportionLock"
:get #(-> % u/proxy->shape :proportion-lock boolean)
:set
@@ -987,7 +978,7 @@
parent-y (:y parent)]
(st/emit! (dw/update-position id {:y (+ parent-y value)})))))}
{:name "boardX"
{:name "frameX"
:get (fn [self]
(let [shape (u/proxy->shape self)
frame-id (:parent-id shape)
@@ -1010,7 +1001,7 @@
frame-x (:x frame)]
(st/emit! (dw/update-position id {:x (+ frame-x value)})))))}
{:name "boardY"
{:name "frameY"
:get (fn [self]
(let [shape (u/proxy->shape self)
frame-id (:parent-id shape)

View File

@@ -175,12 +175,7 @@
:set
(fn [self value]
(let [font (fonts/get-font-data (obj/get self "fontId"))
weight (dm/str value)
style (obj/get self "fontStyle")
variant
(or
(fonts/find-variant font {:style style :weight weight})
(fonts/find-variant font {:weight weight}))]
variant (fonts/find-variant font {:weight (dm/str value)})]
(cond
(nil? variant)
(u/display-not-valid :fontWeight (dm/str "Font weight '" value "' not supported for the current font"))
@@ -198,12 +193,7 @@
:set
(fn [self value]
(let [font (fonts/get-font-data (obj/get self "fontId"))
style (dm/str value)
weight (obj/get self "fontWeight")
variant
(or
(fonts/find-variant font {:weight weight :style style})
(fonts/find-variant font {:style style}))]
variant (fonts/find-variant font {:weight (dm/str value)})]
(cond
(nil? variant)
(u/display-not-valid :fontStyle (dm/str "Font style '" value "' not supported for the current font"))
@@ -449,12 +439,7 @@
(fn [self value]
(let [id (obj/get self "$id")
font (fonts/get-font-data (obj/get self "fontId"))
weight (dm/str value)
style (obj/get self "fontStyle")
variant
(or
(fonts/find-variant font {:style style :weight weight})
(fonts/find-variant font {:weight weight}))]
variant (fonts/find-variant font {:weight (dm/str value)})]
(cond
(nil? variant)
(u/display-not-valid :fontWeight (dm/str "Font weight '" value "' not supported for the current font"))
@@ -471,12 +456,7 @@
(fn [self value]
(let [id (obj/get self "$id")
font (fonts/get-font-data (obj/get self "fontId"))
style (dm/str value)
weight (obj/get self "fontWeight")
variant
(or
(fonts/find-variant font {:weight weight :style style})
(fonts/find-variant font {:style style}))]
variant (fonts/find-variant font {:weight (dm/str value)})]
(cond
(nil? variant)
(u/display-not-valid :fontStyle (dm/str "Font style '" value "' not supported for the current font"))

View File

@@ -80,9 +80,6 @@
(= id :multiple)
(= file-id :multiple)))
(def empty-color
(into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity :image]))
(defn get-color-name
[color]
(or (:color-library-name color)

View File

@@ -760,8 +760,10 @@
(.back (.-history js/window)))
(defn reload-current-window
[]
(.reload (.-location js/window)))
([]
(.reload globals/location))
([force?]
(.reload globals/location force?)))
(defn scroll-by!
([element x y]

View File

@@ -148,14 +148,33 @@
(defn nav-root
"Navigate to the root page."
[]
(set! (.-href globals/location) "/"))
(ptk/reify ::nav-root
ptk/EffectEvent
(effect [_ _ _]
(set! (.-href globals/location) "/"))))
(defn reload
[force?]
(ptk/reify ::reload
ptk/EffectEvent
(effect [_ _ _]
(ts/asap (partial dom/reload-current-window force?)))))
(defn nav-raw
[href]
[& {:keys [href uri]}]
(ptk/reify ::nav-raw
ptk/EffectEvent
(effect [_ _ _]
(set! (.-href globals/location) href))))
(cond
(string? uri)
(.replace globals/location uri)
(string? href)
(set! (.-href globals/location) href)))))
(defn get-current-href
[]
(.-href globals/location))
(defn get-current-path
[]
@@ -164,6 +183,7 @@
(subs hash 1)
hash)))
;; --- History API
(defn initialize-history

View File

@@ -10,75 +10,148 @@
[app.common.transit :as t]
[app.util.functions :as fns]
[app.util.globals :as g]
[cuerdas.core :as str]))
[cuerdas.core :as str]
[okulary.util :as ou]))
;; Using ex/ignoring because can receive a DOMException like this when
;; importing the code as a library: Failed to read the 'localStorage'
;; property from 'Window': Storage is disabled inside 'data:' URLs.
(defonce ^:private local-storage
(defonce ^:private local-storage-backend
(ex/ignoring (unchecked-get g/global "localStorage")))
(defonce ^:private session-storage-backend
(ex/ignoring (unchecked-get g/global "sessionStorage")))
(def ^:dynamic *sync*
"Dynamic variable which determines the mode of operation of the
storage mutatio actions. By default is asynchronous."
false)
(defn- encode-key
[k]
[prefix k]
(assert (keyword? k) "key must be keyword")
(let [kns (namespace k)
kn (name k)]
(str "penpot:" kns "/" kn)))
(str prefix ":" kns "/" kn)))
(defn- decode-key
[k]
(when (str/starts-with? k "penpot:")
(let [k (subs k 7)]
[prefix k]
(when (str/starts-with? k prefix)
(let [l (+ (count prefix) 1)
k (subs k l)]
(if (str/starts-with? k "/")
(keyword (subs k 1))
(let [[kns kn] (str/split k "/" 2)]
(keyword kns kn))))))
(defn- lookup-by-index
[result index]
[backend prefix result index]
(try
(let [key (.key ^js local-storage index)
key' (decode-key key)]
(let [key (.key ^js backend index)
key' (decode-key prefix key)]
(if key'
(let [val (.getItem ^js local-storage key)]
(let [val (.getItem ^js backend key)]
(assoc! result key' (t/decode-str val)))
result))
(catch :default _
result)))
(defn- load
[]
(when (some? local-storage)
(let [length (.-length ^js local-storage)]
(defn- load-data
[backend prefix]
(if (some? backend)
(let [length (.-length ^js backend)]
(loop [index 0
result (transient {})]
(if (< index length)
(recur (inc index)
(lookup-by-index result index))
(persistent! result))))))
(lookup-by-index backend prefix result index))
(persistent! result))))
{}))
(defonce ^:private latest-state (load))
(defn create-storage
[backend prefix]
(let [initial (load-data backend prefix)
curr-data #js {:content initial}
last-data #js {:content initial}
watches (js/Map.)
(defn- on-change*
[curr-state]
(let [prev-state latest-state]
(try
(run! (fn [key]
(let [prev-val (get prev-state key)
curr-val (get curr-state key)]
(when-not (identical? curr-val prev-val)
(if (some? curr-val)
(.setItem ^js local-storage (encode-key key) (t/encode-str curr-val))
(.removeItem ^js local-storage (encode-key key))))))
(into #{} (concat (keys curr-state)
(keys prev-state))))
(finally
(set! latest-state curr-state)))))
update-key
(fn [key val]
(when (some? backend)
(if (some? val)
(.setItem ^js backend (encode-key prefix key) (t/encode-str val))
(.removeItem ^js backend (encode-key prefix key)))))
(defonce on-change
(fns/debounce on-change* 2000))
on-change*
(fn [curr-state]
(let [prev-state (unchecked-get last-data "content")]
(try
(run! (fn [key]
(let [prev-val (get prev-state key)
curr-val (get curr-state key)]
(when-not (identical? curr-val prev-val)
(update-key key curr-val))))
(into #{} (concat (keys curr-state)
(keys prev-state))))
(finally
(unchecked-set last-data "content" curr-state)))))
(defonce storage (atom latest-state))
(add-watch storage :persistence
(fn [_ _ _ curr-state]
(on-change curr-state)))
on-change
(fns/debounce on-change* 2000)]
(reify
IAtom
IDeref
(-deref [_] (unchecked-get curr-data "content"))
ILookup
(-lookup [coll k]
(-lookup coll k nil))
(-lookup [_ k not-found]
(let [state (unchecked-get curr-data "content")]
(-lookup state k not-found)))
IReset
(-reset! [self newval]
(let [oldval (unchecked-get curr-data "content")]
(unchecked-set curr-data "content" newval)
(if *sync*
(on-change* newval)
(on-change newval))
(when (> (.-size watches) 0)
(-notify-watches self oldval newval))
newval))
ISwap
(-swap! [self f]
(let [state (unchecked-get curr-data "content")]
(-reset! self (f state))))
(-swap! [self f x]
(let [state (unchecked-get curr-data "content")]
(-reset! self (f state x))))
(-swap! [self f x y]
(let [state (unchecked-get curr-data "content")]
(-reset! self (f state x y))))
(-swap! [self f x y more]
(let [state (unchecked-get curr-data "content")]
(-reset! self (apply f state x y more))))
IWatchable
(-notify-watches [self oldval newval]
(ou/doiter
(.entries watches)
(fn [n]
(let [f (aget n 1)
k (aget n 0)]
(f k self oldval newval)))))
(-add-watch [self key f]
(.set watches key f)
self)
(-remove-watch [_ key]
(.delete watches key)))))
(defonce storage (create-storage local-storage-backend "penpot"))
(defonce session (create-storage session-storage-backend "penpot"))

View File

@@ -31,7 +31,7 @@
(defn asap
[f]
(-> (p/resolved nil)
(p/then f)))
(p/then (fn [_] (f)))))
(defn interval
[ms func]

View File

@@ -103,6 +103,14 @@
(let [cboard (unchecked-get js/navigator "clipboard")]
(.writeText ^js cboard data)))
(defn write-to-clipboard-promise
[mimetype promise]
(let [cboard (unchecked-get js/navigator "clipboard")
data (js/ClipboardItem.
(-> (obj/create)
(obj/set! mimetype promise)))]
(.write ^js cboard #js [data])))
(defn read-from-clipboard
[]
(try

View File

@@ -202,108 +202,61 @@
(defn make-local-external-references
[file file-id]
(let [detach-text
(let [change-fill
(fn [fill]
(cond-> fill
(not= file-id (:fill-color-ref-file fill))
(assoc :fill-color-ref-file file-id)))
change-stroke
(fn [stroke]
(cond-> stroke
(not= file-id (:stroke-color-ref-file stroke))
(assoc :stroke-color-ref-file file-id)))
change-text
(fn [content]
(->> content
(ct/transform-nodes
#(cond-> %
(not= file-id (:fill-color-ref-file %))
(assoc :fill-color-ref-file file-id)
(fn [node]
(-> node
(d/update-when :fills #(mapv change-fill %))
(cond-> (not= file-id (:typography-ref-file node))
(assoc :typography-ref-file file-id)))))))
(not= file-id (:typography-ref-file %))
(assoc :typography-ref-file file-id)))))
detach-shape
change-shape
(fn [shape]
(cond-> shape
(not= file-id (:fill-color-ref-file shape))
(assoc :fill-color-ref-file file-id)
(-> shape
(d/update-when :fills #(mapv change-fill %))
(d/update-when :strokes #(mapv change-stroke %))
(cond-> (not= file-id (:component-file shape))
(assoc :component-file file-id))
(not= file-id (:stroke-color-ref-file shape))
(assoc :stroke-color-ref-file file-id)
(cond-> (= :text (:type shape))
(update :content change-text))))
(not= file-id (:component-file shape))
(assoc :component-file file-id)
(= :text (:type shape))
(update :content detach-text)))
detach-objects
change-objects
(fn [objects]
(->> objects
(d/mapm #(detach-shape %2))))
(d/mapm #(change-shape %2))))
detach-pages
change-pages
(fn [pages-index]
(->> pages-index
(d/mapm
(fn [_ data]
(-> data
(update :objects detach-objects))))))]
(update :objects change-objects))))))]
(-> file
(update-in [:data :pages-index] detach-pages))))
(defn collect-external-references
[file]
(let [get-text-refs
(fn [content]
(->> content
(ct/node-seq #(or (contains? % :fill-color-ref-id)
(contains? % :typography-ref-id)))
(mapcat (fn [node]
(cond-> []
(contains? node :fill-color-ref-id)
(conj {:id (:fill-color-ref-id node)
:file-id (:fill-color-ref-file node)})
(contains? node :typography-ref-id)
(conj {:id (:typography-ref-id node)
:file-id (:typography-ref-file node)}))))
(into [])))
get-shape-refs
(fn [[_ shape]]
(cond-> []
(contains? shape :fill-color-ref-id)
(conj {:id (:fill-color-ref-id shape)
:file-id (:fill-color-ref-file shape)})
(contains? shape :stroke-color-ref-id)
(conj {:id (:stroke-color-ref-id shape)
:file-id (:stroke-color-ref-file shape)})
(contains? shape :component-id)
(conj {:id (:component-id shape)
:file-id (:component-file shape)})
(= :text (:type shape))
(into (get-text-refs (:content shape)))))]
(->> (get-in file [:data :pages-index])
(vals)
(mapcat :objects)
(mapcat get-shape-refs)
(filter (comp some? :file-id))
(filter (comp some? :id))
(group-by :file-id)
(d/mapm #(mapv :id %2)))))
(update-in [:data :pages-index] change-pages))))
(defn merge-assets [target-file assets-files]
(let [external-refs (collect-external-references target-file)
merge-file-assets
(let [merge-file-assets
(fn [target file]
(let [colors (-> (get-in file [:data :colors])
(select-keys (get external-refs (:id file))))
typographies (-> (get-in file [:data :typographies])
(select-keys (get external-refs (:id file))))
media (-> (get-in file [:data :media])
(select-keys (get external-refs (:id file))))
components (-> (ctkl/components (:data file))
(select-keys (get external-refs (:id file))))]
(let [colors (get-in file [:data :colors])
typographies (get-in file [:data :typographies])
media (get-in file [:data :media])
components (ctkl/components (:data file))]
(cond-> target
(d/not-empty? colors)
(update-in [:data :colors] merge colors)
@@ -323,16 +276,20 @@
(defn process-export
[file-id export-type files]
(case export-type
:all files
:merge (let [file-list (-> files (d/without-keys [file-id]) vals)]
(-> (select-keys files [file-id])
(update file-id merge-assets file-list)
(update file-id make-local-external-references file-id)
(update file-id dissoc :libraries)))
:detach (-> (select-keys files [file-id])
(update file-id ctf/detach-external-references file-id)
(update file-id dissoc :libraries))))
(let [result
(case export-type
:all files
:merge (let [file-list (-> files (d/without-keys [file-id]) vals)]
(-> (select-keys files [file-id])
(update file-id merge-assets file-list)
(update file-id make-local-external-references file-id)
(update file-id dissoc :libraries)))
:detach (-> (select-keys files [file-id])
(update file-id ctf/detach-external-references file-id)
(update file-id dissoc :libraries)))]
;;(.log js/console (clj->js result))
result))
(defn collect-files
[file-id export-type features]

View File

@@ -223,22 +223,22 @@
(t/is (= (-> (. shape -strokes) (aget 0) (aget "strokeWidth")) 5))))
(t/testing "Relative properties"
(let [board (.createBoard context)]
(set! (.-x board) 100)
(set! (.-y board) 200)
(t/is (= (.-x board) 100))
(t/is (= (.-y board) 200))
(.appendChild board shape)
(let [frame (.createFrame context)]
(set! (.-x frame) 100)
(set! (.-y frame) 200)
(t/is (= (.-x frame) 100))
(t/is (= (.-y frame) 200))
(.appendChild frame shape)
(t/testing " - boardX"
(set! (.-boardX shape) 10)
(t/is (m/close? (.-boardX shape) 10))
(t/testing " - frameX"
(set! (.-frameX shape) 10)
(t/is (m/close? (.-frameX shape) 10))
(t/is (m/close? (.-x shape) 110))
(t/is (m/close? (get-in @store (get-shape-path :x)) 110)))
(t/testing " - boardY"
(set! (.-boardY shape) 20)
(t/is (m/close? (.-boardY shape) 20))
(t/testing " - frameY"
(set! (.-frameY shape) 20)
(t/is (m/close? (.-frameY shape) 20))
(t/is (m/close? (.-y shape) 220))
(t/is (m/close? (get-in @store (get-shape-path :y)) 220)))