Compare commits

..

35 Commits

Author SHA1 Message Date
David Barragán Merino
fef4d3bdbe wip 2024-09-11 18:08:36 +02:00
Alejandro Alonso
042b3a71d8 Merge remote-tracking branch 'origin/staging' into develop 2024-09-11 12:46:04 +02:00
Alejandro Alonso
eadae5e2cd Merge remote-tracking branch 'origin/staging' into develop 2024-09-11 12:05:45 +02:00
Alejandro Alonso
7f9c4df284 Merge remote-tracking branch 'origin/staging' into develop 2024-09-11 11:34:35 +02:00
Alejandro Alonso
9e3f8e7827 Merge remote-tracking branch 'origin/staging' into develop 2024-09-09 11:09:53 +02:00
Eva Marco
3a4e9ccc5a 👷 Fix CI error 2024-09-09 10:32:50 +02:00
Belén Albeza
eb720b053a Merge pull request #5057 from penpot/eva-fix-css-compilation
🔧 Rearrange css files for compilation
2024-09-06 14:45:52 +02:00
Alejandro Alonso
efc61241a0 Merge remote-tracking branch 'origin/staging' into develop 2024-09-06 13:50:37 +02:00
Andrey Antukh
cfad1d178f Merge pull request #5068 from penpot/alotor-plugins-install-profile
 Change installation data to profile
2024-09-06 12:05:52 +02:00
alonso.torres
c24b2dadec Change installation data to profile 2024-09-06 11:10:32 +02:00
Andrey Antukh
9a3b5337d7 Merge pull request #5062 from penpot/alotor-plugins-fix-interactions
🐛 Fix plugins add interaction
2024-09-05 16:22:46 +02:00
alonso.torres
396cbb27b2 🐛 Fix plugins add interaction 2024-09-05 16:00:04 +02:00
Alejandro
b4e6f8bc73 Merge pull request #5061 from penpot/niwinz-challenge
 Add support for optional human challenge
2024-09-05 15:49:50 +02:00
Andrey Antukh
d88f28f5c2 Add support for optional human challenge 2024-09-05 15:35:39 +02:00
Eva Marco
e36cf1d963 🐛 Fix onboarding slide after rearrange 2024-09-05 14:46:49 +02:00
Eva Marco
a0bb5e5ef3 ♻️ Remove unnecesary code 2024-09-05 09:41:11 +02:00
Eva Marco
34cc211912 🔧 Rearrange css files for compilation 2024-09-05 09:39:43 +02:00
Eva Marco
e95713c1df 🐛 Fix visual integration test 2024-09-05 09:39:43 +02:00
Alejandro Alonso
e189dc965d Merge remote-tracking branch 'origin/staging' into develop 2024-09-05 09:37:16 +02:00
Belén Albeza
53f580ad40 Merge pull request #5017 from penpot/eva-add-select-to-ds
 Add select component to the DS
2024-09-04 15:51:10 +02:00
Andrey Antukh
cf0045681e Merge pull request #5054 from penpot/alotor-plugins-fixes
Update API types
2024-09-04 14:16:01 +02:00
alonso.torres
762a883b39 🐛 Fix problem with font weight and style 2024-09-04 13:52:48 +02:00
alonso.torres
a63ded1ba1 Change type names in plugins 2024-09-04 13:29:56 +02:00
alonso.torres
f812b28892 ⬆️ Update plugin dependencies 2024-09-04 12:38:50 +02:00
Andrey Antukh
873c9b1903 Merge pull request #5050 from penpot/hiru-ordered-maps
🔧 Add serializable ordered collections
2024-09-04 12:26:21 +02:00
Alejandro Alonso
edeb16bc26 Merge remote-tracking branch 'origin/staging' into develop 2024-09-04 12:02:31 +02:00
Alejandro Alonso
90d947391a Merge remote-tracking branch 'origin/staging' into develop 2024-09-04 08:59:05 +02:00
Andrés Moya
47cc80a93f 🔧 Add serializable ordered collections 2024-09-03 23:35:53 +02:00
Andrey Antukh
1f8cfde1cf Merge pull request #5046 from penpot/alotor-plugins-fixes
Plugins small fixes
2024-09-03 14:36:59 +02:00
Alejandro Alonso
5f2ec595cb 📎 Update changelog 2024-09-03 13:15:48 +02:00
alonso.torres
37a6446e32 🐛 Fix problem with font style 2024-09-03 13:10:28 +02:00
alonso.torres
be84b1cb01 🐛 Change place for circular dependency workaround 2024-09-03 13:10:28 +02:00
Eva Marco
298db46722 Add documentation to select on storybook 2024-09-02 16:56:53 +02:00
Eva Marco
0c6b0598fa Add new select ds component to storybook 2024-08-29 14:14:12 +02:00
Eva Marco
f2a2d772b0 Add new select component to the ds 2024-08-29 14:14:08 +02:00
77 changed files with 3331 additions and 3053 deletions

