mirror of
https://github.com/penpot/penpot.git
synced 2025-12-25 23:48:53 -05:00
Compare commits
126 Commits
eva-replac
...
eva-replac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91b1c210d1 | ||
|
|
eb1eeb4750 | ||
|
|
a78477592b | ||
|
|
8707ff6511 | ||
|
|
3d8a251741 | ||
|
|
0956b66281 | ||
|
|
007b3f11f9 | ||
|
|
e8201402a7 | ||
|
|
8a22477b96 | ||
|
|
a661b2564f | ||
|
|
2c3732f3f4 | ||
|
|
e16645227b | ||
|
|
45665a3c21 | ||
|
|
3e684ea54f | ||
|
|
179e6a195d | ||
|
|
98039f13d8 | ||
|
|
40c27591f6 | ||
|
|
91d20a46d1 | ||
|
|
50bead7c56 | ||
|
|
b75b999903 | ||
|
|
810f1721c8 | ||
|
|
8a8f360c7f | ||
|
|
a4646373cf | ||
|
|
f111cbb2a4 | ||
|
|
e35fc85c3d | ||
|
|
a614207f7e | ||
|
|
1798461d21 | ||
|
|
6ce3249c6d | ||
|
|
dde0fddd6f | ||
|
|
7d36bc4025 | ||
|
|
4637aced8c | ||
|
|
9dfe5b0865 | ||
|
|
33bcc9544a | ||
|
|
babd481b7f | ||
|
|
a9733c792d | ||
|
|
7be8ac3fd7 | ||
|
|
b0351be724 | ||
|
|
9216d965ef | ||
|
|
d04fdb5fbd | ||
|
|
81e0e4f222 | ||
|
|
b8392b3731 | ||
|
|
77dba477ca | ||
|
|
b6598d1f07 | ||
|
|
f13b3c8737 | ||
|
|
520e979363 | ||
|
|
a0f8559ffc | ||
|
|
bf1dc21c75 | ||
|
|
46c20a993f | ||
|
|
0e0106f69a | ||
|
|
19bb69cc60 | ||
|
|
504eb70988 | ||
|
|
a38f425dd3 | ||
|
|
75a2331edf | ||
|
|
c2b4c9907d | ||
|
|
bd5bbcae26 | ||
|
|
84273508ad | ||
|
|
9245ba6bc2 | ||
|
|
4be046406d | ||
|
|
84c747cd31 | ||
|
|
0036a9a0cd | ||
|
|
2105c3a68c | ||
|
|
38efa88460 | ||
|
|
6e254c2cf4 | ||
|
|
416980f063 | ||
|
|
f76710296c | ||
|
|
6251fa6b22 | ||
|
|
aedd8cc11e | ||
|
|
d1379c55f6 | ||
|
|
b125c7b5a3 | ||
|
|
496d37795b | ||
|
|
2f0853f5cc | ||
|
|
648e660bcf | ||
|
|
9f6899007a | ||
|
|
bee2f70bfa | ||
|
|
00f8eac8fa | ||
|
|
df7caacb45 | ||
|
|
641df77834 | ||
|
|
49bbdfb257 | ||
|
|
4e84deca44 | ||
|
|
0d21e52068 | ||
|
|
1b29e9a50f | ||
|
|
94af978be8 | ||
|
|
feababe2a8 | ||
|
|
5ef06685fc | ||
|
|
9f567c3bf4 | ||
|
|
1ba15e5d10 | ||
|
|
57fcec5afc | ||
|
|
58f82da61e | ||
|
|
a28c5b61ca | ||
|
|
53aad7bc15 | ||
|
|
9123d199b7 | ||
|
|
37e45a8bbf | ||
|
|
3471d40f46 | ||
|
|
c6b64a8e39 | ||
|
|
511e80c948 | ||
|
|
f5a640d104 | ||
|
|
3ae7c514e4 | ||
|
|
57297741f5 | ||
|
|
eeaf28bb25 | ||
|
|
d63d692d34 | ||
|
|
fad9ed1c48 | ||
|
|
0caaefefea | ||
|
|
b179aa79b1 | ||
|
|
6b8091bb90 | ||
|
|
fe72d0af82 | ||
|
|
405ddb60d8 | ||
|
|
bba02473d5 | ||
|
|
95c0d42d5b | ||
|
|
721b337511 | ||
|
|
359379be09 | ||
|
|
876d5783cf | ||
|
|
786f73767b | ||
|
|
95b7784a42 | ||
|
|
4690f740b9 | ||
|
|
4282cdcd2c | ||
|
|
e889413f26 | ||
|
|
115273b478 | ||
|
|
fdddd3284a | ||
|
|
51385a04a0 | ||
|
|
f96ed8ccd6 | ||
|
|
bda5de5c1b | ||
|
|
59f3b4db4c | ||
|
|
7ee03ad911 | ||
|
|
130b8c8214 | ||
|
|
0198d41757 | ||
|
|
567a955151 |
21
.github/workflows/build-nitrate-module.yml
vendored
Normal file
21
.github/workflows/build-nitrate-module.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: _NITRATE MODULE
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '36 5-20 * * 1-5'
|
||||
|
||||
jobs:
|
||||
build-bundle:
|
||||
uses: ./.github/workflows/build-bundle.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "nitrate-module"
|
||||
build_wasm: "yes"
|
||||
build_storybook: "yes"
|
||||
|
||||
build-docker:
|
||||
needs: build-bundle
|
||||
uses: ./.github/workflows/build-docker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "nitrate-module"
|
||||
1
.github/workflows/build-tag.yml
vendored
1
.github/workflows/build-tag.yml
vendored
@@ -23,6 +23,7 @@ jobs:
|
||||
|
||||
notify:
|
||||
name: Notifications
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-docker
|
||||
|
||||
steps:
|
||||
|
||||
10
CHANGES.md
10
CHANGES.md
@@ -8,12 +8,17 @@
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Fix mask issues with component swap (by @dfelinto) [Github #7675](https://github.com/penpot/penpot/issues/7675)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565)
|
||||
- Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460)
|
||||
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
|
||||
|
||||
|
||||
## 2.12.0 (Unreleased)
|
||||
@@ -77,6 +82,7 @@ example. It's still usable as before, we just removed the example.
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Ensure consistent snap behavior across all zoom levels [Github #7774](https://github.com/penpot/penpot/pull/7774) by [@Tokytome](https://github.com/Tokytome)
|
||||
- Fix crash in token grid view due to tooltip validation (by @dfelinto) [Github #7887](https://github.com/penpot/penpot/pull/7887)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
@@ -104,6 +110,10 @@ example. It's still usable as before, we just removed the example.
|
||||
- Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294)
|
||||
- Fix copy/pasting application/transit+json [Taiga #12721](https://tree.taiga.io/project/penpot/issue/12721)
|
||||
- Fix problem with plugins content attribute [Plugins #209](https://github.com/penpot/penpot-plugins/issues/209)
|
||||
- Fix U and E icon displayed in project list [Taiga #12806](https://tree.taiga.io/project/penpot/issue/12806)
|
||||
- Fix unpublish library modal not scrolling a long file list [Taiga #12285](https://tree.taiga.io/project/penpot/issue/12285)
|
||||
- Fix incorrect interaction betwen hower and scroll on assets sidebar [Taiga #12389](https://tree.taiga.io/project/penpot/issue/12389)
|
||||
- Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841)
|
||||
|
||||
## 2.11.1
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
|
||||
export PENPOT_SECRET_KEY=super-secret-devenv-key
|
||||
export PENPOT_HOST=devenv
|
||||
export PENPOT_PUBLIC_URI=https://localhost:3449
|
||||
|
||||
export PENPOT_FLAGS="\
|
||||
$PENPOT_FLAGS \
|
||||
|
||||
@@ -106,17 +106,17 @@
|
||||
(let [content-part (MimeBodyPart.)
|
||||
alternative-mpart (MimeMultipart. "alternative")]
|
||||
|
||||
(when-let [content (get body "text/plain")]
|
||||
(let [text-part (MimeBodyPart.)]
|
||||
(.setText text-part ^String content ^String charset)
|
||||
(.addBodyPart alternative-mpart text-part)))
|
||||
|
||||
(when-let [content (get body "text/html")]
|
||||
(let [html-part (MimeBodyPart.)]
|
||||
(.setContent html-part ^String content
|
||||
(str "text/html; charset=" charset))
|
||||
(.addBodyPart alternative-mpart html-part)))
|
||||
|
||||
(when-let [content (get body "text/plain")]
|
||||
(let [text-part (MimeBodyPart.)]
|
||||
(.setText text-part ^String content ^String charset)
|
||||
(.addBodyPart alternative-mpart text-part)))
|
||||
|
||||
(.setContent content-part alternative-mpart)
|
||||
(.addBodyPart mixed-mpart content-part))
|
||||
|
||||
|
||||
@@ -79,18 +79,6 @@
|
||||
(remove #(contains? reserved-props (key %))))
|
||||
props))
|
||||
|
||||
(defn event-from-rpc-params
|
||||
"Create a base event skeleton with pre-filled some important
|
||||
data that can be extracted from RPC params object"
|
||||
[params]
|
||||
(let [context {:external-session-id (::rpc/external-session-id params)
|
||||
:external-event-origin (::rpc/external-event-origin params)
|
||||
:triggered-by (::rpc/handler-name params)}]
|
||||
{::type "action"
|
||||
::profile-id (::rpc/profile-id params)
|
||||
::ip-addr (::rpc/ip-addr params)
|
||||
::context (d/without-nils context)}))
|
||||
|
||||
(defn get-external-session-id
|
||||
[request]
|
||||
(when-let [session-id (yreq/get-header request "x-external-session-id")]
|
||||
@@ -99,13 +87,24 @@
|
||||
(str/blank? session-id))
|
||||
session-id)))
|
||||
|
||||
(defn- get-external-event-origin
|
||||
(defn- get-client-event-origin
|
||||
[request]
|
||||
(when-let [origin (yreq/get-header request "x-event-origin")]
|
||||
(when-not (or (> (count origin) 256)
|
||||
(= origin "null")
|
||||
(when-not (or (= origin "null")
|
||||
(str/blank? origin))
|
||||
origin)))
|
||||
(str/prune origin 200))))
|
||||
|
||||
(defn get-client-user-agent
|
||||
[request]
|
||||
(when-let [user-agent (yreq/get-header request "user-agent")]
|
||||
(str/prune user-agent 500)))
|
||||
|
||||
(defn- get-client-version
|
||||
[request]
|
||||
(when-let [origin (yreq/get-header request "x-frontend-version")]
|
||||
(when-not (or (= origin "null")
|
||||
(str/blank? origin))
|
||||
(str/prune origin 100))))
|
||||
|
||||
;; --- SPECS
|
||||
|
||||
@@ -134,6 +133,33 @@
|
||||
(def ^:private check-event
|
||||
(sm/check-fn schema:event))
|
||||
|
||||
(defn- prepare-context-from-request
|
||||
[request]
|
||||
(let [client-event-origin (get-client-event-origin request)
|
||||
client-version (get-client-version request)
|
||||
client-user-agent (get-client-user-agent request)
|
||||
session-id (get-external-session-id request)
|
||||
token-id (::actoken/id request)]
|
||||
(d/without-nils
|
||||
{:external-session-id session-id
|
||||
:access-token-id (some-> token-id str)
|
||||
:client-event-origin client-event-origin
|
||||
:client-user-agent client-user-agent
|
||||
:client-version client-version
|
||||
:version (:full cf/version)})))
|
||||
|
||||
(defn event-from-rpc-params
|
||||
"Create a base event skeleton with pre-filled some important
|
||||
data that can be extracted from RPC params object"
|
||||
[params]
|
||||
(let [context (some-> params meta ::http/request prepare-context-from-request)
|
||||
event {::type "action"
|
||||
::profile-id (or (::rpc/profile-id params) uuid/zero)
|
||||
::ip-addr (::rpc/ip-addr params)}]
|
||||
(cond-> event
|
||||
(some? context)
|
||||
(assoc ::context context))))
|
||||
|
||||
(defn prepare-event
|
||||
[cfg mdata params result]
|
||||
(let [resultm (meta result)
|
||||
@@ -148,18 +174,10 @@
|
||||
(merge (::props resultm))
|
||||
(dissoc :profile-id)
|
||||
(dissoc :type)))
|
||||
|
||||
(clean-props))
|
||||
|
||||
token-id (::actoken/id request)
|
||||
context (-> (::context resultm)
|
||||
(assoc :external-session-id
|
||||
(get-external-session-id request))
|
||||
(assoc :external-event-origin
|
||||
(get-external-event-origin request))
|
||||
(assoc :access-token-id (some-> token-id str))
|
||||
(d/without-nils))
|
||||
|
||||
context (merge (::context resultm)
|
||||
(prepare-context-from-request request))
|
||||
ip-addr (inet/parse-request request)]
|
||||
|
||||
{::type (or (::type resultm)
|
||||
|
||||
@@ -12,8 +12,11 @@
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.variant :as cfv]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.logging :as log]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.logic.variant-properties :as clvp]
|
||||
@@ -25,6 +28,7 @@
|
||||
[app.common.types.library :as ctl]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.path.segment :as segment]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
@@ -1874,6 +1878,44 @@
|
||||
roperations'
|
||||
uoperations')))))))
|
||||
|
||||
(defn- set-path-new-values
|
||||
[current-shape prev-shape transform]
|
||||
(let [new-content (segment/transform-content
|
||||
(:content current-shape)
|
||||
(gmt/transform-in (gpt/point 0 0) transform))
|
||||
new-points (-> (segment/content->selrect new-content)
|
||||
(grc/rect->points))
|
||||
points-center (gco/points->center new-points)
|
||||
new-selrect (gsh/calculate-selrect new-points points-center)
|
||||
shape (assoc current-shape
|
||||
:content new-content
|
||||
:points new-points
|
||||
:selrect new-selrect)
|
||||
|
||||
prev-center (segment/content-center (:content prev-shape))
|
||||
delta (gpt/subtract points-center (first new-points))
|
||||
new-pos (gpt/subtract prev-center delta)]
|
||||
(gsh/absolute-move shape new-pos)))
|
||||
|
||||
(defn- switch-path-change-value
|
||||
[prev-shape ;; The shape before the switch
|
||||
current-shape ;; The shape after the switch (a clean copy)
|
||||
ref-shape ;; The referenced shape on the main component
|
||||
;; before the switch
|
||||
attr]
|
||||
(let [old-width (-> ref-shape :selrect :width)
|
||||
new-width (-> prev-shape :selrect :width)
|
||||
|
||||
old-height (-> ref-shape :selrect :height)
|
||||
new-height (-> prev-shape :selrect :height)
|
||||
|
||||
transform (-> (gpt/point (/ new-width old-width)
|
||||
(/ new-height old-height))
|
||||
(gmt/scale-matrix))
|
||||
|
||||
shape (set-path-new-values current-shape prev-shape transform)]
|
||||
(get shape attr)))
|
||||
|
||||
|
||||
(defn- switch-text-change-value
|
||||
[prev-content ;; The :content of the text before the switch
|
||||
@@ -2025,6 +2067,10 @@
|
||||
(= :content attr)
|
||||
(touched attr-group))
|
||||
|
||||
path-change?
|
||||
(and (= :path (:type current-shape))
|
||||
(contains? #{:points :selrect :content} attr))
|
||||
|
||||
;; position-data is a special case because can be affected by :geometry-group and :content-group
|
||||
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
|
||||
;; so it's calculated again
|
||||
@@ -2053,6 +2099,12 @@
|
||||
(:content origin-ref-shape)
|
||||
touched)
|
||||
|
||||
path-change?
|
||||
(switch-path-change-value previous-shape
|
||||
current-shape
|
||||
origin-ref-shape
|
||||
attr)
|
||||
|
||||
:else
|
||||
(get previous-shape attr)))
|
||||
|
||||
@@ -2439,11 +2491,13 @@
|
||||
(ctk/get-swap-slot))
|
||||
(constantly false))
|
||||
|
||||
;; In the cases where the swapped shape was the first element of the masked group it would make the group to loose the
|
||||
;; mask property as part of the sanitization check on generate-delete-shapes, passing "ignore-mask" to prevent this
|
||||
[all-parents changes]
|
||||
(-> changes
|
||||
(cls/generate-delete-shapes
|
||||
file page objects (d/ordered-set (:id shape))
|
||||
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn}))
|
||||
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn :ignore-mask true}))
|
||||
[new-shape changes]
|
||||
(-> changes
|
||||
(generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))]
|
||||
|
||||
@@ -123,8 +123,10 @@
|
||||
;; ignore-children-fn is used to ignore some descendants
|
||||
;; on the deletion process. It should receive a shape and
|
||||
;; return a boolean
|
||||
ignore-children-fn]
|
||||
:or {ignore-children-fn (constantly false)}}]
|
||||
ignore-children-fn
|
||||
ignore-mask]
|
||||
:or {ignore-children-fn (constantly false)
|
||||
ignore-mask false}}]
|
||||
(let [objects (pcb/get-objects changes)
|
||||
data (pcb/get-library-data changes)
|
||||
page-id (pcb/get-page-id changes)
|
||||
@@ -162,18 +164,20 @@
|
||||
lookup (d/getf objects)
|
||||
|
||||
groups-to-unmask
|
||||
(reduce (fn [group-ids id]
|
||||
;; When the shape to delete is the mask of a masked group,
|
||||
;; the mask condition must be removed, and it must be
|
||||
;; converted to a normal group.
|
||||
(let [obj (lookup id)
|
||||
parent (lookup (:parent-id obj))]
|
||||
(if (and (:masked-group parent)
|
||||
(= id (first (:shapes parent))))
|
||||
(conj group-ids (:id parent))
|
||||
group-ids)))
|
||||
#{}
|
||||
ids-to-delete)
|
||||
(when-not ignore-mask
|
||||
(reduce (fn [group-ids id]
|
||||
;; When the shape to delete is the mask of a masked group,
|
||||
;; the mask condition must be removed, and it must be
|
||||
;; converted to a normal group.
|
||||
(let [obj (lookup id)
|
||||
parent (lookup (:parent-id obj))]
|
||||
(if (and (:masked-group parent)
|
||||
(= id (first (:shapes parent))))
|
||||
(conj group-ids (:id parent))
|
||||
group-ids)))
|
||||
#{}
|
||||
ids-to-delete)
|
||||
[])
|
||||
|
||||
interacting-shapes
|
||||
(filter (fn [shape]
|
||||
|
||||
@@ -132,3 +132,94 @@ Some naming conventions:
|
||||
(if-let [last-period (str/last-index-of s ".")]
|
||||
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
|
||||
[s ""]))
|
||||
|
||||
;; Tree building functions --------------------------------------------------
|
||||
|
||||
"Build tree structure from flat list of paths"
|
||||
|
||||
"`build-tree-root` is the main function to build the tree."
|
||||
|
||||
"Receives a list of segments with 'name' properties representing paths,
|
||||
and a separator string."
|
||||
"E.g segments = [{... :name 'one/two/three'} {... :name 'one/two/four'} {... :name 'one/five'}]"
|
||||
|
||||
"Transforms into a tree structure like:
|
||||
[{:name 'one'
|
||||
:path 'one'
|
||||
:depth 0
|
||||
:leaf nil
|
||||
:children-fn (fn [] [{:name 'two'
|
||||
:path 'one.two'
|
||||
:depth 1
|
||||
:leaf nil
|
||||
:children-fn (fn [] [{... :name 'three'} {... :name 'four'}])}
|
||||
{:name 'five'
|
||||
:path 'one.five'
|
||||
:depth 1
|
||||
:leaf {... :name 'five'}
|
||||
...}])}]"
|
||||
|
||||
(defn- sort-by-children
|
||||
"Sorts segments so that those with children come first."
|
||||
[segments separator]
|
||||
(sort-by (fn [segment]
|
||||
(let [path (split-path (:name segment) :separator separator)
|
||||
path-length (count path)]
|
||||
(if (= path-length 1)
|
||||
1
|
||||
0)))
|
||||
segments))
|
||||
|
||||
(defn- group-by-first-segment
|
||||
"Groups segments by their first path segment and update segment name."
|
||||
[segments separator]
|
||||
(reduce (fn [acc segment]
|
||||
(let [[first-segment & remaining-segments] (split-path (:name segment) :separator separator)
|
||||
rest-path (when (seq remaining-segments) (join-path remaining-segments :separator separator :with-spaces? false))]
|
||||
(update acc first-segment (fnil conj [])
|
||||
(if rest-path
|
||||
(assoc segment :name rest-path)
|
||||
segment))))
|
||||
{}
|
||||
segments))
|
||||
|
||||
(defn- sort-and-group-segments
|
||||
"Sorts elements and groups them by their first path segment."
|
||||
[segments separator]
|
||||
(let [sorted (sort-by-children segments separator)
|
||||
grouped (group-by-first-segment sorted separator)]
|
||||
grouped))
|
||||
|
||||
(defn- build-tree-node
|
||||
"Builds a single tree node with lazy children."
|
||||
[segment-name remaining-segments separator parent-path depth]
|
||||
(let [current-path (if parent-path
|
||||
(str parent-path "." segment-name)
|
||||
segment-name)
|
||||
|
||||
is-leaf? (and (seq remaining-segments)
|
||||
(every? (fn [segment]
|
||||
(let [remaining-segment-name (first (split-path (:name segment) :separator separator))]
|
||||
(= segment-name remaining-segment-name)))
|
||||
remaining-segments))
|
||||
|
||||
leaf-segment (when is-leaf? (first remaining-segments))
|
||||
node {:name segment-name
|
||||
:path current-path
|
||||
:depth depth
|
||||
:leaf leaf-segment
|
||||
:children-fn (when-not is-leaf?
|
||||
(fn []
|
||||
(let [grouped-elements (sort-and-group-segments remaining-segments separator)]
|
||||
(mapv (fn [[child-segment-name remaining-child-segments]]
|
||||
(build-tree-node child-segment-name remaining-child-segments separator current-path (inc depth)))
|
||||
grouped-elements))))}]
|
||||
node))
|
||||
|
||||
(defn build-tree-root
|
||||
"Builds the root level of the tree."
|
||||
[segments separator]
|
||||
(let [grouped-elements (sort-and-group-segments segments separator)]
|
||||
(mapv (fn [[segment-name remaining-segments]]
|
||||
(build-tree-node segment-name remaining-segments separator nil 0))
|
||||
grouped-elements)))
|
||||
|
||||
@@ -284,7 +284,20 @@
|
||||
(defn check-fn
|
||||
"Create a predefined check function"
|
||||
[s & {:keys [hint type code]}]
|
||||
(let [s (schema s)
|
||||
(let [s #?(:clj
|
||||
(schema s)
|
||||
:cljs
|
||||
(try
|
||||
(schema s)
|
||||
(catch :default cause
|
||||
(let [data (ex-data cause)]
|
||||
(if (= :malli.core/invalid-schema (:type data))
|
||||
(throw (ex-info
|
||||
(str "Invalid schema\n"
|
||||
(pp/pprint-str (:data data)))
|
||||
{}))
|
||||
(throw cause))))))
|
||||
|
||||
validator* (delay (m/validator s))
|
||||
explainer* (delay (m/explainer s))
|
||||
hint (or ^boolean hint "check error")
|
||||
@@ -304,7 +317,7 @@
|
||||
|
||||
(defn coercer
|
||||
[schema & {:as opts}]
|
||||
(let [decode-fn (decoder schema json-transformer)
|
||||
(let [decode-fn (lazy-decoder schema json-transformer)
|
||||
check-fn (check-fn schema opts)]
|
||||
(fn [data]
|
||||
(-> data decode-fn check-fn))))
|
||||
|
||||
@@ -234,16 +234,15 @@
|
||||
"Calculate the boolean content from shape and objects. Returns a
|
||||
packed PathData instance"
|
||||
[shape objects]
|
||||
(let [content (if (fn? wasm:calc-bool-content)
|
||||
(wasm:calc-bool-content (get shape :bool-type)
|
||||
(get shape :shapes))
|
||||
(calc-bool-content* shape objects))]
|
||||
(let [content (calc-bool-content* shape objects)]
|
||||
(impl/path-data content)))
|
||||
|
||||
(defn update-bool-shape
|
||||
"Calculates the selrect+points for the boolean shape"
|
||||
[shape objects]
|
||||
(let [content (calc-bool-content shape objects)
|
||||
(let [content (if (fn? wasm:calc-bool-content)
|
||||
(wasm:calc-bool-content shape objects)
|
||||
(calc-bool-content shape objects))
|
||||
shape (assoc shape :content content)]
|
||||
(update-geometry shape)))
|
||||
|
||||
|
||||
@@ -1575,10 +1575,10 @@ Will return a value that matches this schema:
|
||||
(if (map? shadow)
|
||||
(let [legacy-shadow-type (get "type" shadow)]
|
||||
(-> shadow
|
||||
(set/rename-keys {"x" :offsetX
|
||||
"offsetX" :offsetX
|
||||
"y" :offsetY
|
||||
"offsetY" :offsetY
|
||||
(set/rename-keys {"x" :offset-x
|
||||
"offsetX" :offset-x
|
||||
"y" :offset-y
|
||||
"offsetY" :offset-y
|
||||
"blur" :blur
|
||||
"spread" :spread
|
||||
"color" :color
|
||||
@@ -1589,7 +1589,7 @@ Will return a value that matches this schema:
|
||||
(= "false" %) false
|
||||
(= legacy-shadow-type "innerShadow") true
|
||||
:else false))
|
||||
(select-keys [:offsetX :offsetY :blur :spread :color :inset])))
|
||||
(select-keys [:offset-x :offset-y :blur :spread :color :inset])))
|
||||
shadow))]
|
||||
(cond
|
||||
;; Reference value - keep as string
|
||||
@@ -1860,8 +1860,8 @@ Will return a value that matches this schema:
|
||||
(mapv (fn [shadow]
|
||||
(if (map? shadow)
|
||||
(-> shadow
|
||||
(set/rename-keys {:offsetX "offsetX"
|
||||
:offsetY "offsetY"
|
||||
(set/rename-keys {:offset-x "offsetX"
|
||||
:offset-y "offsetY"
|
||||
:blur "blur"
|
||||
:spread "spread"
|
||||
:color "color"
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
(defn parse
|
||||
[data]
|
||||
(cond
|
||||
(str/starts-with? data "%")
|
||||
(or (str/starts-with? data "%")
|
||||
(= data "develop"))
|
||||
{:full "develop"
|
||||
:branch "develop"
|
||||
:base "0.0.0"
|
||||
|
||||
@@ -1897,15 +1897,15 @@
|
||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")]
|
||||
(t/is (some? token))
|
||||
(t/is (= :shadow (:type token)))
|
||||
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
|
||||
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
|
||||
(:value token)))))
|
||||
|
||||
(t/testing "multiple shadow token"
|
||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")]
|
||||
(t/is (some? token))
|
||||
(t/is (= :shadow (:type token)))
|
||||
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset true}
|
||||
{:offsetX "0", :offsetY "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
|
||||
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset true}
|
||||
{:offset-x "0", :offset-y "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
|
||||
(:value token)))))
|
||||
|
||||
(t/testing "shadow token with reference"
|
||||
@@ -1918,7 +1918,7 @@
|
||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")]
|
||||
(t/is (some? token))
|
||||
(t/is (= :shadow (:type token)))
|
||||
(t/is (= [{:offsetX "0", :offsetY "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
|
||||
(t/is (= [{:offset-x "0", :offset-y "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
|
||||
(:value token)))))
|
||||
|
||||
(t/testing "shadow token with description"
|
||||
@@ -1937,14 +1937,14 @@
|
||||
(ctob/make-token
|
||||
{:name "shadow.single"
|
||||
:type :shadow
|
||||
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}]
|
||||
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}]
|
||||
:description "A single shadow"})
|
||||
"shadow.multiple"
|
||||
(ctob/make-token
|
||||
{:name "shadow.multiple"
|
||||
:type :shadow
|
||||
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}
|
||||
{:offsetX "0" :offsetY "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
|
||||
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}
|
||||
{:offset-x "0" :offset-y "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
|
||||
"shadow.ref"
|
||||
(ctob/make-token
|
||||
{:name "shadow.ref"
|
||||
@@ -1991,7 +1991,7 @@
|
||||
(ctob/make-token
|
||||
{:name "shadow.test"
|
||||
:type :shadow
|
||||
:value [{:offsetX "1" :offsetY "1" :blur "1" :spread "1" :color "red" :inset true}]
|
||||
:value [{:offset-x "1" :offset-y "1" :blur "1" :spread "1" :color "red" :inset true}]
|
||||
:description "Round trip test"})
|
||||
"shadow.ref"
|
||||
(ctob/make-token
|
||||
|
||||
@@ -395,6 +395,8 @@ COPY files/tmux.conf /root/.tmux.conf
|
||||
COPY files/sudoers /etc/sudoers
|
||||
|
||||
COPY files/Caddyfile /home/
|
||||
COPY files/selfsigned.crt /home/
|
||||
COPY files/selfsigned.key /home/
|
||||
COPY files/start-tmux.sh /home/start-tmux.sh
|
||||
COPY files/start-tmux-back.sh /home/start-tmux-back.sh
|
||||
COPY files/entrypoint.sh /home/entrypoint.sh
|
||||
|
||||
@@ -33,6 +33,8 @@ services:
|
||||
- 3447:3447
|
||||
- 3448:3448
|
||||
- 3449:3449
|
||||
- 3449:3449/udp
|
||||
- 3450:3450
|
||||
- 6006:6006
|
||||
- 6060:6060
|
||||
- 6061:6061
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
auto_https off
|
||||
}
|
||||
|
||||
localhost:3449 {
|
||||
tls internal
|
||||
reverse_proxy localhost:4449
|
||||
}
|
||||
reverse_proxy localhost:4449
|
||||
tls /home/selfsigned.crt /home/selfsigned.key
|
||||
}
|
||||
|
||||
http://localhost:3450 {
|
||||
reverse_proxy localhost:4449
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
nginx;
|
||||
caddy run -c /home/Caddyfile;
|
||||
nginx
|
||||
caddy start -c /home/Caddyfile
|
||||
tail -f /dev/null;
|
||||
|
||||
@@ -38,11 +38,11 @@ http {
|
||||
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 3;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml application/wasm;
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
@@ -223,16 +223,19 @@ http {
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
|
||||
location ~* \.(jpg|png|svg|ttf|woff|woff2)$ {
|
||||
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
location ~* \.(js|css|wasm)$ {
|
||||
add_header Cache-Control "no-store" always;
|
||||
}
|
||||
|
||||
location ~ ^/[^/]+/(.*)$ {
|
||||
return 301 " /404";
|
||||
}
|
||||
|
||||
add_header Cache-Control "no-store";
|
||||
# This header is what we need to use on prod
|
||||
# add_header Cache-Control "public, must-revalidate, max-age=0";
|
||||
add_header Cache-Control "no-store" always;
|
||||
try_files $uri /index.html$is_args$args /index.html =404;
|
||||
}
|
||||
}
|
||||
|
||||
22
docker/devenv/files/selfsigned.crt
Normal file
22
docker/devenv/files/selfsigned.crt
Normal file
@@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDuzCCAqOgAwIBAgIUa3THJQSn1+ErK65g1jDL0tjUkBYwDQYJKoZIhvcNAQEL
|
||||
BQAwXzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBUxvY2FsMQ4wDAYDVQQHDAVMb2Nh
|
||||
bDEOMAwGA1UECgwFTG9jYWwxDDAKBgNVBAsMA0RldjESMBAGA1UEAwwJbG9jYWxo
|
||||
b3N0MB4XDTI1MTIwMjA4MjUyM1oXDTI2MTIwMjA4MjUyM1owXzELMAkGA1UEBhMC
|
||||
VVMxDjAMBgNVBAgMBUxvY2FsMQ4wDAYDVQQHDAVMb2NhbDEOMAwGA1UECgwFTG9j
|
||||
YWwxDDAKBgNVBAsMA0RldjESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyVIlfpIPE+QyL/q7IQOilEA7wEOZ6wbsh2Fr
|
||||
59H1gSLFvgoCxI6RVUkQ/MFRnw/r1ZbAqRpc2xAl5a9Ml14q20Zlj6dAHsWX6O2J
|
||||
EwNsD18dQmX3BncnjV3yCZM2iQcMFKuXG4KQNdIQNNvdIgtlrHYp0ohS9s3XC7cj
|
||||
KxNrm/pW9EAXfn9AYDd/qER090L2E4ipP9m/5l3MjinNc4l2kpH9rLOgb79H0RLt
|
||||
PK3/KP8ErZhAvzdmDBAdM5Z5K37b+TfB/kSVNUKL6qyw5CCjlShERLhBNprlnRfz
|
||||
tHNIQ1RHq3qJJN19ZnJrLqICuQ5ztvj7hBDiOSV0LnmyKgXr6wIDAQABo28wbTAd
|
||||
BgNVHQ4EFgQUPL8WGf6z/wB8TimJBx1zybsIeikwHwYDVR0jBBgwFoAUPL8WGf6z
|
||||
/wB8TimJBx1zybsIeikwDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2Nh
|
||||
bGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggEBACMMVyR3kbNxnzuUc2lahKH4
|
||||
cPXVWOsvCvnDtjzm41XmKjUJTbtjn3p5d/ZmLbZ4zzIQULfWXO3XG/HevkvVo0g6
|
||||
6pJXTXc6C6ZhFG0rIYMcPPzmGmalDV5n+lUaCVx5XbFFxvRQ7893auwhRATdwGs+
|
||||
xiMyYbE2w9otKqyDItmJZJ5nW6vmXJ42YHxlXF18u9U88xqtOSMd5xZahbsmw7Gg
|
||||
A4/o4TPoAX5QfA306sL443WaczsF7bmsTf9qcYa/3xxQkP5Seyqx8ePWpS22qysE
|
||||
jG6XPpymxb6sb2mVaFBAzhEMb/eBvE9nRAopxmB7uV4TbqC51K/U3uo6jFX4Jbw=
|
||||
-----END CERTIFICATE-----
|
||||
28
docker/devenv/files/selfsigned.key
Normal file
28
docker/devenv/files/selfsigned.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJUiV+kg8T5DIv
|
||||
+rshA6KUQDvAQ5nrBuyHYWvn0fWBIsW+CgLEjpFVSRD8wVGfD+vVlsCpGlzbECXl
|
||||
r0yXXirbRmWPp0AexZfo7YkTA2wPXx1CZfcGdyeNXfIJkzaJBwwUq5cbgpA10hA0
|
||||
290iC2WsdinSiFL2zdcLtyMrE2ub+lb0QBd+f0BgN3+oRHT3QvYTiKk/2b/mXcyO
|
||||
Kc1ziXaSkf2ss6Bvv0fREu08rf8o/wStmEC/N2YMEB0zlnkrftv5N8H+RJU1Qovq
|
||||
rLDkIKOVKEREuEE2muWdF/O0c0hDVEereokk3X1mcmsuogK5DnO2+PuEEOI5JXQu
|
||||
ebIqBevrAgMBAAECggEABqtE+LNn8nW9v98jcc2IBjc2g4D5yVJaZYWxqGVJJ7T6
|
||||
Lfhw7Qf4AoZAHM9en9FMM7Ahw7hO2SboynoLJHyHGOp1FNQqiJptFNdBkjKr0rqI
|
||||
4pk0HK+3zLQO/4gz50gne0vP3qZtlorV5Jpf8e/Et3jWm9XOQcTB2e6AKL4k827B
|
||||
dv4Tld+/7PoZVXjahfrUWuIZr5mzyF1eUkD8sPOpdr3HJxSueqsOMjbG8XMRqCQ+
|
||||
5eCWWSW5yPQlMr7M7cXM+a0k73Xn1sKl7fP3/9byji25zxGUaMu5RA1kw0Oqseid
|
||||
RXuRxGphGZgnx1aFxDAPg3FtmGch7/Cc6WfqboOL0QKBgQD4GZO1gGaE8cg4lvuo
|
||||
ZUX2YJu6UJuNOmuhfvG3ui4WO9PHy3btc2q+3kutSuBcyIjhi+qbXasBcX/QOOJF
|
||||
udyTZc5PopNkJojS4JdXAZCiu5sKI3lp4DIt9qNISlXGgrJgdxGUO+DzarBctXdn
|
||||
BSwXFw5hcjJjl7wsPGQl1tBTQwKBgQDPuz5MEM5ZeUe9CT5sQDq/ld0u4aL5AHmx
|
||||
aaA2gzDgd9l2R5wHX6wLzjoVWXOmeqaYzJopt2JN4iXrtbjWkyePgZeZMyWoyJ/v
|
||||
clW9bi8HM9f9EpPr7czSj9sLUnsjd9cuTD+JuXK//jRGbRpw7r7nWtLHImjj6d2v
|
||||
APZRq0v2OQKBgBcESG/OObSbubeGSlKVEqiIzem7ELNJeDLDVCl3XE8zvbILbj0Z
|
||||
OA39EYhCKg5xjEFgeaNwTS0VGoZ2wIc3dv81sq4wpvvjl035CBFKU+DFBt0p7Vml
|
||||
MwKQnxVV0B9agLHyWe8mnvf2LeZr72ffUvfRa8QelA4pRYvVDnV0OF+BAoGAW6rM
|
||||
+tQPuvwB5DFIEozlX9XKHP4E5MyI5vktceDCmMtKcx92gup9CVif2Pv4ROaqzZK8
|
||||
FNyPzL6W7UTrpASb2H/fXgNsAudFbGyP2V/d8Ne34D1qeRoe4GwKxRxIqoYftpZ/
|
||||
E096i66pcsqCeINiSsWRbb6JesmgwbEzAScOBkECgYEA6O/Dibc9PaqRpaiE6Qut
|
||||
S3W/Rr1Pd1jbN4rOVI2TFCgMJQmc6jOdq2fCntR9acsa8HPx+djOlXTUBPKBZ/Ae
|
||||
p8umRdXVWcNMnwWVWHt7tsEuR/gYkxQ5xjXeS1VDPnEre9+EaevMBuVs8HdRsKQO
|
||||
uzvNGeAFEfqwIqn7CFQ+ndU=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -7,8 +7,10 @@ RUN set -ex; \
|
||||
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
||||
mkdir -p /opt/data/assets; \
|
||||
chown -R penpot:penpot /opt/data; \
|
||||
mkdir -p /etc/nginx/overrides/main.d/; \
|
||||
mkdir -p /etc/nginx/overrides/http.d/; \
|
||||
mkdir -p /etc/nginx/overrides/server.d/; \
|
||||
mkdir -p /etc/nginx/overrides/assets.d/; \
|
||||
mkdir -p /etc/nginx/overrides/location.d/;
|
||||
|
||||
ARG BUNDLE_PATH="./bundle-frontend/"
|
||||
|
||||
@@ -42,11 +42,11 @@ http {
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_static on;
|
||||
gzip_comp_level 4;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml application/wasm;
|
||||
|
||||
proxy_buffer_size 16k;
|
||||
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
|
||||
@@ -110,6 +110,8 @@ http {
|
||||
recursive_error_pages on;
|
||||
proxy_intercept_errors on;
|
||||
error_page 301 302 307 = @handle_redirect;
|
||||
|
||||
include /etc/nginx/overrides/assets.d/*.conf;
|
||||
}
|
||||
|
||||
location /internal/assets {
|
||||
@@ -142,24 +144,15 @@ http {
|
||||
location / {
|
||||
include /etc/nginx/overrides/location.d/*.conf;
|
||||
|
||||
location ~ ^/js/config.js$ {
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|jpg|svg|png|mjs|map)$ {
|
||||
add_header Cache-Control "max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
|
||||
location ~* \.(js|css|jpg|png|svg|ttf|woff|woff2|wasm)$ {
|
||||
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
location ~ ^/[^/]+/(.*)$ {
|
||||
return 301 " /404";
|
||||
}
|
||||
|
||||
add_header Last-Modified $date_gmt;
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
if_modified_since off;
|
||||
try_files $uri /index.html$is_args$args /index.html =404;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,19 +10,19 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/your-account">
|
||||
<h2>Your account →</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Access your account settings and manage personal access tokens</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/teams">
|
||||
<h2>Teams →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Create and manage your teams</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/comments/">
|
||||
<h2>Comments →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Give and receive feedback right over your designs</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,31 +10,31 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/assets">
|
||||
<h2>Assets →</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Store elements and styles to easily reuse them</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/libraries">
|
||||
<h2>Libraries →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Organize and manage your stored elements with Libraries</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/components">
|
||||
<h2>Components →</h2>
|
||||
<p>Speed your design workflow</p>
|
||||
<p>Speed your design workflow with reusable components</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/variants">
|
||||
<h2>Variants →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Group components into a single, customizable one</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/design-tokens">
|
||||
<h2>Design Tokens →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Synchronize visual elements across your designs</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -5,7 +5,7 @@ desc: Use Penpot's libraries for reusable design elements! Learn to create, mana
|
||||
---
|
||||
<h1 id="libraries">Libraries</h1>
|
||||
|
||||
<p class="main-paragraph">Libraries may include components, graphics, colors and typographies. Learn how to create and manage them to better organize the pieces of your designs and speed your workflow.</p>
|
||||
<p class="main-paragraph">Libraries may include components, colors and typographies. Learn how to create and manage them to better organize the pieces of your designs and speed your workflow.</p>
|
||||
|
||||
<h3 id="file-libraries">File libraries</h3>
|
||||
<p>Each file has its own file library which is where the assets that belong to this file are stored.</p>
|
||||
|
||||
@@ -10,31 +10,31 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/designing/workspace-basics">
|
||||
<h2>Workspace basics →</h2>
|
||||
<p>Workspace basics</p>
|
||||
<p>Get to know the Workspace, where designs are created</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/layers">
|
||||
<h2>Layers →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Objects available in Penpot and how to get the most of them</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/color-stroke/">
|
||||
<h2>Color & Strokes→</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Styling options available for each layer</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/text-typo">
|
||||
<h2>Text & Typography→</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Styling text content & using custom fonts</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/flexible-layouts">
|
||||
<h2>Flexible layouts →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Create designs that adapt automatically</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,13 +10,13 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/export-import/export-import-files/">
|
||||
<h2>Export/Import Penpot files →</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>How to export and import your Penpot files</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/export-import/exporting-layers/">
|
||||
<h2>Exporting layers →</h2>
|
||||
<p>Exporting layers</p>
|
||||
<p>How to export elements from your design into different file formats</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -16,7 +16,7 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/first-steps/the-interface">
|
||||
<h2>Interface tour →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Take a tour of Penpot's main areas</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -28,7 +28,7 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/first-steps/info">
|
||||
<h2>Tutorials & info →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Useful resources to better understand Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -22,49 +22,49 @@ eleventyNavigation:
|
||||
<li>
|
||||
<a href="/user-guide/designing/layers/">
|
||||
<h2>Layers</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Objects available in Penpot and how to get the most of them</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/flexible-layouts/">
|
||||
<h2>Flexible layouts</h2>
|
||||
<p>Create designs that adapt automatically.</p>
|
||||
<p>Create designs that adapt automatically</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/components/">
|
||||
<h2>Components</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Speed your design workflow with reusable components</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/variants/">
|
||||
<h2>Variants</h2>
|
||||
<p>Penpot's main areas and features</p>
|
||||
<p>Group components into a single, customizable one</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/design-tokens/">
|
||||
<h2>Design Tokens</h2>
|
||||
<p>Penpot's main areas and features</p>
|
||||
<p>Synchronize visual elements across your designs</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/dev-tools/#inspect-design">
|
||||
<h2>Inspect design</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Get production-ready code</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/prototyping/">
|
||||
<h2>Prototyping</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Build interactive prototypes to mimic your product behaviour</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/libraries/">
|
||||
<h2>Libraries</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Organize and manage your stored elements with Libraries</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,13 +10,13 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/prototyping">
|
||||
<h2>Prototyping →</h2>
|
||||
<p>Ways to start with Penpot</p>
|
||||
<p>Build interactive prototypes to mimic your product behaviour</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/testing-view-mode">
|
||||
<h2>Testing: View mode →</h2>
|
||||
<p>Info of interest about Penpot</p>
|
||||
<p>Test your designs and play the interactions</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -7,4 +7,5 @@ bb -i '(babashka.wait/wait-for-port "localhost" 9630)';
|
||||
bb -i '(babashka.wait/wait-for-path "target/app.js")';
|
||||
sleep 2;
|
||||
|
||||
export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
exec node target/app.js
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
(def browser-pool-factory
|
||||
(letfn [(create []
|
||||
(p/let [opts #js {:args #js ["--font-render-hinting=none"]}
|
||||
(p/let [opts #js {:args #js ["--allow-insecure-localhost" "--font-render-hinting=none"]}
|
||||
browser (.launch pw/chromium opts)
|
||||
id (swap! pool-browser-id inc)]
|
||||
(l/info :origin "factory" :action "create" :browser-id id)
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
(p/fmap (fn [resource]
|
||||
(assoc exchange :response/body resource)))
|
||||
(p/merr (fn [cause]
|
||||
(l/error :hint "unexpected error on export multiple"
|
||||
(l/error :hint "unexpected error on single export"
|
||||
:cause cause)
|
||||
(p/rejected cause))))))
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
(redis/pub! topic data))))
|
||||
|
||||
on-error (fn [cause]
|
||||
(l/error :hint "unexpected error on multiple exportation" :cause cause)
|
||||
(l/error :hint "unexpected error on multiple export" :cause cause)
|
||||
(if wait
|
||||
(p/rejected cause)
|
||||
(redis/pub! topic {:type :export-update
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { withThemeByClassName } from "@storybook/addon-themes";
|
||||
|
||||
|
||||
import Components from "@target/components";
|
||||
import translations from "@public/translation.en.js";
|
||||
Components.setDefaultTranslations(translations);
|
||||
|
||||
import '../resources/public/css/ds.css';
|
||||
|
||||
export const decorators = [
|
||||
|
||||
@@ -50,5 +50,8 @@
|
||||
|
||||
:shadow-cljs
|
||||
{:main-opts ["-m" "shadow.cljs.devtools.cli"]
|
||||
:jvm-opts ["--sun-misc-unsafe-memory-access=allow" "-Dpenpot.wasm.profile-marks=true"]}
|
||||
:jvm-opts ["--sun-misc-unsafe-memory-access=allow"
|
||||
"-Dpenpot.wasm.profile-marks=true"
|
||||
"-XX:+UnlockExperimentalVMOptions"
|
||||
"-XX:CompileCommand=blackhole,criterium.blackhole.Blackhole::consume"]}
|
||||
}}
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"@penpot/hljs": "portal:./vendor/hljs",
|
||||
"@penpot/mousetrap": "portal:./vendor/mousetrap",
|
||||
"@penpot/plugins-runtime": "1.3.2",
|
||||
"@penpot/svgo": "penpot/svgo#v3.1",
|
||||
"@penpot/svgo": "penpot/svgo#v3.2",
|
||||
"@penpot/text-editor": "portal:./text-editor",
|
||||
"@tokens-studio/sd-transforms": "1.2.11",
|
||||
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
|
||||
|
||||
58
frontend/playwright/data/text-editor/get-file-blank.json
Normal file
58
frontend/playwright/data/text-editor/get-file-blank.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true,
|
||||
"~:can-read": true,
|
||||
"~:is-logged": true
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "New File 1",
|
||||
"~:revn": 11,
|
||||
"~:modified-at": "~m1713873823633",
|
||||
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||
"~:is-shared": false,
|
||||
"~:version": 46,
|
||||
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
|
||||
"~:created-at": "~m1713536343369",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~u66697432-c33d-8055-8006-2c62cc084cad"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~u66697432-c33d-8055-8006-2c62cc084cad": {
|
||||
"~#penpot/pointer": [
|
||||
"~ude58c8f6-c5c2-8196-8004-3df9e2e52d88",
|
||||
{
|
||||
"~:created-at": "~m1713873823636"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||
"~:options": {
|
||||
"~:components-v2": true
|
||||
},
|
||||
"~:recent-colors": [
|
||||
{
|
||||
"~:color": "#0000ff",
|
||||
"~:opacity": 1,
|
||||
"~:id": null,
|
||||
"~:file-id": null,
|
||||
"~:image": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
345
frontend/playwright/data/text-editor/get-file-lorem-ipsum.json
Normal file
345
frontend/playwright/data/text-editor/get-file-lorem-ipsum.json
Normal file
@@ -0,0 +1,345 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"fdata/path-data",
|
||||
"plugins/runtime",
|
||||
"design-tokens/v1",
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type",
|
||||
"text-editor/v2"
|
||||
]
|
||||
},
|
||||
"~:team-id": "~u9e6e22b2-db76-81d6-8006-75d7cdbb8bad",
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true,
|
||||
"~:can-read": true,
|
||||
"~:is-logged": true
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "Bug 11552",
|
||||
"~:revn": 3,
|
||||
"~:modified-at": "~m1753957736516",
|
||||
"~:vern": 0,
|
||||
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
|
||||
"~:is-shared": false,
|
||||
"~:migrations": {
|
||||
"~#ordered-set": [
|
||||
"legacy-2",
|
||||
"legacy-3",
|
||||
"legacy-5",
|
||||
"legacy-6",
|
||||
"legacy-7",
|
||||
"legacy-8",
|
||||
"legacy-9",
|
||||
"legacy-10",
|
||||
"legacy-11",
|
||||
"legacy-12",
|
||||
"legacy-13",
|
||||
"legacy-14",
|
||||
"legacy-16",
|
||||
"legacy-17",
|
||||
"legacy-18",
|
||||
"legacy-19",
|
||||
"legacy-25",
|
||||
"legacy-26",
|
||||
"legacy-27",
|
||||
"legacy-28",
|
||||
"legacy-29",
|
||||
"legacy-31",
|
||||
"legacy-32",
|
||||
"legacy-33",
|
||||
"legacy-34",
|
||||
"legacy-36",
|
||||
"legacy-37",
|
||||
"legacy-38",
|
||||
"legacy-39",
|
||||
"legacy-40",
|
||||
"legacy-41",
|
||||
"legacy-42",
|
||||
"legacy-43",
|
||||
"legacy-44",
|
||||
"legacy-45",
|
||||
"legacy-46",
|
||||
"legacy-47",
|
||||
"legacy-48",
|
||||
"legacy-49",
|
||||
"legacy-50",
|
||||
"legacy-51",
|
||||
"legacy-52",
|
||||
"legacy-53",
|
||||
"legacy-54",
|
||||
"legacy-55",
|
||||
"legacy-56",
|
||||
"legacy-57",
|
||||
"legacy-59",
|
||||
"legacy-62",
|
||||
"legacy-65",
|
||||
"legacy-66",
|
||||
"legacy-67",
|
||||
"0001-remove-tokens-from-groups",
|
||||
"0002-normalize-bool-content-v2",
|
||||
"0002-clean-shape-interactions",
|
||||
"0003-fix-root-shape",
|
||||
"0003-convert-path-content-v2",
|
||||
"0004-clean-shadow-color",
|
||||
"0005-deprecate-image-type",
|
||||
"0006-fix-old-texts-fills",
|
||||
"0007-clear-invalid-strokes-and-fills-v2",
|
||||
"0008-fix-library-colors-v4",
|
||||
"0009-clean-library-colors",
|
||||
"0009-add-partial-text-touched-flags"
|
||||
]
|
||||
},
|
||||
"~:version": 67,
|
||||
"~:project-id": "~u9e6e22b2-db76-81d6-8006-75d7cdc30669",
|
||||
"~:created-at": "~m1753957644225",
|
||||
"~:data": {
|
||||
"~:pages": ["~u238a17e0-75ff-8075-8006-934586ea2231"],
|
||||
"~:pages-index": {
|
||||
"~u238a17e0-75ff-8075-8006-934586ea2231": {
|
||||
"~:objects": {
|
||||
"~u00000000-0000-0000-0000-000000000000": {
|
||||
"~#shape": {
|
||||
"~:y": 0,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:name": "Root Frame",
|
||||
"~:width": 0.01,
|
||||
"~:type": "~:frame",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.01
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.01
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 0,
|
||||
"~:proportion": 1.0,
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 0,
|
||||
"~:y": 0,
|
||||
"~:width": 0.01,
|
||||
"~:height": 0.01,
|
||||
"~:x1": 0,
|
||||
"~:y1": 0,
|
||||
"~:x2": 0.01,
|
||||
"~:y2": 0.01
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#FFFFFF",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 0.01,
|
||||
"~:flip-y": null,
|
||||
"~:shapes": ["~ucc6f0580-449c-8019-8006-9345db077fa0"]
|
||||
}
|
||||
},
|
||||
"~ucc6f0580-449c-8019-8006-9345db077fa0": {
|
||||
"~#shape": {
|
||||
"~:y": 438,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:auto-width",
|
||||
"~:content": {
|
||||
"~:type": "root",
|
||||
"~:key": "1s4am1jl24s",
|
||||
"~:children": [
|
||||
{
|
||||
"~:type": "paragraph-set",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "13p0zwl2yhc",
|
||||
"~:font-size": "14",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "Lorem ipsum"
|
||||
}
|
||||
],
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "20hf3kmyoub",
|
||||
"~:font-size": "14",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:type": "paragraph",
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"~:vertical-align": "top"
|
||||
},
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Lorem ipsum",
|
||||
"~:width": 77,
|
||||
"~:type": "~:text",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 404,
|
||||
"~:y": 438
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 481,
|
||||
"~:y": 438
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 481,
|
||||
"~:y": 455
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 404,
|
||||
"~:y": 455
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:id": "~ucc6f0580-449c-8019-8006-9345db077fa0",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:x": 404,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 404,
|
||||
"~:y": 438,
|
||||
"~:width": 77,
|
||||
"~:height": 17,
|
||||
"~:x1": 404,
|
||||
"~:y1": 438,
|
||||
"~:x2": 481,
|
||||
"~:y2": 455
|
||||
}
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 17,
|
||||
"~:flip-y": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2231",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
},
|
||||
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1 @@
|
||||
{
|
||||
"~:revn": 2,
|
||||
"~:lagged": []
|
||||
|
||||
}
|
||||
w
|
||||
|
||||
4
frontend/playwright/data/text-editor/update-file.json
Normal file
4
frontend/playwright/data/text-editor/update-file.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"~:revn": 2,
|
||||
"~:lagged": []
|
||||
}
|
||||
@@ -5947,8 +5947,8 @@
|
||||
"~:spread": "10",
|
||||
"~:color": "rgb(160, 73, 73)",
|
||||
"~:inset": true,
|
||||
"~:offsetX": "10",
|
||||
"~:offsetY": "10"
|
||||
"~:offset-x": "10",
|
||||
"~:offset-y": "10"
|
||||
}
|
||||
],
|
||||
"~:description": "",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"~:id": "~u088df3d4-d383-80f6-8004-527e50ea4f1f",
|
||||
"~:revn": 21,
|
||||
"~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||
"~:session-id": "~u1dc6d4fa-7bd3-803a-8004-527dd9df2c62",
|
||||
"~:changes": []
|
||||
}
|
||||
]
|
||||
36
frontend/playwright/helpers/Clipboard.js
Normal file
36
frontend/playwright/helpers/Clipboard.js
Normal file
@@ -0,0 +1,36 @@
|
||||
export class Clipboard {
|
||||
static Permission = {
|
||||
ONLY_READ: ['clipboard-read'],
|
||||
ONLY_WRITE: ['clipboard-write'],
|
||||
ALL: ['clipboard-read', 'clipboard-write']
|
||||
}
|
||||
|
||||
static enable(context, permissions) {
|
||||
return context.grantPermissions(permissions)
|
||||
}
|
||||
|
||||
static writeText(page, text) {
|
||||
return page.evaluate((text) => navigator.clipboard.writeText(text), text);
|
||||
}
|
||||
|
||||
static readText(page) {
|
||||
return page.evaluate(() => navigator.clipboard.readText());
|
||||
}
|
||||
|
||||
constructor(page, context) {
|
||||
this.page = page
|
||||
this.context = context
|
||||
}
|
||||
|
||||
enable(permissions) {
|
||||
return Clipboard.enable(this.context, permissions);
|
||||
}
|
||||
|
||||
writeText(text) {
|
||||
return Clipboard.writeText(this.page, text);
|
||||
}
|
||||
|
||||
readText() {
|
||||
return Clipboard.readText(this.page);
|
||||
}
|
||||
}
|
||||
30
frontend/playwright/helpers/Transit.js
Normal file
30
frontend/playwright/helpers/Transit.js
Normal file
@@ -0,0 +1,30 @@
|
||||
export class Transit {
|
||||
static parse(value) {
|
||||
if (typeof value !== 'string')
|
||||
return value
|
||||
|
||||
if (value.startsWith('~'))
|
||||
return value.slice(2)
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
static get(object, ...path) {
|
||||
let aux = object;
|
||||
for (const name of path) {
|
||||
if (typeof name !== 'string') {
|
||||
if (!(name in aux)) {
|
||||
return undefined;
|
||||
}
|
||||
aux = aux[name];
|
||||
} else {
|
||||
const transitName = `~:${name}`;
|
||||
if (!(transitName in aux)) {
|
||||
return undefined;
|
||||
}
|
||||
aux = aux[transitName];
|
||||
}
|
||||
}
|
||||
return this.parse(aux);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,27 @@
|
||||
export class BasePage {
|
||||
/**
|
||||
* Mocks multiple RPC calls in a single call.
|
||||
*
|
||||
* @param {Page} page
|
||||
* @param {object<string, string>} paths
|
||||
* @param {*} options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async mockRPCs(page, paths, options) {
|
||||
for (const [path, jsonFilename] of Object.entries(paths)) {
|
||||
await this.mockRPC(page, path, jsonFilename, options)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks an RPC call using a file.
|
||||
*
|
||||
* @param {Page} page
|
||||
* @param {string} path
|
||||
* @param {string} jsonFilename
|
||||
* @param {*} options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async mockRPC(page, path, jsonFilename, options) {
|
||||
if (!page) {
|
||||
throw new TypeError("Invalid page argument. Must be a Playwright page.");
|
||||
@@ -73,7 +96,7 @@ export class BasePage {
|
||||
}
|
||||
|
||||
static async mockConfigFlags(page, flags) {
|
||||
const url = "**/js/config.js";
|
||||
const url = "**/js/config.js*";
|
||||
return await page.route(url, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
@@ -93,6 +116,10 @@ export class BasePage {
|
||||
return this.#page;
|
||||
}
|
||||
|
||||
async mockRPCs(paths, options) {
|
||||
return BasePage.mockRPCs(this.page, paths, options);
|
||||
}
|
||||
|
||||
async mockRPC(path, jsonFilename, options) {
|
||||
return BasePage.mockRPC(this.page, path, jsonFilename, options);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,146 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { BaseWebSocketPage } from "./BaseWebSocketPage";
|
||||
import { Transit } from '../../helpers/Transit';
|
||||
|
||||
export class WorkspacePage extends BaseWebSocketPage {
|
||||
static TextEditor = class TextEditor {
|
||||
constructor(workspacePage) {
|
||||
this.workspacePage = workspacePage;
|
||||
|
||||
// locators.
|
||||
this.fontSize = this.workspacePage.rightSidebar.getByRole("textbox", {
|
||||
name: "Font Size",
|
||||
});
|
||||
this.lineHeight = this.workspacePage.rightSidebar.getByRole("textbox", {
|
||||
name: "Line Height",
|
||||
});
|
||||
this.letterSpacing = this.workspacePage.rightSidebar.getByRole(
|
||||
"textbox",
|
||||
{
|
||||
name: "Letter Spacing",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
get page() {
|
||||
return this.workspacePage.page;
|
||||
}
|
||||
|
||||
async waitForStyle(locator, styleName) {
|
||||
return locator.evaluate(
|
||||
(element, styleName) => element.style.getPropertyValue(styleName),
|
||||
styleName,
|
||||
);
|
||||
}
|
||||
|
||||
async waitForEditor() {
|
||||
return this.page.waitForSelector('[data-itype="editor"]');
|
||||
}
|
||||
|
||||
async waitForRoot() {
|
||||
return this.page.waitForSelector('[data-itype="root"]');
|
||||
}
|
||||
|
||||
async waitForParagraph(nth) {
|
||||
if (!nth) {
|
||||
return this.page.waitForSelector('[data-itype="paragraph"]');
|
||||
}
|
||||
return this.page.waitForSelector(
|
||||
`[data-itype="paragraph"]:nth-child(${nth})`,
|
||||
);
|
||||
}
|
||||
|
||||
async waitForParagraphStyle(nth, styleName) {
|
||||
const paragraph = await this.waitForParagraph(nth);
|
||||
return this.waitForStyle(paragraph, styleName);
|
||||
}
|
||||
|
||||
async waitForTextSpan(nth = 0) {
|
||||
if (!nth) {
|
||||
return this.page.waitForSelector('[data-itype="inline"]');
|
||||
}
|
||||
return this.page.waitForSelector(
|
||||
`[data-itype="inline"]:nth-child(${nth})`,
|
||||
);
|
||||
}
|
||||
|
||||
async waitForTextSpanContent(nth = 0) {
|
||||
const textSpan = await this.waitForTextSpan(nth);
|
||||
const textContent = await textSpan.textContent();
|
||||
return textContent;
|
||||
}
|
||||
|
||||
async waitForTextSpanStyle(nth, styleName) {
|
||||
const textSpan = await this.waitForTextSpan(nth);
|
||||
return this.waitForStyle(textSpan, styleName);
|
||||
}
|
||||
|
||||
async startEditing() {
|
||||
await this.page.keyboard.press("Enter");
|
||||
return this.waitForEditor();
|
||||
}
|
||||
|
||||
stopEditing() {
|
||||
return this.page.keyboard.press("Escape");
|
||||
}
|
||||
|
||||
async moveToLeft(amount = 0) {
|
||||
for (let i = 0; i < amount; i++) {
|
||||
await this.page.keyboard.press("ArrowLeft");
|
||||
}
|
||||
}
|
||||
|
||||
async moveToRight(amount = 0) {
|
||||
for (let i = 0; i < amount; i++) {
|
||||
await this.page.keyboard.press("ArrowRight");
|
||||
}
|
||||
}
|
||||
|
||||
async moveFromStart(offset = 0) {
|
||||
await this.page.keyboard.press("ArrowLeft");
|
||||
await this.moveToRight(offset);
|
||||
}
|
||||
|
||||
async moveFromEnd(offset = 0) {
|
||||
await this.page.keyboard.press("ArrowRight");
|
||||
await this.moveToLeft(offset);
|
||||
}
|
||||
|
||||
async selectFromStart(length, offset = 0) {
|
||||
await this.moveFromStart(offset);
|
||||
await this.page.keyboard.down("Shift");
|
||||
await this.moveToRight(length);
|
||||
await this.page.keyboard.up("Shift");
|
||||
}
|
||||
|
||||
async selectFromEnd(length, offset = 0) {
|
||||
await this.moveFromEnd(offset);
|
||||
await this.page.keyboard.down("Shift");
|
||||
await this.moveToLeft(length);
|
||||
await this.page.keyboard.up("Shift");
|
||||
}
|
||||
|
||||
async changeNumericInput(locator, newValue) {
|
||||
await expect(locator).toBeVisible();
|
||||
await locator.focus();
|
||||
await locator.fill(`${newValue}`);
|
||||
await locator.blur();
|
||||
}
|
||||
|
||||
changeFontSize(newValue) {
|
||||
return this.changeNumericInput(this.fontSize, newValue);
|
||||
}
|
||||
|
||||
changeLineHeight(newValue) {
|
||||
return this.changeNumericInput(this.lineHeight, newValue);
|
||||
}
|
||||
|
||||
changeLetterSpacing(newValue) {
|
||||
return this.changeNumericInput(this.letterSpacing, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be called on `test.beforeEach`.
|
||||
*
|
||||
@@ -11,50 +150,21 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
static async init(page) {
|
||||
await BaseWebSocketPage.initWebSockets(page);
|
||||
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"logged-in-user/get-profile-logged-in.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-team-users?file-id=*",
|
||||
"logged-in-user/get-team-users-single-user.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-comment-threads?file-id=*",
|
||||
"workspace/get-comment-threads-empty.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-project?id=*",
|
||||
"workspace/get-project-default.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-team?id=*",
|
||||
"workspace/get-team-default.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams.json");
|
||||
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-team-members?team-id=*",
|
||||
"logged-in-user/get-team-members-your-penpot.json",
|
||||
);
|
||||
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-profiles-for-file-comments?file-id=*",
|
||||
"workspace/get-profile-for-file-comments.json",
|
||||
);
|
||||
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"update-profile-props",
|
||||
"workspace/update-profile-empty.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPCs(page, {
|
||||
"get-profile": "logged-in-user/get-profile-logged-in.json",
|
||||
"get-team-users?file-id=*":
|
||||
"logged-in-user/get-team-users-single-user.json",
|
||||
"get-comment-threads?file-id=*":
|
||||
"workspace/get-comment-threads-empty.json",
|
||||
"get-project?id=*": "workspace/get-project-default.json",
|
||||
"get-team?id=*": "workspace/get-team-default.json",
|
||||
"get-teams": "get-teams.json",
|
||||
"get-team-members?team-id=*":
|
||||
"logged-in-user/get-team-members-your-penpot.json",
|
||||
"get-profiles-for-file-comments?file-id=*":
|
||||
"workspace/get-profile-for-file-comments.json",
|
||||
"update-profile-props": "workspace/update-profile-empty.json",
|
||||
});
|
||||
}
|
||||
|
||||
static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a";
|
||||
@@ -62,9 +172,20 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
|
||||
static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
|
||||
|
||||
/**
|
||||
* WebSocket mock
|
||||
*
|
||||
* @type {MockWebSocketHelper}
|
||||
*/
|
||||
#ws = null;
|
||||
|
||||
constructor(page) {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {Page} page
|
||||
* @param {} [options]
|
||||
*/
|
||||
constructor(page, options) {
|
||||
super(page);
|
||||
this.pageName = page.getByTestId("page-name");
|
||||
|
||||
@@ -112,11 +233,14 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
"tokens-context-menu-for-set",
|
||||
);
|
||||
this.contextMenuForShape = page.getByTestId("context-menu");
|
||||
if (options?.textEditor) {
|
||||
this.textEditor = new WorkspacePage.TextEditor(this);
|
||||
}
|
||||
}
|
||||
|
||||
async goToWorkspace({
|
||||
fileId = WorkspacePage.anyFileId,
|
||||
pageId = WorkspacePage.anyPageId,
|
||||
fileId = this.fileId ?? WorkspacePage.anyFileId,
|
||||
pageId = this.pageId ?? WorkspacePage.anyPageId,
|
||||
} = {}) {
|
||||
await this.page.goto(
|
||||
`/#/workspace?team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`,
|
||||
@@ -141,48 +265,59 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
}
|
||||
|
||||
async setupEmptyFile() {
|
||||
await this.mockRPC(
|
||||
"get-profile",
|
||||
"logged-in-user/get-profile-logged-in.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-team-users?file-id=*",
|
||||
"logged-in-user/get-team-users-single-user.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-comment-threads?file-id=*",
|
||||
"workspace/get-comment-threads-empty.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-project?id=*",
|
||||
"workspace/get-project-default.json",
|
||||
);
|
||||
await this.mockRPC("get-team?id=*", "workspace/get-team-default.json");
|
||||
await this.mockRPC(
|
||||
"get-profiles-for-file-comments?file-id=*",
|
||||
"workspace/get-profile-for-file-comments.json",
|
||||
);
|
||||
await this.mockRPC(/get\-file\?/, "workspace/get-file-blank.json");
|
||||
await this.mockRPC(
|
||||
"get-file-object-thumbnails?file-id=*",
|
||||
"workspace/get-file-object-thumbnails-blank.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-font-variants?team-id=*",
|
||||
"workspace/get-font-variants-empty.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-file-fragment?file-id=*",
|
||||
"workspace/get-file-fragment-blank.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-file-libraries?file-id=*",
|
||||
"workspace/get-file-libraries-empty.json",
|
||||
);
|
||||
await this.mockRPCs({
|
||||
"get-profile": "logged-in-user/get-profile-logged-in.json",
|
||||
"get-team-users?file-id=*":
|
||||
"logged-in-user/get-team-users-single-user.json ",
|
||||
"get-comment-threads?file-id=*":
|
||||
"workspace/get-comment-threads-empty.json",
|
||||
"get-project?id=*": "workspace/get-project-default.json",
|
||||
"get-team?id=*": "workspace/get-team-default.json",
|
||||
"get-profiles-for-file-comments?file-id=*":
|
||||
"workspace/get-profile-for-file-comments.json",
|
||||
"get-file-object-thumbnails?file-id=*":
|
||||
"workspace/get-file-object-thumbnails-blank.json",
|
||||
"get-font-variants?team-id=*": "workspace/get-font-variants-empty.json",
|
||||
"get-file-fragment?file-id=*": "workspace/get-file-fragment-blank.json",
|
||||
"get-file-libraries?file-id=*": "workspace/get-file-libraries-empty.json",
|
||||
});
|
||||
|
||||
if (this.textEditor) {
|
||||
await this.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
}
|
||||
|
||||
// by default we mock the blank file.
|
||||
await this.mockGetFile("workspace/get-file-blank.json");
|
||||
}
|
||||
|
||||
async mockGetFile(jsonFile) {
|
||||
await this.mockRPC(/get\-file\?/, jsonFile);
|
||||
async mockGetFile(jsonFilename, options) {
|
||||
const page = this.page;
|
||||
const jsonPath = `playwright/data/${jsonFilename}`;
|
||||
const body = await readFile(jsonPath, "utf-8");
|
||||
const payload = JSON.parse(body);
|
||||
|
||||
const fileId = Transit.get(payload, "id");
|
||||
const pageId = Transit.get(payload, "data", "pages", 0);
|
||||
const teamId = Transit.get(payload, "team-id");
|
||||
|
||||
this.fileId = fileId ?? this.anyFileId;
|
||||
this.pageId = pageId ?? this.anyPageId;
|
||||
this.teamId = teamId ?? this.anyTeamId;
|
||||
|
||||
const path = /get\-file\?/;
|
||||
const url = typeof path === "string" ? `**/api/main/methods/${path}` : path;
|
||||
const interceptConfig = {
|
||||
status: 200,
|
||||
contentType: "application/transit+json",
|
||||
...options,
|
||||
};
|
||||
return page.route(url, (route) =>
|
||||
route.fulfill({
|
||||
...interceptConfig,
|
||||
body,
|
||||
}),
|
||||
);
|
||||
// await this.mockRPC(/get\-file\?/, jsonFile);
|
||||
}
|
||||
|
||||
async mockGetAsset(regex, asset) {
|
||||
@@ -190,22 +325,15 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
}
|
||||
|
||||
async setupFileWithComments() {
|
||||
await this.mockRPC(
|
||||
"get-comment-threads?file-id=*",
|
||||
"workspace/get-comment-threads-unread.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-file-fragment?file-id=*&fragment-id=*",
|
||||
"viewer/get-file-fragment-single-board.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-comments?thread-id=*",
|
||||
"workspace/get-thread-comments.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"update-comment-thread-status",
|
||||
"workspace/update-comment-thread-status.json",
|
||||
);
|
||||
await this.mockRPCs({
|
||||
"get-comment-threads?file-id=*":
|
||||
"workspace/get-comment-threads-unread.json",
|
||||
"get-file-fragment?file-id=*&fragment-id=*":
|
||||
"viewer/get-file-fragment-single-board.json",
|
||||
"get-comments?thread-id=*": "workspace/get-thread-comments.json",
|
||||
"update-comment-thread-status":
|
||||
"workspace/update-comment-thread-status.json",
|
||||
});
|
||||
}
|
||||
|
||||
async clickWithDragViewportAt(x, y, width, height) {
|
||||
@@ -223,6 +351,67 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.page.mouse.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks and moves from the coordinates x1,y1 to x2,y2
|
||||
*
|
||||
* @param {number} x1
|
||||
* @param {number} y1
|
||||
* @param {number} x2
|
||||
* @param {number} y2
|
||||
*/
|
||||
async clickAndMove(x1, y1, x2, y2) {
|
||||
await this.page.waitForTimeout(100);
|
||||
await this.viewport.hover({ position: { x: x1, y: y1 } });
|
||||
await this.page.mouse.down();
|
||||
await this.viewport.hover({ position: { x: x2, y: y2 } });
|
||||
await this.page.mouse.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Text Shape in the specified coordinates
|
||||
* with an initial text.
|
||||
*
|
||||
* @param {number} x1
|
||||
* @param {number} y1
|
||||
* @param {number} x2
|
||||
* @param {number} y2
|
||||
* @param {string} initialText
|
||||
* @param {*} [options]
|
||||
*/
|
||||
async createTextShape(x1, y1, x2, y2, initialText, options) {
|
||||
const timeToWait = options?.timeToWait ?? 100;
|
||||
await this.page.keyboard.press("T");
|
||||
await this.page.waitForTimeout(timeToWait);
|
||||
await this.clickAndMove(x1, y1, x2, y2);
|
||||
await this.page.waitForTimeout(timeToWait);
|
||||
if (initialText) {
|
||||
await this.page.keyboard.type(initialText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the selected element into the clipboard.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async copy() {
|
||||
return this.page.keyboard.press("Control+C");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pastes something from the clipboard.
|
||||
*
|
||||
* @param {"keyboard"|"context-menu"} [kind="keyboard"]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async paste(kind = "keyboard") {
|
||||
if (kind === "context-menu") {
|
||||
await this.viewport.click({ button: "right" });
|
||||
return this.page.getByText("PasteCtrlV").click();
|
||||
}
|
||||
return this.page.keyboard.press("Control+V");
|
||||
}
|
||||
|
||||
async panOnViewportAt(x, y, width, height) {
|
||||
await this.page.waitForTimeout(100);
|
||||
await this.viewport.hover({ position: { x, y } });
|
||||
@@ -250,10 +439,15 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async doubleClickLeafLayer(name, clickOptions = {}) {
|
||||
await this.clickLeafLayer(name, clickOptions);
|
||||
await this.clickLeafLayer(name, clickOptions);
|
||||
}
|
||||
|
||||
async clickToggableLayer(name, clickOptions = {}) {
|
||||
const layer = this.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: name });
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: name });
|
||||
const button = layer.getByRole("button");
|
||||
|
||||
await button.waitFor();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
@@ -360,7 +360,7 @@ test("Renders a file with texts with paragraphs and breaking lines", async ({
|
||||
id: "a5f238bd-dd8a-8164-8007-1bc3481eaf05",
|
||||
pageId: "a5f238bd-dd8a-8164-8007-1bc3481eaf06",
|
||||
});
|
||||
await workspace.waitForFirstRender();
|
||||
await workspace.waitForFirstRenderWithoutUI();
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ const setupFile = async (workspacePage) => {
|
||||
fileId: "7b2da435-6186-815a-8007-0daa95d2f26d",
|
||||
pageId: "ce79274b-11ab-8088-8007-0487ad43f789",
|
||||
});
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
"workspace/update-file-empty.json",
|
||||
);
|
||||
};
|
||||
|
||||
const shapeToLayerName = {
|
||||
|
||||
@@ -1,12 +1,317 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { Clipboard } from '../../helpers/Clipboard';
|
||||
import { WorkspacePage } from "../pages/WorkspacePage";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const timeToWait = 100;
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
await Clipboard.enable(context, Clipboard.Permission.ONLY_WRITE);
|
||||
|
||||
await WorkspacePage.init(page);
|
||||
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
|
||||
});
|
||||
|
||||
test.skip("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
||||
test.afterEach(async ({ context}) => {
|
||||
context.clearPermissions();
|
||||
})
|
||||
|
||||
test("Create a new text shape", async ({ page }) => {
|
||||
const initialText = "Lorem ipsum";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.createTextShape(190, 150, 300, 200, initialText);
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe(initialText);
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Create a new text shape from pasting text", async ({ page, context }) => {
|
||||
const textToPaste = "Lorem ipsum";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockRPC(
|
||||
"update-file?id=*",
|
||||
"text-editor/update-file.json",
|
||||
);
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.clickAt(190, 150);
|
||||
await workspace.paste("keyboard");
|
||||
|
||||
await page.waitForTimeout(timeToWait);
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe(textToPaste);
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Create a new text shape from pasting text using context menu", async ({ page, context }) => {
|
||||
const textToPaste = "Lorem ipsum";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.clickAt(190, 150);
|
||||
await workspace.paste("context-menu");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe(textToPaste);
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
})
|
||||
|
||||
test("Update an already created text shape by appending text", async ({ page }) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromEnd(0);
|
||||
await page.keyboard.type(" dolor sit amet");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem ipsum dolor sit amet");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update an already created text shape by prepending text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromStart(0);
|
||||
await page.keyboard.type("Dolor sit amet ");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update an already created text shape by inserting text in between", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromStart(5);
|
||||
await page.keyboard.type(" dolor sit amet");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem dolor sit amet ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape appending text by pasting text", async ({ page, context }) => {
|
||||
const textToPaste = " dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromEnd();
|
||||
await workspace.paste("keyboard");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem ipsum dolor sit amet");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape prepending text by pasting text", async ({
|
||||
page, context
|
||||
}) => {
|
||||
const textToPaste = "Dolor sit amet ";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromStart();
|
||||
await workspace.paste("keyboard");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape replacing (starting) text with pasted text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const textToPaste = "Dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5);
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.paste("keyboard");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Dolor sit amet ipsum");
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape replacing (ending) text with pasted text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const textToPaste = "dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromEnd(5);
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.paste("keyboard");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem dolor sit amet");
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape replacing (in between) text with pasted text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const textToPaste = "dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5, 3);
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.paste("keyboard");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lordolor sit ametsum");
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update text font size selecting a part of it (starting)", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.mockRPC(
|
||||
"update-file?id=*",
|
||||
"text-editor/update-file.json",
|
||||
);
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5);
|
||||
await workspace.textEditor.changeFontSize(36);
|
||||
|
||||
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
|
||||
expect(textContent1).toBe("Lorem");
|
||||
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
|
||||
expect(textContent2).toBe(" ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test.skip("Update text line height selecting a part of it (starting)", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5);
|
||||
await workspace.textEditor.changeLineHeight(1.4);
|
||||
|
||||
const lineHeight = await workspace.textEditor.waitForParagraphStyle(1, 'line-height');
|
||||
expect(lineHeight).toBe("1.4");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem ipsum");
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test.skip("Update text letter spacing selecting a part of it (starting)", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5);
|
||||
await workspace.textEditor.changeLetterSpacing(10);
|
||||
|
||||
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
|
||||
expect(textContent1).toBe("Lorem");
|
||||
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
|
||||
expect(textContent2).toBe(" ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
||||
const workspace = new WorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-11552.json");
|
||||
@@ -14,21 +319,16 @@ test.skip("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
||||
"update-file?id=*",
|
||||
"text-editor/update-file-11552.json",
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
fileId: "238a17e0-75ff-8075-8006-934586ea2230",
|
||||
pageId: "238a17e0-75ff-8075-8006-934586ea2231",
|
||||
});
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.doubleClickLeafLayer("Lorem ipsum");
|
||||
|
||||
const fontSizeInput = workspace.rightSidebar.getByRole("textbox", {
|
||||
name: "Font Size",
|
||||
});
|
||||
await expect(fontSizeInput).toBeVisible();
|
||||
|
||||
await workspace.page.keyboard.press("Enter");
|
||||
await workspace.page.keyboard.press("ArrowRight");
|
||||
await page.keyboard.press("Enter");
|
||||
await page.keyboard.press("ArrowRight");
|
||||
|
||||
await fontSizeInput.fill("36");
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
BIN
frontend/resources/images/features/2.12-export-pdf.gif
Normal file
BIN
frontend/resources/images/features/2.12-export-pdf.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 821 KiB |
BIN
frontend/resources/images/features/2.12-slide-0.jpg
Normal file
BIN
frontend/resources/images/features/2.12-slide-0.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
frontend/resources/images/features/2.12-tokens-sidebar.gif
Normal file
BIN
frontend/resources/images/features/2.12-tokens-sidebar.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 676 KiB |
BIN
frontend/resources/images/features/2.12-variants.gif
Normal file
BIN
frontend/resources/images/features/2.12-variants.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -746,20 +746,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
@include flexCenter;
|
||||
height: $s-48;
|
||||
width: $s-48;
|
||||
border-radius: $br-circle;
|
||||
background-color: var(--empty-message-background-color);
|
||||
svg {
|
||||
@extend .button-icon;
|
||||
height: $s-28;
|
||||
width: $s-28;
|
||||
stroke: var(--empty-message-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.attr-title {
|
||||
div {
|
||||
margin-left: 0;
|
||||
|
||||
@@ -17,23 +17,25 @@
|
||||
<meta name="twitter:site" content="@penpotapp">
|
||||
<meta name="twitter:creator" content="@penpotapp">
|
||||
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
|
||||
<link id="theme" href="css/main.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
<link id="theme" href="css/main.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
{{#isDebug}}
|
||||
<link href="css/debug.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
<link href="css/debug.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
{{/isDebug}}
|
||||
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
{{# manifest}}
|
||||
<script>window.penpotWorkerURI="{{& worker_main}}"</script>
|
||||
<script src="{{& config}}"></script>
|
||||
<script src="{{& polyfills}}"></script>
|
||||
{{/manifest}}
|
||||
|
||||
<script type="module">
|
||||
globalThis.penpotTranslations = JSON.parse({{& translations}});
|
||||
globalThis.penpotVersion = "%version%";
|
||||
globalThis.penpotBuildDate = "%buildDate%";
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||
</script>
|
||||
|
||||
{{# manifest}}
|
||||
<script src="{{& config}}"></script>
|
||||
<script src="{{& polyfills}}"></script>
|
||||
<script type="importmap">{{& importmap }}</script>
|
||||
{{/manifest}}
|
||||
|
||||
<!--cookie-consent-->
|
||||
</head>
|
||||
<body>
|
||||
@@ -46,7 +48,7 @@
|
||||
{{# manifest}}
|
||||
<script type="module" src="{{& libs}}"></script>
|
||||
<script type="module">
|
||||
import { init } from "{{& main}}";
|
||||
import { init } from "{{& app_main}}";
|
||||
init();
|
||||
</script>
|
||||
{{/manifest}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<link href="./css/ds.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
<link href="./css/ds.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
@@ -9,7 +9,3 @@
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
window.penpotTranslations = JSON.parse({{& translations}});
|
||||
</script>
|
||||
|
||||
@@ -6,22 +6,22 @@
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
|
||||
<script>
|
||||
window.penpotVersion = "%version%";
|
||||
window.penpotBuildDate = "%buildDate%";
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||
</script>
|
||||
|
||||
{{# manifest}}
|
||||
<script>window.penpotWorkerURI="{{& worker_main}}"</script>
|
||||
<script src="{{& config}}"></script>
|
||||
<script src="{{& polyfills}}"></script>
|
||||
<script type="importmap">{{& importmap }}</script>
|
||||
{{/manifest}}
|
||||
|
||||
</head>
|
||||
<body>
|
||||
{{# manifest}}
|
||||
<script type="module" src="{{& libs}}"></script>
|
||||
<script type="module">
|
||||
import { init } from "{{& rasterizer}}";
|
||||
import { init } from "{{& rasterizer_main}}";
|
||||
init();
|
||||
</script>
|
||||
{{/manifest}}
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
|
||||
<script>
|
||||
window.penpotVersion = "%version%";
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
</script>
|
||||
|
||||
{{# manifest}}
|
||||
<script src="{{& config}}"></script>
|
||||
<script src="{{& polyfills}}"></script>
|
||||
<script type="importmap">{{& importmap }}</script>
|
||||
{{/manifest}}
|
||||
</head>
|
||||
<body>
|
||||
@@ -20,7 +22,7 @@
|
||||
{{# manifest}}
|
||||
<script type="module" src="{{& libs}}"></script>
|
||||
<script type="module">
|
||||
import { init } from "{{& render}}";
|
||||
import { init } from "{{& render_main}}";
|
||||
init();
|
||||
</script>
|
||||
{{/manifest}}
|
||||
|
||||
@@ -28,6 +28,8 @@ export function startWorker() {
|
||||
}
|
||||
|
||||
export const isDebug = process.env.NODE_ENV !== "production";
|
||||
export const CURRENT_VERSION = process.env.CURRENT_VERSION || "develop";
|
||||
export const BUILD_DATE = process.env.BUILD_DATE || "" + new Date();
|
||||
|
||||
async function findFiles(basePath, predicate, options = {}) {
|
||||
predicate =
|
||||
@@ -47,8 +49,7 @@ async function findFiles(basePath, predicate, options = {}) {
|
||||
function syncDirs(originPath, destPath) {
|
||||
const command = `rsync -ar --delete ${originPath} ${destPath}`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
proc.exec(command, (cause, stdout) => {
|
||||
return new Promise((resolve, reject) => {proc.exec(command, (cause, stdout) => {
|
||||
if (cause) {
|
||||
reject(cause);
|
||||
} else {
|
||||
@@ -180,44 +181,41 @@ export async function watch(baseDir, predicate, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
async function readManifestFile() {
|
||||
const manifestPath = "resources/public/js/manifest.json";
|
||||
async function readManifestFile(resource) {
|
||||
const manifestPath = "resources/public/" + resource;
|
||||
let content = await fs.readFile(manifestPath, { encoding: "utf8" });
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
async function readShadowManifest() {
|
||||
const ts = Date.now();
|
||||
try {
|
||||
const content = await readManifestFile();
|
||||
const index = {
|
||||
app_main: "./js/main.js",
|
||||
render_main: "./js/render.js",
|
||||
rasterizer_main: "./js/rasterizer.js",
|
||||
|
||||
const index = {
|
||||
ts: ts,
|
||||
config: "./js/config.js",
|
||||
polyfills: "./js/polyfills.js",
|
||||
worker_main: "./js/worker/main.js",
|
||||
libs: "./js/libs.js",
|
||||
};
|
||||
config: "./js/config.js?version=" + CURRENT_VERSION,
|
||||
polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION,
|
||||
libs: "./js/libs.js?version=" + CURRENT_VERSION,
|
||||
worker_main: "./js/worker/main.js?version=" + CURRENT_VERSION,
|
||||
|
||||
for (let item of content) {
|
||||
index[item.name] = "./js/" + item["output-name"] + "";
|
||||
}
|
||||
importmap: JSON.stringify({
|
||||
"imports": {
|
||||
"./js/shared.js": "./js/shared.js?version=" + CURRENT_VERSION,
|
||||
"./js/main.js": "./js/main.js?version=" + CURRENT_VERSION,
|
||||
"./js/render.js": "./js/render.js?version=" + CURRENT_VERSION,
|
||||
"./js/render-wasm.js": "./js/render-wasm.js?version=" + CURRENT_VERSION,
|
||||
"./js/rasterizer.js": "./js/rasterizer.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-dashboard.js": "./js/main-dashboard.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-auth.js": "./js/main-auth.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-viewer.js": "./js/main-viewer.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-settings.js": "./js/main-settings.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-workspace.js": "./js/main-workspace.js?version=" + CURRENT_VERSION,
|
||||
"./js/util-highlight.js": "./js/util-highlight.js?version=" + CURRENT_VERSION
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
return index;
|
||||
} catch (cause) {
|
||||
const index = {
|
||||
ts: ts,
|
||||
config: "./js/config.js",
|
||||
polyfills: "./js/polyfills.js",
|
||||
main: "./js/main.js",
|
||||
shared: "./js/shared.js",
|
||||
worker_main: "./js/worker/main.js",
|
||||
rasterizer: "./js/rasterizer.js",
|
||||
libs: "./js/libs.js",
|
||||
};
|
||||
|
||||
return index;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
async function renderTemplate(path, context = {}, partials = {}) {
|
||||
@@ -257,7 +255,7 @@ const markedOptions = {
|
||||
|
||||
marked.use(markedOptions);
|
||||
|
||||
async function readTranslations() {
|
||||
export async function compileTranslations() {
|
||||
const langs = [
|
||||
"ar",
|
||||
"ca",
|
||||
@@ -294,9 +292,10 @@ async function readTranslations() {
|
||||
["uk", "ukr_UA"],
|
||||
"ha",
|
||||
];
|
||||
const result = {};
|
||||
|
||||
for (let lang of langs) {
|
||||
const result = {};
|
||||
|
||||
let filename = `${lang}.po`;
|
||||
if (l.isArray(lang)) {
|
||||
filename = `${lang[1]}.po`;
|
||||
@@ -315,11 +314,6 @@ async function readTranslations() {
|
||||
for (let key of Object.keys(trdata)) {
|
||||
if (key === "") continue;
|
||||
const comments = trdata[key].comments || {};
|
||||
|
||||
if (l.isNil(result[key])) {
|
||||
result[key] = {};
|
||||
}
|
||||
|
||||
const isMarkdown = l.includes(comments.flag, "markdown");
|
||||
|
||||
const msgs = trdata[key].msgstr;
|
||||
@@ -329,9 +323,9 @@ async function readTranslations() {
|
||||
message = marked.parseInline(message);
|
||||
}
|
||||
|
||||
result[key][lang] = message;
|
||||
result[key] = message;
|
||||
} else {
|
||||
result[key][lang] = msgs.map((item) => {
|
||||
result[key] = msgs.map((item) => {
|
||||
if (isMarkdown) {
|
||||
return marked.parseInline(item);
|
||||
} else {
|
||||
@@ -340,22 +334,12 @@ async function readTranslations() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const esm = `export default ${JSON.stringify(result, null, 0)};\n`;
|
||||
const outputDir = "resources/public/js/";
|
||||
const outputFile = ph.join(outputDir, "translation." + lang + ".js");
|
||||
await fs.writeFile(outputFile, esm);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function filterTranslations(translations, langs = [], keyFilter) {
|
||||
const filteredEntries = Object.entries(translations)
|
||||
.filter(([translationKey, _]) => keyFilter(translationKey))
|
||||
.map(([translationKey, value]) => {
|
||||
const langEntries = Object.entries(value).filter(([lang, _]) =>
|
||||
langs.includes(lang),
|
||||
);
|
||||
return [translationKey, Object.fromEntries(langEntries)];
|
||||
});
|
||||
|
||||
return Object.fromEntries(filteredEntries);
|
||||
}
|
||||
|
||||
async function generateSvgSprite(files, prefix) {
|
||||
@@ -407,14 +391,6 @@ async function generateTemplates() {
|
||||
const isDebug = process.env.NODE_ENV !== "production";
|
||||
await fs.mkdir("./resources/public/", { recursive: true });
|
||||
|
||||
let translations = await readTranslations();
|
||||
const storybookTranslations = JSON.stringify(
|
||||
filterTranslations(translations, ["en"], (key) =>
|
||||
key.startsWith("labels."),
|
||||
),
|
||||
);
|
||||
translations = JSON.stringify(translations);
|
||||
|
||||
const manifest = await readShadowManifest();
|
||||
let content;
|
||||
|
||||
@@ -436,13 +412,16 @@ async function generateTemplates() {
|
||||
"../public/images/sprites/assets.svg": assetsSprite,
|
||||
};
|
||||
|
||||
const context = {
|
||||
manifest: manifest,
|
||||
version: CURRENT_VERSION,
|
||||
build_date: BUILD_DATE,
|
||||
isDebug,
|
||||
};
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/index.mustache",
|
||||
{
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(translations),
|
||||
isDebug,
|
||||
},
|
||||
context,
|
||||
partials,
|
||||
);
|
||||
|
||||
@@ -450,41 +429,30 @@ async function generateTemplates() {
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/challenge.mustache",
|
||||
{},
|
||||
context,
|
||||
partials,
|
||||
);
|
||||
await fs.writeFile("./resources/public/challenge.html", content);
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/preview-body.mustache",
|
||||
{
|
||||
manifest: manifest,
|
||||
},
|
||||
context,
|
||||
partials,
|
||||
);
|
||||
await fs.writeFile("./.storybook/preview-body.html", content);
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/preview-head.mustache",
|
||||
{
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(storybookTranslations),
|
||||
},
|
||||
context,
|
||||
partials,
|
||||
);
|
||||
await fs.writeFile("./.storybook/preview-head.html", content);
|
||||
|
||||
content = await renderTemplate("resources/templates/render.mustache", {
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(translations),
|
||||
});
|
||||
content = await renderTemplate("resources/templates/render.mustache", context);
|
||||
|
||||
await fs.writeFile("./resources/public/render.html", content);
|
||||
|
||||
content = await renderTemplate("resources/templates/rasterizer.mustache", {
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(translations),
|
||||
});
|
||||
content = await renderTemplate("resources/templates/rasterizer.mustache", context);
|
||||
|
||||
await fs.writeFile("./resources/public/rasterizer.html", content);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ rm -rf target/dist;
|
||||
mkdir -p resources/public;
|
||||
mkdir -p target/dist;
|
||||
|
||||
yarn run build:app:main $EXTRA_PARAMS || exit 1
|
||||
yarn run build:app:main $EXTRA_PARAMS;
|
||||
|
||||
if [ "$INCLUDE_WASM" = "yes" ]; then
|
||||
yarn run build:wasm || exit 1;
|
||||
@@ -38,16 +38,6 @@ fi
|
||||
yarn run build:app:libs || exit 1;
|
||||
yarn run build:app:assets || exit 1;
|
||||
|
||||
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./resources/public/index.html;
|
||||
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./resources/public/render.html;
|
||||
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./resources/public/rasterizer.html;
|
||||
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./resources/public/index.html;
|
||||
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./resources/public/rasterizer.html;
|
||||
|
||||
if [ "$INCLUDE_WASM" = "yes" ]; then
|
||||
sed -i "s/version=develop/version=$CURRENT_VERSION/g" ./resources/public/js/render_wasm.js;
|
||||
fi
|
||||
|
||||
rsync -avr resources/public/ target/dist/;
|
||||
|
||||
if [ "$INCLUDE_STORYBOOK" = "yes" ]; then
|
||||
|
||||
@@ -4,5 +4,6 @@ await h.compileStyles();
|
||||
await h.copyAssets();
|
||||
await h.copyWasmPlayground();
|
||||
await h.compileSvgSprites();
|
||||
await h.compileTranslations();
|
||||
await h.compileTemplates();
|
||||
await h.compilePolyfills();
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import * as h from "./_helpers.js";
|
||||
|
||||
await fs.mkdir("resources/public/js", {recursive: true});
|
||||
|
||||
await h.compileStorybookStyles();
|
||||
await h.copyAssets();
|
||||
await h.compileSvgSprites();
|
||||
await h.compileTranslations();
|
||||
await h.compileTemplates();
|
||||
await h.compilePolyfills();
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export OPTIONS="-A:dev -J-XX:-OmitStackTraceInFastThrow";
|
||||
export JAVA_OPTS="\
|
||||
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-Djdk.attach.allowAttachSelf \
|
||||
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
|
||||
-Djdk.tracePinnedThreads=full \
|
||||
-XX:+EnableDynamicAgentLoading \
|
||||
-XX:-OmitStackTraceInFastThrow \
|
||||
-XX:+UnlockDiagnosticVMOptions \
|
||||
-XX:+DebugNonSafepoints \
|
||||
--sun-misc-unsafe-memory-access=allow \
|
||||
--enable-preview \
|
||||
--enable-native-access=ALL-UNNAMED";
|
||||
|
||||
export OPTIONS="-A:dev"
|
||||
|
||||
set -ex
|
||||
exec clojure $OPTIONS -M -m rebel-readline.main
|
||||
exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main
|
||||
|
||||
@@ -52,6 +52,7 @@ await fs.mkdir("./resources/public/css/", { recursive: true });
|
||||
await compileSassAll();
|
||||
await h.copyAssets();
|
||||
await h.copyWasmPlayground();
|
||||
await h.compileTranslations();
|
||||
await h.compileSvgSprites();
|
||||
await h.compileTemplates();
|
||||
await h.compilePolyfills();
|
||||
@@ -81,7 +82,7 @@ h.watch("resources/templates", null, async function (path) {
|
||||
log.info("watch: translations (~)");
|
||||
h.watch("translations", null, async function (path) {
|
||||
log.info("changed:", path);
|
||||
await h.compileTemplates();
|
||||
await h.compileTranslations();
|
||||
});
|
||||
|
||||
log.info("watch: assets (~)");
|
||||
|
||||
@@ -23,28 +23,28 @@
|
||||
|
||||
:util-highlight
|
||||
{:entries [app.util.code-highlight]
|
||||
:depends-on #{:main}}
|
||||
:depends-on #{:shared}}
|
||||
|
||||
:main-auth
|
||||
{:entries [app.main.ui.auth
|
||||
app.main.ui.auth.verify-token]
|
||||
:depends-on #{:main}}
|
||||
:depends-on #{:shared}}
|
||||
|
||||
:main-viewer
|
||||
{:entries [app.main.ui.viewer]
|
||||
:depends-on #{:main :main-auth}}
|
||||
:depends-on #{:shared :main-auth}}
|
||||
|
||||
:main-workspace
|
||||
{:entries [app.main.ui.workspace]
|
||||
:depends-on #{:main}}
|
||||
:depends-on #{:shared}}
|
||||
|
||||
:main-dashboard
|
||||
{:entries [app.main.ui.dashboard]
|
||||
:depends-on #{:main}}
|
||||
:depends-on #{:shared}}
|
||||
|
||||
:main-settings
|
||||
{:entries [app.main.ui.settings]
|
||||
:depends-on #{:main}}
|
||||
:depends-on #{:shared}}
|
||||
|
||||
:render
|
||||
{:entries [app.render]
|
||||
@@ -81,7 +81,7 @@
|
||||
:source-map-detail-level :all}}}
|
||||
|
||||
:worker
|
||||
{:target :esm
|
||||
{:target :browser
|
||||
:output-dir "resources/public/js/worker/"
|
||||
:asset-path "/js/worker"
|
||||
:devtools {:browser-inject :main
|
||||
@@ -92,6 +92,7 @@
|
||||
{:main
|
||||
{:entries [app.worker]
|
||||
:web-worker true
|
||||
:prepend-js "importScripts('/js/worker/render.js');"
|
||||
:depends-on #{}}}
|
||||
|
||||
:js-options
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
(def default-theme "default")
|
||||
(def default-language "en")
|
||||
|
||||
(def translations (obj/get global "penpotTranslations"))
|
||||
(def themes (obj/get global "penpotThemes"))
|
||||
|
||||
(def build-date (parse-build-date global))
|
||||
@@ -127,7 +126,7 @@
|
||||
public-uri))
|
||||
|
||||
(def worker-uri
|
||||
(obj/get global "penpotWorkerURI" "/js/worker.js"))
|
||||
(obj/get global "penpotWorkerURI" "/js/worker/main.js"))
|
||||
|
||||
(defn external-feature-flag
|
||||
[flag value]
|
||||
@@ -189,7 +188,11 @@
|
||||
(true? thumbnail?) (u/join (dm/str id "/thumbnail"))
|
||||
(false? thumbnail?) (u/join (dm/str id)))))))
|
||||
|
||||
(defn resolve-static-asset
|
||||
[path]
|
||||
(let [uri (u/join public-uri path)]
|
||||
(assoc uri :query (dm/str "version=" (:full version)))))
|
||||
(defn resolve-href
|
||||
[resource]
|
||||
(let [version (get version :full)
|
||||
href (-> public-uri
|
||||
(u/ensure-path-slash)
|
||||
(u/join resource)
|
||||
(get :path))]
|
||||
(str href "?version=" version)))
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
(defn ^:export init
|
||||
[]
|
||||
(mw/init!)
|
||||
(i18n/init! cf/translations)
|
||||
(i18n/init)
|
||||
(cur/init-styles)
|
||||
(thr/init!)
|
||||
(init-ui)
|
||||
@@ -114,11 +114,4 @@
|
||||
[]
|
||||
(reinit))
|
||||
|
||||
;; Reload the UI when the language changes
|
||||
(add-watch
|
||||
i18n/locale "locale"
|
||||
(fn [_ _ old-value current-value]
|
||||
(when (not= old-value current-value)
|
||||
(reinit))))
|
||||
|
||||
(set! (.-stackTraceLimit js/Error) 50)
|
||||
|
||||
@@ -148,17 +148,17 @@
|
||||
:width 768
|
||||
:height 1024}
|
||||
{:name "Google Pixel 7 Pro"
|
||||
:width 1440
|
||||
:height 3120}
|
||||
:width 412
|
||||
:height 892}
|
||||
{:name "Google Pixel 6a/6"
|
||||
:width 1080
|
||||
:height 2400}
|
||||
:width 412
|
||||
:height 915}
|
||||
{:name "Google Pixel 4a/5"
|
||||
:width 393
|
||||
:height 851}
|
||||
{:name "Samsung Galaxy S22"
|
||||
:width 1080
|
||||
:height 2340}
|
||||
:width 360
|
||||
:height 780}
|
||||
{:name "Samsung Galaxy S20+"
|
||||
:width 384
|
||||
:height 854}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
(map :page-id))
|
||||
|
||||
(defn- apply-changes-localy
|
||||
[{:keys [file-id redo-changes] :as commit} pending]
|
||||
[{:keys [file-id redo-changes ignore-wasm?] :as commit} pending]
|
||||
(ptk/reify ::apply-changes-localy
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -103,7 +103,7 @@
|
||||
pids (into #{} xf:map-page-id redo-changes)]
|
||||
(reduce #(ctst/update-object-indices %1 %2) fdata pids)))]
|
||||
|
||||
(if (features/active-feature? state "render-wasm/v1")
|
||||
(if (and (not ignore-wasm?) (features/active-feature? state "render-wasm/v1"))
|
||||
;; Update the wasm model
|
||||
(let [shape-changes (volatile! {})
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
(defn commit
|
||||
"Create a commit event instance"
|
||||
[{:keys [commit-id redo-changes undo-changes origin save-undo? features
|
||||
file-id file-revn file-vern undo-group tags stack-undo? source]}]
|
||||
file-id file-revn file-vern undo-group tags stack-undo? source ignore-wasm?]}]
|
||||
|
||||
(assert (cpc/check-changes redo-changes)
|
||||
"expect valid vector of changes for redo-changes")
|
||||
@@ -147,7 +147,8 @@
|
||||
:save-undo? save-undo?
|
||||
:undo-group undo-group
|
||||
:tags tags
|
||||
:stack-undo? stack-undo?}]
|
||||
:stack-undo? stack-undo?
|
||||
:ignore-wasm? ignore-wasm?}]
|
||||
|
||||
(ptk/reify ::commit
|
||||
cljs.core/IDeref
|
||||
|
||||
@@ -67,8 +67,8 @@
|
||||
[]
|
||||
(let [uagent (new ua/UAParser)]
|
||||
(merge
|
||||
{:app-version (:full cf/version)
|
||||
:locale @i18n/locale}
|
||||
{:version (:full cf/version)
|
||||
:locale i18n/*current-locale*}
|
||||
(let [browser (.getBrowser uagent)]
|
||||
{:browser (obj/get browser "name")
|
||||
:browser-version (obj/get browser "version")})
|
||||
@@ -98,7 +98,9 @@
|
||||
(def context
|
||||
(atom (d/without-nils (collect-context))))
|
||||
|
||||
(add-watch i18n/locale ::events #(swap! context assoc :locale %4))
|
||||
(add-watch i18n/state "events"
|
||||
(fn [_ _ _ v]
|
||||
(swap! context assoc :locale (get v :locale))))
|
||||
|
||||
;; --- EVENT TRANSLATION
|
||||
|
||||
|
||||
@@ -53,11 +53,16 @@
|
||||
(assoc :profile-id id)
|
||||
(assoc :profile profile)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [profile (:profile state)]
|
||||
(->> (rx/from (i18n/set-locale (:lang profile)))
|
||||
(rx/ignore))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [profile (:profile state)]
|
||||
(swap! storage/user assoc :profile profile)
|
||||
(i18n/set-locale! (:lang profile))
|
||||
(plugins.register/init)))))
|
||||
|
||||
(def profile-fetched?
|
||||
|
||||
@@ -59,9 +59,15 @@
|
||||
"Parses `value` of a color `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
||||
If the value is not parseable and/or has missing references returns a map with `:errors`."
|
||||
[value]
|
||||
(if-let [tc (tinycolor/valid-color value)]
|
||||
{:value value :unit (tinycolor/color-format tc)}
|
||||
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))
|
||||
(let [missing-references (seq (cto/find-token-value-references value))]
|
||||
(if-let [tc (tinycolor/valid-color value)]
|
||||
{:value value :unit (tinycolor/color-format tc)}
|
||||
(cond
|
||||
missing-references
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||
:references missing-references}
|
||||
:else
|
||||
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))))
|
||||
|
||||
(defn- numeric-string? [s]
|
||||
(and (string? s)
|
||||
@@ -120,7 +126,7 @@
|
||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`.
|
||||
If the `value` is parseable but is out of range returns a map with `warnings`."
|
||||
[value]
|
||||
(let [missing-references? (seq (cto/find-token-value-references value))
|
||||
(let [missing-references? (seq (seq (cto/find-token-value-references value)))
|
||||
parsed-value (cft/parse-token-value value)
|
||||
out-of-scope (not (<= 0 (:value parsed-value) 1))
|
||||
references (seq (cto/find-token-value-references value))]
|
||||
@@ -255,14 +261,19 @@
|
||||
|
||||
(defn- parse-sd-token-font-family-value
|
||||
[value]
|
||||
(let [missing-references (seq (some cto/find-token-value-references value))]
|
||||
(let [value (-> (js->clj value) (flatten))
|
||||
valid-font-family (or (string? value) (every? string? value))
|
||||
missing-references (seq (some cto/find-token-value-references value))]
|
||||
(cond
|
||||
(not valid-font-family)
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-font-family value)]}
|
||||
|
||||
missing-references
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||
:references missing-references}
|
||||
|
||||
:else
|
||||
{:value (-> (js->clj value) (flatten))})))
|
||||
{:value value})))
|
||||
|
||||
(defn parse-atomic-typography-value [token-type token-value]
|
||||
(case token-type
|
||||
@@ -368,8 +379,8 @@
|
||||
(let [add-keyed-errors (fn [shadow-result k errors]
|
||||
(update shadow-result :errors concat
|
||||
(map #(assoc % :shadow-key k :shadow-index shadow-index) errors)))
|
||||
parsers {:offsetX parse-sd-token-general-value
|
||||
:offsetY parse-sd-token-general-value
|
||||
parsers {:offset-x parse-sd-token-general-value
|
||||
:offset-y parse-sd-token-general-value
|
||||
:blur parse-sd-token-shadow-blur
|
||||
:spread parse-sd-token-shadow-spread
|
||||
:color parse-sd-token-color-value
|
||||
@@ -389,35 +400,42 @@
|
||||
(defn- parse-sd-token-shadow-value
|
||||
"Parses shadow value and validates it."
|
||||
[value]
|
||||
(cond
|
||||
;; Reference value (string)
|
||||
(string? value) {:value value}
|
||||
(let [missing-references
|
||||
(when (string? value)
|
||||
(seq (cto/find-token-value-references value)))]
|
||||
(cond
|
||||
missing-references
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||
:references missing-references}
|
||||
|
||||
(string? value)
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-shadow value)]}
|
||||
|
||||
;; Empty value
|
||||
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
|
||||
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
|
||||
|
||||
;; Invalid value
|
||||
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
|
||||
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
|
||||
|
||||
;; Array of shadows
|
||||
:else
|
||||
(let [converted (js->clj value :keywordize-keys true)
|
||||
:else
|
||||
(let [converted (js->clj value :keywordize-keys true)
|
||||
;; Parse each shadow with its index
|
||||
parsed-shadows (map-indexed
|
||||
(fn [idx shadow-map]
|
||||
(parse-single-shadow shadow-map idx))
|
||||
converted)
|
||||
parsed-shadows (map-indexed
|
||||
(fn [idx shadow-map]
|
||||
(parse-single-shadow shadow-map idx))
|
||||
converted)
|
||||
|
||||
;; Collect all errors from all shadows
|
||||
all-errors (mapcat :errors parsed-shadows)
|
||||
all-errors (mapcat :errors parsed-shadows)
|
||||
|
||||
;; Collect all values from shadows that have values
|
||||
all-values (into [] (keep :value parsed-shadows))]
|
||||
all-values (into [] (keep :value parsed-shadows))]
|
||||
|
||||
(if (seq all-errors)
|
||||
{:errors all-errors
|
||||
:value all-values}
|
||||
{:value all-values}))))
|
||||
(if (seq all-errors)
|
||||
{:errors all-errors
|
||||
:value all-values}
|
||||
{:value all-values})))))
|
||||
|
||||
(defn collect-shadow-errors [token shadow-index]
|
||||
(group-by :shadow-key
|
||||
|
||||
@@ -351,19 +351,31 @@
|
||||
(on-success))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
|
||||
(def ^:private schema:create-invitation
|
||||
[:and
|
||||
[:map
|
||||
[:emails {:optional true} [::sm/set ::sm/email]]
|
||||
[:invitations {:optional true}
|
||||
[:vector
|
||||
[:map
|
||||
[:email ::sm/email]
|
||||
[:role [::sm/one-of ctt/valid-roles]]]]]
|
||||
[:team-id ::sm/uuid]
|
||||
[:resend? {:optional true} ::sm/boolean]]
|
||||
[:fn (fn [attrs]
|
||||
(or (contains? attrs :emails)
|
||||
(contains? attrs :invitations)))]])
|
||||
|
||||
(def ^:private check-create-invitations-params
|
||||
(sm/check-fn schema:create-invitation))
|
||||
|
||||
(defn create-invitations
|
||||
"Unified function to create invitations. Supports two parameter formats:
|
||||
1. {:emails #{...} :role :admin :team-id uuid} - single role for all emails
|
||||
2. {:invitations [{:email ... :role ...}] :team-id uuid} - individual roles per email"
|
||||
[{:keys [emails role team-id invitations resend?] :as params}]
|
||||
|
||||
(assert (uuid? team-id))
|
||||
;; Validate input format - must have either emails+role OR invitations
|
||||
(assert (or (and emails role (sm/check-set-of-emails emails) (keyword? role))
|
||||
(and invitations
|
||||
(sm/check-set-of-emails (map :email invitations))
|
||||
(every? #(contains? ctt/valid-roles (:role %)) invitations)))
|
||||
"Must provide either emails+role or invitations with individual roles")
|
||||
(check-create-invitations-params params)
|
||||
|
||||
(ptk/reify ::create-invitations
|
||||
ev/Event
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
[app.common.path-names :as cpn]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.component :as ctc]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.variant :as ctv]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.changes :as dch]
|
||||
[app.main.data.comments :as dcmt]
|
||||
@@ -551,7 +553,6 @@
|
||||
component-id (:component-id shape)
|
||||
undo-id (js/Symbol)]
|
||||
|
||||
|
||||
(when valid?
|
||||
(if (ctc/is-variant-container? shape)
|
||||
;; Rename the full variant when it is a variant container
|
||||
@@ -566,6 +567,43 @@
|
||||
(dwl/rename-component component-id clean-name))
|
||||
(dwu/commit-undo-transaction undo-id))))))))))
|
||||
|
||||
(defn rename-shape-or-variant
|
||||
([id name]
|
||||
(rename-shape-or-variant nil nil id name))
|
||||
([file-id page-id id name]
|
||||
(ptk/reify ::rename-shape-or-variant
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (d/nilv file-id (:current-file-id state))
|
||||
page-id (d/nilv page-id (:current-page-id state))
|
||||
|
||||
file-data (dsh/lookup-file-data state file-id)
|
||||
shape
|
||||
(-> (dsh/lookup-page-objects state file-id page-id)
|
||||
(get id))
|
||||
|
||||
is-variant? (ctc/is-variant? shape)
|
||||
variant-id (when is-variant? (:variant-id shape))
|
||||
variant-name (when is-variant? (:variant-name shape))
|
||||
component-id (:component-id shape)
|
||||
component (ctkl/get-component file-data (:component-id shape))
|
||||
variant-properties (:variant-properties component)]
|
||||
(cond
|
||||
(and variant-name (ctv/valid-properties-formula? name))
|
||||
(rx/of (dwva/update-properties-names-and-values
|
||||
component-id variant-id variant-properties (ctv/properties-formula->map name))
|
||||
(dwva/remove-empty-properties variant-id)
|
||||
(dwva/update-error component-id))
|
||||
|
||||
variant-name
|
||||
(rx/of (dwva/update-properties-names-and-values
|
||||
component-id variant-id variant-properties {})
|
||||
(dwva/remove-empty-properties variant-id)
|
||||
(dwva/update-error component-id name))
|
||||
|
||||
:else
|
||||
(rx/of (end-rename-shape id name))))))))
|
||||
|
||||
;; --- Update Selected Shapes attrs
|
||||
|
||||
(defn update-selected-shapes
|
||||
|
||||
@@ -102,7 +102,8 @@
|
||||
{:origin it
|
||||
:redo-changes changes
|
||||
:undo-changes []
|
||||
:save-undo? false})))))))
|
||||
:save-undo? false
|
||||
:ignore-wasm? true})))))))
|
||||
|
||||
;; FIXME: would be nice to not execute this code twice per page in the
|
||||
;; same working session, maybe some local memoization can improve that
|
||||
@@ -119,4 +120,5 @@
|
||||
{:origin it
|
||||
:redo-changes changes
|
||||
:undo-changes []
|
||||
:save-undo? false})))))))
|
||||
:save-undo? false
|
||||
:ignore-wasm? true})))))))
|
||||
|
||||
@@ -649,7 +649,7 @@
|
||||
(propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))
|
||||
|
||||
ids
|
||||
(into [] xf:without-uuid-zero (keys transforms))
|
||||
(into (set (keys modif-tree)) xf:without-uuid-zero (keys transforms))
|
||||
|
||||
update-shape
|
||||
(fn [shape]
|
||||
|
||||
@@ -831,7 +831,8 @@
|
||||
(effect [_ state _]
|
||||
(when (features/active-feature? state "text-editor/v2")
|
||||
(let [instance (:workspace-editor state)
|
||||
attrs-to-override (some-> (editor.v2/getCurrentStyle instance) (styles/get-styles-from-style-declaration))
|
||||
attrs-to-override (some-> (editor.v2/getCurrentStyle instance)
|
||||
(styles/get-styles-from-style-declaration))
|
||||
overriden-attrs (merge attrs-to-override attrs)
|
||||
styles (styles/attrs->styles overriden-attrs)]
|
||||
(editor.v2/applyStylesToSelection instance styles))))))
|
||||
|
||||
@@ -153,11 +153,11 @@
|
||||
(defn value->shadow
|
||||
"Transform a token shadow value into penpot shadow data structure"
|
||||
[value]
|
||||
(mapv (fn [{:keys [offsetX offsetY blur spread color inset]}]
|
||||
(mapv (fn [{:keys [offset-x offset-y blur spread color inset]}]
|
||||
{:id (random-uuid)
|
||||
:hidden false
|
||||
:offset-x offsetX
|
||||
:offset-y offsetY
|
||||
:offset-x offset-x
|
||||
:offset-y offset-y
|
||||
:blur blur
|
||||
:color (value->color color)
|
||||
:spread spread
|
||||
|
||||
@@ -88,6 +88,10 @@
|
||||
{:error/code :error.style-dictionary/invalid-token-value-font-weight
|
||||
:error/fn #(tr "workspace.tokens.invalid-font-weight-token-value" %)}
|
||||
|
||||
:error.style-dictionary/invalid-token-value-font-family
|
||||
{:error/code :error.style-dictionary/invalid-token-value-font-family
|
||||
:error/fn #(tr "workspace.tokens.invalid-font-family-token-value" %)}
|
||||
|
||||
:error.style-dictionary/invalid-token-value-typography
|
||||
{:error/code :error.style-dictionary/invalid-token-value-typography
|
||||
:error/fn #(tr "workspace.tokens.invalid-token-value-typography" %)}
|
||||
@@ -108,6 +112,10 @@
|
||||
{:error/code :error.style-dictionary/invalid-token-value-shadow-spread
|
||||
:error/fn #(tr "workspace.tokens.shadow-spread-range")}
|
||||
|
||||
:error.style-dictionary/invalid-token-value-shadow
|
||||
{:error/code :error.style-dictionary/invalid-token-value-shadow
|
||||
:error/fn #(tr "workspace.tokens.invalid-token-value-shadow" %)}
|
||||
|
||||
:error/unknown
|
||||
{:error/code :error/unknown
|
||||
:error/fn #(tr "labels.unknown-error")}})
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
:letter-spacing "Letter Spacing"
|
||||
:text-case "Text Case"
|
||||
:text-decoration "Text Decoration"
|
||||
:offsetX "X"
|
||||
:offsetY "Y"
|
||||
:offset-x "X"
|
||||
:offset-y "Y"
|
||||
:blur "Blur"
|
||||
:spread "Spread"
|
||||
:color "Color"
|
||||
|
||||
@@ -128,14 +128,16 @@
|
||||
related-components (cfv/find-variant-components data objects variant-id)
|
||||
|
||||
props (-> related-components last :variant-properties)
|
||||
prop-name (-> props (nth pos) :name)
|
||||
valid-pos? (> (count props) pos)
|
||||
prop-name (when valid-pos? (-> props (nth pos) :name))
|
||||
|
||||
changes (-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data data)
|
||||
(clvp/generate-update-property-name variant-id pos new-name))
|
||||
changes (when valid-pos?
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data data)
|
||||
(clvp/generate-update-property-name variant-id pos new-name)))
|
||||
undo-id (js/Symbol)]
|
||||
(when (not= prop-name new-name)
|
||||
(when (and valid-pos? (not= prop-name new-name))
|
||||
(rx/of
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
(log/set-level! :warn)
|
||||
|
||||
(def google-fonts
|
||||
(preload-gfonts "fonts/gfonts.2025.05.19.json"))
|
||||
(preload-gfonts "fonts/gfonts.2025.11.28.json"))
|
||||
|
||||
(def local-fonts
|
||||
[{:id "sourcesanspro"
|
||||
@@ -342,8 +342,8 @@
|
||||
(fn [result {:keys [font-id] :as node}]
|
||||
(let [current-font
|
||||
(if (some? font-id)
|
||||
(select-keys node [:font-id :font-variant-id])
|
||||
(select-keys txt/default-typography [:font-id :font-variant-id]))]
|
||||
(select-keys node [:font-id :font-variant-id :font-weight :font-style])
|
||||
(select-keys txt/default-typography [:font-id :font-variant-id :font-weight :font-style]))]
|
||||
(conj result current-font)))
|
||||
#{})))
|
||||
|
||||
|
||||
@@ -372,6 +372,9 @@
|
||||
(def workspace-modifiers
|
||||
(l/derived :workspace-modifiers st/state))
|
||||
|
||||
(def workspace-wasm-modifiers
|
||||
(l/derived :workspace-wasm-modifiers st/state))
|
||||
|
||||
(def ^:private workspace-modifiers-with-objects
|
||||
(l/derived
|
||||
(fn [state]
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[app.main.ui.components.dropdown :refer [dropdown-content*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.timers :as tm]
|
||||
@@ -53,14 +54,13 @@
|
||||
(def ^:private valid-option?
|
||||
(sm/lazy-validator schema:option))
|
||||
|
||||
(mf/defc context-menu*
|
||||
[{:keys [show on-close options selectable selected
|
||||
(mf/defc context-menu-inner*
|
||||
[{:keys [on-close options selectable selected
|
||||
top left fixed min-width origin width]
|
||||
:as props}]
|
||||
|
||||
(assert (every? valid-option? options) "expected valid options")
|
||||
(assert (fn? on-close) "missing `on-close` prop")
|
||||
(assert (boolean? show) "missing `show` prop")
|
||||
(assert (vector? options) "missing `options` prop")
|
||||
|
||||
(let [width (d/nilv width "initial")
|
||||
@@ -80,14 +80,15 @@
|
||||
offset-x (get state :offset-x)
|
||||
offset-y (get state :offset-y)
|
||||
levels (get state :levels)
|
||||
internal-id (mf/use-id)
|
||||
|
||||
on-local-close
|
||||
(mf/use-fn
|
||||
(mf/deps on-close)
|
||||
(fn []
|
||||
(swap! state* assoc :levels [{:parent nil
|
||||
:options options}])
|
||||
(on-close)))
|
||||
(swap! state* assoc :levels [{:parent nil :options options}])
|
||||
(when (fn? on-close)
|
||||
(on-close))))
|
||||
|
||||
props
|
||||
(mf/spread-props props {:on-close on-local-close})
|
||||
@@ -216,11 +217,22 @@
|
||||
(swap! state* assoc :levels [{:parent nil
|
||||
:options options}]))
|
||||
|
||||
(mf/with-effect [internal-id]
|
||||
(ug/dispatch! (ug/event "penpot:context-menu:open" #js {:id internal-id})))
|
||||
|
||||
(mf/with-effect [internal-id on-local-close]
|
||||
(letfn [(on-event [event]
|
||||
(when-let [detail (unchecked-get event "detail")]
|
||||
(when (not= internal-id (unchecked-get detail "id"))
|
||||
(on-local-close event))))]
|
||||
(ug/listen "penpot:context-menu:open" on-event)
|
||||
(partial ug/unlisten "penpot:context-menu:open" on-event)))
|
||||
|
||||
(mf/with-effect [ids]
|
||||
(tm/schedule-on-idle
|
||||
#(dom/focus! (dom/get-element (first ids)))))
|
||||
|
||||
(when (and show (some? levels))
|
||||
(when (some? levels)
|
||||
[:> dropdown-content* props
|
||||
(let [level (peek levels)
|
||||
options (:options level)
|
||||
@@ -229,7 +241,7 @@
|
||||
[:div {:class (stl/css-case
|
||||
:is-selectable selectable
|
||||
:context-menu true
|
||||
:is-open show
|
||||
:is-open true
|
||||
:fixed fixed)
|
||||
:style {:top (+ top offset-y)
|
||||
:left (+ left offset-x)}
|
||||
@@ -241,7 +253,7 @@
|
||||
:role "menu"
|
||||
:ref check-menu-offscreen}
|
||||
|
||||
(when-let [parent (:parent level)]
|
||||
(when parent
|
||||
[:*
|
||||
[:li {:id "go-back-sub-option"
|
||||
:class (stl/css :context-menu-item)
|
||||
@@ -256,7 +268,7 @@
|
||||
|
||||
[:li {:class (stl/css :separator)}]])
|
||||
|
||||
(for [[index option] (d/enumerate (:options level))]
|
||||
(for [[index option] (d/enumerate options)]
|
||||
(let [name (:name option)
|
||||
id (:id option)
|
||||
sub-options (:options option)
|
||||
@@ -297,3 +309,12 @@
|
||||
:data-testid id}
|
||||
name
|
||||
[:span {:class (stl/css :submenu-icon)} deprecated-icon/arrow]])]))))]])])))
|
||||
|
||||
(mf/defc context-menu*
|
||||
{::mf/private true}
|
||||
[{:keys [show] :as props}]
|
||||
|
||||
(assert (boolean? show) "expected `show` prop to be a boolean")
|
||||
|
||||
(when ^boolean show
|
||||
[:> context-menu-inner* props]))
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
(def current-zoom (mf/create-context nil))
|
||||
|
||||
(def workspace-read-only? (mf/create-context nil))
|
||||
(def is-render? (mf/create-context false))
|
||||
(def is-component? (mf/create-context false))
|
||||
|
||||
(def sidebar
|
||||
|
||||
@@ -151,14 +151,16 @@
|
||||
|
||||
(mf/defc menu-team-icon*
|
||||
[{:keys [subscription-type]}]
|
||||
[:span {:class (stl/css :subscription-icon)
|
||||
:title (if (= subscription-type "unlimited")
|
||||
(tr "subscription.dashboard.power-up.unlimited-plan")
|
||||
(tr "subscription.dashboard.power-up.enterprise-plan"))
|
||||
:data-testid "subscription-icon"}
|
||||
(case subscription-type
|
||||
"unlimited" i/character-u
|
||||
"enterprise" i/character-e)])
|
||||
[:span {:class (stl/css :subscription-icon-wrapper)}
|
||||
[:> icon* {:icon-id (case subscription-type
|
||||
"unlimited" i/character-u
|
||||
"enterprise" i/character-e)
|
||||
:class (stl/css :subscription-icon)
|
||||
:size "s"
|
||||
:title (if (= subscription-type "unlimited")
|
||||
(tr "subscription.dashboard.power-up.unlimited-plan")
|
||||
(tr "subscription.dashboard.power-up.enterprise-plan"))
|
||||
:data-testid "subscription-icon"}]])
|
||||
|
||||
(mf/defc main-menu-power-up*
|
||||
[{:keys [close-sub-menu]}]
|
||||
|
||||
@@ -144,20 +144,20 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.subscription-icon {
|
||||
@extend .button-icon;
|
||||
.subscription-icon-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--color-background-primary);
|
||||
stroke: var(--color-foreground-secondary);
|
||||
border-radius: 6px;
|
||||
border-radius: $br-6;
|
||||
border: 1.75px solid var(--color-foreground-secondary);
|
||||
stroke-width: 2.25px;
|
||||
width: var(--sp-xl);
|
||||
height: var(--sp-xl);
|
||||
block-size: var(--sp-xl);
|
||||
inline-size: var(--sp-xl);
|
||||
}
|
||||
|
||||
svg {
|
||||
block-size: var(--sp-m);
|
||||
inline-size: var(--sp-m);
|
||||
}
|
||||
.subscription-icon {
|
||||
stroke: var(--color-foreground-secondary);
|
||||
stroke-width: 2.25px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
|
||||
@@ -106,14 +106,14 @@
|
||||
(when (not= 0 count-libraries)
|
||||
(if (pos? (count references))
|
||||
[:*
|
||||
[:div
|
||||
(when (and (string? scd-msg) (not= scd-msg ""))
|
||||
[:h3 {:class (stl/css :modal-scd-msg)} scd-msg])
|
||||
[:ul {:class (stl/css :element-list)}
|
||||
(for [[file-id file-name] references]
|
||||
[:li {:class (stl/css :list-item)
|
||||
:key (dm/str file-id)}
|
||||
[:span "- " file-name]])]]
|
||||
(when (and (string? scd-msg) (not= scd-msg ""))
|
||||
[:p {:class (stl/css :modal-scd-msg)} scd-msg])
|
||||
|
||||
[:ul {:class (stl/css :element-list)}
|
||||
(for [[file-id file-name] references]
|
||||
[:li {:class (stl/css :list-item)
|
||||
:key (dm/str file-id)}
|
||||
[:span "- " file-name]])]
|
||||
(when (and (string? hint) (not= hint ""))
|
||||
[:> context-notification* {:level :info
|
||||
:appearance :ghost}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "refactor/basic-rules.scss" as *;
|
||||
@use "ds/typography.scss" as t;
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
@@ -15,14 +16,19 @@
|
||||
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
display: grid;
|
||||
gap: var(--sp-xxl);
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
.list-wrapper {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include deprecated.headlineMediumTypography;
|
||||
@include t.use-typography("headline-medium");
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
@@ -31,13 +37,16 @@
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include deprecated.bodySmallTypography;
|
||||
margin-bottom: deprecated.$s-24;
|
||||
@include t.use-typography("body-small");
|
||||
display: grid;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.element-list {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
@include t.use-typography("body-large");
|
||||
color: var(--modal-text-foreground-color);
|
||||
overflow-y: scroll;
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@@ -55,10 +64,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal-scd-msg {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.modal-scd-msg,
|
||||
.modal-subtitle,
|
||||
.modal-msg {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
@include t.use-typography("body-large");
|
||||
color: var(--modal-text-foreground-color);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.main.ui.ds
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.checkbox :refer [checkbox*]]
|
||||
@@ -33,6 +32,7 @@
|
||||
[app.main.ui.ds.product.avatar :refer [avatar*]]
|
||||
[app.main.ui.ds.product.cta :refer [cta*]]
|
||||
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
|
||||
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
|
||||
[app.main.ui.ds.product.input-with-meta :refer [input-with-meta*]]
|
||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||
[app.main.ui.ds.product.milestone :refer [milestone*]]
|
||||
@@ -44,8 +44,6 @@
|
||||
[app.util.i18n :as i18n]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(i18n/init! cf/translations)
|
||||
|
||||
(def default
|
||||
"A export used for storybook"
|
||||
(mf/object
|
||||
@@ -59,6 +57,7 @@
|
||||
:HintMessage hint-message*
|
||||
:InputWithMeta input-with-meta*
|
||||
:EmptyPlaceholder empty-placeholder*
|
||||
:EmptyState empty-state*
|
||||
:Loader loader*
|
||||
:RawSvg raw-svg*
|
||||
:Select select*
|
||||
@@ -80,6 +79,11 @@
|
||||
:Milestone milestone*
|
||||
:MilestoneGroup milestone-group*
|
||||
:Date date*
|
||||
|
||||
:set-default-translations
|
||||
(fn [data]
|
||||
(i18n/set-translations "en" data))
|
||||
|
||||
;; meta / misc
|
||||
:meta
|
||||
{:icons (clj->js (sort icon-list))
|
||||
|
||||
@@ -32,13 +32,19 @@
|
||||
min-width: var(--sp-l);
|
||||
}
|
||||
|
||||
// TODO: Review if we need other type of button, so we don't need important here
|
||||
.invisible-button {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
opacity: var(--opacity-button);
|
||||
|
||||
background-color: var(--color-background-quaternary) !important;
|
||||
&:hover {
|
||||
background-color: var(--color-background-quaternary);
|
||||
--opacity-button: 1;
|
||||
}
|
||||
&:focus {
|
||||
background-color: var(--color-background-quaternary);
|
||||
--opacity-button: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps disabled)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(when-not disabled
|
||||
(swap! is-open* not))))
|
||||
|
||||
@@ -53,10 +53,15 @@
|
||||
"true")
|
||||
:aria-describedby (when has-hint
|
||||
(str id "-hint"))
|
||||
:aria-labelledby tooltip-id
|
||||
:type (d/nilv type "text")
|
||||
:id id
|
||||
:max-length (d/nilv max-length max-input-length)})
|
||||
|
||||
props (if (and aria-label (not (some? icon)))
|
||||
(mf/spread-props props
|
||||
{:aria-label aria-label})
|
||||
(mf/spread-props props
|
||||
{:aria-labelledby tooltip-id}))
|
||||
inside-class (stl/css-case :input-wrapper true
|
||||
:has-hint has-hint
|
||||
:hint-type-hint (= hint-type "hint")
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
[:div {:class (stl/css :pill-dot)}])]]
|
||||
|
||||
(when-not ^boolean disabled
|
||||
[:> icon-button* {:variant "action"
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:class (stl/css :invisible-button)
|
||||
:icon i/broken-link
|
||||
:ref token-detach-btn-ref
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/colors.scss" as *;
|
||||
@use "ds/mixins.scss" as *;
|
||||
|
||||
.token-field {
|
||||
--token-field-bg-color: var(--color-background-tertiary);
|
||||
@@ -16,9 +17,8 @@
|
||||
--token-field-outline-color: none;
|
||||
--token-field-height: var(--sp-xxxl);
|
||||
--token-field-margin: unset;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
width: inherit;
|
||||
column-gap: var(--sp-xs);
|
||||
align-items: center;
|
||||
position: relative;
|
||||
@@ -27,6 +27,7 @@
|
||||
border-radius: $br-8;
|
||||
padding: var(--sp-xs);
|
||||
outline: $b-1 solid var(--token-field-outline-color);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
--token-field-bg-color: var(--color-background-quaternary);
|
||||
@@ -39,7 +40,7 @@
|
||||
}
|
||||
|
||||
.with-icon {
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.token-field-disabled {
|
||||
@@ -57,6 +58,8 @@
|
||||
--pill-bg-color: var(--color-background-tertiary);
|
||||
--pill-fg-color: var(--color-token-foreground);
|
||||
@include t.use-typography("code-font");
|
||||
@include textEllipsis;
|
||||
display: block;
|
||||
height: var(--sp-xxl);
|
||||
width: fit-content;
|
||||
background: var(--pill-bg-color);
|
||||
@@ -65,6 +68,7 @@
|
||||
color: var(--pill-fg-color);
|
||||
border-radius: $br-6;
|
||||
padding-inline: $sz-6;
|
||||
max-width: 100%;
|
||||
&:hover {
|
||||
--pill-bg-color: var(--color-token-background);
|
||||
--pill-fg-color: var(--color-foreground-primary);
|
||||
@@ -115,6 +119,9 @@
|
||||
}
|
||||
|
||||
.invisible-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
opacity: var(--opacity-button);
|
||||
|
||||
&:hover {
|
||||
|
||||
49
frontend/src/app/main/ui/ds/layers/layer_button.cljs
Normal file
49
frontend/src/app/main/ui/ds/layers/layer_button.cljs
Normal file
@@ -0,0 +1,49 @@
|
||||
;; 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.layers.layer-button
|
||||
(:require-macros
|
||||
[app.main.style :as stl])
|
||||
(:require
|
||||
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema:layer-button
|
||||
[:map
|
||||
[:label :string]
|
||||
[:description {:optional true} [:maybe :string]]
|
||||
[:class {:optional true} :string]
|
||||
[:expandable {:optional true} :boolean]
|
||||
[:expanded {:optional true} :boolean]
|
||||
[:icon {:optional true} :string]
|
||||
[:on-toggle-expand fn?]])
|
||||
|
||||
(mf/defc layer-button*
|
||||
{::mf/schema schema:layer-button}
|
||||
[{:keys [label description class is-expandable expanded icon on-toggle-expand children] :rest props}]
|
||||
(let [button-props (mf/spread-props props
|
||||
{:class [class (stl/css-case :layer-button true
|
||||
:layer-button--expandable is-expandable
|
||||
:layer-button--expanded expanded)]
|
||||
:type "button"
|
||||
:on-click on-toggle-expand})]
|
||||
[:div {:class (stl/css :layer-button-wrapper)}
|
||||
[:> "button" button-props
|
||||
[:div {:class (stl/css :layer-button-content)}
|
||||
(when is-expandable
|
||||
(if expanded
|
||||
[:> icon* {:icon-id i/arrow-down :class (stl/css :folder-node-icon)}]
|
||||
[:> icon* {:icon-id i/arrow-right :class (stl/css :folder-node-icon)}]))
|
||||
(when icon
|
||||
[:> icon* {:icon-id icon :class (stl/css :layer-button-icon)}])
|
||||
[:span {:class (stl/css :layer-button-name)}
|
||||
label]
|
||||
(when description
|
||||
[:span {:class (stl/css :layer-button-description)}
|
||||
description])
|
||||
[:span {:class (stl/css :layer-button-quantity)}]]]
|
||||
[:div {:class (stl/css :layer-button-actions)}
|
||||
children]]))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user