diff --git a/CHANGES.md b/CHANGES.md index 001577184c..cddaeec72c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -37,6 +37,7 @@ - [DESIGN TOKENS] Integration with components. - [DESIGN TOKENS] Import and export tokens from a JSON file. - [DESIGN TOKENS] Apply Themes and Sets at document level. +- Add more descriptive tooltip to boards for first time users [Taiga #9426](https://tree.taiga.io/project/penpot/us/9426) ### :bug: Bugs fixed @@ -51,6 +52,8 @@ - Fix problem with default shadows value in plugins [Plugins #191](https://github.com/penpot/penpot-plugins/issues/191) - Fix problem with constraints when creating group [Taiga #10455](https://tree.taiga.io/project/penpot/issue/10455) - Fix opening pen with shortcut multiple times breaks toolbar [Taiga #10566](https://tree.taiga.io/project/penpot/issue/10566) +- Fix actions when workspace is visited first time [Taiga #10548](https://tree.taiga.io/project/penpot/issue/10548) +- Chat icon overlaps "Show" button in carrousel section [Taiga #10542](https://tree.taiga.io/project/penpot/issue/10542) ## 2.5.4 diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 5006d0c47d..80268f617e 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -82,15 +82,22 @@ "Assoc a k v pair, in the order position just before the other key." [o ks k v before-k] (let [f (fn [o'] - (cond-> (reduce - (fn [acc [k' v']] - (cond - (and before-k (= k' before-k)) (assoc acc k v k' v') - (= k k') acc - :else (assoc acc k' v'))) - (ordered-map) - o') - (not before-k) (assoc k v)))] + (let [found (volatile! false) + result (reduce + (fn [acc [k' v']] + (cond + (and before-k (= k' before-k)) + (do + (vreset! found true) + (assoc acc k v k' v')) + + (= k k') acc + :else (assoc acc k' v'))) + (ordered-map) + o')] + (if (or (not before-k) (not @found)) + (assoc result k v) + result)))] (if (seq ks) (oupdate-in o ks f) (f o)))) diff --git a/common/src/app/common/logic/tokens.cljc b/common/src/app/common/logic/tokens.cljc index 3e7ca4a08d..39f2374bf3 100644 --- a/common/src/app/common/logic/tokens.cljc +++ b/common/src/app/common/logic/tokens.cljc @@ -65,13 +65,7 @@ to (nth tree to-index) before (case position :top to - :bot (let [v (nth tree (inc to-index) nil)] - ;; if the next index is a group, we need to set it as - ;; nil because if we set a path on different subpath, - ;; the move algorightm will simply remove the set - (if (:group? v) - nil - v)) + :bot (nth tree (inc to-index) nil) :center nil) prev-before (if (:group? from) @@ -87,6 +81,7 @@ (= :bot position) (:group? to) (not (get collapsed-paths (:path to))))) + from-path (:path from) to-parent-path (if drop-as-direct-group-child? (:path to) diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index bba70ba5ac..12f5c5d3e8 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -60,7 +60,7 @@ (token-types t)) (def token-name-ref - [:and :string [:re #"^(?!\$)([a-zA-Z0-9-$]+\.?)*(?Podman instead of Docker. * Try the under development Penpot Desktop app. * Try a simple Kubernetes Deployment option penpot-kubernetes. +* Penpot is available in the catalog of apps installable on YunoHost instances. * Or try a fully manual installation if you have a really specific use case.. For help, you can look at the [Architecture][1] section and the Docker configuration files. [1]: /technical-guide/developer/architecture diff --git a/docs/user-guide/components/index.njk b/docs/user-guide/components/index.njk index bac8b0ed97..44893da64d 100644 --- a/docs/user-guide/components/index.njk +++ b/docs/user-guide/components/index.njk @@ -1,5 +1,5 @@ --- -title: 10· Components +title: 11· Components ---

Components

diff --git a/docs/user-guide/custom-fonts/index.njk b/docs/user-guide/custom-fonts/index.njk index 0b8965b1c2..096ab313dd 100644 --- a/docs/user-guide/custom-fonts/index.njk +++ b/docs/user-guide/custom-fonts/index.njk @@ -1,5 +1,5 @@ --- -title: 16· Custom fonts +title: 17· Custom fonts ---

Custom fonts

diff --git a/docs/user-guide/design-tokens/index.njk b/docs/user-guide/design-tokens/index.njk new file mode 100644 index 0000000000..1e6228b911 --- /dev/null +++ b/docs/user-guide/design-tokens/index.njk @@ -0,0 +1,377 @@ +--- +title: 10· Design Tokens +--- + +

Design Tokens

+

Design tokens are the building blocks of all UI elements, the same tokens are used in designs, tools, and code. They include colors, typography, spacing, shadows, and any visual element that affects an object: all these properties collectively make up a design system or a visual inheritance.

+ +
+ Tokens cover +
+ +

Why Design Tokens?

+

Design tokens act as a single source of truth, a common language that can be translated and used in any other tool or framework capable of reading the token format. With Design Tokens, you can create, manage, and synchronize these visual elements within Penpot and across other design tools, keeping your designs consistent and making your workflows faster and easier to maintain.

+

You can also integrate Design Tokens with other core Penpot features, such as components and grid & flex layout, plus plugins will be able to access the tokens API (coming soon) making it even more powerful.

+ +

W3C DTCG Format

+

Penpot Design Tokens adhere to the Design Tokens Format Module and its definitions, a draft by the W3C DTCG. Penpot ensures compatibility across various disciplines, tools, and technologies by following the most standardized approach available for design tokens.

+

Tokens can be exported from Penpot or integrated into other tools directly, without conversion. Additionally, the knowledge gained from using Design Tokens in Penpot remains valuable, regardless of whether you continue using Penpot or a different tool or technology.

+ +

Using Tokens

+

Creating a token

+

You can create reusable and semantic tokens to be referenced in your designs at the Tokens panel. In this panel, you’ll find all the available types of tokens in Penpot arranged alphabetically, with existing tokens being shown at the top of the list.

+
+ Tokens create +
+

To create a token, click on the + next to the type of token you want to create. You’ll then be prompted to define the token’s:

+ +

Once you have named the token and assigned it a value, click Save to store the token and start referencing it.

+ +

Referencing tokens into values (aliases)

+

When assigning a value to a token, you can reference existing tokens - these are called aliases at the DTCG Glossary.

+
+ Tokens aliases +
+

For example, if you have created a dimension.small token, with a value of 64, you could create a spacing.small token with a value of {dimension.small}. The spacing.small token would thereby have a value of 64.

+

When referencing an existing token in the value of a new token, you must reference it within {}.

+

If the value of the referenced token changes, this will also change the value of the tokens where it is referenced.

+

References to existing tokens are case sensitive.

+ +

Using equations

+

Token types with numerical values also accept mathematical equations. If, for example, you create a spacing.small token with the value of 2, and you then want to create a spacing.medium token that is twice as large, you could do so by writing {spacing.small} * 2 in its value. As a result, spacing.medium would have a value of 4.

+

Say you have a spacing.scale token with a value of 2. You could also use this token in the equation to calculate the value of spacing.medium by writing {spacing.small} * {spacing.scale} in its value.

+
+ Tokens math +
+

Mathematical equations can be performed using:

+ + +

Editing a token

+

Tokens can be edited by right-clicking the token and selecting Edit token. This will allow you to change the tokens name, value and description. Once the changes are made, click Save.

+
+ Tokens edit +
+

Renaming tokens will break any references to their old names. If a token is already applied somewhere, you'll need to reapply it after renaming. This can lead to extra work, so rename with caution. We're actively working on a solution to handle this automatically, ensuring renamed tokens stay linked to their properties without additional effort.

+ +

Duplicating a token

+

Tokens can be duplicated by right-clicking the token you wish to duplicate and selecting Duplicate token. This will create a copy of the selected token within the same set, with -copy added to its name.

+ +

Deleting a token

+

Tokens can be deleted by right-clicking the token you wish to delete and selecting Delete token.

+ +

Available tokens

+

You can apply tokens to the properties of any object. There are two ways to apply tokens to a selection:

+ +

Tokens can be applied to multiple selected elements, but not to groups.

+ +

Border radius

+

Border radius tokens allow you to define specific values for border-radius properties, offering flexibility in how you style the corners of elements.

+
+ Tokens radius +
+

Applying Border Radius Tokens

+

To apply the border radius token to an element, select the element and choose the token from the list:

+ + +

Color

+

Color tokens support color properties that can be applied to many different design elements, including boards, groups, shapes, and text.

+
+ Tokens color create +
+

You can define a color token’s value using:

+
    +
  1. The color picker, select the color switch to the left of the token Value input to open the color picker; here you’ll also be able to define the colors opacity.
  2. +
  3. + The color Spaces, define your color token in the following color spaces: + +
  4. +
+

Applying Color Tokens

+

Color tokens can define a design element's fill or stroke color. To apply the color token to an element, select the element and choose the token from the list:

+ +
+ Tokens color +
+ +

Dimensions

+

Dimension tokens allow you to define an amount of distance that can be used to set the size, space, radius or position of specific elements within a design.

+

Applying Dimension Tokens

+

To apply a dimension token, select the element and choose the token from the list:

+ +

Sizing (dimension)

+

The sizing property of the dimension token defines the height or width of design elements like boards, shapes, and groups.

+
+ Tokens dimensions sizing +
+

When using dimension tokens for sizing, you can apply the token to an element's size by selecting:

+ +

If you are working with flex-layout boards, you can also apply the token to an element’s size by selecting:

+ +

If you apply the min/max height/width properties to a board before flex-layout is applied to it, you may have to remove and re-apply the token for it to take effect.

+ +

Spacing (dimension)

+

The spacing property of the dimension token defines the distance between elements and it must be applied to flex-layout boards.

+
+ Tokens dimensions spacing +
+

If you apply the token to a board before flex-layout is applied to it, you may have to remove and re-apply the token for it to take effect.

+

When using dimension tokens for spacing, you can apply the token to an element's padding and gap. Specifically, you can select:

+ + +

Border radius (dimension)

+

The border radius property of the dimension token defines the roundness of the corner of elements like boards, shapes, and groups.

+

You can apply the border radius property by right-clicking on the dimension token and selecting:

+ + +

Stroke width (dimension)

+

The stroke width property specifies the thickness of a border for elements that already have a stroke property applied.

+

If you apply the border property to an element before it has a stroke applied to it, you may have to remove and re-apply the token for it to take effect.

+ +

X Position (dimension)

+

The X property specifies the position of the element on the X axis of the canvas.

+ +

Y Position (dimension)

+

The Y property specifies the position of the element on the Y axis of the canvas.

+ +

Opacity

+

Opacity tokens allow you to define the opacity of a layer, ranging from fully opaque to fully transparent.

+

Opacity tokens can be applied to any design element that supports transparency. You can use any decimal value between 0 and 1 to set varying levels of opacity or you can use any value between 0 and 100 with `%` sign at the end of the value. For example, you can use 45% which would resolve to .45.

+

Applying Opacity Tokens

+

To apply the opacity token to an element, select the element and click on the desired token.

+ +

Rotation

+

Rotation tokens are used to define and standardize rotational values within a design system. These tokens represent rotation angles, typically measured in degrees, and can be applied to elements such as icons or images, to ensure consistent rotation throughout a design.

+

Applying Rotation Tokens

+

To apply a rotation token, select the element and choose the token from the list.

+ +

Sizing

+

Sizing tokens can define various size-related design properties, namely the height and width of design elements.The sizing token supports numeric values, which include negative values.

+
+ Tokens spacing +
+

Applying Sizing Tokens

+

To apply the sizing token to an element, select the element and choose the token from the list:

+ +

If you are working with flex-layout boards, you can also apply the token to an element’s size by selecting:

+ + +

Spacing

+

The spacing token defines the distance between design elements and supports numeric values, which include negative values. Spacing tokens must be applied to Flex Layout boards.

+

If you apply the token to a board before flex-layout is applied to it, you may have to remove and re-apply the token for it to take effect.

+

Applying Spacing Tokens

+

To apply the spacing token to an element, select the element and choose the token from the list:

+ + +

Stroke Width

+

The stroke width token, also known as the border width token, defines the thickness of a stroke around a design element. It can be applied to boards, groups, rectangles and text elements.

+ + +

Token Sets

+

Token Sets allow you to split your tokens up into multiple files in order to create organized groups or collections of tokens. It enables efficient management and customization within design files. For example you can group all your color sets, sizing sets or platform-specific sets. The purpose of tokens sets is to organize them in a way that matches your needs.

+
+ Tokens sets +
+

When you create your first token, a default set is created. You can rename, group or move it later. As you create new token sets, they’ll be added sequentially after existing ones. You can reorder token sets by dragging and dropping them.

+

The order of the token sets is essential! If you have tokens with the same name (and different values) across multiple sets, the tokens that are in the set that appears last in the list will override the previous ones - similar to how Cascading Style Sheets work.

+

When creating a token set, it’s recommended that you assign it a unique name to ensure clarity. Token set names are not included in individual token names by default so it is possible to have tokens with the same name belonging to different token sets.

+

Token sets can be enabled or disabled. If a set is disabled, its tokens will be excluded from the token resolution process.

+ +

Creating Token Sets

+

There are two ways to create a token set at the Tokens tab:

+
    +
  1. Click on the + next to Sets;
  2. +
  3. Click on the Create one button.
  4. +
+

You’ll then need to name your token set. Set names should be specific, as it is not possible to create multiple token sets with the same name.

+

When a token set is selected, the tokens within the selected set are displayed on the panel below.

+ + +

Deleting and Renaming a Token Set

+

Token sets can be renamed or deleted by right-clicking on the token set and:

+
    +
  1. Selecting Rename, entering a new name, and hitting Enter.
  2. +
  3. Selecting Delete.
  4. +
+
+ Tokens sets edit +
+ +

+

Once you have created a token set, you can start creating tokens within that token set. To do so, simply select the token set and create a new token.

+

If a token with the same name already exists in another set, a new token can still be created in the current set.

+ +

Creating Token Groups

+

You can create a token set group by simply naming your token sets to have a folder path. For example, you can create a Light group with a Global set and a Colors set using: Light/Global, Light/Colors.

+
+ Tokens sets group +
+ +

Token Themes

+

Themes are a way to configure your sets to be applied in a specific context, such as a brand, a mode or a touchpoint. Themes enable switching between different styles dynamically by applying different token values depending on the selected theme.

+

Themes are multidimensional, this means that you can have more than one theme active at the same time, combining the values of the active themes.

+ +

Theme groups

+

Using Theme Groups you can categorise your themes into groups. This will allow you to generate a number of combinations involving color themes, brands, platforms, density, and more. Using groups will reduce the need to create an excessive number of individual themes with every combination.

+
+ Tokens themes +
+

For example:

+

Group - theme options

+ +

When you have various themes inside a group, only one of the themes in this group can be active.

+

Having your sets clubbed under groups makes it more accessible to switch from a matrix of themes.

+ +

Creating Token Themes

+

To create a new theme, click the Create one button in the Themes section. You can create a group (this is optional) or add an existing one, and then you then need to assign a name to your theme and click on Save Theme.

+

Your new theme will now appear on the Theme lists. You’ll need to enable the tokens sets that you want to include in the theme, clicking on the button “no active sets”. Here you can also activate and deactivate it, as well as delete the theme.

+
+ Tokens themes create +
+ +

Editing Themes

+

In the Themes section, you can find a dropdown to activate and deactivate themes. If there are no active themes, the dropdown shows a message of: “no theme active”.

+
+ Tokens themes edit +
+

To edit existing themes, you can click on the Edit button next to the dropdown in the Themes section or open the dropdown and select Edit themes:

+
+ Tokens themes list +
+

This action will open a modal window where you can activate or deactivate themes, as well as select which Token sets should be part of the theme.

+
    +
  1. You can enable and disable the themes.
  2. +
  3. Configure the token sets you want to be included in the theme.
  4. +
  5. Deletes the theme.
  6. +
  7. Creates a new theme.
  8. +
+ +

Grouping Themes

+

You can categorize your themes into groups. This allows you to generate a matrix of potential combinations involving color themes, brands, modes, and more.

+
+ Tokens themes group +
+ +
    +
  1. Select a particular group.
  2. +
  3. Select the theme from the group.
  4. +
  5. You can define what token sets should be used as part of this theme option.
  6. +
  7. Click save theme to see the changes.
  8. +
  9. Cancel to clear the edits.
  10. +
  11. Delete.
  12. +
+ +

Importing and Exporting Tokens

+

You can export Tokens from Penpot and import them from your computer to a Penpot file. Tokens can be imported from the Tools option at the bottom of the Tokens tab.

+

The Import functionality allows you to upload and replace the global token set using a single file, while the Export functionality lets you download the current global token set using a single file to your system.

+

These features support JSON files formatted according to specific guidelines and preserve the ability to undo changes if needed.

+
+ Tokens import export +
+
    +
  1. Import: At the Tools option, select Import, then select your tokens.json file.
  2. +
  3. Export: At the Tools option, select Export. This will export all the tokens, including token sets and themes.
  4. +
diff --git a/docs/user-guide/import-export/index.njk b/docs/user-guide/import-export/index.njk index 3d1dcdf02a..7aa9cb4f3f 100644 --- a/docs/user-guide/import-export/index.njk +++ b/docs/user-guide/import-export/index.njk @@ -1,5 +1,5 @@ --- -title: 14· Import/export files +title: 15· Import/export files ---

Import and export files

diff --git a/docs/user-guide/inspect/index.njk b/docs/user-guide/inspect/index.njk index a95ba5840f..869429287d 100644 --- a/docs/user-guide/inspect/index.njk +++ b/docs/user-guide/inspect/index.njk @@ -1,5 +1,5 @@ --- -title: 13· Inspect designs +title: 14· Inspect designs ---

Inspect designs

diff --git a/docs/user-guide/plugins/index.njk b/docs/user-guide/plugins/index.njk index 7ed67f339a..8916145b6e 100644 --- a/docs/user-guide/plugins/index.njk +++ b/docs/user-guide/plugins/index.njk @@ -1,5 +1,5 @@ --- -title: 17· Plugins +title: 18· Plugins ---

Penpot Plugins

diff --git a/docs/user-guide/prototyping/index.njk b/docs/user-guide/prototyping/index.njk index 5242877baf..388f9b2d1b 100644 --- a/docs/user-guide/prototyping/index.njk +++ b/docs/user-guide/prototyping/index.njk @@ -1,5 +1,5 @@ --- -title: 11· Prototyping +title: 12· Prototyping ---

Prototyping interactions

diff --git a/docs/user-guide/teams/index.njk b/docs/user-guide/teams/index.njk index ce9cb1e789..0cd6ae9418 100644 --- a/docs/user-guide/teams/index.njk +++ b/docs/user-guide/teams/index.njk @@ -1,5 +1,5 @@ --- -title: 15· Teams +title: 16· Teams ---

Teams

diff --git a/docs/user-guide/view-mode/index.njk b/docs/user-guide/view-mode/index.njk index 081d69f4f5..d829e66753 100644 --- a/docs/user-guide/view-mode/index.njk +++ b/docs/user-guide/view-mode/index.njk @@ -1,5 +1,5 @@ --- -title: 12· View mode +title: 13· View mode ---

View mode

diff --git a/frontend/playwright/data/workspace/update-profile-empty.json b/frontend/playwright/data/workspace/update-profile-empty.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/frontend/playwright/data/workspace/update-profile-empty.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/playwright/ui/specs/versions.spec.js b/frontend/playwright/ui/specs/versions.spec.js index 93b883a18f..edd8fc9ea9 100644 --- a/frontend/playwright/ui/specs/versions.spec.js +++ b/frontend/playwright/ui/specs/versions.spec.js @@ -20,11 +20,23 @@ test("Save and restore version", async ({ page }) => { "workspace/update-file-create-rect.json", ); + await workspacePage.mockRPC( + "push-audit-events", + "workspace/audit-event-empty.json", + ); + + await workspacePage.mockRPC( + "update-profile-props", + "workspace/update-profile-empty.json", + ); + await workspacePage.goToWorkspace({ fileId: "406b7b01-d3e2-80e4-8005-3138ac5d449c", pageId: "406b7b01-d3e2-80e4-8005-3138ac5d449d", }); + await workspacePage.moveButton.click(); + await workspacePage.mockRPC( "get-file-snapshots?file-id=*", "workspace/versions-snapshot-1.json", diff --git a/frontend/src/app/main/data/persistence.cljs b/frontend/src/app/main/data/persistence.cljs index 727bb8548b..4ecbac5458 100644 --- a/frontend/src/app/main/data/persistence.cljs +++ b/frontend/src/app/main/data/persistence.cljs @@ -204,6 +204,9 @@ (rx/filter #(= % ::force-persist))))] (rx/merge + (->> notifier-s + (rx/map #(ptk/data-event ::persistence-notification))) + (->> local-commits-s (rx/debounce 200) (rx/map (fn [_] diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 2c1a00620d..8af363d3ca 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -44,6 +44,7 @@ [app.main.data.helpers :as dsh] [app.main.data.modal :as modal] [app.main.data.notifications :as ntf] + [app.main.data.persistence :as-alias dps] [app.main.data.plugins :as dp] [app.main.data.profile :as du] [app.main.data.project :as dpj] @@ -354,6 +355,11 @@ (-> (workspace-initialized file-id) (with-meta {:file-id file-id})))))) + (->> stream + (rx/filter (ptk/type? ::dps/persistence-notification)) + (rx/take 1) + (rx/map dwc/set-workspace-visited)) + (when-let [component-id (some-> rparams :component-id parse-uuid)] (->> stream (rx/filter (ptk/type? ::workspace-initialized)) @@ -476,10 +482,13 @@ ptk/WatchEvent (watch [_ state _] (if-let [page (dsh/lookup-page state file-id page-id)] - (rx/of (initialize-page* file-id page-id page) - (dwth/watch-state-changes file-id page-id) - (dwl/watch-component-changes) - (select-frame-tool file-id page-id)) + (rx/concat (rx/of (initialize-page* file-id page-id page) + (dwth/watch-state-changes file-id page-id) + (dwl/watch-component-changes)) + (let [profile (:profile state) + props (get profile :props)] + (when (not (:workspace-visited props)) + (rx/of (select-frame-tool file-id page-id))))) (rx/of (dcm/go-to-workspace :file-id file-id ::rt/replace true)))))) (defn finalize-page diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 8b9242b016..8a4cf0d981 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -7,7 +7,6 @@ (ns app.main.data.workspace.common (:require [app.common.logging :as log] - [app.config :as cf] [app.main.data.profile :as du] [app.main.data.workspace.layout :as dwl] [beicon.v2.core :as rx] @@ -38,7 +37,7 @@ (watch [_ state _] (let [profile (:profile state) props (get profile :props)] - (when (and (cf/external-feature-flag "boards-03" "test") (not (:workspace-visited props))) + (when (not (:workspace-visited props)) (rx/of (du/update-profile-props {:workspace-visited true}))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index 0fefbd47a0..467a999192 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -14,7 +14,6 @@ [app.main.data.changes :as dch] [app.main.data.helpers :as dsh] [app.main.data.persistence :as-alias dps] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.notifications :as-alias wnt] [app.main.rasterizer :as thr] [app.main.refs :as refs] @@ -293,10 +292,4 @@ (rx/mapcat #(into #{} %)) (rx/map #(update-thumbnail file-id page-id % "frame" "watch-state-changes")))) - ;; WARNING: This is a workaround for an AB test, in case we consolidate this change we should - ;; find a better way to handle this. - (->> notifier-s - (rx/take 1) - (rx/map dwc/set-workspace-visited)) - (rx/take-until stopper-s)))))) diff --git a/frontend/src/app/main/data/workspace/viewport.cljs b/frontend/src/app/main/data/workspace/viewport.cljs index 190440811e..97e88bc29a 100644 --- a/frontend/src/app/main/data/workspace/viewport.cljs +++ b/frontend/src/app/main/data/workspace/viewport.cljs @@ -10,6 +10,7 @@ [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] [app.common.geom.align :as gal] + [app.common.geom.point :as gpt] [app.common.geom.rect :as gpr] [app.common.geom.shapes :as gsh] [app.common.math :as mth] @@ -83,6 +84,25 @@ (fn [local] (setup state local))))))) +(defn calculate-centered-viewbox + "Updates the viewbox coordinates for a given center position" + [local position] + (let [vbox (:vbox local) + nw (/ (:width vbox) 2) + nh (/ (:height vbox) 2) + nx (- (:x position) nw) + ny (- (:y position) nh)] + (update local :vbox assoc :x nx :y ny))) + +(defn update-viewport-position-center + [position] + (assert (gpt/point? position) "expected a point instance for `position` param") + + (ptk/reify ::update-viewport-position-center + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local calculate-centered-viewbox position)))) + (defn update-viewport-position [{:keys [x y] :or {x identity y identity}}] diff --git a/frontend/src/app/main/data/workspace/zoom.cljs b/frontend/src/app/main/data/workspace/zoom.cljs index f799549cd4..843ea5f3f9 100644 --- a/frontend/src/app/main/data/workspace/zoom.cljs +++ b/frontend/src/app/main/data/workspace/zoom.cljs @@ -20,7 +20,7 @@ [beicon.v2.core :as rx] [potok.v2.core :as ptk])) -(defn- impl-update-zoom +(defn impl-update-zoom [{:keys [vbox] :as local} center zoom] (let [new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom) old-zoom (:zoom local) diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index bb8f0fc2fe..2955543540 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -17,6 +17,7 @@ [app.main.data.comments :as dcm] [app.main.data.modal :as modal] [app.main.data.workspace.comments :as dwcm] + [app.main.data.workspace.viewport :as dwv] [app.main.data.workspace.zoom :as dwz] [app.main.refs :as refs] [app.main.store :as st] @@ -1097,15 +1098,35 @@ groups)) (group-bubbles zoom remaining visited (cons [current] groups))))))) -(defn- calculate-zoom-scale-to-ungroup-bubbles - "Calculate the minimum zoom scale needed for a group of bubbles to avoid overlap among them" - [zoom threads] +(defn- inside-vbox? + "Checks if a bubble or a bubble group is inside a viewbox" + [thread-group wl] + (let [vbox (:vbox wl) + positions (mapv :position thread-group) + position (gpt/center-points positions) + pos-x (:x position) + pos-y (:y position) + x1 (:x vbox) + y1 (:y vbox) + x2 (+ x1 (:width vbox)) + y2 (+ y1 (:height vbox))] + (and (> x2 pos-x x1) (> y2 pos-y y1)))) + +(defn- calculate-zoom-scale + "Calculates the zoom level needed to ungroup the largest number of bubbles while + keeping them all visible in the viewbox." + [position zoom threads wl] (let [num-threads (count threads) - num-grouped-threads (count (group-bubbles zoom threads)) - zoom-scale-step 1.75] - (if (= num-threads num-grouped-threads) + grouped-threads (group-bubbles zoom threads) + num-grouped-threads (count grouped-threads) + zoom-scale-step 1.75 + scaled-zoom (* zoom zoom-scale-step) + zoomed-wl (dwz/impl-update-zoom wl position scaled-zoom) + outside-vbox? (complement inside-vbox?)] + (if (or (= num-threads num-grouped-threads) + (some #(outside-vbox? % zoomed-wl) grouped-threads)) zoom - (calculate-zoom-scale-to-ungroup-bubbles (* zoom zoom-scale-step) threads)))) + (calculate-zoom-scale position scaled-zoom threads zoomed-wl)))) (mf/defc comment-floating-group* {::mf/wrap [mf/memo]} @@ -1126,11 +1147,14 @@ on-click (mf/use-fn - (mf/deps thread-group position) + (mf/deps thread-group position zoom) (fn [] - (let [updated-zoom (calculate-zoom-scale-to-ungroup-bubbles zoom thread-group) + (let [wl (deref refs/workspace-local) + centered-wl (dwv/calculate-centered-viewbox wl position) + updated-zoom (calculate-zoom-scale position zoom thread-group centered-wl) scale-zoom (/ updated-zoom zoom)] - (st/emit! (dwz/set-zoom position scale-zoom)))))] + (st/emit! (dwv/update-viewport-position-center position) + (dwz/set-zoom position scale-zoom)))))] [:div {:style {:top (dm/str pos-y "px") :left (dm/str pos-x "px")} diff --git a/frontend/src/app/main/ui/dashboard/projects.scss b/frontend/src/app/main/ui/dashboard/projects.scss index a40fce6a3b..252da62cd8 100644 --- a/frontend/src/app/main/ui/dashboard/projects.scss +++ b/frontend/src/app/main/ui/dashboard/projects.scss @@ -13,6 +13,7 @@ margin-right: $s-16; border-top: $s-1 solid var(--panel-border-color); overflow-y: auto; + padding-bottom: $s-32; } .dashboard-projects { diff --git a/frontend/src/app/main/ui/dashboard/templates.scss b/frontend/src/app/main/ui/dashboard/templates.scss index d1a9957261..8d5a6ffb52 100644 --- a/frontend/src/app/main/ui/dashboard/templates.scss +++ b/frontend/src/app/main/ui/dashboard/templates.scss @@ -11,9 +11,10 @@ bottom: 0; border-bottom-left-radius: $br-8; border-bottom-right-radius: $br-8; + border-top-right-radius: $br-8; display: flex; flex-direction: column; - height: $s-228; + height: $s-244; justify-content: flex-end; margin-left: $s-6; margin-right: $s-6; @@ -39,8 +40,8 @@ .title { pointer-events: all; - width: 100%; - top: calc(-1 * $s-56); + width: $s-420; + top: calc(-1 * $s-40); text-align: right; height: $s-56; position: absolute; @@ -102,8 +103,8 @@ .move-button { position: absolute; - top: $s-80; - border: $s-2 solid var(--button-secondary-background-color-rest); + top: $s-96; + border: $s-2 solid var(--color-foreground-secondary); border-radius: 50%; text-align: center; width: $s-36; @@ -139,7 +140,7 @@ font-size: $fs-14; color: var(--color-foreground-primary); margin-bottom: -8px; - margin-top: -4px; + margin-top: $s-16; margin-left: $s-16; visibility: visible; } diff --git a/frontend/src/app/main/ui/hooks/resize.cljs b/frontend/src/app/main/ui/hooks/resize.cljs index 54d5f4e2df..84a89125f9 100644 --- a/frontend/src/app/main/ui/hooks/resize.cljs +++ b/frontend/src/app/main/ui/hooks/resize.cljs @@ -14,6 +14,7 @@ [app.main.ui.context :as ctx] [app.main.ui.hooks :as hooks] [app.util.dom :as dom] + [app.util.globals :as glob] [app.util.storage :as storage] [rumext.v2 :as mf])) @@ -34,6 +35,28 @@ (set! last-resize-type type)) (defn use-resize-hook + "Allows a node to be resized by dragging, and calculates the new size based on the drag setting a maximum and minimum value. + + Parameters: + - `key` - A unique key to identify the resize hook. + - `initial` - The initial size of the node. + - `min-val` - The minimum value the size can be. + - `max-val` - The maximum value the size can be. It can be a number or a string representing either a fixed value or a percentage (0 to 1). + - `axis` - The axis to resize on, either `:x` or `:y`. + - `negate?` - If `true`, the axis is negated. + - `resize-type` - The type of resize, either `:width` or `:height`. + - `on-change-size` - A function to call when the size changes. + + Returns: + - An object with the following: + + - `:on-pointer-down` - A function to call when the pointer is pressed down. + - `:on-lost-pointer-capture` - A function to call when the pointer is released. + - `:on-pointer-move` - A function to call when the pointer is moved. + - `:parent-ref` - A reference to the node. + - `:set-size` - A function to set the size. + - `:size` - The current size." + ([key initial min-val max-val axis negate? resize-type] (use-resize-hook key initial min-val max-val axis negate? resize-type nil)) @@ -48,14 +71,26 @@ start-size-ref (mf/use-ref nil) start-ref (mf/use-ref nil) - window-height (dom/get-window-height) + ;; Since Penpot is not responsive designed, this value will only refer to vertical axis. + window-height* (mf/use-state #(dom/get-window-height)) + window-height (deref window-height*) + ;; In case max-val is a string, we need to parse it as a double. max-val (mf/with-memo [max-val window-height] (let [parsed-max-val (when (string? max-val) (d/parse-double max-val))] (if parsed-max-val (* window-height parsed-max-val) max-val))) + set-size + (mf/use-fn + (mf/deps file-id key min-val max-val window-height) + (fn [new-size] + (let [new-size (mth/clamp new-size min-val max-val)] + (reset! current-size* new-size) + ;; Save the new size in the local storage for this file and this specific node. + (swap! storage/user update-persistent-state file-id key new-size)))) + on-pointer-down (mf/use-fn (mf/deps current-size) @@ -89,21 +124,28 @@ start-size (mf/ref-val start-size-ref) new-size (-> (+ start-size delta) (max min-val) (min max-val))] - (reset! current-size* new-size) - (swap! storage/user update-persistent-state file-id key new-size))))) - set-size + (set-size new-size))))) + + on-resize-window (mf/use-fn - (mf/deps on-change-size file-id key) - (fn [new-size] - (let [new-size (mth/clamp new-size min-val max-val)] - (reset! current-size* new-size) - (swap! storage/user update-persistent-state file-id key new-size))))] + (fn [] + (let [new-window-height (dom/get-window-height)] + (reset! window-height* new-window-height))))] (mf/with-effect [on-change-size current-size] (when on-change-size (on-change-size current-size))) + (mf/with-effect [] + (.addEventListener glob/window "resize" on-resize-window) + (fn [] + (.removeEventListener glob/window "resize" on-resize-window))) + + (mf/with-effect [window-height] + (let [new-size (mth/clamp current-size min-val max-val)] + (set-size new-size))) + {:on-pointer-down on-pointer-down :on-lost-pointer-capture on-lost-pointer-capture :on-pointer-move on-pointer-move diff --git a/frontend/src/app/main/ui/workspace/tokens/form.cljs b/frontend/src/app/main/ui/workspace/tokens/form.cljs index b9e817d911..9306904317 100644 --- a/frontend/src/app/main/ui/workspace/tokens/form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/form.cljs @@ -50,7 +50,7 @@ Caution: This will allow a trailing dot like `token-name.`, But we will trim that in the `finalize-name`, to not throw too many errors while the user is editing." - #"(?!\$)([a-zA-Z0-9-$]+\.?)*") + #"(?!\$)([a-zA-Z0-9-$_]+\.?)*") (def valid-token-name-schema (m/-simple-schema @@ -235,7 +235,14 @@ token-properties (wtch/get-token-properties token) color? (wtt/color-token? token) selected-set-tokens (mf/deref refs/workspace-selected-token-set-tokens) - active-theme-tokens (mf/deref refs/workspace-active-theme-sets-tokens) + + active-theme-tokens (cond-> (mf/deref refs/workspace-active-theme-sets-tokens) + ;; Ensure that the resolved value uses the currently editing token + ;; even if the name has been overriden by a token with the same name + ;; in another set below. + (and (:name token) (:value token)) + (assoc (:name token) token)) + resolved-tokens (sd/use-resolved-tokens active-theme-tokens {:cache-atom form-token-cache-atom :interactive? true}) token-path (mf/use-memo diff --git a/frontend/src/app/main/ui/workspace/tokens/modals.cljs b/frontend/src/app/main/ui/workspace/tokens/modals.cljs index 2f23ea933b..fe7e4504b7 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.tokens.modals (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.main.data.modal :as modal] [app.main.refs :as refs] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] @@ -20,30 +21,55 @@ (defn calculate-position "Calculates the style properties for the given coordinates and position" - [{vh :height} position x y] - (let [;; picker height in pixels - h 510 + [{vh :height} position x y color?] + (let [;; picker height in pixels + ;; TODO: Revisit these harcoded values + h (if color? 610 510) ;; Checks for overflow outside the viewport height - overflow-fix (max 0 (+ y (- 50) h (- vh))) - - x-pos 325] + max-y (- vh h) + overflow-fix (max 0 (+ y (- 50) h (- vh))) + bottom-offset "1rem" + top-offset (dm/str (- y 70) "px") + max-height-top (str "calc(100vh - " top-offset) + max-height-bottom (str "calc(100vh -" bottom-offset) + x-pos 325 + rulers? (mf/deref refs/rulers?) + left-offset (if rulers? 80 58) + left-position (dm/str (- x x-pos) "px")] (cond - (or (nil? x) (nil? y)) {:left "auto" :right "16rem" :top "4rem"} - (= position :left) {:left (str (- x x-pos) "px") - :top (str (- y 50 overflow-fix) "px")} - :else {:left (str (+ x 80) "px") - :top (str (- y 70 overflow-fix) "px")}))) + (or (nil? x) (nil? y)) + {:left "auto" :right "16rem" :top "4rem"} -(defn use-viewport-position-style [x y position] + (= position :left) + (if (> y max-y) + {:left left-position + :bottom bottom-offset + :maxHeight max-height-bottom} + + {:left left-position + :maxHeight max-height-top + :top (dm/str (- y 50 overflow-fix) "px")}) + + :else + (if (> y max-y) + {:left (dm/str (+ x left-offset) "px") + :bottom bottom-offset + :maxHeight max-height-bottom} + + {:left (dm/str (+ x left-offset) "px") + :top (dm/str (- y 70 overflow-fix) "px") + :maxHeight max-height-top})))) + +(defn use-viewport-position-style [x y position color?] (let [vport (-> (l/derived :vport refs/workspace-local) (mf/deref))] - (-> (calculate-position vport position x y) + (-> (calculate-position vport position x y color?) (clj->js)))) (mf/defc token-update-create-modal {::mf/wrap-props false} [{:keys [x y position token token-type action selected-token-set-name] :as _args}] - (let [wrapper-style (use-viewport-position-style x y position) + (let [wrapper-style (use-viewport-position-style x y position (= token-type :color)) modal-size-large* (mf/use-state false) modal-size-large? (deref modal-size-large*) close-modal (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs index d118136fc6..7050e47a0e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs @@ -160,7 +160,7 @@ (fn [event] (dom/prevent-default event) (dom/stop-propagation event) - (when-not is-editing + (when (and can-edit? (not is-editing)) (st/emit! (dt/assign-token-set-context-menu {:position (dom/get-client-position event) :is-group true diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index 16f27a42f1..2fe682fb3a 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -233,6 +233,7 @@ (mf/defc token-sets-section* {::mf/private true} [{:keys [resize-height] :as props}] + (let [can-edit? (mf/use-ctx ctx/can-edit?)] diff --git a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs index 029791d9db..d6fc48b7c0 100644 --- a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs @@ -336,7 +336,7 @@ [tokens & {:keys [interactive?]}] (let [state* (mf/use-state tokens)] (mf/with-effect [tokens interactive?] - (when (seq tokens) + (if (seq tokens) (let [tpoint (dt/tpoint-ms) promise (if interactive? (resolve-tokens-interactive+ tokens) @@ -346,5 +346,6 @@ (p/fmap (fn [resolved-tokens] (let [elapsed (tpoint)] (l/dbg :hint "use-resolved-tokens*" :elapsed elapsed) - (reset! state* resolved-tokens)))))))) + (reset! state* resolved-tokens)))))) + (reset! state* tokens))) @state*)) diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.cljs b/frontend/src/app/main/ui/workspace/top_toolbar.cljs index bec682c80d..0532008ffe 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/top_toolbar.cljs @@ -10,7 +10,6 @@ [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.media :as cm] - [app.config :as cf] [app.main.data.event :as ev] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] @@ -126,8 +125,7 @@ profile (mf/deref refs/profile) props (get profile :props) test-tooltip-board-text - (if (and (cf/external-feature-flag "boards-03" "test") - (not (:workspace-visited props))) + (if (not (:workspace-visited props)) (tr "workspace.toolbar.frame-first-time" (sc/get-tooltip :draw-frame)) (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame)))]