mirror of
https://github.com/penpot/penpot.git
synced 2026-02-18 23:29:12 -05:00
Compare commits
1 Commits
alotor-fix
...
test-inner
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
369979ffe6 |
83
.github/workflows/tests.yml
vendored
83
.github/workflows/tests.yml
vendored
@@ -8,6 +8,8 @@ on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
- synchronize
|
||||
push:
|
||||
branches:
|
||||
@@ -15,12 +17,12 @@ on:
|
||||
- staging
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event.pull_request.number || github.ref }}
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: "Linter"
|
||||
name: "Code Linter"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
@@ -30,7 +32,10 @@ jobs:
|
||||
|
||||
- name: Check clojure code format
|
||||
run: |
|
||||
./scripts/lint
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install
|
||||
yarn run fmt:clj:check
|
||||
|
||||
test-common:
|
||||
name: "Common Tests"
|
||||
@@ -49,7 +54,10 @@ jobs:
|
||||
- name: Run tests on NODE
|
||||
working-directory: ./common
|
||||
run: |
|
||||
./scripts/test
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run test;
|
||||
|
||||
test-frontend:
|
||||
name: "Frontend Tests"
|
||||
@@ -63,36 +71,25 @@ jobs:
|
||||
- name: Unit Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/test
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run test;
|
||||
|
||||
- name: Component Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/test-components
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run build:storybook
|
||||
|
||||
test-render-wasm:
|
||||
name: "Render WASM Tests"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
|
||||
"npx http-server storybook-static --port 6006 --silent" \
|
||||
"npx wait-on tcp:6006 && yarn test:storybook"
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Format
|
||||
working-directory: ./render-wasm
|
||||
- name: Check SCSS Format
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
cargo fmt --check
|
||||
|
||||
- name: Lint
|
||||
working-directory: ./render-wasm
|
||||
run: |
|
||||
./lint
|
||||
|
||||
- name: Test
|
||||
working-directory: ./render-wasm
|
||||
run: |
|
||||
./test
|
||||
yarn run lint:scss;
|
||||
|
||||
test-backend:
|
||||
name: "Backend Tests"
|
||||
@@ -145,7 +142,11 @@ jobs:
|
||||
- name: Run tests
|
||||
working-directory: ./library
|
||||
run: |
|
||||
./scripts/test
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run build:bundle;
|
||||
yarn run test;
|
||||
|
||||
build-integration:
|
||||
name: "Build Integration Bundle"
|
||||
@@ -196,7 +197,11 @@ jobs:
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/test-e2e --shard="1/4";
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list --shard="1/4";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -226,7 +231,11 @@ jobs:
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/test-e2e --shard="2/4";
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list --shard "2/4";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -256,7 +265,11 @@ jobs:
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/test-e2e --shard="3/4";
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list --shard "3/4";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -268,7 +281,7 @@ jobs:
|
||||
retention-days: 3
|
||||
|
||||
test-integration-4:
|
||||
name: "Integration Tests 4/4"
|
||||
name: "Integration Tests 3/4"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
needs: build-integration
|
||||
@@ -286,7 +299,11 @@ jobs:
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/test-e2e --shard="4/4";
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list --shard "4/4";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -80,4 +80,3 @@ node_modules
|
||||
/playwright/.cache/
|
||||
/render-wasm/target/
|
||||
/**/.yarn/*
|
||||
/.pnpm-store
|
||||
|
||||
@@ -61,7 +61,6 @@ 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
|
||||
|
||||
@@ -88,10 +87,6 @@ example. It's still usable as before, we just removed the example.
|
||||
- Fix problem with plugins generating code for pages different than current one [Taiga #12312](https://tree.taiga.io/project/penpot/issue/12312)
|
||||
- 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)
|
||||
|
||||
## 2.11.1
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
From: {{profile.fullname}} <{{profile.email}}> / {{profile.id}}
|
||||
Subject: {{feedback-subject}}
|
||||
Type: {{feedback-type}}
|
||||
|
||||
{% if feedback-error-href %}
|
||||
{%- if feedback-error-href %}
|
||||
HREF: {{feedback-error-href}}
|
||||
{% endif -%}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
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,6 +79,18 @@
|
||||
(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")]
|
||||
@@ -87,24 +99,13 @@
|
||||
(str/blank? session-id))
|
||||
session-id)))
|
||||
|
||||
(defn- get-client-event-origin
|
||||
(defn- get-external-event-origin
|
||||
[request]
|
||||
(when-let [origin (yreq/get-header request "x-event-origin")]
|
||||
(when-not (or (= origin "null")
|
||||
(when-not (or (> (count origin) 256)
|
||||
(= origin "null")
|
||||
(str/blank? 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))))
|
||||
origin)))
|
||||
|
||||
;; --- SPECS
|
||||
|
||||
@@ -133,33 +134,6 @@
|
||||
(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)
|
||||
@@ -174,10 +148,18 @@
|
||||
(merge (::props resultm))
|
||||
(dissoc :profile-id)
|
||||
(dissoc :type)))
|
||||
|
||||
(clean-props))
|
||||
|
||||
context (merge (::context resultm)
|
||||
(prepare-context-from-request request))
|
||||
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))
|
||||
|
||||
ip-addr (inet/parse-request request)]
|
||||
|
||||
{::type (or (::type resultm)
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
:uid uuid/zero})
|
||||
body (t/encode {:events events})
|
||||
headers {"content-type" "application/transit+json"
|
||||
"origin" (str (cf/get :public-uri))
|
||||
"origin" (cf/get :public-uri)
|
||||
"cookie" (u/map->query-string {:auth-token token})}
|
||||
params {:uri uri
|
||||
:timeout 12000
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[selmer.parser :as sp]))
|
||||
|
||||
;; (sp/cache-off!)
|
||||
(sp/cache-off!)
|
||||
|
||||
(defn render
|
||||
[path context]
|
||||
|
||||
@@ -318,35 +318,3 @@
|
||||
;; check that we have all no objects
|
||||
(let [rows (th/db-exec! ["select * from storage_object where deleted_at is null"])]
|
||||
(t/is (= 0 (count rows))))))
|
||||
|
||||
(t/deftest tempfile-bucket-test
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
content1 (sto/content "content1")
|
||||
now (ct/now)
|
||||
|
||||
object1 (sto/put-object! storage {::sto/content content1
|
||||
::sto/touched-at (ct/plus now {:minutes 1})
|
||||
:bucket "tempfile"
|
||||
:content-type "text/plain"})]
|
||||
|
||||
|
||||
(binding [ct/*clock* (clock/fixed now)]
|
||||
(let [res (th/run-task! :storage-gc-touched {})]
|
||||
(t/is (= 0 (:freeze res)))
|
||||
(t/is (= 0 (:delete res)))))
|
||||
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/plus now {:minutes 1}))]
|
||||
(let [res (th/run-task! :storage-gc-touched {})]
|
||||
(t/is (= 0 (:freeze res)))
|
||||
(t/is (= 1 (:delete res)))))
|
||||
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/plus now {:hours 1}))]
|
||||
(let [res (th/run-task! :storage-gc-deleted {})]
|
||||
(t/is (= 0 (:deleted res)))))
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/plus now {:hours 2}))]
|
||||
(let [res (th/run-task! :storage-gc-deleted {})]
|
||||
(t/is (= 0 (:deleted res)))))))
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
org.slf4j/slf4j-api {:mvn/version "2.0.17"}
|
||||
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.40"}
|
||||
|
||||
selmer/selmer {:mvn/version "1.12.69"}
|
||||
selmer/selmer {:mvn/version "1.12.62"}
|
||||
criterium/criterium {:mvn/version "0.4.6"}
|
||||
|
||||
metosin/jsonista {:mvn/version "0.3.13"}
|
||||
@@ -48,8 +48,12 @@
|
||||
com.sun.mail/jakarta.mail {:mvn/version "2.0.2"}
|
||||
org.la4j/la4j {:mvn/version "0.6.0"}
|
||||
|
||||
;; exception printing
|
||||
fipp/fipp {:mvn/version "0.6.29"}
|
||||
|
||||
me.flowthing/pp {:mvn/version "2024-11-13.77"}
|
||||
|
||||
|
||||
io.aviso/pretty {:mvn/version "1.4.4"}
|
||||
environ/environ {:mvn/version "1.2.0"}}
|
||||
:paths ["src" "vendor" "target/classes"]
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run test;
|
||||
@@ -9,10 +9,10 @@
|
||||
(:refer-clojure :exclude [get-in select-keys str with-open max])
|
||||
#?(:cljs (:require-macros [app.common.data.macros]))
|
||||
(:require
|
||||
#?(:clj [cljs.analyzer.api :as aapi])
|
||||
#?(:clj [clojure.core :as c]
|
||||
:cljs [cljs.core :as c])
|
||||
[app.common.data :as d]
|
||||
[cljs.analyzer.api :as aapi]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defmacro select-keys
|
||||
@@ -44,43 +44,42 @@
|
||||
[& params]
|
||||
`(str/concat ~@params))
|
||||
|
||||
#?(:clj
|
||||
(defmacro export
|
||||
"A helper macro that allows reexport a var in a current namespace."
|
||||
[v]
|
||||
(if (boolean (:ns &env))
|
||||
(defmacro export
|
||||
"A helper macro that allows reexport a var in a current namespace."
|
||||
[v]
|
||||
(if (boolean (:ns &env))
|
||||
|
||||
;; Code for ClojureScript
|
||||
(let [mdata (aapi/resolve &env v)
|
||||
arglists (second (get-in mdata [:meta :arglists]))
|
||||
sym (symbol (c/name v))
|
||||
andsym (symbol "&")
|
||||
procarg #(if (= % andsym) % (gensym "param"))]
|
||||
(if (pos? (count arglists))
|
||||
`(def
|
||||
~(with-meta sym (:meta mdata))
|
||||
(fn ~@(for [args arglists]
|
||||
(let [args (map procarg args)]
|
||||
(if (some #(= andsym %) args)
|
||||
(let [[sargs dargs] (split-with #(not= andsym %) args)]
|
||||
`([~@sargs ~@dargs] (apply ~v ~@sargs ~@(rest dargs))))
|
||||
`([~@args] (~v ~@args)))))))
|
||||
`(def ~(with-meta sym (:meta mdata)) ~v)))
|
||||
;; Code for ClojureScript
|
||||
(let [mdata (aapi/resolve &env v)
|
||||
arglists (second (get-in mdata [:meta :arglists]))
|
||||
sym (symbol (c/name v))
|
||||
andsym (symbol "&")
|
||||
procarg #(if (= % andsym) % (gensym "param"))]
|
||||
(if (pos? (count arglists))
|
||||
`(def
|
||||
~(with-meta sym (:meta mdata))
|
||||
(fn ~@(for [args arglists]
|
||||
(let [args (map procarg args)]
|
||||
(if (some #(= andsym %) args)
|
||||
(let [[sargs dargs] (split-with #(not= andsym %) args)]
|
||||
`([~@sargs ~@dargs] (apply ~v ~@sargs ~@(rest dargs))))
|
||||
`([~@args] (~v ~@args)))))))
|
||||
`(def ~(with-meta sym (:meta mdata)) ~v)))
|
||||
|
||||
;; Code for Clojure
|
||||
(let [vr (resolve v)
|
||||
m (meta vr)
|
||||
n (:name m)
|
||||
n (with-meta n
|
||||
(cond-> {}
|
||||
(:dynamic m) (assoc :dynamic true)
|
||||
(:protocol m) (assoc :protocol (:protocol m))))]
|
||||
`(let [m# (meta ~vr)]
|
||||
(def ~n (deref ~vr))
|
||||
(alter-meta! (var ~n) merge (dissoc m# :name))
|
||||
;; (when (:macro m#)
|
||||
;; (.setMacro (var ~n)))
|
||||
~vr)))))
|
||||
;; Code for Clojure
|
||||
(let [vr (resolve v)
|
||||
m (meta vr)
|
||||
n (:name m)
|
||||
n (with-meta n
|
||||
(cond-> {}
|
||||
(:dynamic m) (assoc :dynamic true)
|
||||
(:protocol m) (assoc :protocol (:protocol m))))]
|
||||
`(let [m# (meta ~vr)]
|
||||
(def ~n (deref ~vr))
|
||||
(alter-meta! (var ~n) merge (dissoc m# :name))
|
||||
;; (when (:macro m#)
|
||||
;; (.setMacro (var ~n)))
|
||||
~vr))))
|
||||
|
||||
(defmacro fmt
|
||||
"String interpolation helper. Can only be used with strings known at
|
||||
|
||||
@@ -842,6 +842,38 @@
|
||||
choices))]
|
||||
{:pred pred}))})
|
||||
|
||||
;; (register!
|
||||
;; {:type ::inst
|
||||
;; :pred tm/instant?
|
||||
;; :type-properties
|
||||
;; {:title "inst"
|
||||
;; :description "Satisfies Inst protocol"
|
||||
;; :error/message "should be an instant"
|
||||
;; :gen/gen (->> (sg/small-int :min 0 :max 100000)
|
||||
;; (sg/fmap (fn [v] (tm/parse-inst v))))
|
||||
|
||||
;; :decode/string tm/parse-inst
|
||||
;; :encode/string tm/format-inst
|
||||
;; :decode/json tm/parse-inst
|
||||
;; :encode/json tm/format-inst
|
||||
;; ::oapi/type "string"
|
||||
;; ::oapi/format "iso"}})
|
||||
|
||||
;; (register!
|
||||
;; {:type ::timestamp
|
||||
;; :pred tm/instant?
|
||||
;; :type-properties
|
||||
;; {:title "inst"
|
||||
;; :description "Satisfies Inst protocol, the same as ::inst but encodes to epoch"
|
||||
;; :error/message "should be an instant"
|
||||
;; :gen/gen (->> (sg/small-int)
|
||||
;; (sg/fmap (fn [v] (tm/parse-inst v))))
|
||||
;; :decode/string tm/parse-inst
|
||||
;; :encode/string inst-ms
|
||||
;; :decode/json tm/parse-inst
|
||||
;; :encode/json inst-ms
|
||||
;; ::oapi/type "string"
|
||||
;; ::oapi/format "number"}})
|
||||
|
||||
#?(:clj
|
||||
(register!
|
||||
@@ -919,7 +951,7 @@
|
||||
:pred #(and (string? %) (not (str/blank? %)))
|
||||
:property-pred
|
||||
(fn [{:keys [min max] :as props}]
|
||||
(if (or min max)
|
||||
(if (seq props)
|
||||
(fn [value]
|
||||
(let [size (count value)]
|
||||
(cond
|
||||
|
||||
@@ -234,15 +234,16 @@
|
||||
"Calculate the boolean content from shape and objects. Returns a
|
||||
packed PathData instance"
|
||||
[shape objects]
|
||||
(let [content (calc-bool-content* 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))]
|
||||
(impl/path-data content)))
|
||||
|
||||
(defn update-bool-shape
|
||||
"Calculates the selrect+points for the boolean shape"
|
||||
[shape objects]
|
||||
(let [content (if (fn? wasm:calc-bool-content)
|
||||
(wasm:calc-bool-content shape objects)
|
||||
(calc-bool-content shape objects))
|
||||
(let [content (calc-bool-content shape objects)
|
||||
shape (assoc shape :content content)]
|
||||
(update-geometry shape)))
|
||||
|
||||
@@ -266,4 +267,3 @@
|
||||
(-> (stp/convert-to-path shape objects)
|
||||
(update :content impl/path-data))))
|
||||
|
||||
(dm/export impl/decode-segments)
|
||||
|
||||
@@ -565,9 +565,6 @@
|
||||
(def check-content
|
||||
(sm/check-fn schema:content))
|
||||
|
||||
(def decode-segments
|
||||
(sm/lazy-decoder schema:segments sm/json-transformer))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; CONSTRUCTORS & PREDICATES
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -101,45 +101,13 @@ RUN set -eux; \
|
||||
corepack enable; \
|
||||
rm -rf /tmp/nodejs.tar.gz;
|
||||
|
||||
|
||||
################################################################################
|
||||
## CADDYSERVER SETUP
|
||||
################################################################################
|
||||
|
||||
FROM base AS setup-caddy
|
||||
|
||||
ENV CADDY_VERSION=2.10.2
|
||||
|
||||
RUN set -eux; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
case "${ARCH}" in \
|
||||
aarch64|arm64) \
|
||||
BINARY_URL="https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_arm64.tar.gz"; \
|
||||
;; \
|
||||
amd64|x86_64) \
|
||||
BINARY_URL="https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_amd64.tar.gz"; \
|
||||
;; \
|
||||
*) \
|
||||
echo "Unsupported arch: ${ARCH}"; \
|
||||
exit 1; \
|
||||
;; \
|
||||
esac; \
|
||||
curl -LfsSo /tmp/caddy.tar.gz ${BINARY_URL}; \
|
||||
mkdir -p /tmp/caddy; \
|
||||
cd /tmp/caddy; \
|
||||
tar -xf /tmp/caddy.tar.gz; \
|
||||
chown -R root /tmp/caddy; \
|
||||
mv /tmp/caddy/caddy /usr/bin/; \
|
||||
rm -rf /tmp/caddy.tar.gz; \
|
||||
rm -rf /tmp/caddy;
|
||||
|
||||
################################################################################
|
||||
## JVM SETUP
|
||||
################################################################################
|
||||
|
||||
FROM base AS setup-jvm
|
||||
|
||||
ENV CLOJURE_VERSION=1.12.3.1577
|
||||
ENV CLOJURE_VERSION=1.12.2.1565
|
||||
|
||||
RUN set -eux; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
@@ -425,7 +393,6 @@ COPY --from=setup-utils /opt/utils /opt/utils
|
||||
COPY --from=setup-rust /opt/cargo /opt/cargo
|
||||
COPY --from=setup-rust /opt/rustup /opt/rustup
|
||||
COPY --from=setup-rust /opt/emsdk /opt/emsdk
|
||||
COPY --from=setup-caddy /usr/bin/caddy /usr/bin/caddy
|
||||
|
||||
COPY files/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY files/nginx-mime.types /etc/nginx/mime.types
|
||||
@@ -436,9 +403,6 @@ COPY files/vimrc /root/.vimrc
|
||||
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,8 +33,6 @@ services:
|
||||
- 3447:3447
|
||||
- 3448:3448
|
||||
- 3449:3449
|
||||
- 3449:3449/udp
|
||||
- 3450:3450
|
||||
- 6006:6006
|
||||
- 6060:6060
|
||||
- 6061:6061
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
auto_https off
|
||||
}
|
||||
|
||||
localhost:3449 {
|
||||
reverse_proxy localhost:4449
|
||||
tls /home/selfsigned.crt /home/selfsigned.key
|
||||
}
|
||||
|
||||
http://localhost:3450 {
|
||||
reverse_proxy localhost:4449
|
||||
}
|
||||
@@ -2,5 +2,4 @@
|
||||
|
||||
set -e
|
||||
nginx
|
||||
caddy start -c /home/Caddyfile
|
||||
tail -f /dev/null;
|
||||
tail -f /dev/null
|
||||
|
||||
@@ -12,7 +12,7 @@ http {
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 100;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
|
||||
@@ -55,7 +55,7 @@ http {
|
||||
proxy_cache_key "$host$request_uri";
|
||||
|
||||
server {
|
||||
listen 4449 default_server;
|
||||
listen 3449 default_server;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 300M;
|
||||
@@ -223,6 +223,16 @@ http {
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location ~ ^/js/config.js$ {
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|jpg|svg|png|mjs|map)$ {
|
||||
# We set no cache only on devenv
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
# add_header Cache-Control "max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
|
||||
}
|
||||
|
||||
@@ -230,9 +240,9 @@ http {
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
-----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-----
|
||||
@@ -1,28 +0,0 @@
|
||||
-----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-----
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
@@ -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>Access your account settings and manage personal access tokens</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/teams">
|
||||
<h2>Teams →</h2>
|
||||
<p>Create and manage your teams</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/comments/">
|
||||
<h2>Comments →</h2>
|
||||
<p>Give and receive feedback right over your designs</p>
|
||||
<p>Info of interest about Penpot</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>Store elements and styles to easily reuse them</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/libraries">
|
||||
<h2>Libraries →</h2>
|
||||
<p>Organize and manage your stored elements with Libraries</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/components">
|
||||
<h2>Components →</h2>
|
||||
<p>Speed your design workflow with reusable components</p>
|
||||
<p>Speed your design workflow</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/variants">
|
||||
<h2>Variants →</h2>
|
||||
<p>Group components into a single, customizable one</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/design-tokens">
|
||||
<h2>Design Tokens →</h2>
|
||||
<p>Synchronize visual elements across your designs</p>
|
||||
<p>Info of interest about Penpot</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, 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, graphics, 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>
|
||||
|
||||
@@ -107,25 +107,6 @@ desc: Streamline your design workflow with Penpot's Components guide! Learn to c
|
||||
<li>Select the variant copy, press right-click, and select the menu option <strong>Restore variant</strong> (will show if the main component still exists).</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="component-variants-toggle">Toggle for boolean variants</h3>
|
||||
<p>When a variant property represents a boolean state, Penpot can display it as a toggle instead of a dropdown. This offers a quicker and more visual way to switch between two opposite values when working with copies.</p>
|
||||
<p>The toggle appears in place of the property values dropdown, <strong>only when a copy is selected</strong>.</p>
|
||||
<figure>
|
||||
<img src="/img/variants/07-variants-boolean.webp" alt="Boolean variant option" />
|
||||
</figure>
|
||||
<h4>Accepted value pairs</h4>
|
||||
<p>For Penpot to recognize the property as a boolean and display the toggle, the property must be defined with exactly two opposing values. These can be any of the following pairs:</p>
|
||||
<ul>
|
||||
<li><code>true</code> / <code>false</code></li>
|
||||
<li><code>on</code> / <code>off</code></li>
|
||||
<li><code>yes</code> / <code>no</code></li>
|
||||
</ul>
|
||||
<p>The order of the values does not matter. Penpot automatically maps them to ON and OFF states:</p>
|
||||
<ul>
|
||||
<li><strong>ON state:</strong> <code>true</code>, <code>yes</code>, <code>on</code></li>
|
||||
<li><strong>OFF state:</strong> <code>false</code>, <code>no</code>, <code>off</code></li>
|
||||
</ul>
|
||||
|
||||
<h3 id="component-use-variants">Use variants</h3>
|
||||
<p>Once you have created your variants, you can place a copy of a component with variants into your design and then switch between its different versions.</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>Get to know the Workspace, where designs are created</p>
|
||||
<p>Workspace basics</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/layers">
|
||||
<h2>Layers →</h2>
|
||||
<p>Objects available in Penpot and how to get the most of them</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/color-stroke/">
|
||||
<h2>Color & Strokes→</h2>
|
||||
<p>Styling options available for each layer</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/text-typo">
|
||||
<h2>Text & Typography→</h2>
|
||||
<p>Styling text content & using custom fonts</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/flexible-layouts">
|
||||
<h2>Flexible layouts →</h2>
|
||||
<p>Create designs that adapt automatically</p>
|
||||
<p>Info of interest about Penpot</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>How to export and import your Penpot files</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/export-import/exporting-layers/">
|
||||
<h2>Exporting layers →</h2>
|
||||
<p>How to export elements from your design into different file formats</p>
|
||||
<p>Exporting layers</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>Take a tour of Penpot's main areas</p>
|
||||
<p>Info of interest about Penpot</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>Useful resources to better understand Penpot</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -22,49 +22,49 @@ eleventyNavigation:
|
||||
<li>
|
||||
<a href="/user-guide/designing/layers/">
|
||||
<h2>Layers</h2>
|
||||
<p>Objects available in Penpot and how to get the most of them</p>
|
||||
<p>Ways to start with Penpot</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>Speed your design workflow with reusable components</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/variants/">
|
||||
<h2>Variants</h2>
|
||||
<p>Group components into a single, customizable one</p>
|
||||
<p>Penpot's main areas and features</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/design-tokens/">
|
||||
<h2>Design Tokens</h2>
|
||||
<p>Synchronize visual elements across your designs</p>
|
||||
<p>Penpot's main areas and features</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/dev-tools/#inspect-design">
|
||||
<h2>Inspect design</h2>
|
||||
<p>Get production-ready code</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/prototyping/">
|
||||
<h2>Prototyping</h2>
|
||||
<p>Build interactive prototypes to mimic your product behaviour</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/libraries/">
|
||||
<h2>Libraries</h2>
|
||||
<p>Organize and manage your stored elements with Libraries</p>
|
||||
<p>Ways to start with Penpot</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>Build interactive prototypes to mimic your product behaviour</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/testing-view-mode">
|
||||
<h2>Testing: View mode →</h2>
|
||||
<p>Test your designs and play the interactions</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -18,15 +18,4 @@ cp ../.yarnrc.yml target/;
|
||||
cp yarn.lock target/;
|
||||
cp package.json target/;
|
||||
|
||||
cat <<EOF | tee target/setup
|
||||
#/usr/bin/env bash
|
||||
set -e;
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install
|
||||
yarn run playwright install chromium;
|
||||
EOF
|
||||
|
||||
chmod +x target/setup;
|
||||
|
||||
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/app.js;
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"build:storybook:cljs": "clojure -M:dev:shadow-cljs compile storybook",
|
||||
"build:app:libs": "node ./scripts/build-libs.js",
|
||||
"build:app:main": "clojure -M:dev:shadow-cljs release main worker",
|
||||
"build:app:worker": "clojure -M:dev:shadow-cljs release worker",
|
||||
"build:app": "yarn run clear:shadow-cache && yarn run build:app:main && yarn run build:app:libs",
|
||||
"e2e:server": "node ./scripts/e2e-server.js",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
@@ -45,9 +44,9 @@
|
||||
"translations": "node ./scripts/translations.js",
|
||||
"watch:app:assets": "node ./scripts/watch.js",
|
||||
"watch:app:libs": "node ./scripts/build-libs.js --watch",
|
||||
"watch:app:main": "clojure -M:dev:shadow-cljs watch main storybook",
|
||||
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
|
||||
"clear:shadow-cache": "rm -rf .shadow-cljs",
|
||||
"watch:app": "yarn run clear:shadow-cache && yarn run build:app:worker && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
|
||||
"watch:app": "yarn run clear:shadow-cache && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
|
||||
"watch": "yarn run watch:app:assets",
|
||||
"watch:storybook": "yarn run build:storybook:assets && concurrently \"storybook dev -p 6006 --no-open\" \"yarn run watch:storybook:assets\"",
|
||||
"watch:storybook:assets": "node ./scripts/watch-storybook.js"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"~: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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
{
|
||||
"~: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 +1,5 @@
|
||||
w
|
||||
{
|
||||
"~:revn": 2,
|
||||
"~:lagged": []
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"~:revn": 2,
|
||||
"~:lagged": []
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
[
|
||||
{
|
||||
"~:id": "~u088df3d4-d383-80f6-8004-527e50ea4f1f",
|
||||
"~:revn": 21,
|
||||
"~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||
"~:session-id": "~u1dc6d4fa-7bd3-803a-8004-527dd9df2c62",
|
||||
"~:changes": []
|
||||
}
|
||||
]
|
||||
@@ -1,36 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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,27 +1,4 @@
|
||||
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.");
|
||||
@@ -116,10 +93,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export class WasmWorkspacePage extends WorkspacePage {
|
||||
}
|
||||
|
||||
async waitForFirstRenderWithoutUI() {
|
||||
await this.waitForFirstRender();
|
||||
await waitForFirstRender();
|
||||
await this.hideUI();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,146 +1,7 @@
|
||||
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`.
|
||||
*
|
||||
@@ -150,21 +11,50 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
static async init(page) {
|
||||
await BaseWebSocketPage.initWebSockets(page);
|
||||
|
||||
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",
|
||||
});
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a";
|
||||
@@ -172,20 +62,9 @@ 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
|
||||
*
|
||||
* @param {Page} page
|
||||
* @param {} [options]
|
||||
*/
|
||||
constructor(page, options) {
|
||||
constructor(page) {
|
||||
super(page);
|
||||
this.pageName = page.getByTestId("page-name");
|
||||
|
||||
@@ -233,14 +112,11 @@ 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 = this.fileId ?? WorkspacePage.anyFileId,
|
||||
pageId = this.pageId ?? WorkspacePage.anyPageId,
|
||||
fileId = WorkspacePage.anyFileId,
|
||||
pageId = WorkspacePage.anyPageId,
|
||||
} = {}) {
|
||||
await this.page.goto(
|
||||
`/#/workspace?team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`,
|
||||
@@ -265,59 +141,48 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
}
|
||||
|
||||
async setupEmptyFile() {
|
||||
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");
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
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 mockGetFile(jsonFile) {
|
||||
await this.mockRPC(/get\-file\?/, jsonFile);
|
||||
}
|
||||
|
||||
async mockGetAsset(regex, asset) {
|
||||
@@ -325,15 +190,22 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
}
|
||||
|
||||
async setupFileWithComments() {
|
||||
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",
|
||||
});
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
async clickWithDragViewportAt(x, y, width, height) {
|
||||
@@ -351,67 +223,6 @@ 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 } });
|
||||
@@ -439,15 +250,10 @@ 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();
|
||||
|
||||
@@ -258,22 +258,6 @@ test("Renders a file with nested frames with inherited blur", async ({
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with nested clipping frames", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile(
|
||||
"render-wasm/get-file-frame-nested-clipping.json",
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "44471494-966a-8178-8006-c5bd93f0fe72",
|
||||
pageId: "44471494-966a-8178-8006-c5bd93f0fe73",
|
||||
});
|
||||
await workspace.waitForFirstRenderWithoutUI();
|
||||
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a clipped frame with a large blur drop shadow", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Binary file not shown.
|
Before 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.waitForFirstRenderWithoutUI();
|
||||
await workspace.waitForFirstRender();
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
|
||||
@@ -18,10 +18,6 @@ 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 = {
|
||||
|
||||
@@ -9,399 +9,403 @@ test.beforeEach(async ({ page }) => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("Team with unlimited subscription has specific icon in menu", async ({
|
||||
page,
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
test.describe("Subscriptions: dashboard", () => {
|
||||
test("Team with unlimited subscription has specific icon in menu", async ({
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-unlimited-subscription.json",
|
||||
);
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-unlimited-subscription.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-projects?team-id=*",
|
||||
"dashboard/get-projects-second-team.json",
|
||||
);
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
await dashboardPage.goToSecondTeamDashboard();
|
||||
await expect(page.getByTestId("subscription-icon")).toBeVisible();
|
||||
});
|
||||
|
||||
test("The Unlimited subscription has its name in the sidebar dropdown", async ({
|
||||
page,
|
||||
"get-projects?team-id=*",
|
||||
"dashboard/get-projects-second-team.json",
|
||||
);
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
await dashboardPage.goToSecondTeamDashboard();
|
||||
await expect(page.getByTestId("subscription-icon")).toBeVisible();
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-unlimited-subscription.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage-one-editor.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
await dashboardPage.goToDashboard();
|
||||
|
||||
await expect(page.getByTestId("subscription-name")).toHaveText(
|
||||
"Unlimited plan (trial)",
|
||||
);
|
||||
});
|
||||
|
||||
test("When the subscription status is unpaid, the sidebar dropdown displays the name Professional for the Unlimited subscription", async ({
|
||||
page,
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-unlimited-unpaid-subscription.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
await dashboardPage.goToDashboard();
|
||||
|
||||
await expect(page.getByTestId("subscription-name")).toHaveText(
|
||||
"Professional plan",
|
||||
);
|
||||
});
|
||||
|
||||
test("When the subscription status is canceled, the sidebar dropdown displays the name Professional for the Enterprise subscription", async ({
|
||||
page,
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-enterprise-canceled-subscription.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
await dashboardPage.goToDashboard();
|
||||
|
||||
await expect(page.getByTestId("subscription-name")).toHaveText(
|
||||
"Professional plan",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("The Unlimited subscription has its name in the sidebar dropdown", async ({
|
||||
page,
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
test.describe("Subscriptions: team members and invitations", () => {
|
||||
test("Team settings has susbscription name and no manage subscription link when is member", async ({
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-unlimited-subscription.json",
|
||||
);
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"logged-in-user/get-profile-logged-in.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-member.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-projects?team-id=*",
|
||||
"dashboard/get-projects-second-team.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-members?team-id=*",
|
||||
"subscription/get-team-members-subscription-member.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-stats?team-id=*",
|
||||
"dashboard/get-team-stats.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
|
||||
await dashboardPage.goToSecondTeamSettingsSection();
|
||||
await expect(page.getByText("Unlimited (trial)")).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Manage your subscription" }),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("Team settings has susbscription name and manage subscription link when is owner", async ({
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage-one-editor.json",
|
||||
);
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-unlimited-subscription.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-projects?team-id=*",
|
||||
"dashboard/get-projects-second-team.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-members?team-id=*",
|
||||
"subscription/get-team-members-subscription-owner.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-stats?team-id=*",
|
||||
"dashboard/get-team-stats.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
|
||||
await dashboardPage.goToSecondTeamSettingsSection();
|
||||
|
||||
await expect(page.getByText("Unlimited (trial)")).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Manage your subscription" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Members tab has warning message when user has more seats than editors.", async ({
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-unlimited-subscription.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-projects?team-id=*",
|
||||
"dashboard/get-projects-second-team.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-members?team-id=*",
|
||||
"subscription/get-team-members-subscription-eight-member.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
|
||||
await dashboardPage.goToSecondTeamMembersSection();
|
||||
|
||||
const ctas = page.getByTestId("cta");
|
||||
await expect(ctas).toHaveCount(2);
|
||||
await expect(
|
||||
page.getByText("Inviting people while on the unlimited plan"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Invitations tab has warning message when user has more seats than editors.", async ({
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-unlimited-subscription.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
await dashboardPage.goToDashboard();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
|
||||
await expect(page.getByTestId("subscription-name")).toHaveText(
|
||||
"Unlimited plan (trial)",
|
||||
);
|
||||
});
|
||||
|
||||
test("The sidebar dropdown displays the correct subscription name when status is Unpaid", async ({
|
||||
page,
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-unlimited-unpaid-subscription.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
await dashboardPage.goToDashboard();
|
||||
|
||||
await expect(page.getByTestId("subscription-name")).toHaveText(
|
||||
"Professional plan",
|
||||
);
|
||||
});
|
||||
|
||||
test("The sidebar dropdown displays the correct subscription name when status is cancelled", async ({
|
||||
page,
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-enterprise-canceled-subscription.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
await dashboardPage.goToDashboard();
|
||||
|
||||
await expect(page.getByTestId("subscription-name")).toHaveText(
|
||||
"Professional plan",
|
||||
);
|
||||
});
|
||||
|
||||
test("Team settings has susbscription name and no manage subscription link when is member", async ({
|
||||
page,
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"logged-in-user/get-profile-logged-in.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-member.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-projects?team-id=*",
|
||||
"dashboard/get-projects-second-team.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-members?team-id=*",
|
||||
"subscription/get-team-members-subscription-member.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-stats?team-id=*",
|
||||
"dashboard/get-team-stats.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
|
||||
await dashboardPage.goToSecondTeamSettingsSection();
|
||||
await expect(page.getByText("Unlimited (trial)")).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Manage your subscription" }),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("Team settings has susbscription name and manage subscription link when is owner", async ({
|
||||
page,
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-unlimited-subscription.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-projects?team-id=*",
|
||||
"dashboard/get-projects-second-team.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-members?team-id=*",
|
||||
"subscription/get-team-members-subscription-owner.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-stats?team-id=*",
|
||||
"dashboard/get-team-stats.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
|
||||
await dashboardPage.goToSecondTeamSettingsSection();
|
||||
|
||||
await expect(page.getByText("Unlimited (trial)")).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Manage your subscription" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Members tab has warning message when user has more seats than editors", async ({
|
||||
page,
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-unlimited-subscription.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-projects?team-id=*",
|
||||
"dashboard/get-projects-second-team.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-members?team-id=*",
|
||||
"subscription/get-team-members-subscription-eight-member.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
|
||||
await dashboardPage.goToSecondTeamMembersSection();
|
||||
|
||||
const ctas = page.getByTestId("cta");
|
||||
await expect(ctas).toHaveCount(2);
|
||||
await expect(
|
||||
page.getByText("Inviting people while on the unlimited plan"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Invitations tab has warning message when user has more seats than editors", async ({
|
||||
page,
|
||||
}) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"subscription/get-profile-unlimited-subscription.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-subscription-usage",
|
||||
"subscription/get-subscription-usage.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-projects?team-id=*",
|
||||
"dashboard/get-projects-second-team.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-members?team-id=*",
|
||||
"subscription/get-team-members-subscription-eight-member.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-invitations?team-id=*",
|
||||
"subscription/get-team-invitations.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
|
||||
await dashboardPage.goToSecondTeamInvitationsSection();
|
||||
|
||||
const ctas = page.getByTestId("cta");
|
||||
await expect(ctas).toHaveCount(2);
|
||||
await expect(
|
||||
page.getByText("Inviting people while on the unlimited plan"),
|
||||
).toBeVisible();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-info",
|
||||
"subscription/get-team-info-subscriptions.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"subscription/get-teams-unlimited-subscription-owner.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-projects?team-id=*",
|
||||
"dashboard/get-projects-second-team.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-members?team-id=*",
|
||||
"subscription/get-team-members-subscription-eight-member.json",
|
||||
);
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-team-invitations?team-id=*",
|
||||
"subscription/get-team-invitations.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"push-audit-events",
|
||||
"workspace/audit-event-empty.json",
|
||||
);
|
||||
|
||||
await dashboardPage.goToSecondTeamInvitationsSection();
|
||||
|
||||
const ctas = page.getByTestId("cta");
|
||||
await expect(ctas).toHaveCount(2);
|
||||
await expect(
|
||||
page.getByText("Inviting people while on the unlimited plan"),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,317 +1,12 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { Clipboard } from '../../helpers/Clipboard';
|
||||
import { WorkspacePage } from "../pages/WorkspacePage";
|
||||
|
||||
const timeToWait = 100;
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
await Clipboard.enable(context, Clipboard.Permission.ONLY_WRITE);
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WorkspacePage.init(page);
|
||||
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
|
||||
});
|
||||
|
||||
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 }) => {
|
||||
test.skip("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");
|
||||
@@ -319,16 +14,21 @@ test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
||||
"update-file?id=*",
|
||||
"text-editor/update-file-11552.json",
|
||||
);
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.doubleClickLeafLayer("Lorem ipsum");
|
||||
|
||||
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");
|
||||
|
||||
const fontSizeInput = workspace.rightSidebar.getByRole("textbox", {
|
||||
name: "Font Size",
|
||||
});
|
||||
await expect(fontSizeInput).toBeVisible();
|
||||
|
||||
await page.keyboard.press("Enter");
|
||||
await page.keyboard.press("ArrowRight");
|
||||
await workspace.page.keyboard.press("Enter");
|
||||
await workspace.page.keyboard.press("ArrowRight");
|
||||
|
||||
await fontSizeInput.fill("36");
|
||||
|
||||
|
||||
@@ -499,7 +499,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
await valueField.fill("");
|
||||
// TODO: We need to fix this translation
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Token value cannot be empty"),
|
||||
tokensUpdateCreateModal.getByText("Empty field"),
|
||||
).toBeVisible();
|
||||
await valueSaturationSelector.click({ position: { x: 50, y: 50 } });
|
||||
await expect(valueField).toHaveValue(/^#[A-Fa-f\d]+$/);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 821 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 676 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 97 KiB |
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
|
||||
yarn run lint:scss;
|
||||
yarn run test;
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run build:storybook
|
||||
|
||||
exec npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
|
||||
"npx http-server storybook-static --port 6006 --silent" \
|
||||
"npx wait-on tcp:6006 && yarn test:storybook"
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list "$@";
|
||||
@@ -76,7 +76,7 @@
|
||||
(map :page-id))
|
||||
|
||||
(defn- apply-changes-localy
|
||||
[{:keys [file-id redo-changes ignore-wasm?] :as commit} pending]
|
||||
[{:keys [file-id redo-changes] :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 (and (not ignore-wasm?) (features/active-feature? state "render-wasm/v1"))
|
||||
(if (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 ignore-wasm?]}]
|
||||
file-id file-revn file-vern undo-group tags stack-undo? source]}]
|
||||
|
||||
(assert (cpc/check-changes redo-changes)
|
||||
"expect valid vector of changes for redo-changes")
|
||||
@@ -147,8 +147,7 @@
|
||||
:save-undo? save-undo?
|
||||
:undo-group undo-group
|
||||
:tags tags
|
||||
:stack-undo? stack-undo?
|
||||
:ignore-wasm? ignore-wasm?}]
|
||||
:stack-undo? stack-undo?}]
|
||||
|
||||
(ptk/reify ::commit
|
||||
cljs.core/IDeref
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
[]
|
||||
(let [uagent (new ua/UAParser)]
|
||||
(merge
|
||||
{:version (:full cf/version)
|
||||
{:app-version (:full cf/version)
|
||||
:locale @i18n/locale}
|
||||
(let [browser (.getBrowser uagent)]
|
||||
{:browser (obj/get browser "name")
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
[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]
|
||||
@@ -379,23 +377,6 @@
|
||||
(->> (rx/from added)
|
||||
(rx/map process-wasm-object)))))))
|
||||
|
||||
(when render-wasm?
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? :wasm/position-data))
|
||||
(rx/map deref)
|
||||
(rx/filter
|
||||
(fn [{:keys [position-data]}]
|
||||
(some? position-data)))
|
||||
(rx/map
|
||||
(fn [{:keys [id position-data]}]
|
||||
(prn "???" id position-data)
|
||||
(dwsh/update-shapes
|
||||
[id]
|
||||
(fn [shape]
|
||||
(.log js/console (clj->js shape))
|
||||
(assoc shape :position-data position-data))
|
||||
{:ignore-wasm? true})))))
|
||||
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
@@ -570,6 +551,7 @@
|
||||
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
|
||||
@@ -584,43 +566,6 @@
|
||||
(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,8 +102,7 @@
|
||||
{:origin it
|
||||
:redo-changes changes
|
||||
:undo-changes []
|
||||
:save-undo? false
|
||||
:ignore-wasm? true})))))))
|
||||
:save-undo? false})))))))
|
||||
|
||||
;; FIXME: would be nice to not execute this code twice per page in the
|
||||
;; same working session, maybe some local memoization can improve that
|
||||
@@ -120,5 +119,4 @@
|
||||
{:origin it
|
||||
:redo-changes changes
|
||||
:undo-changes []
|
||||
:save-undo? false
|
||||
:ignore-wasm? true})))))))
|
||||
:save-undo? false})))))))
|
||||
|
||||
@@ -649,7 +649,7 @@
|
||||
(propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))
|
||||
|
||||
ids
|
||||
(into (set (keys modif-tree)) xf:without-uuid-zero (keys transforms))
|
||||
(into [] xf:without-uuid-zero (keys transforms))
|
||||
|
||||
update-shape
|
||||
(fn [shape]
|
||||
|
||||
@@ -50,8 +50,7 @@
|
||||
([ids update-fn] (update-shapes ids update-fn nil))
|
||||
([ids update-fn
|
||||
{:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id
|
||||
ignore-touched undo-group with-objects? changed-sub-attr
|
||||
ignore-wasm?]
|
||||
ignore-touched undo-group with-objects? changed-sub-attr]
|
||||
:or {reg-objects? false
|
||||
save-undo? true
|
||||
stack-undo? false
|
||||
@@ -90,7 +89,6 @@
|
||||
:ignore-tree ignore-tree
|
||||
:ignore-touched ignore-touched
|
||||
:with-objects? with-objects?})
|
||||
(assoc :ignore-wasm? ignore-wasm?)
|
||||
(cond-> undo-group
|
||||
(pcb/set-undo-group undo-group)))
|
||||
|
||||
|
||||
@@ -831,8 +831,7 @@
|
||||
(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))))))
|
||||
|
||||
@@ -128,16 +128,14 @@
|
||||
related-components (cfv/find-variant-components data objects variant-id)
|
||||
|
||||
props (-> related-components last :variant-properties)
|
||||
valid-pos? (> (count props) pos)
|
||||
prop-name (when valid-pos? (-> props (nth pos) :name))
|
||||
prop-name (-> props (nth pos) :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)))
|
||||
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))
|
||||
undo-id (js/Symbol)]
|
||||
(when (and valid-pos? (not= prop-name new-name))
|
||||
(when (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.11.28.json"))
|
||||
(preload-gfonts "fonts/gfonts.2025.05.19.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 :font-weight :font-style])
|
||||
(select-keys txt/default-typography [:font-id :font-variant-id :font-weight :font-style]))]
|
||||
(select-keys node [:font-id :font-variant-id])
|
||||
(select-keys txt/default-typography [:font-id :font-variant-id]))]
|
||||
(conj result current-font)))
|
||||
#{})))
|
||||
|
||||
|
||||
@@ -372,9 +372,6 @@
|
||||
(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,7 +14,6 @@
|
||||
[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]
|
||||
@@ -54,13 +53,14 @@
|
||||
(def ^:private valid-option?
|
||||
(sm/lazy-validator schema:option))
|
||||
|
||||
(mf/defc context-menu-inner*
|
||||
[{:keys [on-close options selectable selected
|
||||
(mf/defc context-menu*
|
||||
[{:keys [show 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,15 +80,14 @@
|
||||
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}])
|
||||
(when (fn? on-close)
|
||||
(on-close))))
|
||||
(swap! state* assoc :levels [{:parent nil
|
||||
:options options}])
|
||||
(on-close)))
|
||||
|
||||
props
|
||||
(mf/spread-props props {:on-close on-local-close})
|
||||
@@ -217,22 +216,11 @@
|
||||
(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 (some? levels)
|
||||
(when (and show (some? levels))
|
||||
[:> dropdown-content* props
|
||||
(let [level (peek levels)
|
||||
options (:options level)
|
||||
@@ -241,7 +229,7 @@
|
||||
[:div {:class (stl/css-case
|
||||
:is-selectable selectable
|
||||
:context-menu true
|
||||
:is-open true
|
||||
:is-open show
|
||||
:fixed fixed)
|
||||
:style {:top (+ top offset-y)
|
||||
:left (+ left offset-x)}
|
||||
@@ -253,7 +241,7 @@
|
||||
:role "menu"
|
||||
:ref check-menu-offscreen}
|
||||
|
||||
(when parent
|
||||
(when-let [parent (:parent level)]
|
||||
[:*
|
||||
[:li {:id "go-back-sub-option"
|
||||
:class (stl/css :context-menu-item)
|
||||
@@ -268,7 +256,7 @@
|
||||
|
||||
[:li {:class (stl/css :separator)}]])
|
||||
|
||||
(for [[index option] (d/enumerate options)]
|
||||
(for [[index option] (d/enumerate (:options level))]
|
||||
(let [name (:name option)
|
||||
id (:id option)
|
||||
sub-options (:options option)
|
||||
@@ -309,12 +297,3 @@
|
||||
: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,7 +30,6 @@
|
||||
(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,16 +151,14 @@
|
||||
|
||||
(mf/defc menu-team-icon*
|
||||
[{:keys [subscription-type]}]
|
||||
[: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"}]])
|
||||
[: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)])
|
||||
|
||||
(mf/defc main-menu-power-up*
|
||||
[{:keys [close-sub-menu]}]
|
||||
|
||||
@@ -144,20 +144,20 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.subscription-icon-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--color-background-primary);
|
||||
border-radius: $br-6;
|
||||
border: 1.75px solid var(--color-foreground-secondary);
|
||||
block-size: var(--sp-xl);
|
||||
inline-size: var(--sp-xl);
|
||||
}
|
||||
|
||||
.subscription-icon {
|
||||
@extend .button-icon;
|
||||
background: var(--color-background-primary);
|
||||
stroke: var(--color-foreground-secondary);
|
||||
border-radius: 6px;
|
||||
border: 1.75px solid var(--color-foreground-secondary);
|
||||
stroke-width: 2.25px;
|
||||
width: var(--sp-xl);
|
||||
height: var(--sp-xl);
|
||||
|
||||
svg {
|
||||
block-size: var(--sp-m);
|
||||
inline-size: var(--sp-m);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
|
||||
@@ -106,14 +106,14 @@
|
||||
(when (not= 0 count-libraries)
|
||||
(if (pos? (count references))
|
||||
[:*
|
||||
(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]])]
|
||||
[: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? hint) (not= hint ""))
|
||||
[:> context-notification* {:level :info
|
||||
:appearance :ghost}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "refactor/basic-rules.scss" as *;
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
@@ -16,19 +15,14 @@
|
||||
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
display: grid;
|
||||
gap: var(--sp-xxl);
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
max-height: 100%;
|
||||
.modal-header {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include t.use-typography("headline-medium");
|
||||
@include deprecated.headlineMediumTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
@@ -37,16 +31,13 @@
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include t.use-typography("body-small");
|
||||
display: grid;
|
||||
gap: var(--sp-s);
|
||||
@include deprecated.bodySmallTypography;
|
||||
margin-bottom: deprecated.$s-24;
|
||||
}
|
||||
|
||||
.element-list {
|
||||
@include t.use-typography("body-large");
|
||||
@include deprecated.bodyLargeTypography;
|
||||
color: var(--modal-text-foreground-color);
|
||||
overflow-y: scroll;
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@@ -64,14 +55,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal-scd-msg {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.modal-scd-msg,
|
||||
.modal-subtitle,
|
||||
.modal-msg {
|
||||
@include t.use-typography("body-large");
|
||||
@include deprecated.bodyLargeTypography;
|
||||
color: var(--modal-text-foreground-color);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
[:id {:optional true} :string]
|
||||
[:offset {:optional true} :int]
|
||||
[:delay {:optional true} :int]
|
||||
[:content [:or fn? :string map?]]
|
||||
[:content [:or fn? :string]]
|
||||
[:placement {:optional true}
|
||||
[:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]]])
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
(ns app.main.ui.flex-controls.gap
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
@@ -17,8 +16,6 @@
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.css-cursors :as cur]
|
||||
@@ -30,11 +27,10 @@
|
||||
(mf/defc gap-display
|
||||
[{:keys [frame-id zoom gap-type gap on-pointer-enter on-pointer-leave
|
||||
rect-data hover? selected? mouse-pos hover-value
|
||||
on-move-selected on-context-menu on-change]}]
|
||||
on-move-selected on-context-menu]}]
|
||||
(let [resizing (mf/use-var nil)
|
||||
start (mf/use-var nil)
|
||||
original-value (mf/use-var 0)
|
||||
last-pos (mf/use-var nil)
|
||||
negate? (:resize-negate? rect-data)
|
||||
axis (:resize-axis rect-data)
|
||||
|
||||
@@ -47,55 +43,32 @@
|
||||
(reset! start (dom/get-client-position event))
|
||||
(reset! original-value (:initial-value rect-data))))
|
||||
|
||||
calc-modifiers
|
||||
(mf/use-fn
|
||||
(mf/deps frame-id gap-type gap)
|
||||
(fn [pos]
|
||||
(let [delta
|
||||
(-> (gpt/to-vec @start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
val
|
||||
(int (max (+ @original-value (/ delta zoom)) 0))
|
||||
|
||||
layout-gap (assoc gap gap-type val)]
|
||||
[val
|
||||
(dwm/create-modif-tree
|
||||
[frame-id]
|
||||
(ctm/change-property (ctm/empty) :layout-gap layout-gap))])))
|
||||
|
||||
on-lost-pointer-capture
|
||||
(mf/use-fn
|
||||
(mf/deps calc-modifiers)
|
||||
(mf/deps frame-id gap-type gap)
|
||||
(fn [event]
|
||||
(dom/release-pointer event)
|
||||
|
||||
(when (and (features/active-feature? @st/state "render-wasm/v1") (= @resizing gap-type))
|
||||
(let [[_ modifiers] (calc-modifiers @last-pos)]
|
||||
(st/emit! (dwm/apply-wasm-modifiers modifiers)
|
||||
(dwt/finish-transform))))
|
||||
|
||||
(reset! resizing nil)
|
||||
(reset! start nil)
|
||||
(reset! original-value 0)
|
||||
(when (not (features/active-feature? @st/state "render-wasm/v1"))
|
||||
(st/emit! (dwm/apply-modifiers)))))
|
||||
(st/emit! (dwm/apply-modifiers))))
|
||||
|
||||
on-pointer-move
|
||||
(mf/use-fn
|
||||
(mf/deps calc-modifiers on-change)
|
||||
(mf/deps frame-id gap-type gap)
|
||||
(fn [event]
|
||||
(let [pos (dom/get-client-position event)]
|
||||
(reset! last-pos pos)
|
||||
(reset! mouse-pos (point->viewport pos))
|
||||
(when (= @resizing gap-type)
|
||||
(let [[val modifiers] (calc-modifiers pos)]
|
||||
(let [delta (-> (gpt/to-vec @start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
val (int (max (+ @original-value (/ delta zoom)) 0))
|
||||
layout-gap (assoc gap gap-type val)
|
||||
modifiers (dwm/create-modif-tree [frame-id] (ctm/change-property (ctm/empty) :layout-gap layout-gap))]
|
||||
|
||||
(reset! hover-value val)
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(st/emit! (dwm/set-wasm-modifiers modifiers))
|
||||
(st/emit! (dwm/set-modifiers modifiers)))
|
||||
(when on-change
|
||||
(on-change modifiers)))))))]
|
||||
(st/emit! (dwm/set-modifiers modifiers)))))))]
|
||||
|
||||
[:g.gap-rect
|
||||
[:rect.info-area
|
||||
@@ -147,17 +120,10 @@
|
||||
pill-width (/ fcc/flex-display-pill-width zoom)
|
||||
pill-height (/ fcc/flex-display-pill-height zoom)
|
||||
workspace-modifiers (mf/deref refs/workspace-modifiers)
|
||||
workspace-wasm-modifiers (mf/deref refs/workspace-wasm-modifiers)
|
||||
|
||||
gap-selected (mf/deref refs/workspace-gap-selected)
|
||||
hover (mf/use-state nil)
|
||||
hover-value (mf/use-state 0)
|
||||
mouse-pos (mf/use-state nil)
|
||||
current-modifiers (mf/use-state nil)
|
||||
|
||||
frame
|
||||
(ctm/apply-structure-modifiers frame (dm/get-in @current-modifiers [frame-id :modifiers]))
|
||||
|
||||
padding (:layout-padding frame)
|
||||
gap (:layout-gap frame)
|
||||
{:keys [width height x1 y1]} (:selrect frame)
|
||||
@@ -166,12 +132,6 @@
|
||||
(reset! hover-value val))
|
||||
|
||||
on-pointer-leave #(reset! hover nil)
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(fn [modifiers]
|
||||
(reset! current-modifiers modifiers)))
|
||||
|
||||
negate {:column-gap (if flip-x true false)
|
||||
:row-gap (if flip-y true false)}
|
||||
|
||||
@@ -183,16 +143,8 @@
|
||||
(= :column-reverse saved-dir))
|
||||
(drop-last children)
|
||||
(rest children))
|
||||
children-to-display
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(let [modifiers (into {} workspace-wasm-modifiers)]
|
||||
(->> children-to-display
|
||||
;;(map #(gsh/transform-shape % (get-in workspace-modifiers [(:id %) :modifiers])))
|
||||
(map (fn [shape]
|
||||
(gsh/apply-transform shape (get modifiers (:id shape)))))))
|
||||
|
||||
(->> children-to-display
|
||||
(map #(gsh/transform-shape % (get-in workspace-modifiers [(:id %) :modifiers])))))
|
||||
children-to-display (->> children-to-display
|
||||
(map #(gsh/transform-shape % (get-in workspace-modifiers [(:id %) :modifiers]))))
|
||||
|
||||
wrap-blocks
|
||||
(let [block-children (->> children
|
||||
@@ -320,22 +272,20 @@
|
||||
[:g.gaps {:pointer-events "visible"}
|
||||
(for [[index display-item] (d/enumerate (concat display-blocks display-children))]
|
||||
(let [gap-type (:gap-type display-item)]
|
||||
[:& gap-display
|
||||
{:key (str frame-id index)
|
||||
:frame-id frame-id
|
||||
:zoom zoom
|
||||
:gap-type gap-type
|
||||
:gap gap
|
||||
:on-pointer-enter (partial on-pointer-enter gap-type (get gap gap-type))
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-move-selected on-move-selected
|
||||
:on-context-menu on-context-menu
|
||||
:on-change on-change
|
||||
:rect-data display-item
|
||||
:hover? (= @hover gap-type)
|
||||
:selected? (= gap-selected gap-type)
|
||||
:mouse-pos mouse-pos
|
||||
:hover-value hover-value}]))
|
||||
[:& gap-display {:key (str frame-id index)
|
||||
:frame-id frame-id
|
||||
:zoom zoom
|
||||
:gap-type gap-type
|
||||
:gap gap
|
||||
:on-pointer-enter (partial on-pointer-enter gap-type (get gap gap-type))
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-move-selected on-move-selected
|
||||
:on-context-menu on-context-menu
|
||||
:rect-data display-item
|
||||
:hover? (= @hover gap-type)
|
||||
:selected? (= gap-selected gap-type)
|
||||
:mouse-pos mouse-pos
|
||||
:hover-value hover-value}]))
|
||||
|
||||
(when @hover
|
||||
[:& fcc/flex-display-pill
|
||||
|
||||
@@ -6,12 +6,9 @@
|
||||
|
||||
(ns app.main.ui.flex-controls.margin
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.css-cursors :as cur]
|
||||
@@ -20,14 +17,11 @@
|
||||
[app.util.dom :as dom]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc margin-display
|
||||
[{:keys [shape-id zoom hover-all? hover-v? hover-h? margin-num margin
|
||||
on-pointer-enter on-pointer-leave on-change
|
||||
rect-data hover? selected? mouse-pos hover-value]}]
|
||||
(mf/defc margin-display [{:keys [shape-id zoom hover-all? hover-v? hover-h? margin-num margin on-pointer-enter on-pointer-leave
|
||||
rect-data hover? selected? mouse-pos hover-value]}]
|
||||
(let [resizing? (mf/use-var false)
|
||||
start (mf/use-var nil)
|
||||
original-value (mf/use-var 0)
|
||||
last-pos (mf/use-var nil)
|
||||
negate? (true? (:resize-negate? rect-data))
|
||||
axis (:resize-axis rect-data)
|
||||
|
||||
@@ -40,69 +34,39 @@
|
||||
(reset! start (dom/get-client-position event))
|
||||
(reset! original-value (:initial-value rect-data))))
|
||||
|
||||
calc-modifiers
|
||||
(mf/use-fn
|
||||
(mf/deps shape-id margin-num margin hover-all? hover-v? hover-h?)
|
||||
(fn [pos]
|
||||
(let [delta
|
||||
(-> (gpt/to-vec @start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
|
||||
val
|
||||
(int (max (+ @original-value (/ delta zoom)) 0))
|
||||
|
||||
layout-item-margin
|
||||
(cond
|
||||
hover-all? (assoc margin :m1 val :m2 val :m3 val :m4 val)
|
||||
hover-v? (assoc margin :m1 val :m3 val)
|
||||
hover-h? (assoc margin :m2 val :m4 val)
|
||||
:else (assoc margin margin-num val))
|
||||
|
||||
layout-item-margin-type
|
||||
(if (= (:m1 margin) (:m2 margin) (:m3 margin) (:m4 margin)) :simple :multiple)]
|
||||
|
||||
[val
|
||||
(dwm/create-modif-tree
|
||||
[shape-id]
|
||||
(-> (ctm/empty)
|
||||
(ctm/change-property :layout-item-margin layout-item-margin)
|
||||
(ctm/change-property :layout-item-margin-type layout-item-margin-type)))])))
|
||||
|
||||
on-lost-pointer-capture
|
||||
(mf/use-fn
|
||||
(mf/deps calc-modifiers)
|
||||
(mf/deps shape-id margin-num margin)
|
||||
(fn [event]
|
||||
(dom/release-pointer event)
|
||||
|
||||
(when (features/active-feature? @st/state "render-wasm/v1")
|
||||
(let [[_ modifiers] (calc-modifiers @last-pos)]
|
||||
(st/emit! (dwm/apply-wasm-modifiers modifiers)
|
||||
(dwt/finish-transform))))
|
||||
|
||||
(reset! resizing? false)
|
||||
(reset! start nil)
|
||||
(reset! original-value 0)
|
||||
|
||||
(when (not (features/active-feature? @st/state "render-wasm/v1"))
|
||||
(st/emit! (dwm/apply-modifiers)))))
|
||||
(st/emit! (dwm/apply-modifiers))))
|
||||
|
||||
on-pointer-move
|
||||
(mf/use-fn
|
||||
(mf/deps calc-modifiers on-change)
|
||||
(mf/deps shape-id margin-num margin hover-all? hover-v? hover-h?)
|
||||
(fn [event]
|
||||
(let [pos (dom/get-client-position event)]
|
||||
(reset! mouse-pos (point->viewport pos))
|
||||
(reset! last-pos pos)
|
||||
(when @resizing?
|
||||
(let [[val modifiers] (calc-modifiers pos)]
|
||||
(let [delta (-> (gpt/to-vec @start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
val (int (max (+ @original-value (/ delta zoom)) 0))
|
||||
layout-item-margin (cond
|
||||
hover-all? (assoc margin :m1 val :m2 val :m3 val :m4 val)
|
||||
hover-v? (assoc margin :m1 val :m3 val)
|
||||
hover-h? (assoc margin :m2 val :m4 val)
|
||||
:else (assoc margin margin-num val))
|
||||
layout-item-margin-type (if (= (:m1 margin) (:m2 margin) (:m3 margin) (:m4 margin)) :simple :multiple)
|
||||
modifiers (dwm/create-modif-tree [shape-id]
|
||||
(-> (ctm/empty)
|
||||
(ctm/change-property :layout-item-margin layout-item-margin)
|
||||
(ctm/change-property :layout-item-margin-type layout-item-margin-type)))]
|
||||
(reset! hover-value val)
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(st/emit! (dwm/set-wasm-modifiers modifiers))
|
||||
(st/emit! (dwm/set-modifiers modifiers)))
|
||||
|
||||
(when on-change
|
||||
(on-change modifiers)))))))]
|
||||
(st/emit! (dwm/set-modifiers modifiers)))))))]
|
||||
|
||||
[:rect.margin-rect
|
||||
{:x (:x rect-data)
|
||||
@@ -125,11 +89,6 @@
|
||||
pill-width (/ fcc/flex-display-pill-width zoom)
|
||||
pill-height (/ fcc/flex-display-pill-height zoom)
|
||||
margins-selected (mf/deref refs/workspace-margins-selected)
|
||||
current-modifiers (mf/use-state nil)
|
||||
|
||||
shape
|
||||
(ctm/apply-structure-modifiers shape (dm/get-in @current-modifiers [shape-id :modifiers]))
|
||||
|
||||
hover-value (mf/use-state 0)
|
||||
mouse-pos (mf/use-state nil)
|
||||
hover (mf/use-state nil)
|
||||
@@ -138,67 +97,50 @@
|
||||
hover-h? (and (or (= @hover :m2) (= @hover :m4)) shift?)
|
||||
margin (:layout-item-margin shape)
|
||||
{:keys [width height x1 x2 y1 y2]} (:selrect shape)
|
||||
|
||||
on-pointer-enter
|
||||
(mf/use-fn
|
||||
(fn [hover-type val]
|
||||
(reset! hover hover-type)
|
||||
(reset! hover-value val)))
|
||||
|
||||
on-pointer-leave
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(reset! hover nil)))
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(fn [modifiers]
|
||||
(reset! current-modifiers modifiers)))
|
||||
|
||||
hover?
|
||||
(fn [value]
|
||||
(or hover-all?
|
||||
(and (or (= value :m1) (= value :m3)) hover-v?)
|
||||
(and (or (= value :m2) (= value :m4)) hover-h?)
|
||||
(= @hover value)))
|
||||
|
||||
margin-display-data
|
||||
{:m1 {:key (str shape-id "-m1")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) y2 (- y1 (:m1 margin)))
|
||||
:width width
|
||||
:height (:m1 margin)
|
||||
:initial-value (:m1 margin)
|
||||
:resize-type :top
|
||||
:resize-axis :y
|
||||
:resize-negate? (:flip-y frame)}
|
||||
:m2 {:key (str shape-id "-m2")
|
||||
:x (if (:flip-x frame) (- x1 (:m2 margin)) x2)
|
||||
:y y1
|
||||
:width (:m2 margin)
|
||||
:height height
|
||||
:initial-value (:m2 margin)
|
||||
:resize-type :left
|
||||
:resize-axis :x
|
||||
:resize-negate? (:flip-x frame)}
|
||||
:m3 {:key (str shape-id "-m3")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) (- y1 (:m3 margin)) y2)
|
||||
:width width
|
||||
:height (:m3 margin)
|
||||
:initial-value (:m3 margin)
|
||||
:resize-type :top
|
||||
:resize-axis :y
|
||||
:resize-negate? (:flip-y frame)}
|
||||
:m4 {:key (str shape-id "-m4")
|
||||
:x (if (:flip-x frame) x2 (- x1 (:m4 margin)))
|
||||
:y y1
|
||||
:width (:m4 margin)
|
||||
:height height
|
||||
:initial-value (:m4 margin)
|
||||
:resize-type :left
|
||||
:resize-axis :x
|
||||
:resize-negate? (:flip-x frame)}}]
|
||||
on-pointer-enter (fn [hover-type val]
|
||||
(reset! hover hover-type)
|
||||
(reset! hover-value val))
|
||||
on-pointer-leave #(reset! hover nil)
|
||||
hover? #(or hover-all?
|
||||
(and (or (= % :m1) (= % :m3)) hover-v?)
|
||||
(and (or (= % :m2) (= % :m4)) hover-h?)
|
||||
(= @hover %))
|
||||
margin-display-data {:m1 {:key (str shape-id "-m1")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) y2 (- y1 (:m1 margin)))
|
||||
:width width
|
||||
:height (:m1 margin)
|
||||
:initial-value (:m1 margin)
|
||||
:resize-type :top
|
||||
:resize-axis :y
|
||||
:resize-negate? (:flip-y frame)}
|
||||
:m2 {:key (str shape-id "-m2")
|
||||
:x (if (:flip-x frame) (- x1 (:m2 margin)) x2)
|
||||
:y y1
|
||||
:width (:m2 margin)
|
||||
:height height
|
||||
:initial-value (:m2 margin)
|
||||
:resize-type :left
|
||||
:resize-axis :x
|
||||
:resize-negate? (:flip-x frame)}
|
||||
:m3 {:key (str shape-id "-m3")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) (- y1 (:m3 margin)) y2)
|
||||
:width width
|
||||
:height (:m3 margin)
|
||||
:initial-value (:m3 margin)
|
||||
:resize-type :top
|
||||
:resize-axis :y
|
||||
:resize-negate? (:flip-y frame)}
|
||||
:m4 {:key (str shape-id "-m4")
|
||||
:x (if (:flip-x frame) x2 (- x1 (:m4 margin)))
|
||||
:y y1
|
||||
:width (:m4 margin)
|
||||
:height height
|
||||
:initial-value (:m4 margin)
|
||||
:resize-type :left
|
||||
:resize-axis :x
|
||||
:resize-negate? (:flip-x frame)}}]
|
||||
|
||||
[:g.margins {:pointer-events "visible"}
|
||||
(for [[margin-num rect-data] margin-display-data]
|
||||
@@ -213,7 +155,6 @@
|
||||
:margin margin
|
||||
:on-pointer-enter (partial on-pointer-enter margin-num (get margin margin-num))
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-change on-change
|
||||
:rect-data rect-data
|
||||
:hover? (hover? margin-num)
|
||||
:selected? (get margins-selected margin-num)
|
||||
|
||||
@@ -6,12 +6,9 @@
|
||||
|
||||
(ns app.main.ui.flex-controls.padding
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.css-cursors :as cur]
|
||||
@@ -21,13 +18,11 @@
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc padding-display
|
||||
[{:keys [frame-id zoom hover-all? hover-v? hover-h? padding-num padding on-pointer-enter
|
||||
on-pointer-leave rect-data hover? selected? mouse-pos hover-value on-move-selected
|
||||
on-context-menu on-change]}]
|
||||
[{:keys [frame-id zoom hover-all? hover-v? hover-h? padding-num padding on-pointer-enter on-pointer-leave
|
||||
rect-data hover? selected? mouse-pos hover-value on-move-selected on-context-menu]}]
|
||||
(let [resizing? (mf/use-var false)
|
||||
start (mf/use-var nil)
|
||||
original-value (mf/use-var 0)
|
||||
last-pos (mf/use-var nil)
|
||||
negate? (true? (:resize-negate? rect-data))
|
||||
axis (:resize-axis rect-data)
|
||||
|
||||
@@ -40,69 +35,41 @@
|
||||
(reset! start (dom/get-client-position event))
|
||||
(reset! original-value (:initial-value rect-data))))
|
||||
|
||||
calc-modifiers
|
||||
(mf/use-fn
|
||||
(mf/deps frame-id padding-num padding hover-all? hover-v? hover-h?)
|
||||
(fn [pos]
|
||||
(let [delta
|
||||
(-> (gpt/to-vec @start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
|
||||
val
|
||||
(int (max (+ @original-value (/ delta zoom)) 0))
|
||||
|
||||
layout-padding
|
||||
(cond
|
||||
hover-all? (assoc padding :p1 val :p2 val :p3 val :p4 val)
|
||||
hover-v? (assoc padding :p1 val :p3 val)
|
||||
hover-h? (assoc padding :p2 val :p4 val)
|
||||
:else (assoc padding padding-num val))
|
||||
|
||||
|
||||
layout-padding-type
|
||||
(if (= (:p1 padding) (:p2 padding) (:p3 padding) (:p4 padding)) :simple :multiple)]
|
||||
[val
|
||||
(dwm/create-modif-tree
|
||||
[frame-id]
|
||||
(-> (ctm/empty)
|
||||
(ctm/change-property :layout-padding layout-padding)
|
||||
(ctm/change-property :layout-padding-type layout-padding-type)))])))
|
||||
|
||||
on-lost-pointer-capture
|
||||
(mf/use-fn
|
||||
(mf/deps calc-modifiers)
|
||||
(mf/deps frame-id padding-num padding)
|
||||
(fn [event]
|
||||
(dom/release-pointer event)
|
||||
|
||||
(when (features/active-feature? @st/state "render-wasm/v1")
|
||||
(let [[_ modifiers] (calc-modifiers @last-pos)]
|
||||
(st/emit! (dwm/apply-wasm-modifiers modifiers)
|
||||
(dwt/finish-transform))))
|
||||
|
||||
(reset! resizing? false)
|
||||
(reset! start nil)
|
||||
(reset! original-value 0)
|
||||
|
||||
(when (not (features/active-feature? @st/state "render-wasm/v1"))
|
||||
(st/emit! (dwm/apply-modifiers)))))
|
||||
(st/emit! (dwm/apply-modifiers))))
|
||||
|
||||
on-pointer-move
|
||||
(mf/use-fn
|
||||
(mf/deps calc-modifiers on-change)
|
||||
(mf/deps frame-id padding-num padding hover-all? hover-v? hover-h?)
|
||||
(fn [event]
|
||||
(let [pos (dom/get-client-position event)]
|
||||
(reset! mouse-pos (point->viewport pos))
|
||||
(reset! last-pos pos)
|
||||
(when @resizing?
|
||||
(let [[val modifiers] (calc-modifiers pos)]
|
||||
(reset! hover-value val)
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(st/emit! (dwm/set-wasm-modifiers modifiers))
|
||||
(st/emit! (dwm/set-modifiers modifiers)))
|
||||
(let [delta (-> (gpt/to-vec @start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
val (int (max (+ @original-value (/ delta zoom)) 0))
|
||||
layout-padding (cond
|
||||
hover-all? (assoc padding :p1 val :p2 val :p3 val :p4 val)
|
||||
hover-v? (assoc padding :p1 val :p3 val)
|
||||
hover-h? (assoc padding :p2 val :p4 val)
|
||||
:else (assoc padding padding-num val))
|
||||
|
||||
(when on-change
|
||||
(on-change modifiers)))))))]
|
||||
|
||||
layout-padding-type (if (= (:p1 padding) (:p2 padding) (:p3 padding) (:p4 padding)) :simple :multiple)
|
||||
modifiers (dwm/create-modif-tree [frame-id]
|
||||
(-> (ctm/empty)
|
||||
(ctm/change-property :layout-padding layout-padding)
|
||||
(ctm/change-property :layout-padding-type layout-padding-type)))]
|
||||
(reset! hover-value val)
|
||||
(st/emit! (dwm/set-modifiers modifiers)))))))]
|
||||
|
||||
[:g.padding-rect
|
||||
[:rect.info-area
|
||||
@@ -138,108 +105,77 @@
|
||||
:on-lost-pointer-capture on-lost-pointer-capture
|
||||
:on-pointer-move on-pointer-move
|
||||
:on-context-menu on-context-menu
|
||||
:class
|
||||
(when (or hover? selected?)
|
||||
(if (= (:resize-axis rect-data) :x)
|
||||
(cur/get-dynamic "resize-ew" 0)
|
||||
(cur/get-dynamic "resize-ew" 90)))
|
||||
|
||||
:style
|
||||
{:fill (if (or hover? selected?) fcc/distance-color "none")
|
||||
:opacity (if selected? 0 1)}}])]))
|
||||
:class (when (or hover? selected?)
|
||||
(if (= (:resize-axis rect-data) :x) (cur/get-dynamic "resize-ew" 0) (cur/get-dynamic "resize-ew" 90)))
|
||||
:style {:fill (if (or hover? selected?) fcc/distance-color "none")
|
||||
:opacity (if selected? 0 1)}}])]))
|
||||
|
||||
(mf/defc padding-rects
|
||||
[{:keys [frame zoom alt? shift? on-move-selected on-context-menu]}]
|
||||
(let [frame-id (:id frame)
|
||||
paddings-selected (mf/deref refs/workspace-paddings-selected)
|
||||
current-modifiers (mf/use-state nil)
|
||||
|
||||
frame
|
||||
(ctm/apply-structure-modifiers frame (dm/get-in @current-modifiers [frame-id :modifiers]))
|
||||
|
||||
hover-value (mf/use-state 0)
|
||||
mouse-pos (mf/use-state nil)
|
||||
hover (mf/use-state nil)
|
||||
|
||||
hover-all? (and (not (nil? @hover)) alt?)
|
||||
hover-v? (and (or (= @hover :p1) (= @hover :p3)) shift?)
|
||||
hover-h? (and (or (= @hover :p2) (= @hover :p4)) shift?)
|
||||
padding (:layout-padding frame)
|
||||
{:keys [width height x1 x2 y1 y2]} (:selrect frame)
|
||||
on-pointer-enter (fn [hover-type val]
|
||||
(reset! hover hover-type)
|
||||
(reset! hover-value val))
|
||||
on-pointer-leave #(reset! hover nil)
|
||||
pill-width (/ fcc/flex-display-pill-width zoom)
|
||||
pill-height (/ fcc/flex-display-pill-height zoom)
|
||||
hover? #(or hover-all?
|
||||
(and (or (= % :p1) (= % :p3)) hover-v?)
|
||||
(and (or (= % :p2) (= % :p4)) hover-h?)
|
||||
(= @hover %))
|
||||
negate {:p1 (if (:flip-y frame) true false)
|
||||
:p2 (if (:flip-x frame) true false)
|
||||
:p3 (if (:flip-y frame) true false)
|
||||
:p4 (if (:flip-x frame) true false)}
|
||||
negate (cond-> negate
|
||||
(not= :auto (:layout-item-h-sizing frame)) (assoc :p2 (not (:p2 negate)))
|
||||
(not= :auto (:layout-item-v-sizing frame)) (assoc :p3 (not (:p3 negate))))
|
||||
|
||||
negate
|
||||
{:p1 (if (:flip-y frame) true false)
|
||||
:p2 (if (:flip-x frame) true false)
|
||||
:p3 (if (:flip-y frame) true false)
|
||||
:p4 (if (:flip-x frame) true false)}
|
||||
|
||||
negate
|
||||
(cond-> negate
|
||||
(not= :auto (:layout-item-h-sizing frame)) (assoc :p2 (not (:p2 negate)))
|
||||
(not= :auto (:layout-item-v-sizing frame)) (assoc :p3 (not (:p3 negate))))
|
||||
|
||||
padding-rect-data
|
||||
{:p1 {:key (str frame-id "-p1")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) (- y2 (:p1 padding)) y1)
|
||||
:width width
|
||||
:height (:p1 padding)
|
||||
:initial-value (:p1 padding)
|
||||
:resize-type (if (:flip-y frame) :bottom :top)
|
||||
:resize-axis :y
|
||||
:resize-negate? (:p1 negate)}
|
||||
:p2 {:key (str frame-id "-p2")
|
||||
:x (if (:flip-x frame) x1 (- x2 (:p2 padding)))
|
||||
:y y1
|
||||
:width (:p2 padding)
|
||||
:height height
|
||||
:initial-value (:p2 padding)
|
||||
:resize-type :left
|
||||
:resize-axis :x
|
||||
:resize-negate? (:p2 negate)}
|
||||
:p3 {:key (str frame-id "-p3")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) y1 (- y2 (:p3 padding)))
|
||||
:width width
|
||||
:height (:p3 padding)
|
||||
:initial-value (:p3 padding)
|
||||
:resize-type :bottom
|
||||
:resize-axis :y
|
||||
:resize-negate? (:p3 negate)}
|
||||
:p4 {:key (str frame-id "-p4")
|
||||
:x (if (:flip-x frame) (- x2 (:p4 padding)) x1)
|
||||
:y y1
|
||||
:width (:p4 padding)
|
||||
:height height
|
||||
:initial-value (:p4 padding)
|
||||
:resize-type (if (:flip-x frame) :right :left)
|
||||
:resize-axis :x
|
||||
:resize-negate? (:p4 negate)}}
|
||||
|
||||
on-pointer-enter
|
||||
(mf/use-fn
|
||||
(fn [hover-type val]
|
||||
(reset! hover hover-type)
|
||||
(reset! hover-value val)))
|
||||
|
||||
on-pointer-leave
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(reset! hover nil)))
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(fn [modifiers]
|
||||
(reset! current-modifiers modifiers)))
|
||||
|
||||
hover?
|
||||
(fn [value]
|
||||
(or hover-all?
|
||||
(and (or (= value :p1) (= value :p3)) hover-v?)
|
||||
(and (or (= value :p2) (= value :p4)) hover-h?)
|
||||
(= @hover value)))]
|
||||
padding-rect-data {:p1 {:key (str frame-id "-p1")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) (- y2 (:p1 padding)) y1)
|
||||
:width width
|
||||
:height (:p1 padding)
|
||||
:initial-value (:p1 padding)
|
||||
:resize-type (if (:flip-y frame) :bottom :top)
|
||||
:resize-axis :y
|
||||
:resize-negate? (:p1 negate)}
|
||||
:p2 {:key (str frame-id "-p2")
|
||||
:x (if (:flip-x frame) x1 (- x2 (:p2 padding)))
|
||||
:y y1
|
||||
:width (:p2 padding)
|
||||
:height height
|
||||
:initial-value (:p2 padding)
|
||||
:resize-type :left
|
||||
:resize-axis :x
|
||||
:resize-negate? (:p2 negate)}
|
||||
:p3 {:key (str frame-id "-p3")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) y1 (- y2 (:p3 padding)))
|
||||
:width width
|
||||
:height (:p3 padding)
|
||||
:initial-value (:p3 padding)
|
||||
:resize-type :bottom
|
||||
:resize-axis :y
|
||||
:resize-negate? (:p3 negate)}
|
||||
:p4 {:key (str frame-id "-p4")
|
||||
:x (if (:flip-x frame) (- x2 (:p4 padding)) x1)
|
||||
:y y1
|
||||
:width (:p4 padding)
|
||||
:height height
|
||||
:initial-value (:p4 padding)
|
||||
:resize-type (if (:flip-x frame) :right :left)
|
||||
:resize-axis :x
|
||||
:resize-negate? (:p4 negate)}}]
|
||||
|
||||
[:g.paddings {:pointer-events "visible"}
|
||||
(for [[padding-num rect-data] padding-rect-data]
|
||||
@@ -258,11 +194,9 @@
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-move-selected on-move-selected
|
||||
:on-context-menu on-context-menu
|
||||
:on-change on-change
|
||||
:hover? (hover? padding-num)
|
||||
:selected? (get paddings-selected padding-num)
|
||||
:rect-data rect-data}])
|
||||
|
||||
(when @hover
|
||||
[:& fcc/flex-display-pill
|
||||
{:height pill-height
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
padding-bottom: deprecated.$s-16;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-gutter: stable;
|
||||
padding-inline: var(--sp-m);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
|
||||
.tool-windows {
|
||||
block-size: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
@@ -124,7 +124,8 @@
|
||||
.inspect-tab-switcher-label {
|
||||
@include use-typography("body-medium");
|
||||
color: var(--color-foreground-primary);
|
||||
flex: 0 1 40%;
|
||||
flex: 0;
|
||||
min-inline-size: fit-content;
|
||||
}
|
||||
|
||||
.inspect-tab-switcher-controls {
|
||||
@@ -150,6 +151,7 @@
|
||||
}
|
||||
|
||||
.inspect-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -157,5 +159,6 @@
|
||||
--tabs-nav-padding-inline-start: 0;
|
||||
--tabs-nav-padding-inline-end: var(--sp-m);
|
||||
|
||||
block-size: calc(100vh - px2rem(200)); // TODO: Fix this hardcoded value
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.styles-tab {
|
||||
block-size: calc(100vh - px2rem(200)); // TODO: Fix this hardcoded value
|
||||
}
|
||||
@@ -6,15 +6,6 @@
|
||||
|
||||
@use "ds/typography.scss" as *;
|
||||
|
||||
// TODO: this must be a custom property in the design system
|
||||
:global(.light) {
|
||||
--low-emphasis-background: #fafafa;
|
||||
}
|
||||
|
||||
:global(.default) {
|
||||
--low-emphasis-background: #121214;
|
||||
}
|
||||
|
||||
.style-box {
|
||||
--title-gap: var(--sp-xs);
|
||||
--title-padding: var(--sp-s);
|
||||
@@ -22,9 +13,12 @@
|
||||
--arrow-color: var(--color-foreground-secondary);
|
||||
--box-border-color: var(--color-background-primary);
|
||||
|
||||
// TODO: this must be a custom property in the design system
|
||||
--lowEmphasis-background: #121214;
|
||||
|
||||
padding-block: var(--sp-s);
|
||||
padding-inline: var(--sp-m);
|
||||
background-color: var(--low-emphasis-background);
|
||||
background-color: var(--lowEmphasis-background);
|
||||
|
||||
border-block-end: 2px solid var(--box-border-color);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
[app.main.ui.releases.v2-1]
|
||||
[app.main.ui.releases.v2-10]
|
||||
[app.main.ui.releases.v2-11]
|
||||
[app.main.ui.releases.v2-12]
|
||||
[app.main.ui.releases.v2-2]
|
||||
[app.main.ui.releases.v2-3]
|
||||
[app.main.ui.releases.v2-4]
|
||||
@@ -103,4 +102,4 @@
|
||||
|
||||
(defmethod rc/render-release-notes "0.0"
|
||||
[params]
|
||||
(rc/render-release-notes (assoc params :version "2.12")))
|
||||
(rc/render-release-notes (assoc params :version "2.11")))
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.releases.v2-12
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.releases.common :as c]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defmethod c/render-release-notes "2.12"
|
||||
[{:keys [slide klass next finish navigate version]}]
|
||||
(mf/html
|
||||
(case slide
|
||||
:start
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.12-slide-0.jpg"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot 2.12 is here!"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"What’s new in Penpot?"]
|
||||
|
||||
[:div {:class (stl/css :version-tag)}
|
||||
(dm/str "Version " version)]]
|
||||
|
||||
[:div {:class (stl/css :features-block)}
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"Better tokens visibility and more!"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"This release focuses on making your everyday workflow feel clearer, faster and more intuitive. Tokens are now easier to see and apply, appearing directly where you work and giving the designs better context during code inspection. Variants gain a more natural flow thanks to simple boolean toggles that remove friction when switching states. And PDF export becomes more flexible, letting you choose exactly which boards to share so your files match the story you want to tell."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Together, these enhancements bring greater control and fluidity to your entire design process."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Let’s dive in!"]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:button {:class (stl/css :next-btn)
|
||||
:on-click next} "Continue"]]]]]]
|
||||
|
||||
0
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.12-tokens-sidebar.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Better tokens visibility"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Better tokens visibility"]]
|
||||
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Design systems should be both powerful and effortless to use. This release brings tokens closer to where you work, making them easier to apply and easier to understand."]
|
||||
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"Apply color tokens right from the sidebar"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Your color tokens now appear directly in the properties sidebar, making it faster to apply or unapply tokens from the design tab. No more digging: now you can use tokens within your design flow."]
|
||||
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"See token names in the Inspect panel"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Developers now get a clearer context during handoff. The Inspect panel shows the actual token used in your design, in a similar way to how styles are displayed. This small detail reduces ambiguity, aligns everyone on the same language, and strengthens collaboration across the team."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 3}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
1
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.12-variants.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Simpler boolean variants"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Simpler boolean variants"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Variants are central to building flexible, scalable components. With this release, boolean properties become far easier to work with."]
|
||||
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"A simple toggle for boolean values"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Binary states now use a clean toggle, to be able to switch visually, instead of a dropdown. This makes adjusting component states more intuitive and speeds up working with multiple instances."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"It’s a subtle improvement, but it removes friction you feel hundreds of times a week, and makes component work flow more naturally."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 3}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
|
||||
2
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.12-export-pdf.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Smarter PDF export"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Smarter PDF export"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Exporting your work is now more precise and flexible."]
|
||||
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"Select specific boards when exporting"]
|
||||
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"You’re now in control of which boards make it into your PDF. Share just the final screens, just a flow, just the workshop materials. This streamlined export flow adapts to the way real teams work: share the story you want to tell, with exactly the boards you need."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 3}]
|
||||
|
||||
[:button {:on-click finish
|
||||
:class (stl/css :next-btn)} "Let's go"]]]]]])))
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
display: grid;
|
||||
grid-template-columns: deprecated.$s-324 1fr;
|
||||
height: deprecated.$s-500;
|
||||
width: deprecated.$s-888;
|
||||
border-radius: deprecated.$br-8;
|
||||
background-color: var(--modal-background-color);
|
||||
border: deprecated.$s-2 solid var(--modal-border-color);
|
||||
}
|
||||
|
||||
.start-image {
|
||||
width: deprecated.$s-324;
|
||||
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: deprecated.$s-40;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr deprecated.$s-32;
|
||||
gap: deprecated.$s-24;
|
||||
|
||||
a {
|
||||
color: var(--button-primary-background-color-rest);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: grid;
|
||||
gap: deprecated.$s-8;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
@include deprecated.flexCenter;
|
||||
@include deprecated.headlineSmallTypography;
|
||||
height: deprecated.$s-32;
|
||||
width: deprecated.$s-96;
|
||||
background-color: var(--communication-tag-background-color);
|
||||
color: var(--communication-tag-foreground-color);
|
||||
border-radius: deprecated.$br-8;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include deprecated.headlineLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.features-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-16;
|
||||
width: deprecated.$s-440;
|
||||
}
|
||||
|
||||
.feature {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-8;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
@include deprecated.bodyMediumTypography;
|
||||
margin: 0;
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
@include deprecated.bodyMediumTypography;
|
||||
color: var(--modal-text-foreground-color);
|
||||
list-style: disc;
|
||||
display: grid;
|
||||
gap: deprecated.$s-8;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-areas: "bullets button";
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
@extend .button-primary;
|
||||
width: deprecated.$s-100;
|
||||
justify-self: flex-end;
|
||||
grid-area: button;
|
||||
}
|
||||
@@ -28,7 +28,6 @@
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [{:keys [position-data content] :as shape} (obj/get props "shape")
|
||||
is-render? (mf/use-ctx ctx/is-render?)
|
||||
is-component? (mf/use-ctx ctx/is-component?)]
|
||||
|
||||
(mf/with-memo [content]
|
||||
@@ -42,5 +41,5 @@
|
||||
;; Only use this for component preview, otherwise the dashboard thumbnails
|
||||
;; will give a tainted canvas error because the `foreignObject` cannot be
|
||||
;; rendered.
|
||||
(and (nil? position-data) (or is-component? is-render?))
|
||||
(and (nil? position-data) is-component?)
|
||||
[:> fo/text-shape props])))
|
||||
|
||||
@@ -12,20 +12,18 @@
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc text-edition-outline
|
||||
[{:keys [shape zoom modifiers]}]
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(let [{:keys [width height]} (wasm.api/get-text-dimensions (:id shape))
|
||||
selrect-transform (mf/deref refs/workspace-selrect)
|
||||
[selrect transform] (dsh/get-selrect selrect-transform shape)]
|
||||
(let [selrect-transform (mf/deref refs/workspace-selrect)
|
||||
[{:keys [x y width height]} transform] (dsh/get-selrect selrect-transform shape)]
|
||||
[:rect.main.viewport-selrect
|
||||
{:x (:x selrect)
|
||||
:y (:y selrect)
|
||||
:width (max width (:width selrect))
|
||||
:height (max height (:height selrect))
|
||||
{:x x
|
||||
:y y
|
||||
:width width
|
||||
:height height
|
||||
:transform transform
|
||||
:style {:stroke "var(--color-accent-tertiary)"
|
||||
:stroke-width (/ 1 zoom)
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
[app.util.object :as obj]
|
||||
[app.util.text.content :as content]
|
||||
[app.util.text.content.styles :as styles]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn get-contrast-color [background-color]
|
||||
@@ -269,12 +268,7 @@
|
||||
"bottom" "flex-end"
|
||||
nil))
|
||||
|
||||
(defn- font-family-from-font-id [font-id]
|
||||
(if (str/includes? font-id "gfont-noto-sans")
|
||||
(let [lang (str/replace font-id #"gfont\-noto\-sans\-" "")]
|
||||
(if (>= (count lang) 3) (str/capital lang) (str/upper lang)))
|
||||
"Noto Color Emoji"))
|
||||
|
||||
;;
|
||||
;; Text Editor Wrapper
|
||||
;; This is an SVG element that wraps the HTML editor.
|
||||
;;
|
||||
@@ -287,10 +281,6 @@
|
||||
(let [shape-id (dm/get-prop shape :id)
|
||||
modifiers (dm/get-in modifiers [shape-id :modifiers])
|
||||
|
||||
fallback-fonts (wasm.api/fonts-from-text-content (:content shape) false)
|
||||
fallback-families (map (fn [font]
|
||||
(font-family-from-font-id (:font-id font))) fallback-fonts)
|
||||
|
||||
clip-id (dm/str "text-edition-clip" shape-id)
|
||||
|
||||
text-modifier-ref
|
||||
@@ -320,22 +310,20 @@
|
||||
|
||||
[{:keys [x y width height]} transform]
|
||||
(if render-wasm?
|
||||
(let [{:keys [width height]} (wasm.api/get-text-dimensions shape-id)
|
||||
(let [{:keys [height]} (wasm.api/get-text-dimensions shape-id)
|
||||
selrect-transform (mf/deref refs/workspace-selrect)
|
||||
[selrect transform] (dsh/get-selrect selrect-transform shape)
|
||||
selrect-height (:height selrect)
|
||||
selrect-width (:width selrect)
|
||||
max-width (max width selrect-width)
|
||||
max-height (max height selrect-height)
|
||||
valign (-> shape :content :vertical-align)
|
||||
y (:y selrect)
|
||||
y (if (and valign (> height selrect-height))
|
||||
y (if (> height selrect-height)
|
||||
(case valign
|
||||
"bottom" (- y (- height selrect-height))
|
||||
"center" (- y (/ (- height selrect-height) 2))
|
||||
"top" y)
|
||||
y)]
|
||||
[(assoc selrect :y y :width max-width :height max-height) transform])
|
||||
[(assoc selrect :y y :width (:width selrect) :height max-height) transform])
|
||||
|
||||
(let [bounds (gst/shape->rect shape)
|
||||
x (mth/min (dm/get-prop bounds :x)
|
||||
@@ -353,8 +341,7 @@
|
||||
render-wasm?
|
||||
(obj/merge!
|
||||
#js {"--editor-container-width" (dm/str width "px")
|
||||
"--editor-container-height" (dm/str height "px")
|
||||
"--fallback-families" (if (seq fallback-families) (dm/str (str/join ", " fallback-families)) "sourcesanspro")})
|
||||
"--editor-container-height" (dm/str height "px")})
|
||||
|
||||
(not render-wasm?)
|
||||
(obj/merge!
|
||||
|
||||
@@ -56,8 +56,9 @@
|
||||
(update file :data dissoc :pages-index))
|
||||
refs/file))
|
||||
|
||||
(mf/defc assets-local-library*
|
||||
{::mf/private true}
|
||||
(mf/defc assets-local-library
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/wrap-props false}
|
||||
[{:keys [filters]}]
|
||||
(let [file (mf/deref ref:local-library)]
|
||||
[:> file-library*
|
||||
@@ -67,7 +68,7 @@
|
||||
:filters filters}]))
|
||||
|
||||
(defn- toggle-values
|
||||
[v a b]
|
||||
[v [a b]]
|
||||
(if (= v a) b a))
|
||||
|
||||
(mf/defc assets-toolbox*
|
||||
@@ -96,7 +97,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps ordering)
|
||||
(fn []
|
||||
(let [new-value (toggle-values ordering :asc :desc)]
|
||||
(let [new-value (toggle-values ordering [:asc :desc])]
|
||||
(swap! filters* assoc :ordering new-value)
|
||||
(dwa/set-current-assets-ordering! new-value))))
|
||||
|
||||
@@ -104,7 +105,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps list-style)
|
||||
(fn []
|
||||
(let [new-value (toggle-values list-style :thumbs :list)]
|
||||
(let [new-value (toggle-values list-style [:thumbs :list])]
|
||||
(swap! filters* assoc :list-style new-value)
|
||||
(dwa/set-current-assets-list-style! new-value))))
|
||||
|
||||
@@ -208,5 +209,5 @@
|
||||
[:& (mf/provider cmm/assets-toggle-ordering) {:value toggle-ordering}
|
||||
[:& (mf/provider cmm/assets-toggle-list-style) {:value toggle-list-style}
|
||||
[:*
|
||||
[:> assets-local-library* {:filters filters}]
|
||||
[:& assets-local-library {:filters filters}]
|
||||
[:> assets-libraries* {:filters filters}]]]]]]))
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
cursor: pointer;
|
||||
|
||||
.title-menu {
|
||||
visibility: visible;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
}
|
||||
|
||||
.title-menu {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.variant :as ctv]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.variants :as dwv]
|
||||
[app.main.store :as st]
|
||||
[app.util.debug :as dbg]
|
||||
[app.util.dom :as dom]
|
||||
@@ -68,7 +69,15 @@
|
||||
name (str/trim (dom/get-value name-input))]
|
||||
(on-stop-edit)
|
||||
(reset! edition* false)
|
||||
(st/emit! (dw/rename-shape-or-variant shape-id name)))))
|
||||
(if variant-name
|
||||
(if (ctv/valid-properties-formula? name)
|
||||
(st/emit! (dwv/update-properties-names-and-values component-id variant-id variant-properties (ctv/properties-formula->map name))
|
||||
(dwv/remove-empty-properties variant-id)
|
||||
(dwv/update-error component-id))
|
||||
(st/emit! (dwv/update-properties-names-and-values component-id variant-id variant-properties {})
|
||||
(dwv/remove-empty-properties variant-id)
|
||||
(dwv/update-error component-id name)))
|
||||
(st/emit! (dw/end-rename-shape shape-id name))))))
|
||||
|
||||
cancel-edit
|
||||
(mf/use-fn
|
||||
|
||||
@@ -38,18 +38,30 @@
|
||||
(features/use-feature "render-wasm/v1")
|
||||
|
||||
has-invalid-shapes?
|
||||
(some (if render-wasm-enabled?
|
||||
cfh/frame-shape?
|
||||
#(or (cfh/frame-shape? %) (cfh/text-shape? %)))
|
||||
shapes-with-children)
|
||||
(if render-wasm-enabled?
|
||||
false
|
||||
(some (fn [shape]
|
||||
(or (cfh/frame-shape? shape)
|
||||
(cfh/text-shape? shape)))
|
||||
shapes-with-children))
|
||||
|
||||
head-not-group-like?
|
||||
(and (= 1 total-selected)
|
||||
(not is-group?)
|
||||
(not is-bool?))
|
||||
|
||||
disabled-bool-btns (or (zero? total-selected) has-invalid-shapes? head-not-group-like?)
|
||||
disabled-flatten (or (zero? total-selected) has-invalid-shapes?)
|
||||
disabled-bool-btns
|
||||
(if render-wasm-enabled?
|
||||
false
|
||||
(or (zero? total-selected)
|
||||
has-invalid-shapes?
|
||||
head-not-group-like?))
|
||||
|
||||
disabled-flatten
|
||||
(if render-wasm-enabled?
|
||||
false
|
||||
(or (zero? total-selected)
|
||||
has-invalid-shapes?))
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
|
||||
@@ -687,7 +687,7 @@
|
||||
(str/upper (tr "workspace.assets.local-library"))
|
||||
(dm/get-in libraries [current-library-id :name]))
|
||||
|
||||
current-lib-data (mf/with-memo [libraries current-library-id]
|
||||
current-lib-data (mf/with-memo [libraries]
|
||||
(get-in libraries [current-library-id :data]))
|
||||
|
||||
current-lib-counts (mf/with-memo [current-lib-data]
|
||||
|
||||
@@ -378,7 +378,6 @@
|
||||
:step 0.1
|
||||
:default-value "1.2"
|
||||
:class (stl/css :line-height-input)
|
||||
:aria-label (tr "inspect.attributes.typography.line-height")
|
||||
:value (attr->string line-height)
|
||||
:placeholder (if (= :multiple line-height) (tr "settings.multiple") "--")
|
||||
:nillable (= :multiple line-height)
|
||||
@@ -397,7 +396,6 @@
|
||||
:step 0.1
|
||||
:default-value "0"
|
||||
:class (stl/css :letter-spacing-input)
|
||||
:aria-label (tr "inspect.attributes.typography.letter-spacing")
|
||||
:value (attr->string letter-spacing)
|
||||
:placeholder (if (= :multiple letter-spacing) (tr "settings.multiple") "--")
|
||||
:on-change #(handle-change % :letter-spacing)
|
||||
|
||||
@@ -32,12 +32,6 @@
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- token-value-error-fn
|
||||
[{:keys [value]}]
|
||||
(when (or (str/empty? value)
|
||||
(str/blank? value))
|
||||
(tr "workspace.tokens.empty-input")))
|
||||
|
||||
(defn- make-schema
|
||||
[tokens-tree]
|
||||
(sm/schema
|
||||
@@ -50,7 +44,7 @@
|
||||
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
|
||||
#(not (cft/token-name-path-exists? % tokens-tree))]]]
|
||||
|
||||
[:value [::sm/text {:error/fn token-value-error-fn}]]
|
||||
[:value ::sm/text]
|
||||
|
||||
[:resolved-value ::sm/any]
|
||||
|
||||
|
||||
@@ -32,12 +32,6 @@
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- token-value-error-fn
|
||||
[{:keys [value]}]
|
||||
(when (or (str/empty? value)
|
||||
(str/blank? value))
|
||||
(tr "workspace.tokens.empty-input")))
|
||||
|
||||
(defn- make-schema
|
||||
[tokens-tree]
|
||||
(sm/schema
|
||||
@@ -50,7 +44,7 @@
|
||||
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
|
||||
#(not (cft/token-name-path-exists? % tokens-tree))]]]
|
||||
|
||||
[:value [::sm/text {:error/fn token-value-error-fn}]]
|
||||
[:value ::sm/text]
|
||||
|
||||
[:resolved-value ::sm/any]
|
||||
|
||||
|
||||
@@ -32,12 +32,6 @@
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- token-value-error-fn
|
||||
[{:keys [value]}]
|
||||
(when (or (str/empty? value)
|
||||
(str/blank? value))
|
||||
(tr "workspace.tokens.empty-input")))
|
||||
|
||||
(defn- make-schema
|
||||
[tokens-tree]
|
||||
(sm/schema
|
||||
@@ -50,7 +44,7 @@
|
||||
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
|
||||
#(not (cft/token-name-path-exists? % tokens-tree))]]]
|
||||
|
||||
[:value [::sm/text {:error/fn token-value-error-fn}]]
|
||||
[:value ::sm/text]
|
||||
|
||||
[:resolved-value ::sm/any]
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user