View File

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

View File

@@ -292,7 +292,7 @@
[:map {:title "create-comment-thread"}
[:file-id ::sm/uuid]
[:position ::gpt/point]
[:content [:string {:max 750}]]
[:content [:string {:max 250}]]
[: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 :deleted-at :revn :data-ref-id :data-backend])]
(let [opts (assoc opts ::sql/columns [:id :modified-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/set ::sm/uuid]]]
[:object-id {:optional true} [:or ::sm/uuid ::sm/coll-of-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=?
DO UPDATE SET updated_at=?, media_id=?, deleted_at=null
RETURNING *")
(defn- persist-thumbnail!
@@ -251,19 +251,17 @@
:content-type mtype
:bucket "file-object-thumbnail"})))
(defn- create-file-object-thumbnail!
[{:keys [::sto/storage] :as cfg} file object-id media tag]
(let [file-id (:id file)
timestamp (dt/now)
media (persist-thumbnail! storage media timestamp)
[{:keys [::sto/storage] :as cfg} file-id object-id media tag]
(let [tsnow (dt/now)
media (persist-thumbnail! storage media tsnow)
[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)
timestamp
(:id media)
(:deleted-at file)])]
file-id object-id tag (:id media)
tsnow (:id media)])]
[th1 th2])))]
(when (and (some? th1)
@@ -296,8 +294,8 @@
(media/validate-media-size! media)
(db/run! cfg files/check-edition-permissions! profile-id file-id)
(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"))))
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame")))
;; --- MUTATION COMMAND: delete-file-object-thumbnail

View File

@@ -75,7 +75,6 @@
: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,13 +133,7 @@
file))
(def ^:private sql:get-files-for-library
"SELECT f.id,
f.data,
f.modified_at,
f.features,
f.version,
f.data_backend,
f.data_ref_id
"SELECT f.id, f.data, f.modified_at, f.features, f.version
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,6 +6,8 @@
(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)
([a] (conj lkm/empty-linked-map a))
([a & xs] (apply conj lkm/empty-linked-map a xs)))
([k a] (assoc lkm/empty-linked-map k a))
([k a & xs] (apply assoc lkm/empty-linked-map k a xs)))
(defn ordered-set?
[o]
@@ -564,6 +564,41 @@
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,7 +545,6 @@
(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 54)
(def version 51)

View File

@@ -863,9 +863,11 @@
(assoc shadow :color color)))
(update-object [object]
(let [xform (comp (map fix-shadow)
(filter valid-shadow?))]
(d/update-when object :shadow #(into [] xform %))))
(d/update-when object :shadow
#(into []
(comp (map fix-shadow)
(filter valid-shadow?))
%)))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
@@ -1008,44 +1010,13 @@
(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}
@@ -1088,7 +1059,4 @@
{: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 52 :migrate-up migrate-up-52}
{:id 53 :migrate-up migrate-up-26}
{:id 54 :migrate-up migrate-up-54}])
{:id 51 :migrate-up migrate-up-51}])

View File

