Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2025-03-26 13:15:58 +01:00
commit 416e9e8e1d
52 changed files with 607 additions and 86 deletions

View file

@ -204,6 +204,9 @@
(rx/filter #(= % ::force-persist))))]
(rx/merge
(->> notifier-s
(rx/map #(ptk/data-event ::persistence-notification)))
(->> local-commits-s
(rx/debounce 200)
(rx/map (fn [_]

View file

@ -44,6 +44,7 @@
[app.main.data.helpers :as dsh]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.persistence :as-alias dps]
[app.main.data.plugins :as dp]
[app.main.data.profile :as du]
[app.main.data.project :as dpj]
@ -354,6 +355,11 @@
(-> (workspace-initialized file-id)
(with-meta {:file-id file-id}))))))
(->> stream
(rx/filter (ptk/type? ::dps/persistence-notification))
(rx/take 1)
(rx/map dwc/set-workspace-visited))
(when-let [component-id (some-> rparams :component-id parse-uuid)]
(->> stream
(rx/filter (ptk/type? ::workspace-initialized))
@ -476,10 +482,13 @@
ptk/WatchEvent
(watch [_ state _]
(if-let [page (dsh/lookup-page state file-id page-id)]
(rx/of (initialize-page* file-id page-id page)
(dwth/watch-state-changes file-id page-id)
(dwl/watch-component-changes)
(select-frame-tool file-id page-id))
(rx/concat (rx/of (initialize-page* file-id page-id page)
(dwth/watch-state-changes file-id page-id)
(dwl/watch-component-changes))
(let [profile (:profile state)
props (get profile :props)]
(when (not (:workspace-visited props))
(rx/of (select-frame-tool file-id page-id)))))
(rx/of (dcm/go-to-workspace :file-id file-id ::rt/replace true))))))
(defn finalize-page

View file

@ -7,7 +7,6 @@
(ns app.main.data.workspace.common
(:require
[app.common.logging :as log]
[app.config :as cf]
[app.main.data.profile :as du]
[app.main.data.workspace.layout :as dwl]
[beicon.v2.core :as rx]
@ -38,7 +37,7 @@
(watch [_ state _]
(let [profile (:profile state)
props (get profile :props)]
(when (and (cf/external-feature-flag "boards-03" "test") (not (:workspace-visited props)))
(when (not (:workspace-visited props))
(rx/of (du/update-profile-props {:workspace-visited true})))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -14,7 +14,6 @@
[app.main.data.changes :as dch]
[app.main.data.helpers :as dsh]
[app.main.data.persistence :as-alias dps]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.notifications :as-alias wnt]
[app.main.rasterizer :as thr]
[app.main.refs :as refs]
@ -293,10 +292,4 @@
(rx/mapcat #(into #{} %))
(rx/map #(update-thumbnail file-id page-id % "frame" "watch-state-changes"))))
;; WARNING: This is a workaround for an AB test, in case we consolidate this change we should
;; find a better way to handle this.
(->> notifier-s
(rx/take 1)
(rx/map dwc/set-workspace-visited))
(rx/take-until stopper-s))))))

View file

@ -10,6 +10,7 @@
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.align :as gal]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as gpr]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
@ -83,6 +84,25 @@
(fn [local]
(setup state local)))))))
(defn calculate-centered-viewbox
"Updates the viewbox coordinates for a given center position"
[local position]
(let [vbox (:vbox local)
nw (/ (:width vbox) 2)
nh (/ (:height vbox) 2)
nx (- (:x position) nw)
ny (- (:y position) nh)]
(update local :vbox assoc :x nx :y ny)))
(defn update-viewport-position-center
[position]
(assert (gpt/point? position) "expected a point instance for `position` param")
(ptk/reify ::update-viewport-position-center
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local calculate-centered-viewbox position))))
(defn update-viewport-position
[{:keys [x y] :or {x identity y identity}}]

View file

@ -20,7 +20,7 @@
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn- impl-update-zoom
(defn impl-update-zoom
[{:keys [vbox] :as local} center zoom]
(let [new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom)
old-zoom (:zoom local)

View file

@ -17,6 +17,7 @@
[app.main.data.comments :as dcm]
[app.main.data.modal :as modal]
[app.main.data.workspace.comments :as dwcm]
[app.main.data.workspace.viewport :as dwv]
[app.main.data.workspace.zoom :as dwz]
[app.main.refs :as refs]
[app.main.store :as st]
@ -1097,15 +1098,35 @@
groups))
(group-bubbles zoom remaining visited (cons [current] groups)))))))
(defn- calculate-zoom-scale-to-ungroup-bubbles
"Calculate the minimum zoom scale needed for a group of bubbles to avoid overlap among them"
[zoom threads]
(defn- inside-vbox?
"Checks if a bubble or a bubble group is inside a viewbox"
[thread-group wl]
(let [vbox (:vbox wl)
positions (mapv :position thread-group)
position (gpt/center-points positions)
pos-x (:x position)
pos-y (:y position)
x1 (:x vbox)
y1 (:y vbox)
x2 (+ x1 (:width vbox))
y2 (+ y1 (:height vbox))]
(and (> x2 pos-x x1) (> y2 pos-y y1))))
(defn- calculate-zoom-scale
"Calculates the zoom level needed to ungroup the largest number of bubbles while
keeping them all visible in the viewbox."
[position zoom threads wl]
(let [num-threads (count threads)
num-grouped-threads (count (group-bubbles zoom threads))
zoom-scale-step 1.75]
(if (= num-threads num-grouped-threads)
grouped-threads (group-bubbles zoom threads)
num-grouped-threads (count grouped-threads)
zoom-scale-step 1.75
scaled-zoom (* zoom zoom-scale-step)
zoomed-wl (dwz/impl-update-zoom wl position scaled-zoom)
outside-vbox? (complement inside-vbox?)]
(if (or (= num-threads num-grouped-threads)
(some #(outside-vbox? % zoomed-wl) grouped-threads))
zoom
(calculate-zoom-scale-to-ungroup-bubbles (* zoom zoom-scale-step) threads))))
(calculate-zoom-scale position scaled-zoom threads zoomed-wl))))
(mf/defc comment-floating-group*
{::mf/wrap [mf/memo]}
@ -1126,11 +1147,14 @@
on-click
(mf/use-fn
(mf/deps thread-group position)
(mf/deps thread-group position zoom)
(fn []
(let [updated-zoom (calculate-zoom-scale-to-ungroup-bubbles zoom thread-group)
(let [wl (deref refs/workspace-local)
centered-wl (dwv/calculate-centered-viewbox wl position)
updated-zoom (calculate-zoom-scale position zoom thread-group centered-wl)
scale-zoom (/ updated-zoom zoom)]
(st/emit! (dwz/set-zoom position scale-zoom)))))]
(st/emit! (dwv/update-viewport-position-center position)
(dwz/set-zoom position scale-zoom)))))]
[:div {:style {:top (dm/str pos-y "px")
:left (dm/str pos-x "px")}

View file

@ -13,6 +13,7 @@
margin-right: $s-16;
border-top: $s-1 solid var(--panel-border-color);
overflow-y: auto;
padding-bottom: $s-32;
}
.dashboard-projects {

View file

@ -11,9 +11,10 @@
bottom: 0;
border-bottom-left-radius: $br-8;
border-bottom-right-radius: $br-8;
border-top-right-radius: $br-8;
display: flex;
flex-direction: column;
height: $s-228;
height: $s-244;
justify-content: flex-end;
margin-left: $s-6;
margin-right: $s-6;
@ -39,8 +40,8 @@
.title {
pointer-events: all;
width: 100%;
top: calc(-1 * $s-56);
width: $s-420;
top: calc(-1 * $s-40);
text-align: right;
height: $s-56;
position: absolute;
@ -102,8 +103,8 @@
.move-button {
position: absolute;
top: $s-80;
border: $s-2 solid var(--button-secondary-background-color-rest);
top: $s-96;
border: $s-2 solid var(--color-foreground-secondary);
border-radius: 50%;
text-align: center;
width: $s-36;
@ -139,7 +140,7 @@
font-size: $fs-14;
color: var(--color-foreground-primary);
margin-bottom: -8px;
margin-top: -4px;
margin-top: $s-16;
margin-left: $s-16;
visibility: visible;
}

View file

@ -14,6 +14,7 @@
[app.main.ui.context :as ctx]
[app.main.ui.hooks :as hooks]
[app.util.dom :as dom]
[app.util.globals :as glob]
[app.util.storage :as storage]
[rumext.v2 :as mf]))
@ -34,6 +35,28 @@
(set! last-resize-type type))
(defn use-resize-hook
"Allows a node to be resized by dragging, and calculates the new size based on the drag setting a maximum and minimum value.
Parameters:
- `key` - A unique key to identify the resize hook.
- `initial` - The initial size of the node.
- `min-val` - The minimum value the size can be.
- `max-val` - The maximum value the size can be. It can be a number or a string representing either a fixed value or a percentage (0 to 1).
- `axis` - The axis to resize on, either `:x` or `:y`.
- `negate?` - If `true`, the axis is negated.
- `resize-type` - The type of resize, either `:width` or `:height`.
- `on-change-size` - A function to call when the size changes.
Returns:
- An object with the following:
- `:on-pointer-down` - A function to call when the pointer is pressed down.
- `:on-lost-pointer-capture` - A function to call when the pointer is released.
- `:on-pointer-move` - A function to call when the pointer is moved.
- `:parent-ref` - A reference to the node.
- `:set-size` - A function to set the size.
- `:size` - The current size."
([key initial min-val max-val axis negate? resize-type]
(use-resize-hook key initial min-val max-val axis negate? resize-type nil))
@ -48,14 +71,26 @@
start-size-ref (mf/use-ref nil)
start-ref (mf/use-ref nil)
window-height (dom/get-window-height)
;; Since Penpot is not responsive designed, this value will only refer to vertical axis.
window-height* (mf/use-state #(dom/get-window-height))
window-height (deref window-height*)
;; In case max-val is a string, we need to parse it as a double.
max-val (mf/with-memo [max-val window-height]
(let [parsed-max-val (when (string? max-val) (d/parse-double max-val))]
(if parsed-max-val
(* window-height parsed-max-val)
max-val)))
set-size
(mf/use-fn
(mf/deps file-id key min-val max-val window-height)
(fn [new-size]
(let [new-size (mth/clamp new-size min-val max-val)]
(reset! current-size* new-size)
;; Save the new size in the local storage for this file and this specific node.
(swap! storage/user update-persistent-state file-id key new-size))))
on-pointer-down
(mf/use-fn
(mf/deps current-size)
@ -89,21 +124,28 @@
start-size (mf/ref-val start-size-ref)
new-size (-> (+ start-size delta) (max min-val) (min max-val))]
(reset! current-size* new-size)
(swap! storage/user update-persistent-state file-id key new-size)))))
set-size
(set-size new-size)))))
on-resize-window
(mf/use-fn
(mf/deps on-change-size file-id key)
(fn [new-size]
(let [new-size (mth/clamp new-size min-val max-val)]
(reset! current-size* new-size)
(swap! storage/user update-persistent-state file-id key new-size))))]
(fn []
(let [new-window-height (dom/get-window-height)]
(reset! window-height* new-window-height))))]
(mf/with-effect [on-change-size current-size]
(when on-change-size
(on-change-size current-size)))
(mf/with-effect []
(.addEventListener glob/window "resize" on-resize-window)
(fn []
(.removeEventListener glob/window "resize" on-resize-window)))
(mf/with-effect [window-height]
(let [new-size (mth/clamp current-size min-val max-val)]
(set-size new-size)))
{:on-pointer-down on-pointer-down
:on-lost-pointer-capture on-lost-pointer-capture
:on-pointer-move on-pointer-move

View file

@ -50,7 +50,7 @@
Caution: This will allow a trailing dot like `token-name.`,
But we will trim that in the `finalize-name`,
to not throw too many errors while the user is editing."
#"(?!\$)([a-zA-Z0-9-$]+\.?)*")
#"(?!\$)([a-zA-Z0-9-$_]+\.?)*")
(def valid-token-name-schema
(m/-simple-schema
@ -235,7 +235,14 @@
token-properties (wtch/get-token-properties token)
color? (wtt/color-token? token)
selected-set-tokens (mf/deref refs/workspace-selected-token-set-tokens)
active-theme-tokens (mf/deref refs/workspace-active-theme-sets-tokens)
active-theme-tokens (cond-> (mf/deref refs/workspace-active-theme-sets-tokens)
;; Ensure that the resolved value uses the currently editing token
;; even if the name has been overriden by a token with the same name
;; in another set below.
(and (:name token) (:value token))
(assoc (:name token) token))
resolved-tokens (sd/use-resolved-tokens active-theme-tokens {:cache-atom form-token-cache-atom
:interactive? true})
token-path (mf/use-memo

View file

@ -7,6 +7,7 @@
(ns app.main.ui.workspace.tokens.modals
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
@ -20,30 +21,55 @@
(defn calculate-position
"Calculates the style properties for the given coordinates and position"
[{vh :height} position x y]
(let [;; picker height in pixels
h 510
[{vh :height} position x y color?]
(let [;; picker height in pixels
;; TODO: Revisit these harcoded values
h (if color? 610 510)
;; Checks for overflow outside the viewport height
overflow-fix (max 0 (+ y (- 50) h (- vh)))
x-pos 325]
max-y (- vh h)
overflow-fix (max 0 (+ y (- 50) h (- vh)))
bottom-offset "1rem"
top-offset (dm/str (- y 70) "px")
max-height-top (str "calc(100vh - " top-offset)
max-height-bottom (str "calc(100vh -" bottom-offset)
x-pos 325
rulers? (mf/deref refs/rulers?)
left-offset (if rulers? 80 58)
left-position (dm/str (- x x-pos) "px")]
(cond
(or (nil? x) (nil? y)) {:left "auto" :right "16rem" :top "4rem"}
(= position :left) {:left (str (- x x-pos) "px")
:top (str (- y 50 overflow-fix) "px")}
:else {:left (str (+ x 80) "px")
:top (str (- y 70 overflow-fix) "px")})))
(or (nil? x) (nil? y))
{:left "auto" :right "16rem" :top "4rem"}
(defn use-viewport-position-style [x y position]
(= position :left)
(if (> y max-y)
{:left left-position
:bottom bottom-offset
:maxHeight max-height-bottom}
{:left left-position
:maxHeight max-height-top
:top (dm/str (- y 50 overflow-fix) "px")})
:else
(if (> y max-y)
{:left (dm/str (+ x left-offset) "px")
:bottom bottom-offset
:maxHeight max-height-bottom}
{:left (dm/str (+ x left-offset) "px")
:top (dm/str (- y 70 overflow-fix) "px")
:maxHeight max-height-top}))))
(defn use-viewport-position-style [x y position color?]
(let [vport (-> (l/derived :vport refs/workspace-local)
(mf/deref))]
(-> (calculate-position vport position x y)
(-> (calculate-position vport position x y color?)
(clj->js))))
(mf/defc token-update-create-modal
{::mf/wrap-props false}
[{:keys [x y position token token-type action selected-token-set-name] :as _args}]
(let [wrapper-style (use-viewport-position-style x y position)
(let [wrapper-style (use-viewport-position-style x y position (= token-type :color))
modal-size-large* (mf/use-state false)
modal-size-large? (deref modal-size-large*)
close-modal (mf/use-fn

View file

@ -160,7 +160,7 @@
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(when-not is-editing
(when (and can-edit? (not is-editing))
(st/emit! (dt/assign-token-set-context-menu
{:position (dom/get-client-position event)
:is-group true

View file

@ -233,6 +233,7 @@
(mf/defc token-sets-section*
{::mf/private true}
[{:keys [resize-height] :as props}]
(let [can-edit?
(mf/use-ctx ctx/can-edit?)]

View file

@ -336,7 +336,7 @@
[tokens & {:keys [interactive?]}]
(let [state* (mf/use-state tokens)]
(mf/with-effect [tokens interactive?]
(when (seq tokens)
(if (seq tokens)
(let [tpoint (dt/tpoint-ms)
promise (if interactive?
(resolve-tokens-interactive+ tokens)
@ -346,5 +346,6 @@
(p/fmap (fn [resolved-tokens]
(let [elapsed (tpoint)]
(l/dbg :hint "use-resolved-tokens*" :elapsed elapsed)
(reset! state* resolved-tokens))))))))
(reset! state* resolved-tokens))))))
(reset! state* tokens)))
@state*))

View file

@ -10,7 +10,6 @@
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.media :as cm]
[app.config :as cf]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.workspace :as dw]
@ -126,8 +125,7 @@
profile (mf/deref refs/profile)
props (get profile :props)
test-tooltip-board-text
(if (and (cf/external-feature-flag "boards-03" "test")
(not (:workspace-visited props)))
(if (not (:workspace-visited props))
(tr "workspace.toolbar.frame-first-time" (sc/get-tooltip :draw-frame))
(tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame)))]