mirror of
https://github.com/penpot/penpot.git
synced 2026-11-06 10:00:38 +00:00
607 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6acb360f4a
|
♻️ Use DS icons and an icon button in the undo history panel (#12472)
* ♻️ Use DS icons and an icon button in the undo history panel The undo history panel (History > Actions) drew its entry type icons with the deprecated icon set, and the arrow that shows the details of an entry was a div with a click handler: no button role, no keyboard access and no accessible name. - The entry type icons use the DS icon (drop-icon is i/drop in the DS). - The arrow becomes a DS action icon button labeled "Show details", with aria-expanded for its state. It keeps its data attribute, its classes and its click handler, and it also shows on keyboard focus. The DS icon draws 12px inside a 16px box, so the entry icon gets a negative margin, and the button wrapper a negative end margin: icon, arrow and text stay where they were. The arrow turns through its own class instead of an svg rule. Add the workspace.undo.entry.show-details key (en, es) and a visual test that draws a rectangle, opens the actions list and expands the entry. AI-assisted-by: claude-opus-5-5 * ♻️ Move undo history panel styles off the deprecated styles Move history.scss to DS tokens, sizes, borders and logical properties, and drop the refactor import. The entry color aliases become local variables of the entry, set by its disabled and hover states, and the arrow opacity goes through one as well. AI-assisted-by: claude-opus-5-5 * 💄 Place the undo history icon and arrow without negative margins Drop the -2px margin of the entry icon and the -6px end margin of the details button wrapper. Keep text and arrow in place with token spacing instead: the text after the 16px icon box starts with --sp-xs instead of --sp-s, and the entry end padding is --sp-xxs, since the 24px button leaves 6px around its 12px arrow. The icon now sits 2px to the right, centered in its box. AI-assisted-by: claude-opus-5-5 |
||
|
|
d6e850e917
|
♻️ Use DS icon buttons for the bottom palette toggles (#12443)
* ♻️ Use DS icon buttons for the bottom palette toggles The color palette and typographies toggles of the bottom palette were raw buttons with deprecated icons. Replace them with DS ghost icon buttons with small icons, as before, that show the open palette through aria-pressed. The layout flags are keywords, so they are coerced to a boolean for aria-pressed. Their title moves to the label, shown in the DS tooltip with the shortcut. The palette actions button spans the palette height, not a DS button size, so it stays raw; only its deprecated icon moves to the DS icon. Add a visual test that compares the palette with each toggle selected with no tolerance. AI-assisted-by: claude-opus-5-5 * ♻️ Move bottom palette styles off the deprecated styles The palette toggles are DS icon buttons, so their placeholders go away. The actions button keeps the tertiary button look through local state variables, and its icon takes the color from the button. Spacing, sizes, borders, opacity and the color aliases move to DS tokens and logical properties, except right and width of the palette, which are the transitioned properties. The grid template is split into areas, rows and columns, since Sass reads its slash as a division with a size variable. The z-index values came from the legacy global scale. The resize area, the toggles and the actions button had their own (4, 2 and 2), plus 0 overrides while collapsed, but they sit in separate grid areas and the palette content does not reach them: removing them changes no pixel with the text or color palette, the actions menu open, the 80px size, or the palette collapsed and hidden. Only the palette's own z-index stays, with a comment: it keeps the palette above the viewport overlays (z-index 1), and drops to their level while collapsed. The help button styles are not used in palette.cljs, but the external chat widget injected into the palette wrapper may use them, so they stay, inlined on DS tokens. palette.scss no longer imports refactor/. AI-assisted-by: claude-opus-5-5 * ♻️ Use a DS icon button for the bottom palette actions The palette actions button (⋮) stayed a raw button because it spans the palette height, not a DS button size. The DS icon button takes its height from --button-height, so a class now stretches it to the palette and the hand-made colors and states go away. It uses the ghost variant, like the palette toggles next to it. The button had no accessible name and did not say that it opens a menu: it now has the "Palette options" label (new en and es keys) with its tooltip, aria-expanded and aria-haspopup. The class given to its icon did not exist in palette.scss and is gone. Only the button corners change in the palette screenshots: the raw button painted its own rounded background, the ghost button is transparent at rest. AI-assisted-by: claude-opus-5-5 |
||
|
|
1d54d170cf
|
♻️ Move layers panel styles off the deprecated button styles (#12478)
The layers panel already uses DS icons and buttons; its two raw buttons (the layer filter pill and the focus mode title) stay raw, since they are custom widgets. Their styles still used the deprecated %button-tag placeholder and color aliases. - Inline %button-tag in the filter pill: border, cursor, centering, and its focus and pressed states through local variables. Its hover rule set the icons to the color they already had, so it goes away. - Resolve the icon, title, pill and menu color aliases to DS tokens. The filter menu item texts are the same color in every state, so only the background changes, through a local variable. The refactor import stays for the shared %menu-dropdown placeholder. Add a visual test of the focus mode title, a filter pill and the filter menu; the panel looks the same as before, pixel for pixel. AI-assisted-by: claude-opus-5-5 |
||
|
|
5259b4b2d5
|
♻️ Use DS buttons in the workspace right header (#12454)
* ♻️ Use DS buttons in the workspace right header The comments and history buttons were raw buttons with deprecated icons; they become DS ghost icon buttons that show their state through aria-pressed, with the unread dot as a child of the comments button. The view mode link had a click handler and no href; it becomes a DS icon button too, so it gets a button role and keyboard activation. Their tooltips open below and to the left, at the top right corner of the screen. The zoom menu's Reset is a DS ghost button. The share link stays raw with its deprecated icon until the DS has a share icon; it now uses its own class instead of the view mode one. Add a visual test that captures the header at rest, with comments active, and with the zoom menu open. AI-assisted-by: claude-opus-5-5 * ♻️ Move workspace right header styles off the deprecated styles The comments, history and view mode buttons are DS icon buttons, so their placeholders go away; the comments class only holds the unread dot. The zoom widget color goes through a local variable, and spacing, sizes, borders and the color aliases move to DS tokens and logical properties. Delete the save status rules: the status widget moved to the left header in #5773 and nothing here uses them anymore. refactor/ stays only for the shared menu placeholders and for the tertiary button and icon placeholders of the share link. AI-assisted-by: claude-opus-5-5 * 🐛 Give the action icon button a color while pressed or active The action variant of the DS icon button set the active background but not the active foreground, so it kept the base value, initial. A custom property set to initial is invalid at computed time, so the icon took the color of its container while the button was pressed (:active) or toggled on (aria-pressed): yellow in the workspace header, for example. Use the accent color, as the ghost and secondary variants do. AI-assisted-by: claude-opus-5-5 * 💄 Use the action variant for the right header icon buttons The comments, history and view mode buttons had no background at rest, on hover or while active. The DS action variant keeps that: no background at rest and on hover, accent icon on hover. It is 24px instead of the ghost 32px, and shows a small background while comments or history is on. The unread dot moves to 4px from the button corner, so it stays over the top right corner of the icon in the smaller button. AI-assisted-by: claude-opus-5-5 * 💄 Drop the background of a pressed action icon button A toggled-on action icon button (aria-pressed) showed the quaternary background next to the accent icon. Keep only the accent icon, as the workspace header buttons looked before the DS migration. The background stays for the click feedback (:active). Only the right header comments and history buttons use the action variant with aria-pressed today. AI-assisted-by: claude-opus-5-5 * 💄 Widen the gap between the right header actions The action icon buttons are 24px wide, smaller than the old ones, so the comments, history and view mode buttons sat closer together than before the DS migration. Raise the header gap from --sp-s to --sp-m to keep the old spacing between the icons without resizing the buttons. The separator absorbs the extra space, so the header width stays the same. AI-assisted-by: claude-opus-5-5 * ♻️ Make the right header zoom widget a button The zoom percentage of the right header opened its menu from a div with a click handler: no button role and no keyboard access. Make it a button with aria-expanded, keeping its classes, title and handler; its visible percentage is its accessible name. Reset the browser padding of the button so the percentage stays in place. AI-assisted-by: claude-opus-5-5 * ✨ Toggle the zoom dropdown from the zoom widget Clicking the zoom widget while its dropdown was open left it open: the handler always set the open state, and it stops the click before the dropdown sees it. The widget now toggles the dropdown, as its aria-expanded already suggests. Closing from outside or with Escape works as before. AI-assisted-by: claude-opus-5-5 |
||
|
|
5dbf0731e5
|
♻️ Use DS buttons and icons in the plugins dialogs (#12470)
* ♻️ Use DS buttons and icons in the plugins dialogs The plugins manager and the permissions, permissions update and try-out dialogs used raw buttons, input elements styled as buttons and deprecated icons. Replace them with the DS components: - The four close buttons become ghost icon buttons labeled "Close", shown in their DS tooltip. The class that places them in the corner goes on the tooltip wrapper, so the tooltip anchors to the button. - Install, and the cancel and accept buttons of the three dialogs, become DS primary and secondary buttons. - The empty list puzzle and the external link icons use the DS icon. The permission icons (oauth-*) stay: they are brand art. Add a visual test with fixtures for a team with the plugins feature and a profile with one installed plugin. It shows the empty manager, the manager with a plugin and the permissions dialog. AI-assisted-by: claude-opus-5-5 * ♻️ Move plugins dialog styles off the deprecated styles Remove the styles the DS buttons and icons no longer need (close-icon, primary-button, cancel-button, search-icon and the svg rules of the migrated icons) and move the rest of plugins.scss to DS tokens, DS typographies and logical properties. - The close button wrapper keeps only its position; the deprecated tertiary button style was landing on the tooltip wrapper. - The full-width cancel and accept buttons center their label again: the DS button does not center it when it grows. - The external link icon gets a class instead of an svg rule. - The deprecated body-small used line-height 1.4 and the DS one 1.3, so the permissions dialog is a few pixels shorter. The refactor import stays for the modal placeholders until the modals move to the DS modal. AI-assisted-by: claude-opus-5-5 |
||
|
|
858d5dad9c
|
♻️ Use DS buttons in the debug shape panel (#12484)
* ♻️ Use DS buttons in the debug shape panel The object and tree dump buttons of the debug shape panel were raw buttons with the browser default look. Use DS secondary buttons. The DS buttons are taller, so the title row centers the shape name. Add a visual test that turns on the shape-panel debug option and shows the title row of the selected shape. AI-assisted-by: claude-opus-5-5 * ♻️ Move debug shape panel styles off the deprecated styles Move debug_shape_info.scss to DS spacing tokens, borders and logical properties, resolve the panel background alias, and drop the refactor import. The two font sizes match no DS typography, so they stay with a TODO, and the unused shape-info-title class is commented out for a manual check. The panel looks the same as with the previous commit. AI-assisted-by: claude-opus-5-5 * 🐛 Let the debugging tools options scroll The debugging tools panel lists 32 options in the left sidebar, whose content area hides what overflows. On a short window the last options were cut off with no way to reach them. Make the options list fill the space under the title and scroll on its own. AI-assisted-by: claude-opus-5-5 * ♻️ Move debugging tools panel styles to DS tokens Move debug.scss spacing, size and the panel background alias to DS tokens and logical properties. The refactor import stays only for the checkbox placeholders, until the checkboxes move to the DS. The panel looks the same as before. AI-assisted-by: claude-opus-5-5 |
||
|
|
47d7ad2de1
|
♻️ Explain layout_container's deprecated import and format visual spec (#12449)
* ♻️ Explain why layout_container.scss keeps the deprecated import The TODO at the top said to remove the refactor/ import once the buttons were replaced, and sat above the DS imports instead of that one. The buttons are now DS buttons (#12428, #12431), but the import is still needed by the numeric inputs (%input-element, %disabled-input) and by the two raw track header buttons, which keep the secondary and tertiary placeholders. Replace the TODO with a comment on the import that says so. AI-assisted-by: claude-opus-5-5 * 📎 Format the workspace visual spec with Prettier Three expectations added in #12423, #12428 and #12431 broke a call chain in a way Prettier rewrites. check-fmt:js did not catch it: its playwright/**/*.js glob is expanded by the shell without globstar, so it does not reach visual-specs/. AI-assisted-by: claude-opus-5-5 |
||
|
|
9904e50889
|
♻️ Use DS icon buttons in the component options (#12394)
* ♻️ Use a DS icon button for the component pill menu Replace the raw three dots button of the component pill with the DS secondary icon button. It had no label, so it reuses the "Options" one, shown in the DS tooltip above and to the left (it sits at the right edge of the sidebar), and aria-pressed while the menu is open instead of the selected class. Its class drops the button placeholders and only keeps the pill segment shape: full height and square corners on the left. The tooltip wrapper fills the segment too. Add a visual test that selects a component copy so the pill shows. AI-assisted-by: claude-opus-5-5 * ♻️ Use DS icon buttons for the component annotation actions The save/create, discard, edit and delete actions of a component annotation were divs with a click handler: no keyboard focus and no button role. Replace them with DS ghost icon buttons. Their title moves to the label, shown in the DS tooltip above and to the left, and save is disabled while the text is blank, which it already ignored. The class that hides them until the title is hovered, or while editing or creating, now goes on the tooltip wrapper. The green and red hover colors and the fixed size go away with the DS look. While editing or creating, the actions get a small padding so their hover background does not touch the annotation border; the title row grows from 32 to 36px there. Add a visual test that creates an annotation on a main component and shows both sets of actions. AI-assisted-by: claude-opus-5-5 * 💄 Open component options tooltips above and to the left The variants help, create variant, add property and remove property icon buttons sit at the right edge of the sidebar: open their tooltips above and to the left, like the other actions of the component options. AI-assisted-by: claude-opus-5-5 * ♻️ Move component options styles off the deprecated styles Inline the dropdown placeholders of the component pill menu with DS tokens and a local variable for the item colors that change on hover, and give the item label its own class instead of styling the span. Resolve the annotation title border alias to its DS token and delete the unused .component-title-actions rule. The item label uses the DS body-small typography; its line-height is 1.3 instead of 1.4, with no visible change since the item centers it. component.scss no longer imports refactor/. AI-assisted-by: claude-opus-5-5 |
||
|
|
7e5e589744
|
♻️ Use a DS icon button for the guide defaults menu (#12391)
* ♻️ Use a DS icon button for the guide defaults menu Replace the raw three dots button that opens the use default / set as default menu of a guide with the DS ghost icon button. It had no label, so it gets a new "Guide defaults" one, shown in the DS tooltip, and aria-pressed while the menu is open instead of the selected class. The add, show/hide and remove guide buttons, and the new one, sit at the right edge of the sidebar: open their tooltips above and to the left. The advanced options toggle stays a raw button: it is one segment of the guide type control and an icon button would break that shape. Add a visual test that adds a guide to a board and opens its advanced options. AI-assisted-by: claude-opus-5-5 * ♻️ Remove guide defaults button styles The three dots guide defaults button is a DS icon button now: delete the .show-more-options rule, its state variables and its svg rule. The rest of frame_grid.scss already uses DS tokens. AI-assisted-by: claude-opus-5-5 |
||
|
|
fffc3ee222 | Merge remote-tracking branch 'origin/staging' into develop | ||
|
|
cdb7dc2977
|
♻️ Use a DS button for edit grid in the grid cell options (#12385)
* ♻️ Use a DS button for edit grid in the grid cell options Replace the raw "Edit grid" button of the grid cell options with the DS secondary button. Drop its alt attribute, which is not valid on a button; the label already names it. The column and row icons label the numeric inputs, so they move with the numeric inputs migration. Add a visual test that selects a cell in grid edit mode so the options show. AI-assisted-by: claude-opus-5-5 * ♻️ Move grid cell options styles to DS tokens The edit grid button is a DS button now, so its button placeholder and typography mixin go away; the class only stretches it and centers its label. Merge the two .grid-cell-menu-container rules, inline the flex mixins, and move spacing, borders and color aliases to DS tokens and logical properties. Use the DS body-small typography in the inputs; its line-height is 1.3 instead of 1.4. The refactor/ import stays only for %input-element and %button-icon, used by the inputs and their legacy icons, which move with the numeric inputs migration. AI-assisted-by: claude-opus-5-5 |
||
|
|
1c70b0ce2d
|
♻️ Use the DS icon in the workspace left header (#12432)
* ♻️ Use the DS icon in the workspace left header The shared library badge of the left header used a deprecated icon; use the DS library icon instead. Drop the deprecated icons inside the save status dot. They rendered as 500px SVGs with no stroke, so they never showed: the dot is only its background color, and its title keeps the status text. The Penpot logo is brand art and stays. Add a visual test that opens a shared file and shows the header with the badge, then the saved status after drawing a shape. AI-assisted-by: claude-opus-5-5 * ♻️ Move workspace left header styles off the deprecated styles Inline the flex and ellipsis mixins, move spacing, sizes and borders to DS tokens and logical properties, and resolve the color aliases to their DS tokens. The status dot takes its color from a local variable. The badge icon gets its color through a class instead of an svg selector. The project name uses the DS headline-small typography: 12px instead of 11px. The file name matches title-small exactly. Left with a TODO: the rename input font size and z-index have no DS match, and the pending status orange (#f5a91b) has no DS color. left_header.scss no longer imports refactor/. AI-assisted-by: claude-opus-5-5 * ♻️ Resolve the TODOs left in the workspace left header styles The status dot used the legacy warning orange #f5a91b, which drifted from the DS one when both replaced #fe4811 a few days apart in early 2025. Use the DS --color-accent-warning (#fe9c07), the orange of every other DS warning, so the dot looks a little more orange while saving. The rename input had z-index 20, which did nothing: the input is neither positioned nor a flex or grid item. Remove it. The rename input only set its font size and got the rest from the global reset; it now uses the DS title-small typography, like the file name. Its line-height goes from normal to 1.2, so it grows from 16 to 16.8px. AI-assisted-by: claude-opus-5-5 |
||
|
|
e4da0527fc
|
♻️ Use DS buttons and icons in the grid layout options (#12431)
* ♻️ Use DS buttons and icons in the grid layout options The "Edit grid" button of a grid layout and the "Exit" button of the grid editor were raw buttons with the deprecated secondary style. Replace them with DS secondary buttons; their text and actions stay the same. Drop the "alt" attribute of "Edit grid": it is not valid on a button. The expand and add buttons of the column and row headers are 52px parts of a custom header, not DS buttons, so they stay raw; only their deprecated icons move to the DS icon, as does the direction icon of each track. Add a visual test that shows the grid layout options and the grid editor with the column tracks expanded. AI-assisted-by: claude-opus-5-5 * ♻️ Remove deprecated button styles from the grid layout options "Edit grid" and "Exit" are now DS buttons, so their secondary placeholder and typography go away; the classes only place them in the grid and center their label. The track header and direction icons drop the deprecated icon placeholder and get their colors through classes. The header buttons keep their secondary and tertiary placeholders, since they stay raw. AI-assisted-by: claude-opus-5-5 |
||
|
|
3d6823eacd
|
♻️ Use DS icon buttons for the flex wrap and padding mode (#12428)
* ♻️ Use DS icon buttons for the flex wrap and padding mode The wrap button of a flex layout and the button that shows the four padding sides (flex and grid layouts) were raw buttons with deprecated icons. Replace them with DS ghost icon buttons that show their state through aria-pressed, with the tooltip above and to the left since they sit at the right edge of the sidebar. The wrap label is now always "Wrap": its title switched to "No wrap" while pressed, which conflicts with the pressed state. Add a visual test that selects a flex board and shows both buttons at rest and pressed. AI-assisted-by: claude-opus-5-5 * ♻️ Remove deprecated wrap and padding toggle styles The wrap and padding mode buttons are now DS icon buttons, so their tertiary button, icon and selected placeholders go away. The tooltip wrappers take the same grid cells the buttons had. AI-assisted-by: claude-opus-5-5 |
||
|
|
398678e54c
|
♻️ Use a DS icon button for the layout element margin mode (#12423)
* ♻️ Use a DS icon button for the layout element margin mode The button that switches a flex or grid element between simple and multiple margins was a raw button with a deprecated icon. Replace it with a DS ghost icon button that shows the multiple state through aria-pressed. Its title moves to the label, shown in the DS tooltip above and to the left since the button sits at the right edge of the sidebar. The grid column class goes on the tooltip wrapper. The legacy margin icons label the numeric inputs and stay for the numeric inputs group; the radio buttons are out of scope. Add a visual test that selects a flex element and shows both margin modes. AI-assisted-by: claude-opus-5-5 * ♻️ Remove deprecated button styles from layout element options The margin mode button is now a DS icon button, so its tertiary button, icon and selected placeholders and the deprecated 32px height go away. The class only places the tooltip wrapper in the third grid column. The refactor/ import stays for %input-element, still used by the legacy numeric inputs. AI-assisted-by: claude-opus-5-5 |
||
|
|
c345c7cbaa
|
♻️ Use a DS icon button to remove imported SVG attributes (#12382)
* ♻️ Use a DS icon button to remove imported SVG attributes Replace the raw remove button of each imported SVG attribute with the DS ghost icon button and the DS remove icon. The button had no title, so it gets a new "Remove attribute" label, which the DS tooltip shows. Drop the wrapper div that only held the button. Add a visual test that selects a group imported from SVG so the section shows. AI-assisted-by: claude-opus-5-5 * ♻️ Move imported SVG attributes styles to DS tokens The remove button is a DS icon button now, so .attr-actions and .attr-action-btn with their button placeholders go away. Inline the flex and ellipsis mixins, move spacing to DS tokens and logical properties, sizes to px2rem and the title color alias to its DS token. Use the DS body-small typography; its line-height is 1.3 instead of 1.4. Drop the line clamp rules of the attribute name: its later display: inline-block already overrode the -webkit-box they need. The refactor/ import stays only for %input-element on the value input, which moves with the inputs migration. AI-assisted-by: claude-opus-5-5 |
||
|
|
c99980327f
|
🐛 Resize fit-content flex boards while typing in v3 text editor (#12330)
* 🐛 Resize fit-content flex boards while typing in v3 text editor Typing in an auto-width text did not resize its fit-content layout parent until the editor closed. The v3 editor now previews the live text size in the layout on each keystroke, also when the text sits in a group. * 🐛 Preview text layout when typing with a pending caret style Both v3 editor commit paths share v3-update-editor-content. * 🐛 Skip the layout preview for absolute texts Absolute layout items do not affect their board. * ⚡ Preview text layout only when the text size changes Each preview propagates modifiers, so skip unchanged sizes. * 🐛 Leave interactive render mode when the v3 editor closes Finalize only ends it when it applies geometry. |
||
|
|
18ca25518e
|
♻️ Use a DS icon button to close the nudge amount modal (#12439)
* ♻️ Use a DS icon button to close the nudge amount modal The close button of the nudge amount modal (main menu, Preferences) was a raw button with a deprecated icon and no accessible name. Replace it with a DS ghost icon button labeled "Close", shown in its DS tooltip. The class that places it in the corner goes on the tooltip wrapper, so the tooltip anchors to the button. The numeric inputs stay for the inputs group. Add a visual test that opens the modal from the main menu. AI-assisted-by: claude-opus-5-5 * ♻️ Move nudge modal styles off the deprecated styles The close button is a DS icon button, so its placeholder goes away; it is now 32px wide instead of 28px, and sits 4px from the edge instead of 6px so the icon stays in the same place. Spacing, sizes and the color aliases move to DS tokens and logical properties, the flex mixin is inlined, and the label rule targets its class instead of the label element. The title matches the DS headline-medium typography exactly. The content and labels use body-large and the wrapper body-small, whose line-heights differ by 0.1 from the legacy ones; the labels keep their 1.5 override, so nothing visible changes. refactor/ stays only for the shared modal and labeled input placeholders. AI-assisted-by: claude-opus-5-5 |
||
|
|
f4f581ab74
|
🐛 Fix export scale dropdown clipped in Inspect (#12351)
* 🐛 Fix export scale dropdown clipped in Inspect The export selects in the Inspect tab used a dropdown class that always opened them upwards, overriding the select's own up/down choice. On elements with few properties the export section sits near the top of the panel, so the scale list (now eight options) went past the top of the scroll area and hid 0.5x. Keep only the dropdown width and let the select pick the direction. Closes #12346 AI-assisted-by: claude-opus-5-5 * ♻️ Move Inspect exports styles to DS tokens Replace the deprecated spacing and size variables, the flex mixins, the color aliases and the typography mixins with DS tokens, and use logical properties. The typographies switch to the closest DS ones: body-small for the suffix input (line-height 1.3 instead of 1.4) and headline-small for the export button (12px instead of 11px). The @extend placeholders still come from refactor/ and are left for a later migration. AI-assisted-by: claude-opus-5-5 * ♻️ Use DS buttons in Inspect exports Replace the add, remove and export buttons in the Inspect exports section with the DS icon-button and button, as the workspace export section does. Delete their now unused styles and the sizes import; the export button class only stretches the button and centers its label. AI-assisted-by: claude-opus-5-5 * 🐛 Fix add export button lookup in the Inspect exports e2e test The add export button in Inspect is a DS icon button now and lost its .add-export class, so the test timed out waiting for it. Find it by its role and accessible name instead. AI-assisted-by: claude-opus-5-5 |
||
|
|
dfc55a7192 | Merge remote-tracking branch 'origin/staging' into develop | ||
|
|
9ea4c3404f | Merge remote-tracking branch 'origin/main' into staging | ||
|
|
ab7a994d6c
|
🐛 Fix libraries modal visual test (#12367)
The libraries modal now opens on the "This file" tab, so the visual test timed out looking for the shared library. Open the "Libraries" tab first, as the functional test does, mock the file with the library linked, and check the "Added" label instead of the old "File library" text. AI-assisted-by: claude-opus-5-5 |
||
|
|
539983db2f
|
♻️ Use a DS icon button for flatten in the boolean options (#12368)
* ♻️ Use a DS icon button for flatten in the boolean options Replace the raw flatten button and its deprecated icon with icon-button* and the DS boolean-flatten icon. The title text moves to aria-label, so the DS tooltip shows it. The grid column goes on the tooltip wrapper through tooltip-class. Add a visual test that selects two shapes so the boolean options show. AI-assisted-by: claude-opus-5-5 * ♻️ Remove deprecated button styles from the boolean options The flatten button is a DS icon button now, so its placeholders, sizes and icon colors go away; only the grid column stays, on the tooltip wrapper. Use block-size and drop the refactor/ import. AI-assisted-by: claude-opus-5-5 |
||
|
|
d26a401637
|
♻️ Use DS buttons for more colors in the selected colors (#12369)
* ♻️ Use DS buttons for more colors in the selected colors Replace the three raw "more colors" buttons (library colors, colors and color tokens) with the DS secondary button. Add a visual test that selects a board with more than three colors so the button shows. AI-assisted-by: claude-opus-5-5 * ♻️ Move selected colors styles to DS tokens The "more colors" buttons are DS buttons now, so their placeholder and typography mixin go away; the class only centers the label, since the column stretches the button. Delete the unused .add-fill rule, move spacing to DS tokens and logical properties, and drop the refactor/ import. AI-assisted-by: claude-opus-5-5 |
||
|
|
47f0e15744
|
♻️ Use DS icon buttons in the align options (#12366)
* ♻️ Use DS icon buttons in the align options Replace the eight raw align and distribute buttons with icon-button* and DS icons. The title text moves to aria-label, so the DS tooltip shows the same text and shortcut. Disabled distribute buttons now use the DS disabled color. Add a visual test that selects two shapes so the align options show. AI-assisted-by: claude-opus-5-5 * ♻️ Remove deprecated button styles from the align options The align buttons are DS icon buttons now, so the .align-button rules and their placeholders go away. Move the row height to a DS size token and drop the refactor/ import. AI-assisted-by: claude-opus-5-5 |
||
|
|
d57cdb4588 |
⏪ Backport persistence related fixes from staging (#12306)
* ✨ Add async test helpers and the with-mocks* macro Backport of the test scaffolding that the persistence retry work needs, taken from staging. Purely additive: no existing helper changes behavior, so the suite result is unchanged (740 tests, 0 failures). - frontend-tests.helpers.async: ->promise, await-response, settle, observe and wait-for. wait-for checks immediately and then polls one macrotask at a time, bounded, so it fails instead of hanging. - mock/with-mocks*: installs mocks with set! and evaluates its body in a generated ^:async fn, so the test reads sequentially and there is no done to thread. Evaluates to a promise. - mock/asap and mock/run-mocked: run-mocked awaits the body and restores the originals always, in that order, reporting a rejection or a non-promise return as an :error. The matching change that made with-mocks defer its body via asap is NOT backported: it would force all 58 existing uses across 14 files into done-chained contexts, which is a separate refactor. AI-assisted-by: space-bunny-free * ✨ Add an error report governor with compact payloads One browser session could emit an unbounded number of identical error reports, and a dropped connection was reported as an application defect with the full payload. Both are fixed here, in one place, because the persistence retry work that follows depends on the new submit-report contract. - A report governor in app.main.errors. Each report carries a fingerprint (report-name|type|code|hint|first stack frame); the first occurrence is always emitted, repeats inside a 2 minute window are counted, and the next emitted report carries them as :occurrences. The cache is bounded by evicting the oldest entry, so memory stays fixed. - flash reserves the report before generating it, so a suppressed occurrence never pays the report-building cost. Its whole body now runs inside one ts/schedule callback, a reporting or notification failure is logged instead of propagating, and it returns a total promise so tests can await it. - generate-report accepts :format :compact|:full. The compact payload keeps the context header plus type, code and uri, and skips the stack, the ex-data dump (it can carry request headers) and the last-events list. - environment-error-types is derived from rp/retryable-types, so the transport classification has a single source of truth. Environment failures are audit-only handled-exceptions and never reach the exception page or the alerts. :offline gets its own handler instead of falling through to :default. :occurrences travels inside the audit event :props, which the backend schema accepts as a free-form map, so no backend change is needed. The dedup governor test file is new: 7 tests over the fingerprint, the pure reservation decision, and the flash pipeline. Relates to #11726, #11743 AI-assisted-by: space-bunny-free * ♻️ Answer commit refusals with an empty stream commit-changes used to return nil when it refused to build a commit, which forced every caller that wanted to observe the outcome to special-case nil. It now returns an empty stream, so a refusal and an empty result look the same and callers can subscribe uniformly. Only the refusal branch changes: the accepted branch already answered with (rx/of commit). Potok treats a nil watch result as "nothing", so both spellings were already correct at the store level. The 57 call sites that wrap this event in rx/of are unaffected. They hold the ::commit-changes event itself, not the value its watch returns, so they keep processing exactly as before. AI-assisted-by: space-bunny-free * ✨ Retry transient saves with backoff and a reconnect notice Any failed update-file landed on the same path: the queue halted in :error, the retained changes were flushed away, and the user was told to reload. A dropped connection and a revoked permission are not the same thing, so they now take different paths. transient-error? splits them, checking :type and :cause-type because persistence-failed wraps the original cause. Transient failures enter a :retrying episode: the head commit stays queued and resends with backoff at 2s, 8s and 20s, then falls through to today's terminal path. Everything else is terminal and keeps the exact :error path. - Resends rotate the ::request-id stamp only once the old request has left active-requests, carry the same :commit-id, and never double send while one request is in flight. - Retry timers carry an episode token; a timer whose episode was superseded stays silent instead of sending or failing. - One tagged reconnect notice per episode, hidden by tag on save and on terminal failure. Recovery is silent. - The browser online event re-enters the runner, but only for a live :retrying episode, guarded by exists? js/window. - wait-persisted-or-error waits through :retrying instead of cutting the wait short. The episode keeps its :run-id, so a new local edit only joins the queue. Re-entering the runner on every edit would bypass the backoff and spend one attempt per batched commit. The retry test namespace is new: 13 tests over the classification, the backoff schedule, stamp rotation, stale tokens and the notice lifecycle. Relates to #11724 AI-assisted-by: space-bunny-free * 🐛 Fix stalled saves that never answer and edits lost on leave Two holes the retry work did not close. An update-file request that never gets an answer kept its attempt stamp forever, so the queue could not move and the user saw a saving state that never resolved. The request now has a 7 minute timeout that fails it as a transient network error, which hands it to the retry path instead of blocking. The timeout is longer than the 5 minute stall threshold on purpose: the stalled report must fire before the retry, not instead of it. The leave-page warning also had gaps. It now tracks unsaved edits directly, through sync-unload-guard, which holds :persistence in st/ongoing-tasks while edits sit in the buffer or the queue. Edits kept in the queue after a failed save keep the guard on too, so closing the tab warns instead of dropping them. store.cljs guards its ongoing-tasks watcher with exists? js/window, because the new tests exercise it without a window. Four new tests: three for the guard across buffering, an in-flight save and a retained edit, and one for a hung request that must resend and save. Closes #12223 AI-assisted-by: space-bunny-free * 🐛 Skip the save when a missing font cannot be remapped fix-deleted-fonts emitted text and typography changes even when no replacement font existed, so opening a page that uses a deleted custom font rewrote identical content and cleared position-data on every visit. Every visit became a save with nothing to save. Both loops now compare the remapped value with the original and emit nothing when they match. The remapped path is unchanged. Two new tests cover the no-replacement case and the remapped case. Closes #12151 AI-assisted-by: space-bunny-free * ✨ Add a debug helper that simulates an intermediary response A save that never reaches the backend is hard to reproduce: you need a proxy, a captive portal or a CDN in the way. This wraps fetch and answers the named API method with a status and a body that carries no Penpot error code, leaving every other request alone. Without a duration the simulation is spent after answering one call. With one it answers every matching call for that long, which is how to watch the save retry, warn, keep retrying and finally recover. debug.simulateHttpError('update-file') debug.simulateHttpError('get-comment-threads', 403) debug.simulateHttpError('update-file', 524, 120) debug.clearHttpErrorSimulation() Dev-only and self-contained: no production path reads it. AI-assisted-by: space-bunny-free * 🌐 Add the save retry and connection error strings Three strings the new persistence and error paths need: - errors.save-retrying, shown once per retry episode while the queue resends a queued save after a connection loss. - workspace.header.retrying, the save indicator state for an episode in progress. - errors.connection-error, shown by the network and offline handlers, which no longer fall through to the generic message. Added to en.po only. The translations script also normalizes every other locale and drops 439 strings that were already stale before this work; that belongs in its own maintenance pass, not in a backport. AI-assisted-by: space-bunny-free * 🐛 Report save failures as unhandled when they are our defect flash-persistence reported every failure it did not delegate as handled-exception, including the ones that are plainly our fault. A validation failure means the client sent something the server refused, yet it landed in the audit log with a name that skips the internal reports and the alerts. Nobody was paged. The report type now follows the cause. :validation, :server-error and :internal report as unhandled-exception, so they alert. The rest are unchanged. Two exclusions are deliberate: - Environment failures stay handled. They reach this point after the retry budget is spent, and the user's connection is not our bug. - :assertion stays handled, because its own handler already reports it that way and the two paths should not disagree. The cause type is read from :cause-type first, since persistence-failed wraps the original under :type :persistence. Reading :type alone would match the wrapper every time and change nothing. The user still gets the same sticky toast and report link; only the audit event name moves. The test that asserted a validation failure is "still notified" now asserts which event name it gets, across the bare and wrapped shapes, the environment types that must stay handled, and an unclassified one. AI-assisted-by: space-bunny-free * 🐛 Unblock persistence when opening a file after a terminal failure A terminal save failure set the state to :error and left the queue alone. recover-persistence only resumed for :save-permission-denied, so for every other terminal code both of its branches were false and it emitted nothing. Leaving and reopening the same file therefore changed nothing: the editor stayed halted until a reload, which rebuilds the whole store and loses the queue anyway. The path had two jobs under one name. initialize-persistence fires when the workspace view mounts and tears down the previous subscription; recover-persistence fires when a file is opened or the role changes. They are now separate: initialization picks up a queue left behind by a previous session, and deliberately does not lift a halt, so undeliverable edits are released when the user acts rather than when the view mounts. Recovery lifts the halt by releasing what can never be saved. release-failed-file drops every queued commit of the file that failed, then resume-persistence clears the error and settles an empty queue to :saved. Retaining them until that point is deliberate: it is what keeps wait-persisted-or-error rejecting, which is what stops workspace/versions.cljs from building a snapshot out of unsaved edits. Releasing at failure time would make waiters resolve instead. Permission loss is treated as permanent too, which the previous exception got wrong. The backend raises :not-found for a missing edit permission, so a resend does not succeed — it routes the user to the exception page. The old branch made that reachable: it resumed the save whenever the cached can-edit flag said yes, and the server disagreed. The only case where the flag can return to true is a team switch, where the queue is gone anyway, so nothing real is lost by releasing. Because the queue is shared, dropping the failed file is also what lets the other files' pending edits drain. One file that cannot be saved no longer blocks everything else. Tests: the failure retains the queue and notifies once, recovery releases it without replaying, a failed file does not block a second one, permission loss is released rather than resumed, and initialization leaves a halt alone. AI-assisted-by: space-bunny-free * ✨ Offer a reload on the save failure toast A terminal save failure stops autosave for the whole session, but the toast asked the user to reload without offering the reload. It listed five implied actions, none of them reachable from the banner, and it never said that the changes could not be saved. flash now takes a :links vector of actions, appended after the download link that :report-link? produces, so a caller can add recovery actions without reaching into the report handling. flash-persistence passes a reload built from labels.reload-page and location.reload, the pattern already used elsewhere in the app for a full restart. The message now says what happened, what is lost, and what to do about it: autosave has stopped for this session, the changes since the last successful save cannot be saved, and either reloading or leaving the file and opening it again restarts it. Naming both matters, because the toast links are easy to miss. Saying the edits are lost is not optional: recovery releases them, so without it the user finds out when they come back to the file. Translated in en.po and es.po only. The translations script would also normalize all 40 locales and drop entries that were already stale, which is a maintenance pass of its own. The reload callback cannot be invoked under the test runner, since js/location does not exist there, so it is covered by its label and by being callable. Whether the reload works is a manual check. AI-assisted-by: space-bunny-free * 🐛 Classify the responses the backend never wrote A reply written by a proxy instead of the backend carries no Penpot error code, so handle-response raised it as :internal. A 504 on a save skipped the retry cycle and stopped autosave at once, and because :internal reports as unhandled, it raised an alert. In a standard deployment this is how a slow save ends: nginx answers 504 after 300s, before the client's 7 minute timeout fires. The status now decides the type: - 504 and the Cloudflare 520-524 family raise :gateway-error, which joins retryable-types. Saves retry with backoff like 502 and 503, GETs retry, and a save that spends its retries reports as handled, because the report rules derive from the same set. - 429 raises :rate-limit, and any other error status :unexpected-response. Neither retries; both show a toast instead of the error page. - :internal is kept for a response below 400 that does not decode. Bodies we cannot read are cut to 500 characters, since proxy error pages run to tens of kilobytes. Tests: classification and retries per status, the transport handlers keep the page, a 504 and a 524 built by the real handle-response go through the save path and land on a retry, and the new types stay handled once the retries are spent. AI-assisted-by: claude-opus-5-5 * ✨ Keep retrying failed saves on a slow cycle Once the three backoff retries were spent, a save that failed on the connection stopped autosave like any terminal failure, and opening a file again dropped its edits. A connection that comes back after a minute lost the work all the same. A transport failure that outlasts the backoff now arms a slow cycle: one silent resend every 30s, until a save lands or 24h have passed since the failure started. The cycle stops when persistence restarts, the queue resumes another way, or the workspace closes. A save that keeps failing slowly is not resent for a day. A gateway that answers 504 after its own timeout may leave the backend applying the save, so each resend can cost the whole save again, and the failing file holds back every other file's queued edits. An attempt that fails after 90s or more counts as a slow failure; at three, the slow cycle stops sending the commit and the save fails as before. Fast failures, such as being offline, never count. - The first failure shows a toast that says the changes are kept and keep being sent, with no reload link, since a reload loses them. A save that lands hides it. - Opening a file sends a queue still inside its retry window once more instead of releasing it. Failures outside the window, and every non-transport failure, are still released. - A freshly loaded file gets back the queued edits of its version, so reopening it shows the changes that are still waiting to be saved. An edit that no longer applies to the loaded data leaves the file as the server sent it, so the file still opens; the edits stay queued. - A commit whose earlier request is no longer in flight is simply sent again under the same commit id: changes are idempotent, so there is no unknown-outcome guard any more. A request still in flight still blocks a parallel send. flash takes a :tag so the save failure toast can be hidden by tag. Tests: the spent backoff arms the slow cycle, a 504 arms it, a terminal failure does not, the cycle stops when the window closes or at the slow failure limit, recovery resends instead of releasing, reopening restores only the loaded file's edits and survives one that no longer applies, and the retrying toast offers no reload. * 🐛 Turn off the leave-page warning when failed edits are released The leave-page warning only followed the queue on appends and discards. Opening a file again releases the edits of a file that failed for good, but that release changed the queue without either event, so the warning stayed on with nothing left to save. Leaving the page asked about unsaved changes until the next save went through. The warning now also follows the release. Tests: releasing the last failed edit turns the warning off. * 🐛 Stop retrying failed saves outside the workspace Leaving the workspace was meant to stop the slow retry cycle, but only a cycle already armed stopped. A backoff timer kept firing after the user left, and a backoff spent outside the workspace armed a slow cycle that kept sending for up to a day from the dashboard. Those sends check the edit permission of whatever team is open, so a switch to a team where the user only views failed the queued edits for good, and the next file they opened dropped them. Leaving the workspace now suspends retries. A pending backoff timer goes stale, and while suspended a transport failure schedules no backoff, a spent backoff arms no slow cycle and the online event sends nothing. The edits stay queued and a request in flight still lands. Opening a file lifts the suspension and resends a retrying save whose backoff was cut short; a role change does not lift it. Tests: leaving stops a pending backoff, a failure after leaving schedules no retry and ignores the online event, a backoff spent outside the workspace arms no slow cycle, and opening a file saves the held edit in each case. --------- Co-authored-by: alonso.torres <alonso.torres@kaleidos.net> |
||
|
|
0c0336da42 | Merge remote-tracking branch 'origin/staging' into develop | ||
|
|
d800b893bc
|
🐛 Fix resize cursor when selection has mixed values (#12229)
* 🐛 Fix resize cursor when selection has mixed values * 📎 PR changes * ✨ Add test |
||
|
|
473a59b134 | Merge remote-tracking branch 'origin/staging' into develop | ||
|
|
2ffdd16038
|
🐛 Fix typing after a triple-click in the text editor (#12218)
A triple-click can leave the browser selection outside the hidden input surface (it reads as "\n"). That selection is not editable, so typed keys fired keydown but no input, and never reached the WASM editor. Move the selection back into the surface before plain keys reach the browser, and after double and triple clicks. Closes #12187 AI-assisted-by: claude-opus-5-5 |
||
|
|
85ba670278
|
💄 Unify copy in the shortcuts settings, modals and sidebar (#12174)
* 💄 Unify copy in the shortcuts settings, modals and sidebar Use one term per concept across the customizable shortcuts UI: "Custom" and "Customize" for user changes, "shortcut" for the keys, and "Reset" with "default" to go back to the defaults. Rewrite the conflict message so it says what saving does, fix typos and grammar, unify the empty states and rename the font size actions. Give the reset all button, the search placeholder, the import column, the invalid import error and the edit button aria-label their own keys, and update the e2e tests to the new copy. Closes #12173 AI-assisted-by: claude-opus-5-5 * 💄 Reword the shortcuts sidebar link and custom empty state The sidebar link no longer names a settings section that does not exist in Penpot. The empty state of the custom tab now says there are no custom shortcuts and points to the All tab by name. AI-assisted-by: claude-opus-5-5 |
||
|
|
a5df69f60f | Merge remote-tracking branch 'origin/staging' into develop | ||
|
|
2f47be6c15
|
🐛 Show the size badge on paths and straight lines (#12066)
* 🐛 Show the size badge on paths and straight lines The badge was hidden when the shape was smaller than the badge in either side. Flat paths have a zero width or height, so they never showed it, and straight two-point paths hid it on purpose. Paths now only compare their longest side with the badge width. A straight line compares its length, and the badge runs along the line, rotated with it and kept upright. Near the bottom of the viewport the badge moves to the other side of the line. The badge now reads the size from the shape :selrect, like the measures inputs, so a rotated path shows the same values in both places. The three badge branches share a single render block. AI-assisted-by: claude-opus-5-5 * ♻️ Extract the size badge layout and add tests for it Move the badge geometry out of selection-size-badge* into the pure size-badge-layout function, so it can be tested without rendering. The component now only picks the color and draws the result. Add unit tests for rects, flat paths and straight lines: when the badge hides, where it sits, how it rotates and how it moves when it reaches the bottom of the viewport. Keep the line rotation between -90 and 90 degrees instead of returning 360 for lines drawn leftward. The e2e test still expected straight lines to hide the badge. It now checks that the badge shows and follows the line angle. AI-assisted-by: claude-opus-5-5 * ✨ Show the length of straight lines in the size badge A straight two-point path is measured by its length, not by the box around it, so its size badge now shows the length with two decimals at most instead of width x height. Other shapes and paths keep width x height. The shorter text makes the badge narrower, so it also shows on shorter lines; the rule is the same, the badge needs the line to be at least as long as the badge. Relates to #12064 AI-assisted-by: claude-opus-5-5 * 🐛 Hide the path size badge when the path is lower than it Paths other than a straight line only hid the size badge when their longest side was narrower than it, so a wide but low path, or a narrow but tall one, still showed it. Flat paths no longer need that special case, since straight lines get their own length badge. Use the same rule as any other shape: hide the badge when either the width or the height of the path is smaller than the badge. AI-assisted-by: claude-opus-5-5 |
||
|
|
746184caf9 | Merge remote-tracking branch 'origin/staging' into develop | ||
|
|
dacfac17bb
|
🐛 Show the top stroke width in the old render (#12093)
* 🐛 Show the top stroke width in the old render With the old render, the stroke width input showed "Mixed" when the four sides had different widths or tokens. The old render only draws the top side, and the input must show it as the global stroke width. Move the input value and token logic into two pure functions. When per side strokes are disabled, both return the top side: its width and its token, or no token when the top has none. Closes #12063 AI-assisted-by: claude-opus-5-5 * 🐛 Add e2e tests for the stroke width input in both renders Cover the global stroke width input when the four sides differ. With the new render it shows Mixed. With the old render it shows the top side: its width, or its token when the top has one. The old render cannot create per side data from the UI, and switching the render reloads the page and drops the test state. The tests patch the existing stroke token fixture instead, appending the per side fields to a rectangle and a board. Relates to #12063 AI-assisted-by: claude-opus-5-5 |
||
|
|
b06a43404b
|
🎉 Add detach to typography and shadow tokens (#11969)
* 🎉 Add detach to typography tokens * 🎉 Add detach to shadow tokens * 🎉 Add tests |
||
|
|
0389435ced
|
🐛 Block typography creation from missing fonts or multiple texts (#11938)
Creating a typography from a text whose font is no longer installed baked the broken font-id into a new asset. With several texts selected there is no single style to capture either. The add-typography event now does nothing in both cases, and the "Add typography" button in the local library is disabled with a label that explains why. The button lives in its own component so selection, shape and editor changes re-render only the button, not the typography list, and shared libraries do not subscribe to that state at all. The event and the button read the editor data through the new `editor-text-options`, so both see the same font for the wasm, v2 and v1 text editors. AI-assisted-by: claude-opus-5-5 |
||
|
|
f311c7ab05
|
🐛 Fix token propagation on canvas color (#11950)
* 🐛 Update canvas background when its color token changes Token propagation only walked the shapes of each page, so a canvas background linked to a color token kept its old value after switching the active set or editing the token. Propagation now also updates the background of every page whose `:background-token` resolves to a new color, inside the same undo transaction. AI-assisted-by: claude-opus-5-5 * 🐛 Select the dragged token set by id instead of by path Starting a drag on an unselected token set stored its path as `:selected-token-set-id`. The sidebar then crashed on the `(uuid? force-set-id)` assert of `get-tokens-in-active-sets-force`. This could happen when toggling a set checkbox with a slight mouse move. AI-assisted-by: claude-opus-5-5 * 🎉 Add playwright test |
||
|
|
8f1100d0f5
|
✨ Enable stroke-per-side flag (#11942)
* ✨ Enable stroke-per-side flag * 🐛 Fix stroke per side tests for the enabled flag The default flags now include :enable-stroke-per-side, so frontend tests and Playwright specs that assumed the flag was off need to turn it off explicitly. Update the token context menu test expectations: rects and boards now expose the stroke-width submenu, and force the flag off in the "per-side is disabled" cases. Pass disable-stroke-per-side in the two Playwright specs that check the flag-off behavior, since app.config always merges the default flags. AI-assisted-by: deepseek-v4.1-flash |
||
|
|
18c9108d47
|
✨ Enable WASM text editor with render-wasm (#11936)
When render-wasm/v1 is active, also enable text-editor-wasm/v1 so the WASM text editor turns on with the renderer. Keep forcing text-editor/v2 as before; the viewport still prefers the WASM editor when both features are set. Classic unchanged. Closes #11934 Relates to #11935 |
||
|
|
20c818661d
|
♻️ Add DS tooltip to token pills" (#11900)
* 🐛 Fix can-edit permission wiring for token pills * ✨ Use DS tooltip component in token pill * 🎉 Add playwright test * 🐛 Fix tests |
||
|
|
7e3f779d8c
|
🎉 Add tokens to the canvas (#11923)
* ✨ Show color tokens toggle on canvas background picker * 🐛 Apply token click on canvas background with no shape selected * ✨ Apply and persist color tokens on canvas background * 🌐 Translate canvas background section label |
||
|
|
98daf1592d
|
✨ Apply tokens to stroke per side (#10913)
* ♻️ Derive per-side stroke widths from the side values The per-stroke `:stroke-per-side` boolean only gated whether the renderer looked at the four side widths, and the CSS generator used it to decide whether to emit one `border-width` or four. Comparing the sides is enough, so drop the attribute from the shape schema and from the stroke attribute list. The WASM property and upload bridges and `stroke-per-side-widths` now derive the per-side widths from the values alone. AI-assisted-by: deepseek-v4.1-flash * ✨ Declare per-side stroke width token attributes Replace the single `:stroke-width` token attribute with `:stroke-width-top`, `:stroke-width-right`, `:stroke-width-bottom` and `:stroke-width-left`, add `per-side-stroke-width-keys`, and map the new attributes to the strokes shape attribute and to the dimensions token type. AI-assisted-by: deepseek-v4.1-flash * 🐛 Unapply only the token of the changed stroke side A stroke change that reports a single per-side sub-attribute now resolves to that side's token only. A plain `:stroke-width` change still resolves to every side, and a change with no sub-attribute resolves to all width keys plus the color. Add a regression test that tokens on untouched sides survive a change to another side. AI-assisted-by: deepseek-v4.1-flash * ✨ Add a predicate for per-side stroke shapes Boards and rectangles support independent stroke widths per side. Expose `per-side-stroke-shape?` so callers can gate the per-side UI, and cover the supported and unsupported shape types with a test. AI-assisted-by: deepseek-v4.1-flash * ✨ Add stroke side width materialization helper `materialize-stroke-side-widths` concretizes the four per-side width keys from a stroke: edited sides take the new value, the others keep their current width (0 when there is no stroke), and `:stroke-width` mirrors the top side for legacy consumers. This pulls the logic already duplicated in the token apply path into a shared helper, ready for the direct-edit path. AI-assisted-by: deepseek-v4.1-flash * ✨ Apply a stroke width token to every side `update-stroke-width` now writes the four per-side width keys, both when the shape already has a stroke and when it gets a new default one, so the applied-token bookkeeping matches the stroke attributes. AI-assisted-by: deepseek-v4.1-flash * ✨ Apply a stroke width token to a single side Add `update-stroke-width-side`, which changes only the sides named in `attributes` on the first stroke of each shape. The remaining sides keep their current width (0 when the shape had no stroke) and all side keys are materialized through the shared helper, so consumers never fall back to `:stroke-width`. Route the per-side token keys to the new function and update the apply, remap and component tests. AI-assisted-by: deepseek-v4.1-flash * ✨ Complete a partially applied per-side token on toggle When explicit attributes come from an input or a plugin call, toggle the token off only if it already covers every target attribute on every selected shape. A partial per-side application is completed instead of removed. The token pill keeps the previous any-attribute behavior. Add tests for both the completion and the full removal. AI-assisted-by: deepseek-v4.1-flash * ✨ Offer per-side stroke width actions in the token menu Boards and rectangles get a stroke width submenu with an all-sides action and one action per side; other shapes keep the single global action. The global action targets every per-side attribute so the design tab keeps showing the token on each side. Add the pill labels for the new attributes, the menu test, and the `workspace.tokens.stroke-width` string. AI-assisted-by: deepseek-v4.1-flash * ✨ Persist the per-side stroke preference Add `:stroke-per-side` to the user profile props schema and expose it through a derived `stroke-per-side` ref. The design tab will read the preference from here instead of a per-stroke attribute. AI-assisted-by: deepseek-v4.1-flash * ✨ Add per-side stroke width helpers to the stroke menu Add `stroke-width-all-attrs` and `per-side-stroke-available?`, which checks the feature flag and that a single board or rectangle, or a uniform multi-selection of them, is selected. Use it instead of the inline shape-type check and drop the old per-side toggle handler. Cover both helpers with a test. AI-assisted-by: deepseek-v4.1-flash * ✨ Add per-side stroke width token inputs to the design tab Turn the four side width fields into token inputs with detach actions and a `:multiple` mixed value when the sides differ. The per-side toggle now reads the persisted profile preference instead of a per-stroke attribute, so previous per-side edits survive. AI-assisted-by: deepseek-v4.1-flash * 🐛 Materialize stroke sides on direct width edit The stroke menu per-side handler only wrote the edited side key and, for the top side, the global `:stroke-width`. A stroke holding just `:stroke-width` made every consumer fall back to the global value, so editing one side changed all the others. Add a `change-stroke-side-width` event that materializes the four side keys through the shared helper and reports only the edited key as changed, so tokens on untouched sides are not unapplied. Route the menu handler through it. AI-assisted-by: deepseek-v4.1-flash * 🐛 Make stroke width fields non nullable Drop `:nillable` from the global and per-side stroke width inputs and use `:multiple` for the mixed state, so an empty field no longer represents a null width. AI-assisted-by: deepseek-v4.1-flash * 🐛 Fix the numeric-input props schema key The schema declared `:applied-token`, but the component body and every caller use `:applied-token-name`, so the prop was never validated. Rename the schema entry to match. AI-assisted-by: deepseek-v4.1-flash * ✨ Add token-disabled support to the numeric input The design-system numeric input accepts `:token-disabled` and `:token-tooltip`; the token button is disabled and shows the reason. Scope the disabled input style to `input:disabled` so a disabled token button no longer dims the whole field. Pass both props through the token wrapper. AI-assisted-by: deepseek-v4.1-flash * 🐛 Disable token controls below the first fill or stroke Design tokens only apply to the first fill or stroke of a shape. Add `tokens-allowed-position?` and mark the fill and stroke lists with `tokens-first-only`, so later entries disable their token controls and explain why. The colorpicker opens on the direct color tab and disables the token tab for those rows. Cover the helper with a test and add the new translation. AI-assisted-by: deepseek-v4.1-flash * ♻️ Refactor colorpicker style switcher to DS radio buttons Replace the legacy `components/radio-buttons` markup in the colorpicker with the design system `radio-buttons*`, using its declarative options API. Switching between direct color and token mode now passes string values, as the DS component expects. The previous keyword values broke the round trip back to color mode: the DOM stringifies keywords with a leading colon, so the value never matched `:direct-color`. Using plain strings keeps the conversion clean. AI-assisted-by: deepseek-v4.1-flash * ✨ Add playwright tests * ✨ Scope per-side stroke controls to each stroke Give every stroke row its own expanded state instead of sharing one profile-wide preference. The state lives in `:workspace-local`, keyed by `[ids index]`, so it survives selecting another shape and coming back but resets on reload. Remove the `:stroke-per-side` profile prop and its ref. The ref now derives from `:workspace-local`. Update the Playwright spec to expand the controls per stroke through the toggle, and assert that strokes toggle independently, that the state resets on reload, and that it survives switching shapes. AI-assisted-by: deepseek-v4.1-flash * 🐛 Keep stroke tokens when editing or removing later strokes The token unapply logic decided which tokens to clear from the shape using only the changed sub-attributes, without knowing which stroke was edited. Since stroke tokens only live on the first stroke, editing or removing a later stroke cleared the first stroke's tokens. Add a `:changed-item-index` option to `generate-update-shapes` and skip unapplying fill/stroke tokens when the changed item is not the first. The stroke color, attrs, side-width and remove events now report the index they touch. AI-assisted-by: deepseek-v4.1-flash * 🐛 Ignore token shortcuts when tokens are disabled for input The numeric input opened the token dropdown on `{` regardless of `token-disabled?`, so inputs that cannot hold tokens (for example, strokes after the first one) still opened it, and typing `{token}` plus `}` could apply a token there. Extract the key handling into `token-shortcut`, which returns nil when tokens are disabled, and use it for both `{` and `}`. AI-assisted-by: deepseek-v4.1-flash * 🐛 Gate per-side stroke tokens on the WASM renderer The token context menu offered per-side stroke width actions whenever the feature flag was on and the shape was a board or rectangle, without checking the renderer. The classic renderer only draws the single `:stroke-width`, so applying a per-side token there wrote inert data, the token pill reported it, and the stroke changed appearance when the WASM renderer was later enabled. Add `per-side-stroke-enabled?` (flag + WASM renderer) and use it from both the design tab and the token context menu. Thread the renderer flag into the context menu through `:render-wasm`. AI-assisted-by: deepseek-v4.1-flash * 🐛 Keep first-stroke tokens when reordering later strokes Fill and stroke tokens only ever live on the first item of the collection. When a stroke update arrives without a changed item index (for example reordering the second and third strokes), the unapply logic assumed the first item had been edited and removed every stroke token from the shape. Compare the first item before and after the update instead: when no item index is given, unapply only if the first item actually changed. Reordering later strokes now leaves the first stroke and its tokens untouched, while moving the first stroke away still detaches them. Explicit item edits keep their previous behavior. AI-assisted-by: deepseek-v4-flash |
||
|
|
ec5a1edbed
|
✨ Make export follow the active renderer only (#11910)
Drop the separate :wasm-export flag and wasm-export/v1 feature. Single export, clipboard PNG, plugins, and batch :is-wasm now key off render-wasm/v1 alone. The exporter trusts :is-wasm for headless WASM and always keeps a worker pool ready. |
||
|
|
eb7019fce4
|
✨ Preserve sidebar scroll positions across tab switches (#11694)
* 🐛 Preserve layers panel scroll position across tab switches Fixes #7440. Switching between the Layers, Assets and Tokens tabs in the workspace left sidebar unmounts the active panel component, causing its scroll position to reset to the top on re-entry. Add a module-level `scroll-positions` atom keyed by page-id. The layers scroll handler now also saves the current scrollTop value into the atom; a `mf/with-effect` on the page-id dep restores it whenever the `layers-toolbox*` component mounts or the page changes. Co-Authored-By: Paperclip <noreply@paperclip.ing> * 🐛 Preserve sidebar scroll positions across tab switches Replace the Layers-only global atom with a scroll store held in a use-var in left-sidebar*, shared by the Layers, Assets and Tokens panels through a new sidebar.scroll helper. Positions are keyed per panel and page (or token set) and restore waits for list content to settle, so deep positions in lazily rendered lists survive tab switches. Closes #7440. AI-assisted-by: muse-spark-1.3-contributor * 🐛 Add e2e coverage for sidebar scroll preservation Port the regression tests from closed PR #7544 for issue #7440, adapted to the current ref-based implementation and fixtures: async restore needs polled assertions, and setup uses the shared tokens helpers. Also add data-scroll-container hooks to the Assets and Tokens scroll containers so the specs can locate them. AI-assisted-by: muse-spark-1.3-contributor * 📎 Fix rebase issue --------- Co-authored-by: Sumit Ridhal <sridhal@redhat.com> Co-authored-by: Paperclip <noreply@paperclip.ing> |
||
|
|
3cd9bfa9de
|
✨ Add sync with design tokens in external libraries (#10293)
* ✨ Auto link tokens when adding external libraries (provisional) * 🔧 Refactor tokens-lib initialization * 🔧 Add separated TokenStatus to store status apart of TokensLib * 🔧 Make all status operations use the new data structure * 🔧 Normalize status helper functions and access token sets by id * 🔧 Rename :tokens-file to :tokens-source * 🎉 Allow the user to choose the tokens-source of a file * 🎉 Make tokens library readonly when it's in an external file * 🎉 Show tokens in library summaries * 🎉 Show source info in sidebar * 🔧 Fix integration tests * 🐛 Propagate changes of token values in external library * 🎉 Layout updates * 🔧 Refactor tokens source calculations * 🔧 Add harder checks for nil or empty values in everything * 🐛 Fix some integration tests * 🔧 Add integration tests for tokens in external libs * 🔧 Validate and repair missing tokens status * 🎉 Make ui changes optional with config flag * 🐛 Propagate tokens after synchronizing components in ext library * 🐛 Propagate tokens after creating new instances * 🐛 Propagate tokens after synchronizing tokens in ext library * 🐛 Add a tokens source icon to libraries section (#11439) * 🐛 Add a tokens source icon to libraries section * 🐛 Fix ellipsis on library names * ♻️ Remove code under flag on legacy component * 🐛 Fix token theme name on inspect tab * 🎉 Add changes notification (#11476) * 🎉 Add changes notification * ♻️ Change fn names * 🐛 Fix tokens source label truncation and missing translations (#11533) * 🐛 Fix tokens source label truncation and missing translations The tokens source file name always showed, even for the current file, and long names wrapped onto a second line instead of truncating because the header used flex-wrap and overflow-wrap: break-word instead of single-line ellipsis. Show the source row unconditionally (it now displays "This file" when the source is the current file, matching the connected-library case), truncate the file name to one line with an ellipsis, and only attach a tooltip with the full name when the text is actually truncated. Replace the hardcoded UI strings with translated ones and add their English and Spanish entries. AI-assisted-by: claude-sonnet-5 * 🐛 Remove redundant effect dependency in tokens source file-name-truncated? was listed as a dependency of the with-effect that checks and observes label truncation, even though it isn't read inside the effect body. Since the effect itself flips that state via check-file-name-truncated, including it as a dependency caused the ResizeObserver to be needlessly disconnected and reconnected on every truncation change. AI-assisted-by: claude-sonnet-5 * 🐛 Fix small visual error * 🐛 Fix problem with plugins * 🐛 Fix playwright tests --------- Co-authored-by: Eva Marco <evamarcod@gmail.com> Co-authored-by: Eva Marco <eva.marco@kaleidos.net> Co-authored-by: alonso.torres <alonso.torres@kaleidos.net> |
||
|
|
b32a73519e
|
🎉 Add combined organization/team switcher to the dashboard (#11760)
* 🎉 Add combined organization/team switcher to the dashboard * 📎 Code review |
||
|
|
dc8160c13a
|
🎉 Add Menu DS component (#11511)
* 🎉 Add Menu design-system component Adds Menu, MenuItem, MenuSeparator, SubMenu, and ContextMenu to the shared UI package and exposes them through the CLJS design-system wrapper, with Storybook stories and MDX docs. Built on react-aria-components for keyboard navigation, focus management, and dismissal. Penpot's own DS buttons aren't react-aria-aware, so trigger positioning, focus-on-open, and close-on-select are wired explicitly instead of relying on the library's default trigger detection. Includes a temporary manual-test harness in the dashboard to check the components against the real app. CSS is functional but doesn't match the DS visual design yet — that comes in a follow-up. AI-assisted-by: claude-sonnet-5 * ✨ Add left/right corner placements to Menu design-system component Menu and ContextMenu only exposed 8 of react-aria's placement values, missing every left/right corner variant (right bottom, right top, left bottom, left top) that the top/bottom sides already had via start/end. Add the four missing corners, matching the start/end pattern already used for top/bottom, so a menu can open toward any corner of its trigger. AI-assisted-by: claude-sonnet-5 * ✨ Add drilldown variant to SubMenu design-system component SubMenu only opened as a flyout: a nested popover next to the trigger item. That doesn't scale to a tree too deep or wide for a chain of flyouts, e.g. move-to-project's team -> project nesting, which needs a mobile-style drilldown (replace the current items with the submenu's own, plus a way back) instead. Add a `variant` prop, `"flyout"` (default, unchanged) or `"drilldown"`. Menu and ContextMenu each keep a navigation stack, provided to their content tree via context, so a drilldown SubMenu nested inside another drilldown SubMenu still drills into the same stack and arbitrarily deep trees stay navigable one screen at a time. Switching levels remounts the level's content wrapped in a keyed Fragment rather than updating it in place, since react-stately's Collection requires each item's id to stay stable across an update and the back item's label (and everything under it) genuinely changes identity between levels. AI-assisted-by: claude-sonnet-5 * ♻️ Wire the DS Menu/SubMenu into the dashboard file menu file_menu.cljs used context-menu-a11y's data-driven options list, rendered via a generic recursive renderer. Rewritten as real JSX composition (menu-item*/sub-menu*/menu-separator*) using the DS Menu component, preserving every existing conditional branch (single-file, multi-select, restore-mode, permission gates). "Move to" -> "Move to other team" -> team -> project now uses sub-menu*'s drilldown variant at every level. Split into file-menu-items* (the item tree, no popover of its own) and a thin file-menu* wrapper (Menu, anchored to the "..." button), so grid.cljs can render the same items a second time inside a ContextMenu for right-click, matching the previous behavior of opening either via the button or a right-click anywhere on the row. grid.cljs's trigger handling is simplified accordingly: DS's Menu/ ContextMenu handle their own positioning (including auto-flip near viewport edges) and dismissal internally, so the manual click- coordinate math, the dashboard-local :menu-open/:menu-pos globals, and the portal-on-document* wrapper (Popover already portals itself) are all gone. The now-fully-dead show-file-menu-with-position/ show-file-menu/hide-file-menu actions are removed from data/dashboard.cljs. Also fixes two issues found wiring this up: - The add-shared/unpublish-shared toggle rendered two different menu-item* ids at the same list position; :is-shared can flip while the popover stays open (the action's own side effect), and react-stately's Collection requires an item's id to stay stable across such an update. Both branches now share one id. - Menu's own trigger wrapper (align-self: start, needed generically so it doesn't stretch in an arbitrary parent) overrode .project-thumbnail-actions's centering of the "..." button; grid.scss now re-asserts centering for that specific consumer. Removes the temporary menu-test* harness from dashboard.cljs now that there's a real integration to test against instead. AI-assisted-by: claude-sonnet-5 * 🐛 Fix Menu/ContextMenu popover interaction bugs Found testing the dashboard file menu integration: - Reopening the same trigger right after closing (e.g. right-click, dismiss, right-click again) could silently fail or briefly show two overlapping instances. Closing played a 100ms exit fade, and a reopen landing mid-fade raced the still-live Popover instance. Closing now always skips the exit animation, so by the time any subsequent open request arrives there's no ambiguous in-between DOM state left to race. - Right-clicking a different row while one file's context menu was open didn't close the first one. Menu/ContextMenu don't use react-aria-components' own MenuTrigger (Penpot's DS buttons aren't react-aria-pressable), so they also don't get its built-in RootMenuTriggerStateContext coordination between sibling instances. A window CustomEvent broadcast restores it: opening announces this instance's id, and every other mounted instance closes on hearing a different one. - With that coordination in place, right-clicking elsewhere still did nothing at all: Popover defaults to modal, which marks the rest of the app inert (unfocusable *and* unclickable, not just visually blocked) while open. Correct for a real Dialog, wrong for a lightweight dismissable menu. Fixed with isNonModal on all three Popover usages (Menu, ContextMenu, SubMenu's flyout). - isNonModal has its own side effect: react-aria only wires up its click-outside-closes behavior when a popover is "dismissable", which isNonModal forces off (for anything but a submenu flyout) with no separate prop to turn back on. Reimplemented directly: a pointerdown landing outside the popover's own rendered content closes it, via a ref now passed to Popover. AI-assisted-by: claude-sonnet-5 * 🐛 Fix Menu visual styling and two overflow bugs Border and shadow, to match the legacy context-menu-a11y menu this replaces: the DS component had neither (a filter: drop-shadow with a different blur radius stood in for the shadow, and there was no border at all). Used the pattern already established by sibling DS dropdowns (options-dropdown.scss et al.) rather than porting the legacy tokens directly — border: 1px solid var(--color-background-quaternary) + box-shadow: 0 0 12px 0 var(--color-shadow-dark), both already in use elsewhere in this same file. Found two real bugs verifying that against a long "move to" list: - .menuItem/.separator had no flex-shrink: 0, so once a list's natural height exceeded the menu's max-block-size, flexbox shrank every row to fit them all rather than triggering the scrollbar — overflow only kicks in after flex-shrink has done its best, and shrinking was never opted out of. - The menu's own fixed max-block-size: 300px ignored react-aria's Popover, which sets its own max-height (inline, on our direct parent) to whatever space is actually available between the trigger and the viewport edge. In a small viewport that computed value can be under 300px; since the parent has no overflow of its own, our independent 300px cap just rendered straight past it and off the edge of the window. max-block-size: inherit picks up the parent's own computed value instead, at the cost of no longer capping how tall the menu can get when there's plenty of room (verified: 348px in a normal-height viewport, vs the old fixed 300px) — an acceptable tradeoff against content becoming inaccessible. AI-assisted-by: claude-sonnet-5 * 💄 Adjust Menu design-system component item states and spacing Give menu items a distinct keyboard-focus ring (accent-primary outline plus tertiary background) separate from the mouse hover/click state, which keeps its existing quaternary background unchanged. Restyle disabled items with a tertiary background and secondary text color, shrink the submenu chevron to 12x12, and tighten the menu's vertical padding to 4px. * 📚 Document drilldown submenu and tighten Menu docs Add the drilldown submenu story to the Menu docs page, show the idiomatic controlled-state shape in the usage example (callbacks bound in the let with mf/use-fn, explicit deref of the open state), and trim the prose down to the information a consumer needs. * 🐛 Fix Menu outside-click closing on its own trigger and submenus useCloseOnOutsideClick restores the dismiss behavior isNonModal turns off, but it tested containment against the popover element alone. That missed two cases react-aria's own useOverlay accounts for. A root Popover wraps its content in a display:contents div and portals every SubmenuTrigger's nested popover into that same div, so a flyout submenu is a sibling of the popover, not a descendant. Pressing an item in one counted as an outside click: the whole tree unmounted on pointerdown and the item's action never fired on pointerup. Test the group container instead. The trigger was likewise treated as outside, so closing on its pointerdown let the click's own handler read the already-false open state and reopen the menu — a trigger wired to a toggle could never close it. Exclude it in Menu; ContextMenu keeps the old behavior, since right-clicking elsewhere should reopen it against a new anchor. * 🐛 Target the clicked file when it is not in the dashboard selection The file menu adopted the whole selection whenever it was non-empty, guarding only against it being empty. That left the case where the selection holds files this row is not one of: toggle-file-select is a no-op across projects, so shift-right-clicking a file in another project leaves the previous project's selection intact and the menu opened on the pointed-at file while offering rename, duplicate, move and delete for a different one. Adopt the selection only when it actually contains this file, which covers the deferred-dispatch case the previous guard was written for just as well. * 🐛 Drop the file menu teams cache that outlived a logout The cache was a module-global defonce atom, and logging out does not reload the page — it resets the store and navigates. The next profile to sign in on the same tab therefore opened its first file menu with the previous account's team and project names listed under "Move to", until the background fetch replaced them. The cache only ever saved the brief absence of one submenu, which is already guarded on having data and so does not shift any layout, so remove it rather than scope it to a profile. Dispose the subscription too: it wrote to component state after unmount. * 🐛 Keep the Menu open when its own trigger takes focus Excluding the trigger from the outside-click dismiss was not enough to make a toggle trigger able to close the menu: usePopover passes shouldCloseOnBlur unconditionally, and useOverlay acts on it regardless of isNonModal, so focus moving to the trigger on its own pointerdown closed the popover before the click ran. The click then read an open state that was already false and reopened it. shouldCloseOnInteractOutside is the one exception useOverlay consults before closing on blur, so use it to exempt the trigger. * 🔧 Add interaction tests for the Menu component Cover the two dismissal regressions just fixed — closing the menu from its own trigger, and a press inside a flyout submenu not being treated as an outside click — plus drilldown navigation, the navigation stack resetting between open/close cycles, and Escape and outside click. Both regression tests fail against the code as it was before the fixes. The story trigger now toggles instead of only ever opening, which is what a real caller does (the dashboard's own is a swap!) and what makes the reopen bug observable at all. * ✨ Add max-width, density, and drilldown sizing to Menu/ContextMenu Add a max-width prop (default 250px) to Menu, ContextMenu, and flyout SubMenu, and an is-dense prop to Menu/ContextMenu that shrinks every item — including nested flyout SubMenus, via a shared density context — to a 28px row. Pin a drilldown SubMenu's popover to at least the root level's own size, so navigating into a shorter or narrower list doesn't shrink the menu mid-navigation. Also truncate a plain MenuItem's text with an ellipsis instead of letting it wrap and blow out the row height, matching the existing SubMenu trigger label, and fix that label's own truncation: it was missing min-inline-size: 0, without which a flex item can't shrink below its content size and text-overflow: ellipsis never engages. Exposed through the ClojureScript facade as :max-width/:is-dense, documented with new example canvases, and covered by five new Storybook interaction tests, each verified to fail without its corresponding fix. * ♻️ Wire the DS Menu/ContextMenu into the dashboard project menu Replace the legacy context-menu-a11y-based project menu (grid, sidebar, and per-project file view) with the DS Menu/ContextMenu components, mirroring the earlier file menu migration. Drop the manual :menu-open/:menu-pos position tracking in favor of the DS components' own positioning, and split project-menu-items* out so both the "..." trigger and right-click share the same options. The hidden file input behind the "Import" option moves out of the popover content and into whichever parent stays mounted regardless of the menu's own open state: the DS popover really unmounts its content on close (unlike context-menu-a11y, which only hid it), and selecting "Import" closes the menu in the same tick a ref owned inside it would already be gone. Add an onOpenChange notification to ContextMenu (it stays uncontrolled, this only reports state changes) so the project row's "..."/pin/add-file actions can stay visible for as long as either menu is open, the same way they already do on hover. Fix a related visibility bug this exposed: closing a menu restores focus to its trigger regardless of whether the open happened via mouse or keyboard, so :focus-within alone kept the actions visible after closing with the pointer away — swapped for :has(:focus-visible), which only matches real keyboard navigation. * 🐛 Forward MenuItem's id to the DOM as data-testid MenuItem's function signature never forwarded anything beyond its explicitly-typed props to the underlying RACMenuItem, so a caller's id — meant as a stable per-item identifier — only ever reached the DOM as react-aria's own internal data-key, never as data-testid. This silently broke dashboard.spec.js's "Multiple elements in context" test after the file menu's migration to this component, since every existing menu item id was already relied on as its test id. id is already unique per item for selection/on-action, so deriving data-testid from it directly means every item is reachable in a test with no separate prop to remember to pass. SubMenu's own trigger row is a MenuItem too, so this covers it for free. * 🔧 Add Playwright coverage for the project options menu Covers all four places the migrated project menu is reachable: the dashboard grid's "..." button and title right-click, the sidebar's right-click, and the per-project files page's "..." button. Checks rename/duplicate/pin/move-to/delete render (and that the default Drafts project correctly hides all of them), that rename opens the inline editor, that delete opens the confirm modal, and that the move-to submenu lists other teams. Also drop an unused React import from context_menu.stories.jsx, spotted in passing. * ♻️ Add datatest id * ♻️ Fix linter * 🐛 Build @penpot/ui automatically after pnpm install packages/ui/dist is gitignored (build output) and nothing in the install pipeline built it, so a fresh checkout — CI included — never had it. Any code importing "@penpot/ui/menu" (the frontend's own cljs-runtime tests among them) failed at module resolution with ERR_MODULE_NOT_FOUND rather than any real test failure. Build it in postinstall, the same way plugins-runtime already does, so it's always present after `pnpm install` without a separate manual build step. * 🔥 Remove flaky Menu dense/ellipsis Storybook tests Test Dense Shrinks Items and Test Long Label Ellipses Instead Of Wrapping asserted computed pixel styles that passed consistently locally (including with a fresh packages/ui install) but failed in CI, suggesting a CI-only timing/environment discrepancy in when the computed style stabilizes. Dropping them rather than chasing a non-reproducible flake. * 💄 Open the file/project options menu right, top-aligned Switch the dashboard file and project "..." options menus from "bottom end" to "right top" placement, so they open beside the trigger button instead of below it. * 🐛 Stop drilldown SubMenu jumping to the opposite edge A drilldown SubMenu swaps its parent Menu/ContextMenu popover's own content in place, and react-aria re-runs its flip/collision placement on every layout change. Drilling into a shorter level than the root could shrink the popover enough that react-aria decided there was now room on the other side, flipping it there — a visible jump even though the popover never actually moved from the caller's point of view. The previous fix padded every drilled-in level out to the root's own min-inline-size/min-block-size so the popover never got small enough to trigger a re-flip, but that meant a level naturally much shorter than the root still rendered at the root's full height. Replace it with shouldUpdatePosition={false} on the Popover for as long as any level is drilled in. This freezes whichever edge react-aria already resolved for the root, so a shorter level just shrinks from the opposite edge instead of triggering a new placement decision, and a taller level grows from that same opposite edge in the direction the root already opened. shouldUpdatePosition goes back to true once the stack returns to the root, so a fresh open still resolves normally. * ♻️ Update menu placements and use buttons from DS --------- Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net> |
||
|
|
b660ea9d53 |
✨ Route the app by query string with screen key
Move SPA routing out of the URL fragment into the normal query string. The screen travels in a reserved `screen` key holding the route name (`?screen=workspace&team-id=…`); every other param keeps its name. `rt/nav` and `rt/resolve` keep their signatures. This deletes the fragment-mirroring URL surgery, simplifies link-preview (the server sees everything) and nginx (single path, no SPA fallback rules needed), and migrates OIDC redirects, email links, e2e helpers and plugin test utils to the new format. Legacy `#/…` URLs translate client-side for one Penpot version (`legacy-routes`, marked TODO(next-version)); non-SPA paths are untouched. AI-assisted-by: muse-spark-1.3-contributor |
||
|
|
a91d81c695 | 🎉 Add link preview metadata for shared links |