@@ -15,6 +15,7 @@
java.time.Instant
java.time.OffsetDateTime
java.util.List
linked.map.LinkedMap
org.fressian.Reader
org.fressian.StreamingWriter
org.fressian.Writer
@@ -109,6 +110,13 @@
(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)
@@ -225,6 +233,11 @@
: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,11 +158,7 @@
empty-parents
;; Any parent whose children are all deleted, must be deleted too.
;; 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 #{}))
#{})
(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.core :as lk]
[linked.map :as lkm]
[linked.set :as lks])
#?(:clj
(:import
@@ -24,6 +24,7 @@
java.time.Instant
java.time.OffsetDateTime
lambdaisland.uri.URI
linked.map.LinkedMap
linked.set.LinkedSet)))
(def write-handlers (atom nil))
@@ -118,10 +119,15 @@
{: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 (lk/set) %)}
:rfn #(into lks/empty-linked-set %)}
{:id "duration"
:class #?(:clj Duration :cljs lxn/Duration)

View File

@@ -80,23 +80,21 @@
[: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 schema:color-attrs
[: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]]
[::sm/contains-any {:strict true} [:color :gradient :image]]])
(def schema:recent-color
@@ -113,13 +111,12 @@
(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 check-color!
(sm/check-fn schema:color))
(def valid-color?
(sm/lazy-validator schema:color))
(def check-recent-color!
(sm/check-fn schema:recent-color))
(def valid-recent-color?
(sm/lazy-validator schema:recent-color))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS

View File

@@ -125,9 +125,6 @@
(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,6 +27,3 @@
[: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 \
build-essential \
openssh-client \
redis-tools \
locales \
gnupg2 \
ca-certificates \
wget \
sudo \
tmux \
vim \
curl \
bash \
git \
rlwrap \
unzip \
rsync \
build-essential \
ca-certificates \
fakeroot \
file \
less \
git \
gnupg2 \
jq \
less \
locales \
nginx \
openssh-client \
redis-tools \
rlwrap \
rsync \
sudo \
tmux \
unzip \
url \
vim \
wget \
; \
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
locale-gen; \
@@ -50,32 +50,28 @@ RUN set -ex; \
RUN set -ex; \
apt-get -qq update; \
apt-get -qqy install --no-install-recommends \
python3 \
python3-tabulate \
imagemagick \
ghostscript \
netpbm \
poppler-utils \
potrace \
webp \
woff-tools \
woff2 \
fontforge \
fonts-liberation \
gconf-service \
ghostscript \
imagemagick \
libappindicator1 \
libasound2 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libatk1.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 \
@@ -92,10 +88,14 @@ RUN set -ex; \
libxshmfence1 \
libxss1 \
libxtst6 \
fonts-liberation \
libappindicator1 \
libnss3 \
libgbm1 \
netpbm \
poppler-utils \
potrace \
python3 \
python3-tabulate \
webp \
woff-tools \
woff2 \
xvfb \
; \
rm -rf /var/lib/apt/lists/*;

View File

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

View File

@@ -1,4 +1,4 @@
FROM ubuntu:22.04
FROM debian:bookworm
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ENV LANG=en_US.UTF-8 \
@@ -13,11 +13,9 @@ 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 \
curl \
tzdata \
locales \
ca-certificates \
fontconfig \
locales \
tzdata \
xz-utils \
; \
rm -rf /var/lib/apt/lists/*; \
@@ -27,33 +25,32 @@ RUN set -ex; \
RUN set -ex; \
apt-get -qq update; \
apt-get -qqy install \
imagemagick \
ghostscript \
netpbm \
poppler-utils \
potrace \
fonts-liberation \
gconf-service \
ghostscript \
imagemagick \
libasound2 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libatk1.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 \
@@ -65,9 +62,9 @@ RUN set -ex; \
libxshmfence1 \
libxss1 \
libxtst6 \
fonts-liberation \
libnss3 \
libgbm1 \
netpbm \
poppler-utils \
potrace \
; \
rm -rf /var/lib/apt/lists/*;

View File

@@ -177,15 +177,3 @@ 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.getByTestId("code").click();
await viewerPage.page.getByRole("tab", { name: "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,20 +115,30 @@ 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 [...dsFiles, ...appFiles]) {
for (let path of [...oldComponentsFiles, ...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/technical-guide/plugins/getting-started/#examples"))
(def plugins-list-uri (obj/get global "penpotPluginsListUri" "https://penpot-docs-plugins.pages.dev/plugins/getting-started/#examples"))
(defn- normalize-uri
[uri-str]

View File

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

View File

@@ -19,6 +19,7 @@
[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]
@@ -137,7 +138,9 @@
(swap! s/storage assoc :profile profile)
(i18n/set-locale! (:lang profile))
(when (not= previous-email email)
(set-current-team! nil)))))))
(set-current-team! nil))
(register/init))))))
(defn- on-fetch-profile-exception
[cause]
@@ -148,7 +151,7 @@
href (->> path
(js/encodeURIComponent)
(str "/challenge.html?redirect="))]
(rx/of (rt/nav-raw :href href)))
(rx/of (rt/nav-raw href)))
(rx/throw cause))))
(defn fetch-profile
@@ -168,25 +171,13 @@
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 (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
(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}))
(rx/of (rt/nav' :dashboard-projects {:team-id team-id})))))]
(ptk/reify ::logged-in

View File

@@ -85,8 +85,7 @@
[beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[potok.v2.core :as ptk]
[promesa.core :as p]))
[potok.v2.core :as ptk]))
(def default-workspace-local {:zoom 1})
(log/set-level! :debug)
@@ -1552,40 +1551,15 @@
shapes (->> (cfh/selected-with-children objects selected)
(keep (d/getf objects)))]
;; 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))))))))))
(->> (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,9 +12,6 @@
[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]
@@ -24,6 +21,7 @@
[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]
@@ -167,15 +165,6 @@
(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 _]
@@ -186,15 +175,6 @@
(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 _]
@@ -207,21 +187,13 @@
(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
@@ -300,25 +272,17 @@
;; example using the color selection from
;; multiple shapes) let's use the first stop
;; color
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'))))))))
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))))))))
(defn add-shadow
[ids shadow]
(dm/assert!
"expected a valid shadow struct"
(check-shadow! shadow))
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(sm/check-coll-of-uuid! ids))
(ptk/reify ::add-shadow
ptk/WatchEvent
@@ -329,15 +293,6 @@
(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 [_ _ _]
@@ -346,11 +301,6 @@
(defn remove-stroke
[ids position]
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::remove-stroke
ptk/WatchEvent
(watch [_ _ _]
@@ -364,11 +314,6 @@
(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 [_ _ _]
@@ -431,7 +376,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)
@@ -450,57 +395,26 @@
(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
[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))
[new-color shapes-by-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 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/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/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 _]
@@ -523,10 +437,9 @@
result (cond-> result (not group?) (conj cur))]
(recur (rest pending) result))))]
(if stroke?
(rx/of (change-stroke ids color 0))
(rx/of (change-fill ids color 0)))))))
(rx/of (change-stroke ids (merge uc/empty-color color) 0))
(rx/of (change-fill ids (merge uc/empty-color color) 0)))))))
(declare activate-colorpicker-color)
(declare activate-colorpicker-gradient)
@@ -535,22 +448,15 @@
(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 [_ _ _]
;; 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)))))))
(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))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -690,8 +596,7 @@
(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)]
@@ -709,8 +614,7 @@
: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)))))))
@@ -782,7 +686,6 @@
(defn select-color
[position add-color]
;; FIXME: revisit
(ptk/reify ::select-color
ptk/WatchEvent
(watch [_ state _]

View File

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

View File

@@ -42,8 +42,7 @@
(mf/lazy-component app.main.ui.workspace/workspace))
(mf/defc main-page
{::mf/props :obj
::mf/private true}
{::mf/props :obj}
[{:keys [route profile]}]
(let [{:keys [data params]} route
props (get profile :props)
@@ -69,7 +68,6 @@
(: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,7 +23,6 @@
[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]))
@@ -45,28 +44,13 @@
[]
(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
(st/emit! (rt/nav-raw :uri redirect-uri))
(.replace js/location redirect-uri)
(log/error :hint "unexpected response from OIDC method"
:resp (pr-str rsp))))
(fn [cause]
@@ -135,7 +119,6 @@
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,7 +35,6 @@
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")
@@ -89,8 +88,7 @@
:on-blur on-blur
:value value
:placeholder placeholder
:on-change on-change*
:max-length max-length}]))
:on-change on-change*}]))
(mf/defc reply-form
[{:keys [thread] :as props}]
@@ -130,8 +128,7 @@
:on-focus on-focus
:select-on-focus? false
:on-ctrl-enter on-submit
:on-change on-change
:max-length 750}]
:on-change on-change}]
(when (or @show-buttons? (seq @content))
[:div {:class (stl/css :buttons-wrapper)}
[:input.btn-secondary
@@ -199,8 +196,7 @@
:select-on-focus? false
:on-esc on-esc
:on-change on-change
:on-ctrl-enter on-submit
:max-length 750}]
:on-ctrl-enter on-submit}]
[:div {:class (stl/css :buttons-wrapper)}
[:input {:on-click on-esc
@@ -237,8 +233,7 @@
:select-on-focus true
:select-on-focus? false
:on-ctrl-enter on-submit*
:on-change on-change
:max-length 750}]
:on-change on-change}]
[:div {:class (stl/css :buttons-wrapper)}
[:input {:type "button"
:value "Cancel"

View File

@@ -42,7 +42,9 @@
(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))
@@ -82,10 +84,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
[:*
@@ -144,8 +146,7 @@
(l/derived :current-team-id st/state))
(mf/defc dashboard
{::mf/props :obj}
[{:keys [route profile]}]
[{:keys [route profile] :as props}]
(let [section (get-in route [:data :name])
params (parse-params route)
@@ -180,13 +181,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,10 +519,8 @@
@include bodySmallTypography;
color: var(--modal-title-foreground-color);
}
// 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 {
.custom-input-checkbox {
align-items: flex-start;
}

View File

@@ -9,7 +9,8 @@
[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.forms.input :refer [input*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.select :refer [select*]]
[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]]
@@ -33,6 +34,7 @@
:Input input*
:Loader loader*
:RawSvg raw-svg*
:Select select*
:Text text*
:TabSwitcher tab-switcher*
:Toast toast*

View File

@@ -9,4 +9,6 @@
// 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

@@ -4,7 +4,7 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.ds.forms.input
(ns app.main.ui.ds.controls.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="Forms/Input" />
<Meta title="Controls/Input" />
# Input

View File

@@ -1,3 +1,9 @@
// 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: "Forms/Input",
title: "Controls/Input",
component: Components.Input,
argTypes: {
icon: {

View File

@@ -0,0 +1,245 @@
;; 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

@@ -0,0 +1,63 @@
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

@@ -0,0 +1,147 @@
// 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

@@ -0,0 +1,65 @@
// 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

@@ -116,9 +116,9 @@
(mf/defc tab-switcher*
{::mf/props :obj
::mf/schema schema:tab-switcher}
[{: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*))
[{: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*)
tabs-nodes-refs (mf/use-ref nil)
tabs-ref (mf/use-ref nil)
@@ -175,7 +175,8 @@
class (dm/str class " " (stl/css :tabs))
props (mf/spread-props props {:class class})]
props (mf/spread-props props {:class class
:on-key-down on-key-down})]
(mf/with-effect [tabs]
(mf/set-ref-val! tabs-ref tabs))
@@ -187,7 +188,6 @@
: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,17 +101,7 @@
}
.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,9 +369,7 @@
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,14 +260,6 @@
(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,7 +372,6 @@
[:& 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 {:optional true} [::sm/set {:kind ::sm/email}]]])
[:emails [::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,21 +7,19 @@
(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 :as register]
[app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page terms-register]]
[app.main.ui.dashboard.sidebar :refer [sidebar]]
[app.main.ui.icons :as i]
[app.main.ui.viewer.header :as viewer.header]
[app.main.ui.viewer.header :as header]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
@@ -31,14 +29,10 @@
[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)
on-nav-root (mf/use-fn #(st/emit! (rt/nav-root)))]
(let [profile-id (:profile-id @st/state)]
[:section {:class (stl/css :exception-layout)}
[:button
{:class (stl/css :exception-header)
@@ -50,7 +44,7 @@
[:div {:class (stl/css :deco-before)} i/logo-error-screen]
(when-not profile-id
[:button {:class (stl/css :login-header)
:on-click on-nav-root}
:on-click rt/nav-root}
(tr "labels.login")])
[:div {:class (stl/css :exception-content)}
@@ -71,8 +65,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
@@ -91,37 +85,29 @@
#(reset! current-section :login))
success-login
(mf/use-fn
(fn []
(reset! show-dialog false)
(st/emit! (rt/reload true))))
(fn []
(reset! show-dialog false)
(.reload js/window.location true))
success-register
(mf/use-fn
(fn [data]
(reset! register-token (:token data))
(reset! current-section :register-validate)))
(fn [data]
(reset! register-token (:token data))
(reset! current-section :register-validate))
register-email-sent
(mf/use-fn
(fn [email]
(reset! user-email email)
(reset! current-section :register-email-sent)))
(fn [email]
(reset! user-email email)
(reset! current-section :register-email-sent))
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)))]
(fn [email]
(reset! user-email email)
(reset! current-section :recovery-email-sent))]
[: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 on-nav-root}
[:button {:class (stl/css :modal-close-button) :on-click rt/nav-root}
i/close]]
[:div {:class (stl/css :login)}
[:div {:class (stl/css :logo)} i/logo]
@@ -132,21 +118,19 @@
[: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
:params {:save-login-redirect true}}]
:on-success-callback success-login}]
[: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/register-methods {:on-success-callback success-register :hide-separator true}]
[:& 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)}
@@ -156,13 +140,12 @@
:on-click set-section} (tr "auth.login-here")]]
[:div {:class (stl/css :links)}
[:hr {:class (stl/css :separator)}]
[:& register/terms-register]]]
[:& terms-register]]]
:register-validate
[:div {:class (stl/css :form-container)}
[:& register/register-validate-form
{:params {:token @register-token}
:on-success-callback register-email-sent}]
[:& 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"
@@ -171,7 +154,7 @@
:register-email-sent
[:div {:class (stl/css :form-container)}
[:& register/register-success-page {:params {:email @user-email :hide-logo true}}]]
[:& register-success-page {:params {:email @user-email :hide-logo true}}]]
:recovery-request
[:& recovery-request-page {:go-back-callback set-section-login
@@ -183,57 +166,43 @@
(mf/defc request-dialog
{::mf/props :obj}
[{:keys [title content button-text on-button-click cancel-text on-close]}]
(let [on-click (or on-button-click on-close)]
[{:keys [title content button-text on-button-click cancel-text]}]
(let [on-click (or on-button-click rt/nav-root)]
[: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 on-close}
[:button {:class (stl/css :modal-close-button) :on-click rt/nav-root}
i/close]]
[:div {:class (stl/css :dialog-title)} title]
(for [[index content] (d/enumerate content)]
[:div {:key index} content])
(for [txt content]
[:div txt])
[:div {:class (stl/css :sign-info)}
(when cancel-text
[:button {:class (stl/css :cancel-button)
:on-click on-close}
cancel-text])
[:button {:class (stl/css :cancel-button) :on-click rt/nav-root} 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 (mf/deref refs/profile)
(let [profile (:profile @st/state)
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)
@@ -245,24 +214,17 @@
[: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)}
;; 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}]])
[:& 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)}
@@ -280,27 +242,22 @@
[:& 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")
:on-close on-close}]
[:& request-dialog {:title (tr "not-found.no-permission.project") :button-text (tr "not-found.no-permission.go-dashboard")}]
(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")
:on-close on-close}]
:button-text (tr "not-found.no-permission.go-dashboard")}]
(: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")
:on-close on-close}]
:button-text (tr "not-found.no-permission.go-dashboard")}]
(: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")
:on-close on-close}]
:button-text (tr "not-found.no-permission.go-dashboard")}]
(some? file-id)
[:& request-dialog {:title (tr "not-found.no-permission.file")
@@ -308,8 +265,7 @@
(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")
:on-close on-close}]
:cancel-text (tr "not-found.no-permission.go-dashboard")}]
(some? team-id)
[:& request-dialog {:title (tr "not-found.no-permission.project")
@@ -317,8 +273,7 @@
(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")
:on-close on-close}]))]))
:cancel-text (tr "not-found.no-permission.go-dashboard")}]))]))
(mf/defc not-found*
[]
@@ -424,92 +379,64 @@
[: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 [type (:type data)
path (:path route)
(let [file-info (mf/use-state {:pending true})
team-info (mf/use-state {:pending true})
type (:type data)
path (:path route)
query-params (:query-params route)
path-params (:path-params route)
workspace? (str/includes? path "workspace")
dashboard? (str/includes? path "dashboard")
view? (str/includes? path "view")
workspace? (str/includes? path "workspace")
dashboard? (str/includes? path "dashboard")
view? (str/includes? path "view")
request-access? (and
(or workspace? dashboard? view?)
(or (not (str/empty? (:file-id @file-info))) (not (str/empty? (:team-id @team-info)))))
;; We stora the request access info int this state
info* (mf/use-state nil)
info (deref info*)
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)})))]
loaded? (get info :loaded false)
(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}))
request-access?
(and
(or workspace? dashboard? view?)
(or (:file-id info)
(:team-id info)))]
(when (and (:file-id pparams) (:pending @file-info))
(->> (rp/cmd! :get-file-info {:id (:file-id pparams)})
(rx/subs! on-file-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))))
(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 [path-params info]
(when-not (:loaded info)
(->> (load-info path-params)
(rx/subs! (partial reset! info*)))))
(when loaded?
(case (:type data)
:not-found
(if request-access?
[:& request-access {:file-id (:file-id info)
:team-id (:team-id info)
:is-default (:team-default info)
[:& 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* {}]
: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* {}])
:authentication
[:> not-found* {}]
:bad-gateway
[:> bad-gateway* props]
:bad-gateway
[:> bad-gateway* props]
:service-unavailable
[:& service-unavailable]
:service-unavailable
[:& service-unavailable]
[:> internal-error* props])))))
[:> internal-error* props])))

View File

@@ -244,8 +244,7 @@
(fn [result]
(reset! images-data* result)))))
[:div {:class (stl/css-case :element-options true
:viewer-code-block (= :viewer from))}
[:div {:class (stl/css :element-options)}
[: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-160}); // TODO: Fix this hardcoded value
height: calc(100vh - #{$s-128}); // TODO: Fix this hardcoded value
overflow: hidden;
padding-bottom: $s-16;
overflow-y: auto;
@@ -17,10 +17,6 @@
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,8 +136,7 @@
:else
[:div {:class (stl/css :settings-bar-content)}
[:> tab-switcher* {:tabs tabs
:default-selected "layers"
:selected (name section)
:default-selected (dm/str 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)
:always (assoc :file-id file-id)))
true (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 color (kbd/alt? event)))))
(dc/apply-color-from-palette (merge uc/empty-color color) (kbd/alt? event)))))
rename-color
(mf/use-fn

View File

@@ -14,7 +14,6 @@
[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]))
@@ -22,96 +21,82 @@
(defn- prepare-colors
[shapes file-id shared-libs]
(let [data (into [] (remove nil? (ctc/extract-all-colors shapes file-id shared-libs)))
groups (d/group-by :attrs #(dissoc % :attrs) data)
grouped-colors (group-by :attrs data)
all-colors (distinct (mapv :attrs data))
tmp (group-by #(some? (:id %)) all-colors)
library-colors (get tmp true)
colors (get tmp false)]
{:groups groups
{:grouped-colors grouped-colors
: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 [groups library-colors colors]} (mf/with-memo [shapes file-id shared-libs]
(prepare-colors 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))
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)
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 [])
grouped-colors* (mf/use-var nil)
prev-colors* (mf/use-var [])
on-change
(mf/use-fn
(fn [new-color old-color from-picker?]
(prn "new-color" new-color)
(prn "old-color" old-color)
(let [old-color (-> old-color
(dissoc :name :path)
(d/without-nils))
(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
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)]
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)]
(when from-picker?
(let [color (-> new-color
(dissoc :name :path)
(d/without-nils))]
(mf/set-ref-val! prev-colors-ref
(conj prev-colors color))))
(swap! prev-colors* conj (-> new-color (dissoc :name :path) d/without-nils)))
(st/emit! (dc/change-color-in-selected cops new-color old-color)))))
(st/emit! (dc/change-color-in-selected new-color shapes-by-color (or prev-color old-color))))))
on-open
(mf/use-fn #(mf/set-ref-val! prev-colors-ref []))
(mf/use-fn
(fn []
(reset! prev-colors* [])))
on-close
(mf/use-fn #(mf/set-ref-val! prev-colors-ref []))
(mf/use-fn
(fn []
(reset! prev-colors* [])))
on-detach
(mf/use-fn
(fn [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)))))
(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)))))
select-only
(mf/use-fn
(fn [color]
(let [groups (mf/ref-val groups-ref)
cops (get groups color)
ids (into (d/ordered-set) xf:map-shape-id cops)]
(let [shapes-by-color (get @grouped-colors* color)
ids (into (d/ordered-set) (map :shape-id) shapes-by-color)]
(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 [value]
(fn [new-value]
(let [color (-> color
(assoc :color value)
(assoc :color new-value)
(dissoc :gradient))]
(st/emit! (dwl/add-recent-color color)
(on-change color)))))
@@ -146,9 +146,7 @@
:else
color)
cpos (dom/get-client-position event)
x (dm/get-prop cpos :x)
y (dm/get-prop cpos :y)
{:keys [x y]} (dom/get-client-position event)
props {:x x
:y y
@@ -156,14 +154,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 % true)
:on-change #(on-change (merge uc/empty-color %) true)
:on-close (fn [value opacity id file-id]
(when on-close
(on-close value opacity id file-id)))
:data color}]
(when (fn? on-open)
(on-open color))
(when on-open
(on-open (merge uc/empty-color color)))
(modal/show! :colorpicker props))))

View File

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

View File

@@ -10,8 +10,12 @@
[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.register :as register]
[app.plugins.shape :as shape]
[app.util.globals :refer [global]]
[app.util.object :as obj]
[beicon.v2.core :as rx]
@@ -20,7 +24,6 @@
(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
@@ -36,3 +39,10 @@
(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)))))
(createFrame
(createBoard
[_]
(create-shape $plugin :frame))

View File

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

View File

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

View File

@@ -10,8 +10,10 @@
[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]
[app.util.storage :refer [storage]]))
[beicon.v2.core :as rx]))
;; Stores the installed plugins information
(defonce ^:private registry (atom {}))
@@ -58,40 +60,14 @@
: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
[]
(swap! storage assoc :plugins @registry))
(->> (rp/cmd! :update-profile-props {:props {:plugins @registry}})
(rx/subs! identity)))
(defn load-from-store
[]
(if (:plugins @storage)
(reset! registry (:plugins @storage))
(do (legacy-load-from-store)
(save-to-store))))
(reset! registry (get-in @st/state [:profile :props :plugins] {})))
(defn init
[]

View File

@@ -551,10 +551,10 @@
;; Interactions
(addInteraction
[self interaction]
[self trigger action delay]
(let [interaction
(-> ctsi/default-interaction
(d/patch-object (parser/parse-interaction interaction)))]
(d/patch-object (parser/parse-interaction trigger action delay)))]
(cond
(not (sm/validate ::ctsi/interaction interaction))
(u/display-not-valid :addInteraction interaction)
@@ -576,12 +576,6 @@
(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
@@ -611,7 +605,7 @@
:get #(-> % u/proxy->shape :id str)}
{:name "type"
:get #(-> % u/proxy->shape :type d/name)}
:get #(-> % u/proxy->shape :type format/shape-type)}
{:name "name"
:get #(-> % u/proxy->shape :name)
@@ -662,6 +656,21 @@
(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
@@ -978,7 +987,7 @@
parent-y (:y parent)]
(st/emit! (dw/update-position id {:y (+ parent-y value)})))))}
{:name "frameX"
{:name "boardX"
:get (fn [self]
(let [shape (u/proxy->shape self)
frame-id (:parent-id shape)
@@ -1001,7 +1010,7 @@
frame-x (:x frame)]
(st/emit! (dw/update-position id {:x (+ frame-x value)})))))}
{:name "frameY"
{:name "boardY"
:get (fn [self]
(let [shape (u/proxy->shape self)
frame-id (:parent-id shape)

View File

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

View File

@@ -80,6 +80,9 @@
(= 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,10 +760,8 @@
(.back (.-history js/window)))
(defn reload-current-window
([]
(.reload globals/location))
([force?]
(.reload globals/location force?)))
[]
(.reload (.-location js/window)))
(defn scroll-by!
([element x y]

View File

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

View File

@@ -10,148 +10,75 @@
[app.common.transit :as t]
[app.util.functions :as fns]
[app.util.globals :as g]
[cuerdas.core :as str]
[okulary.util :as ou]))
[cuerdas.core :as str]))
;; 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-backend
(defonce ^:private local-storage
(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
[prefix k]
[k]
(assert (keyword? k) "key must be keyword")
(let [kns (namespace k)
kn (name k)]
(str prefix ":" kns "/" kn)))
(str "penpot:" kns "/" kn)))
(defn- decode-key
[prefix k]
(when (str/starts-with? k prefix)
(let [l (+ (count prefix) 1)
k (subs k l)]
[k]
(when (str/starts-with? k "penpot:")
(let [k (subs k 7)]
(if (str/starts-with? k "/")
(keyword (subs k 1))
(let [[kns kn] (str/split k "/" 2)]
(keyword kns kn))))))
(defn- lookup-by-index
[backend prefix result index]
[result index]
(try
(let [key (.key ^js backend index)
key' (decode-key prefix key)]
(let [key (.key ^js local-storage index)
key' (decode-key key)]
(if key'
(let [val (.getItem ^js backend key)]
(let [val (.getItem ^js local-storage key)]
(assoc! result key' (t/decode-str val)))
result))
(catch :default _
result)))
(defn- load-data
[backend prefix]
(if (some? backend)
(let [length (.-length ^js backend)]
(defn- load
[]
(when (some? local-storage)
(let [length (.-length ^js local-storage)]
(loop [index 0
result (transient {})]
(if (< index length)
(recur (inc index)
(lookup-by-index backend prefix result index))
(persistent! result))))
{}))
(lookup-by-index result index))
(persistent! result))))))
(defn create-storage
[backend prefix]
(let [initial (load-data backend prefix)
curr-data #js {:content initial}
last-data #js {:content initial}
watches (js/Map.)
(defonce ^:private latest-state (load))
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)))))
(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)))))
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 on-change
(fns/debounce on-change* 2000))
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"))
(defonce storage (atom latest-state))
(add-watch storage :persistence
(fn [_ _ _ curr-state]
(on-change curr-state)))

View File

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

View File

@@ -103,14 +103,6 @@
(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,61 +202,108 @@
(defn make-local-external-references
[file file-id]
(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
(let [detach-text
(fn [content]
(->> content
(ct/transform-nodes
(fn [node]
(-> node
(d/update-when :fills #(mapv change-fill %))
(cond-> (not= file-id (:typography-ref-file node))
(assoc :typography-ref-file file-id)))))))
#(cond-> %
(not= file-id (:fill-color-ref-file %))
(assoc :fill-color-ref-file file-id)
change-shape
(not= file-id (:typography-ref-file %))
(assoc :typography-ref-file file-id)))))
detach-shape
(fn [shape]
(-> 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))
(cond-> shape
(not= file-id (:fill-color-ref-file shape))
(assoc :fill-color-ref-file file-id)
(cond-> (= :text (:type shape))
(update :content change-text))))
(not= file-id (:stroke-color-ref-file shape))
(assoc :stroke-color-ref-file file-id)
change-objects
(not= file-id (:component-file shape))
(assoc :component-file file-id)
(= :text (:type shape))
(update :content detach-text)))
detach-objects
(fn [objects]
(->> objects
(d/mapm #(change-shape %2))))
(d/mapm #(detach-shape %2))))
change-pages
detach-pages
(fn [pages-index]
(->> pages-index
(d/mapm
(fn [_ data]
(-> data
(update :objects change-objects))))))]
(update :objects detach-objects))))))]
(-> file
(update-in [:data :pages-index] change-pages))))
(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)))))
(defn merge-assets [target-file assets-files]
(let [merge-file-assets
(let [external-refs (collect-external-references target-file)
merge-file-assets
(fn [target 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))]
(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))))]
(cond-> target
(d/not-empty? colors)
(update-in [:data :colors] merge colors)
@@ -276,20 +323,16 @@
(defn process-export
[file-id export-type files]
(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))
(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))))
(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 [frame (.createFrame context)]
(set! (.-x frame) 100)
(set! (.-y frame) 200)
(t/is (= (.-x frame) 100))
(t/is (= (.-y frame) 200))
(.appendChild frame shape)
(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)
(t/testing " - frameX"
(set! (.-frameX shape) 10)
(t/is (m/close? (.-frameX shape) 10))
(t/testing " - boardX"
(set! (.-boardX shape) 10)
(t/is (m/close? (.-boardX shape) 10))
(t/is (m/close? (.-x shape) 110))
(t/is (m/close? (get-in @store (get-shape-path :x)) 110)))
(t/testing " - frameY"
(set! (.-frameY shape) 20)
(t/is (m/close? (.-frameY shape) 20))
(t/testing " - boardY"
(set! (.-boardY shape) 20)
(t/is (m/close? (.-boardY shape) 20))
(t/is (m/close? (.-y shape) 220))
(t/is (m/close? (get-in @store (get-shape-path :y)) 220)))