From 6bedd5ac76c469d5fe58c82a83ae5046f53a1560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Thu, 27 Aug 2026 12:41:32 +0200 Subject: [PATCH 1/7] :bug: Fix move organization modal (#11390) * :bug: Fix move organization modal * :paperclip: Code review --- frontend/src/app/main/data/nitrate.cljs | 10 ++- frontend/src/app/main/ui/dashboard/team.cljs | 90 +++++++++++++------- frontend/src/app/main/ui/dashboard/team.scss | 4 + frontend/translations/en.po | 11 ++- frontend/translations/es.po | 11 ++- 5 files changed, 84 insertions(+), 42 deletions(-) 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/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 80d010c75b..a39b5eb9be 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 3408a4697e..34f97d4850 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/translations/en.po b/frontend/translations/en.po index b85b0f4cc0..d4b5f28d2c 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" @@ -1111,11 +1114,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" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index acfeeb8fab..d9421628a4 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" @@ -1118,11 +1121,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" From 43bbf756b0a89b2ff76b7e15cb40134549fc1e91 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 27 Aug 2026 18:30:33 +0200 Subject: [PATCH 2/7] :bug: Fix font change regression and activate font preview by default (#11341) * :sparkles: Materialize font-preview flag to make it permanent * :bug: Fix font change regression when WebGL is disabled * :paperclip: Add fixes for e2e tests * :bug: Fix font change from legacy render --------- Co-authored-by: Luis de Dios --- .../playwright/ui/specs/tokens/crud.spec.js | 8 +-- .../src/app/main/data/workspace/texts.cljs | 17 +++-- frontend/src/app/main/fonts.cljs | 6 +- frontend/src/app/main/ui/workspace.cljs | 8 +-- .../main/ui/workspace/shapes/text/editor.cljs | 38 ++++++----- .../workspace/sidebar/options/menus/text.cljs | 2 +- .../sidebar/options/menus/typography.cljs | 64 ++++++++----------- .../sidebar/options/menus/typography.scss | 21 +++--- .../src/app/main/ui/workspace/viewport.cljs | 4 +- .../app/main/ui/workspace/viewport_wasm.cljs | 4 +- frontend/src/app/util/dom.cljs | 10 +++ .../data/workspace_texts_test.cljs | 10 +-- 12 files changed, 95 insertions(+), 97 deletions(-) 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/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 3db75ccb8e..dd8741218d 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -174,7 +174,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 @@ -850,16 +855,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/fonts.cljs b/frontend/src/app/main/fonts.cljs index 644169d3de..6db6fd3817 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -242,7 +242,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")] @@ -306,7 +306,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))) @@ -346,7 +346,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/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 278663bbcb..a5920570a6 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 536febb61b..6f949e5e72 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/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 60b0c15c02..2054cc763c 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 1a085ef1c4..53a5a26b01 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 c5b1a59d2f..967e9d7c0f 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 4814be5042..0f5fbfeeac 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -455,8 +455,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 5a9601502d..1967662aa1 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -747,8 +747,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 6a0f514018..5995a51264 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 5822b571cf..ebb315b3f6 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))) From 11224940ab6e54a00a8f063f28325efe7a27e160 Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Thu, 27 Aug 2026 18:32:22 +0200 Subject: [PATCH 3/7] :bug: Fix wrong selection color on inputs (#11400) --- frontend/resources/styles/common/base.scss | 5 +++++ .../src/app/main/ui/ds/controls/utilities/input_field.scss | 1 + 2 files changed, 6 insertions(+) diff --git a/frontend/resources/styles/common/base.scss b/frontend/resources/styles/common/base.scss index 41d79a0bf4..6407bb5969 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/ui/ds/controls/utilities/input_field.scss b/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss index 9aede7e748..df04187129 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 { From 1baa8bb29f6ffff54d283d11c44b97630ec40cb6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 27 Aug 2026 18:33:48 +0200 Subject: [PATCH 4/7] :bug: Center canvas and select layer when navigating layer search results (#10422) (#11391) When using previous/next buttons in the layer search panel, the canvas now centers on the matched layer and selects it in the layers panel. Previously only the internal search cursor was advanced without any visible canvas or selection update. Co-authored-by: Sumit Ridhal --- frontend/src/app/main/data/workspace.cljs | 1 + .../src/app/main/data/workspace/zoom.cljs | 26 +++++++++++++++++++ .../app/main/ui/workspace/sidebar/layers.cljs | 8 +++--- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 518729d806..da6a9bb076 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1698,6 +1698,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/zoom.cljs b/frontend/src/app/main/data/workspace/zoom.cljs index 1a5afc7d0b..4be06e5e28 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/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index e9db4a2343..844fbb4efd 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 From cdfdbf9adc419093d6be14c2f2abd9683199a7a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Fri, 28 Aug 2026 09:49:13 +0200 Subject: [PATCH 5/7] :bug: Fix labels for teams navigation and personal files section (#11398) --- frontend/src/app/main/ui/dashboard/sidebar.cljs | 10 +++++----- frontend/translations/en.po | 8 ++++---- frontend/translations/es.po | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index ed39edaaf1..3036dad42c 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/translations/en.po b/frontend/translations/en.po index d4b5f28d2c..50bf6b331e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -875,8 +875,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" @@ -1440,8 +1440,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 d9421628a4..4ff509285e 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -879,8 +879,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" @@ -1447,8 +1447,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" From 18ebf5a92d6eb2752b010b5e69861a82b9c3fad9 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Fri, 28 Aug 2026 10:40:25 +0200 Subject: [PATCH 6/7] :books: Add highlights file (#11406) --- .gitignore | 1 + HIGHLIGHTS.md | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 HIGHLIGHTS.md diff --git a/.gitignore b/.gitignore index 382b89c92f..ec067552ff 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 + + + + From eb0c51ed7440a8f55bb8531836db25070d242bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Fri, 28 Aug 2026 10:41:25 +0200 Subject: [PATCH 7/7] :books: Rename personal space label from Your Penpot to Personal Projects (#11399) --- docs/img/teams/team-selector-projects.webp | Bin 10182 -> 6850 bytes docs/img/teams/team-selector.webp | Bin 2484 -> 1256 bytes .../account-teams/projects-files.njk | 2 +- docs/user-guide/account-teams/teams.njk | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/img/teams/team-selector-projects.webp b/docs/img/teams/team-selector-projects.webp index e1696680ec94df5c4e26675fd21e40f2e50b1581..c7ba762604210316ba1bac82687a5a1f1206db3c 100644 GIT binary patch literal 6850 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 literal 10182 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 SbkqQuaS(t20000000016eRK5y literal 2484 zcmaKuc{tQ-8^<3}3`#=Ts%BVH^`7^=pX&JBbB99+{sc&a-C?|? zxh}-+5F@<)#;$*3H-G$&J`U1o`d$v&*=xskPRHxvXJr9BxgZw@fxsM~fzHnPp*du@ zJOI?b0>Ga1E9VMzuPO!r;SaxZ(o_KWq5-Jv{+0XH6Mq-H%a7M_KyjC+CjhJW0oZE? zfWR97xG(-#gTnu)4Gt}dLv`Vx;RP-O4*&-k;0N4*B1FFdC2$g)hQ2Voo)Z9nHIU7A zoW*trX#1*AFa?r%5)ydBh={#pAHVS+EOUh4JT7T*^z6}ZBqc5Nt6;{HHM=pjh`yK4 zoGG(e>&EhphYxLZH8f5328mUoR!O2c*Sc^DjO4G%Yfk-J3rityDJjU6tcfx9732)+ zQ=M4&(>1ZWB}xv*GqA;+t6-v;NQH3IF!t~hEc&);S(%x7uAkRXCS#gZ^WQ*G{9NSL zrrEHkr7%%=bxZ6(x+WU)bjvST82zf~+?J5=#1Iz1f_nZ`LFc|-hCqJzXY zSR(skc#T{d$;EEf2Qf|}; z9L$*~Xe8V5v#|egENQvui02Yd4eeHssv3Wo_qCgbx&$WosgG*tVMNhsU1u4KMj(IkUHl-+bI%pab`9sw#IXGF zdyZ#}@%LRnCX`a|s8}#0NgiCwrXA_podQITW(iqBwafhcnNs-P2D`B&rXb-4%thA! zs?uxhTfvVQ`m8&HW?G#@1NRTzaCf}tcWO_R%;DWY#VFyfcP%R5{Z#z+?DyIIHLdTY z<{INmH)c50N55H(3%`zvz!{f!WH1o>j>nuZv#FrG%nTEA`RMT>^)5TzcG%-C~|3x1kAXaR8yELBXB!y z#EL%7mEg<%I%2U{kfcn`%1)7=c5LoL|I5hQ@HdxO``m$nty`A)19n-T2OCtmM%ysN zZ;2Rgc>0k1C+Aw<@X@tKELKPxc^mN8u4SP1Wzxn#>o83E5XlgCghTz>(8KmG^e58k zccKllNHG25IsuF`Ab07p{?f3~b${y#4cbs}>~%ezI26yZR1Hd~rW{Mr;f0loEn6>R zA~Yf{I2YrC^$WT@81;UHMK8-W4xYJ54!b7zOb{^tXmqM|eD?kDZO!w2R-uz;lZc8P zCIKcnbA22^08X>JbM>JVO-x0bev#lfi#wH`5B8f4{g5aIjJUghjm`>?aVI=%e88t{i9VIP9d;^r<=Ift9V388xnSs#juN9P+!e{cj|;2d%i z)^H46FCn=JjGKAC>QS$E)x(v)-)~{ogv9V}R;=@hf2o!`EG@Duhmfn~3L%#X5o;cN z5sdd(x%Z4;M;F#;x*dFjA{u;`mv(H`gdDPE+-$>hZgj=a*%e0r(Q!fDzC(fL`6 zyR2QUDR)vl)?ra^Mp&;br*AqO6Tv;v{p?p6cSho7)k)MBgL6ft7kyL{2nXkfe0I6A zY|g%|TGWdh+309ex65XZ3C1Utlz(lodY=?7?4mAhFRAt-Lp2@^$8-0j20R@zcg8ec zl-?lDZHzaHAZGS46$D$zZN&AQChvVzB{h;MDn(+6O?Nm*ad+v0ov7XK6Gp!8#mM6B zOwTzX50gYBQx>;iE!!e;x@)#cg`fV=Oy=2!=zNk(*UW1ZmFGg-dEipkae`pm1dheqS>KXLoryUD|bEqEM_l(K-`;Ol0-&IE})6{_PsWg zdXsKL(WP78DXVB=1cippZgJ?&Ug5lbc}&4>mFCY44|D26Wb04?;1qG5B`ic`)o&Q* ZX_1c9D?t~SAlV$r<^tUg{sPr(DY^gv 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