diff --git a/.gitignore b/.gitignore index 9481cf6b87..1c277c16b3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ opencode.json !AGENTS.md !CODE_OF_CONDUCT.md !SECURITY.md +!HIGHLIGHTS.md /*.png /*.svg /*.sql diff --git a/HIGHLIGHTS.md b/HIGHLIGHTS.md new file mode 100644 index 0000000000..e66b4b4497 --- /dev/null +++ b/HIGHLIGHTS.md @@ -0,0 +1,26 @@ +# HIGHLIGHTS + +## 2.17.0 + +- Background blur is here +- WebGL rendering gets stronger +- MCP connection status and more +- Design tokens: more visible, more user-friendly + + +## 2.16.0 + +- Design tokens in the design panel +- Major community contributions +- WebGL rendering (beta) + + +## 2.15.0 + +- AI connected to real design context +- Multi-directional workflow +- Your stack, your model, your decision + + + + diff --git a/docs/img/teams/team-selector-projects.webp b/docs/img/teams/team-selector-projects.webp index e1696680ec..c7ba762604 100644 Binary files a/docs/img/teams/team-selector-projects.webp and b/docs/img/teams/team-selector-projects.webp differ diff --git a/docs/img/teams/team-selector.webp b/docs/img/teams/team-selector.webp index c29357d264..e7af5e1523 100644 Binary files a/docs/img/teams/team-selector.webp and b/docs/img/teams/team-selector.webp differ diff --git a/docs/user-guide/account-teams/projects-files.njk b/docs/user-guide/account-teams/projects-files.njk index 9345cd4f45..02f1afd38c 100644 --- a/docs/user-guide/account-teams/projects-files.njk +++ b/docs/user-guide/account-teams/projects-files.njk @@ -10,7 +10,7 @@ desc: Learn how to organize your work in Penpot. Create, manage and organize pro

Projects

Projects are containers that help you organize and group related design files together. Think of them as folders in a file system. You can create as many projects as you need to organize your work by client, product, feature, or any other structure that fits your workflow.

-

If you're working with others, projects should be created inside a team so that team members can collaborate on the files within them. Projects created in your personal space ("Your Penpot") remain private to you.

+

If you're working with others, projects should be created inside a team so that team members can collaborate on the files within them. Projects created in your personal space ("Personal Projects") remain private to you.

Projects view in dashboard
diff --git a/docs/user-guide/account-teams/teams.njk b/docs/user-guide/account-teams/teams.njk index 67fbc78187..550cf81390 100644 --- a/docs/user-guide/account-teams/teams.njk +++ b/docs/user-guide/account-teams/teams.njk @@ -16,7 +16,7 @@ member is allowed to do depends on their permissions.

Select team

At the top left of the dashboard you can find the team selector.

-

"Your Penpot" is the name of your personal space at Penpot. It is like any other team but in which no members can be invited so that you will always have your own private dashboard. Create or join other teams to collaborate with other Penpot users.

+

"Personal Projects" is the name of your personal space at Penpot. It is like any other team but in which no members can be invited so that you will always have your own private dashboard. Create or join other teams to collaborate with other Penpot users.

Teams selector

Create teams

diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index 4ba8d7fd49..35922e0af1 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -569,7 +569,7 @@ test.describe("Tokens - creation", () => { }); await selectDropdown.click(); - const fontOption = tokensUpdateCreateModal.getByText("ABeeZee"); + const fontOption = tokensUpdateCreateModal.getByRole('img', { name: 'ABeeZee' }) await expect(fontOption).toBeVisible(); await fontOption.click(); @@ -583,7 +583,7 @@ test.describe("Tokens - creation", () => { name: "Search font", }); await searchField.fill("alme"); - const fontOption2 = tokensUpdateCreateModal.getByText("Almendra Display"); + const fontOption2 = tokensUpdateCreateModal.getByRole('img', {name: "Almendra Display"}); await expect(fontOption2).toBeVisible(); await fontOption2.click(); @@ -1521,7 +1521,7 @@ test.describe("Tokens - creation", () => { }); await selectDropdown.click(); - const fontOption = tokensUpdateCreateModal.getByText("ABeeZee"); + const fontOption = tokensUpdateCreateModal.getByRole("img", {name: "ABeeZee"}); await expect(fontOption).toBeVisible(); await fontOption.click(); @@ -1536,7 +1536,7 @@ test.describe("Tokens - creation", () => { name: "Search font", }); await searchField.fill("alme"); - const fontOption2 = tokensUpdateCreateModal.getByText("Almendra Display"); + const fontOption2 = tokensUpdateCreateModal.getByRole("img", {name: "Almendra Display"}); await expect(fontOption2).toBeVisible(); await fontOption2.click(); await expect( diff --git a/frontend/resources/styles/common/base.scss b/frontend/resources/styles/common/base.scss index 595ddb0dac..588d484826 100644 --- a/frontend/resources/styles/common/base.scss +++ b/frontend/resources/styles/common/base.scss @@ -41,6 +41,11 @@ body { scrollbar-width: thin; } +::selection { + background: var(--color-accent-background-select); + color: var(--color-static-white); +} + img { height: auto; width: 100%; diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index 81008c3fff..2addcc0429 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -415,6 +415,7 @@ is-own? (= profile-id (:owner-id organization))] (or (= perm "any") is-own?))) all-organizations) team (first (filter #(= (:id %) team-id) teams)) + current-organization (:organization team) on-confirm (fn [organization-id] (st/emit! (add-team-to-organization {:team-id team-id :organization-id organization-id}))) @@ -422,11 +423,11 @@ (fn [organizations-allowed] (let [has-filtered? (< (count organizations) (count all-organizations)) extra-props (when has-filtered? - {:info-message-key "dashboard.select-organization-modal.permission-info"})] + {:info-message-key "dashboard.select-organization-modal.permission-info-add"})] (modal/show :select-organization-modal (merge {:organizations organizations :organizations-allowed organizations-allowed - :current-organization-id (dm/get-in team [:organization :id]) + :current-organization current-organization :on-confirm on-confirm :team-id team-id :title-key "dashboard.select-organization-modal.title" @@ -509,11 +510,12 @@ :title (tr "dashboard.change-organization-modal.title")}) (modal/show :select-organization-modal (merge {:organizations selectable-organizations - :organizations-allowed organizations-allowed - :current-organization-id current-organization-id + :organizations-allowed organizations-allowed + :current-organization source-organization :on-confirm on-confirm :team-id team-id :title-key "dashboard.change-organization-modal.title" + :description-key "dashboard.change-organization-modal.description" :choose-key "dashboard.change-organization-modal.choose" :placeholder-key "dashboard.change-organization-modal.select" :accept-key "dashboard.change-organization-modal.accept" diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index af62c11962..d9afb0cc62 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1699,6 +1699,7 @@ (dm/export dwgu/set-hover-guide) ;; Zoom +(dm/export dwz/center-on-shape) (dm/export dwz/reset-zoom) (dm/export dwz/zoom-to-selected-shape) (dm/export dwz/start-zooming) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 526c31236b..1908884677 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -175,7 +175,12 @@ (rx/mapcat (fn [_] (rx/from (fonts/ensure-loaded! font-id font-variant-id)))) (rx/take-until (text-work-stopper stream)) - (rx/ignore) + (rx/mapcat (fn [_] + (st/emit! (dwsh/update-shapes + ids + #(dissoc % :position-data) + {:save-undo? false})) + (rx/empty))) (wrf/with-pending :font ids)))) ;; -- Content helpers @@ -857,16 +862,16 @@ ::resize-text-debounce-event))))) (rx/empty)))))) -(defn save-font +(defn save-default-font [data] - (ptk/reify ::save-font + (ptk/reify ::save-default-font ptk/UpdateEvent (update [_ state] - (let [multiple? (->> data vals (d/seek #(= % :multiple)))] + (let [multiple? (->> data vals (d/seek #(= % :multiple))) + font (dissoc data :typography-ref-id :typography-ref-file)] (cond-> state (not multiple?) - (assoc-in [:workspace-global :default-font] - (dissoc data :typography-ref-id :typography-ref-file))))))) + (update :workspace-global assoc :default-font font)))))) (defn apply-text-modifier [shape text-modifier] diff --git a/frontend/src/app/main/data/workspace/zoom.cljs b/frontend/src/app/main/data/workspace/zoom.cljs index 8345951f82..bf53b9462f 100644 --- a/frontend/src/app/main/data/workspace/zoom.cljs +++ b/frontend/src/app/main/data/workspace/zoom.cljs @@ -135,6 +135,32 @@ (effect [_ state _] (dwvw/maybe-sync-workspace-local-viewport! state)))) +(defn center-on-shape + "Pan the viewport to center on the shape with the given id without changing zoom." + [id] + (ptk/reify ::center-on-shape + ptk/UpdateEvent + (update [_ state] + (if (dwvw/render-context-lost? state) + state + (let [page-id (:current-page-id state) + objects (dsh/lookup-page-objects state page-id) + shape (get objects id) + srect (:selrect shape)] + (if (nil? srect) + state + (update state :workspace-local + (fn [{:keys [vbox] :as local}] + (let [cx (+ (:x srect) (/ (:width srect) 2)) + cy (+ (:y srect) (/ (:height srect) 2)) + new-x (- cx (/ (:width vbox) 2)) + new-y (- cy (/ (:height vbox) 2))] + (update local :vbox assoc :x new-x :y new-y)))))))) + + ptk/EffectEvent + (effect [_ state _] + (dwvw/maybe-sync-workspace-local-viewport! state)))) + (def zoom-to-selected-shape (ptk/reify ::zoom-to-selected-shape ptk/UpdateEvent diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index 3a8edf5134..4af7f46ccc 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -221,7 +221,7 @@ (when (zero? (:refs new-state)) (dom/remove! node)))) -(defn- add-font-css! +(defn- add-font-css "Creates a style element and attaches it to the dom." [id css] (let [node (dom/create-element "style")] @@ -284,7 +284,7 @@ (->> (request-gfont-css url) (rx/map process-gfont-css) (rx/tap #(on-loaded id)) - (rx/subs! (partial add-font-css! id) + (rx/subs! (partial add-font-css id) #(when (fn? on-failed) (on-failed %)))) nil))) @@ -324,7 +324,7 @@ (when (globals/browser?) (log/dbg :hint "load-font" :font-id id :backend "custom") (let [css (generate-custom-font-css font)] - (add-font-css! id css) + (add-font-css id css) (when (fn? on-loaded) (on-loaded))))) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 57844a6d9b..7a07e4b2b9 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -371,8 +371,8 @@ [:span {:class (stl/css :my-teams-icon)} [:> raw-svg* {:id penpot-logo-icon-subtle}]] [:span {:class (stl/css :team-text) - :title (tr "dashboard.my-teams")} - (tr "dashboard.my-teams")] + :title (tr "dashboard.other-teams")} + (tr "dashboard.other-teams")] (when (= default-team-id (:default-team-id organization)) tick-icon)] (when (seq organizations) @@ -438,7 +438,7 @@ (when-not (contains? cf/flags :admin-console) [:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon]) - [:span {:class (stl/css :team-text)} (if (contains? cf/flags :admin-console) (tr "dashboard.my-files") (tr "dashboard.your-penpot"))] + [:span {:class (stl/css :team-text)} (if (contains? cf/flags :admin-console) (tr "dashboard.personal-projects") (tr "dashboard.your-penpot"))] (when (= default-team-id (:id team)) tick-icon)] @@ -835,7 +835,7 @@ [:span {:class (stl/css :my-teams-icon-xxxl)} [:> raw-svg* {:id penpot-logo-icon-subtle}]] [:span {:class (stl/css :team-text)} - (tr "dashboard.my-teams")]] + (tr "dashboard.other-teams")]] [:* [:> organization-avatar* {:organization current-organization :size "xxxl"}] [:span {:class (stl/css :team-text)} @@ -972,7 +972,7 @@ :team-name-no-logo nitrate?)} (when-not nitrate? [:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon]) - [:span {:class (stl/css :team-text)} (if nitrate? (tr "dashboard.my-files") (tr "dashboard.default-team-name"))]] + [:span {:class (stl/css :team-text)} (if nitrate? (tr "dashboard.personal-projects") (tr "dashboard.default-team-name"))]] (and (contains? cf/flags :subscriptions) (not is-default?) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 25c718f0c6..d7167ff779 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -915,34 +915,57 @@ (mf/defc select-organization-modal {::mf/register modal/components ::mf/register-as :select-organization-modal} - [{:keys [organizations organizations-allowed current-organization-id on-confirm title-key text-key choose-key placeholder-key accept-key cancel-key info-message-key team-id]}] - (let [valid-organizations (mf/with-memo [organizations] - (remove #(= (:id %) current-organization-id) organizations)) - options (mf/with-memo [valid-organizations organizations-allowed] - (mapv (fn [organization] - (let [organization-id (:id organization) - ;; organizations-allowed is a map of organization-id and a boolean indicating if it is allowed - enabled? (or (nil? organizations-allowed) - (true? (get organizations-allowed organization-id)))] - (cond-> {:id (str organization-id) - :label (:name organization) - :disabled (not enabled?) - :dimmed (not enabled?) - :avatar {:render-fn render-organization-combobox-avatar* - :organization organization - :size "xl"}} - (not enabled?) - (assoc :title (tr "dashboard.team-organization.disabled-organization-tooltip"))))) - valid-organizations)) + [{:keys [organizations organizations-allowed current-organization on-confirm title-key text-key choose-key placeholder-key accept-key cancel-key info-message-key description-key team-id]}] + (let [current-organization-id (:id current-organization) + has-current-org? (some? current-organization) + valid-organizations (mf/with-memo [organizations current-organization-id] + (remove #(= (:id %) current-organization-id) organizations)) + all-organizations (mf/with-memo [organizations current-organization] + (cond-> organizations + (and has-current-org? + (not (some #(= (:id %) current-organization-id) organizations))) + (conj current-organization))) + options (mf/with-memo [valid-organizations organizations-allowed current-organization] + (let [other-options + (mapv (fn [organization] + (let [organization-id (:id organization) + enabled? (or (nil? organizations-allowed) + (true? (get organizations-allowed organization-id)))] + (cond-> {:id (str organization-id) + :label (:name organization) + :disabled (not enabled?) + :dimmed (not enabled?) + :avatar {:render-fn render-organization-combobox-avatar* + :organization organization + :size "xl"}} + (not enabled?) + (assoc :title (tr "dashboard.team-organization.disabled-organization-tooltip"))))) + valid-organizations)] + (if has-current-org? + (into [{:id (str current-organization-id) + :label (:name current-organization) + :avatar {:render-fn render-organization-combobox-avatar* + :organization current-organization + :size "xl"}}] + other-options) + other-options))) - form (fm/use-form :schema schema:organization-form :initial {}) + initial-form (mf/with-memo [has-current-org? current-organization-id] + (if has-current-org? + {:selected-id (str current-organization-id)} + {})) + form (fm/use-form :schema schema:organization-form :initial initial-form) - warning-info* (mf/use-state nil) - warning-info (deref warning-info*) - selected-organization (mf/with-memo [warning-info valid-organizations] - (when warning-info - (d/seek #(= (:id %) (:organization-id warning-info)) valid-organizations))) + warning-info* (mf/use-state nil) + warning-info (deref warning-info*) + selected-organization (mf/with-memo [warning-info all-organizations] + (when warning-info + (d/seek #(= (:id %) (:organization-id warning-info)) all-organizations))) + selected-id (dm/get-in @form [:data :selected-id]) + disabled? (or (not (:valid @form)) + (and has-current-org? + (= (str selected-id) (str current-organization-id)))) on-change (mf/use-fn (mf/deps form team-id) @@ -978,18 +1001,25 @@ [:div {:class (stl/css :modal-content :modal-select-organization-text)} (tr text-key)]) [:div {:class (stl/css :modal-select-organization-body)} - (when info-message-key + (when (or description-key info-message-key) [:div {:class (stl/css :modal-select-organization-info)} - (tr info-message-key)]) + (when description-key + [:div + (tr description-key)]) + (when info-message-key + [:div + (tr info-message-key)])]) [:div {:class (stl/css :modal-select-organization-content)} (tr choose-key)] [:> combobox* {:id "selected-id" :class (stl/css :team-member) :options options :select-only true - :default-selected (or (some-> (get-in @form [:data :selected-id]) str) "") :placeholder (tr placeholder-key) - :on-change on-change}] + :on-change on-change + :default-selected (if has-current-org? + (str current-organization-id) + "")}] ;; Warning for external invitations (when (and warning-info @@ -1017,7 +1047,7 @@ {:class (stl/css :accept-btn) :variant "primary" :type "button" - :disabled (not (:valid @form)) + :disabled disabled? :on-click on-confirm'} (tr accept-key)]]]]])) diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss index a5fa5b4822..2a8b754496 100644 --- a/frontend/src/app/main/ui/dashboard/team.scss +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -963,7 +963,11 @@ .modal-select-organization-info { @include t.use-typography("body-medium"); + display: flex; + flex-direction: column; + gap: var(--sp-m); color: var(--color-foreground-secondary); + margin-block-end: var(--sp-xxl); } .modal-select-organization-title { diff --git a/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss b/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss index e50809f761..10de19e494 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss +++ b/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss @@ -97,6 +97,7 @@ &::selection { background: var(--color-accent-background-select); + color: var(--color-static-white); } &::placeholder { diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 9105d954b6..c626b36a29 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -8,7 +8,6 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] - [app.config :as cf] [app.main.data.common :as dcm] [app.main.data.helpers :as dsh] [app.main.data.persistence :as dps] @@ -229,12 +228,8 @@ (st/emit! (dps/initialize-persistence) (dpl/update-plugins-permissions-peek))) - ;; FLAG :font-preview — prefetch the preview sprite markup on workspace mount - ;; (kept in memory, not the DOM) so the typography selector renders previews on - ;; open with no network wait. Remove the flag check to drop the feature. (mf/with-effect [] - (when (contains? cf/flags :font-preview) - (fonts/prefetch-preview-sprite!))) + (fonts/prefetch-preview-sprite!)) ;; Setting the layout preset by its name (mf/with-effect [layout-name] @@ -302,4 +297,3 @@ (when (uuid? file-id) [:> workspace* props]))) - diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index d9bbcb1fad..770a1ebec7 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -30,18 +30,24 @@ ;; --- Text Editor Rendering -(mf/defc block-component* - [{:keys [block-props] :as props}] - (let [data (.-data ^js block-props) - style (sts/generate-paragraph-styles (.-shape ^js block-props) data) - dir (:text-direction data "auto")] +(mf/defc block-component + {::mf/wrap-props false} + [props] + (let [bprops (obj/get props "blockProps") + data (obj/get bprops "data") + style (sts/generate-paragraph-styles (obj/get bprops "shape") + (obj/get bprops "data")) + dir (:text-direction data "auto")] + [:div {:style style :dir dir} [:> draft/EditorBlock props]])) -(mf/defc selection-component* - [{:keys [children]}] - [:span {:style {:background "#ccc" :display "inline-block"}} children]) +(mf/defc selection-component + {::mf/wrap-props false} + [props] + (let [children (obj/get props "children")] + [:span {:style {:background "#ccc" :display "inline-block"}} children])) (defn- render-block [block shape] @@ -49,7 +55,7 @@ (case type "unstyled" #js {:editable true - :component block-component* + :component block-component :props #js {:data (ted/get-editor-block-data block) :shape shape}} nil))) @@ -63,7 +69,7 @@ (sts/generate-text-styles shape data {:show-text? false}))) (def default-decorator - (ted/create-decorator "PENPOT_SELECTION" selection-component*)) + (ted/create-decorator "PENPOT_SELECTION" selection-component)) (def empty-editor-state (ted/create-editor-state nil default-decorator)) @@ -89,11 +95,12 @@ "bottom" "flex-end" nil)) -(mf/defc text-shape-edit-html* +(mf/defc text-shape-edit-html {::mf/wrap [mf/memo] + ::mf/wrap-props false ::mf/forward-ref true} - [{:keys [shape]} _] - (let [{:keys [id content]} shape + [props _] + (let [{:keys [id content] :as shape} (obj/get props "shape") state-map (mf/deref refs/workspace-editor-state) state (get state-map id empty-editor-state) @@ -268,7 +275,8 @@ (-> (gpt/subtract pt box) (gpt/multiply zoom))))) -(mf/defc text-editor-svg* +(mf/defc text-editor-svg + {::mf/wrap-props false} [{:keys [shape modifiers]}] (let [shape-id (dm/get-prop shape :id) modifiers (dm/get-in modifiers [shape-id :modifiers]) @@ -341,6 +349,6 @@ [:foreignObject {:x x :y y :width width :height height} [:div {:style style} - [:> text-shape-edit-html* + [:& text-shape-edit-html {:shape shape :key (dm/str shape-id)}]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 9d6e52ded2..babc144cc9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -438,7 +438,7 @@ navigate-next (mf/use-fn - (mf/deps text-match-count) + (mf/deps text-match-ids text-match-count) (fn [_] (when (pos? text-match-count) (let [ids (mf/ref-val text-match-ids-ref) @@ -447,11 +447,11 @@ (mf/set-ref-val! match-idx-ref next-idx) (swap! state* assoc :current-match-idx next-idx) (st/emit! (dw/select-shape id) - dw/zoom-to-selected-shape))))) + (dw/center-on-shape id)))))) navigate-prev (mf/use-fn - (mf/deps text-match-count) + (mf/deps text-match-ids text-match-count) (fn [_] (when (pos? text-match-count) (let [ids (mf/ref-val text-match-ids-ref) @@ -460,7 +460,7 @@ (mf/set-ref-val! match-idx-ref prev-idx) (swap! state* assoc :current-match-idx prev-idx) (st/emit! (dw/select-shape id) - dw/zoom-to-selected-shape))))) + (dw/center-on-shape id)))))) handle-replace (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 71ed30673a..159d082478 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -412,7 +412,7 @@ (select-keys txt/text-node-attrs))] (when (features/active-feature? @st/state "text-editor-wasm/v1") (st/emit! (dwt-v3/v3-update-text-editor-styles (first ids) attrs))) - (st/emit! (dwt/save-font updated-attrs) + (st/emit! (dwt/save-default-font updated-attrs) (dwt/update-all-attrs ids attrs))))) on-change diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index fd34032175..8d20d7ab7b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -12,7 +12,6 @@ [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.types.text :as txt] - [app.config :as cf] [app.main.constants :refer [max-input-length]] [app.main.data.common :as dcm] [app.main.data.fonts :as fts] @@ -91,19 +90,13 @@ (constantly nil))))) @loaded?)) -;; --- FEATURE: font preview (flag :font-preview) ------------------------------ -;; font-item-preview* and use-font-lazy-load are the whole feature. They are only -;; rendered/called behind the `:font-preview` flag check in font-item* below, so -;; their hooks never run when the flag is off. To remove the flag, inline -;; font-item-preview* into font-item* and drop the plain-name branch. - (mf/defc font-item-preview* "Row content with previews: a vector preview from the shared sprite for catalog fonts, or the font's own name lazily loaded for custom fonts the sprite doesn't cover." {::mf/wrap [mf/memo]} [{:keys [font]}] - (let [font-id (:id font) + (let [font-id (get font :id) sprite (mf/deref fonts/preview-sprite) ;; The sprite is only referenceable once it's been attached to the DOM, @@ -133,29 +126,20 @@ {::mf/wrap [mf/memo]} [{:keys [font is-current on-click style]}] (let [item-ref (mf/use-ref) - on-click (mf/use-fn (mf/deps font) #(on-click font)) - ;; FLAG :font-preview — gates the feature markup AND its row styling - ;; (.font-item-preview-on in the scss). Remove this and its two uses below. - preview? (contains? cf/flags :font-preview)] + on-click (mf/use-fn (mf/deps font) #(on-click font))] - (mf/use-effect - (mf/deps is-current) - (fn [] - (when is-current - (let [element (mf/ref-val item-ref)] - (when-not (dom/is-in-viewport? element) - (dom/scroll-into-view! element)))))) + (mf/with-effect [is-current] + (when is-current + (let [element (mf/ref-val item-ref)] + (when-not (dom/is-in-viewport? element) + (dom/scroll-into-view! element))))) [:div {:class (stl/css :font-wrapper) :style style :ref item-ref :on-click on-click} - [:div {:class (stl/css-case :font-item true - :font-item-preview-on preview? - :selected is-current)} - (if preview? - [:> font-item-preview* {:font font}] - [:span {:class (stl/css :font-item-label)} (:name font)]) + [:div {:class (stl/css-case :font-item true :selected is-current)} + [:> font-item-preview* {:font font}] (when is-current [:> icon* {:icon-id i/tick :size "s"}])]])) @@ -260,14 +244,13 @@ (let [key (events/listen js/document "keydown" on-key-down)] #(events/unlistenByKey key))) - ;; FLAG :font-preview — materialize the preview sprite into the DOM only while - ;; the picker is open (markup is prefetched on workspace load), removing it on - ;; close so its ~2000 nodes aren't kept around idle. The attachment is deferred - ;; so the dropdown can paint first with plain names, then the sprite swaps in - ;; on the next tick. Remove the flag clause to drop the feature. + ;; Materialize the preview sprite into the DOM only while the picker is open + ;; (markup is prefetched on workspace load), removing it on close so its + ;; ~2000 nodes aren't kept around idle. The attachment is deferred so the + ;; dropdown can paint first with plain names, then the sprite swaps in on the + ;; next tick. (mf/with-effect [sprite-status] - (when (and (contains? cf/flags :font-preview) - (= :ready sprite-status)) + (when (= :ready sprite-status) (let [node* (volatile! nil) task (tm/schedule (fn [] @@ -278,9 +261,11 @@ (fonts/detach-preview-sprite! n)))))) (mf/with-effect [@selected] - (when-let [inst (mf/ref-val flist)] - (when-let [index (:index @selected)] - (.scrollToRow ^js inst index)))) + (let [node (mf/ref-val flist) + index (:index @selected)] + ;; This is nil safe operation, do nothing if node or index are + ;; invalid. + (dom/scroll-to-row node index))) (mf/with-effect [@selected] (on-select @selected)) @@ -291,11 +276,12 @@ (st/emit! (dsc/pop-shortcuts :typography)))) (mf/with-effect [] - (let [index (d/index-of-pred fonts #(= (:id %) (:id current-font))) - inst (mf/ref-val flist)] + (let [index (d/index-of-pred fonts #(= (:id %) (:id current-font))) + node (mf/ref-val flist)] (tm/schedule - #(let [offset (.getOffsetForRow ^js inst #js {:alignment "center" :index index})] - (.scrollToPosition ^js inst offset))))) + #(let [offset (.getOffsetForRow ^js node #js {:alignment "center" :index index})] + ;; Safe operaton, do nothing if node or offset has invalid values + (dom/scroll-to-position node offset))))) [:div {:class [(stl/css-case :font-selector true :fonts-on-modal (not full-size?))]} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss index 379c27718a..6fbb3e4d1b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss @@ -388,20 +388,7 @@ $font-preview-box-height: 28px; &.selected { color: var(--color-foreground-primary); } -} -.font-item-label { - @include t.use-typography("body-small"); - @include text-ellipsis; - - flex-grow: 1; - min-inline-size: 0; -} - -// --- FLAG :font-preview row styling. Only applied when font-item* adds the -// .font-item-preview-on modifier; remove this whole block with the flag so rows -// render exactly as before. -.font-item-preview-on { // Center & clip so a previewed font's own metrics never grow/overflow the row. align-items: center; overflow: hidden; @@ -414,6 +401,14 @@ $font-preview-box-height: 28px; } } +.font-item-label { + @include t.use-typography("body-small"); + @include text-ellipsis; + + flex-grow: 1; + min-inline-size: 0; +} + // `currentColor` makes the glyph fill follow the row text color (theme + selected). .font-item-preview { flex-grow: 1; diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 4771f201d3..1e2070c965 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -460,8 +460,8 @@ [:& editor-v2/text-editor {:shape editing-shape :canvas-ref canvas-ref :modifiers modifiers}] - [:> editor-v1/text-editor-svg* {:shape editing-shape - :modifiers modifiers}])) + [:& editor-v1/text-editor-svg {:shape editing-shape + :modifiers modifiers}])) (when show-frame-outline? (let [outlined-frame-id diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 70138b24ea..6badb43703 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -759,8 +759,8 @@ :canvas-ref canvas-ref :ref text-editor-ref}] - :else [:> editor-v1/text-editor-svg* {:shape editing-shape - :ref text-editor-ref}])) + :else [:& editor-v1/text-editor-svg {:shape editing-shape + :ref text-editor-ref}])) (when show-frame-outline? (let [outlined-frame-id (->> @hover-ids diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 27c9e7915b..779265b93d 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -244,6 +244,16 @@ height (.-clientHeight scroll-node)] (/ distance height))) +(defn scroll-to-row + [node index] + (when (and (some? node) (number? index)) + (.scrollToRow ^js node index))) + +(defn scroll-to-position + [node offset] + (when (and (some? node) (number? offset)) + (.scrollToPosition ^js node offset))) + (def get-target-val (comp get-value get-target)) (def get-target-scroll (comp get-scroll-position get-target)) diff --git a/frontend/test/frontend_tests/data/workspace_texts_test.cljs b/frontend/test/frontend_tests/data/workspace_texts_test.cljs index fc8f08cb79..8e41b36de8 100644 --- a/frontend/test/frontend_tests/data/workspace_texts_test.cljs +++ b/frontend/test/frontend_tests/data/workspace_texts_test.cljs @@ -379,14 +379,14 @@ "float letter-spacing is normalised to 2-decimal string"))))))) ;; --------------------------------------------------------------------------- -;; Tests: save-font must not persist typography refs into the global default font +;; Tests: save-default-font must not persist typography refs into the global default font ;; ;; Root cause of #10925: typography assets are file-specific references, but -;; save-font used to write :typography-ref-id / :typography-ref-file into the +;; save-default-font used to write :typography-ref-id / :typography-ref-file into the ;; session-global [:workspace-global :default-font]. That state survives a file ;; switch, and v2-default-text-content bakes it into brand-new text shapes in ;; the other file, so they got a non-existent typography asset instead of the -;; default Penpot font. save-font now strips those two keys. +;; default Penpot font. save-default-font now strips those two keys. ;; --------------------------------------------------------------------------- (t/deftest save-font-strips-typography-refs-from-default-font @@ -405,7 +405,7 @@ :typography-ref-id (uuid/next) :typography-ref-file (:id file)}] (ths/run-store - store done [(dwt/save-font attrs)] + store done [(dwt/save-default-font attrs)] (fn [new-state] (let [default-font (get-in new-state [:workspace-global :default-font])] (t/is (some? default-font)) @@ -425,7 +425,7 @@ :letter-spacing "0" :typography-ref-id (uuid/next) :typography-ref-file (uuid/next)}] - (ths/run-store store done [(dwt/save-font attrs)] + (ths/run-store store done [(dwt/save-default-font attrs)] (fn [new-state] (let [default-font (get-in new-state [:workspace-global :default-font])] (t/is (= "Open Sans" (:font-family default-font))) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 701c91e5b7..a3eed55ea4 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -388,6 +388,9 @@ msgstr "" msgid "dashboard.change-organization-modal.title" msgstr "Change team's organization" +msgid "dashboard.change-organization-modal.description" +msgstr "Projects and files will remain available to team members. The team will get the configuration from the new organization." + #: src/app/main/ui/dashboard/deleted.cljs:316 msgid "dashboard.clear-trash-button" msgstr "Clear trash" @@ -901,8 +904,8 @@ msgid "dashboard.move-to-other-team" msgstr "Move to other team" #: src/app/main/ui/dashboard/sidebar.cljs:348, src/app/main/ui/dashboard/sidebar.cljs:349, src/app/main/ui/dashboard/sidebar.cljs:761 -msgid "dashboard.my-teams" -msgstr "My Teams" +msgid "dashboard.other-teams" +msgstr "Other teams" #: src/app/main/ui/dashboard/files.cljs:106, src/app/main/ui/dashboard/projects.cljs:253, src/app/main/ui/dashboard/projects.cljs:254 msgid "dashboard.new-file" @@ -1140,11 +1143,11 @@ msgstr "" msgid "dashboard.select-organization-modal.external-invitations-will-be-canceled" msgstr "Pending invitations to external users will be canceled." -#, unused msgid "dashboard.select-organization-modal.permission-info" -msgstr "" -"Here you find all your organizations where you are allowed to create or add " -"teams." +msgstr "Here you'll find the organizations you are part of where you are allowed to move the team." + +msgid "dashboard.select-organization-modal.permission-info-add" +msgstr "Here you'll find the organizations you are part of where you are allowed to create or add teams." #, unused msgid "dashboard.select-organization-modal.select" @@ -1466,8 +1469,8 @@ msgstr "Your name" msgid "dashboard.your-penpot" msgstr "Your Penpot" -msgid "dashboard.my-files" -msgstr "My Files" +msgid "dashboard.personal-projects" +msgstr "Personal Projects" #: src/app/main/ui/alert.cljs:36 msgid "ds.alert-ok" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 945d30a19e..6ba1d6a107 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -395,6 +395,9 @@ msgstr "" msgid "dashboard.change-organization-modal.title" msgstr "Cambiar el equipo de organización" +msgid "dashboard.change-organization-modal.description" +msgstr "Los proyectos y archivos permanecerán disponibles para los miembros del equipo. El equipo obtendrá la configuración de la nueva organización." + #: src/app/main/ui/dashboard/deleted.cljs:316 msgid "dashboard.clear-trash-button" msgstr "Vaciar papelera" @@ -905,8 +908,8 @@ msgid "dashboard.move-to-other-team" msgstr "Mover a otro equipo" #: src/app/main/ui/dashboard/sidebar.cljs:348, src/app/main/ui/dashboard/sidebar.cljs:349, src/app/main/ui/dashboard/sidebar.cljs:761 -msgid "dashboard.my-teams" -msgstr "Mis Equipos" +msgid "dashboard.other-teams" +msgstr "Otros equipos" #: src/app/main/ui/dashboard/files.cljs:106, src/app/main/ui/dashboard/projects.cljs:253, src/app/main/ui/dashboard/projects.cljs:254 msgid "dashboard.new-file" @@ -1147,11 +1150,11 @@ msgstr "" msgid "dashboard.select-organization-modal.external-invitations-will-be-canceled" msgstr "Las invitaciones pendientes a usuarios externos serán canceladas." -#, unused msgid "dashboard.select-organization-modal.permission-info" -msgstr "" -"Aquí encontrarás todas las organizaciones en las que tienes permiso para " -"crear o añadir equipos." +msgstr "Aquí encontrarás las organizaciones de las que eres parte donde tienes permiso para mover el equipo." + +msgid "dashboard.select-organization-modal.permission-info-add" +msgstr "Aquí encontrarás las organizaciones de las que eres parte donde tienes permiso para crear o añadir equipos." #, unused msgid "dashboard.select-organization-modal.select" @@ -1473,8 +1476,8 @@ msgstr "Tu nombre" msgid "dashboard.your-penpot" msgstr "Tu Penpot" -msgid "dashboard.my-files" -msgstr "Mis Archivos" +msgid "dashboard.personal-projects" +msgstr "Proyectos Personales" #: src/app/main/ui/alert.cljs:36 msgid "ds.alert-ok"