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

This commit is contained in:
Andrey Antukh
2026-08-28 11:22:52 +02:00
29 changed files with 258 additions and 158 deletions
+1
View File
@@ -24,6 +24,7 @@ opencode.json
!AGENTS.md
!CODE_OF_CONDUCT.md
!SECURITY.md
!HIGHLIGHTS.md
/*.png
/*.svg
/*.sql
+26
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -10,7 +10,7 @@ desc: Learn how to organize your work in Penpot. Create, manage and organize pro
<h2 id="projects-management">Projects</h2>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<figure>
<img src="/img/files-projects/01-projects.webp" alt="Projects view in dashboard" />
</figure>
+1 -1
View File
@@ -16,7 +16,7 @@ member is allowed to do depends on their permissions.</p>
<h3>Select team</h3>
<p>At the top left of the dashboard you can find the team selector.</p>
<p>"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.</p>
<p>"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.</p>
<figure><img src="/img/teams/team-selector.webp" alt="Teams selector" /></figure>
<h3>Create teams</h3>
@@ -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(
@@ -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%;
+6 -4
View File
@@ -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"
@@ -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)
@@ -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]
@@ -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
+3 -3
View File
@@ -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)))))
@@ -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?)
+60 -30
View File
@@ -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)]]]]]))
@@ -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 {
@@ -97,6 +97,7 @@
&::selection {
background: var(--color-accent-background-select);
color: var(--color-static-white);
}
&::placeholder {
+1 -7
View File
@@ -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])))
@@ -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)}]]]]))
@@ -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
@@ -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
@@ -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?))]}
@@ -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;
@@ -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
@@ -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
+10
View File
@@ -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))
@@ -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)))
+11 -8
View File
@@ -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"
+11 -8
View File
@@ -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"