diff --git a/backend/src/app/features/fdata.clj b/backend/src/app/features/fdata.clj index 8a57a1aa17..4715c5bfb5 100644 --- a/backend/src/app/features/fdata.clj +++ b/backend/src/app/features/fdata.clj @@ -22,12 +22,21 @@ (defn enable-objects-map [file] - (let [update-fn #(d/update-when % :objects omap/wrap)] + (let [update-container + (fn [container] + (if (and (pmap/pointer-map? container) + (not (pmap/loaded? container))) + container + (d/update-when container :objects omap/wrap))) + + update-data + (fn [fdata] + (-> fdata + (update :pages-index d/update-vals update-container) + (d/update-when :components d/update-vals update-container)))] + (-> file - (update :data (fn [fdata] - (-> fdata - (update :pages-index update-vals update-fn) - (d/update-when :components update-vals update-fn)))) + (update :data update-data) (update :features conj "fdata/objects-map")))) (defn process-objects @@ -72,14 +81,15 @@ "Given a database connection and the final file-id, persist all pointers to the underlying storage (the database)." [system file-id] - (doseq [[id item] @pmap/*tracked*] - (when (pmap/modified? item) - (l/trc :hint "persist pointer" :file-id (str file-id) :id (str id)) - (let [content (-> item deref blob/encode)] - (db/insert! system :file-data-fragment - {:id id - :file-id file-id - :content content}))))) + (let [conn (db/get-connection system)] + (doseq [[id item] @pmap/*tracked*] + (when (pmap/modified? item) + (l/trc :hint "persist pointer" :file-id (str file-id) :id (str id)) + (let [content (-> item deref blob/encode)] + (db/insert! conn :file-data-fragment + {:id id + :file-id file-id + :content content})))))) (defn process-pointers "Apply a function to all pointers on the file. Usuly used for diff --git a/backend/src/app/rpc/commands/files_update.clj b/backend/src/app/rpc/commands/files_update.clj index cd7436742e..9c7c5fb4b0 100644 --- a/backend/src/app/rpc/commands/files_update.clj +++ b/backend/src/app/rpc/commands/files_update.clj @@ -30,7 +30,6 @@ [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.util.blob :as blob] - [app.util.objects-map :as omap] [app.util.pointer-map :as pmap] [app.util.services :as sv] [app.util.time :as dt] @@ -119,18 +118,11 @@ [f] (fn [cfg {:keys [id] :as file}] (binding [pmap/*tracked* (pmap/create-tracked) - pmap/*load-fn* (partial feat.fdata/load-pointer cfg id) - cfeat/*wrap-with-pointer-map-fn* pmap/wrap] + pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] (let [result (f cfg file)] (feat.fdata/persist-pointers! cfg id) result)))) -(defn- wrap-with-objects-map-context - [f] - (fn [cfg file] - (binding [cfeat/*wrap-with-objects-map-fn* omap/wrap] - (f cfg file)))) - (declare get-lagged-changes) (declare send-notifications!) (declare update-file) @@ -199,10 +191,7 @@ update-fn (cond-> update-file* (contains? features "fdata/pointer-map") - (wrap-with-pointer-map-context) - - (contains? features "fdata/objects-map") - (wrap-with-objects-map-context)) + (wrap-with-pointer-map-context)) changes (if changes-with-metadata (->> changes-with-metadata (mapcat :changes) vec) @@ -328,6 +317,7 @@ ;; leeave it on lazy status (-> (files/get-file cfg id :migrate? false) (update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved + (update :data feat.fdata/process-objects (partial into {})) (fmg/migrate-file)))))) (d/index-by :id))) diff --git a/backend/src/app/srepl/helpers.clj b/backend/src/app/srepl/helpers.clj index 9df761db02..07b89ab7cd 100644 --- a/backend/src/app/srepl/helpers.clj +++ b/backend/src/app/srepl/helpers.clj @@ -85,6 +85,14 @@ {:id id}) team)) +(defn get-raw-file + "Get the migrated data of one file." + ([id] (get-raw-file (or *system* main/system) id)) + ([system id] + (db/run! system + (fn [system] + (files/get-file system id :migrate? false))))) + (defn reset-file-data! "Hardcode replace of the data of one file." [system id data] diff --git a/backend/src/app/util/pointer_map.clj b/backend/src/app/util/pointer_map.clj index bb7b252939..f5933ecd6a 100644 --- a/backend/src/app/util/pointer_map.clj +++ b/backend/src/app/util/pointer_map.clj @@ -68,6 +68,7 @@ (get-id [_]) (load! [_]) (modified? [_]) + (loaded? [_]) (clone [_])) (deftype PointerMap [id mdata @@ -90,6 +91,7 @@ (or odata {})) (modified? [_] modified?) + (loaded? [_] loaded?) (get-id [_] id) (clone [this] @@ -210,8 +212,6 @@ (defn create ([] (let [id (uuid/next) - - mdata (assoc *metadata* :created-at (dt/now)) pmap (PointerMap. id mdata {} true true)] (some-> *tracked* (swap! assoc id pmap)) diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index bcfd556488..0dcf23e7ed 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -7,7 +7,7 @@ (ns app.common.data "A collection if helpers for working with data structures and other data resources." - (:refer-clojure :exclude [read-string hash-map merge name + (:refer-clojure :exclude [read-string hash-map merge name update-vals parse-double group-by iteration concat mapcat parse-uuid max min]) #?(:cljs @@ -403,6 +403,13 @@ [coll] (partial get coll)) +(defn update-vals + [m f] + (reduce-kv (fn [acc k v] + (assoc acc k (f v))) + m + m)) + (defn update-in-when [m key-seq f & args] (let [found (get-in m key-seq sentinel)] diff --git a/common/src/app/common/geom/proportions.cljc b/common/src/app/common/geom/proportions.cljc index 8294e4301c..d6f37c2167 100644 --- a/common/src/app/common/geom/proportions.cljc +++ b/common/src/app/common/geom/proportions.cljc @@ -22,8 +22,8 @@ :proportion (/ width height) :proportion-lock true))) -(defn setup-proportions-svg - [{:keys [width height] :as shape}] +(defn setup-proportions-size + [{{:keys [width height]} :selrect :as shape}] (assoc shape :proportion (/ width height) :proportion-lock true)) @@ -35,9 +35,11 @@ :proportion-lock false)) (defn setup-proportions - [shape] - (case (:type shape) - :svg-raw (setup-proportions-svg shape) - :image (setup-proportions-image shape) - :text shape - (setup-proportions-const shape))) + [{:keys [type] :as shape}] + (let [image-fill? (every? #(some? (:fill-image %)) (:fills shape))] + (cond + (= type :svg-raw) (setup-proportions-size shape) + (= type :image) (setup-proportions-image shape) + image-fill? (setup-proportions-size shape) + (= type :text) shape + :else (setup-proportions-const shape)))) diff --git a/common/src/app/common/geom/shapes/flex_layout/params.cljc b/common/src/app/common/geom/shapes/flex_layout/params.cljc index 7bd1ce855f..b1e15c70bd 100644 --- a/common/src/app/common/geom/shapes/flex_layout/params.cljc +++ b/common/src/app/common/geom/shapes/flex_layout/params.cljc @@ -88,8 +88,8 @@ parent-selrect (:selrect parent) padding (when (and (not (nil? parent)) (> (count shapes) 0)) - {:p1 (min (- min-y (:y1 parent-selrect)) (- (:y2 parent-selrect) max-y)) - :p2 (min (- min-x (:x1 parent-selrect)) (- (:x2 parent-selrect) max-x))})] + {:p1 (- min-y (:y1 parent-selrect)) + :p2 (- min-x (:x1 parent-selrect))})] (cond-> {:layout-flex-dir direction :layout-gap layout-gap} (not (nil? padding)) diff --git a/common/src/app/common/types/color.cljc b/common/src/app/common/types/color.cljc index 3a726d77a5..382530ac2a 100644 --- a/common/src/app/common/types/color.cljc +++ b/common/src/app/common/types/color.cljc @@ -52,7 +52,8 @@ [:width :int] [:height :int] [:mtype {:optional true} [:maybe :string]] - [:id ::sm/uuid]]) + [:id ::sm/uuid] + [:keep-aspect-ratio {:optional true} :boolean]]) (sm/define! ::gradient [:map {:title "Gradient"} diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index 17d0d1bc1b..9014481973 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -35,12 +35,6 @@ {:entries [app.main.ui.viewer] :depends-on #{:main :main-auth}} - :main-onboarding - {:entries [app.main.ui.onboarding - app.main.ui.onboarding.questions - app.main.ui.releases] - :depends-on #{:main}} - :main-workspace {:entries [app.main.ui.workspace] :depends-on #{:main}} diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index 9f19e14e66..ada7e31af2 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -254,36 +254,39 @@ (prepare-restore-component nil library-data component-id it page (gpt/point 0 0) nil nil nil))) ([changes library-data component-id it page delta old-id parent-id frame-id] - (let [component (ctkl/get-deleted-component library-data component-id) - parent (get-in page [:objects parent-id]) - main-inst (get-in component [:objects (:main-instance-id component)]) + (let [component (ctkl/get-deleted-component library-data component-id) + parent (get-in page [:objects parent-id]) + main-inst (get-in component [:objects (:main-instance-id component)]) inside-component? (some? (ctn/get-instance-root (:objects page) parent)) + origin-frame (get-in page [:objects (:frame-id main-inst)]) + ;; We are using a deleted component andit's coordenates are absolute, we must adjust them to its containing frame to adjust the delta + delta (gpt/subtract delta (-> origin-frame :selrect gpt/point)) + shapes (cfh/get-children-with-self (:objects component) (:main-instance-id component)) + shapes (map #(gsh/move % delta) shapes) - shapes (cfh/get-children-with-self (:objects component) (:main-instance-id component)) - shapes (map #(gsh/move % delta) shapes) - first-shape (cond-> (first shapes) - (not (nil? parent-id)) - (assoc :parent-id parent-id) - (not (nil? frame-id)) - (assoc :frame-id frame-id) - (and (nil? frame-id) parent (= :frame (:type parent))) - (assoc :frame-id parent-id) - (and (nil? frame-id) parent (not= :frame (:type parent))) - (assoc :frame-id (:frame-id parent)) - inside-component? - (dissoc :component-root) - (not inside-component?) - (assoc :component-root true)) + first-shape (cond-> (first shapes) + (not (nil? parent-id)) + (assoc :parent-id parent-id) + (not (nil? frame-id)) + (assoc :frame-id frame-id) + (and (nil? frame-id) parent (= :frame (:type parent))) + (assoc :frame-id parent-id) + (and (nil? frame-id) parent (not= :frame (:type parent))) + (assoc :frame-id (:frame-id parent)) + inside-component? + (dissoc :component-root) + (not inside-component?) + (assoc :component-root true)) - changes (-> (or changes (pcb/empty-changes it)) - (pcb/with-page page) - (pcb/with-objects (:objects page)) - (pcb/with-library-data library-data)) - changes (cond-> (pcb/add-object changes first-shape {:ignore-touched true}) - (some? old-id) (pcb/amend-last-change #(assoc % :old-id old-id))) ; on copy/paste old id is used later to reorder the paster layers - changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) - changes - (rest shapes))] + changes (-> (or changes (pcb/empty-changes it)) + (pcb/with-page page) + (pcb/with-objects (:objects page)) + (pcb/with-library-data library-data)) + changes (cond-> (pcb/add-object changes first-shape {:ignore-touched true}) + (some? old-id) (pcb/amend-last-change #(assoc % :old-id old-id))) ; on copy/paste old id is used later to reorder the paster layers + changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) + changes + (rest shapes))] {:changes (pcb/restore-component changes component-id (:id page) main-inst) :shape (first shapes)}))) diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs index c68dfe00a2..3dc6c8d7a1 100644 --- a/frontend/src/app/main/data/workspace/media.cljs +++ b/frontend/src/app/main/data/workspace/media.cljs @@ -72,7 +72,8 @@ :width width :height height :mtype mtype - :id id}}]}] + :id id + :keep-aspect-ratio true}}]}] (rx/of (dwsh/create-and-add-shape :rect x y shape)))))) (defn svg-uploaded @@ -358,7 +359,8 @@ :id id :width width :height height - :mtype mtype}}] + :mtype mtype + :keep-aspect-ratio true}}] :name name :frame-id (:id frame-shape) :parent-id (:id frame-shape)})] diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 2c70a31cd2..a16bd93f0f 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -90,8 +90,7 @@ (defmethod ptk/handle-error :default [error] - (when-let [cause (::instance error)] - (ts/schedule #(st/emit! (rt/assign-exception cause)))) + (st/async-emit! (rt/assign-exception error)) (print-group! "Unhandled Error" (fn [] (print-trace! error) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 68bd5d58af..8d6306075a 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -15,6 +15,9 @@ [app.main.ui.frame-preview :as frame-preview] [app.main.ui.icons :as i] [app.main.ui.messages :as msgs] + [app.main.ui.onboarding :refer [onboarding-modal]] + [app.main.ui.onboarding.questions :refer [questions-modal]] + [app.main.ui.releases :refer [release-notes-modal]] [app.main.ui.static :as static] [app.util.dom :as dom] [app.util.i18n :refer [tr]] @@ -39,15 +42,6 @@ (def workspace-page (mf/lazy-component app.main.ui.workspace/workspace)) -(def questions-modal - (mf/lazy-component app.main.ui.onboarding.questions/questions)) - -(def onboarding-modal - (mf/lazy-component app.main.ui.onboarding/onboarding-modal)) - -(def release-modal - (mf/lazy-component app.main.ui.releases/release-notes-modal)) - (mf/defc on-main-error [{:keys [error] :as props}] (mf/with-effect @@ -55,7 +49,8 @@ [:span "Internal application error"]) (mf/defc main-page - {::mf/wrap [#(mf/catch % {:fallback on-main-error})]} + {::mf/wrap [#(mf/catch % {:fallback on-main-error})] + ::mf/props :obj} [{:keys [route profile]}] (let [{:keys [data params]} route] [:& (mf/provider ctx/current-route) {:value route} @@ -116,7 +111,7 @@ (:onboarding-viewed props) (not= (:release-notes-viewed props) (:main cf/version)) (not= "0.0" (:main cf/version))) - [:& release-modal {:version (:main cf/version)}])) + [:& release-notes-modal {:version (:main cf/version)}])) (when profile [:& dashboard-page {:route route :profile profile}])] diff --git a/frontend/src/app/main/ui/components/radio_buttons.cljs b/frontend/src/app/main/ui/components/radio_buttons.cljs index 92563917a9..a4366ba2a9 100644 --- a/frontend/src/app/main/ui/components/radio_buttons.cljs +++ b/frontend/src/app/main/ui/components/radio_buttons.cljs @@ -17,9 +17,9 @@ (mf/create-context nil)) (mf/defc radio-button - {::mf/wrap-props false} + {::mf/props :obj} [props] - (let [context (mf/use-ctx context) + (let [context (mf/use-ctx context) icon (unchecked-get props "icon") id (unchecked-get props "id") value (unchecked-get props "value") @@ -27,7 +27,10 @@ title (unchecked-get props "title") unique-key (unchecked-get props "unique-key") icon-class (unchecked-get props "icon-class") - type (or (unchecked-get props "type") "radio") + type (or (unchecked-get props "type") + (if (unchecked-get context "allow-empty") + "checkbox" + "radio")) on-change (unchecked-get context "on-change") selected (unchecked-get context "selected") @@ -59,14 +62,15 @@ :checked checked?}]])) (mf/defc radio-buttons - {::mf/wrap-props false} + {::mf/props :obj} [props] - (let [children (unchecked-get props "children") - on-change (unchecked-get props "on-change") - selected (unchecked-get props "selected") - name (unchecked-get props "name") - class (unchecked-get props "class") - wide (unchecked-get props "wide") + (let [children (unchecked-get props "children") + on-change (unchecked-get props "on-change") + selected (unchecked-get props "selected") + name (unchecked-get props "name") + class (unchecked-get props "class") + wide (unchecked-get props "wide") + allow-empty? (unchecked-get props "allow-empty") encode-fn (d/nilv (unchecked-get props "encode-fn") identity) decode-fn (d/nilv (unchecked-get props "encode-fn") identity) @@ -87,7 +91,8 @@ (mf/deps on-change) (fn [event] (let [input-node (dom/get-target event) - value (dom/get-target-val event)] + value (dom/get-target-val event) + value (when (not= value selected) value)] (when (fn? on-change) (do (on-change (decode-fn value) event) (dom/blur! input-node)))))) @@ -98,7 +103,8 @@ :on-change on-change' :name name :encode-fn encode-fn - :decode-fn decode-fn})] + :decode-fn decode-fn + :allow-empty allow-empty?})] [:& (mf/provider context) {:value context-value} [:div {:class (dm/str class " " (stl/css :radio-btn-wrapper)) diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss index be1f9a6139..51cd9ac4f0 100644 --- a/frontend/src/app/main/ui/dashboard/team.scss +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -727,4 +727,5 @@ .email-input { @extend .input-base; + height: auto; } diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs index b16589df32..00a19a11a0 100644 --- a/frontend/src/app/main/ui/onboarding.cljs +++ b/frontend/src/app/main/ui/onboarding.cljs @@ -133,7 +133,6 @@ :data-test "onboarding-next-btn"} (tr "labels.continue")]]]])) - (mf/defc onboarding-modal {::mf/register modal/components ::mf/register-as :onboarding} diff --git a/frontend/src/app/main/ui/onboarding/questions.cljs b/frontend/src/app/main/ui/onboarding/questions.cljs index d324b16992..3d72a10112 100644 --- a/frontend/src/app/main/ui/onboarding/questions.cljs +++ b/frontend/src/app/main/ui/onboarding/questions.cljs @@ -206,8 +206,11 @@ :default "" :name :team-size}]]])) -(mf/defc questions - [{:keys []}] +;; NOTE: we don't register it on registry modal because we reference +;; this modal directly on the ui namespace. + +(mf/defc questions-modal + [] (let [container (mf/use-ref) step (mf/use-state 1) clean-data (mf/use-state {}) diff --git a/frontend/src/app/main/ui/releases.cljs b/frontend/src/app/main/ui/releases.cljs index 084d77dfdc..fc01fe2943 100644 --- a/frontend/src/app/main/ui/releases.cljs +++ b/frontend/src/app/main/ui/releases.cljs @@ -33,7 +33,7 @@ ;;; --- RELEASE NOTES MODAL (mf/defc release-notes - {::mf/wrap-props false} + {::mf/props :obj} [{:keys [version]}] (let [slide* (mf/use-state :start) slide (deref slide*) @@ -90,4 +90,4 @@ (defmethod rc/render-release-notes "0.0" [params] - (rc/render-release-notes (assoc params :version "1.18"))) + (rc/render-release-notes (assoc params :version "1.19"))) diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs index 71cd5bf461..366a0df9ad 100644 --- a/frontend/src/app/main/ui/settings/sidebar.cljs +++ b/frontend/src/app/main/ui/settings/sidebar.cljs @@ -20,52 +20,44 @@ [potok.v2.core :as ptk] [rumext.v2 :as mf])) +(def ^:private go-settings-profile + #(st/emit! (rt/nav :settings-profile))) + +(def ^:private go-settings-feedback + #(st/emit! (rt/nav :settings-feedback))) + +(def ^:private go-settings-password + #(st/emit! (rt/nav :settings-password))) + +(def ^:private go-settings-options + #(st/emit! (rt/nav :settings-options))) + +(def ^:private go-settings-access-tokens + #(st/emit! (rt/nav :settings-access-tokens))) + +(defn- show-release-notes + [event] + (let [version (:main cf/version)] + (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) + + (if (and (kbd/alt? event) (kbd/mod? event)) + (st/emit! (modal/show {:type :onboarding})) + (st/emit! (modal/show {:type :release-notes :version version}))))) + (mf/defc sidebar-content - [{:keys [profile section] :as props}] + {::mf/props :obj} + [{:keys [profile section]}] (let [profile? (= section :settings-profile) password? (= section :settings-password) options? (= section :settings-options) feedback? (= section :settings-feedback) access-tokens? (= section :settings-access-tokens) + team-id (du/get-current-team-id profile) go-dashboard - (mf/use-callback - (mf/deps profile) - #(st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)}))) - - go-settings-profile - (mf/use-callback - (mf/deps profile) - #(st/emit! (rt/nav :settings-profile))) - - go-settings-feedback - (mf/use-callback - (mf/deps profile) - #(st/emit! (rt/nav :settings-feedback))) - - go-settings-password - (mf/use-callback - (mf/deps profile) - #(st/emit! (rt/nav :settings-password))) - - go-settings-options - (mf/use-callback - (mf/deps profile) - #(st/emit! (rt/nav :settings-options))) - - go-settings-access-tokens - (mf/use-callback - (mf/deps profile) - #(st/emit! (rt/nav :settings-access-tokens))) - - show-release-notes - (mf/use-callback - (fn [event] - (let [version (:main cf/version)] - (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) - (if (and (kbd/alt? event) (kbd/mod? event)) - (st/emit! (modal/show {:type :onboarding})) - (st/emit! (modal/show {:type :release-notes :version version}))))))] + (mf/use-fn + (mf/deps team-id) + #(st/emit! (rt/nav :dashboard-projects {:team-id team-id})))] [:div {:class (stl/css :sidebar-content)} [:div {:class (stl/css :sidebar-content-section)} @@ -73,6 +65,7 @@ :on-click go-dashboard} [:span {:class (stl/css :icon)} i/arrow-down] [:span {:class (stl/css :text)} (tr "labels.dashboard")]]] + [:hr] [:div {:class (stl/css :sidebar-content-section)} @@ -108,7 +101,8 @@ [:span {:class (stl/css :element-title)} (tr "labels.give-feedback")]])]]])) (mf/defc sidebar - {::mf/wrap [mf/memo]} + {::mf/wrap [mf/memo] + ::mf/props :obj} [{:keys [profile locale section]}] [:div {:class (stl/css :dashboard-sidebar :settings)} [:& sidebar-content {:profile profile diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs index e4cb1eee34..076a662c04 100644 --- a/frontend/src/app/main/ui/shapes/fills.cljs +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -117,9 +117,10 @@ :style style}] (if (:fill-image value) (let [uri (cf/resolve-file-media (:fill-image value)) + keep-ar? (-> value :fill-image :keep-aspect-ratio) image-props #js {:id (dm/str "fill-image-" render-id "-" fill-index) :href (get uris uri uri) - :preserveAspectRatio "xMidYMid slice" + :preserveAspectRatio (if keep-ar? "xMidYMid slice" "none") :width width :height height :key (dm/str fill-index) diff --git a/frontend/src/app/main/ui/workspace.scss b/frontend/src/app/main/ui/workspace.scss index 0c7df32056..0cc517eae5 100644 --- a/frontend/src/app/main/ui/workspace.scss +++ b/frontend/src/app/main/ui/workspace.scss @@ -29,6 +29,7 @@ grid-template-areas: "left-sidebar viewport right-sidebar"; grid-template-rows: 1fr; grid-template-columns: auto 1fr auto; + overflow: hidden; .workspace-loader { @include flexCenter; diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index e74f835251..dbd0da26a9 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -84,7 +84,10 @@ on-fill-image-success (mf/use-fn (fn [image] - (st/emit! (dc/update-colorpicker-color {:image (select-keys image [:id :width :height :mtype :name])} (not @drag?))))) + (st/emit! (dc/update-colorpicker-color + {:image (-> (select-keys image [:id :width :height :mtype :name]) + (assoc :keep-aspect-ratio true))} + (not @drag?))))) on-fill-image-click (mf/use-callback #(dom/click (mf/ref-val fill-image-ref))) @@ -94,6 +97,16 @@ (fn [file] (st/emit! (dwm/upload-fill-image file on-fill-image-success)))) + handle-change-keep-aspect-ratio + (mf/use-fn + (mf/deps current-color) + (fn [] + (let [keep-aspect-ratio? (-> current-color :image :keep-aspect-ratio not)] + (st/emit! (dc/update-colorpicker-color + {:image (-> (:image current-color) + (assoc :keep-aspect-ratio keep-aspect-ratio?))} + true))))) + set-tab! (mf/use-fn (fn [event] @@ -248,11 +261,24 @@ :on-select-stop handle-change-stop}]) (if (= selected-mode :image) - (let [uri (cfg/resolve-file-media (:image current-color))] + (let [uri (cfg/resolve-file-media (:image current-color)) + keep-aspect-ratio? (-> current-color :image :keep-aspect-ratio)] [:div {:class (stl/css :select-image)} [:div {:class (stl/css :content)} (when (:image current-color) [:img {:src uri}])] + + (when (some? (:image current-color)) + [:div {:class (stl/css :checkbox-option)} + [:label {:for "keep-aspect-ratio" + :class (stl/css-case :global/checked keep-aspect-ratio?)} + [:span {:class (stl/css-case :global/checked keep-aspect-ratio?)} + (when keep-aspect-ratio? i/status-tick-refactor)] + (tr "media.keep-aspect-ratio") + [:input {:type "checkbox" + :id "keep-aspect-ratio" + :checked keep-aspect-ratio? + :on-change handle-change-keep-aspect-ratio}]]]) [:button {:class (stl/css :choose-image) :title (tr "media.choose-image") diff --git a/frontend/src/app/main/ui/workspace/colorpicker.scss b/frontend/src/app/main/ui/workspace/colorpicker.scss index f3049f3878..62a7e55271 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker.scss @@ -166,3 +166,8 @@ margin-top: $s-12; height: $s-32; } + +.checkbox-option { + @extend .input-checkbox; + margin: $s-16 0 0 0; +} diff --git a/frontend/src/app/main/ui/workspace/palette.scss b/frontend/src/app/main/ui/workspace/palette.scss index 995d886a00..70f0639e03 100644 --- a/frontend/src/app/main/ui/workspace/palette.scss +++ b/frontend/src/app/main/ui/workspace/palette.scss @@ -39,10 +39,7 @@ &.wide { width: 100%; } - &.mid-palette, - &.small-palette { - grid-template-columns: $s-64 auto 1fr; - } + .resize-area { grid-area: resize; height: $s-8; @@ -108,56 +105,62 @@ width: 100%; min-width: 0; } - .handler { - @include buttonStyle; - @include flexCenter; - width: $s-12; +} + +.handler { + @include buttonStyle; + @include flexCenter; + width: $s-12; + height: 100%; + .handler-btn { + width: $s-4; height: 100%; - .handler-btn { - width: $s-4; - height: 100%; - max-height: $s-40; - margin: $s-8 $s-4; - padding: 0; - border-radius: $s-4; - background-color: var(--palette-handler-background-color); - } - } - &.hidden-bts { - right: 10px; - z-index: 1; - width: 22px; - grid-template-columns: $s-8 auto 1fr; + max-height: $s-40; + margin: $s-8 $s-4; padding: 0; - &.small-palette, - &.mid-palette { - right: 10px; - } - .palette-btn-list { - opacity: $op-0; - visibility: hidden; - width: 0; - .palette-item { - opacity: $op-0; - visibility: hidden; - z-index: 0; - } - } - .resize-area { - visibility: hidden; - z-index: 0; - width: 0; - } - .palette-actions { - visibility: hidden; - z-index: 0; - } - .palette { - visibility: hidden; - z-index: 0; - } - .handler { - padding-bottom: $s-8; - } + border-radius: $s-4; + background-color: var(--palette-handler-background-color); + } +} + +.mid-palette, +.small-palette { + grid-template-columns: $s-64 auto 1fr; +} + +.hidden-bts { + right: $s-2; + z-index: $z-index-1; + width: 22px; + grid-template-columns: $s-8 auto 1fr; + padding: 0; + border-inline-start: 0; + border-start-start-radius: 0; + border-end-start-radius: 0; + .palette-btn-list { + opacity: $op-0; + visibility: hidden; + width: 0; + .palette-item { + opacity: $op-0; + visibility: hidden; + z-index: 0; + } + } + .resize-area { + visibility: hidden; + z-index: 0; + width: 0; + } + .palette-actions { + visibility: hidden; + z-index: 0; + } + .palette { + visibility: hidden; + z-index: 0; + } + .handler { + padding-bottom: $s-8; } } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs index 8ea0346d3b..6929eb5050 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs @@ -230,7 +230,8 @@ [{:keys [is-col? align-self on-change] :as props}] [:& radio-buttons {:selected (d/name align-self) :on-change on-change - :name "flex-align-self"} + :name "flex-align-self" + :allow-empty true} [:& radio-button {:value "start" :icon (get-layout-flex-icon :align-self :start is-col?) :title "Align self start" diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index e3e2e874c4..121efb9d7f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -27,6 +27,10 @@ [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) + +(def ^:private detach-icon + (i/icon-xref :detach-refactor (stl/css :detach-icon))) + (defn opacity->string [opacity] (if (= opacity :multiple) @@ -189,14 +193,14 @@ :dnd-over-top (= (:over dprops) :top) :dnd-over-bot (= (:over dprops) :bot)) :ref dref} - [:span {:class (stl/css :color-info)} - [:span {:class (stl/css-case :color-name-wrapper true - :no-opacity (or disable-opacity - (not opacity?)) - :library-name-wrapper library-color? - :editing editing-text? - :gradient-name-wrapper gradient-color?)} - [:span {:class (stl/css :color-bullet-wrapper)} + [:div {:class (stl/css :color-info)} + [:div {:class (stl/css-case :color-name-wrapper true + :no-opacity (or disable-opacity + (not opacity?)) + :library-name-wrapper library-color? + :editing editing-text? + :gradient-name-wrapper gradient-color?)} + [:div {:class (stl/css :color-bullet-wrapper)} [:& cbn/color-bullet {:color (cond-> color (nil? color-name) (assoc :id nil @@ -218,7 +222,7 @@ :on-pointer-enter #(reset! hover-detach true) :on-pointer-leave #(reset! hover-detach false) :on-click detach-value} - i/detach-refactor])] + detach-icon])] ;; Rendering a gradient gradient-color? diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss index ed7f7bc3ae..f02cf1aeb7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss @@ -10,14 +10,16 @@ @include flexRow; &.dnd-over-top { - border-top: $s-1 solid var(--layer-row-foreground-color-drag); + border-block-start: $s-1 solid var(--layer-row-foreground-color-drag); } &.dnd-over-bot { - border-bottom: $s-1 solid var(--layer-row-foreground-color-drag); + border-block-end: $s-1 solid var(--layer-row-foreground-color-drag); } } .color-info { + --detach-icon-foreground-color: none; + display: flex; align-items: center; gap: $s-2; @@ -26,147 +28,153 @@ height: $s-32; width: 100%; flex-grow: 1; + min-width: 0; - .color-name-wrapper { - @extend .input-element; - flex-grow: 1; - width: 100%; - border-radius: $br-8 0 0 $br-8; - padding: 0; - margin-right: 0; - gap: $s-4; - input { - padding: 0; - } - .color-bullet-wrapper { - height: $s-28; - padding: 0 $s-2 0 $s-8; - border-radius: $br-8 0 0 $br-8; + &:hover { + --detach-icon-foreground-color: var(--input-foreground-color-active); + + .detach-btn, + .select-btn { background-color: transparent; - &:hover { - background-color: transparent; - } } - .color-name { - @include titleTipography; - @include textEllipsis; - padding-inline: $s-6; + } +} + +.color-name-wrapper { + @extend .input-element; + flex-grow: 1; + width: 100%; + border-radius: $br-8 0 0 $br-8; + padding: 0; + margin-inline-end: 0; + gap: $s-4; + input { + padding: 0; + } + .color-bullet-wrapper { + height: $s-28; + padding: 0 $s-2 0 $s-8; + border-radius: $br-8 0 0 $br-8; + background-color: transparent; + display: flex; + align-items: center; + &:hover { + background-color: transparent; + } + } + .color-name { + @include titleTipography; + @include textEllipsis; + padding-inline: $s-6; + border-radius: $br-8; + color: var(--input-foreground-color-active); + } + .detach-btn { + @extend .button-tertiary; + height: $s-28; + width: $s-28; + margin-inline-start: auto; + border-radius: 0 $br-8 $br-8 0; + display: none; + } + .detach-icon { + @extend .button-icon; + stroke: var(--detach-icon-foreground-color); + } + .color-input-wrapper { + @include titleTipography; + display: flex; + align-items: center; + height: $s-28; + padding: 0 $s-0; + width: 100%; + margin: 0; + flex-grow: 1; + background-color: var(--input-background-color); + color: var(--input-foreground-color); + border-radius: $br-0; + } + &.no-opacity { + border-radius: $br-8; + .color-input-wrapper { border-radius: $br-8; - color: var(--input-foreground-color-active); + } + } + &:hover { + --detach-icon-foreground-color: var(--input-foreground-color-active); + + background-color: var(--input-background-color-hover); + border: $s-1 solid var(--input-border-color-hover); + .color-bullet-wrapper, + .color-name, + .detach-btn, + .color-input-wrapper { + background-color: var(--input-background-color-hover); } .detach-btn { - @extend .button-tertiary; - height: $s-28; - width: $s-28; - border-radius: 0 $br-8 $br-8 0; - display: none; - svg { - @extend .button-icon; - } - } - .color-input-wrapper { - @include titleTipography; display: flex; - align-items: center; - height: $s-28; - padding: 0 $s-0; - width: 100%; - margin: 0; - flex-grow: 1; - background-color: var(--input-background-color); - color: var(--input-foreground-color); - border-radius: $br-0; } - &.no-opacity { - border-radius: $br-8; - .color-input-wrapper { - border-radius: $br-8; - } - } - &:hover { - background-color: var(--input-background-color-hover); - border: $s-1 solid var(--input-border-color-hover); + &.editing { + background-color: var(--input-background-color-active); .color-bullet-wrapper, .color-name, .detach-btn, .color-input-wrapper { - background-color: var(--input-background-color-hover); - } - .detach-btn { - display: flex; - svg { - stroke: var(--input-foreground-color-active); - } - } - &.editing { background-color: var(--input-background-color-active); - .color-bullet-wrapper, - .color-name, - .detach-btn, - .color-input-wrapper { - background-color: var(--input-background-color-active); - } - } - &:focus, - &:focus-within { - background-color: var(--input-background-color-focus); - border: $s-1 solid var(--input-border-color-focus); } } - &:focus, &:focus-within { background-color: var(--input-background-color-focus); border: $s-1 solid var(--input-border-color-focus); - &:hover { - background-color: var(--input-background-color-hover); - border: $s-1 solid var(--input-border-color-focus); - } - } - - &.editing { - background-color: var(--input-background-color-active); - &:hover { - border: $s-1 solid var(--input-border-color-active); - } - } - } - .gradient-name-wrapper { - border-radius: 0 $br-8 $br-8 0; - .color-name { - @include flexRow; - border-radius: 0 $br-8 $br-8 0; - } - } - .library-name-wrapper { - border-radius: $br-8; - } - .opacity-element-wrapper { - @extend .input-element; - width: $s-60; - border-radius: 0 $br-8 $br-8 0; - .opacity-input { - padding: 0; - border-radius: 0 $br-8 $br-8 0; - min-width: $s-28; - } - .icon-text { - @include flexCenter; - height: $s-32; - margin-right: $s-4; - padding-top: $s-2; } } - &:hover { - .detach-btn, - .select-btn { - background-color: transparent; - svg { - stroke: var(--input-foreground-color-active); - } + &:focus, + &:focus-within { + background-color: var(--input-background-color-focus); + border: $s-1 solid var(--input-border-color-focus); + &:hover { + background-color: var(--input-background-color-hover); + border: $s-1 solid var(--input-border-color-focus); } } + + &.editing { + background-color: var(--input-background-color-active); + &:hover { + border: $s-1 solid var(--input-border-color-active); + } + } +} + +.gradient-name-wrapper { + border-radius: 0 $br-8 $br-8 0; + .color-name { + @include flexRow; + border-radius: 0 $br-8 $br-8 0; + } +} + +.library-name-wrapper { + border-radius: $br-8; +} + +.opacity-element-wrapper { + @extend .input-element; + width: $s-60; + border-radius: 0 $br-8 $br-8 0; + .opacity-input { + padding: 0; + border-radius: 0 $br-8 $br-8 0; + min-width: $s-28; + } + .icon-text { + @include flexCenter; + height: $s-32; + margin-inline-end: $s-4; + margin-block-start: $s-2; + } } .remove-btn, diff --git a/frontend/src/app/main/ui/workspace/text_palette.scss b/frontend/src/app/main/ui/workspace/text_palette.scss index faee2df1b1..43bbe062ba 100644 --- a/frontend/src/app/main/ui/workspace/text_palette.scss +++ b/frontend/src/app/main/ui/workspace/text_palette.scss @@ -124,9 +124,6 @@ } } &.small-item { - .typography-name { - height: $s-12; - } .typography-data, .typography-font { display: none; diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.cljs b/frontend/src/app/main/ui/workspace/top_toolbar.cljs index 86f3484dff..2c20e9c255 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/top_toolbar.cljs @@ -108,7 +108,7 @@ (when-not ^boolean read-only? [:aside {:class (stl/css-case :main-toolbar true :not-rulers-present (not rulers?) - :hidden-toolbar hide-toolbar?)} + :main-toolbar-hidden hide-toolbar?)} [:ul {:class (stl/css :main-toolbar-options)} [:li [:button diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.scss b/frontend/src/app/main/ui/workspace/top_toolbar.scss index 993953c898..aec2d2f6be 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.scss +++ b/frontend/src/app/main/ui/workspace/top_toolbar.scss @@ -9,7 +9,6 @@ .main-toolbar { cursor: initial; position: absolute; - top: $s-28; left: calc(50% - $s-160); display: flex; align-items: center; @@ -24,6 +23,20 @@ top 0.3s, height 0.3s, opacity 0.3s; + &:not(.main-toolbar-hidden) { + top: $s-28; + } +} + +.main-toolbar-hidden { + top: $s-24; + height: $s-16; + z-index: $z-index-1; + border-radius: 0 0 $s-8 $s-8; + border-block-start: 0; + .main-toolbar-options { + opacity: $op-0; + } } .main-toolbar-options { @@ -76,16 +89,6 @@ } } -.main-toolbar.hidden-toolbar { - top: $s-20; - height: $s-16; - z-index: $z-index-1; - border-radius: 0 0 $s-8 $s-8; - .main-toolbar-options { - opacity: $op-0; - } -} - .main-toolbar.not-rulers-present { top: $s-8; &.hidden-toolbar { diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 316cf6d0b5..ab0b107775 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -166,16 +166,16 @@ :y -11 :width (max 0 (- text-width text-pos-x)) :height 20 - :class "workspace-frame-label" + :class (stl/css :workspace-frame-label-wrapper) :style {:fill color} - :visibility (if show-artboard-names? "visible" "hidden") - :on-pointer-down on-pointer-down - :on-double-click on-double-click - :on-context-menu on-context-menu - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave} + :visibility (if show-artboard-names? "visible" "hidden")} [:div {:class (stl/css :workspace-frame-label) - :style {:color color}} + :style {:color color} + :on-pointer-down on-pointer-down + :on-double-click on-double-click + :on-context-menu on-context-menu + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave} (if show-id? (dm/str (dm/str (:id frame)) " - " (:name frame)) (:name frame))]]]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.scss b/frontend/src/app/main/ui/workspace/viewport/widgets.scss index 326057ce17..19d5e467f7 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.scss +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.scss @@ -63,10 +63,16 @@ } } +.workspace-frame-label-wrapper { + pointer-events: none; +} + .workspace-frame-label { font-size: $fs-12; color: black; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; + max-width: fit-content; + pointer-events: all; } diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index f43df1cf24..2130ff4147 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -347,7 +347,12 @@ (->> (upload-media-files context file-id name (:href image-data)) (rx/catch #(do (.error js/console "Error uploading media: " name) (rx/of node))) - (rx/map #(vector (:id image-data) %))))) + (rx/map (fn [data] + (let [data + (cond-> data + (some? (:keep-aspect-ratio image-data)) + (assoc :keep-aspect-ratio (:keep-aspect-ratio image-data)))] + [(:id image-data) data])))))) (rx/reduce (fn [acc [id data]] (assoc acc id data)) {}) (rx/map (fn [images] @@ -360,7 +365,8 @@ (assoc-in [:attrs :penpot:media-width] (:width media)) (assoc-in [:attrs :penpot:media-height] (:height media)) (assoc-in [:attrs :penpot:media-mtype] (:mtype media)) - + (cond-> (some? (:keep-aspect-ratio media)) + (assoc-in [:attrs :penpot:media-keep-aspect-ratio] (:keep-aspect-ratio media))) (assoc-in [:attrs :penpot:fill-color] (:fill image-fill)) (assoc-in [:attrs :penpot:fill-color-ref-file] (:fill-color-ref-file image-fill)) (assoc-in [:attrs :penpot:fill-color-ref-id] (:fill-color-ref-id image-fill)) diff --git a/frontend/src/app/worker/import/parser.cljs b/frontend/src/app/worker/import/parser.cljs index 90ba834afa..017a87710a 100644 --- a/frontend/src/app/worker/import/parser.cljs +++ b/frontend/src/app/worker/import/parser.cljs @@ -195,15 +195,22 @@ (d/deep-mapm (fn [pair] (->> pair (mapv convert))))))) -(def search-data-node? #{:rect :image :path :circle}) +(def search-data-node? #{:rect :path :circle}) (defn get-svg-data [type node] - (let [node-attrs (add-attrs {} (:attrs node))] (cond (search-data-node? type) - (let [data-tags #{:ellipse :rect :path :text :foreignObject :image}] + (let [data-tags #{:ellipse :rect :path :text :foreignObject}] + (->> node + (node-seq) + (filter #(contains? data-tags (:tag %))) + (map #(:attrs %)) + (reduce add-attrs node-attrs))) + + (= type :image) + (let [data-tags #{:rect :image}] (->> node (node-seq) (filter #(contains? data-tags (:tag %))) @@ -523,7 +530,8 @@ (let [metadata {:id (get-meta node :media-id) :width (get-meta node :media-width) :height (get-meta node :media-height) - :mtype (get-meta node :media-mtype)}] + :mtype (get-meta node :media-mtype) + :keep-aspect-ratio (get-meta node :media-keep-aspect-ratio str->bool)}] (cond-> props (= type :image) (assoc :metadata metadata) @@ -881,7 +889,8 @@ (let [id (get-in fill-node [:attrs :penpot:fill-image-id]) image-node (->> node (node-seq) (find-node-by-id id))] {:id id - :href (get-in image-node [:attrs :href])}))) + :href (get-in image-node [:attrs :href]) + :keep-aspect-ratio (not= (get-in image-node [:attrs :preserveAspectRatio]) "none")}))) (filterv #(some? (:id %)))))) (defn has-fill-images? diff --git a/frontend/translations/en.po b/frontend/translations/en.po index f265931802..be4499c877 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5074,6 +5074,9 @@ msgstr "Gradient" msgid "media.choose-image" msgstr "Choose image" +msgid "media.keep-aspect-ratio" +msgstr "Keep aspect ratio" + msgid "workspace.options.guides.title" msgstr "Guides" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 4fe0c535ea..c121150b3d 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -5158,6 +5158,9 @@ msgstr "Gradiente" msgid "media.choose-image" msgstr "Elegir imagen" +msgid "media.keep-aspect-ratio" +msgstr "Mantener la proporción" + msgid "workspace.options.guides.title" msgstr "Guías"