From 73b7c0ee5dd0a6b0e9367ca82cbc85fd939b88b4 Mon Sep 17 00:00:00 2001 From: Nicola Bortoletto Date: Tue, 17 Feb 2026 08:40:40 +0100 Subject: [PATCH 001/288] :globe_with_meridians: Add translations for: Italian Currently translated at 99.5% (2065 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/ --- frontend/translations/it.po | 74 ++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/frontend/translations/it.po b/frontend/translations/it.po index 1358ef1f50..6192a1b857 100644 --- a/frontend/translations/it.po +++ b/frontend/translations/it.po @@ -1,15 +1,15 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-16 08:35+0000\n" +"PO-Revision-Date: 2026-02-17 10:09+0000\n" "Last-Translator: Nicola Bortoletto \n" -"Language-Team: Italian " -"\n" +"Language-Team: Italian \n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.16-dev\n" +"X-Generator: Weblate 5.16\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -8150,7 +8150,7 @@ msgstr "Nessun riferimento trovato" msgid "workspace.tokens.no-remap-needed" msgstr "" "Questo token al momento non è utilizzato nel tuo progetto, quindi non è " -"necessario alcun rimappaggio." +"necessaria alcuna riassegnazione." #: src/app/main/ui/workspace/tokens/sets/lists.cljs:485 msgid "workspace.tokens.no-sets-create" @@ -8803,3 +8803,67 @@ msgstr "Clicca per chiudere il tracciato" #~ msgid "onboarding.slide.1.desc1" #~ msgstr "Crea interazioni complete per imitare al meglio il prodotto finale." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:99 +msgid "workspace.tokens.remap" +msgstr "Riassegna token" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:95 +msgid "workspace.tokens.not-remap" +msgstr "Non riassegnare" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:86 +msgid "workspace.tokens.remap-token-references-title" +msgstr "Riassegnare tutti i token che usano `%s` a `%s`?" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88 +msgid "workspace.tokens.remap-warning-effects" +msgstr "" +"Questo cambierà tutti i livelli e i riferimenti che utilizzano il vecchio " +"nome del token." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89 +msgid "workspace.tokens.remap-warning-time" +msgstr "Questa azione può richiedere un po' di tempo." + +#: src/app/main/ui/dashboard/sidebar.cljs:347 +msgid "dashboard.create-new-org" +msgstr "Crea nuova organizzazione" + +#: src/app/main/errors.cljs:105 +msgid "errors.unexpected-exception" +msgstr "Errore inaspettato: %s" + +#: src/app/main/ui/static.cljs:315 +msgid "errors.webgl-context-lost.desc-message" +msgstr "WebGL ha smesso di funzionare. Ricarica la pagina per ripristinarlo" + +#: src/app/main/ui/static.cljs:314 +msgid "errors.webgl-context-lost.main-message" +msgstr "Oops! Il contesto della canvas è stato perso" + +#: src/app/main/ui/static.cljs:318 +msgid "labels.reload-page" +msgstr "Ricarica pagina" + +#: src/app/main/ui/workspace/sidebar/debug.cljs:38 +msgid "workspace.debug.title" +msgstr "Strumenti di debug" + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:303 +msgid "workspace.tokens.missing-reference" +msgstr "Riferimento mancante" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:204 +msgid "workspace.tokens.reference-composite-shadow" +msgstr "Inserisci un alias per il token ombra" + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +#, unused +msgid "workspace.tokens.warning-name-change" +msgstr "" +"Rinominare questo token interromperà ogni riferimento al suo nome precedente" + +#: src/app/main/ui/workspace/top_toolbar.cljs:231 +msgid "workspace.toolbar.debug" +msgstr "Strumenti di debug" From 627854fbba982f8293f68a98d390a8a1391dc164 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Tue, 17 Feb 2026 05:18:33 +0100 Subject: [PATCH 002/288] :globe_with_meridians: Add translations for: German Currently translated at 94.6% (1964 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/ --- frontend/translations/de.po | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 8a97b44d41..9345fb08e0 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -1,15 +1,15 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-16 08:35+0000\n" -"Last-Translator: Anonymous \n" -"Language-Team: German " -"\n" +"PO-Revision-Date: 2026-02-17 10:09+0000\n" +"Last-Translator: nautilusx \n" +"Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.16-dev\n" +"X-Generator: Weblate 5.16\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -8376,3 +8376,25 @@ msgstr "Automatisch gespeicherte Versionen werden für %s Tage aufbewahrt." #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Klicken Sie, um den Pfad zu schließen" + +#: src/app/main/ui/dashboard/grid.cljs:248 +msgid "dashboard.deleted.will-be-deleted-at" +msgstr "Wird gelöscht am %s" + +#: src/app/main/errors.cljs:105 +msgid "errors.unexpected-exception" +msgstr "Unerwarteter Fehler: %s" + +#: src/app/main/ui/static.cljs:315 +msgid "errors.webgl-context-lost.desc-message" +msgstr "" +"WebGL funktioniert nicht mehr. Bitte laden Sie die Seite neu, um sie " +"zurückzusetzen" + +#: src/app/main/ui/static.cljs:405 +msgid "labels.internal-error.desc-message-first" +msgstr "Etwas Schlimmes ist passiert." + +#: src/app/main/ui/static.cljs:318 +msgid "labels.reload-page" +msgstr "Seite neu laden" From 1c062b4cd03539a56f1110db599cd18af909e282 Mon Sep 17 00:00:00 2001 From: Stephan Paternotte Date: Tue, 17 Feb 2026 06:46:34 +0100 Subject: [PATCH 003/288] :globe_with_meridians: Add translations for: Dutch Currently translated at 99.8% (2070 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/ --- frontend/translations/nl.po | 78 ++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/frontend/translations/nl.po b/frontend/translations/nl.po index f1150666bf..143014fb57 100644 --- a/frontend/translations/nl.po +++ b/frontend/translations/nl.po @@ -1,15 +1,15 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-16 08:35+0000\n" -"Last-Translator: Sebastiaan Pasma \n" -"Language-Team: Dutch " -"\n" +"PO-Revision-Date: 2026-02-17 10:09+0000\n" +"Last-Translator: Stephan Paternotte \n" +"Language-Team: Dutch \n" "Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.16-dev\n" +"X-Generator: Weblate 5.16\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -2115,7 +2115,6 @@ msgid "inspect.tabs.styles.active-themes" msgstr "Actieve thema's" #: src/app/main/ui/inspect/styles/style_box.cljs:68 -#, fuzzy msgid "inspect.tabs.styles.copy-shorthand" msgstr "CSS-code kopiëren naar klembord" @@ -5044,7 +5043,6 @@ msgid "subscription.settings.sucess.dialog.title" msgstr "Je bent %s!" #: src/app/main/ui/settings/subscription.cljs:526 -#, fuzzy msgid "subscription.settings.support-us-since" msgstr "Je hebt ons gesteund met dit abonnement sinds: %s" @@ -8197,7 +8195,6 @@ msgid "workspace.tokens.opacity-range" msgstr "De dekking moet tussen 0 en 100% of 0 en 1 zijn (bijv. 50% of 0,5)." #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 -#, fuzzy msgid "workspace.tokens.original-value" msgstr "Oorspronkelijke waarde: %s" @@ -8232,7 +8229,6 @@ msgid "workspace.tokens.remapping-in-progress" msgstr "Token-referenties opnieuw toewijzen…" #: src/app/main/data/workspace/tokens/warnings.cljs:15, src/app/main/data/workspace/tokens/warnings.cljs:19, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:56, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:84, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:103, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:285, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:459, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:176, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:311, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:251, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:364, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:465, src/app/main/ui/workspace/tokens/management/token_pill.cljs:122 -#, fuzzy msgid "workspace.tokens.resolved-value" msgstr "Opgeloste waarde: %s" @@ -8379,7 +8375,6 @@ msgid "workspace.tokens.themes-list" msgstr "Lijst met thema's" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:275, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:276 -#, fuzzy msgid "workspace.tokens.token-description" msgstr "Beschrijving" @@ -8788,3 +8783,66 @@ msgstr "Automatisch opgeslagen versies worden %s dagen bewaard." #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Klik om het pad te sluiten" + +#: src/app/main/ui/static.cljs:315 +msgid "errors.webgl-context-lost.desc-message" +msgstr "" +"WebGL is gestopt met werken. Laad de pagina opnieuw om deze opnieuw in te " +"stellen" + +#: src/app/main/ui/static.cljs:314 +msgid "errors.webgl-context-lost.main-message" +msgstr "Oeps! De context van het doek ging verloren" + +#: src/app/main/ui/dashboard/sidebar.cljs:347 +msgid "dashboard.create-new-org" +msgstr "Nieuwe org aanmaken" + +#: src/app/main/errors.cljs:105 +msgid "errors.unexpected-exception" +msgstr "Onverwachte for: %s" + +#: src/app/main/ui/static.cljs:318 +msgid "labels.reload-page" +msgstr "Pagina's opnieuw laden" + +#: src/app/main/ui/workspace/sidebar/debug.cljs:38 +msgid "workspace.debug.title" +msgstr "Hulpmiddelen voor foutopsporing" + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:303 +msgid "workspace.tokens.missing-reference" +msgstr "Ontbrekende verwijzing" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:95 +msgid "workspace.tokens.not-remap" +msgstr "Niet opnieuw toewijzen" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:99 +msgid "workspace.tokens.remap" +msgstr "Tokens opnieuw toewijzen" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:86 +msgid "workspace.tokens.remap-token-references-title" +msgstr "Alle tokens die '%s' gebruiken toewijzen aan '%s'?" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88 +msgid "workspace.tokens.remap-warning-effects" +msgstr "" +"Hierdoor worden alle lagen en verwijzingen die de oude tokennaam gebruiken, " +"gewijzigd." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89 +msgid "workspace.tokens.remap-warning-time" +msgstr "Deze actie kan wel even duren." + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +#, unused +msgid "workspace.tokens.warning-name-change" +msgstr "" +"Door dit token te hernoemen, wordt elke verwijzing naar de oude naam " +"verbroken" + +#: src/app/main/ui/workspace/top_toolbar.cljs:231 +msgid "workspace.toolbar.debug" +msgstr "Hulpmiddelen voor foutopsporing" From 53d5fcd8d0b9048f31caab74a9cf6459981bd8b8 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Mon, 16 Feb 2026 10:54:01 +0100 Subject: [PATCH 004/288] :globe_with_meridians: Add translations for: Hebrew Currently translated at 97.1% (2015 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/ --- frontend/translations/he.po | 139 +++++++++++++++++++++++++++++++++--- 1 file changed, 129 insertions(+), 10 deletions(-) diff --git a/frontend/translations/he.po b/frontend/translations/he.po index 2208d0de08..45f92edd68 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -1,16 +1,16 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-16 08:35+0000\n" -"Last-Translator: Anonymous \n" -"Language-Team: Hebrew " -"\n" +"PO-Revision-Date: 2026-02-17 10:09+0000\n" +"Last-Translator: Yaron Shahrabani \n" +"Language-Team: Hebrew \n" "Language: he\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && " "n % 10 == 0) ? 2 : 3));\n" -"X-Generator: Weblate 5.16-dev\n" +"X-Generator: Weblate 5.16\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -409,7 +409,7 @@ msgstr "הוספת קובץ" #: src/app/main/ui/dashboard/file_menu.cljs:322, src/app/main/ui/workspace/main_menu.cljs:650 msgid "dashboard.add-shared" -msgstr "הוספת ספריה משותפת" +msgstr "הוספת ספרייה משותפת" #: src/app/main/ui/settings/profile.cljs:75 msgid "dashboard.change-email" @@ -6452,19 +6452,19 @@ msgstr "אפשרויות מתקדמות" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:686, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:693 msgid "workspace.options.layout-item.layout-item-max-h" -msgstr "גובה מר.‏" +msgstr "גובה מרבי" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:624, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:631 msgid "workspace.options.layout-item.layout-item-max-w" -msgstr "רוחב מר.‏" +msgstr "רוחב מרבי" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:655, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:663 msgid "workspace.options.layout-item.layout-item-min-h" -msgstr "גובה מז.‏" +msgstr "גובה מזערי" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:591, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:600 msgid "workspace.options.layout-item.layout-item-min-w" -msgstr "רוחב מז.‏" +msgstr "רוחב מזערי" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs #, unused @@ -8392,3 +8392,122 @@ msgstr "גרסאות שנשמרו אוטומטית תישמרנה למשך %s י #, unused msgid "workspace.viewport.click-to-close-path" msgstr "לחיצה תסגור את הנתיב" + +#: src/app/main/ui/dashboard/sidebar.cljs:347 +msgid "dashboard.create-new-org" +msgstr "יצירת ארגון חדש" + +#: src/app/main/ui/dashboard/deleted.cljs:262 +msgid "dashboard.delete-all-forever-confirmation.description" +msgstr "למחוק את כל המיזמים והקבצים שלך לצמיתות? זאת פעולה בלתי הפיכה." + +#: src/app/main/ui/dashboard/file_menu.cljs:221 +msgid "dashboard.delete-file-forever-confirmation.description" +msgstr "למחוק את %s לצמיתות? זאת פעולה בלתי הפיכה." + +#: src/app/main/data/dashboard.cljs:778 +msgid "dashboard.delete-files-success-notification" +msgstr "%s קבצים נמחקו בהצלחה." + +#: src/app/main/ui/dashboard/deleted.cljs:51, src/app/main/ui/dashboard/deleted.cljs:53, src/app/main/ui/dashboard/deleted.cljs:261, src/app/main/ui/dashboard/deleted.cljs:263, src/app/main/ui/dashboard/file_menu.cljs:220, src/app/main/ui/dashboard/file_menu.cljs:222 +msgid "dashboard.delete-forever-confirmation.title" +msgstr "מחיקה לצמיתות" + +#: src/app/main/ui/dashboard/deleted.cljs:85 +msgid "dashboard.delete-project-button" +msgstr "מחיקת מיזם" + +#: src/app/main/ui/dashboard/deleted.cljs:52 +msgid "dashboard.delete-project-forever-confirmation.description" +msgstr "" +"למחוק את המיזם %s לצמיתות? הפעולה הזאת תמחק לצמיתות את כל הקבצים שבו. זאת " +"פעולה בלתי הפיכה." + +#: src/app/main/data/dashboard.cljs:777, src/app/main/data/dashboard.cljs:811 +msgid "dashboard.delete-success-notification" +msgstr "%s נמחק בהצלחה." + +#: src/app/main/ui/dashboard/deleted.cljs:327 +msgid "dashboard.deleted.empty-state-description" +msgstr "האשפה שלך ריקה. קבצים ומיזמים שנמחקו יופיעו כאן." + +#: src/app/main/ui/dashboard/grid.cljs:248 +msgid "dashboard.deleted.will-be-deleted-at" +msgstr "%s יימחק" + +#, unused +msgid "dashboard.errors.error-on-delete-file" +msgstr "אירעה שגיאה במחיקת הקובץ %s." + +#: src/app/main/data/dashboard.cljs:781 +msgid "dashboard.errors.error-on-delete-files" +msgstr "אירעה שגיאה במחיקת הקבצים." + +#: src/app/main/data/dashboard.cljs:814 +msgid "dashboard.errors.error-on-delete-project" +msgstr "אירעה שגיאה במחיקת המיזם %s." + +#: src/app/main/data/dashboard.cljs:909, src/app/main/ui/dashboard/file_menu.cljs:201 +msgid "dashboard.errors.error-on-restore-file" +msgstr "אירעה שגיאה בשחזור הקובץ %s." + +#: src/app/main/data/dashboard.cljs:910 +msgid "dashboard.errors.error-on-restore-files" +msgstr "אירעה שגיאה בשחזור הקבצים." + +#: src/app/main/data/dashboard.cljs:942 +msgid "dashboard.errors.error-on-restoring-project" +msgstr "אירעה שגיאה בשחזור המיזם %s עם הקבצים שלו." + +#: src/app/main/ui/dashboard/file_menu.cljs:266 +msgid "dashboard.file-menu.delete-files-permanently-option" +msgid_plural "dashboard.file-menu.delete-files-permanently-option" +msgstr[0] "מחיקת קובץ" +msgstr[1] "מחיקת קבצים" +msgstr[2] "מחיקת קבצים" +msgstr[3] "מחיקת קבצים" + +#: src/app/main/ui/dashboard/file_menu.cljs:263 +msgid "dashboard.file-menu.restore-files-option" +msgid_plural "dashboard.file-menu.restore-files-option" +msgstr[0] "שחזור קובץ" +msgstr[1] "שחזור קבצים" +msgstr[2] "שחזור קבצים" +msgstr[3] "שחזור קבצים" + +#: src/app/main/data/dashboard.cljs:722 +msgid "dashboard.progress-notification.deleting-files" +msgstr "קבצים נמחקים…" + +#: src/app/main/data/dashboard.cljs:843 +msgid "dashboard.progress-notification.restoring-files" +msgstr "קבצים משוחזרים…" + +#: src/app/main/data/dashboard.cljs:723 +msgid "dashboard.progress-notification.slow-delete" +msgstr "המחיקה איטית להחריד" + +#: src/app/main/data/dashboard.cljs:844 +msgid "dashboard.progress-notification.slow-restore" +msgstr "השחזור איטי להחריד" + +#: src/app/main/ui/dashboard/deleted.cljs:274 +msgid "dashboard.restore-all-confirmation.description" +msgstr "" +"הפעולה הזאת תשחזר את כל המיזמים והקבצים שלך. זאת פעולה שיכולה לארוך זמן מה." + +#: src/app/main/ui/dashboard/deleted.cljs:273 +msgid "dashboard.restore-all-confirmation.title" +msgstr "שחזור כל המיזמים והקבצים" + +#: src/app/main/ui/dashboard/deleted.cljs:308 +msgid "dashboard.restore-all-deleted-button" +msgstr "לשחזר הכול" + +#: src/app/main/data/dashboard.cljs:903 +msgid "dashboard.restore-files-success-notification" +msgstr "%s קבצים שוחזרו בהצלחה." + +#: src/app/main/ui/dashboard/deleted.cljs:82 +msgid "dashboard.restore-project-button" +msgstr "שחזור מיזם" From 836616a05b87fda7d6945c2a5df790b3a0135c0f Mon Sep 17 00:00:00 2001 From: Alexis Morin Date: Tue, 17 Feb 2026 02:08:09 +0100 Subject: [PATCH 005/288] :globe_with_meridians: Add translations for: French (Canada) Currently translated at 71.7% (1489 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/ --- frontend/translations/fr_CA.po | 952 ++++++++++++++++++++++++++++++++- 1 file changed, 948 insertions(+), 4 deletions(-) diff --git a/frontend/translations/fr_CA.po b/frontend/translations/fr_CA.po index 77a8dd2e69..47ebf81527 100644 --- a/frontend/translations/fr_CA.po +++ b/frontend/translations/fr_CA.po @@ -1,15 +1,15 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-16 08:15+0000\n" +"PO-Revision-Date: 2026-02-17 10:10+0000\n" "Last-Translator: Alexis Morin \n" -"Language-Team: French (Canada) " -"\n" +"Language-Team: French (Canada) \n" "Language: fr_CA\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 5.16-dev\n" +"X-Generator: Weblate 5.16\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -5458,3 +5458,947 @@ msgstr "Tokens de couleur" #: src/app/main/ui/workspace/colorpicker.cljs:434 msgid "workspace.colorpicker.get-color" msgstr "Pipette de couleur" + +#: src/app/main/ui/static.cljs:314 +msgid "errors.webgl-context-lost.main-message" +msgstr "Oups! Le contexte du canevas a été perdu" + +#: src/app/main/ui/dashboard/sidebar.cljs:347 +msgid "dashboard.create-new-org" +msgstr "Crée une nouvelle organisation" + +#: src/app/main/errors.cljs:105 +msgid "errors.unexpected-exception" +msgstr "Erreur inattendue : %s" + +#: src/app/main/ui/static.cljs:315 +msgid "errors.webgl-context-lost.desc-message" +msgstr "WebGL a arrêté de fonctionner. Recharger la page pour le réinitialiser" + +#: src/app/main/ui/static.cljs:318 +msgid "labels.reload-page" +msgstr "Recharger la page" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:506 +msgid "workspace.component.swap.loop-error" +msgstr "Les composants ne peuvent pas être imbriqués dans eux-mêmes." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:505 +msgid "workspace.component.switch.loop-error-multi" +msgstr "" +"Certaines copies n'ont pu être interchangées. Les composants ne peuvent pas " +"être imbriqués dans eux-mêmes." + +#: src/app/main/ui/workspace/context_menu.cljs:796 +msgid "workspace.context-menu.grid-cells.area" +msgstr "Créer une zone" + +#: src/app/main/ui/workspace/context_menu.cljs:799 +msgid "workspace.context-menu.grid-cells.create-board" +msgstr "Créer un tableau" + +#: src/app/main/ui/workspace/context_menu.cljs:791 +msgid "workspace.context-menu.grid-cells.merge" +msgstr "Fusionner les cellules" + +#: src/app/main/ui/workspace/context_menu.cljs:754 +msgid "workspace.context-menu.grid-track.column.add-after" +msgstr "Ajouter 1 colonne à droite" + +#: src/app/main/ui/workspace/context_menu.cljs:753 +msgid "workspace.context-menu.grid-track.column.add-before" +msgstr "Ajouter 1 colonne à gauche" + +#, unused +msgid "workspace.focus.selection" +msgstr "Sélection" + +#: src/app/main/ui/workspace/context_menu.cljs:755 +msgid "workspace.context-menu.grid-track.column.delete" +msgstr "Supprimer la colonne" + +#: src/app/main/ui/workspace/context_menu.cljs:756 +msgid "workspace.context-menu.grid-track.column.delete-shapes" +msgstr "Supprimer la colonne et les formes" + +#: src/app/main/ui/workspace/context_menu.cljs:752 +msgid "workspace.context-menu.grid-track.column.duplicate" +msgstr "Dupliquer la colonne" + +#: src/app/main/ui/workspace/context_menu.cljs:761 +msgid "workspace.context-menu.grid-track.row.add-after" +msgstr "Ajouter 1 colonne dessous" + +#: src/app/main/ui/workspace/context_menu.cljs:760 +msgid "workspace.context-menu.grid-track.row.add-before" +msgstr "Ajouter 1 colonne dessus" + +#: src/app/main/ui/workspace/context_menu.cljs:762 +msgid "workspace.context-menu.grid-track.row.delete" +msgstr "Supprimer la rangée" + +#: src/app/main/ui/workspace/context_menu.cljs:763 +msgid "workspace.context-menu.grid-track.row.delete-shapes" +msgstr "Supprimer la rangée et les formes" + +#: src/app/main/ui/workspace/context_menu.cljs:759 +msgid "workspace.context-menu.grid-track.row.duplicate" +msgstr "Dupliquer la rangée" + +#: src/app/main/ui/workspace/sidebar/debug.cljs:38 +msgid "workspace.debug.title" +msgstr "Outils de débogage" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:512 +msgid "workspace.focus.focus-mode" +msgstr "Mode focus" + +#: src/app/main/ui/workspace/context_menu.cljs:396, src/app/main/ui/workspace/context_menu.cljs:711 +msgid "workspace.focus.focus-off" +msgstr "Désactiver mode focus" + +#: src/app/main/ui/workspace/context_menu.cljs:395 +msgid "workspace.focus.focus-on" +msgstr "Activer mode focus" + +#: src/app/util/color.cljs:34 +msgid "workspace.gradients.linear" +msgstr "Dégradé linéaire" + +#: src/app/util/color.cljs:35 +msgid "workspace.gradients.radial" +msgstr "Dégradé radial" + +#: src/app/main/ui/workspace/main_menu.cljs:274 +msgid "workspace.header.menu.disable-dynamic-alignment" +msgstr "Désactiver l'alignement dynamique" + +#: src/app/main/ui/workspace/main_menu.cljs:228 +msgid "workspace.header.menu.disable-scale-content" +msgstr "Désactiver la mise à l'échelle proportionnelle" + +#: src/app/main/ui/workspace/header.cljs +#, unused +msgid "workspace.header.menu.disable-scale-text" +msgstr "Désactiver la mise à l'échelle du texte" + +#: src/app/main/ui/workspace/main_menu.cljs:259 +msgid "workspace.header.menu.disable-snap-guides" +msgstr "Désactiver l'accrochage aux guides" + +#: src/app/main/ui/workspace/main_menu.cljs:289 +msgid "workspace.header.menu.disable-snap-pixel-grid" +msgstr "Désactiver l'accrochage aux pixels" + +#: src/app/main/ui/workspace/main_menu.cljs:243 +msgid "workspace.header.menu.disable-snap-ruler-guides" +msgstr "Désactiver l'accrochage aux guides règles" + +#: src/app/main/ui/workspace/main_menu.cljs:275 +msgid "workspace.header.menu.enable-dynamic-alignment" +msgstr "Activer l'alignement dynamique" + +#: src/app/main/ui/workspace/main_menu.cljs:229 +msgid "workspace.header.menu.enable-scale-content" +msgstr "Activer la mise à l'échelle proportionnelle" + +#: src/app/main/ui/workspace/header.cljs +#, unused +msgid "workspace.header.menu.enable-scale-text" +msgstr "Activer la mise à l'échelle du texte" + +#: src/app/main/ui/workspace/main_menu.cljs:260 +msgid "workspace.header.menu.enable-snap-guides" +msgstr "Accrochage aux guides" + +#: src/app/main/ui/workspace/main_menu.cljs:290 +msgid "workspace.header.menu.enable-snap-pixel-grid" +msgstr "Activer l'accrochage aux pixels" + +#: src/app/main/ui/workspace/main_menu.cljs:244 +msgid "workspace.header.menu.enable-snap-ruler-guides" +msgstr "Accrochage aux règles guides" + +#: src/app/main/ui/workspace/main_menu.cljs:422 +msgid "workspace.header.menu.hide-artboard-names" +msgstr "Masquer le nom des tableaux" + +#: src/app/main/ui/workspace/main_menu.cljs:376 +msgid "workspace.header.menu.hide-guides" +msgstr "Masquer les guides" + +#: src/app/main/ui/workspace/main_menu.cljs:393 +msgid "workspace.header.menu.hide-palette" +msgstr "Masquer la palette de couleurs" + +#: src/app/main/ui/workspace/main_menu.cljs:434 +msgid "workspace.header.menu.hide-pixel-grid" +msgstr "Masquer la grille pixel" + +#: src/app/main/ui/workspace/main_menu.cljs:360 +msgid "workspace.header.menu.hide-rules" +msgstr "Masquer les règles" + +#: src/app/main/ui/workspace/main_menu.cljs:407 +msgid "workspace.header.menu.hide-textpalette" +msgstr "Masquer la palette de polices" + +#: src/app/main/ui/workspace/main_menu.cljs:884 +msgid "workspace.header.menu.option.edit" +msgstr "Éditer" + +#: src/app/main/ui/workspace/main_menu.cljs:873 +msgid "workspace.header.menu.option.file" +msgstr "Fichier" + +#: src/app/main/ui/workspace/main_menu.cljs:930 +msgid "workspace.header.menu.option.help-info" +msgstr "Aide et info" + +#: src/app/main/ui/workspace/main_menu.cljs:916 +#, unused +msgid "workspace.header.menu.option.power-up" +msgstr "Bonifier ton forfait" + +#: src/app/main/ui/workspace/main_menu.cljs:906 +msgid "workspace.header.menu.option.preferences" +msgstr "Préférences" + +#: src/app/main/ui/workspace/main_menu.cljs:895 +msgid "workspace.header.menu.option.view" +msgstr "Affichage" + +#: src/app/main/ui/workspace/main_menu.cljs:506 +msgid "workspace.header.menu.redo" +msgstr "Rétablir" + +#: src/app/main/ui/workspace/main_menu.cljs:477 +msgid "workspace.header.menu.select-all" +msgstr "Tout sélectionner" + +#: src/app/main/ui/workspace/main_menu.cljs:423 +msgid "workspace.header.menu.show-artboard-names" +msgstr "Afficher le nom des tableaux" + +#: src/app/main/ui/workspace/main_menu.cljs:377 +msgid "workspace.header.menu.show-guides" +msgstr "Afficher les guides" + +#: src/app/main/ui/workspace/main_menu.cljs:394 +msgid "workspace.header.menu.show-palette" +msgstr "Afficher la palette de couleurs" + +#: src/app/main/ui/workspace/main_menu.cljs:435 +msgid "workspace.header.menu.show-pixel-grid" +msgstr "Afficher la grille pixel" + +#: src/app/main/ui/workspace/main_menu.cljs:361 +msgid "workspace.header.menu.show-rules" +msgstr "Afficher les règles" + +#: src/app/main/ui/workspace/main_menu.cljs:408 +msgid "workspace.header.menu.show-textpalette" +msgstr "Afficher la palette des polices" + +#: src/app/main/ui/workspace/main_menu.cljs:316 +msgid "workspace.header.menu.toggle-dark-theme" +msgstr "Basculer au mode sombre" + +#: src/app/main/ui/workspace/main_menu.cljs:314, src/app/main/ui/workspace/main_menu.cljs:317 +msgid "workspace.header.menu.toggle-light-theme" +msgstr "Basculer au mode clair" + +#: src/app/main/ui/workspace/main_menu.cljs:315 +msgid "workspace.header.menu.toggle-system-theme" +msgstr "Basculer au thème du système" + +#: src/app/main/ui/workspace/main_menu.cljs:492 +msgid "workspace.header.menu.undo" +msgstr "Défaire" + +#: src/app/main/ui/viewer/header.cljs:93, src/app/main/ui/workspace/right_header.cljs:92 +msgid "workspace.header.reset-zoom" +msgstr "Réinitialiser" + +#: src/app/main/ui/workspace/left_header.cljs:128 +msgid "workspace.header.save-error" +msgstr "Erreur de sauvegarde" + +#: src/app/main/ui/workspace/left_header.cljs:127 +msgid "workspace.header.saved" +msgstr "Sauvegardé" + +#: src/app/main/ui/workspace/left_header.cljs:125, src/app/main/ui/workspace/left_header.cljs:126 +msgid "workspace.header.saving" +msgstr "Sauvegarde en cours" + +#: src/app/main/ui/workspace/right_header.cljs:232 +msgid "workspace.header.share" +msgstr "Partager" + +#: src/app/main/ui/workspace/right_header.cljs:48, src/app/main/ui/workspace/right_header.cljs:53 +#, unused +msgid "workspace.header.unsaved" +msgstr "Changements non sauvegardés" + +#: src/app/main/ui/workspace/right_header.cljs:237 +msgid "workspace.header.viewer" +msgstr "Mode visionnement (%s)" + +#: src/app/main/ui/viewer/header.cljs:74, src/app/main/ui/workspace/right_header.cljs:74 +msgid "workspace.header.zoom" +msgstr "Zoom" + +#: src/app/main/ui/viewer/header.cljs:104 +msgid "workspace.header.zoom-fill" +msgstr "Remplir l'écran" + +#: src/app/main/ui/viewer/header.cljs:97 +msgid "workspace.header.zoom-fit" +msgstr "Ajuster aux dimensions" + +#: src/app/main/ui/workspace/right_header.cljs:96 +msgid "workspace.header.zoom-fit-all" +msgstr "Zoom cadrant tout" + +#: src/app/main/ui/viewer/header.cljs:111 +msgid "workspace.header.zoom-full-screen" +msgstr "Plein écran" + +#: src/app/main/ui/workspace/right_header.cljs:104 +msgid "workspace.header.zoom-selected" +msgstr "Zoom à la sélection" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:422 +msgid "workspace.layout-grid.editor.margin.expand" +msgstr "Afficher la marge des 4 côtés" + +#: src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs:275, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:859 +msgid "workspace.layout-grid.editor.options.edit-grid" +msgstr "Éditer la grille" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1539 +msgid "workspace.layout-grid.editor.options.exit" +msgstr "Quitter" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:584, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:593, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:599 +msgid "workspace.layout-grid.editor.padding.bottom" +msgstr "Marge interne du bas" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:669 +msgid "workspace.layout-grid.editor.padding.expand" +msgstr "Afficher la marge interne des 4 côtés" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:416, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:427, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:433 +msgid "workspace.layout-grid.editor.padding.horizontal" +msgstr "Marge interne horizontale" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:618, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:627, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:633 +msgid "workspace.layout-grid.editor.padding.left" +msgstr "Marge interne gauche" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:551, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:560, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:566 +msgid "workspace.layout-grid.editor.padding.right" +msgstr "Marge interne droite" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:517, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:526, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:532 +msgid "workspace.layout-grid.editor.padding.top" +msgstr "Marge interne du haut" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:380, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:391, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:397 +msgid "workspace.layout-grid.editor.padding.vertical" +msgstr "Marge interne verticale" + +#: src/app/main/ui/workspace/viewport/grid_layout_editor.cljs:62 +msgid "workspace.layout-grid.editor.title" +msgstr "Édition de la grille" + +#: src/app/main/ui/workspace/viewport/grid_layout_editor.cljs:70 +msgid "workspace.layout-grid.editor.top-bar.done" +msgstr "Terminé" + +#: src/app/main/ui/workspace/viewport/grid_layout_editor.cljs:66 +msgid "workspace.layout-grid.editor.top-bar.locate" +msgstr "Situer" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1565 +msgid "workspace.layout-grid.editor.top-bar.locate.tooltip" +msgstr "Situer la mise en page grille" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:449 +msgid "workspace.layout-item.fit-content-horizontal" +msgstr "S'ajuster au contenu (horizontal)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:477 +msgid "workspace.layout-item.fit-content-vertical" +msgstr "S'ajuster au contenu (vertical)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:465 +msgid "workspace.layout-item.fix-height" +msgstr "Hauteur fixe" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:439 +msgid "workspace.layout-item.fix-width" +msgstr "Largeur fixe" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:471 +msgid "workspace.layout-item.height-100" +msgstr "Hauteur 100%" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:444 +msgid "workspace.layout-item.width-100" +msgstr "Largeur 100%" + +#: src/app/main/ui/workspace/libraries.cljs +#, unused +msgid "workspace.libraries.add" +msgstr "Ajouter" + +#: src/app/main/ui/workspace/libraries.cljs:100, src/app/main/ui/workspace/libraries.cljs:126 +msgid "workspace.libraries.colors" +msgid_plural "workspace.libraries.colors" +msgstr[0] "1 couleur" +msgstr[1] "%s couleurs" + +#: src/app/main/ui/workspace/color_palette.cljs:147 +msgid "workspace.libraries.colors.empty-palette" +msgstr "Aucun style de couleur dans la bibliothèque" + +#: src/app/main/ui/workspace/text_palette.cljs:161 +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "Aucun style de typographie dans la bibliothèque" + +#: src/app/main/ui/workspace/color_palette_ctx_menu.cljs:88, src/app/main/ui/workspace/colorpicker/libraries.cljs:48, src/app/main/ui/workspace/text_palette_ctx_menu.cljs:49 +msgid "workspace.libraries.colors.file-library" +msgstr "Bibliothèque du fichier" + +#: src/app/main/ui/workspace/colorpicker.cljs +#, unused +msgid "workspace.libraries.colors.hsv" +msgstr "HSV" + +#: src/app/main/ui/workspace/color_palette_ctx_menu.cljs:111, src/app/main/ui/workspace/colorpicker/libraries.cljs:47 +msgid "workspace.libraries.colors.recent-colors" +msgstr "Couleurs récentes" + +#: src/app/main/ui/workspace/colorpicker.cljs +#, unused +msgid "workspace.libraries.colors.rgb-complementary" +msgstr "RVB complémentaire" + +#: src/app/main/ui/workspace/colorpicker.cljs:355 +msgid "workspace.libraries.colors.rgba" +msgstr "RVBA" + +#: src/app/main/ui/workspace/colorpicker.cljs:555 +msgid "workspace.libraries.colors.save-color" +msgstr "Enregistrer le style de couleur" + +#: src/app/main/ui/workspace/libraries.cljs:94, src/app/main/ui/workspace/libraries.cljs:118 +msgid "workspace.libraries.components" +msgid_plural "workspace.libraries.components" +msgstr[0] "1 composant" +msgstr[1] "%s composants" + +#: src/app/main/ui/workspace/libraries.cljs:338 +msgid "workspace.libraries.connected-to" +msgstr "Connecté à" + +#: src/app/main/ui/workspace/libraries.cljs:392 +msgid "workspace.libraries.empty.add-some" +msgstr "Ou ajoute celles-ci pour tester :" + +#: src/app/main/ui/workspace/libraries.cljs:386 +msgid "workspace.libraries.empty.no-libraries" +msgstr "Aucune bibliothèque partagée dans ton équipe, tu peux chercher" + +#: src/app/main/ui/workspace/libraries.cljs:390 +msgid "workspace.libraries.empty.some-templates" +msgstr "des modèles ici" + +#: src/app/main/ui/workspace/libraries.cljs:313 +msgid "workspace.libraries.file-library" +msgstr "Bibliothèque du fichier" + +#: src/app/main/ui/workspace/libraries.cljs:97, src/app/main/ui/workspace/libraries.cljs:122 +msgid "workspace.libraries.graphics" +msgid_plural "workspace.libraries.graphics" +msgstr[0] "1 graphisme" +msgstr[1] "%s graphismes" + +#: src/app/main/ui/workspace/libraries.cljs:307 +msgid "workspace.libraries.in-this-file" +msgstr "BIBLIOTHÈQUES DANS CE FICHIER" + +#: src/app/main/ui/workspace/libraries.cljs:628, src/app/main/ui/workspace/libraries.cljs:648 +msgid "workspace.libraries.libraries" +msgstr "BIBLIOTHÈQUES" + +#: src/app/main/ui/workspace/libraries.cljs +#, unused +msgid "workspace.libraries.library" +msgstr "BIBLIOTHÈQUE" + +#: src/app/main/ui/workspace/libraries.cljs:487 +msgid "workspace.libraries.library-updates" +msgstr "MISES À JOUR DE BIBLIOTHÈQUES" + +#: src/app/main/ui/workspace/libraries.cljs:381 +msgid "workspace.libraries.loading" +msgstr "Chargement…" + +#: src/app/main/ui/workspace/libraries.cljs:387 +#, unused +msgid "workspace.libraries.more-templates" +msgstr "Tu peux chercher " + +#: src/app/main/ui/workspace/libraries.cljs:485 +msgid "workspace.libraries.no-libraries-need-sync" +msgstr "Aucune bibliothèque partagée ne requiert de mise à jour" + +#: src/app/main/ui/workspace/libraries.cljs:399 +msgid "workspace.libraries.no-matches-for" +msgstr "Aucun résultat trouvé pour « %s »" + +#: src/app/main/ui/workspace/libraries.cljs:356 +msgid "workspace.libraries.search-shared-libraries" +msgstr "Recherche des bibliothèques partagées" + +#: src/app/main/ui/workspace/libraries.cljs:352 +msgid "workspace.libraries.shared-libraries" +msgstr "BIBLIOTHÈQUES PARTAGÉES" + +#: src/app/main/ui/workspace/libraries.cljs:372 +msgid "workspace.libraries.shared-library-btn" +msgstr "Connecter une bibliothèque" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:332 +msgid "workspace.libraries.text.multiple-typography" +msgstr "Multiples typographies" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:335 +msgid "workspace.libraries.text.multiple-typography-tooltip" +msgstr "Délier toutes les typographies" + +#: src/app/main/ui/workspace/libraries.cljs:103, src/app/main/ui/workspace/libraries.cljs:130 +msgid "workspace.libraries.typography" +msgid_plural "workspace.libraries.typography" +msgstr[0] "1 typographie" +msgstr[1] "%s typographies" + +#: src/app/main/ui/workspace/libraries.cljs:343 +msgid "workspace.libraries.unlink-library-btn" +msgstr "Déconnecter la bibliothèque" + +#: src/app/main/ui/workspace/libraries.cljs:507 +msgid "workspace.libraries.update" +msgstr "Mettre à jour" + +#: src/app/main/ui/workspace/libraries.cljs:583 +msgid "workspace.libraries.update.see-all-changes" +msgstr "voir tous les changements" + +#: src/app/main/ui/workspace/libraries.cljs:630 +msgid "workspace.libraries.updates" +msgstr "MISES À JOUR" + +#: src/app/main/ui/ds/notifications/shared/notification_pill.cljs:67, src/app/main/ui/ds/notifications/shared/notification_pill.cljs:72 +msgid "workspace.notification-pill.detail" +msgstr "Détails" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:784 +msgid "workspace.options.add-interaction" +msgstr "Cliquer le bouton + pour ajouter des interactions." + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:96 +msgid "workspace.options.blur-options.add-blur" +msgstr "Ajouter un flou" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:119 +msgid "workspace.options.blur-options.remove-blur" +msgstr "Supprimer le flou" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:92, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:112 +msgid "workspace.options.blur-options.title" +msgstr "Flouter" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:91 +msgid "workspace.options.blur-options.title.group" +msgstr "Flou de groupe" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:90 +msgid "workspace.options.blur-options.title.multiple" +msgstr "Flou de la sélection" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:115 +msgid "workspace.options.blur-options.toggle-blur" +msgstr "Basculer le flou" + +#: src/app/main/ui/workspace/sidebar/options/page.cljs:42, src/app/main/ui/workspace/sidebar/options/page.cljs:50 +msgid "workspace.options.canvas-background" +msgstr "Arrière-plan du canevas" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:567 +msgid "workspace.options.clip-content" +msgstr "Rogner le contenu" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1027, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1033, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1282 +msgid "workspace.options.component" +msgstr "Composant" + +#: src/app/main/ui/inspect/annotation.cljs:19, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:194 +msgid "workspace.options.component.annotation" +msgstr "Annotation" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1041 +msgid "workspace.options.component.copy" +msgstr "Copier" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:187 +msgid "workspace.options.component.create-annotation" +msgstr "Créer une annotation" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:186 +msgid "workspace.options.component.edit-annotation" +msgstr "Éditer l'annotation" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1040, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1286 +msgid "workspace.options.component.main" +msgstr "Principal" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:781 +msgid "workspace.options.component.swap" +msgstr "Permuter composant" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:820 +msgid "workspace.options.component.swap.empty" +msgstr "Aucun atout dans cette bibliothèque" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1066 +msgid "workspace.options.component.unlinked" +msgstr "Non-lié" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:570 +msgid "workspace.options.component.variant.duplicated.copy.locate" +msgstr "Localiser les variants en conflit" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:567 +msgid "workspace.options.component.variant.duplicated.copy.title" +msgstr "" +"Ce composant a des variants en conflit. Assure-toi que chaque variant " +"possède des valeurs de propriétés uniques." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1341 +msgid "workspace.options.component.variant.duplicated.group.locate" +msgstr "Localiser les doublons" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1338 +msgid "workspace.options.component.variant.duplicated.group.title" +msgstr "Certains variants ont des propriétés et valeurs identiques" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:267 +msgid "workspace.options.component.variant.duplicated.single.all" +msgstr "" +"Ces variants ont des propriétés et valeurs identiques. Ajuster les valeurs " +"pour qu'elles puissent être trouvées." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:264 +msgid "workspace.options.component.variant.duplicated.single.one" +msgstr "" +"Ce variant a des propriétés et valeurs identiques à un autre variant. " +"Ajuster les valeurs pour qu'elles puissent être trouvées." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:270 +msgid "workspace.options.component.variant.duplicated.single.some" +msgstr "" +"Certains variants ont des propriétés et valeurs identiques. Ajuster les " +"valeurs pour qu'elles puissent être trouvées." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:557 +msgid "workspace.options.component.variant.malformed.copy" +msgstr "" +"Ce composant a des variants avec des noms invalides. Chaque variant doit " +"suivre la structure correcte." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1331 +msgid "workspace.options.component.variant.malformed.group.locate" +msgstr "Localiser les variants invalides" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1328 +msgid "workspace.options.component.variant.malformed.group.title" +msgstr "Certains variants ont des noms invalides" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:560 +msgid "workspace.options.component.variant.malformed.locate" +msgstr "Localiser les variants invalides" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:251 +msgid "workspace.options.component.variant.malformed.single.all" +msgstr "Ces variants ont des noms invalides." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:248 +msgid "workspace.options.component.variant.malformed.single.one" +msgstr "Ce variant a un nom invalide." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:254 +msgid "workspace.options.component.variant.malformed.single.some" +msgstr "Certains variants ont des noms invalides." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:433 +msgid "workspace.options.component.variant.malformed.structure.example" +msgstr "[propriété]=[valeur], [propriété]=[valeur]" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:431 +msgid "workspace.options.component.variant.malformed.structure.title" +msgstr "Utilise la structure suivante :" + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:54 +msgid "workspace.options.component.variants-help-modal.intro" +msgstr "" +"Pour maintenir les changements entre les variants, Penpot connecte les " +"calques qui :" + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:91 +msgid "workspace.options.component.variants-help-modal.outro" +msgstr "" +"Chaque changement (ex. renommer ou grouper un calque) brise la connection. " +"Défaire le changement va la restaurer." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:67 +msgid "workspace.options.component.variants-help-modal.rule1" +msgstr "Ont le même nom." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:76 +msgid "workspace.options.component.variants-help-modal.rule2" +msgstr "Sont du même type." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:77 +msgid "workspace.options.component.variants-help-modal.rule2.detail" +msgstr "" +"Les rectangles, ellipses, chemins et opérations booléennes comptent comme " +"étant du même type." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:87 +msgid "workspace.options.component.variants-help-modal.rule3" +msgstr "Ont le même niveau de hiérarchie." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:88 +msgid "workspace.options.component.variants-help-modal.rule3.detail" +msgstr "Les groupes, tableaux et mises en page sont considérés équivalents." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1045, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1289, src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:47 +msgid "workspace.options.component.variants-help-modal.title" +msgstr "Comment les variants demeurent connectés" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:164 +msgid "workspace.options.constraints" +msgstr "Contraintes" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:151 +msgid "workspace.options.constraints.bottom" +msgstr "Bas" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:142, src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:153 +msgid "workspace.options.constraints.center" +msgstr "Centre" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:224 +msgid "workspace.options.constraints.fix-when-scrolling" +msgstr "Figer au défilement" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:139 +msgid "workspace.options.constraints.left" +msgstr "Gauche" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:141 +msgid "workspace.options.constraints.leftright" +msgstr "Gauche et droite" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:140 +msgid "workspace.options.constraints.right" +msgstr "Droite" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:143, src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:154 +msgid "workspace.options.constraints.scale" +msgstr "Redimensionner" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:150 +msgid "workspace.options.constraints.top" +msgstr "Haut" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:152 +msgid "workspace.options.constraints.topbottom" +msgstr "Haut et bas" + +#: src/app/main/ui/workspace/sidebar/options.cljs:197 +msgid "workspace.options.design" +msgstr "Design" + +#: src/app/main/ui/inspect/exports.cljs:140 +msgid "workspace.options.export" +msgstr "Exporter" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#, unused +msgid "workspace.options.export-multiple" +msgstr "Exporter la sélection" + +#: src/app/main/ui/inspect/exports.cljs:196, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs:273 +msgid "workspace.options.export-object" +msgid_plural "workspace.options.export-object" +msgstr[0] "Exporter 1 élément" +msgstr[1] "Exporter %s éléments" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs:214 +msgid "workspace.options.export.add-export" +msgstr "Ajouter une exportation" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs:226, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs:261 +msgid "workspace.options.export.remove-export" +msgstr "Supprimer l'exportation" + +#: src/app/main/ui/inspect/exports.cljs:179, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs:255 +msgid "workspace.options.export.suffix" +msgstr "Suffixe" + +#: src/app/main/ui/exports/assets.cljs:250 +msgid "workspace.options.exporting-complete" +msgstr "Exportation complétée" + +#: src/app/main/ui/exports/assets.cljs:171, src/app/main/ui/exports/assets.cljs:251, src/app/main/ui/inspect/exports.cljs:195, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs:272 +msgid "workspace.options.exporting-object" +msgstr "En cours d'exportation…" + +#: src/app/main/ui/exports/assets.cljs:249 +msgid "workspace.options.exporting-object-error" +msgstr "L'exportation a échoué" + +#: src/app/main/ui/exports/assets.cljs:252 +msgid "workspace.options.exporting-object-slow" +msgstr "Exportation inopinément lente" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:107, src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:236 +msgid "workspace.options.fill" +msgstr "Remplissage" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:208 +msgid "workspace.options.fill.add-fill" +msgstr "Ajouter un remplissage" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:223 +msgid "workspace.options.fill.remove-fill" +msgstr "Supprimer le remplissage" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:405 +msgid "workspace.options.fit-content" +msgstr "Redimensionner le tableau au contenu" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:704 +msgid "workspace.options.flows.add-flow-start" +msgstr "Ajouter un début de parcours" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:700 +msgid "workspace.options.flows.flow" +msgstr "Parcours" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:164 +msgid "workspace.options.flows.flow-start" +msgstr "Démarrer le parcours" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:672 +msgid "workspace.options.flows.flow-starts" +msgstr "Départs de parcours" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:155 +#, unused +msgid "workspace.options.flows.remove-flow" +msgstr "Supprimer le parcours" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:32 +msgid "workspace.options.grid.auto" +msgstr "Auto" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:163 +msgid "workspace.options.grid.column" +msgstr "Colonnes" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +#, unused +msgid "workspace.options.grid.grid-title" +msgstr "Grille" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:204, src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:247 +msgid "workspace.options.grid.params.color" +msgstr "Couleur" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +#, unused +msgid "workspace.options.grid.params.columns" +msgstr "Colonnes" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:270 +msgid "workspace.options.grid.params.gutter" +msgstr "Gouttière" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:257 +msgid "workspace.options.grid.params.height" +msgstr "Hauteur" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:281 +msgid "workspace.options.grid.params.margin" +msgstr "Marge" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +#, unused +msgid "workspace.options.grid.params.rows" +msgstr "Rangées" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:226, src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:302 +msgid "workspace.options.grid.params.set-default" +msgstr "Définir par défaut" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +#, unused +msgid "workspace.options.grid.params.size" +msgstr "Taille" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +#, unused +msgid "workspace.options.grid.params.type" +msgstr "Type" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:241 +msgid "workspace.options.grid.params.type.bottom" +msgstr "Bas" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:239 +msgid "workspace.options.grid.params.type.center" +msgstr "Centre" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:238 +msgid "workspace.options.grid.params.type.left" +msgstr "Gauche" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:242 +msgid "workspace.options.grid.params.type.right" +msgstr "Droite" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:235 +msgid "workspace.options.grid.params.type.stretch" +msgstr "Étirer" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:237 +msgid "workspace.options.grid.params.type.top" +msgstr "Haut" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:221, src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:300 +msgid "workspace.options.grid.params.use-default" +msgstr "Utiliser la valeur par défaut" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:258 +msgid "workspace.options.grid.params.width" +msgstr "Largeur" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:164 +msgid "workspace.options.grid.row" +msgstr "Rangées" From 06bb2b98a99f8b4880ff1f93d8b95a4987513ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Mon, 16 Feb 2026 17:25:03 +0100 Subject: [PATCH 006/288] :globe_with_meridians: Add translations for: Turkish Currently translated at 99.8% (2070 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/ --- frontend/translations/tr.po | 77 +++++++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index b2095b04d0..3501e863d2 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -1,15 +1,15 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-16 08:35+0000\n" -"Last-Translator: Anonymous \n" -"Language-Team: Turkish " -"\n" +"PO-Revision-Date: 2026-02-17 10:10+0000\n" +"Last-Translator: Oğuz Ersen \n" +"Language-Team: Turkish \n" "Language: tr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.16-dev\n" +"X-Generator: Weblate 5.16\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -2097,7 +2097,6 @@ msgid "inspect.tabs.styles.active-themes" msgstr "Etkin temalar" #: src/app/main/ui/inspect/styles/style_box.cljs:68 -#, fuzzy msgid "inspect.tabs.styles.copy-shorthand" msgstr "CSS kısaltmasını panoya kopyala" @@ -2940,7 +2939,7 @@ msgstr "Sürüm %s notları" #: src/app/main/ui/workspace/sidebar/sitemap.cljs:271 msgid "labels.view-only" -msgstr "YALNIZCA GÖRÜNTÜLE" +msgstr "Yalnızca görüntüle" #: src/app/main/ui/dashboard/team.cljs:131, src/app/main/ui/dashboard/team.cljs:314, src/app/main/ui/dashboard/team.cljs:567, src/app/main/ui/dashboard/team.cljs:603, src/app/main/ui/onboarding/team_choice.cljs:56 msgid "labels.viewer" @@ -5016,7 +5015,6 @@ msgid "subscription.settings.sucess.dialog.title" msgstr "%s oldunuz!" #: src/app/main/ui/settings/subscription.cljs:526 -#, fuzzy msgid "subscription.settings.support-us-since" msgstr "Bu planla bizi şu zamandan beri destekliyorsunuz: %s" @@ -8161,7 +8159,6 @@ msgid "workspace.tokens.opacity-range" msgstr "Opaklık 0 ile %100 veya 0 ile 1 arasında olmalıdır (örneğin %50 veya 0.5)." #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 -#, fuzzy msgid "workspace.tokens.original-value" msgstr "Orijinal değer: %s" @@ -8196,7 +8193,6 @@ msgid "workspace.tokens.remapping-in-progress" msgstr "Token referansları yeniden eşleniyor..." #: src/app/main/data/workspace/tokens/warnings.cljs:15, src/app/main/data/workspace/tokens/warnings.cljs:19, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:56, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:84, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:103, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:285, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:459, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:176, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:311, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:251, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:364, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:465, src/app/main/ui/workspace/tokens/management/token_pill.cljs:122 -#, fuzzy msgid "workspace.tokens.resolved-value" msgstr "Çözülen değer: %s" @@ -8343,7 +8339,6 @@ msgid "workspace.tokens.themes-list" msgstr "Tema listesi" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:275, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:276 -#, fuzzy msgid "workspace.tokens.token-description" msgstr "Açıklama" @@ -8752,3 +8747,63 @@ msgstr "Otomatik kaydedilen sürümler %s gün boyunca saklanacaktır." #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Yolu kapatmak için tıklayın" + +#: src/app/main/ui/dashboard/sidebar.cljs:347 +msgid "dashboard.create-new-org" +msgstr "Yeni organizasyon oluştur" + +#: src/app/main/errors.cljs:105 +msgid "errors.unexpected-exception" +msgstr "Beklenmeyen hata: %s" + +#: src/app/main/ui/static.cljs:315 +msgid "errors.webgl-context-lost.desc-message" +msgstr "" +"WebGL çalışmayı durdurdu. Sıfırlamak için lütfen sayfayı yeniden yükleyin" + +#: src/app/main/ui/static.cljs:314 +msgid "errors.webgl-context-lost.main-message" +msgstr "Oops! Tuval içeriği kayboldu" + +#: src/app/main/ui/static.cljs:318 +msgid "labels.reload-page" +msgstr "Sayfayı yeniden yükle" + +#: src/app/main/ui/workspace/sidebar/debug.cljs:38 +msgid "workspace.debug.title" +msgstr "Hata ayıklama araçları" + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:303 +msgid "workspace.tokens.missing-reference" +msgstr "Eksik referans" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:95 +msgid "workspace.tokens.not-remap" +msgstr "Yeniden eşlenmesin" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:99 +msgid "workspace.tokens.remap" +msgstr "Tokenleri yeniden eşle" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:86 +msgid "workspace.tokens.remap-token-references-title" +msgstr "`%s` kullanan tüm tokenler `%s` olarak yeniden eşlensin mi?" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88 +msgid "workspace.tokens.remap-warning-effects" +msgstr "" +"Bu, eski token adını kullanan tüm katmanları ve referansları değiştirecektir." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89 +msgid "workspace.tokens.remap-warning-time" +msgstr "Bu işlem biraz zaman alabilir." + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +#, unused +msgid "workspace.tokens.warning-name-change" +msgstr "" +"Bu tokenin adını değiştirmek, eski adına yapılan tüm referansları bozacaktır" + +#: src/app/main/ui/workspace/top_toolbar.cljs:231 +msgid "workspace.toolbar.debug" +msgstr "Hata ayıklama araçları" From 89f0e282ec047d4b4209b393441148424c8d27b3 Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Wed, 18 Feb 2026 09:13:59 +0100 Subject: [PATCH 007/288] :globe_with_meridians: Add translations for: Latvian Currently translated at 90.9% (1887 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/ --- frontend/translations/lv.po | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/frontend/translations/lv.po b/frontend/translations/lv.po index 7f018c3b36..8171e93c37 100644 --- a/frontend/translations/lv.po +++ b/frontend/translations/lv.po @@ -1,16 +1,16 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-16 08:35+0000\n" -"Last-Translator: \"Ņikita K.\" \n" -"Language-Team: Latvian " -"\n" +"PO-Revision-Date: 2026-02-18 14:09+0000\n" +"Last-Translator: Edgars Andersons \n" +"Language-Team: Latvian \n" "Language: lv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n % 10 == 0 || n % 100 >= 11 && n % 100 " -"<= 19) ? 0 : ((n % 10 == 1 && n % 100 != 11) ? 1 : 2);\n" -"X-Generator: Weblate 5.16-dev\n" +"Plural-Forms: nplurals=3; plural=(n % 10 == 0 || n % 100 >= 11 && n % 100 <= " +"19) ? 0 : ((n % 10 == 1 && n % 100 != 11) ? 1 : 2);\n" +"X-Generator: Weblate 5.16.1-dev\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -6274,39 +6274,39 @@ msgstr "Papildu opcijas" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:686, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:693 msgid "workspace.options.layout-item.layout-item-max-h" -msgstr "Maks.augstums" +msgstr "Lielākais augstums" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:624, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:631 msgid "workspace.options.layout-item.layout-item-max-w" -msgstr "Maks.platums" +msgstr "Lielākais platums" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:655, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:663 msgid "workspace.options.layout-item.layout-item-min-h" -msgstr "Min.augstums" +msgstr "Mazākais augstums" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:591, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:600 msgid "workspace.options.layout-item.layout-item-min-w" -msgstr "Min.platums" +msgstr "Mazākais platums" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs #, unused msgid "workspace.options.layout-item.title.layout-item-max-h" -msgstr "Maksimālais augstums" +msgstr "Lielākais augstums" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs #, unused msgid "workspace.options.layout-item.title.layout-item-max-w" -msgstr "Maksimālais platums" +msgstr "Lielākais platums" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs #, unused msgid "workspace.options.layout-item.title.layout-item-min-h" -msgstr "Minimālais augstums" +msgstr "Mazākais augstums" #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs #, unused msgid "workspace.options.layout-item.title.layout-item-min-w" -msgstr "Minimālais platums" +msgstr "Mazākais platums" #: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs #, unused From 010074df4b213976b7fcd931ab5be1a9991ff9ff Mon Sep 17 00:00:00 2001 From: Alexis Morin Date: Wed, 18 Feb 2026 14:58:05 +0100 Subject: [PATCH 008/288] :globe_with_meridians: Add translations for: French (Canada) Currently translated at 73.8% (1532 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/ --- frontend/translations/fr_CA.po | 177 ++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 2 deletions(-) diff --git a/frontend/translations/fr_CA.po b/frontend/translations/fr_CA.po index 47ebf81527..cf2eecbc8b 100644 --- a/frontend/translations/fr_CA.po +++ b/frontend/translations/fr_CA.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-17 10:10+0000\n" +"PO-Revision-Date: 2026-02-18 14:09+0000\n" "Last-Translator: Alexis Morin \n" "Language-Team: French (Canada) \n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 5.16\n" +"X-Generator: Weblate 5.16.1-dev\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -6402,3 +6402,176 @@ msgstr "Largeur" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:164 msgid "workspace.options.grid.row" msgstr "Rangées" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:162 +msgid "workspace.options.grid.square" +msgstr "Carrée" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:106 +msgid "workspace.options.group-fill" +msgstr "Remplissage du groupe" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:45 +msgid "workspace.options.group-stroke" +msgstr "Contour du groupe" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:332 +msgid "workspace.options.guides.add-guide" +msgstr "Ajouter un guide" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:191 +msgid "workspace.options.guides.remove-guide" +msgstr "Supprimer le guide" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:329 +msgid "workspace.options.guides.title" +msgstr "Guides" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:187 +msgid "workspace.options.guides.toggle-guide" +msgstr "Basculer le guide" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:435, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:454 +msgid "workspace.options.height" +msgstr "Hauteur" + +#: src/app/main/ui/workspace/sidebar/options.cljs:201 +msgid "workspace.options.inspect" +msgstr "Inspection" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:438 +msgid "workspace.options.interaction-action" +msgstr "Action" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:43, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:344 +msgid "workspace.options.interaction-after-delay" +msgstr "Après un délai" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:578 +msgid "workspace.options.interaction-animation" +msgstr "Animation" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:616 +msgid "workspace.options.interaction-animation-direction-down" +msgstr "Vers le bas" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:593 +msgid "workspace.options.interaction-animation-direction-in" +msgstr "Entrante" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:612 +msgid "workspace.options.interaction-animation-direction-left" +msgstr "Vers la gauche" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:596 +msgid "workspace.options.interaction-animation-direction-out" +msgstr "Sortante" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:608 +msgid "workspace.options.interaction-animation-direction-right" +msgstr "Vers la droite" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:620 +msgid "workspace.options.interaction-animation-direction-up" +msgstr "Vers le haut" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:383 +msgid "workspace.options.interaction-animation-dissolve" +msgstr "Fondu" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:382 +msgid "workspace.options.interaction-animation-none" +msgstr "Aucune" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:389 +msgid "workspace.options.interaction-animation-push" +msgstr "Pousser" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:384 +msgid "workspace.options.interaction-animation-slide" +msgstr "Glisser" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:368 +msgid "workspace.options.interaction-auto" +msgstr "automatique" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:568 +msgid "workspace.options.interaction-background" +msgstr "Ajouter un recouvrement d'arrière-plan" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:561 +msgid "workspace.options.interaction-close-outside" +msgstr "Fermer en cliquant à l'extérieur" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:349 +msgid "workspace.options.interaction-close-overlay" +msgstr "Fermer le recouvrement" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:58 +msgid "workspace.options.interaction-close-overlay-dest" +msgstr "Fermer le recouvrement : %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:426 +msgid "workspace.options.interaction-delay" +msgstr "Délai" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:449 +msgid "workspace.options.interaction-destination" +msgstr "Destination" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:628 +msgid "workspace.options.interaction-duration" +msgstr "Durée" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:641 +msgid "workspace.options.interaction-easing" +msgstr "Lissage de vitesse" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:393 +msgid "workspace.options.interaction-easing-ease" +msgstr "Accélération" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:394 +msgid "workspace.options.interaction-easing-ease-in" +msgstr "Accélération progressive" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:396 +msgid "workspace.options.interaction-easing-ease-in-out" +msgstr "Accélération progressive-dégressive" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:395 +msgid "workspace.options.interaction-easing-ease-out" +msgstr "Accélération dégressive" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:392 +msgid "workspace.options.interaction-easing-linear" +msgstr "Linéaire" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +#, unused +msgid "workspace.options.interaction-in" +msgstr "Entrante" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:41, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:341 +msgid "workspace.options.interaction-mouse-enter" +msgstr "Survol de souris" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:42, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:342 +msgid "workspace.options.interaction-mouse-leave" +msgstr "Sortie de souris" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:430, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:632 +msgid "workspace.options.interaction-ms" +msgstr "ms" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:346 +msgid "workspace.options.interaction-navigate-to" +msgstr "Naviguer vers" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:52 +msgid "workspace.options.interaction-navigate-to-dest" +msgstr "Naviguer vers : %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:53, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:55, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:57, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:357 +msgid "workspace.options.interaction-none" +msgstr "(non défini)" From b4db1df62f53521ea7fe94510b8d05394ef8b0fc Mon Sep 17 00:00:00 2001 From: Egor Filatov Date: Wed, 18 Feb 2026 13:14:40 +0100 Subject: [PATCH 009/288] :globe_with_meridians: Add translations for: Russian Currently translated at 78.7% (1634 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/ --- frontend/translations/ru.po | 260 ++++++++++++++++++++++++++++++++++-- 1 file changed, 250 insertions(+), 10 deletions(-) diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index 0f0b3ec780..7fac6fb5e1 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -1,15 +1,15 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-16 08:35+0000\n" -"Last-Translator: The_BadUser \n" -"Language-Team: Russian " -"\n" +"PO-Revision-Date: 2026-02-18 14:09+0000\n" +"Last-Translator: Egor Filatov \n" +"Language-Team: Russian \n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Weblate 5.16-dev\n" +"X-Generator: Weblate 5.16.1-dev\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -676,7 +676,7 @@ msgstr "Файлы с ошибками загружены не будут." msgid "dashboard.import.import-message" msgid_plural "dashboard.import.import-message" msgstr[0] "1 файл был успешно импортирован." -msgstr[1] "Успешно импортировано файлов: %s" +msgstr[1] "%s файлов было успешно импортировано." #: src/app/main/ui/dashboard/import.cljs:474 msgid "dashboard.import.import-warning" @@ -5289,19 +5289,19 @@ msgstr "Поведение" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:393 msgid "workspace.options.interaction-easing-ease" -msgstr "Ease" +msgstr "Плавно" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:394 msgid "workspace.options.interaction-easing-ease-in" -msgstr "Ease in" +msgstr "Ускорение" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:396 msgid "workspace.options.interaction-easing-ease-in-out" -msgstr "Ease in out" +msgstr "Ускорение и замедление" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:395 msgid "workspace.options.interaction-easing-ease-out" -msgstr "Ease out" +msgstr "Замедление" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:392 msgid "workspace.options.interaction-easing-linear" @@ -6691,3 +6691,243 @@ msgstr "Автосохранённые версии будут хранитьс #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Нажмите для замыкания контура" + +#: src/app/main/ui/dashboard/deleted.cljs:313 +msgid "dashboard.clear-trash-button" +msgstr "Очистить корзину" + +#: src/app/main/ui/dashboard/deleted.cljs:262 +msgid "dashboard.delete-all-forever-confirmation.description" +msgstr "" +"Вы уверены, что хотите навсегда удалить все удаленные проекты и файлы? Это " +"необратимое действие." + +#: src/app/main/ui/dashboard/file_menu.cljs:221 +msgid "dashboard.delete-file-forever-confirmation.description" +msgstr "Вы уверены, что хотите навсегда удалить %s? Это необратимое действие." + +#: src/app/main/data/dashboard.cljs:778 +msgid "dashboard.delete-files-success-notification" +msgstr "%s файлов успешно удалено." + +#: src/app/main/ui/dashboard/deleted.cljs:51, src/app/main/ui/dashboard/deleted.cljs:53, src/app/main/ui/dashboard/deleted.cljs:261, src/app/main/ui/dashboard/deleted.cljs:263, src/app/main/ui/dashboard/file_menu.cljs:220, src/app/main/ui/dashboard/file_menu.cljs:222 +msgid "dashboard.delete-forever-confirmation.title" +msgstr "Удалить навсегда" + +#: src/app/main/ui/dashboard/deleted.cljs:85 +msgid "dashboard.delete-project-button" +msgstr "Удалить проект" + +#: src/app/main/ui/dashboard/deleted.cljs:52 +msgid "dashboard.delete-project-forever-confirmation.description" +msgstr "" +"Вы уверены, что хотите удалить проект %s навсегда? Вы собираетесь удалить " +"его и все находящиеся в нем файлы. Это необратимое действие." + +#: src/app/main/data/dashboard.cljs:777, src/app/main/data/dashboard.cljs:811 +msgid "dashboard.delete-success-notification" +msgstr "%s успешно удален." + +#: src/app/main/ui/dashboard/deleted.cljs:327 +msgid "dashboard.deleted.empty-state-description" +msgstr "Ваша корзина пуста. Удаленные файлы и проекты появятся здесь." + +#: src/app/main/ui/dashboard/grid.cljs:248 +msgid "dashboard.deleted.will-be-deleted-at" +msgstr "Будет удален %s" + +#, unused +msgid "dashboard.errors.error-on-delete-file" +msgstr "Произошла ошибка во время удаления файла %s." + +#: src/app/main/data/dashboard.cljs:781 +msgid "dashboard.errors.error-on-delete-files" +msgstr "Произошла ошибка во время удаления файлов." + +#: src/app/main/data/dashboard.cljs:814 +msgid "dashboard.errors.error-on-delete-project" +msgstr "Произошла ошибка во время удаления проекта %s." + +#: src/app/main/data/dashboard.cljs:909, src/app/main/ui/dashboard/file_menu.cljs:201 +msgid "dashboard.errors.error-on-restore-file" +msgstr "Произошла ошибка во время восстановления файла %s." + +#: src/app/main/data/dashboard.cljs:910 +msgid "dashboard.errors.error-on-restore-files" +msgstr "Произошла ошибка во время восстановления файлов." + +#: src/app/main/data/dashboard.cljs:942 +msgid "dashboard.errors.error-on-restoring-project" +msgstr "Произошла ошибка во время восстановления проекта %s и его файлов." + +#: src/app/main/ui/dashboard/file_menu.cljs:266 +msgid "dashboard.file-menu.delete-files-permanently-option" +msgid_plural "dashboard.file-menu.delete-files-permanently-option" +msgstr[0] "Удаленный файл" +msgstr[1] "Удаленный файлы" + +#: src/app/main/ui/dashboard/file_menu.cljs:263 +msgid "dashboard.file-menu.restore-files-option" +msgid_plural "dashboard.file-menu.restore-files-option" +msgstr[0] "Восстановить файл" +msgstr[1] "Восстановить файлы" + +#: src/app/main/data/dashboard.cljs:722 +msgid "dashboard.progress-notification.deleting-files" +msgstr "Удаление файлов…" + +#: src/app/main/data/dashboard.cljs:843 +msgid "dashboard.progress-notification.restoring-files" +msgstr "Восстановление файлов…" + +#: src/app/main/ui/dashboard/deleted.cljs:274 +msgid "dashboard.restore-all-confirmation.description" +msgstr "" +"Вы собираетесь восстановить все ваши проекты и файлы. Это может занять " +"некоторое время." + +#: src/app/main/ui/dashboard/deleted.cljs:273 +msgid "dashboard.restore-all-confirmation.title" +msgstr "Восстановить все проекты и файлы" + +#: src/app/main/ui/dashboard/deleted.cljs:308 +msgid "dashboard.restore-all-deleted-button" +msgstr "Восстановить все" + +#: src/app/main/data/dashboard.cljs:903 +#, fuzzy +msgid "dashboard.restore-files-success-notification" +msgstr "файлы %s были успешно восстановлены." + +#: src/app/main/ui/dashboard/deleted.cljs:82 +msgid "dashboard.restore-project-button" +msgstr "Восстановить проект" + +#: src/app/main/ui/dashboard/deleted.cljs:41 +msgid "dashboard.restore-project-confirmation.description" +msgstr "Вы собираетесь восстановить проект %s и все файлы в нем." + +#: src/app/main/ui/dashboard/deleted.cljs:40 +msgid "dashboard.restore-project-confirmation.title" +msgstr "Восстановить проект" + +#: src/app/main/data/dashboard.cljs:875, src/app/main/data/dashboard.cljs:902, src/app/main/data/dashboard.cljs:939, src/app/main/ui/dashboard/file_menu.cljs:198 +msgid "dashboard.restore-success-notification" +msgstr "%s был успешно восстановлен." + +#: src/app/main/ui/dashboard/deleted.cljs:298 +msgid "dashboard.trash-info-text-part1" +msgstr "Удаленные файлы останутся в корзине в течение" + +#: src/app/main/ui/dashboard/deleted.cljs:300 +msgid "dashboard.trash-info-text-part2" +msgstr " %s дней. " + +#: src/app/main/ui/dashboard/deleted.cljs:301 +msgid "dashboard.trash-info-text-part3" +msgstr "После этого они будут удалены навсегда." + +#: src/app/main/ui/comments.cljs:530 +msgid "comments.mentions.not-found" +msgstr "Людей не найдено для @%s" + +#: src/app/main/ui/dashboard/deleted.cljs:303 +msgid "dashboard.trash-info-text-part4" +msgstr "" +"Если вы передумаете, вы можете восстановить их или удалить навсегда через " +"меню каждого файла." + +#: src/app/main/errors.cljs:305 +msgid "errors.deprecated.contact.after" +msgstr "чтобы мы могли вам помочь." + +#: src/app/main/errors.cljs:200 +#, fuzzy, unused +msgid "errors.internal-assertion-error" +msgstr "Ошибка внутренней сертификации" + +#: src/app/main/errors.cljs:105 +msgid "errors.unexpected-exception" +msgstr "Неожиданная ошибка: %s" + +#: src/app/main/ui/static.cljs:314 +#, fuzzy +msgid "errors.webgl-context-lost.main-message" +msgstr "Упс! Контекст холста был потерян" + +#: src/app/main/ui/dashboard/team.cljs:933 +msgid "team.invitations-selected" +msgid_plural "team.invitations-selected" +msgstr[0] "1 приглашение выбрано" +msgstr[1] "%s приглашений выбрано" + +#: src/app/main/ui/workspace/libraries.cljs:100, src/app/main/ui/workspace/libraries.cljs:126 +msgid "workspace.libraries.colors" +msgid_plural "workspace.libraries.colors" +msgstr[0] "1 цвет" +msgstr[1] "%s цветов" + +#: src/app/main/ui/workspace/libraries.cljs:94, src/app/main/ui/workspace/libraries.cljs:118 +msgid "workspace.libraries.components" +msgid_plural "workspace.libraries.components" +msgstr[0] "1 компонент" +msgstr[1] "%s компонентов" + +#: src/app/main/data/dashboard.cljs:723 +msgid "dashboard.progress-notification.slow-delete" +msgstr "Удаление идет медленнее ожидаемого" + +#: src/app/main/data/dashboard.cljs:844 +msgid "dashboard.progress-notification.slow-restore" +msgstr "Восстановление идет неожиданно медленно" + +#: src/app/main/ui/static.cljs:315 +msgid "errors.webgl-context-lost.desc-message" +msgstr "" +"WebGL прекратил работу. Пожалуйста, перезагрузите страницу чтобы сбросить его" + +#: src/app/main/ui/settings/feedback.cljs:126 +msgid "feedback.penpot.link" +msgstr "" +"Если обратная связь связана с файлом или проектом, оставьте ссылку на файл " +"penpot здесь:" + +#: src/app/main/ui/exports/files.cljs:124 +msgid "files-download-modal.title" +msgstr "Скачать файлы" + +#: src/app/main/ui/inspect/right_sidebar.cljs:170 +msgid "inspect.color-space-label" +msgstr "Выберите цветовое пространство" + +#: src/app/main/ui/inspect/right_sidebar.cljs:238 +msgid "inspect.empty.more" +msgstr "Больше информации" + +#: src/app/main/ui/inspect/right_sidebar.cljs:110 +msgid "labels.computed" +msgstr "Вычисленные" + +#: src/app/main/ui/static.cljs:415 +msgid "labels.contact-support" +msgstr "Связаться с поддержкой" + +#: src/app/main/ui/dashboard/deleted.cljs:215 +msgid "labels.deleted" +msgstr "Удаленные" + +#: src/app/main/ui/settings/feedback.cljs:134, src/app/main/ui/static.cljs:409 +msgid "labels.download" +msgstr "Скачать %s" + +#: src/app/main/ui/static.cljs:405 +msgid "labels.internal-error.desc-message-first" +msgstr "Что-то пошло не так." + +#: src/app/main/ui/dashboard/file_menu.cljs:208 +msgid "dashboard-restore-file-confirmation.description" +msgstr "Вы собираетесь восстановить %s." + +#: src/app/main/ui/dashboard/file_menu.cljs:207 +msgid "dashboard-restore-file-confirmation.title" +msgstr "Восстановить файл" From 0e182cff18522ab02cc049e25b5093ca016bba81 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Wed, 18 Feb 2026 14:54:50 +0100 Subject: [PATCH 010/288] :globe_with_meridians: Add translations for: Hebrew Currently translated at 99.3% (2060 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/ --- frontend/translations/he.po | 180 +++++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 5 deletions(-) diff --git a/frontend/translations/he.po b/frontend/translations/he.po index 45f92edd68..795755bc3d 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-17 10:09+0000\n" +"PO-Revision-Date: 2026-02-18 14:09+0000\n" "Last-Translator: Yaron Shahrabani \n" "Language-Team: Hebrew \n" @@ -10,7 +10,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && " "n % 10 == 0) ? 2 : 3));\n" -"X-Generator: Weblate 5.16\n" +"X-Generator: Weblate 5.16.1-dev\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -1902,7 +1902,6 @@ msgid "inspect.tabs.styles.active-themes" msgstr "ערכות עיצוב פעילות" #: src/app/main/ui/inspect/styles/style_box.cljs:68 -#, fuzzy msgid "inspect.tabs.styles.copy-shorthand" msgstr "העתקת קיצור CSS ללוח הגזירים" @@ -4789,7 +4788,6 @@ msgid "subscription.settings.sucess.dialog.title" msgstr "התוכנית שלך היא %s!" #: src/app/main/ui/settings/subscription.cljs:526 -#, fuzzy msgid "subscription.settings.support-us-since" msgstr "תמכת בנו עם התוכנית הזאת מאז: %s" @@ -7839,7 +7837,6 @@ msgid "workspace.tokens.opacity-range" msgstr "שקיפות צריכה להיות בין 0 ל־100% או 0 ו־1 (כלומר 50% או 0.5)." #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 -#, fuzzy msgid "workspace.tokens.original-value" msgstr "ערך מקורי: %s" @@ -8511,3 +8508,176 @@ msgstr "%s קבצים שוחזרו בהצלחה." #: src/app/main/ui/dashboard/deleted.cljs:82 msgid "dashboard.restore-project-button" msgstr "שחזור מיזם" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:465 +msgid "workspace.layout-item.fix-height" +msgstr "גובה קבוע" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:439 +msgid "workspace.layout-item.fix-width" +msgstr "רוחב קבוע" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:471 +msgid "workspace.layout-item.height-100" +msgstr "גובה 100%" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:444 +msgid "workspace.layout-item.width-100" +msgstr "רוחב 100%" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:616 +msgid "workspace.options.interaction-animation-direction-down" +msgstr "למטה" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:593 +msgid "workspace.options.interaction-animation-direction-in" +msgstr "פנימה" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:612 +msgid "workspace.options.interaction-animation-direction-left" +msgstr "שמאלה" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:596 +msgid "workspace.options.interaction-animation-direction-out" +msgstr "החוצה" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:608 +msgid "workspace.options.interaction-animation-direction-right" +msgstr "ימינה" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:620 +msgid "workspace.options.interaction-animation-direction-up" +msgstr "למעלה" + +#: src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs:108, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:401 +msgid "workspace.options.orientation.horizontal" +msgstr "אופקי" + +#: src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs:104, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:397 +msgid "workspace.options.orientation.vertical" +msgstr "אנכי" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:297 +msgid "workspace.sidebar.layers.filter" +msgstr "מסנן" + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:303 +msgid "workspace.tokens.missing-reference" +msgstr "הפניה חסרה" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:87 +#, unused +msgid "workspace.tokens.no-references-found" +msgstr "לא נמצאו הפניות" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +#, unused +msgid "workspace.tokens.no-remap-needed" +msgstr "האסימון (token) הזה לא בשימוש בעיצוב שלך, לא צריך מיפוי מחדש." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:95 +msgid "workspace.tokens.not-remap" +msgstr "לא למפות מחדש" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:99 +msgid "workspace.tokens.remap" +msgstr "מיפוי אסימונים מחדש" + +#: src/app/main/ui/dashboard/deleted.cljs:41 +msgid "dashboard.restore-project-confirmation.description" +msgstr "הפעולה הזאת תשחזר את המיזם %s ואת כל הקבצים שבו." + +#: src/app/main/ui/dashboard/deleted.cljs:40 +msgid "dashboard.restore-project-confirmation.title" +msgstr "שחזור מיזם" + +#: src/app/main/data/dashboard.cljs:875, src/app/main/data/dashboard.cljs:902, src/app/main/data/dashboard.cljs:939, src/app/main/ui/dashboard/file_menu.cljs:198 +msgid "dashboard.restore-success-notification" +msgstr "%s שוחזר בהצלחה." + +#: src/app/main/ui/dashboard/deleted.cljs:298 +msgid "dashboard.trash-info-text-part1" +msgstr "קבצים שנמחקו יישארו באשפה למשך" + +#: src/app/main/ui/dashboard/deleted.cljs:300 +msgid "dashboard.trash-info-text-part2" +msgstr " %s ימים. " + +#: src/app/main/ui/dashboard/deleted.cljs:301 +msgid "dashboard.trash-info-text-part3" +msgstr "לאחר מכן, הם יימחקו לצמיתות." + +#: src/app/main/ui/dashboard/deleted.cljs:303 +msgid "dashboard.trash-info-text-part4" +msgstr "" +"אם התחרטת, אפשר לשחזר אותם או למחוק אותם לצמיתות מהתפריט של כל אחד מהקבצים." + +#: src/app/main/errors.cljs:105 +msgid "errors.unexpected-exception" +msgstr "שגיאה מפתיעה: %s" + +#: src/app/main/ui/static.cljs:315 +msgid "errors.webgl-context-lost.desc-message" +msgstr "WebGL הפסיק לעבוד. נא לרענן את העמוד כדי לאפס אותו" + +#: src/app/main/ui/static.cljs:314 +msgid "errors.webgl-context-lost.main-message" +msgstr "אופס! הקשר לוח הציור אבד" + +#: src/app/main/ui/exports/files.cljs:124 +msgid "files-download-modal.title" +msgstr "הורדת קבצים" + +#: src/app/main/ui/dashboard/deleted.cljs:215 +msgid "labels.deleted" +msgstr "נמחקה" + +#: src/app/main/ui/dashboard/deleted.cljs:208 +msgid "labels.recent" +msgstr "אחרונות" + +#: src/app/main/ui/static.cljs:318 +msgid "labels.reload-page" +msgstr "ריענון העמוד" + +#: src/app/main/ui/viewer/header.cljs:187 +msgid "viewer.header.edit-in-workspace" +msgstr "עריכה במרחב העבודה" + +#: src/app/main/ui/workspace/colorpicker.cljs:434 +msgid "workspace.colorpicker.get-color" +msgstr "משיכת צבע" + +#: src/app/main/ui/workspace/sidebar/debug.cljs:38 +msgid "workspace.debug.title" +msgstr "כלי ניפוי שגיאות" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:422 +msgid "workspace.layout-grid.editor.margin.expand" +msgstr "הצגת אפשרויות גבול מ־4 צדדים" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:449 +msgid "workspace.layout-item.fit-content-horizontal" +msgstr "התאמת תוכן (אופקית)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:477 +msgid "workspace.layout-item.fit-content-vertical" +msgstr "התאמת תוכן (אנכית)" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:86 +msgid "workspace.tokens.remap-token-references-title" +msgstr "למפות את כל האסימונים שמשתמשים ב־`%s` ל־`%s`?" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88 +msgid "workspace.tokens.remap-warning-effects" +msgstr "" +"הפעולה הזאת תשנה את כל השכבות וההפניות שמשתמשות בשם הישן של האסימון (token)." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89 +msgid "workspace.tokens.remap-warning-time" +msgstr "הפעולה הזאת עלולה לארוך זמן מה." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:92 +#, unused +msgid "workspace.tokens.remapping-in-progress" +msgstr "הפניות האסימון (token) ממופות מחדש…" From 7df10e2238b5ee5ee91fc923401aaa20276c8984 Mon Sep 17 00:00:00 2001 From: Alexis Morin Date: Thu, 19 Feb 2026 18:38:33 +0100 Subject: [PATCH 011/288] :globe_with_meridians: Add translations for: French (Canada) Currently translated at 84.2% (1748 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/ --- frontend/translations/fr_CA.po | 917 ++++++++++++++++++++++++++++++++- 1 file changed, 916 insertions(+), 1 deletion(-) diff --git a/frontend/translations/fr_CA.po b/frontend/translations/fr_CA.po index cf2eecbc8b..2620b1d545 100644 --- a/frontend/translations/fr_CA.po +++ b/frontend/translations/fr_CA.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-18 14:09+0000\n" +"PO-Revision-Date: 2026-02-20 03:09+0000\n" "Last-Translator: Alexis Morin \n" "Language-Team: French (Canada) \n" @@ -6575,3 +6575,918 @@ msgstr "Naviguer vers : %s" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:53, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:55, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:57, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:357 msgid "workspace.options.interaction-none" msgstr "(non défini)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:654 +msgid "workspace.options.interaction-offset-effect" +msgstr "Effet de décalage" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:37, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:337 +msgid "workspace.options.interaction-on-click" +msgstr "Au clique" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:347 +msgid "workspace.options.interaction-open-overlay" +msgstr "Ouvrir en superposition" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:54 +msgid "workspace.options.interaction-open-overlay-dest" +msgstr "Ouvrir en superposition : %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:61, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:351 +msgid "workspace.options.interaction-open-url" +msgstr "Ouvrir URL" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +#, unused +msgid "workspace.options.interaction-out" +msgstr "Sortante" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:380, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:554 +msgid "workspace.options.interaction-pos-bottom-center" +msgstr "En bas au centre" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:378, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:538 +msgid "workspace.options.interaction-pos-bottom-left" +msgstr "En bas à gauche" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:379, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:546 +msgid "workspace.options.interaction-pos-bottom-right" +msgstr "En bas à droite" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:374, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:506 +msgid "workspace.options.interaction-pos-center" +msgstr "Centre" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:373 +msgid "workspace.options.interaction-pos-manual" +msgstr "Manuelle" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:377, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:530 +msgid "workspace.options.interaction-pos-top-center" +msgstr "En haut au centre" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:375, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:514 +msgid "workspace.options.interaction-pos-top-left" +msgstr "En haut à gauche" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:376, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:522 +msgid "workspace.options.interaction-pos-top-right" +msgstr "En haut à droite" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:492 +msgid "workspace.options.interaction-position" +msgstr "Position" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:460 +msgid "workspace.options.interaction-preserve-scroll" +msgstr "Maintenir le défilement" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:60, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:350 +msgid "workspace.options.interaction-prev-screen" +msgstr "Écran précédent" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:482 +msgid "workspace.options.interaction-relative-to" +msgstr "Relatif à" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:59, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:356, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:370, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:371 +msgid "workspace.options.interaction-self" +msgstr "soi-même" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:348 +msgid "workspace.options.interaction-toggle-overlay" +msgstr "Basculer la superposition" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:56 +msgid "workspace.options.interaction-toggle-overlay-dest" +msgstr "Basculer la superposition : %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:415 +msgid "workspace.options.interaction-trigger" +msgstr "Déclencheur" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:469 +msgid "workspace.options.interaction-url" +msgstr "URL" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:39, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:339 +msgid "workspace.options.interaction-while-hovering" +msgstr "Pendant le survol" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:40, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:340 +msgid "workspace.options.interaction-while-pressing" +msgstr "Pendant l'appui" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:743 +msgid "workspace.options.interactions" +msgstr "Interactions" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:746 +msgid "workspace.options.interactions.add-interaction" +msgstr "Ajouter une interaction" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +#, unused +msgid "workspace.options.interactions.remove-interaction" +msgstr "Supprimer l'interaction" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:197 +msgid "workspace.options.layer-options.blend-mode.color" +msgstr "Couleur" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:186 +msgid "workspace.options.layer-options.blend-mode.color-burn" +msgstr "Densité couleur +" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:189 +msgid "workspace.options.layer-options.blend-mode.color-dodge" +msgstr "Densité couleur -" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:184 +msgid "workspace.options.layer-options.blend-mode.darken" +msgstr "Obscurcir" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:193 +msgid "workspace.options.layer-options.blend-mode.difference" +msgstr "Différence" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:194 +msgid "workspace.options.layer-options.blend-mode.exclusion" +msgstr "Exclusion" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:192 +msgid "workspace.options.layer-options.blend-mode.hard-light" +msgstr "Lumière crue" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:195 +msgid "workspace.options.layer-options.blend-mode.hue" +msgstr "Teinte" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:187 +msgid "workspace.options.layer-options.blend-mode.lighten" +msgstr "Éclaircir" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:198 +msgid "workspace.options.layer-options.blend-mode.luminosity" +msgstr "Luminosité" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:185 +msgid "workspace.options.layer-options.blend-mode.multiply" +msgstr "Produit" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:183 +msgid "workspace.options.layer-options.blend-mode.normal" +msgstr "Normal" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:190 +msgid "workspace.options.layer-options.blend-mode.overlay" +msgstr "Incrustation" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:196 +msgid "workspace.options.layer-options.blend-mode.saturation" +msgstr "Saturation" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:188 +msgid "workspace.options.layer-options.blend-mode.screen" +msgstr "Superposition" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:191 +msgid "workspace.options.layer-options.blend-mode.soft-light" +msgstr "Lumière tamisée" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +#, unused +msgid "workspace.options.layer-options.title" +msgstr "Calque" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +#, unused +msgid "workspace.options.layer-options.title.group" +msgstr "Calques du groupe" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +#, unused +msgid "workspace.options.layer-options.title.multiple" +msgstr "Calques sélectionnés" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:255, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:261 +msgid "workspace.options.layer-options.toggle-layer" +msgstr "Basculer la visibilité du calque" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout-item.advanced-ops" +msgstr "Options avancées" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:686, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:693 +msgid "workspace.options.layout-item.layout-item-max-h" +msgstr "Hauteur max." + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:624, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:631 +msgid "workspace.options.layout-item.layout-item-max-w" +msgstr "Largeur max." + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:655, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:663 +msgid "workspace.options.layout-item.layout-item-min-h" +msgstr "Hauteur min." + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:591, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:600 +msgid "workspace.options.layout-item.layout-item-min-w" +msgstr "Largeur min." + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout-item.title.layout-item-max-h" +msgstr "Hauteur maximale" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout-item.title.layout-item-max-w" +msgstr "Largeur maximale" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout-item.title.layout-item-min-h" +msgstr "Hauteur minimale" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout-item.title.layout-item-min-w" +msgstr "Largeur minimale" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.bottom" +msgstr "Bas" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.direction.column" +msgstr "Colonne" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.direction.column-reverse" +msgstr "Colonne inversée" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.direction.row" +msgstr "Rangée" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.direction.row-reverse" +msgstr "Rangée inversée" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.gap" +msgstr "Gouttière" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.left" +msgstr "Gauche" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout.margin" +msgstr "Marge" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout.margin-all" +msgstr "Tous côtés" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout.margin-simple" +msgstr "Marge simple" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.packed" +msgstr "compacté" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.padding" +msgstr "Marge interne" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.padding-all" +msgstr "Tous côtés" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.padding-simple" +msgstr "Marge interne simple" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.right" +msgstr "Droite" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.space-around" +msgstr "espace autour" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.space-between" +msgstr "espace entre" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.top" +msgstr "Haut" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:241 +msgid "workspace.options.more-colors" +msgstr "Plus de couleurs" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:223 +msgid "workspace.options.more-lib-colors" +msgstr "Plus de couleurs de bibliothèque" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:265 +msgid "workspace.options.more-token-colors" +msgstr "Plus de tokens de couleur" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:229, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:241 +msgid "workspace.options.opacity" +msgstr "Opacité" + +#: src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs:108, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:401 +msgid "workspace.options.orientation.horizontal" +msgstr "Horizontal" + +#: src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs:104, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:397 +msgid "workspace.options.orientation.vertical" +msgstr "Vertical" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#, unused +msgid "workspace.options.position" +msgstr "Position" + +#: src/app/main/ui/workspace/sidebar/options.cljs:199 +msgid "workspace.options.prototype" +msgstr "Prototype" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:182, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:206 +msgid "workspace.options.radius" +msgstr "Rayon" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:271, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:323 +msgid "workspace.options.radius-bottom-left" +msgstr "En bas à gauche" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:290, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:331 +msgid "workspace.options.radius-bottom-right" +msgstr "En bas à droite" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:234, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:307 +msgid "workspace.options.radius-top-left" +msgstr "En haut à gauche" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:253, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:315 +msgid "workspace.options.radius-top-right" +msgstr "En haut à droite" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:340 +msgid "workspace.options.radius.hide-all-corners" +msgstr "Réduire les rayons indépendants" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:341 +msgid "workspace.options.radius.show-single-corners" +msgstr "Afficher les rayons indépendants" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:191 +msgid "workspace.options.recent-fonts" +msgstr "Récentes" + +#: src/app/main/ui/exports/assets.cljs:298 +msgid "workspace.options.retry" +msgstr "Relancer" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:536, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:544 +msgid "workspace.options.rotation" +msgstr "Rotation" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:188 +msgid "workspace.options.search-font" +msgstr "Recherche de police" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:786 +msgid "workspace.options.select-a-shape" +msgstr "" +"Sélectionner une forme, un tableau ou un groupe pour ensuite glisser une " +"connexion à un autre tableau." + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:201 +msgid "workspace.options.selection-color" +msgstr "Couleurs de la sélection" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:105 +msgid "workspace.options.selection-fill" +msgstr "Remplissage de la sélection" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:44 +msgid "workspace.options.selection-stroke" +msgstr "Contour de la sélection" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs:144 +msgid "workspace.options.shadow-options.add-shadow" +msgstr "Ajouter une ombre" + +#: src/app/main/ui/inspect/attributes/shadow.cljs:47, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:181, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:183 +msgid "workspace.options.shadow-options.blur" +msgstr "Flou" + +#: src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:211 +msgid "workspace.options.shadow-options.color" +msgstr "Couleur de l'ombre" + +#: src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:122 +msgid "workspace.options.shadow-options.drop-shadow" +msgstr "Ombre portée" + +#: src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:123 +msgid "workspace.options.shadow-options.inner-shadow" +msgstr "Ombre interne" + +#: src/app/main/ui/inspect/attributes/shadow.cljs:45, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:172 +msgid "workspace.options.shadow-options.offsetx" +msgstr "X" + +#: src/app/main/ui/inspect/attributes/shadow.cljs:46, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:201 +msgid "workspace.options.shadow-options.offsety" +msgstr "Y" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs:157, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:161 +msgid "workspace.options.shadow-options.remove-shadow" +msgstr "Supprimer l'ombre" + +#: src/app/main/ui/inspect/attributes/shadow.cljs:48, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:191, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:193 +msgid "workspace.options.shadow-options.spread" +msgstr "Étalement" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs:139 +msgid "workspace.options.shadow-options.title" +msgstr "Ombre" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs:138 +msgid "workspace.options.shadow-options.title.group" +msgstr "Ombre du groupe" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs:137 +msgid "workspace.options.shadow-options.title.multiple" +msgstr "Ombre de la sélection" + +#: src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:157 +msgid "workspace.options.shadow-options.toggle-shadow" +msgstr "Basculer l'ombre" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:258 +msgid "workspace.options.show-fill-on-export" +msgstr "Afficher dans l'exportation" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:574 +msgid "workspace.options.show-in-viewer" +msgstr "Afficher en mode visionnement" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:168 +msgid "workspace.options.size" +msgstr "Taille" + +#: src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs:71, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:364 +msgid "workspace.options.size-presets" +msgstr "Tailles prédéfinies" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:469 +msgid "workspace.options.size.lock" +msgstr "Verrouiller les proportions" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:469 +msgid "workspace.options.size.unlock" +msgstr "Déverrouiller les proportions" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:44 +#, unused +msgid "workspace.options.stroke" +msgstr "Contour" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +#, unused +msgid "workspace.options.stroke-cap.circle-marker" +msgstr "Marqueur en cercle" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:175 +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "Cercle" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +#, unused +msgid "workspace.options.stroke-cap.diamond-marker" +msgstr "Marquer en diamant" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:176 +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "Diamant" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +#, unused +msgid "workspace.options.stroke-cap.line-arrow" +msgstr "Flèche de ligne" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:172 +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "Flèche" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:170 +msgid "workspace.options.stroke-cap.none" +msgstr "Aucun" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:178 +msgid "workspace.options.stroke-cap.round" +msgstr "Arrondi" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:179 +msgid "workspace.options.stroke-cap.square" +msgstr "Carré" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +#, unused +msgid "workspace.options.stroke-cap.square-marker" +msgstr "Marqueur en carré" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:174 +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "Rectangle" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +#, unused +msgid "workspace.options.stroke-cap.triangle-arrow" +msgstr "Flèche en triangle" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:173 +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "Triangle" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:210 +msgid "workspace.options.stroke-color" +msgstr "Couleur du contour" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:225, src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:230 +msgid "workspace.options.stroke-width" +msgstr "Largeur du contour" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:189 +msgid "workspace.options.stroke.add-stroke" +msgstr "Ajouter un contour" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:111 +msgid "workspace.options.stroke.center" +msgstr "Centre" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:139 +msgid "workspace.options.stroke.dashed" +msgstr "Tirets" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:138 +msgid "workspace.options.stroke.dotted" +msgstr "Pointillés" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:112 +msgid "workspace.options.stroke.inner" +msgstr "Intérieur" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:140 +msgid "workspace.options.stroke.mixed" +msgstr "Mixte" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:113 +msgid "workspace.options.stroke.outer" +msgstr "Extérieur" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:202 +msgid "workspace.options.stroke.remove-stroke" +msgstr "Supprimer le contour" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:137 +msgid "workspace.options.stroke.solid" +msgstr "Plein" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:127 +msgid "workspace.options.text-options.align-bottom" +msgstr "Aligner en bas" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:123 +msgid "workspace.options.text-options.align-middle" +msgstr "Aligner au centre" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:119 +msgid "workspace.options.text-options.align-top" +msgstr "Aligner en haut" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:92 +msgid "workspace.options.text-options.direction-ltr" +msgstr "Gauche à droite" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:96 +msgid "workspace.options.text-options.direction-rtl" +msgstr "Droite à gauche" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:165 +msgid "workspace.options.text-options.grow-auto-height" +msgstr "Hauteur automatique" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:161 +msgid "workspace.options.text-options.grow-auto-width" +msgstr "Largeur automatique" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:157 +msgid "workspace.options.text-options.grow-fixed" +msgstr "Fixe" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:400 +msgid "workspace.options.text-options.letter-spacing" +msgstr "Interlettrage" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:381 +msgid "workspace.options.text-options.line-height" +msgstr "Hauteur de ligne" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#, unused +msgid "workspace.options.text-options.lowercase" +msgstr "Minuscules" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#, unused +msgid "workspace.options.text-options.none" +msgstr "Aucune" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:192 +msgid "workspace.options.text-options.strikethrough" +msgstr "Barré (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:61 +msgid "workspace.options.text-options.text-align-center" +msgstr "Aligner au centre" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:69 +msgid "workspace.options.text-options.text-align-justify" +msgstr "Justifier" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:57 +msgid "workspace.options.text-options.text-align-left" +msgstr "Aligner à gauche" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:65 +msgid "workspace.options.text-options.text-align-right" +msgstr "Aligner à droite" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:205 +msgid "workspace.options.text-options.title" +msgstr "Texte" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:204 +msgid "workspace.options.text-options.title-group" +msgstr "Texte du groupe" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:203 +msgid "workspace.options.text-options.title-selection" +msgstr "Texte de la sélection" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#, unused +msgid "workspace.options.text-options.titlecase" +msgstr "Titré" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:188 +msgid "workspace.options.text-options.underline" +msgstr "Souligné (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#, unused +msgid "workspace.options.text-options.uppercase" +msgstr "Majuscules" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:788 +msgid "workspace.options.use-play-button" +msgstr "" +"Utiliser le bouton de lecture dans le menu du haut pour voir le prototype." + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:420, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:442 +msgid "workspace.options.width" +msgstr "Largeur" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:482, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:505 +msgid "workspace.options.x" +msgstr "Axe X" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:495, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:516 +msgid "workspace.options.y" +msgstr "Axe Y" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:140 +msgid "workspace.path.actions.add-node" +msgstr "Ajouter un nœud (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:148 +msgid "workspace.path.actions.delete-node" +msgstr "Supprimer les nœuds (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:123 +msgid "workspace.path.actions.draw-nodes" +msgstr "Dessiner des nœuds (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:166 +msgid "workspace.path.actions.join-nodes" +msgstr "Connecter les nœuds (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:184 +msgid "workspace.path.actions.make-corner" +msgstr "Convertir en coin (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:192 +msgid "workspace.path.actions.make-curve" +msgstr "Convertir en courbe (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:158 +msgid "workspace.path.actions.merge-nodes" +msgstr "Fusionner les nœuds (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:131 +msgid "workspace.path.actions.move-nodes" +msgstr "Déplacer les nœuds (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:174 +msgid "workspace.path.actions.separate-nodes" +msgstr "Séparer les nœuds (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:203 +msgid "workspace.path.actions.snap-nodes" +msgstr "S'accrocher aux nœuds (%s)" + +#: src/app/main/ui/workspace/plugins.cljs:85 +msgid "workspace.plugins.button-open" +msgstr "Ouvrir" + +#: src/app/main/ui/workspace/plugins.cljs:199 +#, markdown +msgid "workspace.plugins.discover" +msgstr "Découvrir [d'autres plugiciels](%s)" + +#: src/app/main/ui/workspace/plugins.cljs:206 +msgid "workspace.plugins.empty-plugins" +msgstr "Aucun plugiciel installé" + +#: src/app/main/ui/workspace/plugins.cljs:193 +msgid "workspace.plugins.error.manifest" +msgstr "Le manifeste du plugiciel est invalide." + +#: src/app/main/data/plugins.cljs:105, src/app/main/ui/workspace/main_menu.cljs:766, src/app/main/ui/workspace/plugins.cljs:84 +msgid "workspace.plugins.error.need-editor" +msgstr "Tu dois être un éditeur pour utiliser ce plugiciel" + +#: src/app/main/ui/workspace/plugins.cljs:189 +msgid "workspace.plugins.error.url" +msgstr "Le plugiciel n'existe pas ou l'URL est invalide." + +#: src/app/main/ui/workspace/plugins.cljs:185 +msgid "workspace.plugins.install" +msgstr "Installer" + +#: src/app/main/ui/workspace/plugins.cljs:215 +msgid "workspace.plugins.installed-plugins" +msgstr "Plugiciels installés" + +#: src/app/main/ui/workspace/main_menu.cljs:721 +msgid "workspace.plugins.menu.plugins-manager" +msgstr "Gestionnaire de plugiciels" + +#: src/app/main/ui/workspace/main_menu.cljs:918 +msgid "workspace.plugins.menu.title" +msgstr "Plugiciels" + +#: src/app/main/ui/workspace/plugins.cljs:376 +msgid "workspace.plugins.permissions-update.title" +msgstr "MISE À JOUR DU PLUGICIEL" + +#: src/app/main/ui/workspace/plugins.cljs:380 +msgid "workspace.plugins.permissions-update.warning" +msgstr "" +"Ce plugiciel a été modifié depuis sa dernière ouverture. Des accès " +"supplémentaires sont demandés :" + +#: src/app/main/ui/workspace/plugins.cljs:280 +msgid "workspace.plugins.permissions.allow-download" +msgstr "Démarrer des téléchargements." + +#: src/app/main/ui/workspace/plugins.cljs:287 +msgid "workspace.plugins.permissions.allow-localstorage" +msgstr "Stocker des données dans le navigateur." + +#: src/app/main/ui/workspace/plugins.cljs:273 +msgid "workspace.plugins.permissions.comment-read" +msgstr "Lire tes commentaires et réponses." + +#: src/app/main/ui/workspace/plugins.cljs:267 +msgid "workspace.plugins.permissions.comment-write" +msgstr "Lire et modifier tes commentaires et répondre en ton nom." + +#: src/app/main/ui/workspace/plugins.cljs:240 +msgid "workspace.plugins.permissions.content-read" +msgstr "Lire le contenu de fichiers auxquels des usagers ont accès." + +#: src/app/main/ui/workspace/plugins.cljs:234 +msgid "workspace.plugins.permissions.content-write" +msgstr "Lire et modifier le contenu de fichiers auxquels des usagers ont accès." + +#: src/app/main/ui/workspace/plugins.cljs:327 +msgid "workspace.plugins.permissions.disclaimer" +msgstr "" +"Noter que ce plugiciel a été créé par une tierce partie. S'assurer d'y faire " +"confiance avant de permettre l'accès. La confidentialité des données est " +"importante pour nous. Merci de contacter le support en cas de question." + +#: src/app/main/ui/workspace/plugins.cljs:260 +msgid "workspace.plugins.permissions.library-read" +msgstr "Lire tes bibliothèques et atouts." + +#: src/app/main/ui/workspace/plugins.cljs:254 +msgid "workspace.plugins.permissions.library-write" +msgstr "Lire et modifier tes bibliothèques et atouts." + +#: src/app/main/ui/workspace/plugins.cljs:320 +msgid "workspace.plugins.permissions.title" +msgstr "LE PLUGICIEL « %s » DEMANDE D'ACCÉDER À :" + +#: src/app/main/ui/workspace/plugins.cljs:247 +msgid "workspace.plugins.permissions.user-read" +msgstr "Lire l'information de l'usager actuel." + +#: src/app/main/ui/workspace/plugins.cljs:211 +msgid "workspace.plugins.plugin-list-link" +msgstr "Liste des plugiciels" + +#: src/app/main/ui/workspace/plugins.cljs:88 +msgid "workspace.plugins.remove-plugin" +msgstr "Supprimer le plugiciel" + +#: src/app/main/ui/workspace/plugins.cljs:180 +msgid "workspace.plugins.search-placeholder" +msgstr "Inscrire l'URL d'un plugiciel" + +#, unused +msgid "workspace.plugins.success" +msgstr "Plugiciel chargé." + +#: src/app/main/ui/workspace/plugins.cljs:174 +msgid "workspace.plugins.title" +msgstr "Plugiciels" + +#: src/app/main/ui/workspace/plugins.cljs:440 +msgid "workspace.plugins.try-out.cancel" +msgstr "PAS MAINTENANT" + +#: src/app/main/ui/workspace/plugins.cljs:433 +msgid "workspace.plugins.try-out.message" +msgstr "" +"Tu veux jetter un coup d'œil ? Ça ouvrira un nouveau brouillon dans ton " +"équipe actuelle. (Autrement, tu peux accéder aux plugiciels installés dans " +"n'importe quel fichier.)" + +#: src/app/main/ui/workspace/plugins.cljs:429 +msgid "workspace.plugins.try-out.title" +msgstr "LE PLUGICIEL « %s » EST INSTALLÉ POUR TON USAGER!" + +#: src/app/main/ui/workspace/plugins.cljs:446 +msgid "workspace.plugins.try-out.try" +msgstr "ESSAYER LE PLUGICIEL" + +#: src/app/main/ui/workspace/context_menu.cljs:559 +msgid "workspace.shape.menu.add-flex" +msgstr "Ajouter une disposition flex" + +#: src/app/main/ui/workspace/context_menu.cljs:563 +msgid "workspace.shape.menu.add-grid" +msgstr "Ajouter une disposition en grille" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1248, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1272 +msgid "workspace.shape.menu.add-layout" +msgstr "Ajouter une disposition" + +#: src/app/main/ui/workspace/context_menu.cljs:612, src/app/main/ui/workspace/sidebar/assets/common.cljs:508, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1051, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1219, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1293 +msgid "workspace.shape.menu.add-variant" +msgstr "Créer un variant" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:512, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1073, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1221, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1307 +msgid "workspace.shape.menu.add-variant-property" +msgstr "Ajouter une propriété" From 4c416b7c189699fffb6d5c9c9ba58037ac88776d Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Thu, 19 Feb 2026 10:59:36 +0100 Subject: [PATCH 012/288] :globe_with_meridians: Add translations for: Hebrew Currently translated at 99.7% (2068 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/ --- frontend/translations/he.po | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/frontend/translations/he.po b/frontend/translations/he.po index 795755bc3d..66a8f53b94 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-18 14:09+0000\n" +"PO-Revision-Date: 2026-02-20 03:09+0000\n" "Last-Translator: Yaron Shahrabani \n" "Language-Team: Hebrew \n" @@ -7866,7 +7866,6 @@ msgid "workspace.tokens.reference-error" msgstr "שגיאות הפניה: " #: src/app/main/data/workspace/tokens/warnings.cljs:15, src/app/main/data/workspace/tokens/warnings.cljs:19, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:56, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:84, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:103, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:285, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:459, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:176, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:311, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:251, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:364, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:465, src/app/main/ui/workspace/tokens/management/token_pill.cljs:122 -#, fuzzy msgid "workspace.tokens.resolved-value" msgstr "ערך פתור: %s" @@ -7982,7 +7981,6 @@ msgid "workspace.tokens.themes-list" msgstr "רשימת ערכות עיצוב" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:275, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:276 -#, fuzzy msgid "workspace.tokens.token-description" msgstr "תיאור" @@ -8681,3 +8679,31 @@ msgstr "הפעולה הזאת עלולה לארוך זמן מה." #, unused msgid "workspace.tokens.remapping-in-progress" msgstr "הפניות האסימון (token) ממופות מחדש…" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:281 +msgid "workspace.tokens.shadow-token-blur-value-error" +msgstr "ערך הטשטוש לא יכול להיות שלילי" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:287 +#, unused +msgid "workspace.tokens.shadow-token-spread-value-error" +msgstr "ערך הפריסה לא יכול להיות שלילי" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:100 +#, unused +msgid "workspace.tokens.theme.disable" +msgstr "כיבוי" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:96 +#, unused +msgid "workspace.tokens.theme.enable" +msgstr "הפעלה" + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +#, unused +msgid "workspace.tokens.warning-name-change" +msgstr "שינוי השם של האסימון (token) הזה יפגום בהפניות לשם הישן שלו" + +#: src/app/main/ui/workspace/top_toolbar.cljs:231 +msgid "workspace.toolbar.debug" +msgstr "כלי ניפוי שגיאות" From 41003310f882aa4ba5e22c55424d01356bb136ce Mon Sep 17 00:00:00 2001 From: Alexis Morin Date: Fri, 20 Feb 2026 15:01:45 +0100 Subject: [PATCH 013/288] :globe_with_meridians: Add translations for: French (Canada) Currently translated at 85.3% (1771 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/ --- frontend/translations/fr_CA.po | 94 +++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/frontend/translations/fr_CA.po b/frontend/translations/fr_CA.po index 2620b1d545..87d403c196 100644 --- a/frontend/translations/fr_CA.po +++ b/frontend/translations/fr_CA.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-20 03:09+0000\n" +"PO-Revision-Date: 2026-02-21 14:09+0000\n" "Last-Translator: Alexis Morin \n" "Language-Team: French (Canada) \n" @@ -7490,3 +7490,95 @@ msgstr "Créer un variant" #: src/app/main/ui/workspace/sidebar/assets/common.cljs:512, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1073, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1221, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1307 msgid "workspace.shape.menu.add-variant-property" msgstr "Ajouter une propriété" + +#: src/app/main/ui/workspace/context_menu.cljs:282 +msgid "workspace.shape.menu.back" +msgstr "Envoyer à l'arrière-plan" + +#: src/app/main/ui/workspace/context_menu.cljs:279 +msgid "workspace.shape.menu.backward" +msgstr "Envoyer vers l'arrière" + +#: src/app/main/ui/workspace/context_menu.cljs:619, src/app/main/ui/workspace/sidebar/assets/components.cljs:633, src/app/main/ui/workspace/sidebar/assets/groups.cljs:75, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1106 +msgid "workspace.shape.menu.combine-as-variants" +msgstr "Combiner comme variants" + +#: src/app/main/ui/workspace/sidebar/assets/components.cljs:635 +msgid "workspace.shape.menu.combine-as-variants-error" +msgstr "Les composants doivent être sur la même page" + +#: src/app/main/ui/workspace/context_menu.cljs:200 +msgid "workspace.shape.menu.copy" +msgstr "Copier" + +#: src/app/main/ui/workspace/context_menu.cljs:218 +msgid "workspace.shape.menu.copy-css" +msgstr "Copier en CSS" + +#: src/app/main/ui/workspace/context_menu.cljs:220 +msgid "workspace.shape.menu.copy-css-nested" +msgstr "Copier en CSS (calques imbriqués)" + +#: src/app/main/ui/workspace/context_menu.cljs:203 +msgid "workspace.shape.menu.copy-link" +msgstr "Copier le lien" + +#: src/app/main/ui/workspace/context_menu.cljs:216 +msgid "workspace.shape.menu.copy-paste-as" +msgstr "Copier/coller en tant que ..." + +#: src/app/main/ui/workspace/context_menu.cljs:230 +msgid "workspace.shape.menu.copy-props" +msgstr "Copier les propriétés" + +#: src/app/main/ui/workspace/context_menu.cljs:222 +msgid "workspace.shape.menu.copy-svg" +msgstr "Copier en SVG" + +#: src/app/main/ui/workspace/context_menu.cljs:227 +msgid "workspace.shape.menu.copy-text" +msgstr "Copier en texte" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:484 +msgid "workspace.shape.menu.create-annotation" +msgstr "Créer une annotation" + +#: src/app/main/ui/workspace/context_menu.cljs:382 +msgid "workspace.shape.menu.create-artboard-from-selection" +msgstr "Entableauter la sélection" + +#: src/app/main/ui/workspace/context_menu.cljs:592 +msgid "workspace.shape.menu.create-component" +msgstr "Créer un composant" + +#: src/app/main/ui/workspace/context_menu.cljs:596 +msgid "workspace.shape.menu.create-multiple-components" +msgstr "Créer plusieurs composants" + +#: src/app/main/ui/workspace/context_menu.cljs:206 +msgid "workspace.shape.menu.cut" +msgstr "Couper" + +#: src/app/main/ui/workspace/context_menu.cljs:629, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1001, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1290 +msgid "workspace.shape.menu.delete" +msgstr "Supprimer" + +#: src/app/main/ui/workspace/context_menu.cljs:506 +msgid "workspace.shape.menu.delete-flow-start" +msgstr "Supprimer le début de parcours" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:489 +msgid "workspace.shape.menu.detach-instance" +msgstr "Détacher l'instance" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:488 +msgid "workspace.shape.menu.detach-instances-in-bulk" +msgstr "Détacher les instances" + +#: src/app/main/ui/workspace/context_menu.cljs:447, src/app/main/ui/workspace/sidebar/options/menus/bool.cljs:88 +msgid "workspace.shape.menu.difference" +msgstr "Différence" + +#: src/app/main/ui/workspace/context_menu.cljs:212 +msgid "workspace.shape.menu.duplicate" +msgstr "Dupliquer" From 5770a1fdc9739ba9d6c60d0275d0ef3e86cfece6 Mon Sep 17 00:00:00 2001 From: Alexis Morin Date: Sat, 21 Feb 2026 19:26:17 +0100 Subject: [PATCH 014/288] :globe_with_meridians: Add translations for: French (Canada) Currently translated at 86.5% (1796 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/ --- frontend/translations/fr_CA.po | 105 ++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/frontend/translations/fr_CA.po b/frontend/translations/fr_CA.po index 87d403c196..ce49b1799a 100644 --- a/frontend/translations/fr_CA.po +++ b/frontend/translations/fr_CA.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-21 14:09+0000\n" +"PO-Revision-Date: 2026-02-22 16:09+0000\n" "Last-Translator: Alexis Morin \n" "Language-Team: French (Canada) \n" @@ -4663,7 +4663,7 @@ msgstr "Basculer les calques" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:190 msgid "shortcuts.toggle-layout-flex" -msgstr "Ajouter / retirer mise en page flex" +msgstr "Ajouter / retirer disposition flex" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:191 msgid "shortcuts.toggle-layout-grid" @@ -7582,3 +7582,104 @@ msgstr "Différence" #: src/app/main/ui/workspace/context_menu.cljs:212 msgid "workspace.shape.menu.duplicate" msgstr "Dupliquer" + +#: src/app/main/ui/workspace/context_menu.cljs:432 +msgid "workspace.shape.menu.edit" +msgstr "Modifier" + +#: src/app/main/ui/workspace/context_menu.cljs:453, src/app/main/ui/workspace/sidebar/options/menus/bool.cljs:98 +msgid "workspace.shape.menu.exclude" +msgstr "Exclusion" + +#: src/app/main/ui/workspace/context_menu.cljs:437, src/app/main/ui/workspace/context_menu.cljs:460, src/app/main/ui/workspace/sidebar/options/menus/bool.cljs:104 +msgid "workspace.shape.menu.flatten" +msgstr "Aplatir" + +#: src/app/main/ui/workspace/context_menu.cljs:299 +msgid "workspace.shape.menu.flip-horizontal" +msgstr "Retourner horizontalement" + +#: src/app/main/ui/workspace/context_menu.cljs:295 +msgid "workspace.shape.menu.flip-vertical" +msgstr "Retourner verticalement" + +#: src/app/main/ui/workspace/context_menu.cljs:508 +msgid "workspace.shape.menu.flow-start" +msgstr "Début de parcours" + +#: src/app/main/ui/workspace/context_menu.cljs:273 +msgid "workspace.shape.menu.forward" +msgstr "Emmener vers l'avant" + +#: src/app/main/ui/workspace/context_menu.cljs:276 +msgid "workspace.shape.menu.front" +msgstr "Emmener à l'avant-plan" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#, unused +msgid "workspace.shape.menu.go-main" +msgstr "Aller au fichier du composant principal" + +#: src/app/main/ui/workspace/context_menu.cljs:368 +msgid "workspace.shape.menu.group" +msgstr "Grouper" + +#: src/app/main/ui/workspace/context_menu.cljs:477, src/app/main/ui/workspace/sidebar/layer_item.cljs:172 +msgid "workspace.shape.menu.hide" +msgstr "Afficher/Masquer" + +#: src/app/main/ui/workspace/context_menu.cljs:706, src/app/main/ui/workspace/main_menu.cljs:448 +msgid "workspace.shape.menu.hide-ui" +msgstr "Afficher / cacher l'interface utilisateur" + +#: src/app/main/ui/workspace/context_menu.cljs:450, src/app/main/ui/workspace/sidebar/options/menus/bool.cljs:93 +msgid "workspace.shape.menu.intersection" +msgstr "Intersection" + +#: src/app/main/ui/workspace/context_menu.cljs:485, src/app/main/ui/workspace/sidebar/layer_item.cljs:180, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:268 +msgid "workspace.shape.menu.lock" +msgstr "Verrouiller" + +#: src/app/main/ui/workspace/context_menu.cljs:373 +msgid "workspace.shape.menu.mask" +msgstr "Utiliser comme masque" + +#: src/app/main/ui/workspace/context_menu.cljs:209, src/app/main/ui/workspace/context_menu.cljs:703 +msgid "workspace.shape.menu.paste" +msgstr "Coller" + +#: src/app/main/ui/workspace/context_menu.cljs:234 +msgid "workspace.shape.menu.paste-props" +msgstr "Coller les propriétés" + +#: src/app/main/ui/workspace/context_menu.cljs:443 +msgid "workspace.shape.menu.path" +msgstr "Opérations de chemin" + +#: src/app/main/ui/workspace/context_menu.cljs:549 +msgid "workspace.shape.menu.remove-flex" +msgstr "Supprimer la disposition flex" + +#: src/app/main/ui/workspace/context_menu.cljs:552 +msgid "workspace.shape.menu.remove-grid" +msgstr "Supprimer la disposition grille" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1266 +msgid "workspace.shape.menu.remove-layout" +msgstr "Supprimer la disposition" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1157 +msgid "workspace.shape.menu.remove-variant-property" +msgstr "Supprimer la propriété" + +#: src/app/main/ui/workspace/context_menu.cljs:329 +msgid "workspace.shape.menu.rename" +msgstr "Renommer" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:493 +msgid "workspace.shape.menu.reset-overrides" +msgstr "Annuler les modifications" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:499 +msgid "workspace.shape.menu.restore-main" +msgstr "Restaurer le composant principal" From db2689efc96920304a70bd8d7f6d3ce50ef1ddfa Mon Sep 17 00:00:00 2001 From: Alexis Morin Date: Tue, 24 Feb 2026 02:11:53 +0100 Subject: [PATCH 015/288] :globe_with_meridians: Add translations for: French (Canada) Currently translated at 91.1% (1891 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/ --- frontend/translations/fr_CA.po | 419 ++++++++++++++++++++++++++++++++- 1 file changed, 417 insertions(+), 2 deletions(-) diff --git a/frontend/translations/fr_CA.po b/frontend/translations/fr_CA.po index ce49b1799a..fea09045c3 100644 --- a/frontend/translations/fr_CA.po +++ b/frontend/translations/fr_CA.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-22 16:09+0000\n" +"PO-Revision-Date: 2026-02-24 02:09+0000\n" "Last-Translator: Alexis Morin \n" "Language-Team: French (Canada) \n" @@ -7626,7 +7626,7 @@ msgstr "Grouper" #: src/app/main/ui/workspace/context_menu.cljs:477, src/app/main/ui/workspace/sidebar/layer_item.cljs:172 msgid "workspace.shape.menu.hide" -msgstr "Afficher/Masquer" +msgstr "Masquer" #: src/app/main/ui/workspace/context_menu.cljs:706, src/app/main/ui/workspace/main_menu.cljs:448 msgid "workspace.shape.menu.hide-ui" @@ -7683,3 +7683,418 @@ msgstr "Annuler les modifications" #: src/app/main/ui/workspace/sidebar/assets/common.cljs:499 msgid "workspace.shape.menu.restore-main" msgstr "Restaurer le composant principal" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:498 +msgid "workspace.shape.menu.restore-variant" +msgstr "Restaurer le variant" + +#: src/app/main/ui/workspace/context_menu.cljs:263 +msgid "workspace.shape.menu.select-layer" +msgstr "Sélectionner le calque" + +#: src/app/main/ui/workspace/context_menu.cljs:474, src/app/main/ui/workspace/sidebar/layer_item.cljs:171 +msgid "workspace.shape.menu.show" +msgstr "Afficher" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:481, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1217 +msgid "workspace.shape.menu.show-in-assets" +msgstr "Afficher dans le panneau d'atouts" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:502, src/app/main/ui/workspace/sidebar/assets/components.cljs:629 +msgid "workspace.shape.menu.show-main" +msgstr "Afficher le composant principal" + +#: src/app/main/ui/workspace/context_menu.cljs:314 +msgid "workspace.shape.menu.thumbnail-remove" +msgstr "Supprimer l'imagette" + +#: src/app/main/ui/workspace/context_menu.cljs:316 +msgid "workspace.shape.menu.thumbnail-set" +msgstr "Définir en tant qu'imagette" + +#: src/app/main/ui/workspace/context_menu.cljs:436 +#, unused +msgid "workspace.shape.menu.transform-to-path" +msgstr "Transformer en chemin" + +#: src/app/main/ui/workspace/context_menu.cljs:364 +msgid "workspace.shape.menu.ungroup" +msgstr "Dégroupper" + +#: src/app/main/ui/workspace/context_menu.cljs:444, src/app/main/ui/workspace/sidebar/options/menus/bool.cljs:83 +msgid "workspace.shape.menu.union" +msgstr "Union" + +#: src/app/main/ui/workspace/context_menu.cljs:482, src/app/main/ui/workspace/sidebar/layer_item.cljs:179, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:274 +msgid "workspace.shape.menu.unlock" +msgstr "Déverrouiller" + +#: src/app/main/ui/workspace/context_menu.cljs:378 +msgid "workspace.shape.menu.unmask" +msgstr "Retirer le masque" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#, unused +msgid "workspace.shape.menu.update-components-in-bulk" +msgstr "Mettre à jour les composants" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:505 +msgid "workspace.shape.menu.update-main" +msgstr "Mettre à jour le composant principal" + +#: src/app/main/ui/components/tab_container.cljs:52, src/app/main/ui/workspace/sidebar.cljs:62 +msgid "workspace.sidebar.collapse" +msgstr "Réduire le panneau latéral" + +#: src/app/main/ui/workspace/sidebar.cljs:73, src/app/main/ui/workspace/sidebar.cljs:77 +msgid "workspace.sidebar.expand" +msgstr "Afficher le panneau latéral" + +#: src/app/main/ui/workspace/right_header.cljs:226 +msgid "workspace.sidebar.history" +msgstr "Historique" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:509, src/app/main/ui/workspace/sidebar.cljs:154, src/app/main/ui/workspace/sidebar.cljs:157, src/app/main/ui/workspace/sidebar.cljs:164 +msgid "workspace.sidebar.layers" +msgstr "Calques" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:313, src/app/main/ui/workspace/sidebar/layers.cljs:374 +msgid "workspace.sidebar.layers.components" +msgstr "Composants" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:297 +msgid "workspace.sidebar.layers.filter" +msgstr "Filtrer" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:310, src/app/main/ui/workspace/sidebar/layers.cljs:338 +msgid "workspace.sidebar.layers.frames" +msgstr "Tableaux" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:311, src/app/main/ui/workspace/sidebar/layers.cljs:350 +msgid "workspace.sidebar.layers.groups" +msgstr "Groupes" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:315, src/app/main/ui/workspace/sidebar/layers.cljs:398 +msgid "workspace.sidebar.layers.images" +msgstr "Images" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:312, src/app/main/ui/workspace/sidebar/layers.cljs:362 +msgid "workspace.sidebar.layers.masks" +msgstr "Masques" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:293 +msgid "workspace.sidebar.layers.search" +msgstr "Recherche de calques" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:316, src/app/main/ui/workspace/sidebar/layers.cljs:410 +msgid "workspace.sidebar.layers.shapes" +msgstr "Formes" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:314, src/app/main/ui/workspace/sidebar/layers.cljs:386 +msgid "workspace.sidebar.layers.texts" +msgstr "Textes" + +#: src/app/main/ui/inspect/attributes/svg.cljs:56, src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs:101 +msgid "workspace.sidebar.options.svg-attrs.title" +msgstr "Attributs SVG importés" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs:264 +msgid "workspace.sidebar.sitemap" +msgstr "Pages" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs:274 +msgid "workspace.sidebar.sitemap.add-page" +msgstr "Ajouter une page" + +#: src/app/main/ui/workspace/left_header.cljs:98 +msgid "workspace.sitemap" +msgstr "Plan du site" + +#: src/app/main/ui/workspace/tokens/themes/theme_selector.cljs:86 +msgid "workspace.tokens.active-themes" +msgstr "%s thèmes actifs" + +#: src/app/main/ui/workspace/tokens/sidebar.cljs +#, unused +msgid "workspace.tokens.add set" +msgstr "Ajouter un ensemble" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:66, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:151, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:347 +msgid "workspace.tokens.add-new-theme" +msgstr "Ajouter un nouveau thème" + +#: src/app/main/ui/workspace/tokens/sets/context_menu.cljs:62 +msgid "workspace.tokens.add-set-to-group" +msgstr "Ajouter un ensemble à ce groupe" + +#: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:196, src/app/main/ui/workspace/tokens/management/group.cljs:156 +msgid "workspace.tokens.add-token" +msgstr "Ajouter un token : %s" + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:137 +msgid "workspace.tokens.applied-to" +msgstr "Appliqué à" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:330 +msgid "workspace.tokens.axis" +msgstr "Axe" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:356 +msgid "workspace.tokens.back-to-themes" +msgstr "Retour à la liste des thèmes" + +#: src/app/main/ui/workspace/tokens/settings/menu.cljs:89 +msgid "workspace.tokens.base-font-size" +msgstr "Taille de police de base" + +#: src/app/main/ui/workspace/tokens/settings/menu.cljs:43 +msgid "workspace.tokens.base-font-size.error" +msgstr "" +"La taille de police de base doit être une valeur en pixels ou sans unité." + +#: src/app/main/ui/workspace/tokens/modals/import.cljs:127 +#, unused +msgid "workspace.tokens.choose-file" +msgstr "Choisir un fichier" + +#: src/app/main/ui/workspace/tokens/modals/import.cljs:132 +#, unused +msgid "workspace.tokens.choose-folder" +msgstr "Choisir un dossier" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:299, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:130 +msgid "workspace.tokens.color" +msgstr "Couleur" + +#: src/app/main/data/workspace/tokens/errors.cljs:101, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:251 +msgid "workspace.tokens.composite-line-height-needs-font-size" +msgstr "" +"La hauteur de ligne dépend de la taille de police. Ajouter une taille de " +"police pour obtenir la valeur calculée." + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:57 +msgid "workspace.tokens.create-new-theme" +msgstr "Crée le premier thème dès maintenant." + +#: src/app/main/ui/workspace/tokens/sets/lists.cljs:96, src/app/main/ui/workspace/tokens/themes.cljs:44 +msgid "workspace.tokens.create-one" +msgstr "En créer un." + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:235 +msgid "workspace.tokens.create-token" +msgstr "Créer un nouveau token %s" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:353 +msgid "workspace.tokens.delete" +msgstr "Supprimer le token" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:140 +msgid "workspace.tokens.delete-theme-title" +msgstr "Supprimer le thème" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:350 +msgid "workspace.tokens.duplicate" +msgstr "Dupliquer le token" + +#: src/app/main/data/workspace/tokens/library_edit.cljs:240, src/app/main/data/workspace/tokens/library_edit.cljs:509 +msgid "workspace.tokens.duplicate-suffix" +msgstr "copier" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:337 +msgid "workspace.tokens.edit" +msgstr "Éditer le token" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:346 +msgid "workspace.tokens.edit-theme-title" +msgstr "Édition du thème" + +#: src/app/main/ui/workspace/tokens/themes/theme_selector.cljs:74 +msgid "workspace.tokens.edit-themes" +msgstr "Éditer les thèmes" + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:234 +msgid "workspace.tokens.edit-token" +msgstr "Édition du token %s" + +#: src/app/main/data/workspace/tokens/errors.cljs:41 +msgid "workspace.tokens.empty-input" +msgstr "La valeur du token ne peut être nulle" + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 +msgid "workspace.tokens.enter-token-name" +msgstr "Entrer le nom du token %s" + +#: src/app/main/data/workspace/tokens/errors.cljs:15 +msgid "workspace.tokens.error-parse" +msgstr "Erreur d'importation : impossible d'analyser le JSON." + +#: src/app/main/ui/workspace/tokens/export/modal.cljs:49 +msgid "workspace.tokens.export" +msgstr "Exporter" + +#: src/app/main/ui/workspace/tokens/export/modal.cljs:125 +msgid "workspace.tokens.export-tokens" +msgstr "Exporter les tokens" + +#: src/app/main/ui/workspace/tokens/export/modal.cljs:118 +msgid "workspace.tokens.export.multiple-files" +msgstr "Plusieurs fichiers" + +#: src/app/main/ui/workspace/tokens/export/modal.cljs:38 +msgid "workspace.tokens.export.no-tokens-themes-sets" +msgstr "Aucun token, thème ou ensemble à exporter." + +#: src/app/main/ui/workspace/tokens/export/modal.cljs:35 +msgid "workspace.tokens.export.preview" +msgstr "Aperçu :" + +#: src/app/main/ui/workspace/tokens/export/modal.cljs:116 +msgid "workspace.tokens.export.single-file" +msgstr "Un seul fichier" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:129 +msgid "workspace.tokens.font-size-value-enter" +msgstr "Taille de police ou {alias}" + +#: src/app/main/data/workspace/tokens/application.cljs:325 +msgid "workspace.tokens.font-variant-not-found" +msgstr "" +"Erreur de configuration de la graisse/style de la police. Ce style de police " +"n'existe pas pour la police courante" + +#: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:42, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:137 +msgid "workspace.tokens.font-weight-value-enter" +msgstr "Graisse de police (300, gras italique, etc.) ou un {alias}" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:240 +msgid "workspace.tokens.gaps" +msgstr "Gouttières" + +#: src/app/main/ui/workspace/tokens/style_dictionary.cljs +#, unused +msgid "workspace.tokens.generic-error" +msgstr "Erreur : " + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:93 +msgid "workspace.tokens.group-name" +msgstr "Nom du groupe" + +#: src/app/main/ui/workspace/tokens/sets.cljs +#, unused +msgid "workspace.tokens.grouping-set-alert" +msgstr "Le groupement des ensembles de tokens n'est pas encore supporté." + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:233 +msgid "workspace.tokens.import-button-prefix" +msgstr "Importer %s" + +#: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 +msgid "workspace.tokens.import-error" +msgstr "Erreur d'importation :" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:273 +msgid "workspace.tokens.import-menu-folder-option" +msgstr "Dossier" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:271 +msgid "workspace.tokens.import-menu-json-option" +msgstr "Fichier JSON seul" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:272 +msgid "workspace.tokens.import-menu-zip-option" +msgstr "Fichier ZIP" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:241 +msgid "workspace.tokens.import-multiple-files" +msgstr "" +"En fichiers multiples. Le nom / chemin des fichiers seront les noms " +"d'ensembles." + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:240 +msgid "workspace.tokens.import-single-file" +msgstr "" +"En un fichier JSON seul. Les clés de premier niveau doivent être les noms " +"des ensembles de tokens." + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:237 +msgid "workspace.tokens.import-tokens" +msgstr "Importation de tokens" + +#: src/app/main/ui/workspace/tokens/sidebar.cljs:414, src/app/main/ui/workspace/tokens/sidebar.cljs:415 +#, unused +msgid "workspace.tokens.import-tooltip" +msgstr "" +"Importer un fichier JSON écrasera tous les tokens, ensembles et thèmes " +"actuels" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:247 +msgid "workspace.tokens.import-warning" +msgstr "Importer des tokens écrasera tous les tokens, ensembles et thèmes." + +#: src/app/main/ui/workspace/tokens/management.cljs:78 +msgid "workspace.tokens.inactive-set" +msgstr "Inactif" + +#: src/app/main/ui/workspace/tokens/management.cljs:70 +msgid "workspace.tokens.inactive-set-description" +msgstr "" +"Cet ensemble n'est pas actif. Change de thème ou active cet ensemble pour " +"voir les changements dans l'espace de travail" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:240, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:195 +msgid "workspace.tokens.individual-tokens" +msgstr "Tokens individuels" + +#: src/app/main/data/workspace/tokens/errors.cljs:49 +msgid "workspace.tokens.invalid-color" +msgstr "Valeur de couleur invalide : %s" + +#: src/app/main/data/workspace/tokens/errors.cljs:93 +msgid "workspace.tokens.invalid-font-family-token-value" +msgstr "" +"Valeur de token invalide :le token de référence doit être de type famille de " +"police de caractères" + +#: src/app/main/data/workspace/tokens/errors.cljs:89 +msgid "workspace.tokens.invalid-font-weight-token-value" +msgstr "" +"Valeur de graisse de police invalide : utiliser une valeur numérique (100-" +"950) ou des noms standards (mince, léger, régulier, gras, etc.), " +"optionnellement suivis par « italique »" + +#: src/app/main/data/workspace/tokens/errors.cljs:23 +msgid "workspace.tokens.invalid-json" +msgstr "Erreur d'importation : token invalide dans le JSON." + +#: src/app/main/data/workspace/tokens/errors.cljs:27 +msgid "workspace.tokens.invalid-json-token-name" +msgstr "Erreur d'importation : nom de token invalide dans le JSON." + +#: src/app/main/data/workspace/tokens/errors.cljs:28 +msgid "workspace.tokens.invalid-json-token-name-detail" +msgstr "" +"« %s » n'est pas un nom de token valide.\n" +"Les noms de tokens ne doivent contenir que des lettres et des chiffres " +"séparés par le caractère . et ne peuvent commencer par le symbole $." + +#: src/app/main/data/workspace/tokens/errors.cljs:105 +msgid "workspace.tokens.invalid-shadow-type-token-value" +msgstr "" +"Type d'ombre invalide : seulement « innerShadow » et « dropShadow » sont " +"acceptés" + +#: src/app/main/data/workspace/tokens/errors.cljs:81 +msgid "workspace.tokens.invalid-text-case-token-value" +msgstr "" +"Valeut de token invalide : seulement « none », « Uppercase », « Lowercase » " +"ou « Capitalize » sont acceptés" + +#: src/app/main/data/workspace/tokens/errors.cljs:85 +msgid "workspace.tokens.invalid-text-decoration-token-value" +msgstr "" +"Valeur de token invalide : seulement « none », « underline » et « strike-" +"through » sont acceptés" + +#: src/app/main/data/workspace/tokens/errors.cljs:117 +msgid "workspace.tokens.invalid-token-value-shadow" +msgstr "Valeur invalide : doit référencer un token d'ombre composé." From ed97cdde667ae316f2c9d57c8e307e6a6ff73c66 Mon Sep 17 00:00:00 2001 From: Alexis Morin Date: Thu, 26 Feb 2026 14:49:22 +0100 Subject: [PATCH 016/288] :globe_with_meridians: Add translations for: French (Canada) Currently translated at 96.4% (2001 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/ --- frontend/translations/fr_CA.po | 467 ++++++++++++++++++++++++++++++++- 1 file changed, 466 insertions(+), 1 deletion(-) diff --git a/frontend/translations/fr_CA.po b/frontend/translations/fr_CA.po index fea09045c3..8525255895 100644 --- a/frontend/translations/fr_CA.po +++ b/frontend/translations/fr_CA.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-24 02:09+0000\n" +"PO-Revision-Date: 2026-02-26 14:09+0000\n" "Last-Translator: Alexis Morin \n" "Language-Team: French (Canada) \n" @@ -8098,3 +8098,468 @@ msgstr "" #: src/app/main/data/workspace/tokens/errors.cljs:117 msgid "workspace.tokens.invalid-token-value-shadow" msgstr "Valeur invalide : doit référencer un token d'ombre composé." + +#: src/app/main/data/workspace/tokens/errors.cljs:97 +msgid "workspace.tokens.invalid-token-value-typography" +msgstr "Valeur invalide: doit référencer un token de typographie composé." + +#: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 +msgid "workspace.tokens.invalid-value" +msgstr "Valeur de token invalide : %s" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 +msgid "workspace.tokens.label.group" +msgstr "Groupe" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:207 +msgid "workspace.tokens.label.group-placeholder" +msgstr "Ajouter un groupe (ex. Mode)" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:214 +msgid "workspace.tokens.label.theme" +msgstr "Thème" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:215 +msgid "workspace.tokens.label.theme-placeholder" +msgstr "Ajouter un thème (ex. Clair)" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:153 +msgid "workspace.tokens.letter-spacing-value-enter-composite" +msgstr "Interlettrage ou {alias}" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:145 +msgid "workspace.tokens.line-height-value-enter" +msgstr "Hauteur de ligne (multiplicateur, px, %) ou {alias}" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:232 +msgid "workspace.tokens.margins" +msgstr "Marges" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:268 +msgid "workspace.tokens.max-size" +msgstr "Taille max." + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:262 +msgid "workspace.tokens.min-size" +msgstr "Taille min." + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:303 +msgid "workspace.tokens.missing-reference" +msgstr "Référence manquante" + +#: src/app/main/data/workspace/tokens/errors.cljs:57 +msgid "workspace.tokens.missing-references" +msgstr "Références de token manquantes : " + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 +msgid "workspace.tokens.more-options" +msgstr "Clic droit pour voir les options" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:135 +msgid "workspace.tokens.no-active-sets" +msgstr "Aucun ensemble actif" + +#: src/app/main/ui/workspace/tokens/themes/theme_selector.cljs:91 +msgid "workspace.tokens.no-active-theme" +msgstr "Aucun thème actif" + +#: src/app/main/ui/workspace/tokens/sets/lists.cljs:72 +msgid "workspace.tokens.no-permisions-set" +msgstr "Tu dois être un éditeur pour activer / désactiver des ensembles" + +#: src/app/main/ui/workspace/tokens/themes.cljs:54 +msgid "workspace.tokens.no-permission-themes" +msgstr "Tu dois être un éditeur pour utiliser les thèmes" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:87 +#, unused +msgid "workspace.tokens.no-references-found" +msgstr "Aucune référence trouvée" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +#, unused +msgid "workspace.tokens.no-remap-needed" +msgstr "" +"Ce token n'est pas utilisé dans le design. Il n'est pas nécessaire de mettre " +"à jour sa référence." + +#: src/app/main/ui/workspace/tokens/sets/lists.cljs:485 +msgid "workspace.tokens.no-sets-create" +msgstr "Aucun ensemble n'est défini. Crée-en un pour les assigner à un thème." + +#: src/app/main/ui/workspace/tokens/sets/lists.cljs:93, src/app/main/ui/workspace/tokens/sets/lists.cljs:99 +msgid "workspace.tokens.no-sets-yet" +msgstr "Aucun ensemble n'existe." + +#: src/app/main/ui/workspace/tokens/themes.cljs:40 +msgid "workspace.tokens.no-themes" +msgstr "Aucun thème n'existe." + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:53 +msgid "workspace.tokens.no-themes-currently" +msgstr "Aucun thème n'existe actuellement." + +#: src/app/main/data/workspace/tokens/errors.cljs:19 +msgid "workspace.tokens.no-token-files-found" +msgstr "Aucun token, ensemble ou thème n'existe dans ce fichier." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:95 +msgid "workspace.tokens.not-remap" +msgstr "Ne pas remapper" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:134 +msgid "workspace.tokens.num-active-sets" +msgstr "%s ensembles actifs" + +#: src/app/main/data/workspace/tokens/errors.cljs:53 +msgid "workspace.tokens.number-too-large" +msgstr "Valeur de token invalide. La valeur calculée est trop grande : %s" + +#: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 +msgid "workspace.tokens.opacity-range" +msgstr "L'opacité doit être entre 0% et 100% ou entre 0 et 1 (ex. 50% ou 0.5)." + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 +msgid "workspace.tokens.original-value" +msgstr "Valeur d'origine : %s" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:216 +msgid "workspace.tokens.paddings" +msgstr "Marges intérieures" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:292 +msgid "workspace.tokens.radius" +msgstr "Rayon" + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:129 +msgid "workspace.tokens.ref-not-valid" +msgstr "La référence est invalide ou n'existe dans aucun ensemble actif" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:178 +msgid "workspace.tokens.reference-composite" +msgstr "Entrer un alias de token typographique" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:204 +msgid "workspace.tokens.reference-composite-shadow" +msgstr "Entrer un alias de token d'ombre" + +#: src/app/main/ui/workspace/tokens/style_dictionary.cljs +#, unused +msgid "workspace.tokens.reference-error" +msgstr "Erreurs de référence : " + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:99 +msgid "workspace.tokens.remap" +msgstr "Remapper les tokens" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:86 +msgid "workspace.tokens.remap-token-references-title" +msgstr "Remapper tous les tokens qui utilisent `%s` à `%s` ?" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88 +msgid "workspace.tokens.remap-warning-effects" +msgstr "" +"Cette action mettra à jour tous les calques et références qui utilisent " +"l'ancien nom du token." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89 +msgid "workspace.tokens.remap-warning-time" +msgstr "Cette action peut durer longtemps." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:92 +#, unused +msgid "workspace.tokens.remapping-in-progress" +msgstr "Remappage de références de token..." + +#: src/app/main/data/workspace/tokens/warnings.cljs:15, src/app/main/data/workspace/tokens/warnings.cljs:19, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:56, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:84, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:103, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:285, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:459, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:176, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:311, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:251, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:364, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:465, src/app/main/ui/workspace/tokens/management/token_pill.cljs:122 +msgid "workspace.tokens.resolved-value" +msgstr "Valeur calculée : %s" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:251 +msgid "workspace.tokens.save-theme" +msgstr "Sauvegarder le thème" + +#: src/app/main/ui/workspace/tokens/sets/lists.cljs:204, src/app/main/ui/workspace/tokens/sets/lists.cljs:309 +msgid "workspace.tokens.select-set" +msgstr "Sélectionner l'ensemble." + +#: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 +msgid "workspace.tokens.self-reference" +msgstr "Le token s'auto-référence" + +#: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 +msgid "workspace.tokens.set-edit-placeholder" +msgstr "Entrer un nom (utiliser « / » pour les groupes)" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:361 +msgid "workspace.tokens.set-selection-theme" +msgstr "Définir quels ensembles de tokens feront partie de ce thème :" + +#: src/app/main/ui/workspace/tokens/token_pill.cljs:47 +#, unused +msgid "workspace.tokens.set.not-active" +msgstr "L'ensemble de tokens n'est pas actif" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:129 +msgid "workspace.tokens.sets-hint" +msgstr "Éditer le thème et gérer les ensembles" + +#: src/app/main/ui/workspace/tokens/settings/menu.cljs:91 +msgid "workspace.tokens.setting-description" +msgstr "" +"Configuration de la taille de police de base, qui définit la valeur de 1rem :" + +#: src/app/main/ui/workspace/tokens/settings/menu.cljs:84 +msgid "workspace.tokens.settings" +msgstr "Paramètres des tokens" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:232 +msgid "workspace.tokens.shadow-add-shadow" +msgstr "Ajouter une ombre" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:161, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:162, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:165 +msgid "workspace.tokens.shadow-blur" +msgstr "Flou" + +#: src/app/main/data/workspace/tokens/errors.cljs:109 +msgid "workspace.tokens.shadow-blur-range" +msgstr "Le flou d'ombre doit être égal ou supérieur à 0." + +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988 +#, unused +msgid "workspace.tokens.shadow-color" +msgstr "Couleur" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:114 +msgid "workspace.tokens.shadow-inset" +msgstr "Position de l'ombre" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:123 +msgid "workspace.tokens.shadow-remove-shadow" +msgstr "Supprimer l'ombre" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:173, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:174, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:177 +msgid "workspace.tokens.shadow-spread" +msgstr "Étalement" + +#: src/app/main/data/workspace/tokens/errors.cljs:113 +msgid "workspace.tokens.shadow-spread-range" +msgstr "L'étalement d'ombre doit être égal ou supérieur à 0." + +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1215 +#, unused +msgid "workspace.tokens.shadow-title" +msgstr "Ombres" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:281 +msgid "workspace.tokens.shadow-token-blur-value-error" +msgstr "La valeur de flou ne peut être négative" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:287 +#, unused +msgid "workspace.tokens.shadow-token-spread-value-error" +msgstr "La valeur d'étalement ne peut être négative" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:139, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:141 +msgid "workspace.tokens.shadow-x" +msgstr "X" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:150, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:152 +msgid "workspace.tokens.shadow-y" +msgstr "Y" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:256 +msgid "workspace.tokens.size" +msgstr "Taille" + +#: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 +msgid "workspace.tokens.stroke-width-range" +msgstr "L'épaisseur de trait doit être égale ou supérieure à 0." + +#: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 +msgid "workspace.tokens.text-case-value-enter" +msgstr "none | uppercase | lowercase | capitalize ou {alias}" + +#: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:41, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:169 +msgid "workspace.tokens.text-decoration-value-enter" +msgstr "none | underline | strike-through ou {alias}" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:154 +#, unused +msgid "workspace.tokens.theme-name" +msgstr "Thème %s" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:51 +#, unused +msgid "workspace.tokens.theme-name-already-exists" +msgstr "Un thème portant ce nom existe déjà" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:100 +#, unused +msgid "workspace.tokens.theme.disable" +msgstr "Désactiver" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:96 +#, unused +msgid "workspace.tokens.theme.enable" +msgstr "Activer" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:85 +msgid "workspace.tokens.themes-description" +msgstr "" +"Géstion des thèmes, activation / désactivation et configurer les ensembles " +"actifs." + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:49, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:83 +msgid "workspace.tokens.themes-list" +msgstr "Liste des thèmes" + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:275, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:276 +msgid "workspace.tokens.token-description" +msgstr "Description" + +#: src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:122, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:246 +msgid "workspace.tokens.token-font-family-select" +msgstr "Sélectionner une famille de polices de caractères" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:121 +msgid "workspace.tokens.token-font-family-value" +msgstr "Famille de polices" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:120 +msgid "workspace.tokens.token-font-family-value-enter" +msgstr "Famille de police ou liste de polices séparées par une virgule (,)" + +#: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:83, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:112, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:240 +msgid "workspace.tokens.token-name" +msgstr "Nom" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:268, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:223 +msgid "workspace.tokens.token-name-duplication-validation-error" +msgstr "Un token existe déjà au chemin : %s" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:265, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:220 +msgid "workspace.tokens.token-name-length-validation-error" +msgstr "Le nom doit être au moins 1 caractère" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:267, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:222 +msgid "workspace.tokens.token-name-validation-error" +msgstr "" +" n'est pas un nom de token valide.\n" +"Les noms de token ne doivent contenir que des lettres et des chiffres " +"séparés par des . et doivent pas commencer par le symbole $." + +#: src/app/main/ui/workspace/tokens/style_dictionary.cljs:259 +#, unused +msgid "workspace.tokens.token-not-resolved" +msgstr "Impossible de résoudre la référence de token portant le nom : %s" + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:267 +msgid "workspace.tokens.token-value" +msgstr "Valeur" + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:266, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:129 +msgid "workspace.tokens.token-value-enter" +msgstr "Entrer une valeur ou un alias avec {alias}" + +#: src/app/main/ui/workspace/tokens/management.cljs:67 +msgid "workspace.tokens.tokens-section-title" +msgstr "TOKENS - %s" + +#: src/app/main/ui/workspace/tokens/sidebar.cljs:122 +msgid "workspace.tokens.tools" +msgstr "Outils" + +#: src/app/main/data/workspace/tokens/import_export.cljs:46 +msgid "workspace.tokens.unknown-token-type-message" +msgstr "L'importation a réussi. Certains tokens n'ont pas été inclus." + +#: src/app/main/data/workspace/tokens/import_export.cljs:48 +msgid "workspace.tokens.unknown-token-type-section" +msgstr "Le type « %s » n'est pas supporté (%s)\n" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 +msgid "workspace.tokens.use-reference" +msgstr "Utiliser une référence" + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:132 +msgid "workspace.tokens.value-not-valid" +msgstr "La valeur n'est pas valide" + +#: src/app/main/data/workspace/tokens/errors.cljs:69 +msgid "workspace.tokens.value-with-percent" +msgstr "Valeur invalide : % n'est pas permis." + +#: src/app/main/data/workspace/tokens/errors.cljs:65 +msgid "workspace.tokens.value-with-units" +msgstr "Valeur invalide : les unités ne sont pas permises." + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +#, unused +msgid "workspace.tokens.warning-name-change" +msgstr "Renommer ce token brisera toute référence à son ancien nom" + +#: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 +msgid "workspace.toolbar.assets" +msgstr "Atouts" + +#: src/app/main/ui/workspace/palette.cljs:184 +msgid "workspace.toolbar.color-palette" +msgstr "Palette de couleurs (%s)" + +#: src/app/main/ui/workspace/right_header.cljs:217 +msgid "workspace.toolbar.comments" +msgstr "Commentaires (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:195 +msgid "workspace.toolbar.curve" +msgstr "Courbe (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:231 +msgid "workspace.toolbar.debug" +msgstr "Outils de débogage" + +#: src/app/main/ui/workspace/top_toolbar.cljs:172 +msgid "workspace.toolbar.ellipse" +msgstr "Ellipse (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:130 +msgid "workspace.toolbar.frame" +msgstr "Tableau (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:129 +msgid "workspace.toolbar.frame-first-time" +msgstr "Créer un tableau. Cliquer et glisser pour définir sa taille. (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:60 +msgid "workspace.toolbar.image" +msgstr "Image (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:143 +msgid "workspace.toolbar.move" +msgstr "Déplacer (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:205 +msgid "workspace.toolbar.path" +msgstr "Chemin (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:216 +msgid "workspace.toolbar.plugins" +msgstr "Plugiciels (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:162 +msgid "workspace.toolbar.rect" +msgstr "Rectangle (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +#, unused +msgid "workspace.toolbar.shortcuts" +msgstr "Raccourcis (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:182 +msgid "workspace.toolbar.text" +msgstr "Texte (%s)" + +#: src/app/main/ui/workspace/palette.cljs:190 +msgid "workspace.toolbar.text-palette" +msgstr "Typographies (%s)" From c769e782f0334dfcf01b3c89aa2f44ba3302bb53 Mon Sep 17 00:00:00 2001 From: Denys Kisil Date: Thu, 26 Feb 2026 12:55:28 +0100 Subject: [PATCH 017/288] :globe_with_meridians: Add translations for: Ukrainian (ukr_UA) Currently translated at 99.7% (2068 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/ --- frontend/translations/ukr_UA.po | 1120 ++++++++++++++++++++++++++++++- 1 file changed, 1097 insertions(+), 23 deletions(-) diff --git a/frontend/translations/ukr_UA.po b/frontend/translations/ukr_UA.po index 2e7b736c7f..553985e072 100644 --- a/frontend/translations/ukr_UA.po +++ b/frontend/translations/ukr_UA.po @@ -1,16 +1,16 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-16 08:35+0000\n" +"PO-Revision-Date: 2026-02-26 14:10+0000\n" "Last-Translator: Denys Kisil \n" -"Language-Team: Ukrainian " -"\n" +"Language-Team: Ukrainian \n" "Language: ukr_UA\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 5.16-dev\n" +"X-Generator: Weblate 5.16.1-dev\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -652,7 +652,7 @@ msgstr "" #: src/app/main/ui/dashboard.cljs:259 msgid "dashboard.import.bad-url" -msgstr "Імпортування не вдалось. Посилання шаблону неправильне" +msgstr "Імпортування не вдалось. URL шаблону неправильне" #: src/app/main/ui/dashboard.cljs:241 #, unused @@ -857,7 +857,7 @@ msgstr "Пришпилити/відшпилити" #: src/app/main/ui/dashboard.cljs:223 msgid "dashboard.plugins.bad-url" -msgstr "Посилання плагіну неправильне" +msgstr "Недійсний URL плагіну" #: src/app/main/ui/dashboard.cljs:221 msgid "dashboard.plugins.parse-error" @@ -1284,7 +1284,7 @@ msgstr "Під час роботи веб-виконавця сталась по #: src/app/main/ui/components/color_input.cljs:51 msgid "errors.invalid-color" -msgstr "Хибний колір" +msgstr "Недійсний колір" #: src/app/util/forms.cljs:35, src/app/util/forms.cljs:89 msgid "errors.invalid-data" @@ -1310,7 +1310,7 @@ msgstr "Помилковий текст" #: src/app/main/ui/static.cljs:74 msgid "errors.invite-invalid" -msgstr "Хибне запрошення" +msgstr "Недійсне запрошення" #: src/app/main/ui/static.cljs:75 msgid "errors.invite-invalid.info" @@ -1459,7 +1459,7 @@ msgstr "Помилка під'єднання, адреса недосяжна" #: src/app/main/ui/dashboard/team.cljs:1045 msgid "errors.webhooks.invalid-uri" -msgstr "Посилання не пройшло перевірку." +msgstr "URL не пройшов перевірку." #: src/app/main/ui/dashboard/team.cljs:1204 msgid "errors.webhooks.last-delivery" @@ -1687,7 +1687,7 @@ msgstr "Типографія" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:308 msgid "inspect.attributes.typography.font-family" -msgstr "Сімейство шрифта" +msgstr "Сімейство Шрифта" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:326, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:332 msgid "inspect.attributes.typography.font-size" @@ -2061,7 +2061,7 @@ msgstr "Фіґма" #: src/app/main/ui/dashboard/fonts.cljs:432 msgid "labels.font-family" -msgstr "Сімейство шрифтів" +msgstr "Сімейство Шрифтів" #, unused msgid "labels.font-providers" @@ -2745,7 +2745,7 @@ msgstr "Створити вебхук" #: src/app/main/ui/dashboard/team.cljs:1103 msgid "modals.create-webhook.url.label" -msgstr "Посилання на Payload" +msgstr "Адреса пейлоду" #: src/app/main/ui/dashboard/team.cljs:1104 msgid "modals.create-webhook.url.placeholder" @@ -4412,7 +4412,7 @@ msgstr "" #: src/app/main/ui/settings/subscription.cljs:259 msgid "subscription.settings.management.dialog.payment-explanation" -msgstr "(Платіж не буде зроблено)" +msgstr "Плата опісля пробного терміну. Наразі кредитна карта не потрібна." #: src/app/main/ui/settings/subscription.cljs:252, src/app/main/ui/settings/subscription.cljs:256 #, markdown @@ -4474,9 +4474,8 @@ msgid "subscription.settings.sucess.dialog.title" msgstr "Ви %s!" #: src/app/main/ui/settings/subscription.cljs:526 -#, fuzzy msgid "subscription.settings.support-us-since" -msgstr "Ви підтримуєте нас за цим планом з %s" +msgstr "Ви підтримуєте нас цим планом з: %s" #: src/app/main/ui/settings/subscription.cljs:558, src/app/main/ui/settings/subscription.cljs:574 msgid "subscription.settings.try-it-free" @@ -4806,7 +4805,7 @@ msgstr "Сортувати" #: src/app/main/ui/dashboard/grid.cljs:165, src/app/main/ui/dashboard/grid.cljs:220, src/app/main/ui/workspace/sidebar/assets/typographies.cljs:396, src/app/main/ui/workspace/sidebar/assets.cljs:161 msgid "workspace.assets.typography" -msgstr "Типографіка" +msgstr "Типографіки" #: src/app/main/ui/workspace/sidebar/assets/typographies.cljs:404 msgid "workspace.assets.typography.add-typography" @@ -5825,7 +5824,7 @@ msgstr "Вікрити накладення: %s" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:61, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:351 msgid "workspace.options.interaction-open-url" -msgstr "Відкрити посилання" +msgstr "Перейти за URL" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs #, unused @@ -5898,7 +5897,7 @@ msgstr "Подразник" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:469 msgid "workspace.options.interaction-url" -msgstr "Посилання" +msgstr "URL-адреса" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:39, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:339 msgid "workspace.options.interaction-while-hovering" @@ -6575,7 +6574,7 @@ msgstr "Щоб використовувати цей плагін, Ви маєт #: src/app/main/ui/workspace/plugins.cljs:189 msgid "workspace.plugins.error.url" -msgstr "Плагін не існує або посилання на нього неправильне." +msgstr "Плагін не існує або його URL недійсний." #: src/app/main/ui/workspace/plugins.cljs:185 msgid "workspace.plugins.install" @@ -6657,7 +6656,7 @@ msgstr "Видалити плагін" #: src/app/main/ui/workspace/plugins.cljs:180 msgid "workspace.plugins.search-placeholder" -msgstr "Вкажіть посилання на плагін" +msgstr "Вкажіть URL плагіну" #, unused msgid "workspace.plugins.success" @@ -7300,7 +7299,6 @@ msgid "workspace.tokens.opacity-range" msgstr "Непрозорість має бути між 0 та 100% або ж між 0 та 1 (де 0.5 - 50%)." #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 -#, fuzzy msgid "workspace.tokens.original-value" msgstr "Початкове значення: %s" @@ -7322,7 +7320,6 @@ msgid "workspace.tokens.reference-error" msgstr "Помилка посилання: " #: src/app/main/data/workspace/tokens/warnings.cljs:15, src/app/main/data/workspace/tokens/warnings.cljs:19, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:56, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:84, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:103, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:285, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:459, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:176, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:311, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:251, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:364, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:465, src/app/main/ui/workspace/tokens/management/token_pill.cljs:122 -#, fuzzy msgid "workspace.tokens.resolved-value" msgstr "Отримане значення: %s" @@ -7389,7 +7386,6 @@ msgid "workspace.tokens.themes-list" msgstr "Список тем" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:275, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:276 -#, fuzzy msgid "workspace.tokens.token-description" msgstr "Опис" @@ -7754,3 +7750,1081 @@ msgstr "Автозбережені версії зберігатимуться #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Натисність щоб закінчити шлях" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 +msgid "color-row.token-color-row.deleted-token" +msgstr "Даний токен не існує або був видалений." + +#: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 +msgid "color-token.empty-state" +msgstr "" +"Відсутні токени кольорів. Перевірте активні набори/теми чи додайте нові " +"токени." + +#: src/app/main/ui/dashboard/file_menu.cljs:208 +msgid "dashboard-restore-file-confirmation.description" +msgstr "Ви збираєтесь відновити %s." + +#: src/app/main/ui/dashboard/file_menu.cljs:207 +msgid "dashboard-restore-file-confirmation.title" +msgstr "Відновити файл" + +#: src/app/main/ui/dashboard/deleted.cljs:313 +msgid "dashboard.clear-trash-button" +msgstr "Звільнити кошик" + +#: src/app/main/ui/dashboard/sidebar.cljs:347 +msgid "dashboard.create-new-org" +msgstr "Створити організацію" + +#: src/app/main/ui/dashboard/deleted.cljs:262 +msgid "dashboard.delete-all-forever-confirmation.description" +msgstr "" +"Ви впевнені що хочете назавжди позбутись видалених проєктів та файлів? Це " +"незворотня дія." + +#: src/app/main/ui/dashboard/file_menu.cljs:221 +msgid "dashboard.delete-file-forever-confirmation.description" +msgstr "Ви впевнені що хочете назавжди позбутись %s? Це незворотня дія." + +#: src/app/main/data/dashboard.cljs:778 +msgid "dashboard.delete-files-success-notification" +msgstr "%s файлів було успішно вилучено." + +#: src/app/main/ui/dashboard/deleted.cljs:51, src/app/main/ui/dashboard/deleted.cljs:53, src/app/main/ui/dashboard/deleted.cljs:261, src/app/main/ui/dashboard/deleted.cljs:263, src/app/main/ui/dashboard/file_menu.cljs:220, src/app/main/ui/dashboard/file_menu.cljs:222 +msgid "dashboard.delete-forever-confirmation.title" +msgstr "Позбутись назавжди" + +#: src/app/main/ui/dashboard/deleted.cljs:85 +msgid "dashboard.delete-project-button" +msgstr "Вилучити проєкт" + +#: src/app/main/ui/dashboard/deleted.cljs:52 +msgid "dashboard.delete-project-forever-confirmation.description" +msgstr "" +"Ви впевнені що хочете назавжди позбутись %s проєктів? Ви позбудетесь їх та " +"файлів, що в них містились, назавжди. Це незворотня дія." + +#: src/app/main/data/dashboard.cljs:777, src/app/main/data/dashboard.cljs:811 +msgid "dashboard.delete-success-notification" +msgstr "%s було успішно вилучено." + +#: src/app/main/ui/dashboard/deleted.cljs:327 +msgid "dashboard.deleted.empty-state-description" +msgstr "Ваш смітник порожній. Видалені файли та проєкти зʼявлятимуться тут." + +#: src/app/main/ui/dashboard/grid.cljs:248 +msgid "dashboard.deleted.will-be-deleted-at" +msgstr "Буде вилучено %s" + +#, unused +msgid "dashboard.errors.error-on-delete-file" +msgstr "Сталася помилка під час вилучення файлу %s." + +#: src/app/main/data/dashboard.cljs:781 +msgid "dashboard.errors.error-on-delete-files" +msgstr "Сталася помилка під час вилучення файлів." + +#: src/app/main/data/dashboard.cljs:814 +msgid "dashboard.errors.error-on-delete-project" +msgstr "Сталася помилка під час вилучення проєкту %s." + +#: src/app/main/data/dashboard.cljs:909, src/app/main/ui/dashboard/file_menu.cljs:201 +msgid "dashboard.errors.error-on-restore-file" +msgstr "Сталася помилка під час відновлення файлу %s." + +#: src/app/main/data/dashboard.cljs:910 +msgid "dashboard.errors.error-on-restore-files" +msgstr "Сталася помилка під час відновлення файлів." + +#: src/app/main/data/dashboard.cljs:942 +msgid "dashboard.errors.error-on-restoring-project" +msgstr "Сталася помилка під час відновлення проєкту %s та його файлів." + +#: src/app/main/ui/dashboard/file_menu.cljs:266 +msgid "dashboard.file-menu.delete-files-permanently-option" +msgid_plural "dashboard.file-menu.delete-files-permanently-option" +msgstr[0] "Вилучити файл" +msgstr[1] "Вилучити кільки файлів" +msgstr[2] "Вилучити файли" + +#: src/app/main/ui/dashboard/file_menu.cljs:263 +msgid "dashboard.file-menu.restore-files-option" +msgid_plural "dashboard.file-menu.restore-files-option" +msgstr[0] "Відновити файл" +msgstr[1] "Відновити кілька файлів" +msgstr[2] "Відновити файли" + +#: src/app/main/ui/dashboard/team.cljs:765 +msgid "dashboard.invitation-modal.delete" +msgstr "Ви збираєтесь вилучити запрошення до:" + +#: src/app/main/ui/dashboard/team.cljs:766 +msgid "dashboard.invitation-modal.resend" +msgstr "Ви збираєтесь перенадіслати запрошення до:" + +#: src/app/main/ui/dashboard/team.cljs:756 +msgid "dashboard.invitation-modal.title.delete-invitations" +msgstr "Вилучити запрошення" + +#: src/app/main/ui/dashboard/team.cljs:757 +msgid "dashboard.invitation-modal.title.resend-invitations" +msgstr "Перенадіслати запрошення" + +#: src/app/main/ui/dashboard/team.cljs:949 +msgid "dashboard.order-invitations-by-role" +msgstr "Сортувати за роллю" + +#: src/app/main/ui/dashboard/team.cljs:958 +msgid "dashboard.order-invitations-by-status" +msgstr "Сортувати за статусом" + +#: src/app/main/data/dashboard.cljs:722 +msgid "dashboard.progress-notification.deleting-files" +msgstr "Вилучаю файли…" + +#: src/app/main/data/dashboard.cljs:843 +msgid "dashboard.progress-notification.restoring-files" +msgstr "Відновлюю файли…" + +#: src/app/main/data/dashboard.cljs:723 +msgid "dashboard.progress-notification.slow-delete" +msgstr "Вилучення триває неочікувано довго" + +#: src/app/main/data/dashboard.cljs:844 +msgid "dashboard.progress-notification.slow-restore" +msgstr "Відновлення триває неочікувано довго" + +#: src/app/main/ui/dashboard/deleted.cljs:274 +msgid "dashboard.restore-all-confirmation.description" +msgstr "" +"Ви збираєтесь відновити усі проєкти та їх файли. Це може зайняти деякий час." + +#: src/app/main/ui/dashboard/deleted.cljs:273 +msgid "dashboard.restore-all-confirmation.title" +msgstr "Відновити усі проєкти й файли" + +#: src/app/main/ui/dashboard/deleted.cljs:308 +msgid "dashboard.restore-all-deleted-button" +msgstr "Відновити Все" + +#: src/app/main/data/dashboard.cljs:903 +msgid "dashboard.restore-files-success-notification" +msgstr "%s файлів було успішно відновлено." + +#: src/app/main/ui/dashboard/deleted.cljs:82 +msgid "dashboard.restore-project-button" +msgstr "Відновити проєкт" + +#: src/app/main/ui/dashboard/deleted.cljs:41 +msgid "dashboard.restore-project-confirmation.description" +msgstr "Ви збираєтесь відновити %s проєктів та всі їхні файли." + +#: src/app/main/ui/dashboard/deleted.cljs:40 +msgid "dashboard.restore-project-confirmation.title" +msgstr "Відновити Проєкт" + +#: src/app/main/data/dashboard.cljs:875, src/app/main/data/dashboard.cljs:902, src/app/main/data/dashboard.cljs:939, src/app/main/ui/dashboard/file_menu.cljs:198 +msgid "dashboard.restore-success-notification" +msgstr "%s було успішно відновлено." + +#: src/app/main/ui/dashboard/deleted.cljs:298 +msgid "dashboard.trash-info-text-part1" +msgstr "Вилучені файли лишатимуться у смітнику на" + +#: src/app/main/ui/dashboard/deleted.cljs:300 +msgid "dashboard.trash-info-text-part2" +msgstr " %s днів. " + +#: src/app/main/ui/dashboard/deleted.cljs:301 +msgid "dashboard.trash-info-text-part3" +msgstr "Після цього, ви позбудетесь від них назавжди." + +#: src/app/main/ui/dashboard/deleted.cljs:303 +msgid "dashboard.trash-info-text-part4" +msgstr "" +"Якщо вирішете інакше, Ви маєте змогу відновити їх або позбутись їх назавжди " +"у меню кожного з них." + +#: src/app/main/ui/ds/controls/numeric_input.cljs:98 +msgid "ds.inputs.numeric-input.no-applicable-tokens" +msgstr "У активних наборах й темах не знайдено застосовних токенів." + +#: src/app/main/ui/ds/controls/numeric_input.cljs:99 +msgid "ds.inputs.numeric-input.no-matches" +msgstr "Збігів не виявлено." + +#: src/app/main/ui/ds/controls/numeric_input.cljs:652, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:141 +msgid "ds.inputs.numeric-input.open-token-list-dropdown" +msgstr "Відкрити список токенів" + +#: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 +msgid "ds.inputs.token-field.detach-token" +msgstr "Відʼєднати токен" + +#: src/app/main/ui/ds/controls/utilities/token_field.cljs:43, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:99, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:106 +msgid "ds.inputs.token-field.no-active-token-option" +msgstr "" +"Цей токен не міститься в жодному з активних наборів або має недійсне " +"значення." + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:296, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:240 +msgid "errors.field-max-length" +msgstr "Повинно містити щонайменше %s символів." + +#: src/app/main/errors.cljs:105 +msgid "errors.unexpected-exception" +msgstr "Неочікувана помилка: %s" + +#: src/app/main/ui/static.cljs:315 +msgid "errors.webgl-context-lost.desc-message" +msgstr "" +"WebGL припинив працювати. Просимо Вас перезавантажити сторінку, щоб скинути " +"його" + +#: src/app/main/ui/static.cljs:314 +msgid "errors.webgl-context-lost.main-message" +msgstr "От халепа! Полотно загубилось" + +#: src/app/main/ui/settings/feedback.cljs:122 +msgid "feedback.description-placeholder" +msgstr "Просимо Вас описати причину відгуку" + +#: src/app/main/ui/settings/feedback.cljs:143 +msgid "feedback.other-ways-contact" +msgstr "Інші способи звʼязатись з нами" + +#: src/app/main/ui/settings/feedback.cljs:126 +msgid "feedback.penpot.link" +msgstr "" +"Якщо відгук якось повʼязаний з файлом, чи проєктом, вкажіть посилання на " +"нього сюди:" + +#: src/app/main/ui/settings/feedback.cljs:101 +msgid "feedback.title-contact-us" +msgstr "Звʼяжіться з нами" + +#: src/app/main/ui/settings/feedback.cljs:110, src/app/main/ui/settings/feedback.cljs:111 +msgid "feedback.type" +msgstr "Тип" + +#: src/app/main/ui/settings/feedback.cljs:115 +msgid "feedback.type.doubt" +msgstr "Сумнів" + +#: src/app/main/ui/settings/feedback.cljs:113 +msgid "feedback.type.idea" +msgstr "Ідея" + +#: src/app/main/ui/settings/feedback.cljs:114 +msgid "feedback.type.issue" +msgstr "Вада" + +#: src/app/main/ui/exports/files.cljs:124 +msgid "files-download-modal.title" +msgstr "Вивантажити файли" + +#: src/app/main/ui/inspect/styles/rows/color_properties_row.cljs:120 +msgid "inspect.attributes.image.preview" +msgstr "Перегляд зображення заповнення фігури" + +#: src/app/main/ui/inspect/right_sidebar.cljs:170 +msgid "inspect.color-space-label" +msgstr "Обрати простір кольорів" + +#: src/app/main/ui/inspect/right_sidebar.cljs:238 +msgid "inspect.empty.more" +msgstr "Більше інфо" + +#: src/app/main/ui/inspect/right_sidebar.cljs:166 +msgid "inspect.layer-info" +msgstr "Дані шару" + +#: src/app/main/ui/inspect/styles/panels/tokens_panel.cljs:26 +msgid "inspect.tabs.styles.active-sets" +msgstr "Діючі набори" + +#: src/app/main/ui/inspect/styles/panels/tokens_panel.cljs:21 +msgid "inspect.tabs.styles.active-themes" +msgstr "Діючі теми" + +#: src/app/main/ui/inspect/styles/style_box.cljs:68 +msgid "inspect.tabs.styles.copy-shorthand" +msgstr "Копіювати скоропис CSS до буферу обміну" + +#: src/app/main/ui/inspect/styles/property_detail_copiable.cljs:51 +msgid "inspect.tabs.styles.copy-to-clipboard" +msgstr "Копіювати до буферу обміну" + +#: src/app/main/ui/inspect/styles/style_box.cljs:22 +#, unused +msgid "inspect.tabs.styles.geometry-panel" +msgstr "Розміри й Позиції" + +#: src/app/main/ui/inspect/styles/style_box.cljs:60, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:178 +msgid "inspect.tabs.styles.toggle-style" +msgstr "Перемкнути панель %s" + +#: src/app/main/ui/inspect/styles/style_box.cljs:21 +msgid "inspect.tabs.styles.token-panel" +msgstr "Набори й Теми Токенів" + +#: src/app/main/ui/inspect/styles/rows/color_properties_row.cljs:102, src/app/main/ui/inspect/styles/rows/properties_row.cljs:60 +msgid "inspect.tabs.styles.token-resolved-value" +msgstr "Знайдене значення:" + +#: src/app/main/ui/inspect/styles/style_box.cljs:20 +msgid "inspect.tabs.styles.variants-panel" +msgstr "Параметри Варіанту" + +#: src/app/main/ui/dashboard/sidebar.cljs:1138 +msgid "labels.about-penpot" +msgstr "Про Penpot" + +#: src/app/main/ui/inspect/styles/style_box.cljs:26 +msgid "labels.blur" +msgstr "Розмиття" + +#: src/app/main/ui/workspace/colorpicker.cljs:424 +msgid "labels.color" +msgstr "Колір" + +#: src/app/main/ui/dashboard/sidebar.cljs:1125 +msgid "labels.community-contributions" +msgstr "Спільнота й Внески" + +#: src/app/main/ui/inspect/right_sidebar.cljs:110 +msgid "labels.computed" +msgstr "Обчислено" + +#: src/app/main/ui/static.cljs:415 +msgid "labels.contact-support" +msgstr "Звʼязатись з підтримкою" + +#: src/app/main/ui/settings/sidebar.cljs:136 +msgid "labels.contact-us" +msgstr "Звʼяжіться з нами" + +#: src/app/main/ui/static.cljs:67 +msgid "labels.copyright-period" +msgstr "Kaleidos © 2019-дотепер" + +#: src/app/main/ui/dashboard/deleted.cljs:215 +msgid "labels.deleted" +msgstr "Вилучено" + +#: src/app/main/ui/settings/feedback.cljs:134, src/app/main/ui/static.cljs:409 +msgid "labels.download" +msgstr "Завантажити %s" + +#: src/app/main/ui/inspect/styles/style_box.cljs:23 +msgid "labels.fill" +msgstr "Заповнити" + +#: src/app/main/ui/dashboard/sidebar.cljs:1114 +msgid "labels.help-learning" +msgstr "Допомога й Навчання" + +#: src/app/main/ui/static.cljs:405 +msgid "labels.internal-error.desc-message-first" +msgstr "Сталася халепа." + +#: src/app/main/ui/static.cljs:406 +msgid "labels.internal-error.desc-message-second" +msgstr "Спробуйте знову чи повідомте службу підтримки про помилку." + +#: src/app/main/ui/inspect/styles/style_box.cljs:28 +msgid "labels.layout" +msgstr "Шаблон" + +#: src/app/main/ui/dashboard/sidebar.cljs:893 +msgid "labels.learning-center" +msgstr "Навчальний Центр" + +#: src/app/main/ui/ds/controls/numeric_input.cljs:631 +msgid "labels.mixed-values" +msgstr "Змішано" + +#: src/app/main/ui/dashboard/sidebar.cljs:973 +msgid "labels.penpot-changelog" +msgstr "Список Змін Penpot" + +#: src/app/main/ui/dashboard/sidebar.cljs:899 +msgid "labels.penpot-hub" +msgstr "Хаб Penpot" + +#: src/app/main/ui/dashboard/sidebar.cljs:846 +msgid "labels.pinned-projects" +msgstr "Пришпилені проєкти" + +#: src/app/main/ui/dashboard/deleted.cljs:208 +msgid "labels.recent" +msgstr "Нещодавні" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:205, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:179 +msgid "labels.reference" +msgstr "Посилання" + +#: src/app/main/ui/static.cljs:318 +msgid "labels.reload-page" +msgstr "Перезавантажити сторінку" + +#: src/app/main/ui/dashboard/team.cljs:788 +msgid "labels.resend" +msgstr "Перенадіслати" + +#: src/app/main/ui/inspect/styles/style_box.cljs:27, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:229 +msgid "labels.shadow" +msgstr "Тінь" + +#: src/app/main/ui/dashboard/sidebar.cljs:825 +msgid "labels.sources" +msgstr "Джерела" + +#: src/app/main/ui/inspect/styles/style_box.cljs:24, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:46 +msgid "labels.stroke" +msgstr "Обрамлення" + +#: src/app/main/ui/inspect/right_sidebar.cljs:108, src/app/main/ui/inspect/styles.cljs:135 +msgid "labels.styles" +msgstr "Стилі" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:261 +msgid "labels.switch" +msgstr "Перемикач" + +#: src/app/main/ui/inspect/styles/style_box.cljs:25 +msgid "labels.text" +msgstr "Текст" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:189 +msgid "labels.typography" +msgstr "Типографіка" + +#: src/app/main/ui/workspace/libraries.cljs:103, src/app/main/ui/workspace/libraries.cljs:130 +msgid "workspace.libraries.typography" +msgid_plural "workspace.libraries.typography" +msgstr[0] "%s типографіка" +msgstr[1] "%s типографіки" +msgstr[2] "%s типографік" + +#: src/app/main/ui/inspect/right_sidebar.cljs:66, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1039 +msgid "labels.variant" +msgstr "Варіянт" + +#: src/app/main/ui/dashboard/sidebar.cljs:967 +msgid "labels.version-notes" +msgstr "Примітки версії %s" + +#: src/app/main/ui/inspect/styles/style_box.cljs:32 +msgid "labels.visibility" +msgstr "Видимість" + +#: src/app/main/ui/dashboard/team.cljs:825 +msgid "notifications.invitation-deleted" +msgstr "Запрошення вилучено успішно" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:97 +msgid "shortcuts.create-component-variant" +msgstr "Створити компонент/варіянт" + +#: src/app/main/ui/dashboard/subscription.cljs:84 +msgid "subscription.dashboard.power-up.professional.bottom-button" +msgstr "Підсилитись!" + +#: src/app/main/ui/dashboard/subscription.cljs:83 +msgid "subscription.dashboard.power-up.professional.bottom-description" +msgstr "Отримайте більше сховища, відновлення файлів та інше для Ваших команд." + +#: src/app/main/ui/dashboard/subscription.cljs:196 +msgid "subscription.dashboard.professional-dashboard-cta-title" +msgstr "" +"Ви маєте %s редакторів поміж Вашими командами, коли як професійний план " +"покриває до 8." + +#: src/app/main/ui/dashboard/subscription.cljs:204 +#, markdown +msgid "subscription.dashboard.professional-dashboard-cta-upgrade-owner" +msgstr "" +"Будь ласка оновіться до Безлімітного чи Корпоративного щоб мати більше " +"редакторів, сховища та відновлення файлів. [Підʼєднайте зараз.|target:self]" +"(%s)" + +#: src/app/main/ui/dashboard/subscription.cljs:199 +msgid "subscription.dashboard.unlimited-dashboard-cta-title" +msgstr "" +"Ваша команда продовжує рости! Ваш безлімітний план покриває до %s " +"редакторів, проте Ви маєте %s." + +#: src/app/main/ui/dashboard/subscription.cljs:207 +#, markdown +msgid "subscription.dashboard.unlimited-dashboard-cta-upgrade-owner" +msgstr "" +"Будь ласка оновіть план щоб відповідати поточній кількості редакторів. " +"[Підʼєднайте зараз.|target:self](%s)" + +#: src/app/main/ui/dashboard/subscription.cljs:184 +msgid "subscription.dashboard.unlimited-members-extra-editors-cta-text" +msgstr "" +"Тільки нові редактори поміж Ваших команд враховуються в майбутній платіж. " +"Фіксовані $175/місяць досі застосовуються на 25+ редакторів." + +#: src/app/main/ui/dashboard/subscription.cljs:180 +msgid "subscription.dashboard.unlimited-members-extra-editors-cta-title" +msgstr "Запрошення людей з Безлімітним планом" + +#: src/app/main/ui/settings/subscription.cljs:503, src/app/main/ui/settings/subscription.cljs:513, src/app/main/ui/settings/subscription.cljs:571 +msgid "subscription.settings.enterprise.unlimited-storage-benefit" +msgstr "Безлімітне сховище" + +#: src/app/main/ui/settings/subscription.cljs:298 +msgid "subscription.settings.management-dialog.step-2-add-payment-button" +msgstr "Додати платіжні засоби" + +#: src/app/main/ui/settings/subscription.cljs:285 +msgid "subscription.settings.management-dialog.step-2-description" +msgstr "" +"Додайте платіжні засоби щоб продовжити Вашу підписку після кінця пробного " +"терміну й продовжити підтримувати наш відкритий проєкт. Плату поки не буде " +"стягнуто." + +#: src/app/main/ui/settings/subscription.cljs:293 +msgid "subscription.settings.management-dialog.step-2-skip-button" +msgstr "Пропустити й розпочати пробний період" + +#: src/app/main/ui/settings/subscription.cljs:203 +msgid "subscription.settings.management-dialog.step-2-title" +msgstr "Допоможіть нам рости й зробіть свій пробний період простіше" + +#: src/app/main/ui/settings/subscription.cljs:209 +msgid "subscription.settings.management.dialog.currently-editors-title" +msgid_plural "subscription.settings.management.dialog.currently-editors-title" +msgstr[0] "Наразі Ви маєте %s людину, які можуть редагувати, поміж Ваших команд." +msgstr[1] "Наразі Ви маєте %s людини, які можуть редагувати, поміж Ваших команд." +msgstr[2] "Наразі Ви маєте %s людей, які можуть редагувати, поміж Ваших команд." + +#: src/app/main/ui/settings/subscription.cljs:211 +msgid "subscription.settings.management.dialog.editors" +msgstr "Редактори" + +#: src/app/main/ui/settings/subscription.cljs:218 +msgid "subscription.settings.management.dialog.editors-explanation" +msgstr "(Власники, Адміни та Редаткори. Глядачі не є Редакторами)" + +#: src/app/main/ui/settings/subscription.cljs:263 +msgid "subscription.settings.management.dialog.input-error" +msgstr "" +"Ви не можете встановити менше редакторів ніж маєте зараз. Змініть роль " +"(редактор/адмін на глядача) людей, які не редагують файли, в параметрах " +"команди." + +#: src/app/main/ui/settings/subscription.cljs:471, src/app/main/ui/settings/subscription.cljs:542 +msgid "subscription.settings.professional.autosave-benefit" +msgstr "7-денні автозбережені версії та відновлення файлів" + +#: src/app/main/ui/settings/subscription.cljs:470, src/app/main/ui/settings/subscription.cljs:541 +msgid "subscription.settings.professional.storage-benefit" +msgstr "10ГБ сховища" + +#: src/app/main/ui/settings/subscription.cljs:472, src/app/main/ui/settings/subscription.cljs:543 +msgid "subscription.settings.professional.teams-editors-benefit" +msgstr "Безлімітні команди. До 8 редакторів поміж Ваших команд." + +#: src/app/main/ui/settings/subscription.cljs:50 +msgid "subscription.settings.recommended" +msgstr "Рекомендовано" + +#: src/app/main/ui/settings/subscription.cljs:343 +msgid "subscription.settings.success.dialog.thanks" +msgstr "Дякуємо за вибір плану %s Penpot!" + +#: src/app/main/ui/settings/subscription.cljs:480, src/app/main/ui/settings/subscription.cljs:492, src/app/main/ui/settings/subscription.cljs:556 +msgid "subscription.settings.unlimited.autosave-benefit" +msgstr "30-денні автозбережені версії та відновлення файлів" + +#: src/app/main/ui/settings/subscription.cljs:479, src/app/main/ui/settings/subscription.cljs:491, src/app/main/ui/settings/subscription.cljs:555 +msgid "subscription.settings.unlimited.storage-benefit" +msgstr "25ГБ сховища" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:57 +#, markdown +msgid "subscription.workspace.versions.warning.enterprise.subtext-owner" +msgstr "Якщо бажаєте збільшити цей ліміт, напишіть нам на [%s](mailto)" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:59 +#, markdown +msgid "subscription.workspace.versions.warning.subtext-member" +msgstr "" +"Якщо бажаєте збільшити цей ліміт, звʼяжіться з власником команди: [%s]" +"(mailto)" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:58 +#, markdown +msgid "subscription.workspace.versions.warning.subtext-owner" +msgstr "Якщо бажаєте збільшити цей ліміт, [оновіть план|target:self](%s)" + +#: src/app/main/ui/dashboard/team.cljs:933 +msgid "team.invitations-selected" +msgid_plural "team.invitations-selected" +msgstr[0] "%s запрошення обрано" +msgstr[1] "%s запрошення обрано" +msgstr[2] "%s запрошень обрано" + +#: src/app/main/ui/viewer/header.cljs:187 +msgid "viewer.header.edit-in-workspace" +msgstr "Змінити в робочому просторі" + +#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:81 +msgid "workspace.assets.component-group-options" +msgstr "Параметри групи компонентів" + +#: src/app/main/ui/workspace/colorpicker.cljs:428, src/app/main/ui/workspace/colorpicker.cljs:441 +msgid "workspace.colorpicker.color-tokens" +msgstr "Токени кольорів" + +#: src/app/main/ui/workspace/colorpicker.cljs:434 +msgid "workspace.colorpicker.get-color" +msgstr "Отримати колір" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:506 +msgid "workspace.component.swap.loop-error" +msgstr "Компоненти не можна вкладати в них же." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:505 +msgid "workspace.component.switch.loop-error-multi" +msgstr "Деякі копії не можна поміняти. Компоненти не можна вкладати в них же." + +#: src/app/main/ui/workspace/sidebar/debug.cljs:38 +msgid "workspace.debug.title" +msgstr "Інструменти відлагодження" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:422 +msgid "workspace.layout-grid.editor.margin.expand" +msgstr "Показати параметри внутрішнього відступу з 4 боків" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:449 +msgid "workspace.layout-item.fit-content-horizontal" +msgstr "Заповнити вміст (Горизонтально)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:477 +msgid "workspace.layout-item.fit-content-vertical" +msgstr "Заповнити вміст (Вертикально)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:465 +msgid "workspace.layout-item.fix-height" +msgstr "Зафіксувати висоту" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:439 +msgid "workspace.layout-item.fix-width" +msgstr "Зафіксувати ширину" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:471 +msgid "workspace.layout-item.height-100" +msgstr "Висота 100%" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:444 +msgid "workspace.layout-item.width-100" +msgstr "Ширина 100%" + +#: src/app/main/ui/workspace/libraries.cljs:100, src/app/main/ui/workspace/libraries.cljs:126 +msgid "workspace.libraries.colors" +msgid_plural "workspace.libraries.colors" +msgstr[0] "%s колір" +msgstr[1] "%s кольори" +msgstr[2] "%s кольорів" + +#: src/app/main/ui/workspace/libraries.cljs:94, src/app/main/ui/workspace/libraries.cljs:118 +msgid "workspace.libraries.components" +msgid_plural "workspace.libraries.components" +msgstr[0] "%s компонент" +msgstr[1] "%s компоненти" +msgstr[2] "%s компонентів" + +#: src/app/main/ui/workspace/libraries.cljs:338 +msgid "workspace.libraries.connected-to" +msgstr "Підʼєднано до" + +#: src/app/main/ui/workspace/libraries.cljs:97, src/app/main/ui/workspace/libraries.cljs:122 +msgid "workspace.libraries.graphics" +msgid_plural "workspace.libraries.graphics" +msgstr[0] "%s фігура" +msgstr[1] "%s фігури" +msgstr[2] "%s фігур" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:557 +msgid "workspace.options.component.variant.malformed.copy" +msgstr "" +"Цей компонент містить варіянти з недійсними іменами. Впевніться що кожен з " +"варіантів слідує відповідній структурі." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:560 +msgid "workspace.options.component.variant.malformed.locate" +msgstr "Знайти недійсні варіанти" + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:54 +msgid "workspace.options.component.variants-help-modal.intro" +msgstr "" +"Щоб зберегти зміни при перемиканні між варіянтами, Penpot зʼєднує шари, що:" + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:91 +msgid "workspace.options.component.variants-help-modal.outro" +msgstr "" +"Зміна одного з цих (тобто перейменування чи групування шару) ламає звʼязок, " +"та відкидання змін відновить це." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:67 +msgid "workspace.options.component.variants-help-modal.rule1" +msgstr "Розділяють одне імʼя." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:76 +msgid "workspace.options.component.variants-help-modal.rule2" +msgstr "Мають один й той самий тип." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:77 +msgid "workspace.options.component.variants-help-modal.rule2.detail" +msgstr "" +"Прямокутник, еліпс, криві та логічні операції вважаються за той самий тип." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:87 +msgid "workspace.options.component.variants-help-modal.rule3" +msgstr "Мають однаковий рівень у ієрархії." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:88 +msgid "workspace.options.component.variants-help-modal.rule3.detail" +msgstr "Групи, дошки та шаблони вважаються рівними." + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:616 +msgid "workspace.options.interaction-animation-direction-down" +msgstr "Вниз" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:593 +msgid "workspace.options.interaction-animation-direction-in" +msgstr "У" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:612 +msgid "workspace.options.interaction-animation-direction-left" +msgstr "Вліво" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:596 +msgid "workspace.options.interaction-animation-direction-out" +msgstr "З" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:608 +msgid "workspace.options.interaction-animation-direction-right" +msgstr "Вправо" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:620 +msgid "workspace.options.interaction-animation-direction-up" +msgstr "Вгору" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:265 +msgid "workspace.options.more-token-colors" +msgstr "Більше токенів кольорів" + +#: src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs:108, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:401 +msgid "workspace.options.orientation.horizontal" +msgstr "Горизонтально" + +#: src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs:104, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:397 +msgid "workspace.options.orientation.vertical" +msgstr "Вертикально" + +#: src/app/main/ui/workspace/plugins.cljs:287 +msgid "workspace.plugins.permissions.allow-localstorage" +msgstr "Зберігати дані в бровзері." + +#: src/app/main/ui/workspace/context_menu.cljs:619, src/app/main/ui/workspace/sidebar/assets/components.cljs:633, src/app/main/ui/workspace/sidebar/assets/groups.cljs:75, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1106 +msgid "workspace.shape.menu.combine-as-variants" +msgstr "Поєднати як варіянти" + +#: src/app/main/ui/workspace/sidebar/assets/components.cljs:635 +msgid "workspace.shape.menu.combine-as-variants-error" +msgstr "Компонентам потрібно знаходитись на одній сторінці" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1156 +msgid "workspace.shape.menu.remove-variant-property.last-property" +msgstr "Варіант повинен містити принаймні один параметр" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:297 +msgid "workspace.sidebar.layers.filter" +msgstr "Фільтр" + +#: src/app/main/data/workspace/tokens/errors.cljs:101, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:251 +msgid "workspace.tokens.composite-line-height-needs-font-size" +msgstr "" +"Висота Лінії залежить від Розміру Шрифта. Додайте Розмір Шрифта щоб знайти " +"значення." + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:234 +msgid "workspace.tokens.edit-token" +msgstr "Змінити токен %s" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:129 +msgid "workspace.tokens.font-size-value-enter" +msgstr "Розмір шрифту чи {alias}" + +#: src/app/main/data/workspace/tokens/application.cljs:325 +msgid "workspace.tokens.font-variant-not-found" +msgstr "" +"Помилка під час встановлення розміру/стилю шрифта. Цей стиль шрифта не існує " +"у поточному шрифті" + +#: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:42, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:137 +msgid "workspace.tokens.font-weight-value-enter" +msgstr "Вага шрифту (300, Жирний Курсив...) чи {alias}" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:233 +msgid "workspace.tokens.import-button-prefix" +msgstr "Імпортувати %s" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:273 +msgid "workspace.tokens.import-menu-folder-option" +msgstr "Тека" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:271 +msgid "workspace.tokens.import-menu-json-option" +msgstr "Єдиний файл JSON" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:272 +msgid "workspace.tokens.import-menu-zip-option" +msgstr "ZIP-архів" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:240, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:195 +msgid "workspace.tokens.individual-tokens" +msgstr "Використовувати індивідуальні токени" + +#: src/app/main/data/workspace/tokens/errors.cljs:93 +msgid "workspace.tokens.invalid-font-family-token-value" +msgstr "Недійсне значення токену: можна лише посилатись на токен сімʼї шрифтів" + +#: src/app/main/data/workspace/tokens/errors.cljs:89 +msgid "workspace.tokens.invalid-font-weight-token-value" +msgstr "" +"Недійсне значення ваги шрифту: використовуйте числове значення (100-950) чи " +"стандартні назви (тонший, тонкий, звичайний, жирний, інше.) з опціональним " +"'Курсив' перед ними" + +#: src/app/main/data/workspace/tokens/errors.cljs:105 +msgid "workspace.tokens.invalid-shadow-type-token-value" +msgstr "Недійсний тип тіні: дозволено тільки 'innerShadow' чи 'dropShadow'" + +#: src/app/main/data/workspace/tokens/errors.cljs:81 +msgid "workspace.tokens.invalid-text-case-token-value" +msgstr "" +"Недійсне значення токену: тільки верхній чи нижній регістр, великі на " +"початку чи жодне з цих" + +#: src/app/main/data/workspace/tokens/errors.cljs:85 +msgid "workspace.tokens.invalid-text-decoration-token-value" +msgstr "" +"Недійсне значення токену: тільки жодне, нижнє підкреслення та закреслення" + +#: src/app/main/data/workspace/tokens/errors.cljs:117 +msgid "workspace.tokens.invalid-token-value-shadow" +msgstr "Недійсне значення: повинно посилатись на збірний токен тіні." + +#: src/app/main/data/workspace/tokens/errors.cljs:97 +msgid "workspace.tokens.invalid-token-value-typography" +msgstr "Недійсне значення: повинно посилатись на збірний токен типографіки." + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:153 +msgid "workspace.tokens.letter-spacing-value-enter-composite" +msgstr "Літеральний відступ чи {alias}" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:145 +msgid "workspace.tokens.line-height-value-enter" +msgstr "Висота лінії (множник, px, %) чи {alias}" + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:303 +msgid "workspace.tokens.missing-reference" +msgstr "Відсутнє посилання" + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 +msgid "workspace.tokens.more-options" +msgstr "Праве натискання миші для показу параметрів" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:87 +#, unused +msgid "workspace.tokens.no-references-found" +msgstr "Не знайдено посилань" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +#, unused +msgid "workspace.tokens.no-remap-needed" +msgstr "" +"Цей токен наразі не застосовується у Вашому дизайні, потрібний перерозподіл." + +#: src/app/main/data/workspace/tokens/errors.cljs:19 +msgid "workspace.tokens.no-token-files-found" +msgstr "Жодних токенів, наборів чи тем у файлі не виявлено." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:95 +msgid "workspace.tokens.not-remap" +msgstr "Не перерозподіляти" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:178 +msgid "workspace.tokens.reference-composite" +msgstr "Введіть псевдо токену типографіки" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:204 +msgid "workspace.tokens.reference-composite-shadow" +msgstr "Введіть псевдо токену тіні" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:99 +msgid "workspace.tokens.remap" +msgstr "Перерозподілити токени" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:86 +msgid "workspace.tokens.remap-token-references-title" +msgstr "Перерозподілити усі токени що використовуть `%s` до `%s`?" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88 +msgid "workspace.tokens.remap-warning-effects" +msgstr "Це змінить усі шари й посилання що використовують старе імʼя токену." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89 +msgid "workspace.tokens.remap-warning-time" +msgstr "Ця дія може тривати довго." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:92 +#, unused +msgid "workspace.tokens.remapping-in-progress" +msgstr "Перерозподіляю посилання на токени..." + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:232 +msgid "workspace.tokens.shadow-add-shadow" +msgstr "Додати Тінь" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:161, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:162, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:165 +msgid "workspace.tokens.shadow-blur" +msgstr "Розмиття" + +#: src/app/main/data/workspace/tokens/errors.cljs:109 +msgid "workspace.tokens.shadow-blur-range" +msgstr "Розмиття тіні повинне бути більшим чи дорівнювати 0." + +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988 +#, unused +msgid "workspace.tokens.shadow-color" +msgstr "Колір" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:114 +msgid "workspace.tokens.shadow-inset" +msgstr "Вставка" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:123 +msgid "workspace.tokens.shadow-remove-shadow" +msgstr "Вилучити Тінь" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:173, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:174, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:177 +msgid "workspace.tokens.shadow-spread" +msgstr "Розкид" + +#: src/app/main/data/workspace/tokens/errors.cljs:113 +msgid "workspace.tokens.shadow-spread-range" +msgstr "Розкид тіні має бути більшим чи дорівнювати 0." + +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1215 +#, unused +msgid "workspace.tokens.shadow-title" +msgstr "Тіні" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:281 +msgid "workspace.tokens.shadow-token-blur-value-error" +msgstr "Значення розмиття не може бути відʼємним" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:287 +#, unused +msgid "workspace.tokens.shadow-token-spread-value-error" +msgstr "Значення розкиду не може бути відʼємним" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:139, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:141 +msgid "workspace.tokens.shadow-x" +msgstr "Х" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:150, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:152 +msgid "workspace.tokens.shadow-y" +msgstr "" + +#: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 +msgid "workspace.tokens.text-case-value-enter" +msgstr "жоден | верхній регістр | нижній регістр | верхня на початку чи {alias}" + +#: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:41, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:169 +msgid "workspace.tokens.text-decoration-value-enter" +msgstr "жоден | підкреслення | закреслення чи {alias}" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:51 +#, unused +msgid "workspace.tokens.theme-name-already-exists" +msgstr "Тема з цим імʼям вже існує" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:100 +#, unused +msgid "workspace.tokens.theme.disable" +msgstr "Вимкнути" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:96 +#, unused +msgid "workspace.tokens.theme.enable" +msgstr "Задіяти" + +#: src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:122, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:246 +msgid "workspace.tokens.token-font-family-select" +msgstr "Обрати сімейство шрифтів" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:121 +msgid "workspace.tokens.token-font-family-value" +msgstr "Сімейство шрифта" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:120 +msgid "workspace.tokens.token-font-family-value-enter" +msgstr "Сімейство шрифта або список шрифтів, розділені комою (,)" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:268, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:223 +msgid "workspace.tokens.token-name-duplication-validation-error" +msgstr "Токен вже існує на шляху: %s" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:265, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:220 +msgid "workspace.tokens.token-name-length-validation-error" +msgstr "Імʼя повинне містити принаймні 1 символ" + +#: src/app/main/data/workspace/tokens/import_export.cljs:46 +msgid "workspace.tokens.unknown-token-type-message" +msgstr "Імпротування пройшло успішно. Деякі токени не були додані." + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 +msgid "workspace.tokens.use-reference" +msgstr "Використати посилання" + +#: src/app/main/data/workspace/tokens/errors.cljs:69 +msgid "workspace.tokens.value-with-percent" +msgstr "Недійсне значення: %s не дозволений." + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +#, unused +msgid "workspace.tokens.warning-name-change" +msgstr "Перейменування токену зламає усі посилання на старе імʼя" + +#: src/app/main/ui/workspace/top_toolbar.cljs:231 +msgid "workspace.toolbar.debug" +msgstr "Інструменти відлагодження" + +#, unused +msgid "workspace.versions.locked-by-other" +msgstr "Ця версія заблокована %s і не може бути змінена" + +#, unused +msgid "workspace.versions.locked-by-you" +msgstr "Ця версія заблокована Вами" + +#, unused +msgid "workspace.versions.tooltip.locked-version" +msgstr "Заблокована версія - тільки творець може вносити зміни" + +#: src/app/main/ui/settings/subscription.cljs:266 +msgid "subscription.settings.management.dialog.unlimited-capped-warning" +msgstr "" +"Порада: Кількість місць можна збільшити зараз, щоб бути попереду запрошень. " +"На 25+ редакторів поміж команд, Ви насолоджуватиметесь фіксованою платою у " +"$175 на місяць." From cd3a1d63760504833fa2d9be40faf9df63030fc6 Mon Sep 17 00:00:00 2001 From: Alexis Morin Date: Fri, 27 Feb 2026 04:30:40 +0100 Subject: [PATCH 018/288] :globe_with_meridians: Add translations for: French (Canada) Currently translated at 99.7% (2068 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/ --- frontend/translations/fr_CA.po | 275 ++++++++++++++++++++++++++++++++- 1 file changed, 274 insertions(+), 1 deletion(-) diff --git a/frontend/translations/fr_CA.po b/frontend/translations/fr_CA.po index 8525255895..222abe0868 100644 --- a/frontend/translations/fr_CA.po +++ b/frontend/translations/fr_CA.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-26 14:09+0000\n" +"PO-Revision-Date: 2026-02-28 00:09+0000\n" "Last-Translator: Alexis Morin \n" "Language-Team: French (Canada) \n" @@ -8563,3 +8563,276 @@ msgstr "Texte (%s)" #: src/app/main/ui/workspace/palette.cljs:190 msgid "workspace.toolbar.text-palette" msgstr "Typographies (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:235, src/app/main/ui/workspace/top_toolbar.cljs:236 +msgid "workspace.toolbar.toggle-toolbar" +msgstr "Basculer la barre d'outils" + +#: src/app/main/ui/workspace/viewport/top_bar.cljs:41 +msgid "workspace.top-bar.read-only.done" +msgstr "Terminé" + +#: src/app/main/ui/workspace/viewport/top_bar.cljs:37 +#, markdown +msgid "workspace.top-bar.view-only" +msgstr "**Inspection de code** (Lecture seule)" + +#: src/app/main/ui/workspace/sidebar/history.cljs:333 +msgid "workspace.undo.empty" +msgstr "Il n'y a aucun changement à l'historique" + +#: src/app/main/ui/workspace/sidebar/history.cljs:147 +msgid "workspace.undo.entry.delete" +msgstr "%s supprimé" + +#: src/app/main/ui/workspace/sidebar/history.cljs:146 +msgid "workspace.undo.entry.modify" +msgstr "%s modifié" + +#: src/app/main/ui/workspace/sidebar/history.cljs:148 +msgid "workspace.undo.entry.move" +msgstr "Objets déplacés" + +#: src/app/main/ui/workspace/sidebar/history.cljs:111 +msgid "workspace.undo.entry.multiple.circle" +msgstr "cercles" + +#: src/app/main/ui/workspace/sidebar/history.cljs:112 +msgid "workspace.undo.entry.multiple.color" +msgstr "couleurs" + +#: src/app/main/ui/workspace/sidebar/history.cljs:113 +msgid "workspace.undo.entry.multiple.component" +msgstr "composants" + +#: src/app/main/ui/workspace/sidebar/history.cljs:114 +msgid "workspace.undo.entry.multiple.curve" +msgstr "courbes" + +#: src/app/main/ui/workspace/sidebar/history.cljs:115 +msgid "workspace.undo.entry.multiple.frame" +msgstr "tableau" + +#: src/app/main/ui/workspace/sidebar/history.cljs:116 +msgid "workspace.undo.entry.multiple.group" +msgstr "groupes" + +#: src/app/main/ui/workspace/sidebar/history.cljs:117 +msgid "workspace.undo.entry.multiple.media" +msgstr "atouts graphiques" + +#: src/app/main/ui/workspace/sidebar/history.cljs:118 +msgid "workspace.undo.entry.multiple.multiple" +msgstr "objets" + +#: src/app/main/ui/workspace/sidebar/history.cljs:119 +msgid "workspace.undo.entry.multiple.page" +msgstr "pages" + +#: src/app/main/ui/workspace/sidebar/history.cljs:120 +msgid "workspace.undo.entry.multiple.path" +msgstr "chemins" + +#: src/app/main/ui/workspace/sidebar/history.cljs:121 +msgid "workspace.undo.entry.multiple.rect" +msgstr "rectangles" + +#: src/app/main/ui/workspace/sidebar/history.cljs:122 +msgid "workspace.undo.entry.multiple.shape" +msgstr "formes" + +#: src/app/main/ui/workspace/sidebar/history.cljs:123 +msgid "workspace.undo.entry.multiple.text" +msgstr "textes" + +#: src/app/main/ui/workspace/sidebar/history.cljs:124 +msgid "workspace.undo.entry.multiple.typography" +msgstr "typographies" + +#: src/app/main/ui/workspace/sidebar/history.cljs:145 +msgid "workspace.undo.entry.new" +msgstr "Nouveau %s" + +#: src/app/main/ui/workspace/sidebar/history.cljs:125 +msgid "workspace.undo.entry.single.circle" +msgstr "cercle" + +#: src/app/main/ui/workspace/sidebar/history.cljs:126 +msgid "workspace.undo.entry.single.color" +msgstr "couleur" + +#: src/app/main/ui/workspace/sidebar/history.cljs:127 +msgid "workspace.undo.entry.single.component" +msgstr "composant" + +#: src/app/main/ui/workspace/sidebar/history.cljs:128 +msgid "workspace.undo.entry.single.curve" +msgstr "courbe" + +#: src/app/main/ui/workspace/sidebar/history.cljs:129 +msgid "workspace.undo.entry.single.frame" +msgstr "tableau" + +#: src/app/main/ui/workspace/sidebar/history.cljs:130 +msgid "workspace.undo.entry.single.group" +msgstr "groupe" + +#: src/app/main/ui/workspace/sidebar/history.cljs:131 +msgid "workspace.undo.entry.single.image" +msgstr "image" + +#: src/app/main/ui/workspace/sidebar/history.cljs:132 +msgid "workspace.undo.entry.single.media" +msgstr "atout graphique" + +#: src/app/main/ui/workspace/sidebar/history.cljs:133 +msgid "workspace.undo.entry.single.multiple" +msgstr "objet" + +#: src/app/main/ui/workspace/sidebar/history.cljs:134 +msgid "workspace.undo.entry.single.page" +msgstr "page" + +#: src/app/main/ui/workspace/sidebar/history.cljs:135 +msgid "workspace.undo.entry.single.path" +msgstr "chemin" + +#: src/app/main/ui/workspace/sidebar/history.cljs:136 +msgid "workspace.undo.entry.single.rect" +msgstr "rectangle" + +#: src/app/main/ui/workspace/sidebar/history.cljs:137 +msgid "workspace.undo.entry.single.shape" +msgstr "forme" + +#: src/app/main/ui/workspace/sidebar/history.cljs:138 +msgid "workspace.undo.entry.single.text" +msgstr "texte" + +#: src/app/main/ui/workspace/sidebar/history.cljs:139 +msgid "workspace.undo.entry.single.typography" +msgstr "typographie" + +#: src/app/main/ui/workspace/sidebar/history.cljs:149 +msgid "workspace.undo.entry.unknown" +msgstr "Opération sur %s" + +#: src/app/main/ui/workspace/sidebar/history.cljs:335 +#, unused +msgid "workspace.undo.title" +msgstr "Historique" + +#: src/app/main/data/workspace/libraries.cljs:1247, src/app/main/ui/workspace/sidebar/versions.cljs:85 +msgid "workspace.updates.dismiss" +msgstr "Ignorer" + +#: src/app/main/data/workspace/libraries.cljs:1245 +msgid "workspace.updates.more-info" +msgstr "Plus d'info" + +#: src/app/main/data/workspace/libraries.cljs:1243 +msgid "workspace.updates.there-are-updates" +msgstr "Mises à jour dans les bibliothèques partagées" + +#: src/app/main/data/workspace/libraries.cljs:1249 +msgid "workspace.updates.update" +msgstr "Mettre à jour" + +#: src/app/main/ui/ds/product/milestone_group.cljs:73 +msgid "workspace.versions.autosaved.entry" +msgstr "%s versions auto-sauvegardées" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:260 +msgid "workspace.versions.autosaved.version" +msgstr "Auto-sauvegardé %s" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:278 +msgid "workspace.versions.button.pin" +msgstr "Épingler cette version" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:273 +msgid "workspace.versions.button.restore" +msgstr "Restaurer cette version" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:396, src/app/main/ui/workspace/sidebar/versions.cljs:398 +msgid "workspace.versions.button.save" +msgstr "Enregistrer une version" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:405 +msgid "workspace.versions.empty" +msgstr "Aucune version" + +#: src/app/main/ui/ds/product/milestone_group.cljs:67 +msgid "workspace.versions.expand-snapshot" +msgstr "Afficher les versions" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:369 +msgid "workspace.versions.filter.all" +msgstr "Toutes les versions" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:383 +msgid "workspace.versions.filter.label" +msgstr "Filtrer les versions" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:370 +msgid "workspace.versions.filter.mine" +msgstr "Mes versions" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:374 +msgid "workspace.versions.filter.user" +msgstr "Versions de %s" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:391 +msgid "workspace.versions.loading" +msgstr "Chargement..." + +#, unused +msgid "workspace.versions.locked-by-other" +msgstr "Cette version a été verouillée par %s et ne peut être modifiée" + +#, unused +msgid "workspace.versions.locked-by-you" +msgstr "Cette version a été verouillée par toi" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:83 +msgid "workspace.versions.restore-warning" +msgstr "Restaurer cette version?" + +#, unused +msgid "workspace.versions.snapshot-menu" +msgstr "Ouvrir le menu des versions" + +#: src/app/main/ui/workspace/sidebar.cljs:257 +msgid "workspace.versions.tab.actions" +msgstr "Actions" + +#: src/app/main/ui/workspace/sidebar.cljs:255 +msgid "workspace.versions.tab.history" +msgstr "Historique" + +#, unused +msgid "workspace.versions.tooltip.locked-version" +msgstr "Version verrouillée - seule le créateur peut la modifier" + +#: src/app/main/ui/ds/product/milestone.cljs:84, src/app/main/ui/ds/product/milestone_group.cljs:86 +msgid "workspace.versions.version-menu" +msgstr "Ouvrir le menu des versions" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:60 +#, markdown +msgid "workspace.versions.warning.subtext" +msgstr "" +"Si tu aimerais augmenter cette limite, nous écrire au [support@penpot.app]" +"(%s)" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:431 +msgid "workspace.versions.warning.text" +msgstr "Les versions auto-sauvegardées seront conservées pendant %s jours." + +#, unused +msgid "workspace.viewport.click-to-close-path" +msgstr "Cliquer pour fermer le chemin" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1156 +msgid "workspace.shape.menu.remove-variant-property.last-property" +msgstr "Le variant doit avoir au moins une propriété" From bce52c6da88f7e82f4efd36469cf9a8b633aa757 Mon Sep 17 00:00:00 2001 From: Egor Filatov Date: Sat, 28 Feb 2026 09:40:52 +0100 Subject: [PATCH 019/288] :globe_with_meridians: Add translations for: Russian Currently translated at 80.0% (1660 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/ --- frontend/translations/ru.po | 105 ++++++++++++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 3 deletions(-) diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index 7fac6fb5e1..cdc01f675a 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-18 14:09+0000\n" +"PO-Revision-Date: 2026-03-01 09:09+0000\n" "Last-Translator: Egor Filatov \n" "Language-Team: Russian \n" @@ -6795,7 +6795,6 @@ msgid "dashboard.restore-all-deleted-button" msgstr "Восстановить все" #: src/app/main/data/dashboard.cljs:903 -#, fuzzy msgid "dashboard.restore-files-success-notification" msgstr "файлы %s были успешно восстановлены." @@ -6842,7 +6841,7 @@ msgid "errors.deprecated.contact.after" msgstr "чтобы мы могли вам помочь." #: src/app/main/errors.cljs:200 -#, fuzzy, unused +#, unused msgid "errors.internal-assertion-error" msgstr "Ошибка внутренней сертификации" @@ -6931,3 +6930,103 @@ msgstr "Вы собираетесь восстановить %s." #: src/app/main/ui/dashboard/file_menu.cljs:207 msgid "dashboard-restore-file-confirmation.title" msgstr "Восстановить файл" + +#: src/app/main/ui/dashboard/templates.cljs:87 +msgid "labels.show" +msgstr "Показать" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:261 +msgid "labels.switch" +msgstr "Переключить" + +#: src/app/main/ui/inspect/styles/style_box.cljs:25 +msgid "labels.text" +msgstr "Текст" + +#: src/app/main/data/workspace/tokens/errors.cljs:121 +msgid "labels.unknown-error" +msgstr "Неизвестная ошибка" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:204 +msgid "labels.unlock" +msgstr "Разблокировать" + +#: src/app/main/ui/inspect/right_sidebar.cljs:66, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1039 +msgid "labels.variant" +msgstr "Вариант" + +#: src/app/main/ui/inspect/styles/style_box.cljs:32 +msgid "labels.visibility" +msgstr "Видимость" + +#: src/app/main/ui/ds/product/loader.cljs:27 +msgid "loader.tips.04.message" +msgstr "Получите CSS и SVG код напрямую из ваших макетов." + +#: src/app/main/ui/ds/product/loader.cljs:26 +msgid "loader.tips.04.title" +msgstr "Экспорт в код" + +#: src/app/main/ui/ds/product/loader.cljs:31 +msgid "loader.tips.06.message" +msgstr "Вдохните жизнь в ваши идеи с помощью анимаций и переходов." + +#: src/app/main/ui/ds/product/loader.cljs:30 +msgid "loader.tips.06.title" +msgstr "Интерактивные прототипы" + +#: src/app/main/ui/ds/product/loader.cljs:34 +msgid "loader.tips.08.title" +msgstr "Горячие клавиши" + +#: src/app/main/ui/ds/product/loader.cljs:37 +msgid "loader.tips.09.message" +msgstr "Выберите тему, подходящую под ваш стиль." + +#: src/app/main/ui/ds/product/loader.cljs:36 +msgid "loader.tips.09.title" +msgstr "Темный & светлый режим" + +#: src/app/main/ui/ds/product/loader.cljs:39 +msgid "loader.tips.10.message" +msgstr "Дополните Penpot плагинами от сообщества для расширения функционала." + +#: src/app/main/ui/dashboard/team.cljs:222 +msgid "modals.invite-team-member.text" +msgstr "" +"Вы можете пригласить участников в команду, чтобы они получили доступ к этому " +"и другим файлам команды." + +#: src/app/main/ui/static.cljs:287 +msgid "not-found.desc-message.error" +msgstr "404 ошибка" + +#: src/app/main/ui/static.cljs:138 +msgid "not-found.login.free" +msgstr "" +"Penpot — бесплатный инструмент с открытым исходным кодом для совместной " +"работы дизайнеров и разработчиков" + +#: src/app/main/ui/auth/recovery_request.cljs:114 +msgid "not-found.login.sent-recovery" +msgstr "Вы направили письмо для восстановления на" + +#: src/app/main/ui/auth/recovery_request.cljs:116 +msgid "not-found.login.sent-recovery-check" +msgstr "Проверьте вашу почту и перейдите по ссылке, чтобы задать новый пароль." + +#: src/app/main/ui/static.cljs:152 +msgid "not-found.login.signup-free" +msgstr "Зарегистрироваться бесплатно" + +#: src/app/main/ui/static.cljs:153 +msgid "not-found.login.start-using" +msgstr "И начните использовать Penpot мгновенно!" + +#: src/app/main/ui/static.cljs:69 +msgid "not-found.made-with-love" +msgstr "Сделано с любовью и открытым исходным кодом" + +#: src/app/main/ui/static.cljs:248 +msgid "not-found.no-permission.already-requested.file" +msgstr "Вы уже запросили доступ к этому файлу." From 8262b7a3a2124631e095f9487e54f157492794c0 Mon Sep 17 00:00:00 2001 From: Denys Kisil Date: Sun, 1 Mar 2026 18:49:04 +0100 Subject: [PATCH 020/288] :globe_with_meridians: Add translations for: Ukrainian (ukr_UA) Currently translated at 99.7% (2068 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/ --- frontend/translations/ukr_UA.po | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/translations/ukr_UA.po b/frontend/translations/ukr_UA.po index 553985e072..9a3dc3a3b8 100644 --- a/frontend/translations/ukr_UA.po +++ b/frontend/translations/ukr_UA.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-26 14:10+0000\n" +"PO-Revision-Date: 2026-03-02 18:09+0000\n" "Last-Translator: Denys Kisil \n" "Language-Team: Ukrainian \n" @@ -10,7 +10,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 5.16.1-dev\n" +"X-Generator: Weblate 5.17-dev\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -8743,8 +8743,9 @@ msgid "workspace.tokens.shadow-x" msgstr "Х" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:150, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:152 +#, fuzzy msgid "workspace.tokens.shadow-y" -msgstr "" +msgstr "У" #: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 msgid "workspace.tokens.text-case-value-enter" From 8e17f846c9e2c6ff8573ca31e5edfc8ca5a73ec4 Mon Sep 17 00:00:00 2001 From: deveronica Date: Mon, 2 Mar 2026 19:09:25 +0100 Subject: [PATCH 021/288] :globe_with_meridians: Add translations for: Korean Currently translated at 12.0% (250 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/ --- frontend/translations/ko.po | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/frontend/translations/ko.po b/frontend/translations/ko.po index f590f97179..386d378934 100644 --- a/frontend/translations/ko.po +++ b/frontend/translations/ko.po @@ -1,15 +1,15 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-01-30 05:01+0000\n" -"Last-Translator: Dogyeong \n" -"Language-Team: Korean " -"\n" +"PO-Revision-Date: 2026-03-02 18:09+0000\n" +"Last-Translator: deveronica \n" +"Language-Team: Korean \n" "Language: ko\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 5.16-dev\n" +"X-Generator: Weblate 5.17-dev\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -22,7 +22,7 @@ msgstr "이메일을 확인하세요" #: src/app/main/ui/auth/register.cljs:277 #, unused msgid "auth.check-your-email" -msgstr "이메일에 포함된 링크를 클릭하여 계정을 인증하고 펜팟의 사용을 시작하십시오." +msgstr "이메일에 포함된 링크를 클릭하여 계정을 인증하고 Penpot의 사용을 시작하세요." #: src/app/main/ui/auth/recovery.cljs:67 msgid "auth.confirm-password" @@ -30,16 +30,18 @@ msgstr "비밀번호 확인하기" #: src/app/main/ui/auth/register.cljs:227 msgid "auth.create-demo-account" -msgstr "데모 계정을 생성하세요" +msgstr "데모 계정 만들기" #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs #, unused msgid "auth.create-demo-profile" -msgstr "그냥 해볼까요?" +msgstr "체험해 보고 싶으신가요?" #: src/app/main/ui/auth/login.cljs:42 msgid "auth.demo-warning" -msgstr "데모 서비스입니다. 실제 작업에 사용하지 마십시오. 생성된 프로젝트는 주기적으로 삭제될 것입니다." +msgstr "" +"이것은 데모 서비스입니다. 절대 실제 작업에 사용하지 마세요. 프로젝트는 " +"주기적으로 삭제됩니다." #: src/app/main/ui/auth/login.cljs:198, src/app/main/ui/viewer/login.cljs:86 msgid "auth.forgot-password" @@ -51,7 +53,7 @@ msgstr "이름 (성명)" #: src/app/main/ui/auth/login.cljs:271 msgid "auth.login-account-title" -msgstr "내 계정에 로그인하기" +msgstr "내 계정에 로그인" #: src/app/main/ui/auth/register.cljs:219, src/app/main/ui/static.cljs:161, src/app/main/ui/viewer/login.cljs:103 msgid "auth.login-here" @@ -92,7 +94,7 @@ msgstr "새 비밀번호를 입력하세요" #: src/app/main/ui/auth/recovery.cljs:36 msgid "auth.notifications.password-changed-successfully" -msgstr "비밀번호가 성공적으로 변경되었어요" +msgstr "비밀번호가 성공적으로 변경되었습니다" #: src/app/main/ui/auth/recovery_request.cljs:50 msgid "auth.notifications.profile-not-verified" From 0be5119b21328252c372fae7d8fdb281a7a1de60 Mon Sep 17 00:00:00 2001 From: deveronica Date: Tue, 3 Mar 2026 20:00:59 +0100 Subject: [PATCH 022/288] :globe_with_meridians: Add translations for: Korean Currently translated at 14.1% (294 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/ --- frontend/translations/ko.po | 389 ++++++++++++++++++++++++++++-------- 1 file changed, 304 insertions(+), 85 deletions(-) diff --git a/frontend/translations/ko.po b/frontend/translations/ko.po index 386d378934..53e7def20a 100644 --- a/frontend/translations/ko.po +++ b/frontend/translations/ko.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-03-02 18:09+0000\n" +"PO-Revision-Date: 2026-03-03 19:09+0000\n" "Last-Translator: deveronica \n" "Language-Team: Korean \n" @@ -26,11 +26,11 @@ msgstr "이메일에 포함된 링크를 클릭하여 계정을 인증하고 Pen #: src/app/main/ui/auth/recovery.cljs:67 msgid "auth.confirm-password" -msgstr "비밀번호 확인하기" +msgstr "비밀번호 확인" #: src/app/main/ui/auth/register.cljs:227 msgid "auth.create-demo-account" -msgstr "데모 계정 만들기" +msgstr "데모 계정 생성" #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs #, unused @@ -40,12 +40,12 @@ msgstr "체험해 보고 싶으신가요?" #: src/app/main/ui/auth/login.cljs:42 msgid "auth.demo-warning" msgstr "" -"이것은 데모 서비스입니다. 절대 실제 작업에 사용하지 마세요. 프로젝트는 " +"이것은 데모 서비스이며, 절대 실제 작업에 사용하지 마십시오. 프로젝트는 " "주기적으로 삭제됩니다." #: src/app/main/ui/auth/login.cljs:198, src/app/main/ui/viewer/login.cljs:86 msgid "auth.forgot-password" -msgstr "비밀번호를 잊어버리셨나요?" +msgstr "비밀번호를 잊으셨나요?" #: src/app/main/ui/auth/register.cljs:160, src/app/main/ui/auth/register.cljs:328 msgid "auth.fullname" @@ -66,7 +66,7 @@ msgstr "로그인" #: src/app/main/ui/auth/login.cljs:274 msgid "auth.login-tagline" -msgstr "펜팟은 디자인과 코딩의 협업을 위한 무료 오픈소스 디자인 도구입니다" +msgstr "Penpot은 디자인과 코드 협업을 위한 무료 오픈소스 디자인 도구입니다" #: src/app/main/ui/auth/login.cljs:231 msgid "auth.login-with-github-submit" @@ -98,15 +98,15 @@ msgstr "비밀번호가 성공적으로 변경되었습니다" #: src/app/main/ui/auth/recovery_request.cljs:50 msgid "auth.notifications.profile-not-verified" -msgstr "프로필이 검증되지 않았어요. 계속 하려면 검증절차를 완료해주세요." +msgstr "프로필이 인증되지 않았습니다. 계속하기 전에 프로필을 인증하세요." #: src/app/main/ui/auth/recovery_request.cljs:33 msgid "auth.notifications.recovery-token-sent" -msgstr "비밀번호 복구를 위한 링크를 메일함으로 보냈어요." +msgstr "비밀번호 복구 링크가 메일함으로 전송되었습니다." #: src/app/main/ui/auth/verify_token.cljs:49 msgid "auth.notifications.team-invitation-accepted" -msgstr "팀에 성공적으로 합류했어요" +msgstr "팀에 성공적으로 참가했습니다" #: src/app/main/ui/auth/login.cljs:188, src/app/main/ui/auth/register.cljs:174 msgid "auth.password" @@ -114,19 +114,19 @@ msgstr "비밀번호" #: src/app/main/ui/auth/register.cljs:173 msgid "auth.password-length-hint" -msgstr "최소 8개의 문자" +msgstr "최소 8자 이상" #: src/app/main/ui/auth/register.cljs:261 msgid "auth.privacy-policy" -msgstr "개인 정보 정책" +msgstr "개인정보 처리방침" #: src/app/main/ui/auth/recovery_request.cljs:82 msgid "auth.recovery-request-submit" -msgstr "비밀번호 복구하기" +msgstr "비밀번호 복구" #: src/app/main/ui/auth/recovery_request.cljs:95 msgid "auth.recovery-request-subtitle" -msgstr "이용지침을 메일로 전달해드릴거에요" +msgstr "안내 사항이 담긴 이메일을 보내드리겠습니다" #: src/app/main/ui/auth/recovery_request.cljs:94 msgid "auth.recovery-request-title" @@ -134,58 +134,61 @@ msgstr "비밀번호를 잊으셨나요?" #: src/app/main/ui/auth/recovery.cljs:71 msgid "auth.recovery-submit" -msgstr "비밀번호를 바꾸세요" +msgstr "비밀번호 변경" #: src/app/main/ui/auth/login.cljs:287, src/app/main/ui/static.cljs:144, src/app/main/ui/viewer/login.cljs:89 msgid "auth.register" -msgstr "아직 계정이 없으신가요?" +msgstr "계정이 없으신가요?" #: src/app/main/ui/auth/register.cljs:351 msgid "auth.register-account-tagline" -msgstr "대시보드와 이메일 에서 당신을 어떻게 호칭할지 저희에게 알려주세요." +msgstr "대시보드와 이메일에 표시될 이름을 입력해주세요." #: src/app/main/ui/auth/register.cljs:350 +#, fuzzy msgid "auth.register-account-title" msgstr "당신의 이름" #: src/app/main/ui/auth/login.cljs:291, src/app/main/ui/auth/register.cljs:185, src/app/main/ui/auth/register.cljs:337, src/app/main/ui/static.cljs:148, src/app/main/ui/viewer/login.cljs:93 msgid "auth.register-submit" -msgstr "계정을 생성하세요" +msgstr "계정 생성" #: src/app/main/ui/auth/register.cljs:124 #, unused msgid "auth.register-tagline" -msgstr "펜팟 무료 계정과 함께라면, 무제한으로 팀을 만들고 다른 디자이너 및 개발자와 원하는 만큼 프로젝트에서 협업할 수 있습니다. " +msgstr "" +"무료 Penpot 계정으로 팀을 무제한 생성하고, 원하는 만큼 많은 프로젝트에서 " +"다른 디자이너 및 개발자와 협업할 수 있습니다. " #: src/app/main/ui/auth/register.cljs:206 msgid "auth.register-title" -msgstr "계정을 생성하세요" +msgstr "계정 생성" #: src/app/main/ui/auth.cljs #, unused msgid "auth.sidebar-tagline" -msgstr "디자인과 프로토타이핑을 위한 오픈소스 솔루션." +msgstr "디자인 및 프로토타이핑을 위한 오픈소스 솔루션." #: src/app/main/ui/auth/register.cljs:51 #, markdown msgid "auth.terms-and-privacy-agreement" -msgstr "[서비스 약관](%s) 및 [개인정보 처리방침](%s)에 동의합니다." +msgstr "[서비스 이용약관](%s) 및 [개인정보 처리방침](%s)에 동의합니다." #: src/app/main/ui/auth/register.cljs:253, src/app/main/ui/dashboard/sidebar.cljs:979, src/app/main/ui/workspace/main_menu.cljs:184 msgid "auth.terms-of-service" -msgstr "서비스 정책" +msgstr "서비스 이용약관" #, unused msgid "auth.terms-privacy-agreement" -msgstr "새로운 계정을 생성하시면, 사용자는 펜팟의 서비스 정책과 개인 정보 정책에 동의하는 것으로 간주됩니다." +msgstr "새 계정을 만들면 당사의 이용약관 및 개인정보 처리방침에 동의하게 됩니다." #: src/app/main/ui/auth/register.cljs:239 msgid "auth.verification-email-sent" -msgstr "검증 메일을 ~에 보냈어요" +msgstr "다음 이메일로 인증 메일을 보냈습니다:" #: src/app/main/ui/auth/login.cljs:179, src/app/main/ui/auth/recovery_request.cljs:77, src/app/main/ui/auth/register.cljs:167 msgid "auth.work-email" -msgstr "작업용 이메일" +msgstr "업무 이메일" #: src/app/main/ui/onboarding/questions.cljs #, unused @@ -194,7 +197,7 @@ msgstr "...브랜딩, 일러스트레이션, 마케팅 자료 등." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 msgid "color-row.token-color-row.deleted-token" -msgstr "이 토큰은 존재하지 않거나 삭제되었습니다." +msgstr "해당 token이 존재하지 않거나 삭제되었습니다." #: src/app/main/ui/comments.cljs:530 msgid "comments.mentions.not-found" @@ -202,15 +205,15 @@ msgstr "@%s 사용자를 찾을 수 없습니다" #: src/app/main/ui/workspace/libraries.cljs:323 msgid "common.publish" -msgstr "발행하기" +msgstr "게시하기" #: src/app/main/ui/viewer/share_link.cljs:304, src/app/main/ui/viewer/share_link.cljs:315 msgid "common.share-link.all-users" -msgstr "모든 펜팟 유저들" +msgstr "모든 Penpot 사용자" #: src/app/main/ui/viewer/share_link.cljs:204 msgid "common.share-link.confirm-deletion-link-description" -msgstr "정말로 링크를 제거하고 싶으세요? 제거하시면, 더이상 아무도 이용할 수 없어요" +msgstr "이 링크를 삭제하시겠습니까? 삭제하면 더 이상 누구도 접근할 수 없습니다" #: src/app/main/ui/viewer/share_link.cljs:259, src/app/main/ui/viewer/share_link.cljs:289 msgid "common.share-link.current-tag" @@ -218,60 +221,60 @@ msgstr "(현재)" #: src/app/main/ui/viewer/share_link.cljs:211, src/app/main/ui/viewer/share_link.cljs:216 msgid "common.share-link.destroy-link" -msgstr "링크 제거하기" +msgstr "링크 삭제" #: src/app/main/ui/viewer/share_link.cljs:221 msgid "common.share-link.get-link" -msgstr "링크 얻기" +msgstr "링크 가져오기" #: src/app/main/ui/viewer/share_link.cljs:142 msgid "common.share-link.link-copied-success" -msgstr "링크를 성공적으로 복사했어요" +msgstr "링크가 성공적으로 복사되었습니다" #: src/app/main/ui/viewer/share_link.cljs:231 msgid "common.share-link.manage-ops" -msgstr "권한을 관리하세요" +msgstr "권한 관리" #: src/app/main/ui/viewer/share_link.cljs:277 msgid "common.share-link.page-shared" msgid_plural "common.share-link.page-shared" -msgstr[0] "%s 페이지가 공유되었습니다" +msgstr[0] "%s개의 페이지가 공유되었습니다" #: src/app/main/ui/viewer/share_link.cljs:298 msgid "common.share-link.permissions-can-comment" -msgstr "코멘트를 달 수 있어요" +msgstr "댓글 작성 가능" #: src/app/main/ui/viewer/share_link.cljs:309 msgid "common.share-link.permissions-can-inspect" -msgstr "코드를 검사할 수 있어요" +msgstr "코드 검사 가능" #: src/app/main/ui/viewer/share_link.cljs:199 msgid "common.share-link.permissions-hint" -msgstr "링크를 가진 누구나 접근할 수 있어요" +msgstr "링크가 있는 누구나 접근 가능합니다" #: src/app/main/ui/viewer/share_link.cljs:241 msgid "common.share-link.permissions-pages" -msgstr "페이지가 공유됐어요" +msgstr "공유된 페이지" #: src/app/main/ui/viewer/share_link.cljs:189 msgid "common.share-link.placeholder" -msgstr "공유할 수 있는 링크는 여기 나타날거에요" +msgstr "공유 가능한 링크가 여기에 표시됩니다" #: src/app/main/ui/viewer/share_link.cljs:303, src/app/main/ui/viewer/share_link.cljs:314 msgid "common.share-link.team-members" -msgstr "오직 팀원들을 위해" +msgstr "팀 구성원만" #: src/app/main/ui/viewer/share_link.cljs:176 msgid "common.share-link.title" -msgstr "프로토타입을 공유해요" +msgstr "프로토타입 공유" #: src/app/main/ui/viewer/share_link.cljs:269 msgid "common.share-link.view-all" -msgstr "모두 선택해요" +msgstr "모두 선택" #: src/app/main/ui/workspace/libraries.cljs:320 msgid "common.unpublish" -msgstr "발행취소하기" +msgstr "게시 취소" #: src/app/main/ui/dashboard/projects.cljs:93 msgid "dasboard.team-hero.management" @@ -279,36 +282,38 @@ msgstr "팀 관리" #: src/app/main/ui/dashboard/projects.cljs:92 msgid "dasboard.team-hero.text" -msgstr "펜팟은 팀을 위한 도구입니다. 팀원들을 초대하여 프로젝트 및 파일 단위로 협업하십시오" +msgstr "" +"Penpot은 팀을 위한 도구입니다. 프로젝트와 파일에서 함께 작업할 수 있도록 " +"구성원을 초대하세요" #: src/app/main/ui/dashboard/projects.cljs:90 msgid "dasboard.team-hero.title" -msgstr "팀을 이뤄요!" +msgstr "팀을 구성하세요!" #: src/app/main/ui/dashboard/projects.cljs #, unused msgid "dasboard.tutorial-hero.info" -msgstr "본 실습용 튜토리얼을 통해 펜팟의 기본 기능에 대하여 재미있게 학습하십시오." +msgstr "Penpot의 기본 기능을 실습 튜토리얼로 재미있게 배워보세요." #: src/app/main/ui/dashboard/projects.cljs #, unused msgid "dasboard.tutorial-hero.start" -msgstr "튜토리얼을 시작하세요" +msgstr "튜토리얼 시작" #: src/app/main/ui/dashboard/projects.cljs #, unused msgid "dasboard.tutorial-hero.title" -msgstr "실습용 튜토리얼" +msgstr "실습 튜토리얼" #: src/app/main/ui/dashboard/projects.cljs #, unused msgid "dasboard.walkthrough-hero.info" -msgstr "펜팟을 둘러보고 주요 기능에 대한 정보를 습득하십시오." +msgstr "Penpot을 둘러보고 주요 기능을 살펴보세요." #: src/app/main/ui/dashboard/projects.cljs #, unused msgid "dasboard.walkthrough-hero.start" -msgstr "투어를 시작해요" +msgstr "투어 시작" #: src/app/main/ui/dashboard/projects.cljs #, unused @@ -317,7 +322,7 @@ msgstr "인터페이스 둘러보기" #: src/app/main/ui/dashboard/file_menu.cljs:208 msgid "dashboard-restore-file-confirmation.description" -msgstr "%s 파일을 복원하려 합니다." +msgstr "%s 파일이 복원됩니다." #: src/app/main/ui/dashboard/file_menu.cljs:207 msgid "dashboard-restore-file-confirmation.title" @@ -325,23 +330,23 @@ msgstr "파일 복원" #: src/app/main/ui/settings/access_tokens.cljs:103 msgid "dashboard.access-tokens.copied-success" -msgstr "복사된 토큰" +msgstr "토큰 복사됨" #: src/app/main/ui/settings/access_tokens.cljs:189 msgid "dashboard.access-tokens.create" -msgstr "새로운 토큰 생성하기" +msgstr "새로운 토큰 생성" #: src/app/main/ui/settings/access_tokens.cljs:64 msgid "dashboard.access-tokens.create.success" -msgstr "엑세스 토큰이 성공적으로 생성되었습니다." +msgstr "액세스 토큰이 성공적으로 생성되었습니다." #: src/app/main/ui/settings/access_tokens.cljs:286 msgid "dashboard.access-tokens.empty.add-one" -msgstr "\"새로운 토큰 생성하기\" 버튼을 눌러 토큰을 생성하십시오." +msgstr "\"새로운 토큰 생성\" 버튼을 눌러 토큰을 생성하세요." #: src/app/main/ui/settings/access_tokens.cljs:285 msgid "dashboard.access-tokens.empty.no-access-tokens" -msgstr "현재 가지고 있는 토큰이 없습니다." +msgstr "아직 생성된 토큰이 없습니다." #: src/app/main/ui/settings/access_tokens.cljs:135 msgid "dashboard.access-tokens.expiration-180-days" @@ -361,37 +366,37 @@ msgstr "90일" #: src/app/main/ui/settings/access_tokens.cljs:131 msgid "dashboard.access-tokens.expiration-never" -msgstr "기한 없음" +msgstr "만료 없음" #: src/app/main/ui/settings/access_tokens.cljs:268 msgid "dashboard.access-tokens.expired-on" -msgstr "%s에 만료되었습니다" +msgstr "%s에 만료됨" #: src/app/main/ui/settings/access_tokens.cljs:269 msgid "dashboard.access-tokens.expires-on" -msgstr "%s에 만료됩니다" +msgstr "%s에 만료 예정" #: src/app/main/ui/settings/access_tokens.cljs:267 msgid "dashboard.access-tokens.no-expiration" -msgstr "만료 기한 없음" +msgstr "만료일 없음" #: src/app/main/ui/settings/access_tokens.cljs:184 msgid "dashboard.access-tokens.personal" -msgstr "개인용 엑세스 토큰" +msgstr "개인용 액세스 토큰" #: src/app/main/ui/settings/access_tokens.cljs:185 msgid "dashboard.access-tokens.personal.description" msgstr "" -"개인용 엑세스 토큰은 펜팟의 로그인/암호 인증 시스템의 대안으로 사용되며, 어플리케이션의 펜팟 내부 API 엑세스를 위해 사용될 수 " -"있습니다" +"개인용 엑세스 토큰은 로그인/비밀번호 기반 인증을 대신할 수 있는 인증 " +"수단이고, 이를 통해 애플리케이션이 내부 Penpot API에 접근할 수 있습니다" #: src/app/main/ui/settings/access_tokens.cljs:142 msgid "dashboard.access-tokens.token-will-expire" -msgstr "토큰은 %s에 만료 예정입니다" +msgstr "해당 토큰은 %s에 만료됩니다" #: src/app/main/ui/settings/access_tokens.cljs:143 msgid "dashboard.access-tokens.token-will-not-expire" -msgstr "토큰의 만료 기한이 없습니다" +msgstr "해당 토큰은 만료일이 없습니다" #: src/app/main/ui/dashboard/placeholder.cljs:41 msgid "dashboard.add-file" @@ -399,11 +404,11 @@ msgstr "파일 추가" #: src/app/main/ui/dashboard/file_menu.cljs:322, src/app/main/ui/workspace/main_menu.cljs:650 msgid "dashboard.add-shared" -msgstr "공유 라이브러리로 추가하기" +msgstr "공유 라이브러리로 추가" #: src/app/main/ui/settings/profile.cljs:75 msgid "dashboard.change-email" -msgstr "이메일을 변경해요" +msgstr "이메일 변경" #: src/app/main/ui/dashboard/deleted.cljs:313 msgid "dashboard.clear-trash-button" @@ -415,7 +420,7 @@ msgstr "(복사)" #: src/app/main/ui/dashboard/sidebar.cljs:340 msgid "dashboard.create-new-team" -msgstr "새 팀을 생성해요" +msgstr "새 팀 생성" #: src/app/main/ui/workspace/main_menu.cljs:661 msgid "dashboard.create-version-menu" @@ -423,7 +428,7 @@ msgstr "이 버전 고정" #: src/app/main/ui/components/context_menu_a11y.cljs:300, src/app/main/ui/dashboard/sidebar.cljs:638 msgid "dashboard.default-team-name" -msgstr "당신의 펜팟" +msgstr "내 Penpot" #: src/app/main/ui/dashboard/deleted.cljs:262 msgid "dashboard.delete-all-forever-confirmation.description" @@ -435,50 +440,50 @@ msgstr "%s 파일을 영구적으로 삭제하시겠습니까? 이 작업은 되 #: src/app/main/data/dashboard.cljs:778 msgid "dashboard.delete-files-success-notification" -msgstr "%s 파일이 성공적으로 삭제되었습니다." +msgstr "%s개 파일이 성공적으로 삭제되었습니다." #: src/app/main/ui/dashboard/deleted.cljs:51, src/app/main/ui/dashboard/deleted.cljs:53, src/app/main/ui/dashboard/deleted.cljs:261, src/app/main/ui/dashboard/deleted.cljs:263, src/app/main/ui/dashboard/file_menu.cljs:220, src/app/main/ui/dashboard/file_menu.cljs:222 msgid "dashboard.delete-forever-confirmation.title" -msgstr "영구적으로 삭제" +msgstr "영구 삭제" #: src/app/main/ui/dashboard/deleted.cljs:85 msgid "dashboard.delete-project-button" -msgstr "프로젝트 제거" +msgstr "프로젝트 삭제" #: src/app/main/ui/dashboard/deleted.cljs:52 msgid "dashboard.delete-project-forever-confirmation.description" msgstr "" -"%s 프로젝트를 영구적으로 삭제하시겠습니까? 이 프로젝트와 안에 포함된 모든 파일이 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 " -"없습니다." +"%s 프로젝트를 영구적으로 삭제하시겠습니까? 해당 프로젝트와 그 안에 포함된 " +"모든 파일이 함께 영구 삭제됩니다. 이 작업은 되돌릴 수 없습니다." #: src/app/main/data/dashboard.cljs:777, src/app/main/data/dashboard.cljs:811 msgid "dashboard.delete-success-notification" -msgstr "%s가 영구적으로 삭제되었습니다." +msgstr "%s가 성공적으로 삭제되었습니다." #: src/app/main/ui/dashboard/sidebar.cljs:495 msgid "dashboard.delete-team" -msgstr "팀을 해체해요" +msgstr "팀 삭제" #: src/app/main/ui/dashboard/deleted.cljs:327 msgid "dashboard.deleted.empty-state-description" -msgstr "휴지통이 비어 있습니다. 삭제된 파일과 프로젝트가 여기에 표시됩니다." +msgstr "휴지통이 비어있습니다. 삭제된 파일 및 프로젝트가 여기에 표시됩니다." #: src/app/main/ui/dashboard/file_menu.cljs:328, src/app/main/ui/workspace/main_menu.cljs:690 msgid "dashboard.download-binary-file" -msgstr "펜팟 파일(.penpot)을 다운로드해요" +msgstr "Penpot 파일(.penpot) 다운로드" #: src/app/main/ui/dashboard/file_menu.cljs:321, src/app/main/ui/workspace/main_menu.cljs:712 #, unused msgid "dashboard.download-standard-file" -msgstr "표준 파일(.svg + .json)을 다운로드해요" +msgstr "표준 파일(.svg + .json) 다운로드" #: src/app/main/ui/dashboard/file_menu.cljs:304, src/app/main/ui/dashboard/project_menu.cljs:92 msgid "dashboard.duplicate" -msgstr "복제해요" +msgstr "복제" #: src/app/main/ui/dashboard/file_menu.cljs:271 msgid "dashboard.duplicate-multi" -msgstr "%파일을 복제해요" +msgstr "%s개 파일 복제" #: src/app/main/ui/dashboard/placeholder.cljs:111 msgid "dashboard.empty-placeholder-libraries-title" @@ -486,19 +491,19 @@ msgstr "아직 라이브러리가 없습니다." #: src/app/main/ui/dashboard/file_menu.cljs:280 msgid "dashboard.export-binary-multi" -msgstr "%s 펜팟 파일 (.penpot) 다운로드 하기" +msgstr "%s 펜팟 파일 (.penpot) 다운로드" #: src/app/main/ui/workspace/main_menu.cljs:698 msgid "dashboard.export-frames" -msgstr "대지를 PDF로 내보내요" +msgstr "보드를 PDF로 내보내기" #: src/app/main/ui/exports/assets.cljs:201 msgid "dashboard.export-frames.title" -msgstr "PDF로 내보내요" +msgstr "PDF로 내보내기" #: src/app/main/ui/workspace/main_menu.cljs:679 msgid "dashboard.export-shapes" -msgstr "내보내요" +msgstr "내보내기" #: src/app/main/ui/dashboard/sidebar.cljs:858 msgid "dashboard.no-projects-placeholder" @@ -1043,3 +1048,217 @@ msgstr "그룹" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:123 msgid "shortcuts.h-distribute" msgstr "가로로 분배하기" + +#: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 +msgid "color-token.empty-state" +msgstr "" +"사용 가능한 색상 token이 없습니다. 활성 세트/테마를 확인하거나 새로운 " +"token을 추가하세요." + +#: src/app/main/ui/dashboard/sidebar.cljs:347 +msgid "dashboard.create-new-org" +msgstr "새 조직 만들기" + +#: src/app/main/ui/dashboard/grid.cljs:248 +msgid "dashboard.deleted.will-be-deleted-at" +msgstr "%s에 삭제될 예정" + +#: src/app/main/ui/dashboard/files.cljs:203, src/app/main/ui/dashboard/projects.cljs:290 +msgid "dashboard.empty-placeholder-drafts-subtitle" +msgstr "프로젝트 구성원이 초안을 생성하면 이곳에 표시됩니다." + +#: src/app/main/ui/dashboard/files.cljs:198, src/app/main/ui/dashboard/projects.cljs:285 +msgid "dashboard.empty-placeholder-drafts-title" +msgstr "아직 초안이 없습니다." + +#: src/app/main/ui/dashboard/deleted.cljs:176, src/app/main/ui/dashboard/files.cljs:204, src/app/main/ui/dashboard/projects.cljs:291 +msgid "dashboard.empty-placeholder-files-subtitle" +msgstr "프로젝트 구성원이 파일을 생성하면 이곳에 표시됩니다." + +#: src/app/main/ui/dashboard/deleted.cljs:173, src/app/main/ui/dashboard/files.cljs:199, src/app/main/ui/dashboard/projects.cljs:286 +msgid "dashboard.empty-placeholder-files-title" +msgstr "아직 파일이 없습니다." + +#: src/app/main/ui/dashboard/placeholder.cljs:118 +#, markdown +msgid "dashboard.empty-placeholder-libraries" +msgstr "" +"프로젝트에 추가된 라이브러리가 여기에 표시됩니다. 파일을 공유해보거나, " +"[Libraries & templates](https://penpot.app/libraries-templates)에서 " +"추가해보세요." + +#: src/app/main/ui/dashboard/placeholder.cljs +#, markdown, unused +msgid "dashboard.empty-placeholder-libraries-subtitle" +msgstr "" +"프로젝트에 추가된 라이브러리가 여기에 표시됩니다. 파일을 공유하거나 라이브러리 " +"및 템플릿에서 추가해 보세요." + +#: src/app/main/ui/dashboard/placeholder.cljs:114 +msgid "dashboard.empty-placeholder-libraries-subtitle-viewer-role" +msgstr "프로젝트에 추가된 라이브러리가 여기에 표시됩니다." + +#: src/app/main/ui/dashboard/placeholder.cljs:59 +msgid "dashboard.empty-project.add-library" +msgstr "라이브러리 또는 템플릿 추가" + +#: src/app/main/ui/dashboard/placeholder.cljs:43, src/app/main/ui/dashboard/placeholder.cljs:134 +msgid "dashboard.empty-project.create" +msgstr "새 파일 생성" + +#: src/app/main/ui/dashboard/placeholder.cljs:61 +msgid "dashboard.empty-project.explore" +msgstr "몇 가지를 둘러보고 추가해보세요" + +#: src/app/main/ui/dashboard/placeholder.cljs:57 +msgid "dashboard.empty-project.go-to-libraries" +msgstr "Libraries and Templates로 이동" + +#: src/app/main/ui/dashboard/placeholder.cljs:49, src/app/main/ui/dashboard/placeholder.cljs:51 +msgid "dashboard.empty-project.import" +msgstr "파일 가져오기" + +#: src/app/main/ui/dashboard/placeholder.cljs:53 +msgid "dashboard.empty-project.import-penpot" +msgstr ".penpot 파일 가져오기" + +#: src/app/main/ui/dashboard/placeholder.cljs:45 +msgid "dashboard.empty-project.start" +msgstr "놀라운 것들을 만들기 시작하세요" + +#, unused +msgid "dashboard.errors.error-on-delete-file" +msgstr "%s 파일 삭제 중 오류가 발생했습니다." + +#: src/app/main/data/dashboard.cljs:781 +msgid "dashboard.errors.error-on-delete-files" +msgstr "파일을 삭제하는 중 오류가 발생했습니다." + +#: src/app/main/data/dashboard.cljs:814 +msgid "dashboard.errors.error-on-delete-project" +msgstr "%s 프로젝트를 삭제하는 중 오류가 발생했습니다." + +#: src/app/main/data/dashboard.cljs:909, src/app/main/ui/dashboard/file_menu.cljs:201 +msgid "dashboard.errors.error-on-restore-file" +msgstr "%s 파일을 복원하는 중 오류가 발생했습니다." + +#: src/app/main/data/dashboard.cljs:910 +msgid "dashboard.errors.error-on-restore-files" +msgstr "파일을 복원하는 중 오류가 발생했습니다." + +#: src/app/main/data/dashboard.cljs:942 +msgid "dashboard.errors.error-on-restoring-project" +msgstr "%s 프로젝트와 해당 파일을 복원하는 중 오류가 발생했습니다." + +#, unused +msgid "dashboard.export-multi" +msgstr "Penpot 파일 %s개 내보내기" + +#: src/app/main/ui/exports/assets.cljs:108 +msgid "dashboard.export-multiple.selected" +msgstr "전체 %s개 중 %s개 요소가 선택됨" + +#: src/app/main/ui/exports/assets.cljs:179 +msgid "dashboard.export-shapes.how-to" +msgstr "" +"오른쪽 사이드바 하단의 디자인 속성에서 요소에 내보내기 설정을 추가할 수 " +"있습니다." + +#: src/app/main/ui/exports/assets.cljs:183 +msgid "dashboard.export-shapes.how-to-link" +msgstr "Penpot에서 내보내기 설정 방법." + +#: src/app/main/ui/exports/assets.cljs:178 +msgid "dashboard.export-shapes.no-elements" +msgstr "내보내기 설정이 지정된 요소가 없습니다." + +#: src/app/main/ui/exports/assets.cljs:189 +msgid "dashboard.export-shapes.title" +msgstr "선택 항목 내보내기" + +#: src/app/main/ui/dashboard/file_menu.cljs:262 +#, unused +msgid "dashboard.export-standard-multi" +msgstr "표준 파일 %s개(.svg + .json) 다운로드" + +#: src/app/main/ui/exports/files.cljs:155 +#, unused +msgid "dashboard.export.explain" +msgstr "" +"다운로드하려는 파일 중 하나 이상이 공유 라이브러리를 사용하고 있습니다. 해당 " +"라이브러리의 에셋을 어떻게 처리하시겠습니까?" + +#: src/app/main/ui/dashboard/file_menu.cljs:266 +msgid "dashboard.file-menu.delete-files-permanently-option" +msgid_plural "dashboard.file-menu.delete-files-permanently-option" +msgstr[0] "파일 삭제" + +#: src/app/main/ui/dashboard/file_menu.cljs:263 +msgid "dashboard.file-menu.restore-files-option" +msgid_plural "dashboard.file-menu.restore-files-option" +msgstr[0] "파일 복원" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:322 +msgid "dashboard.fonts.deleted-placeholder" +msgstr "누락된 글꼴" + +#: src/app/main/ui/dashboard/fonts.cljs:218 +msgid "dashboard.fonts.dismiss-all" +msgstr "모두 닫기" + +#: src/app/main/ui/dashboard/fonts.cljs:455 +msgid "dashboard.fonts.empty-placeholder" +msgstr "업로드한 사용자 지정 글꼴이 이곳에 표시됩니다." + +#: src/app/main/ui/dashboard/fonts.cljs:458 +msgid "dashboard.fonts.empty-placeholder-viewer" +msgstr "아직 사용자 지정 폰트가 없습니다." + +#: src/app/main/ui/dashboard/fonts.cljs:459 +msgid "dashboard.fonts.empty-placeholder-viewer-sub" +msgstr "프로젝트 구성원이 사용자 지정 폰트를 업로드하면 이곳에 표시됩니다." + +#: src/app/main/ui/dashboard/fonts.cljs:206 +msgid "dashboard.fonts.fonts-added" +msgid_plural "dashboard.fonts.fonts-added" +msgstr[0] "글꼴 %s개 추가됨" + +#: src/app/main/ui/dashboard/fonts.cljs:181 +#, markdown +msgid "dashboard.fonts.hero-text1" +msgstr "" +"여기에 업로드한 웹 폰트는 이 팀의 파일에서 텍스트 속성의 글꼴 목록에 " +"추가됩니다. 같은 글꼴 패밀리 이름을 가진 폰트는 **하나의 글꼴 패밀리**로 " +"묶입니다. 업로드 가능한 형식은 **TTF, OTF, WOFF**이며, 이 중 하나만 있으면 " +"됩니다." + +#: src/app/main/ui/dashboard/fonts.cljs:194 +#, markdown +msgid "dashboard.fonts.hero-text2" +msgstr "" +"Penpot에서 사용 권한이 있는 글꼴만 업로드해야 합니다. 자세한 내용은 " +"[Penpot's Terms of Service](%s)의 콘텐츠 권리 섹션을 확인하세요. 또한 [font " +"licensing](https://www.typography.com/faq)에 대해서도 참고할 수 있습니다." + +#: src/app/main/ui/dashboard/fonts.cljs:214 +msgid "dashboard.fonts.upload-all" +msgstr "모두 업로드" + +#: src/app/main/ui/dashboard/fonts.cljs:199 +#, markdown +msgid "dashboard.fonts.warning-text" +msgstr "" +"서로 다른 운영체제에서의 세로 메트릭(vertical metrics)과 관련하여, 업로드한 " +"폰트에 문제가 있을 가능성이 감지되었습니다. 확인을 위해 세로 메트릭 [검사 " +"서비스](https://vertical-metrics.netlify.app/)를 사용할 수 있습니다. 또한 웹 " +"폰트를 생성하고 오류를 수정하려면 Transfonter(https://transfonter.org/) " +"사용을 권장합니다. " + +#: src/app/main/ui/dashboard/import.cljs:464, src/app/main/ui/dashboard/project_menu.cljs:109 +msgid "dashboard.import" +msgstr "Penpot 파일 가져오기" + +#: src/app/main/ui/dashboard/import.cljs:293, src/app/worker/import.cljs:121, src/app/worker/import.cljs:124 +msgid "dashboard.import.analyze-error" +msgstr "이런! 이 파일을 가져올 수 없습니다" From 380d211b4c1087f01aadde86e3447465770db19d Mon Sep 17 00:00:00 2001 From: deveronica Date: Wed, 4 Mar 2026 21:09:40 +0100 Subject: [PATCH 023/288] :globe_with_meridians: Add translations for: Korean Currently translated at 48.8% (1013 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/ --- frontend/translations/ko.po | 3156 ++++++++++++++++++++++++++++++++++- 1 file changed, 3076 insertions(+), 80 deletions(-) diff --git a/frontend/translations/ko.po b/frontend/translations/ko.po index 53e7def20a..fc087e4a11 100644 --- a/frontend/translations/ko.po +++ b/frontend/translations/ko.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-03-03 19:09+0000\n" +"PO-Revision-Date: 2026-03-04 20:10+0000\n" "Last-Translator: deveronica \n" "Language-Team: Korean \n" @@ -507,7 +507,7 @@ msgstr "내보내기" #: src/app/main/ui/dashboard/sidebar.cljs:858 msgid "dashboard.no-projects-placeholder" -msgstr "고정된 프로젝트가 여기에 표시됩니다" +msgstr "고정된 프로젝트가 여기에 표시됩니다." #: src/app/main/ui/dashboard/deleted.cljs:62, src/app/main/ui/dashboard/projects.cljs:57 msgid "dashboard.projects-title" @@ -519,7 +519,7 @@ msgstr "모든 프로젝트와 파일을 복원하려 합니다. 시간이 다 #: src/app/main/ui/dashboard/deleted.cljs:273 msgid "dashboard.restore-all-confirmation.title" -msgstr "모든 파일과 프로젝트 복원" +msgstr "모든 프로젝트 및 파일 복원" #: src/app/main/ui/dashboard/sidebar.cljs:259, src/app/main/ui/dashboard/sidebar.cljs:260 msgid "dashboard.search-placeholder" @@ -527,7 +527,7 @@ msgstr "검색…" #: src/app/main/ui/dashboard/search.cljs:72 msgid "dashboard.searching-for" -msgstr "“%s” 찾는 중…" +msgstr "\"%s\" 찾는 중…" #: src/app/main/ui/dashboard/team.cljs:1344 msgid "dashboard.team-projects" @@ -547,7 +547,7 @@ msgstr "복구 토큰이 유효하지 않습니다." #: src/app/main/ui/inspect/attributes/blur.cljs:26 msgid "inspect.attributes.blur" -msgstr "흐림" +msgstr "블러" #: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:126 msgid "inspect.attributes.blur.value" @@ -571,7 +571,7 @@ msgstr "채우기" #: src/app/main/ui/inspect/attributes/common.cljs:78, src/app/main/ui/inspect/styles/rows/color_properties_row.cljs:126 msgid "inspect.attributes.image.download" -msgstr "소스 이미지 다운로드" +msgstr "원본 이미지 다운로드" #: src/app/main/ui/inspect/attributes/image.cljs:39 #, unused @@ -581,7 +581,7 @@ msgstr "높이" #: src/app/main/ui/inspect/attributes/image.cljs:32 #, unused msgid "inspect.attributes.image.width" -msgstr "폭" +msgstr "너비" #: src/app/main/ui/inspect/attributes/layout.cljs #, unused @@ -611,12 +611,12 @@ msgstr "회전" #: src/app/main/ui/inspect/attributes/layout.cljs #, unused msgid "inspect.attributes.layout.top" -msgstr "위" +msgstr "위쪽" #: src/app/main/ui/inspect/attributes/layout.cljs #, unused msgid "inspect.attributes.layout.width" -msgstr "폭" +msgstr "너비" #: src/app/main/ui/inspect/attributes/shadow.cljs:65 msgid "inspect.attributes.shadow" @@ -624,7 +624,7 @@ msgstr "그림자" #: src/app/main/ui/inspect/attributes/geometry.cljs:46, src/app/main/ui/inspect/styles/style_box.cljs:22 msgid "inspect.attributes.size" -msgstr "사이즈와 위치" +msgstr "크기 및 위치" #: src/app/main/ui/inspect/attributes/stroke.cljs:90 msgid "inspect.attributes.stroke" @@ -652,12 +652,12 @@ msgstr "혼합" #, unused msgid "inspect.attributes.stroke.style.solid" -msgstr "단색" +msgstr "실선" #: src/app/main/ui/inspect/attributes/stroke.cljs #, unused msgid "inspect.attributes.stroke.width" -msgstr "폭" +msgstr "두께" #: src/app/main/ui/inspect/attributes/text.cljs:53, src/app/main/ui/inspect/attributes/text.cljs:159 msgid "inspect.attributes.typography" @@ -665,15 +665,15 @@ msgstr "타이포그래피" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:308 msgid "inspect.attributes.typography.font-family" -msgstr "폰트 패밀리" +msgstr "글꼴 패밀리" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:326, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:332 msgid "inspect.attributes.typography.font-size" -msgstr "폰트 사이즈" +msgstr "글꼴 크기" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:343 msgid "inspect.attributes.typography.font-style" -msgstr "폰트 스타일" +msgstr "글꼴 스타일" #: src/app/main/ui/inspect/attributes/text.cljs:113 msgid "inspect.attributes.typography.text-decoration.underline" @@ -682,7 +682,7 @@ msgstr "밑줄" #: src/app/main/ui/inspect/attributes/text.cljs:153 #, unused msgid "inspect.attributes.typography.text-transform" -msgstr "텍스트 변형" +msgstr "텍스트 변환" #: src/app/main/ui/inspect/attributes/text.cljs:123, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:441 msgid "inspect.attributes.typography.text-transform.lowercase" @@ -702,7 +702,7 @@ msgstr "컴포넌트" #: src/app/main/ui/inspect/right_sidebar.cljs:145 msgid "inspect.tabs.code.selected.curve" -msgstr "커브" +msgstr "곡선" #: src/app/main/ui/inspect/right_sidebar.cljs:146 msgid "inspect.tabs.code.selected.frame" @@ -742,11 +742,11 @@ msgstr "단축키" #: src/app/main/data/common.cljs:90, src/app/main/ui/dashboard/import.cljs:530 msgid "labels.accept" -msgstr "허가" +msgstr "수락" #: src/app/main/ui/dashboard/team.cljs:1223 msgid "labels.active" -msgstr "활성화" +msgstr "활성" #: src/app/main/ui/dashboard/fonts.cljs:186 msgid "labels.add-custom-font" @@ -754,7 +754,7 @@ msgstr "커스텀 폰트 추가" #: src/app/main/ui/dashboard/team.cljs:134, src/app/main/ui/dashboard/team.cljs:320, src/app/main/ui/dashboard/team.cljs:565, src/app/main/ui/dashboard/team.cljs:595, src/app/main/ui/onboarding/team_choice.cljs:58 msgid "labels.admin" -msgstr "관리자" +msgstr "관리자(Admin)" #: src/app/main/ui/workspace/tokens/management/context_menu.cljs:92, src/app/main/ui/workspace/tokens/management/context_menu.cljs:129, src/app/main/ui/workspace/tokens/management/token_pill.cljs:117 msgid "labels.all" @@ -762,7 +762,7 @@ msgstr "전체" #: src/app/main/ui/auth/register.cljs:257 msgid "labels.and" -msgstr "그리고" +msgstr "및" #: src/app/main/ui/onboarding/team_choice.cljs:186 #, unused @@ -771,7 +771,7 @@ msgstr "뒤로" #: src/app/main/ui/static.cljs:296 msgid "labels.bad-gateway.main-message" -msgstr "잘못된 경로" +msgstr "게이트웨이 오류가 발생했습니다" #: src/app/main/data/common.cljs:119, src/app/main/ui/dashboard/change_owner.cljs:64, src/app/main/ui/dashboard/import.cljs:515, src/app/main/ui/dashboard/team.cljs:780, src/app/main/ui/dashboard/team.cljs:1122, src/app/main/ui/delete_shared.cljs:38, src/app/main/ui/exports/assets.cljs:163, src/app/main/ui/exports/files.cljs:168, src/app/main/ui/settings/access_tokens.cljs:175, src/app/main/ui/viewer/share_link.cljs:208, src/app/main/ui/workspace/sidebar/assets/groups.cljs:159, src/app/main/ui/workspace/tokens/export/modal.cljs:44, src/app/main/ui/workspace/tokens/import/modal.cljs:269, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:297, src/app/main/ui/workspace/tokens/settings/menu.cljs:105, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:245 msgid "labels.cancel" @@ -787,7 +787,7 @@ msgstr "코드" #: src/app/main/ui/viewer/comments.cljs:70, src/app/main/ui/workspace/comments.cljs:128 msgid "labels.comments" -msgstr "코멘트" +msgstr "댓글" #: src/app/main/ui/dashboard/sidebar.cljs:935, src/app/main/ui/workspace/main_menu.cljs:144 msgid "labels.community" @@ -795,7 +795,7 @@ msgstr "커뮤니티" #: src/app/main/ui/settings/password.cljs:93 msgid "labels.confirm-password" -msgstr "비밀번호 확인하기" +msgstr "비밀번호 확인" #: src/app/main/ui/auth/login.cljs:204, src/app/main/ui/dashboard/deleted.cljs:43, src/app/main/ui/dashboard/deleted.cljs:275, src/app/main/ui/dashboard/file_menu.cljs:209, src/app/main/ui/dashboard/import.cljs:521, src/app/main/ui/dashboard/team.cljs:787, src/app/main/ui/exports/files.cljs:173, src/app/main/ui/onboarding/newsletter.cljs:106, src/app/main/ui/settings/subscription.cljs:279, src/app/main/ui/settings/subscription.cljs:313 msgid "labels.continue" @@ -803,23 +803,23 @@ msgstr "계속하기" #: src/app/main/ui/dashboard/team.cljs:650 msgid "labels.copy-invitation-link" -msgstr "링크 복사하기" +msgstr "링크 복사" #: src/app/main/ui/workspace/sidebar/assets/groups.cljs:167, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:203 msgid "labels.create" -msgstr "생성하기" +msgstr "생성" #: src/app/main/ui/dashboard/team_form.cljs:100, src/app/main/ui/dashboard/team_form.cljs:120 msgid "labels.create-team" -msgstr "새로운 팀 만들기" +msgstr "새 팀 생성" #: src/app/main/ui/dashboard/team_form.cljs:112 msgid "labels.create-team.placeholder" -msgstr "새로운 팀명 입력하세요" +msgstr "새 팀 이름을 입력하세요" #, unused msgid "labels.custom-fonts" -msgstr "커스텀 폰트" +msgstr "사용자 지정 글꼴" #: src/app/main/ui/settings/sidebar.cljs:84 msgid "labels.dashboard" @@ -827,19 +827,19 @@ msgstr "대시보드" #: src/app/main/ui/dashboard/file_menu.cljs:336, src/app/main/ui/dashboard/fonts.cljs:267, src/app/main/ui/dashboard/fonts.cljs:343, src/app/main/ui/dashboard/fonts.cljs:357, src/app/main/ui/dashboard/project_menu.cljs:115, src/app/main/ui/dashboard/team.cljs:1158, src/app/main/ui/settings/access_tokens.cljs:196, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:223, src/app/main/ui/workspace/sidebar/versions.cljs:216, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:290, src/app/main/ui/workspace/tokens/management/node_context_menu.cljs:82, src/app/main/ui/workspace/tokens/sets/context_menu.cljs:66, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:381 msgid "labels.delete" -msgstr "삭제하기" +msgstr "삭제" #: src/app/main/ui/comments.cljs:997 msgid "labels.delete-comment" -msgstr "코멘트 삭제하기" +msgstr "댓글 삭제" #: src/app/main/ui/comments.cljs:919 msgid "labels.delete-comment-thread" -msgstr "스레드 제거하기" +msgstr "스레드 삭제" #: src/app/main/ui/dashboard/team.cljs:941 msgid "labels.delete-invitation" -msgstr "초대장 제거하기" +msgstr "초대 삭제" #: src/app/main/ui/dashboard/file_menu.cljs:30, src/app/main/ui/dashboard/files.cljs:80, src/app/main/ui/dashboard/files.cljs:179, src/app/main/ui/dashboard/projects.cljs:229, src/app/main/ui/dashboard/projects.cljs:233, src/app/main/ui/dashboard/sidebar.cljs:820 msgid "labels.drafts" @@ -859,7 +859,7 @@ msgstr "작성자" #: src/app/main/ui/dashboard/team.cljs:668 msgid "labels.expired-invitation" -msgstr "기한이 만료된" +msgstr "만료됨" #: src/app/main/ui/exports/assets.cljs:172, src/app/main/ui/workspace/tokens/sidebar.cljs:134 msgid "labels.export" @@ -867,11 +867,11 @@ msgstr "내보내기" #: src/app/main/ui/dashboard/fonts.cljs:432 msgid "labels.font-family" -msgstr "폰트 패밀리" +msgstr "글꼴 패밀리" #, unused msgid "labels.font-providers" -msgstr "폰트 공급자" +msgstr "글꼴 제공자" #: src/app/main/ui/dashboard/fonts.cljs:433 msgid "labels.font-variants" @@ -879,7 +879,7 @@ msgstr "스타일" #: src/app/main/ui/dashboard/fonts.cljs:61, src/app/main/ui/dashboard/sidebar.cljs:833 msgid "labels.fonts" -msgstr "폰트" +msgstr "글꼴" #: src/app/main/ui/auth/recovery_request.cljs:104, src/app/main/ui/auth/register.cljs:359, src/app/main/ui/static.cljs:175, src/app/main/ui/viewer/login.cljs:113 msgid "labels.go-back" @@ -887,7 +887,7 @@ msgstr "뒤로 가기" #: src/app/main/ui/dashboard/sidebar.cljs:887, src/app/main/ui/workspace/main_menu.cljs:136, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1317, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1345, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1533 msgid "labels.help-center" -msgstr "고객센터" +msgstr "도움말 센터" #: src/app/main/ui/dashboard/team.cljs:1224 msgid "labels.inactive" @@ -907,7 +907,7 @@ msgstr "프로젝트" #: src/app/main/ui/dashboard/deleted.cljs:208 msgid "labels.recent" -msgstr "최근" +msgstr "최근 항목" #: src/app/main/ui/workspace/sidebar/layers.cljs:420, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:787 msgid "labels.search" @@ -927,23 +927,23 @@ msgstr "웹훅" #: src/app/main/ui/comments.cljs:838 msgid "labels.write-new-comment" -msgstr "새 코멘트 쓰기" +msgstr "새 댓글 작성" #: src/app/main/data/media.cljs:51, src/app/main/data/workspace/media.cljs:228, src/app/main/data/workspace/media.cljs:443 msgid "media.loading" -msgstr "이미지 로딩중…" +msgstr "이미지 로드 중…" #: src/app/main/data/common.cljs:120 msgid "modals.add-shared-confirm.accept" -msgstr "공유된 라이브러리로 추가" +msgstr "공유 라이브러리로 추가" #: src/app/main/data/common.cljs:117 msgid "modals.add-shared-confirm.message" -msgstr " " +msgstr "\"%s\"를 공유 라이브러리로 추가" #: src/app/main/ui/settings/change_email.cljs:109 msgid "modals.change-email.confirm-email" -msgstr "새 이메일 인증하기" +msgstr "새 이메일 확인" #: src/app/main/ui/settings/change_email.cljs:102 msgid "modals.change-email.new-email" @@ -951,55 +951,55 @@ msgstr "새 이메일" #: src/app/main/ui/settings/change_email.cljs:117 msgid "modals.change-email.submit" -msgstr "이메일 변경하기" +msgstr "이메일 변경" #: src/app/main/ui/settings/change_email.cljs:90 msgid "modals.change-email.title" -msgstr "이메일을 변경하세요" +msgstr "이메일 주소 변경" #: src/app/main/ui/dashboard/team.cljs:1127 msgid "modals.create-webhook.submit-label" -msgstr "웹훅 만들기" +msgstr "웹훅 생성" #: src/app/main/ui/dashboard/team.cljs:1092 msgid "modals.create-webhook.title" -msgstr "웹훅 생성하기" +msgstr "웹훅 생성" #: src/app/main/ui/comments.cljs:889 msgid "modals.delete-comment-thread.accept" -msgstr "대회 지우기" +msgstr "대화 삭제" #: src/app/main/ui/comments.cljs:887 msgid "modals.delete-comment-thread.title" -msgstr "대화 지우기" +msgstr "대화 삭제" #: src/app/main/ui/dashboard/file_menu.cljs:125 msgid "modals.delete-file-confirm.accept" -msgstr "파일 지우기" +msgstr "파일 삭제" #: src/app/main/ui/dashboard/file_menu.cljs:124 msgid "modals.delete-file-confirm.message" -msgstr "이 파일을 정말로 지우시겠습니까?" +msgstr "이 파일을 삭제하시겠습니까?" #: src/app/main/ui/dashboard/file_menu.cljs:123 msgid "modals.delete-file-confirm.title" -msgstr "파일 삭제중" +msgstr "파일 삭제 중" #: src/app/main/ui/dashboard/file_menu.cljs:119 msgid "modals.delete-file-multi-confirm.accept" -msgstr "여러 파일 지우기" +msgstr "여러 파일 삭제" #: src/app/main/ui/dashboard/fonts.cljs:355 msgid "modals.delete-font-variant.title" -msgstr "폰트 스타일 지우는 중" +msgstr "글꼴 스타일 지우는 중" #: src/app/main/ui/dashboard/fonts.cljs:341 msgid "modals.delete-font.title" -msgstr "폰트 지우는 중" +msgstr "글꼴 삭제 중" #: src/app/main/ui/workspace/context_menu.cljs:675, src/app/main/ui/workspace/sidebar/sitemap.cljs:95 msgid "modals.delete-page.body" -msgstr "정말로 해당 페이지를 지우시겠습니까?" +msgstr "이 페이지를 삭제하시겠습니까?" #: src/app/main/ui/workspace/context_menu.cljs:674, src/app/main/ui/workspace/sidebar/sitemap.cljs:94 msgid "modals.delete-page.title" @@ -1007,15 +1007,15 @@ msgstr "페이지 삭제" #: src/app/main/ui/dashboard/project_menu.cljs:73 msgid "modals.delete-project-confirm.accept" -msgstr "프로젝트 제거" +msgstr "프로젝트 삭제" #: src/app/main/ui/dashboard/project_menu.cljs:72 msgid "modals.delete-project-confirm.message" -msgstr "정말로 해당 프로젝트를 지우시겠습니까?" +msgstr "이 프로젝트를 삭제하시겠습니까?" #: src/app/main/ui/dashboard/project_menu.cljs:71 msgid "modals.delete-project-confirm.title" -msgstr "프로젝트 제거" +msgstr "프로젝트 삭제" #: src/app/main/ui/settings/options.cljs:27, src/app/main/ui/settings/profile.cljs:30 msgid "notifications.profile-saved" @@ -1162,12 +1162,12 @@ msgstr "전체 %s개 중 %s개 요소가 선택됨" #: src/app/main/ui/exports/assets.cljs:179 msgid "dashboard.export-shapes.how-to" msgstr "" -"오른쪽 사이드바 하단의 디자인 속성에서 요소에 내보내기 설정을 추가할 수 " +"디자인 속성(오른쪽 사이드바 하단)에서 요소에 내보내기 설정을 추가할 수 " "있습니다." #: src/app/main/ui/exports/assets.cljs:183 msgid "dashboard.export-shapes.how-to-link" -msgstr "Penpot에서 내보내기 설정 방법." +msgstr "Penpot에서 내보내기 설정 방법 안내." #: src/app/main/ui/exports/assets.cljs:178 msgid "dashboard.export-shapes.no-elements" @@ -1175,7 +1175,7 @@ msgstr "내보내기 설정이 지정된 요소가 없습니다." #: src/app/main/ui/exports/assets.cljs:189 msgid "dashboard.export-shapes.title" -msgstr "선택 항목 내보내기" +msgstr "선택 영역 내보내기" #: src/app/main/ui/dashboard/file_menu.cljs:262 #, unused @@ -1186,8 +1186,8 @@ msgstr "표준 파일 %s개(.svg + .json) 다운로드" #, unused msgid "dashboard.export.explain" msgstr "" -"다운로드하려는 파일 중 하나 이상이 공유 라이브러리를 사용하고 있습니다. 해당 " -"라이브러리의 에셋을 어떻게 처리하시겠습니까?" +"다운로드하려는 하나 이상의 파일이 공유 라이브러리를 사용 중입니다. 해당 " +"에셋*을 어떻게 처리하시겠습니까?" #: src/app/main/ui/dashboard/file_menu.cljs:266 msgid "dashboard.file-menu.delete-files-permanently-option" @@ -1205,11 +1205,11 @@ msgstr "누락된 글꼴" #: src/app/main/ui/dashboard/fonts.cljs:218 msgid "dashboard.fonts.dismiss-all" -msgstr "모두 닫기" +msgstr "모두 무시" #: src/app/main/ui/dashboard/fonts.cljs:455 msgid "dashboard.fonts.empty-placeholder" -msgstr "업로드한 사용자 지정 글꼴이 이곳에 표시됩니다." +msgstr "업로드한 사용자 지정 글꼴이 여기에 표시됩니다." #: src/app/main/ui/dashboard/fonts.cljs:458 msgid "dashboard.fonts.empty-placeholder-viewer" @@ -1217,7 +1217,7 @@ msgstr "아직 사용자 지정 폰트가 없습니다." #: src/app/main/ui/dashboard/fonts.cljs:459 msgid "dashboard.fonts.empty-placeholder-viewer-sub" -msgstr "프로젝트 구성원이 사용자 지정 폰트를 업로드하면 이곳에 표시됩니다." +msgstr "프로젝트 구성원이 사용자 지정 폰트를 업로드하면 여기에 표시됩니다." #: src/app/main/ui/dashboard/fonts.cljs:206 msgid "dashboard.fonts.fonts-added" @@ -1228,18 +1228,19 @@ msgstr[0] "글꼴 %s개 추가됨" #, markdown msgid "dashboard.fonts.hero-text1" msgstr "" -"여기에 업로드한 웹 폰트는 이 팀의 파일에서 텍스트 속성의 글꼴 목록에 " -"추가됩니다. 같은 글꼴 패밀리 이름을 가진 폰트는 **하나의 글꼴 패밀리**로 " -"묶입니다. 업로드 가능한 형식은 **TTF, OTF, WOFF**이며, 이 중 하나만 있으면 " -"됩니다." +"여기에 업로드하는 모든 웹 글꼴은 이 팀의 파일 텍스트 속성에서 사용할 수 있는 " +"폰트 패밀리 목록에 추가됩니다. 동일한 폰트 패밀리 이름을 가진 " +"폰트들은**하나의 글꼴 패밀리**로 그룹화됩니다. 지원 형식: **TTF, OTF, WOFF** " +"(하나만 필요)." #: src/app/main/ui/dashboard/fonts.cljs:194 #, markdown msgid "dashboard.fonts.hero-text2" msgstr "" -"Penpot에서 사용 권한이 있는 글꼴만 업로드해야 합니다. 자세한 내용은 " -"[Penpot's Terms of Service](%s)의 콘텐츠 권리 섹션을 확인하세요. 또한 [font " -"licensing](https://www.typography.com/faq)에 대해서도 참고할 수 있습니다." +"Penpot에서 사용하기 위해서는 본인이 소유하거나 적법한 사용 라이선스를 보유한 " +"글꼴만 업로드해야 합니다. 관련 내용은 [Penpot의 서비스 이용약관](%s)의 " +"콘텐츠 권리 섹션에서 확인할 수 있습니다. 추가로 [글꼴 라이선스 안내](https://" +"www.typography.com/faq)를 참고하시기 바랍니다." #: src/app/main/ui/dashboard/fonts.cljs:214 msgid "dashboard.fonts.upload-all" @@ -1249,11 +1250,10 @@ msgstr "모두 업로드" #, markdown msgid "dashboard.fonts.warning-text" msgstr "" -"서로 다른 운영체제에서의 세로 메트릭(vertical metrics)과 관련하여, 업로드한 " -"폰트에 문제가 있을 가능성이 감지되었습니다. 확인을 위해 세로 메트릭 [검사 " -"서비스](https://vertical-metrics.netlify.app/)를 사용할 수 있습니다. 또한 웹 " -"폰트를 생성하고 오류를 수정하려면 Transfonter(https://transfonter.org/) " -"사용을 권장합니다. " +"운영 체제별 수직 메트릭과 관련된 폰트 문제를 감지했습니다. 확인을 위해 [이 " +"서비스](https://vertical-metrics.netlify.app/)와 같은 수직 메트릭 서비스를 " +"사용할 수 있습니다. 또한, 웹 폰트 생성 및 오류 수정을 위해 [Transfonter]" +"(https://transfonter.org/) 사용을 권장합니다. " #: src/app/main/ui/dashboard/import.cljs:464, src/app/main/ui/dashboard/project_menu.cljs:109 msgid "dashboard.import" @@ -1262,3 +1262,2999 @@ msgstr "Penpot 파일 가져오기" #: src/app/main/ui/dashboard/import.cljs:293, src/app/worker/import.cljs:121, src/app/worker/import.cljs:124 msgid "dashboard.import.analyze-error" msgstr "이런! 이 파일을 가져올 수 없습니다" + +#, unused +msgid "dashboard.import.analyze-error.components-v2" +msgstr "컴포넌트 v2가 활성화된 파일이지만, 이 팀은 아직 이를 지원하지 않습니다." + +#: src/app/main/ui/dashboard.cljs:259 +msgid "dashboard.import.bad-url" +msgstr "가져오기 실패. 템플릿 URL이 올바르지 않습니다" + +#: src/app/main/ui/dashboard.cljs:241 +#, unused +msgid "dashboard.import.error" +msgstr "가져오기 실패. 다시 시도해주세요" + +#: src/app/main/ui/dashboard/import.cljs:292 +#, unused +msgid "dashboard.import.import-error" +msgstr "파일을 가져오는 중 문제가 발생했습니다. 파일이 누락되었습니다." + +#: src/app/main/ui/dashboard/import.cljs:485 +msgid "dashboard.import.import-error.disclaimer" +msgstr "일부 파일이 누락되었습니다" + +#: src/app/main/ui/dashboard/import.cljs:489 +msgid "dashboard.import.import-error.message1" +msgstr "다음 파일에 오류가 있습니다:" + +#: src/app/main/ui/dashboard/import.cljs:494 +msgid "dashboard.import.import-error.message2" +msgstr "오류가 있는 파일은 업로드되지 않습니다." + +#: src/app/main/ui/dashboard/import.cljs:479 +msgid "dashboard.import.import-message" +msgid_plural "dashboard.import.import-message" +msgstr[0] "파일 %s개를 성공적으로 가져왔습니다." + +#: src/app/main/ui/dashboard/import.cljs:474 +msgid "dashboard.import.import-warning" +msgstr "일부 파일에 포함된 유효하지 않은 객체가 제거되었습니다." + +#: src/app/main/ui/dashboard.cljs:260 +msgid "dashboard.import.no-perms" +msgstr "이 팀으로 가져올 권한이 없습니다" + +#: src/app/main/ui/dashboard/import.cljs:128 +msgid "dashboard.import.progress.process-colors" +msgstr "컬러 처리 중" + +#: src/app/main/ui/dashboard/import.cljs:137, src/app/main/ui/dashboard/import.cljs:140 +msgid "dashboard.import.progress.process-components" +msgstr "컴포넌트 처리 중" + +#: src/app/main/ui/dashboard/import.cljs:134 +msgid "dashboard.import.progress.process-media" +msgstr "미디어 처리 중" + +#: src/app/main/ui/dashboard/import.cljs:125 +msgid "dashboard.import.progress.process-page" +msgstr "페이지 처리 중: %s" + +#: src/app/main/ui/dashboard/import.cljs:131 +msgid "dashboard.import.progress.process-typographies" +msgstr "타이포그래피 처리 중" + +#: src/app/main/ui/dashboard/import.cljs:119 +msgid "dashboard.import.progress.upload-data" +msgstr "서버로 데이터 업로드 중 (%s/%s)" + +#: src/app/main/ui/dashboard/import.cljs:122 +msgid "dashboard.import.progress.upload-media" +msgstr "파일 업로드 중: %s" + +#: src/app/main/ui/dashboard/team.cljs:765 +msgid "dashboard.invitation-modal.delete" +msgstr "다음에 대한 초대를 삭제합니다:" + +#: src/app/main/ui/dashboard/team.cljs:766 +msgid "dashboard.invitation-modal.resend" +msgstr "다음에 대한 초대를 재전송합니다:" + +#: src/app/main/ui/dashboard/team.cljs:756 +msgid "dashboard.invitation-modal.title.delete-invitations" +msgstr "초대 삭제" + +#: src/app/main/ui/dashboard/team.cljs:757 +msgid "dashboard.invitation-modal.title.resend-invitations" +msgstr "초대 재전송" + +#: src/app/main/ui/dashboard/team.cljs:122, src/app/main/ui/dashboard/team.cljs:744 +msgid "dashboard.invite-profile" +msgstr "사람 초대" + +#: src/app/main/ui/dashboard/sidebar.cljs:477, src/app/main/ui/dashboard/sidebar.cljs:484, src/app/main/ui/dashboard/sidebar.cljs:489, src/app/main/ui/dashboard/team.cljs:351 +msgid "dashboard.leave-team" +msgstr "팀 나가기" + +#: src/app/main/ui/dashboard/templates.cljs:84, src/app/main/ui/dashboard/templates.cljs:169 +msgid "dashboard.libraries-and-templates" +msgstr "라이브러리 및 템플릿" + +#: src/app/main/ui/dashboard/templates.cljs:267 +msgid "dashboard.libraries-and-templates.description" +msgstr "프로젝트에 추가할 수 있는 라이브러리와 템플릿입니다" + +#: src/app/main/ui/dashboard/templates.cljs:170 +msgid "dashboard.libraries-and-templates.explore" +msgstr "더 많은 항목을 둘러보고 기여하는 방법을 알아보세요" + +#: src/app/main/ui/dashboard/import.cljs:365, src/app/main/ui/workspace/libraries.cljs:145 +msgid "dashboard.libraries-and-templates.import-error" +msgstr "템플릿을 가져오는 중 문제가 발생했습니다. 템플릿이 누락되었습니다." + +#: src/app/main/ui/dashboard/libraries.cljs:69 +msgid "dashboard.libraries-title" +msgstr "라이브러리" + +#: src/app/main/ui/dashboard/placeholder.cljs:143 +msgid "dashboard.loading-files" +msgstr "파일 로드 중…" + +#: src/app/main/ui/dashboard/fonts.cljs:449 +msgid "dashboard.loading-fonts" +msgstr "폰트 로드 중…" + +#: src/app/main/data/comments.cljs:473 +msgid "dashboard.mark-all-as-read.success" +msgstr "모든 알림을 읽음으로 표시했습니다" + +#: src/app/main/ui/dashboard/file_menu.cljs:312, src/app/main/ui/dashboard/project_menu.cljs:101 +msgid "dashboard.move-to" +msgstr "다음으로 이동" + +#: src/app/main/ui/dashboard/file_menu.cljs:276 +msgid "dashboard.move-to-multi" +msgstr "%s개 파일을 다음으로 이동" + +#: src/app/main/ui/dashboard/file_menu.cljs:248 +msgid "dashboard.move-to-other-team" +msgstr "다른 팀으로 이동" + +#: src/app/main/ui/dashboard/files.cljs:107, src/app/main/ui/dashboard/projects.cljs:257, src/app/main/ui/dashboard/projects.cljs:258 +msgid "dashboard.new-file" +msgstr "+ 새 파일" + +#: src/app/main/data/dashboard.cljs:536, src/app/main/data/dashboard.cljs:648 +msgid "dashboard.new-file-prefix" +msgstr "새 파일" + +#: src/app/main/ui/dashboard/projects.cljs:62 +msgid "dashboard.new-project" +msgstr "+ 새 프로젝트" + +#: src/app/main/data/dashboard.cljs:289, src/app/main/data/dashboard.cljs:651 +msgid "dashboard.new-project-prefix" +msgstr "새 프로젝트" + +#: src/app/main/ui/dashboard/search.cljs:77 +msgid "dashboard.no-matches-for" +msgstr "\"%s\"에 대한 검색 결과가 없습니다." + +#: src/app/main/ui/dashboard/comments.cljs:91 +msgid "dashboard.notifications" +msgstr "알림" + +#: src/app/main/ui/auth/verify_token.cljs:34 +msgid "dashboard.notifications.email-changed-successfully" +msgstr "이메일 주소가 성공적으로 업데이트되었습니다" + +#: src/app/main/ui/auth/verify_token.cljs:28 +msgid "dashboard.notifications.email-verified-successfully" +msgstr "이메일 주소가 성공적으로 인증되었습니다" + +#: src/app/main/data/profile.cljs:280 +msgid "dashboard.notifications.notifications-saved" +msgstr "알림 설정이 업데이트되었습니다" + +#: src/app/main/ui/settings/password.cljs:38 +msgid "dashboard.notifications.password-saved" +msgstr "비밀번호가 성공적으로 저장되었습니다!" + +#: src/app/main/ui/dashboard/comments.cljs:45 +msgid "dashboard.notifications.view" +msgstr "알림 보기" + +#: src/app/main/ui/dashboard/team.cljs:1340 +msgid "dashboard.num-of-members" +msgstr "멤버 %s명" + +#: src/app/main/ui/dashboard/file_menu.cljs:295 +msgid "dashboard.open-in-new-tab" +msgstr "새 탭에서 파일 열기" + +#: src/app/main/ui/dashboard/deleted.cljs:157, src/app/main/ui/dashboard/deleted.cljs:158, src/app/main/ui/dashboard/files.cljs:120, src/app/main/ui/dashboard/grid.cljs:442, src/app/main/ui/dashboard/projects.cljs:266, src/app/main/ui/dashboard/projects.cljs:267 +msgid "dashboard.options" +msgstr "옵션" + +#: src/app/main/ui/dashboard/team.cljs:949 +msgid "dashboard.order-invitations-by-role" +msgstr "역할순 정렬" + +#: src/app/main/ui/dashboard/team.cljs:958 +msgid "dashboard.order-invitations-by-status" +msgstr "상태순 정렬" + +#: src/app/main/ui/settings/password.cljs:96, src/app/main/ui/settings/password.cljs:109 +msgid "dashboard.password-change" +msgstr "비밀번호 변경" + +#: src/app/main/data/common.cljs:192 +msgid "dashboard.permissions-change.admin" +msgstr "이제 이 팀의 관리자(Admin)입니다." + +#: src/app/main/data/common.cljs:191 +msgid "dashboard.permissions-change.editor" +msgstr "이제 이 팀의 에디터(Editor)입니다." + +#: src/app/main/data/common.cljs:193 +msgid "dashboard.permissions-change.owner" +msgstr "이제 이 팀의 소유자(Owner)입니다." + +#: src/app/main/data/common.cljs:190 +msgid "dashboard.permissions-change.viewer" +msgstr "이제 이 팀의 뷰어(Viewer)입니다." + +#: src/app/main/ui/dashboard/pin_button.cljs:23, src/app/main/ui/dashboard/project_menu.cljs:96 +msgid "dashboard.pin-unpin" +msgstr "고정/고정 해제" + +#: src/app/main/ui/dashboard.cljs:223 +msgid "dashboard.plugins.bad-url" +msgstr "플러그인 URL이 올바르지 않습니다" + +#: src/app/main/ui/dashboard.cljs:221 +msgid "dashboard.plugins.parse-error" +msgstr "플러그인 매니페스트를 구문 분석할 수 없습니다" + +#: src/app/main/ui/dashboard.cljs:184 +msgid "dashboard.plugins.try-plugin" +msgstr "플러그인 체험하기: " + +#: src/app/main/data/dashboard.cljs:722 +msgid "dashboard.progress-notification.deleting-files" +msgstr "파일 삭제 중…" + +#: src/app/main/data/dashboard.cljs:843 +msgid "dashboard.progress-notification.restoring-files" +msgstr "파일 복원 중…" + +#: src/app/main/data/dashboard.cljs:723 +msgid "dashboard.progress-notification.slow-delete" +msgstr "삭제가 예상보다 느립니다" + +#: src/app/main/data/dashboard.cljs:844 +msgid "dashboard.progress-notification.slow-restore" +msgstr "복원이 예상보다 느립니다" + +#: src/app/main/ui/settings/profile.cljs:86 +msgid "dashboard.remove-account" +msgstr "계정을 삭제하시겠습니까?" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#, unused +msgid "dashboard.remove-shared" +msgstr "공유 라이브러리에서 제거" + +#: src/app/main/data/common.cljs:225 +msgid "dashboard.removed-from-team" +msgstr "더 이상 \"%s\" 팀의 멤버가 아닙니다." + +#: src/app/main/ui/dashboard/deleted.cljs:308 +msgid "dashboard.restore-all-deleted-button" +msgstr "모두 복원" + +#: src/app/main/data/dashboard.cljs:903 +msgid "dashboard.restore-files-success-notification" +msgstr "%s개 파일이 성공적으로 복원되었습니다." + +#: src/app/main/ui/dashboard/deleted.cljs:82 +msgid "dashboard.restore-project-button" +msgstr "프로젝트 복원" + +#: src/app/main/ui/dashboard/deleted.cljs:41 +msgid "dashboard.restore-project-confirmation.description" +msgstr "%s 프로젝트와 그 안에 포함된 모든 파일을 복원하려고 합니다." + +#: src/app/main/ui/dashboard/deleted.cljs:40 +msgid "dashboard.restore-project-confirmation.title" +msgstr "프로젝트 복원" + +#: src/app/main/data/dashboard.cljs:875, src/app/main/data/dashboard.cljs:902, src/app/main/data/dashboard.cljs:939, src/app/main/ui/dashboard/file_menu.cljs:198 +msgid "dashboard.restore-success-notification" +msgstr "%s이(가) 성공적으로 복원되었습니다." + +#: src/app/main/ui/settings/profile.cljs:78 +msgid "dashboard.save-settings" +msgstr "설정 저장" + +#: src/app/main/ui/settings/options.cljs:58 +msgid "dashboard.select-ui-language" +msgstr "UI 언어 선택" + +#: src/app/main/ui/settings/options.cljs:65 +msgid "dashboard.select-ui-theme" +msgstr "테마 선택" + +#: src/app/main/ui/settings/options.cljs:68 +msgid "dashboard.select-ui-theme.dark" +msgstr "Penpot 다크 (기본)" + +#: src/app/main/ui/settings/options.cljs:69 +msgid "dashboard.select-ui-theme.light" +msgstr "Penpot 라이트" + +#: src/app/main/ui/settings/options.cljs:70 +msgid "dashboard.select-ui-theme.system" +msgstr "시스템 테마" + +#: src/app/main/ui/settings/notifications.cljs:57 +msgid "dashboard.settings.notifications.dashboard-comments.all" +msgstr "모든 댓글, 멘션 및 답글" + +#: src/app/main/ui/settings/notifications.cljs:59 +msgid "dashboard.settings.notifications.dashboard-comments.none" +msgstr "없음" + +#: src/app/main/ui/settings/notifications.cljs:58 +msgid "dashboard.settings.notifications.dashboard-comments.partial" +msgstr "멘션 및 답글만" + +#: src/app/main/ui/settings/notifications.cljs:54 +msgid "dashboard.settings.notifications.dashboard-comments.title" +msgstr "파일 댓글" + +#: src/app/main/ui/settings/notifications.cljs:53 +msgid "dashboard.settings.notifications.dashboard.title" +msgstr "대시보드 알림" + +#: src/app/main/ui/settings/notifications.cljs:67 +msgid "dashboard.settings.notifications.email-comments.all" +msgstr "모든 댓글, 멘션 및 답글" + +#: src/app/main/ui/settings/notifications.cljs:69 +msgid "dashboard.settings.notifications.email-comments.none" +msgstr "없음" + +#: src/app/main/ui/settings/notifications.cljs:68 +msgid "dashboard.settings.notifications.email-comments.partial" +msgstr "멘션 및 답글만" + +#: src/app/main/ui/settings/notifications.cljs:64 +msgid "dashboard.settings.notifications.email-comments.title" +msgstr "파일 댓글" + +#: src/app/main/ui/settings/notifications.cljs:76 +msgid "dashboard.settings.notifications.email-invites.all" +msgstr "모든 종류의 초대 및 요청" + +#: src/app/main/ui/settings/notifications.cljs:79 +msgid "dashboard.settings.notifications.email-invites.none" +msgstr "없음" + +#: src/app/main/ui/settings/notifications.cljs:73 +msgid "dashboard.settings.notifications.email-invites.title" +msgstr "초대 및 요청" + +#: src/app/main/ui/settings/notifications.cljs:63 +msgid "dashboard.settings.notifications.email.title" +msgstr "이메일 알림" + +#: src/app/main/ui/settings/notifications.cljs:84 +msgid "dashboard.settings.notifications.submit" +msgstr "설정 업데이트" + +#: src/app/main/ui/settings/notifications.cljs:52 +msgid "dashboard.settings.notifications.title" +msgstr "알림" + +#: src/app/main/ui/dashboard/projects.cljs:309 +msgid "dashboard.show-all-files" +msgstr "모든 파일 보기" + +#: src/app/main/ui/workspace/main_menu.cljs:668 +msgid "dashboard.show-version-history" +msgstr "버전 히스토리" + +#: src/app/main/ui/dashboard/file_menu.cljs:98 +msgid "dashboard.success-delete-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "파일이 성공적으로 삭제되었습니다" + +#: src/app/main/ui/dashboard/project_menu.cljs:63 +msgid "dashboard.success-delete-project" +msgstr "프로젝트가 성공적으로 삭제되었습니다" + +#: src/app/main/ui/dashboard/file_menu.cljs:93 +msgid "dashboard.success-duplicate-file" +msgid_plural "dashboard.success-delete-file" +msgstr[0] "파일이 성공적으로 복사되었습니다" + +#: src/app/main/ui/dashboard/project_menu.cljs:35 +msgid "dashboard.success-duplicate-project" +msgstr "프로젝트가 성공적으로 복제되었습니다" + +#: src/app/main/ui/dashboard/file_menu.cljs:132, src/app/main/ui/dashboard/grid.cljs:634, src/app/main/ui/dashboard/sidebar.cljs:166 +msgid "dashboard.success-move-file" +msgstr "파일이 성공적으로 이동되었습니다" + +#: src/app/main/ui/dashboard/file_menu.cljs:131 +msgid "dashboard.success-move-files" +msgstr "파일들이 성공적으로 이동되었습니다" + +#: src/app/main/ui/dashboard/project_menu.cljs:57 +msgid "dashboard.success-move-project" +msgstr "프로젝트가 성공적으로 이동되었습니다" + +#: src/app/main/ui/dashboard/team.cljs:1323 +msgid "dashboard.team-info" +msgstr "팀 정보" + +#: src/app/main/ui/dashboard/team.cljs:1329 +msgid "dashboard.team-members" +msgstr "팀 구성원" + +#: src/app/main/ui/dashboard/templates.cljs:134 +msgid "dashboard.template.add-to-project" +msgstr "프로젝트에 추가" + +#: src/app/main/ui/settings/options.cljs:63 +msgid "dashboard.theme-change" +msgstr "UI 테마" + +#: src/app/main/ui/dashboard/deleted.cljs:298 +msgid "dashboard.trash-info-text-part1" +msgstr "삭제된 파일은 휴지통에" + +#: src/app/main/ui/dashboard/deleted.cljs:300 +msgid "dashboard.trash-info-text-part2" +msgstr " %s일 동안 보관됩니다. " + +#: src/app/main/ui/dashboard/deleted.cljs:301 +msgid "dashboard.trash-info-text-part3" +msgstr "그 이후에는 영구적으로 삭제됩니다." + +#: src/app/main/ui/dashboard/deleted.cljs:303 +msgid "dashboard.trash-info-text-part4" +msgstr "필요한 경우, 각 파일 메뉴에서 복원하거나 영구 삭제할 수 있습니다." + +#: src/app/main/ui/dashboard/file_menu.cljs:319, src/app/main/ui/workspace/main_menu.cljs:642 +msgid "dashboard.unpublish-shared" +msgstr "라이브러리 게시 취소" + +#: src/app/main/ui/settings/options.cljs:74 +msgid "dashboard.update-settings" +msgstr "설정 업데이트" + +#: src/app/main/ui/dashboard/sidebar.cljs:1071 +msgid "dashboard.upgrade-plan.no-limits" +msgstr "창의력에 한계를 두지 마세요" + +#: src/app/main/ui/dashboard/sidebar.cljs:1069 +msgid "dashboard.upgrade-plan.penpot-free" +msgstr "Penpot 무료 버전" + +#: src/app/main/ui/dashboard/team.cljs:1115 +msgid "dashboard.webhooks.active" +msgstr "활성화 상태" + +#: src/app/main/ui/dashboard/team.cljs:1116 +msgid "dashboard.webhooks.active.explain" +msgstr "이 훅이 실행되면 이벤트 상세 정보가 전달됩니다" + +#: src/app/main/ui/dashboard/team.cljs:1160 +msgid "dashboard.webhooks.cant-edit" +msgstr "본인이 생성한 웹훅만 삭제하거나 수정할 수 있습니다." + +#: src/app/main/ui/dashboard/team.cljs:1106 +msgid "dashboard.webhooks.content-type" +msgstr "콘텐츠 유형" + +#: src/app/main/ui/dashboard/team.cljs:1139 +msgid "dashboard.webhooks.create" +msgstr "웹훅 생성" + +#: src/app/main/ui/dashboard/team.cljs:1031 +msgid "dashboard.webhooks.create.success" +msgstr "웹훅이 성공적으로 생성되었습니다." + +#: src/app/main/ui/dashboard/team.cljs:1136 +msgid "dashboard.webhooks.description" +msgstr "" +"웹훅은 Penpot에서 특정 이벤트가 발생했을 때 다른 웹사이트나 앱이 알림을 받을 " +"수 있는 간단한 방법입니다. 제공하신 각 URL로 POST 요청을 보냅니다." + +#: src/app/main/ui/dashboard/team.cljs:1265 +msgid "dashboard.webhooks.empty.add-one" +msgstr "새 웹훅을 추가하려면 \"웹훅 추가\" 버튼을 누르세요." + +#: src/app/main/ui/dashboard/team.cljs:1264 +msgid "dashboard.webhooks.empty.no-webhooks" +msgstr "아직 생성된 웹훅이 없습니다." + +#, unused +msgid "dashboard.webhooks.update.success" +msgstr "웹훅이 성공적으로 업데이트되었습니다." + +#: src/app/main/ui/settings.cljs:34 +msgid "dashboard.your-account-title" +msgstr "내 계정" + +#: src/app/main/ui/settings/profile.cljs:70 +msgid "dashboard.your-email" +msgstr "이메일" + +#: src/app/main/ui/settings/profile.cljs:62 +msgid "dashboard.your-name" +msgstr "이름" + +#: src/app/main/ui/dashboard/file_menu.cljs:40, src/app/main/ui/dashboard/fonts.cljs:42, src/app/main/ui/dashboard/libraries.cljs:56, src/app/main/ui/dashboard/projects.cljs:355, src/app/main/ui/dashboard/search.cljs:48, src/app/main/ui/dashboard/sidebar.cljs:312, src/app/main/ui/dashboard/team.cljs:537, src/app/main/ui/dashboard/team.cljs:983, src/app/main/ui/dashboard/team.cljs:1251, src/app/main/ui/dashboard/team.cljs:1298 +msgid "dashboard.your-penpot" +msgstr "내 Penpot" + +#: src/app/main/ui/alert.cljs:35 +msgid "ds.alert-ok" +msgstr "확인" + +#: src/app/main/ui/alert.cljs:34, src/app/main/ui/alert.cljs:37 +msgid "ds.alert-title" +msgstr "주의" + +#: src/app/main/ui/confirm.cljs:86 +msgid "ds.component-subtitle" +msgstr "업데이트할 컴포넌트:" + +#: src/app/main/ui/workspace/plugins.cljs:340, src/app/main/ui/workspace/plugins.cljs:394 +msgid "ds.confirm-allow" +msgstr "허용" + +#: src/app/main/ui/comments.cljs:674, src/app/main/ui/confirm.cljs:37, src/app/main/ui/settings/subscription.cljs:273, src/app/main/ui/settings/subscription.cljs:306, src/app/main/ui/workspace/plugins.cljs:334, src/app/main/ui/workspace/plugins.cljs:388 +msgid "ds.confirm-cancel" +msgstr "취소" + +#: src/app/main/ui/confirm.cljs:38, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:157, src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:102 +msgid "ds.confirm-ok" +msgstr "확인" + +#: src/app/main/ui/confirm.cljs:36, src/app/main/ui/confirm.cljs:40 +msgid "ds.confirm-title" +msgstr "정말 진행하시겠습니까?" + +#: src/app/main/ui/ds/controls/numeric_input.cljs:98 +msgid "ds.inputs.numeric-input.no-applicable-tokens" +msgstr "활성 세트나 테마에 적용 가능한 token이 없습니다." + +#: src/app/main/ui/ds/controls/numeric_input.cljs:99 +msgid "ds.inputs.numeric-input.no-matches" +msgstr "일치하는 항목이 없습니다." + +#: src/app/main/ui/ds/controls/numeric_input.cljs:652, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:141 +msgid "ds.inputs.numeric-input.open-token-list-dropdown" +msgstr "token 목록 열기" + +#: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 +msgid "ds.inputs.token-field.detach-token" +msgstr "token 해제" + +#: src/app/main/ui/ds/controls/utilities/token_field.cljs:43, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:99, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:106 +msgid "ds.inputs.token-field.no-active-token-option" +msgstr "이 token은 활성 세트에 없거나 유효하지 않은 값을 가지고 있습니다." + +#: src/app/main/data/auth.cljs:339 +msgid "errors.auth-provider-not-allowed" +msgstr "이 프로필에 허용되지 않는 인증 제공자입니다" + +#: src/app/main/data/auth.cljs:189 +msgid "errors.auth-provider-not-configured" +msgstr "인증 제공자가 구성되지 않았습니다." + +#: src/app/main/errors.cljs:126 +msgid "errors.auth.unable-to-login" +msgstr "인증되지 않았거나 세션이 만료된 것 같습니다." + +#: src/app/main/data/fonts.cljs:206, src/app/main/ui/dashboard/fonts.cljs:120 +msgid "errors.bad-font" +msgstr "%s 글꼴을 로드할 수 없습니다" + +#: src/app/main/data/fonts.cljs:205 +msgid "errors.bad-font-plural" +msgstr "%s 글꼴들을 로드할 수 없습니다" + +#: src/app/main/data/workspace/media.cljs:204 +msgid "errors.cannot-upload" +msgstr "미디어 파일을 업로드할 수 없습니다." + +#: src/app/main/ui/comments.cljs:719, src/app/main/ui/comments.cljs:749, src/app/main/ui/comments.cljs:846 +msgid "errors.character-limit-exceeded" +msgstr "글자 수 제한 초과" + +#: src/app/main/data/workspace/clipboard.cljs:481 +msgid "errors.clipboard-not-implemented" +msgstr "브라우저에서 이 작업을 지원하지 않습니다" + +#: src/app/main/errors.cljs:235 +msgid "errors.comment-error" +msgstr "댓글 처리 중 오류가 발생했습니다" + +#: src/app/main/errors.cljs:302 +msgid "errors.deprecated" +msgstr "" +"죄송합니다! 이 파일은 더 이상 지원되지 않는 이전 버전의 Penpot 에셋을 " +"사용하고 있어 열 수 없습니다." + +#: src/app/main/errors.cljs:305 +msgid "errors.deprecated.contact.after" +msgstr "문의해 주시면 도와드리겠습니다." + +#: src/app/main/errors.cljs:303 +msgid "errors.deprecated.contact.before" +msgstr "Penpot은 더 이상 이 유형의 에셋을 지원하지 않지만," + +#: src/app/main/errors.cljs:304 +msgid "errors.deprecated.contact.text" +msgstr "고객 지원팀에 문의" + +#: src/app/main/data/workspace/tokens/library_edit.cljs:338 +msgid "errors.drop-token-set-parent-to-child" +msgstr "상위 세트를 하위 세트의 경로로 옮길 수 없습니다." + +#: src/app/main/ui/auth/verify_token.cljs:84, src/app/main/ui/settings/change_email.cljs:29 +msgid "errors.email-already-exists" +msgstr "이미 사용 중인 이메일입니다" + +#: src/app/main/ui/auth/verify_token.cljs:89 +msgid "errors.email-already-validated" +msgstr "이미 인증된 이메일입니다." + +#: src/app/main/ui/auth/register.cljs:105, src/app/main/ui/settings/password.cljs:29 +msgid "errors.email-as-password" +msgstr "이메일을 비밀번호로 사용할 수 없습니다" + +#: src/app/main/ui/auth/register.cljs:89 +msgid "errors.email-does-not-match-invitation" +msgstr "초대받은 이메일과 일치하지 않습니다." + +#: src/app/main/data/auth.cljs:341, src/app/main/ui/auth/register.cljs:95 +msgid "errors.email-domain-not-allowed" +msgstr "허용되지 않는 도메인입니다" + +#: src/app/main/ui/auth/recovery_request.cljs:57, src/app/main/ui/auth/register.cljs:98, src/app/main/ui/auth/register.cljs:101, src/app/main/ui/dashboard/team.cljs:627, src/app/main/ui/settings/change_email.cljs:37 +msgid "errors.email-has-permanent-bounces" +msgstr "«%s» 이메일은 지속적인 반송(Bounce) 리포트가 발생하고 있습니다." + +#: src/app/main/ui/dashboard/team.cljs:196, src/app/main/ui/dashboard/team.cljs:858, src/app/main/ui/onboarding/team_choice.cljs:110 +msgid "errors.email-spam-or-permanent-bounces" +msgstr "«%s» 이메일은 스팸으로 신고되었거나 지속적으로 반송되고 있습니다." + +#: src/app/main/errors.cljs:279 +msgid "errors.feature-mismatch" +msgstr "" +"'%s' 기능이 활성화된 파일을 열려고 하지만, 현재 Penpot 버전에서 지원하지 " +"않거나 비활성화되어 있습니다." + +#: src/app/main/errors.cljs:283, src/app/main/errors.cljs:297 +msgid "errors.feature-not-supported" +msgstr "'%s' 기능은 지원되지 않습니다." + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:296, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:240 +msgid "errors.field-max-length" +msgstr "최대 %s자까지 입력 가능합니다." + +#, unused +msgid "errors.field-min-length" +msgstr "최소 1자 이상 입력해야 합니다." + +#: src/app/util/forms.cljs:66 +msgid "errors.field-missing" +msgstr "필수 입력 항목입니다" + +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +#, unused +msgid "errors.field-not-all-whitespace" +msgstr "이름은 공백 외에 다른 문자를 포함해야 합니다." + +#: src/app/main/errors.cljs:275 +msgid "errors.file-feature-mismatch" +msgstr "" +"활성화된 기능과 열려는 파일의 기능이 일치하지 않습니다. 파일을 열기 전에 " +"'%s'에 대한 마이그레이션이 필요합니다." + +#: src/app/main/data/auth.cljs:347, src/app/main/ui/auth/login.cljs:104, src/app/main/ui/auth/register.cljs:110, src/app/main/ui/auth/register.cljs:304, src/app/main/ui/auth/verify_token.cljs:94, src/app/main/ui/dashboard/team.cljs:199, src/app/main/ui/dashboard/team.cljs:861, src/app/main/ui/onboarding/team_choice.cljs:113, src/app/main/ui/settings/access_tokens.cljs:79, src/app/main/ui/settings/feedback.cljs:84 +msgid "errors.generic" +msgstr "문제가 발생했습니다." + +#: src/app/main/errors.cljs:200 +#, unused +msgid "errors.internal-assertion-error" +msgstr "내부 검증 오류(Internal Assertion Error)" + +#: src/app/main/errors.cljs:214 +msgid "errors.internal-worker-error" +msgstr "웹 워커에서 문제가 발생했습니다." + +#: src/app/main/ui/components/color_input.cljs:51 +msgid "errors.invalid-color" +msgstr "유효하지 않은 컬러" + +#: src/app/util/forms.cljs:35, src/app/util/forms.cljs:89 +msgid "errors.invalid-data" +msgstr "유효하지 않은 데이터" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs +#, unused +msgid "errors.invalid-email" +msgstr "올바른 이메일 주소를 입력해주세요" + +#: src/app/main/ui/settings/change_email.cljs:62 +msgid "errors.invalid-email-confirmation" +msgstr "확인용 이메일이 일치하지 않습니다" + +#: src/app/util/forms.cljs +#, unused +msgid "errors.invalid-text" +msgstr "유효하지 않은 텍스트" + +#: src/app/main/ui/static.cljs:74 +msgid "errors.invite-invalid" +msgstr "유효하지 않은 초대" + +#: src/app/main/ui/static.cljs:75 +msgid "errors.invite-invalid.info" +msgstr "이 초대는 취소되었거나 만료되었을 수 있습니다." + +#: src/app/main/ui/auth/login.cljs:89 +msgid "errors.ldap-disabled" +msgstr "LDAP 인증이 비활성화되었습니다." + +#: src/app/main/errors.cljs:291, src/app/main/ui/dashboard/team.cljs:191, src/app/main/ui/onboarding/team_choice.cljs:105 +msgid "errors.max-quota-reached" +msgstr "'%s' 할당량에 도달했습니다. 고객 지원팀에 문의하세요." + +#: src/app/main/ui/dashboard/team.cljs:187, src/app/main/ui/dashboard/team.cljs:849, src/app/main/ui/onboarding/team_choice.cljs:101 +msgid "errors.maximum-invitations-by-request-reached" +msgstr "한 번의 요청으로 초대할 수 있는 최대 이메일 수(%s개)에 도달했습니다" + +#: src/app/main/data/workspace/media.cljs:190 +msgid "errors.media-too-large" +msgstr "이미지 크기가 너무 커서 삽입할 수 없습니다." + +#: src/app/main/data/media.cljs:70, src/app/main/data/workspace/media.cljs:193 +msgid "errors.media-type-mismatch" +msgstr "이미지 내용이 파일 확장자와 일치하지 않는 것 같습니다." + +#: src/app/main/data/media.cljs:67, src/app/main/data/workspace/media.cljs:178, src/app/main/data/workspace/media.cljs:181, src/app/main/data/workspace/media.cljs:184, src/app/main/data/workspace/media.cljs:187 +msgid "errors.media-type-not-allowed" +msgstr "유효한 이미지가 아닌 것 같습니다." + +#: src/app/main/ui/dashboard/team.cljs:622 +msgid "errors.member-is-muted" +msgstr "초대하려는 프로필의 이메일 수신이 거부되었습니다(스팸 신고 또는 높은 반송률)." + +#: src/app/main/errors.cljs:265 +msgid "errors.migration-in-progress" +msgstr "마이그레이션 진행 중" + +#: src/app/main/errors.cljs:174 +msgid "errors.only-creator-can-lock" +msgstr "버전 생성자만 잠글 수 있습니다" + +#: src/app/main/errors.cljs:182 +msgid "errors.only-creator-can-unlock" +msgstr "버전 생성자만 잠금 해제할 수 있습니다" + +#: src/app/main/ui/settings/password.cljs +#, unused +msgid "errors.password-invalid-confirmation" +msgstr "확인용 비밀번호가 일치하지 않습니다" + +#: src/app/main/ui/settings/password.cljs +#, unused +msgid "errors.password-too-short" +msgstr "비밀번호는 최소 8자 이상이어야 합니다" + +#: src/app/main/errors.cljs:155 +msgid "errors.paste-data-validation" +msgstr "클립보드에 유효하지 않은 데이터가 있습니다" + +#: src/app/main/data/auth.cljs:337, src/app/main/ui/auth/login.cljs:85, src/app/main/ui/auth/login.cljs:93 +msgid "errors.profile-blocked" +msgstr "차단된 프로필입니다" + +#: src/app/main/ui/auth/recovery_request.cljs:53, src/app/main/ui/dashboard/team.cljs:182, src/app/main/ui/dashboard/team.cljs:618, src/app/main/ui/dashboard/team.cljs:844, src/app/main/ui/onboarding/team_choice.cljs:97, src/app/main/ui/settings/change_email.cljs:33 +msgid "errors.profile-is-muted" +msgstr "사용자 프로필의 이메일 수신이 거부되었습니다(스팸 신고 또는 높은 반송률)." + +#: src/app/main/data/auth.cljs:335, src/app/main/ui/auth/register.cljs:92 +msgid "errors.registration-disabled" +msgstr "현재 회원가입이 비활성화되어 있습니다." + +#: src/app/main/errors.cljs:226 +msgid "errors.svg-parser.invalid-svg" +msgstr "SVG가 유효하지 않거나 형식이 잘못되었습니다" + +#: src/app/main/errors.cljs:270 +msgid "errors.team-feature-mismatch" +msgstr "호환되지 않는 기능 '%s'이(가) 감지되었습니다" + +#: src/app/main/ui/dashboard/sidebar.cljs:373, src/app/main/ui/dashboard/team.cljs:393 +msgid "errors.team-leave.insufficient-members" +msgstr "팀에 본인만 남아 있어 나갈 수 없습니다. 팀을 삭제해야 합니다." + +#: src/app/main/ui/dashboard/sidebar.cljs:376, src/app/main/ui/dashboard/team.cljs:396 +msgid "errors.team-leave.member-does-not-exists" +msgstr "할당하려는 멤버가 존재하지 않습니다." + +#: src/app/main/ui/dashboard/sidebar.cljs:379, src/app/main/ui/dashboard/team.cljs:399 +msgid "errors.team-leave.owner-cant-leave" +msgstr "" +"소유자(Owner)는 팀을 나갈 수 없습니다. 소유자 역할을 먼저 다른 분께 위임해야 " +"합니다." + +#: src/app/main/ui/workspace/tokens/sets/helpers.cljs:26, src/app/main/ui/workspace/tokens/sets/helpers.cljs:45 +msgid "errors.token-set-already-exists" +msgstr "동일한 이름의 token 세트가 이미 존재합니다" + +#: src/app/main/data/tokens.cljs: +#, unused +msgid "errors.token-set-doesnt-exists" +msgstr "알 수 없는 세트는 복제할 수 없습니다" + +#: src/app/main/data/workspace/tokens/library_edit.cljs:337 +msgid "errors.token-set-exists-on-drop" +msgstr "이동을 완료할 수 없습니다. 해당 경로에 동일한 이름의 세트가 이미 존재합니다." + +#: src/app/main/data/workspace/tokens/library_edit.cljs:125, src/app/main/data/workspace/tokens/library_edit.cljs:144 +msgid "errors.token-theme-already-exists" +msgstr "동일한 이름의 테마 옵션이 이미 존재합니다" + +#: src/app/main/data/media.cljs:73 +msgid "errors.unexpected-error" +msgstr "예기치 않은 오류가 발생했습니다." + +#: src/app/main/errors.cljs:105 +msgid "errors.unexpected-exception" +msgstr "예기치 않은 오류: %s" + +#: src/app/main/ui/auth/verify_token.cljs:62 +msgid "errors.unexpected-token" +msgstr "알 수 없는 token" + +#, unused +msgid "errors.validation" +msgstr "유효성 검사 오류" + +#: src/app/main/errors.cljs:190 +msgid "errors.version-already-locked" +msgstr "이 버전은 이미 잠겨 있습니다" + +#: src/app/main/errors.cljs:166 +msgid "errors.version-locked" +msgstr "이 버전은 잠겨 있어 다른 사람이 삭제할 수 없습니다" + +#: src/app/main/errors.cljs:287 +msgid "errors.version-not-supported" +msgstr "파일의 버전 번호가 호환되지 않습니다" + +#: src/app/main/ui/static.cljs:315 +msgid "errors.webgl-context-lost.desc-message" +msgstr "WebGL이 작동을 멈췄습니다. 문제를 해결하려면 페이지를 새로고침하세요" + +#: src/app/main/ui/static.cljs:314 +msgid "errors.webgl-context-lost.main-message" +msgstr "캔버스 연결이 끊어졌습니다" + +#: src/app/main/ui/dashboard/team.cljs:1051 +msgid "errors.webhooks.connection" +msgstr "연결 오류, URL에 연결할 수 없습니다" + +#: src/app/main/ui/dashboard/team.cljs:1045 +msgid "errors.webhooks.invalid-uri" +msgstr "URL 유효성 검사를 통과하지 못했습니다." + +#: src/app/main/ui/dashboard/team.cljs:1204 +msgid "errors.webhooks.last-delivery" +msgstr "마지막 전송이 성공하지 못했습니다." + +#: src/app/main/ui/dashboard/team.cljs:1047, src/app/main/ui/dashboard/team.cljs:1207 +msgid "errors.webhooks.ssl-validation" +msgstr "SSL 인증 오류." + +#: src/app/main/ui/dashboard/team.cljs:1049 +msgid "errors.webhooks.timeout" +msgstr "시간 초과" + +#: src/app/main/ui/dashboard/team.cljs:1043 +msgid "errors.webhooks.unexpected" +msgstr "유효성 검사 중 예기치 않은 오류 발생" + +#: src/app/main/ui/dashboard/team.cljs:1053, src/app/main/ui/dashboard/team.cljs:1210 +msgid "errors.webhooks.unexpected-status" +msgstr "예기치 않은 상태 %s" + +#: src/app/main/ui/auth/login.cljs:97, src/app/main/ui/auth/login.cljs:101 +msgid "errors.wrong-credentials" +msgstr "이메일 또는 비밀번호가 올바르지 않습니다." + +#: src/app/main/ui/settings/password.cljs:25 +msgid "errors.wrong-old-password" +msgstr "이전 비밀번호가 올바르지 않습니다" + +#: src/app/main/ui/settings/feedback.cljs:120 +msgid "feedback.description" +msgstr "설명" + +#: src/app/main/ui/settings/feedback.cljs:122 +msgid "feedback.description-placeholder" +msgstr "의견을 보내시는 이유를 설명해주세요" + +#: src/app/main/ui/settings/feedback.cljs:150 +msgid "feedback.discourse-subtitle1" +msgstr "" +"함께해주셔서 기쁩니다. 도움이 필요하시면 게시하기 전에 먼저 검색을 " +"이용해보세요." + +#: src/app/main/ui/settings/feedback.cljs:149 +msgid "feedback.discourse-title" +msgstr "Penpot 커뮤니티" + +#: src/app/main/ui/settings/feedback.cljs:143 +msgid "feedback.other-ways-contact" +msgstr "기타 문의 방법" + +#: src/app/main/ui/settings/feedback.cljs:126 +msgid "feedback.penpot.link" +msgstr "파일이나 프로젝트와 관련된 의견인 경우 여기에 Penpot 링크를 추가해주세요:" + +#: src/app/main/ui/settings/feedback.cljs:105 +msgid "feedback.subject" +msgstr "제목" + +#: src/app/main/ui/settings/feedback.cljs:102 +msgid "feedback.subtitle" +msgstr "" +"문의하시는 이유를 이슈, 제안, 궁금한 점 등으로 구분하여 설명해주세요. 담당 " +"멤버가 최대한 빨리 답변해 드리겠습니다." + +#: src/app/main/ui/settings/feedback.cljs:101 +msgid "feedback.title-contact-us" +msgstr "문의하기" + +#: src/app/main/ui/settings/feedback.cljs:156 +msgid "feedback.twitter-subtitle1" +msgstr "기술적인 문의 사항을 도와드립니다." + +#: src/app/main/ui/settings/feedback.cljs:155 +msgid "feedback.twitter-title" +msgstr "X(Twitter) 지원 계정" + +#: src/app/main/ui/settings/feedback.cljs:110, src/app/main/ui/settings/feedback.cljs:111 +msgid "feedback.type" +msgstr "유형" + +#: src/app/main/ui/settings/feedback.cljs:115 +msgid "feedback.type.doubt" +msgstr "궁금한 점" + +#: src/app/main/ui/settings/feedback.cljs:113 +msgid "feedback.type.idea" +msgstr "아이디어/제안" + +#: src/app/main/ui/settings/feedback.cljs:114 +msgid "feedback.type.issue" +msgstr "이슈/문제" + +#: src/app/main/ui/exports/files.cljs:133 +msgid "files-download-modal.description-2" +msgstr "* 컴포넌트, 그래픽, 컬러 및/또는 타이포그래피가 포함될 수 있습니다." + +#: src/app/main/ui/exports/files.cljs:141 +msgid "files-download-modal.options.all.message" +msgstr "공유 라이브러리가 포함된 파일이 연결 상태를 유지하며 내보내기에 포함됩니다." + +#: src/app/main/ui/exports/files.cljs:142 +msgid "files-download-modal.options.all.title" +msgstr "공유 라이브러리 내보내기" + +#: src/app/main/ui/exports/files.cljs:143 +msgid "files-download-modal.options.detach.message" +msgstr "" +"공유 라이브러리는 내보내기에 포함되지 않으며, 라이브러리에 에셋이 추가되지 " +"않습니다. " + +#: src/app/main/ui/exports/files.cljs:144 +msgid "files-download-modal.options.detach.title" +msgstr "공유 라이브러리 에셋을 일반 객체로 변환" + +#: src/app/main/ui/exports/files.cljs:145 +msgid "files-download-modal.options.merge.message" +msgstr "모든 외부 에셋이 파일 라이브러리에 병합된 상태로 파일이 내보내집니다." + +#: src/app/main/ui/exports/files.cljs:146 +msgid "files-download-modal.options.merge.title" +msgstr "공유 라이브러리 에셋을 파일 라이브러리에 포함" + +#: src/app/main/ui/exports/files.cljs:124 +msgid "files-download-modal.title" +msgstr "파일 다운로드" + +#: src/app/main/ui/settings/password.cljs:31 +msgid "generic.error" +msgstr "오류가 발생했습니다" + +#: src/app/main/ui/components/color_input.cljs:31 +msgid "inspect.attributes.color" +msgstr "색상" + +#: src/app/main/ui/inspect/styles/rows/color_properties_row.cljs:120 +msgid "inspect.attributes.image.preview" +msgstr "도형 채우기 이미지 미리보기" + +#, unused +msgid "inspect.attributes.stroke.style.none" +msgstr "없음" + +#: src/app/main/ui/inspect/attributes/text.cljs:113 +#, unused +msgid "inspect.attributes.typography.font-weight" +msgstr "글꼴 굵기" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:397, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:408 +msgid "inspect.attributes.typography.letter-spacing" +msgstr "자간" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:379, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:389 +msgid "inspect.attributes.typography.line-height" +msgstr "행간" + +#: src/app/main/ui/inspect/attributes/text.cljs:140 +#, unused +msgid "inspect.attributes.typography.text-decoration" +msgstr "텍스트 장식" + +#, unused +msgid "inspect.attributes.typography.text-decoration.line-through" +msgstr "취소선" + +#: src/app/main/ui/inspect/attributes/text.cljs:111 +msgid "inspect.attributes.typography.text-decoration.none" +msgstr "없음" + +#: src/app/main/ui/inspect/attributes/text.cljs:125, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:437 +msgid "inspect.attributes.typography.text-transform.capitalize" +msgstr "첫 글자 대문자" + +#: src/app/main/ui/inspect/attributes/text.cljs:124 +msgid "inspect.attributes.typography.text-transform.none" +msgstr "없음" + +#: src/app/main/ui/inspect/attributes/text.cljs:127 +msgid "inspect.attributes.typography.text-transform.unset" +msgstr "설정 해제" + +#: src/app/main/ui/inspect/attributes/variant.cljs:44 +msgid "inspect.attributes.variant" +msgstr "베리언트 속성" + +#: src/app/main/ui/inspect/attributes/variant.cljs:44 +msgid "inspect.attributes.variants" +msgstr "베리언트 속성" + +#: src/app/main/ui/inspect/right_sidebar.cljs:170 +msgid "inspect.color-space-label" +msgstr "색상 공간 선택" + +#: src/app/main/ui/inspect/right_sidebar.cljs:234 +msgid "inspect.empty.help" +msgstr "디자인 검사(Inspect)에 대해 더 알고 싶으시면 Penpot 도움말 센터를 방문하세요" + +#: src/app/main/ui/inspect/right_sidebar.cljs:238 +msgid "inspect.empty.more" +msgstr "더 보기" + +#: src/app/main/ui/inspect/right_sidebar.cljs:232 +msgid "inspect.empty.select" +msgstr "속성과 코드를 검사할 도형, 보드 또는 그룹을 선택하세요" + +#: src/app/main/ui/inspect/right_sidebar.cljs:166 +msgid "inspect.layer-info" +msgstr "레이어 정보" + +#: src/app/main/ui/inspect/right_sidebar.cljs:137 +msgid "inspect.multiple-selected" +msgstr "%s개 선택됨" + +#: src/app/main/ui/inspect/right_sidebar.cljs:68 +msgid "inspect.subtitle.copy" +msgstr "복사" + +#: src/app/main/ui/inspect/right_sidebar.cljs:64 +msgid "inspect.subtitle.main" +msgstr "메인 컴포넌트" + +#: src/app/main/ui/inspect/styles/style_box.cljs:68 +msgid "inspect.tabs.styles.copy-shorthand" +msgstr "CSS 단축 표기를 클립보드에 복사" + +#: src/app/main/ui/inspect/styles/property_detail_copiable.cljs:51 +msgid "inspect.tabs.styles.copy-to-clipboard" +msgstr "클립보드에 복사" + +#: src/app/main/ui/inspect/styles/style_box.cljs:22 +#, unused +msgid "inspect.tabs.styles.geometry-panel" +msgstr "크기 및 위치" + +#: src/app/main/ui/inspect/styles/style_box.cljs:60, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:178 +msgid "inspect.tabs.styles.toggle-style" +msgstr "%s 패널 표시/숨김" + +#: src/app/main/ui/inspect/styles/style_box.cljs:21 +msgid "inspect.tabs.styles.token-panel" +msgstr "token 세트 및 테마" + +#: src/app/main/ui/inspect/styles/rows/color_properties_row.cljs:102, src/app/main/ui/inspect/styles/rows/properties_row.cljs:60 +msgid "inspect.tabs.styles.token-resolved-value" +msgstr "계산된 값:" + +#: src/app/main/ui/inspect/styles/style_box.cljs:20 +msgid "inspect.tabs.styles.variants-panel" +msgstr "베리언트 속성" + +#: src/app/main/ui/dashboard/comments.cljs:96 +msgid "label.mark-all-as-read" +msgstr "모두 읽음으로 표시" + +#: src/app/main/ui/dashboard/sidebar.cljs:1138 +msgid "labels.about-penpot" +msgstr "Penpot 정보" + +#: src/app/main/ui/settings/sidebar.cljs:123 +msgid "labels.access-tokens" +msgstr "액세스 토큰" + +#: src/app/main/ui/workspace/libraries.cljs:169, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1040 +msgid "labels.add" +msgstr "추가" + +#: src/app/main/ui/workspace/libraries.cljs:169 +msgid "labels.adding" +msgstr "추가 중..." + +#: src/app/main/ui/onboarding/questions.cljs:160 +msgid "labels.adobe-xd" +msgstr "Adobe XD" + +#: src/app/main/ui/static.cljs:297 +msgid "labels.bad-gateway.desc-message" +msgstr "잠시 기다린 후 다시 시도해주세요. 현재 서버 유지보수 작업을 진행 중입니다." + +#: src/app/main/ui/inspect/styles/style_box.cljs:26 +msgid "labels.blur" +msgstr "블러" + +#: src/app/main/ui/onboarding/questions.cljs:162 +msgid "labels.canva" +msgstr "Canva" + +#: src/app/main/ui/workspace/tokens/sets/lists.cljs:181 +msgid "labels.collapse" +msgstr "접기" + +#: src/app/main/ui/workspace/colorpicker.cljs:424 +msgid "labels.color" +msgstr "색상" + +#: src/app/main/ui/comments.cljs:901 +msgid "labels.comment" +msgstr "댓글" + +#: src/app/main/ui/comments.cljs:905 +msgid "labels.comment.mark-as-solved" +msgstr "해결됨으로 표시" + +#: src/app/main/ui/dashboard/sidebar.cljs:1125 +msgid "labels.community-contributions" +msgstr "커뮤니티 및 기여" + +#: src/app/main/ui/inspect/right_sidebar.cljs:110 +msgid "labels.computed" +msgstr "계산됨" + +#: src/app/main/ui/static.cljs:415 +msgid "labels.contact-support" +msgstr "고객 지원 문의" + +#: src/app/main/ui/settings/sidebar.cljs:136 +msgid "labels.contact-us" +msgstr "문의하기" + +#, unused +msgid "labels.continue-with" +msgstr "다음으로 계속하기:" + +#: src/app/main/ui/viewer/login.cljs:69 +msgid "labels.continue-with-penpot" +msgstr "Penpot 계정으로 계속할 수 있습니다" + +#: src/app/main/ui/components/copy_button.cljs:41 +msgid "labels.copy" +msgstr "복사" + +#: src/app/main/ui/inspect/attributes/common.cljs:101 +msgid "labels.copy-color" +msgstr "색상 복사" + +#: src/app/main/ui/static.cljs:67 +msgid "labels.copyright-period" +msgstr "Kaleidos © 2019-present" + +#: src/app/main/ui/dashboard/file_menu.cljs:291 +msgid "labels.delete-multi-files" +msgstr "%s개 파일 삭제" + +#: src/app/main/ui/dashboard/deleted.cljs:215 +msgid "labels.deleted" +msgstr "삭제된 항목" + +#: src/app/main/ui/onboarding/questions.cljs:86 +msgid "labels.developer" +msgstr "개발" + +#: src/app/main/ui/onboarding/questions.cljs:260 +#, unused +msgid "labels.director" +msgstr "디렉터" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:211 +msgid "labels.discard" +msgstr "취소" + +#: src/app/main/ui/settings/feedback.cljs:134, src/app/main/ui/static.cljs:409 +msgid "labels.download" +msgstr "%s 다운로드" + +#: src/app/main/ui/workspace/tokens/sets/context_menu.cljs:65 +msgid "labels.duplicate" +msgstr "복제" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:300 +msgid "labels.empty" +msgstr "비어 있음" + +#: src/app/main/ui/dashboard/import.cljs:297 +msgid "labels.error" +msgstr "오류" + +#: src/app/main/ui/onboarding/questions.cljs:404 +#, unused +msgid "labels.event" +msgstr "이벤트" + +#: src/app/main/ui/settings/feedback.cljs:83 +msgid "labels.feedback-disabled" +msgstr "의견 보내기 비활성화됨" + +#: src/app/main/ui/settings/feedback.cljs:74 +msgid "labels.feedback-sent" +msgstr "의견이 전송되었습니다" + +#: src/app/main/ui/onboarding/questions.cljs:156 +msgid "labels.figma" +msgstr "Figma" + +#: src/app/main/ui/inspect/styles/style_box.cljs:23 +msgid "labels.fill" +msgstr "채우기" + +#: src/app/main/ui/onboarding/questions.cljs:259 +#, unused +msgid "labels.founder" +msgstr "CEO 또는 설립자" + +#: src/app/main/ui/onboarding/questions.cljs:258 +#, unused +msgid "labels.freelancer" +msgstr "프리랜서" + +#: src/app/main/ui/dashboard/sidebar.cljs:929, src/app/main/ui/workspace/main_menu.cljs:176 +msgid "labels.github-repo" +msgstr "Github 저장소" + +#: src/app/main/ui/dashboard/sidebar.cljs:904, src/app/main/ui/workspace/main_menu.cljs:205 +msgid "labels.give-feedback" +msgstr "의견 보내기" + +#: src/app/main/ui/onboarding/questions.cljs:88 +msgid "labels.graphic-design" +msgstr "그래픽 디자인" + +#: src/app/main/ui/dashboard/sidebar.cljs:1114 +msgid "labels.help-learning" +msgstr "도움말 및 학습" + +#: src/app/main/ui/dashboard/templates.cljs:91 +msgid "labels.hide" +msgstr "숨기기" + +#: src/app/main/ui/viewer/comments.cljs:103, src/app/main/ui/workspace/comments.cljs:75 +msgid "labels.hide-resolved-comments" +msgstr "해결된 댓글 숨기기" + +#: src/app/main/ui/workspace/tokens/sidebar.cljs:131 +msgid "labels.import" +msgstr "가져오기" + +#: src/app/main/ui/dashboard/fonts.cljs:430 +msgid "labels.installed-fonts" +msgstr "설치된 글꼴" + +#: src/app/main/ui/static.cljs:405 +msgid "labels.internal-error.desc-message-first" +msgstr "문제가 발생했습니다." + +#: src/app/main/ui/static.cljs:406 +msgid "labels.internal-error.desc-message-second" +msgstr "작업을 다시 시도하거나 지원팀에 문의하세요." + +#: src/app/main/ui/static.cljs:402 +msgid "labels.internal-error.main-message" +msgstr "내부 오류" + +#: src/app/main/ui/onboarding/questions.cljs:164 +msgid "labels.invision" +msgstr "InVision" + +#: src/app/main/ui/dashboard/sidebar.cljs:454, src/app/main/ui/dashboard/team.cljs:102, src/app/main/ui/dashboard/team.cljs:110, src/app/main/ui/dashboard/team.cljs:944 +msgid "labels.invitations" +msgstr "초대" + +#: src/app/main/ui/settings/options.cljs:53 +msgid "labels.language" +msgstr "언어" + +#: src/app/main/ui/inspect/styles/style_box.cljs:28 +msgid "labels.layout" +msgstr "레이아웃" + +#: src/app/main/ui/dashboard/sidebar.cljs:893 +msgid "labels.learning-center" +msgstr "학습 센터" + +#: src/app/main/ui/workspace/main_menu.cljs:168 +msgid "labels.libraries-and-templates" +msgstr "라이브러리 및 템플릿" + +#: src/app/main/ui/auth/verify_token.cljs:100, src/app/main/ui/dashboard/grid.cljs:126, src/app/main/ui/dashboard/grid.cljs:147, src/app/main/ui/dashboard/import.cljs:258, src/app/main/ui/dashboard/placeholder.cljs:140, src/app/main/ui/ds/product/loader.cljs:85, src/app/main/ui/exports/files.cljs:60, src/app/main/ui/viewer.cljs:642, src/app/main/ui/workspace/sidebar/assets/file_library.cljs:249, src/app/main/ui/workspace.cljs:129, src/app/main/ui.cljs:70, src/app/main/ui.cljs:108, src/app/main/ui.cljs:127 +msgid "labels.loading" +msgstr "로드 중…" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:210 +msgid "labels.lock" +msgstr "잠금" + +#: src/app/main/ui/viewer/header.cljs:205 +msgid "labels.log-or-sign" +msgstr "로그인 또는 회원가입" + +#: src/app/main/ui/static.cljs:61, src/app/main/ui/static.cljs:137 +msgid "labels.login" +msgstr "로그인" + +#: src/app/main/ui/dashboard/sidebar.cljs:1148 +msgid "labels.logout" +msgstr "로그아웃" + +#: src/app/main/ui/onboarding/questions.cljs:89 +msgid "labels.marketing" +msgstr "마케팅" + +#: src/app/main/ui/dashboard/team.cljs:512 +msgid "labels.member" +msgstr "구성원" + +#: src/app/main/ui/dashboard/sidebar.cljs:450, src/app/main/ui/dashboard/team.cljs:100, src/app/main/ui/dashboard/team.cljs:108 +msgid "labels.members" +msgstr "구성원" + +#: src/app/main/ui/comments.cljs:581 +msgid "labels.mention" +msgstr "멘션" + +#: src/app/main/ui/ds/controls/numeric_input.cljs:631 +msgid "labels.mixed-values" +msgstr "혼합" + +#: src/app/main/ui/settings/password.cljs:86 +msgid "labels.new-password" +msgstr "새 비밀번호" + +#: src/app/main/ui/dashboard/templates.cljs:301, src/app/main/ui/onboarding/questions.cljs:54, src/app/main/ui/viewer.cljs:112 +msgid "labels.next" +msgstr "다음" + +#: src/app/main/ui/dashboard/comments.cljs:122, src/app/main/ui/workspace/comments.cljs:162 +msgid "labels.no-comments-available" +msgstr "모든 알림을 확인했습니다! 새로운 댓글 알림이 여기에 표시됩니다." + +#: src/app/main/ui/dashboard/team.cljs:737 +msgid "labels.no-invitations" +msgstr "대기 중인 초대가 없습니다." + +#: src/app/main/ui/dashboard/team.cljs:739 +msgid "labels.no-invitations-gather-people" +msgstr "사람들을 모아 멋진 결과물을 함께 만들어보세요." + +#: src/app/main/ui/static.cljs +#, unused +msgid "labels.not-found.desc-message" +msgstr "페이지가 존재하지 않거나 접근 권한이 없습니다." + +#: src/app/main/ui/static.cljs:286 +msgid "labels.not-found.main-message" +msgstr "죄송합니다!" + +#: src/app/main/ui/settings/sidebar.cljs:103 +msgid "labels.notifications" +msgstr "알림" + +#: src/app/main/ui/dashboard/projects.cljs:240, src/app/main/ui/dashboard/team.cljs:1354 +msgid "labels.num-of-files" +msgid_plural "labels.num-of-files" +msgstr[0] "파일 %s개" + +#: src/app/main/ui/viewer/thumbnails.cljs:82 +msgid "labels.num-of-frames" +msgid_plural "labels.num-of-frames" +msgstr[0] "보드 %s개" + +#: src/app/main/ui/dashboard/team.cljs:1349 +msgid "labels.num-of-projects" +msgid_plural "labels.num-of-projects" +msgstr[0] "프로젝트 %개" + +#, unused +msgid "labels.ok" +msgstr "확인" + +#: src/app/main/ui/settings/password.cljs:79 +msgid "labels.old-password" +msgstr "이전 비밀번호" + +#: src/app/main/ui/workspace/comments.cljs +#, unused +msgid "labels.only-yours" +msgstr "내 항목만" + +#: src/app/main/ui/comments.cljs:911, src/app/main/ui/comments.cljs:976, src/app/main/ui/workspace/palette.cljs:199, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:107, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:906, src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:155, src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:213, src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:294, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:402, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1031, src/app/main/ui/workspace/sidebar/options/menus/text.cljs:316, src/app/main/ui/workspace/sidebar/options/menus/text.cljs:345, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:146 +msgid "labels.options" +msgstr "옵션" + +#, unused +msgid "labels.or" +msgstr "또는" + +#: src/app/main/ui/onboarding/questions.cljs:131, src/app/main/ui/onboarding/questions.cljs:203, src/app/main/ui/onboarding/questions.cljs:285, src/app/main/ui/onboarding/questions.cljs:358 +msgid "labels.other" +msgstr "기타 (직접 입력)" + +#: src/app/main/ui/onboarding/questions.cljs:91, src/app/main/ui/onboarding/questions.cljs:166, src/app/main/ui/onboarding/questions.cljs:255, src/app/main/ui/onboarding/questions.cljs:324 +msgid "labels.other-short" +msgstr "기타" + +#: src/app/main/ui/dashboard/team.cljs:324, src/app/main/ui/dashboard/team.cljs:564, src/app/main/ui/dashboard/team.cljs:1335 +msgid "labels.owner" +msgstr "소유자(Owner)" + +#: src/app/main/ui/settings/sidebar.cljs:98 +msgid "labels.password" +msgstr "비밀번호" + +#: src/app/main/ui/dashboard/team.cljs:669 +msgid "labels.pending-invitation" +msgstr "대기 중" + +#: src/app/main/ui/dashboard/sidebar.cljs:973 +msgid "labels.penpot-changelog" +msgstr "Penpot 변경 사항" + +#: src/app/main/ui/dashboard/sidebar.cljs:899 +msgid "labels.penpot-hub" +msgstr "Penpot 허브" + +#: src/app/main/ui/comments.cljs:680 +msgid "labels.post" +msgstr "게시" + +#: src/app/main/ui/onboarding/questions.cljs:50, src/app/main/ui/viewer.cljs:105 +msgid "labels.previous" +msgstr "이전" + +#: src/app/main/ui/onboarding/questions.cljs:85 +msgid "labels.product-design" +msgstr "제품 또는 UX 디자인" + +#: src/app/main/ui/onboarding/questions.cljs:90 +msgid "labels.product-management" +msgstr "제품 관리(PM)" + +#: src/app/main/ui/settings/profile.cljs:128, src/app/main/ui/settings/sidebar.cljs:93 +msgid "labels.profile" +msgstr "프로필" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:205, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:179 +msgid "labels.reference" +msgstr "참조" + +#: src/app/main/data/common.cljs:83 +msgid "labels.refresh" +msgstr "새로고침" + +#: src/app/main/ui/settings/sidebar.cljs:129, src/app/main/ui/workspace/main_menu.cljs:160 +msgid "labels.release-notes" +msgstr "릴리즈 노트" + +#: src/app/main/ui/workspace.cljs +#, unused +msgid "labels.reload-file" +msgstr "파일 다시 로드" + +#: src/app/main/ui/static.cljs:318 +msgid "labels.reload-page" +msgstr "페이지 새로고침" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:167, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:406 +msgid "labels.remove" +msgstr "제거" + +#: src/app/main/ui/dashboard/team.cljs:355 +msgid "labels.remove-member" +msgstr "구성원 제거" + +#: src/app/main/ui/dashboard/file_menu.cljs:299, src/app/main/ui/dashboard/project_menu.cljs:88, src/app/main/ui/dashboard/sidebar.cljs:471, src/app/main/ui/workspace/sidebar/assets/groups.cljs:167, src/app/main/ui/workspace/sidebar/versions.cljs:192, src/app/main/ui/workspace/tokens/sets/context_menu.cljs:63 +msgid "labels.rename" +msgstr "이름 바꾸기" + +#: src/app/main/ui/dashboard/team_form.cljs:98 +msgid "labels.rename-team" +msgstr "팀 이름 바꾸기" + +#: src/app/main/ui/comments.cljs:642 +msgid "labels.replies" +msgstr "답글" + +#: src/app/main/ui/comments.cljs:647 +msgid "labels.replies.new" +msgstr "새 답글" + +#: src/app/main/ui/comments.cljs:641 +msgid "labels.reply" +msgstr "답글" + +#: src/app/main/ui/comments.cljs:646 +msgid "labels.reply.new" +msgstr "새 답글" + +#: src/app/main/ui/comments.cljs:713 +msgid "labels.reply.thread" +msgstr "답글 달기" + +#: src/app/main/ui/dashboard/team.cljs:788 +msgid "labels.resend" +msgstr "재전송" + +#: src/app/main/ui/dashboard/team.cljs:938 +msgid "labels.resend-invitation" +msgstr "초대 재전송" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:87, src/app/main/ui/workspace/sidebar/versions.cljs:197 +msgid "labels.restore" +msgstr "복원" + +#: src/app/main/ui/components/progress.cljs:80, src/app/main/ui/static.cljs:299, src/app/main/ui/static.cljs:308, src/app/main/ui/static.cljs:419 +msgid "labels.retry" +msgstr "다시 시도" + +#: src/app/main/ui/dashboard/team.cljs:513, src/app/main/ui/dashboard/team.cljs:945 +msgid "labels.role" +msgstr "역할" + +#: src/app/main/ui/dashboard/fonts.cljs:395, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:204, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:301, src/app/main/ui/workspace/tokens/settings/menu.cljs:110 +msgid "labels.save" +msgstr "저장" + +#: src/app/main/ui/dashboard/fonts.cljs:435 +msgid "labels.search-font" +msgstr "글꼴 검색" + +#: src/app/main/ui/onboarding/questions.cljs:84, src/app/main/ui/onboarding/questions.cljs:230, src/app/main/ui/onboarding/questions.cljs:240 +msgid "labels.select-option" +msgstr "옵션 선택" + +#: src/app/main/ui/settings/feedback.cljs:137 +msgid "labels.send" +msgstr "전송" + +#: src/app/main/ui/settings/feedback.cljs:137 +msgid "labels.sending" +msgstr "전송 중…" + +#: src/app/main/ui/static.cljs:306 +msgid "labels.service-unavailable.desc-message" +msgstr "시스템 정기 점검 중입니다." + +#: src/app/main/ui/static.cljs:305 +msgid "labels.service-unavailable.main-message" +msgstr "서비스를 사용할 수 없음" + +#: src/app/main/ui/workspace/tokens/sidebar.cljs:75 +msgid "labels.sets" +msgstr "세트" + +#: src/app/main/ui/dashboard/sidebar.cljs:464, src/app/main/ui/dashboard/team.cljs:101, src/app/main/ui/dashboard/team.cljs:115, src/app/main/ui/settings/options.cljs:87, src/app/main/ui/settings/sidebar.cljs:109 +msgid "labels.settings" +msgstr "설정" + +#: src/app/main/ui/inspect/styles/style_box.cljs:27, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:229 +msgid "labels.shadow" +msgstr "그림자" + +#: src/app/main/ui/viewer/header.cljs:201 +msgid "labels.share" +msgstr "공유" + +#, unused +msgid "labels.share-prototype" +msgstr "프로토타입 공유" + +#: src/app/main/ui/dashboard/sidebar.cljs:840 +msgid "labels.shared-libraries" +msgstr "라이브러리" + +#: src/app/main/ui/dashboard/templates.cljs:87 +msgid "labels.show" +msgstr "표시" + +#: src/app/main/ui/viewer/comments.cljs:82, src/app/main/ui/workspace/comments.cljs:57, src/app/main/ui/workspace/comments.cljs:136 +msgid "labels.show-all-comments" +msgstr "모든 댓글 보기" + +#: src/app/main/ui/viewer/comments.cljs:115 +msgid "labels.show-comments-list" +msgstr "댓글 목록 보기" + +#: src/app/main/ui/workspace/comments.cljs:69, src/app/main/ui/workspace/comments.cljs:138 +msgid "labels.show-mentions" +msgstr "내 멘션만 보기" + +#: src/app/main/ui/viewer/comments.cljs:91, src/app/main/ui/workspace/comments.cljs:63, src/app/main/ui/workspace/comments.cljs:137 +msgid "labels.show-your-comments" +msgstr "내 댓글만 보기" + +#: src/app/main/ui/onboarding/questions.cljs:158 +msgid "labels.sketch" +msgstr "Sketch" + +#: src/app/main/ui/dashboard/sidebar.cljs:825 +msgid "labels.sources" +msgstr "원본" + +#: src/app/main/ui/onboarding/questions.cljs:55 +msgid "labels.start" +msgstr "시작" + +#: src/app/main/ui/dashboard/team.cljs:954 +msgid "labels.status" +msgstr "상태" + +#: src/app/main/ui/inspect/styles/style_box.cljs:24, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:46 +msgid "labels.stroke" +msgstr "선" + +#: src/app/main/ui/onboarding/questions.cljs:87 +msgid "labels.student-teacher" +msgstr "학생 또는 교사" + +#: src/app/main/ui/inspect/right_sidebar.cljs:108, src/app/main/ui/inspect/styles.cljs:135 +msgid "labels.styles" +msgstr "스타일" + +#: src/app/main/ui/inspect/styles/style_box.cljs:33 +msgid "labels.svg" +msgstr "SVG" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:261 +msgid "labels.switch" +msgstr "전환" + +#: src/app/main/ui/onboarding/questions.cljs:256 +#, unused +msgid "labels.team-leader" +msgstr "팀 리더" + +#: src/app/main/ui/onboarding/questions.cljs:257 +#, unused +msgid "labels.team-member" +msgstr "팀 구성원" + +#: src/app/main/ui/inspect/styles/style_box.cljs:25 +msgid "labels.text" +msgstr "텍스트" + +#: src/app/main/ui/workspace/tokens/themes.cljs:36 +msgid "labels.themes" +msgstr "테마" + +#: src/app/main/ui/workspace/main_menu.cljs:152 +msgid "labels.tutorials" +msgstr "튜토리얼" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:189 +msgid "labels.typography" +msgstr "타이포그래피" + +#: src/app/main/data/workspace/tokens/errors.cljs:121 +msgid "labels.unknown-error" +msgstr "알 수 없는 오류" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:204 +msgid "labels.unlock" +msgstr "잠금 해제" + +#: src/app/main/ui/dashboard/file_menu.cljs:285 +msgid "labels.unpublish-multi-files" +msgstr "%s개 파일 게시 취소" + +#: src/app/main/ui/settings/profile.cljs:111 +msgid "labels.update" +msgstr "업데이트" + +#: src/app/main/ui/dashboard/team_form.cljs:119 +msgid "labels.update-team" +msgstr "팀 업데이트" + +#: src/app/main/ui/dashboard/fonts.cljs:253 +msgid "labels.upload" +msgstr "업로드" + +#: src/app/main/ui/dashboard/fonts.cljs:180 +msgid "labels.upload-custom-fonts" +msgstr "사용자 지정 글꼴 업로드" + +#: src/app/main/ui/dashboard/fonts.cljs:252 +msgid "labels.uploading" +msgstr "업로드 중…" + +#: src/app/main/ui/inspect/right_sidebar.cljs:66, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1039 +msgid "labels.variant" +msgstr "베리언트" + +#: src/app/main/ui/dashboard/sidebar.cljs:967 +msgid "labels.version-notes" +msgstr "버전 %s 노트" + +#: src/app/main/ui/inspect/styles/style_box.cljs:32 +msgid "labels.visibility" +msgstr "가시성" + +#: src/app/main/ui/dashboard/team.cljs:268 +msgid "labels.you" +msgstr "(나)" + +#: src/app/main/ui/dashboard/sidebar.cljs:1101 +msgid "labels.your-account" +msgstr "내 계정" + +#: src/app/main/ui/onboarding/questions.cljs:403 +#, unused +msgid "labels.youtube" +msgstr "YouTube" + +#: src/app/main/ui/ds/product/loader.cljs:21 +msgid "loader.tips.01.message" +msgstr "여러 프로젝트에서 디자인의 일관성을 유지하고 간편하게 업데이트하세요." + +#: src/app/main/ui/ds/product/loader.cljs:20 +msgid "loader.tips.01.title" +msgstr "재사용 가능한 컴포넌트" + +#: src/app/main/ui/ds/product/loader.cljs:23 +msgid "loader.tips.02.message" +msgstr "팀원들과 실시간으로 작업하고 즉시 피드백을 공유하세요." + +#: src/app/main/ui/ds/product/loader.cljs:22 +msgid "loader.tips.02.title" +msgstr "실시간 협업" + +#: src/app/main/ui/ds/product/loader.cljs:25 +msgid "loader.tips.03.message" +msgstr "익숙한 CSS 방식의 레이아웃 컨트롤로 유연하게 디자인하세요." + +#: src/app/main/ui/ds/product/loader.cljs:24 +msgid "loader.tips.03.title" +msgstr "CSS 스타일 레이아웃" + +#: src/app/main/ui/ds/product/loader.cljs:27 +msgid "loader.tips.04.message" +msgstr "디자인에서 직접 CSS 및 SVG 코드를 추출하세요." + +#: src/app/main/ui/ds/product/loader.cljs:26 +msgid "loader.tips.04.title" +msgstr "코드로 내보내기" + +#: src/app/main/ui/ds/product/loader.cljs:29 +msgid "loader.tips.05.message" +msgstr "에셋과 스타일을 공유하여 일관성을 유지하세요." + +#: src/app/main/ui/ds/product/loader.cljs:28 +msgid "loader.tips.05.title" +msgstr "디자인 라이브러리" + +#: src/app/main/ui/ds/product/loader.cljs:31 +msgid "loader.tips.06.message" +msgstr "애니메이션과 트랜지션을 통해 아이디어에 생동감을 불어넣으세요." + +#: src/app/main/ui/ds/product/loader.cljs:30 +msgid "loader.tips.06.title" +msgstr "인터랙티브 프로토타입" + +#: src/app/main/ui/ds/product/loader.cljs:33 +msgid "loader.tips.07.message" +msgstr "Penpot은 원활한 개발을 위해 SVG와 CSS 표준을 사용합니다." + +#: src/app/main/ui/ds/product/loader.cljs:32 +msgid "loader.tips.07.title" +msgstr "웹 표준 포맷" + +#: src/app/main/ui/ds/product/loader.cljs:35 +msgid "loader.tips.08.message" +msgstr "Shift + A(오토 레이아웃)와 같은 편리한 단축키로 워크플로우를 가속화하세요." + +#: src/app/main/ui/ds/product/loader.cljs:34 +msgid "loader.tips.08.title" +msgstr "키보드 단축키" + +#: src/app/main/ui/ds/product/loader.cljs:37 +msgid "loader.tips.09.message" +msgstr "내 스타일에 맞는 테마를 선택하세요." + +#: src/app/main/ui/ds/product/loader.cljs:36 +msgid "loader.tips.09.title" +msgstr "다크 및 라이트 모드" + +#: src/app/main/ui/ds/product/loader.cljs:39 +msgid "loader.tips.10.message" +msgstr "커뮤니티에서 제작한 플러그인으로 Penpot의 기능을 확장해보세요." + +#: src/app/main/ui/ds/product/loader.cljs:38 +msgid "loader.tips.10.title" +msgstr "플러그인 지원" + +#: src/app/main/ui/workspace/colorpicker.cljs:484, src/app/main/ui/workspace/colorpicker.cljs:485, src/app/main/ui/workspace/colorpicker.cljs:487 +msgid "media.choose-image" +msgstr "이미지 선택" + +#: src/app/main/ui/workspace/colorpicker.cljs:257 +msgid "media.gradient" +msgstr "그라데이션" + +#: src/app/main/data/workspace/media.cljs:270, src/app/main/ui/components/color_bullet.cljs:33, src/app/main/ui/components/color_bullet.cljs:46, src/app/main/ui/ds/utilities/swatch.cljs:45, src/app/main/ui/ds/utilities/swatch.cljs:58, src/app/main/ui/inspect/attributes/common.cljs:43, src/app/main/ui/inspect/styles/rows/color_properties_row.cljs:66, src/app/main/ui/workspace/colorpicker.cljs:259, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:402 +msgid "media.image" +msgstr "이미지" + +#: src/app/main/ui/inspect/attributes/common.cljs:53 +msgid "media.image.short" +msgstr "img" + +#: src/app/main/ui/workspace/colorpicker.cljs:477 +msgid "media.keep-aspect-ratio" +msgstr "가로세로 비율 유지" + +#: src/app/main/ui/workspace/colorpicker.cljs:228 +#, unused +msgid "media.linear" +msgstr "선형" + +#: src/app/main/ui/workspace/colorpicker.cljs:229 +#, unused +msgid "media.radial" +msgstr "방사형" + +#: src/app/main/ui/workspace/colorpicker.cljs:255 +msgid "media.solid" +msgstr "단색" + +#: src/app/main/data/common.cljs:118 +msgid "modals.add-shared-confirm-empty.hint" +msgstr "" +"라이브러리가 비어 있습니다. 공유 라이브러리로 추가하면, 앞으로 생성할 " +"에셋들을 다른 파일에서도 사용할 수 있게 됩니다. 정말 게시하시겠습니까?" + +#: src/app/main/data/common.cljs:118 +msgid "modals.add-shared-confirm.hint" +msgstr "" +"공유 라이브러리로 추가하면, 이 파일 라이브러리의 에셋들을 다른 파일에서도 " +"사용할 수 있게 됩니다." + +#: src/app/main/ui/workspace/nudge.cljs:59 +msgid "modals.big-nudge" +msgstr "크게 이동" + +#: src/app/main/ui/settings/change_email.cljs:97 +msgid "modals.change-email.info" +msgstr "본인 확인을 위해 현재 이메일인 \"%s\"로 이메일을 보내드립니다." + +#: src/app/main/ui/settings/access_tokens.cljs:152, src/app/main/ui/settings/access_tokens.cljs:158 +msgid "modals.create-access-token.copy-token" +msgstr "토큰 복사" + +#: src/app/main/ui/settings/access_tokens.cljs:130 +msgid "modals.create-access-token.expiration-date.label" +msgstr "만료일" + +#: src/app/main/ui/settings/access_tokens.cljs:124 +msgid "modals.create-access-token.name.label" +msgstr "이름" + +#: src/app/main/ui/settings/access_tokens.cljs:126 +msgid "modals.create-access-token.name.placeholder" +msgstr "토큰의 용도를 알 수 있는 이름을 입력하세요:" + +#: src/app/main/ui/settings/access_tokens.cljs:178 +msgid "modals.create-access-token.submit-label" +msgstr "토큰 생성" + +#: src/app/main/ui/settings/access_tokens.cljs:111 +msgid "modals.create-access-token.title" +msgstr "액세스 토큰 생성" + +#: src/app/main/ui/dashboard/team.cljs:1103 +msgid "modals.create-webhook.url.label" +msgstr "페이로드(Payload) URL" + +#: src/app/main/ui/dashboard/team.cljs:1104 +msgid "modals.create-webhook.url.placeholder" +msgstr "https://example.com/postreceive" + +#: src/app/main/ui/settings/access_tokens.cljs:257 +msgid "modals.delete-acces-token.accept" +msgstr "토큰 삭제" + +#: src/app/main/ui/settings/access_tokens.cljs:256 +msgid "modals.delete-acces-token.message" +msgstr "정말 이 토큰을 삭제하시겠습니까?" + +#: src/app/main/ui/settings/access_tokens.cljs:255 +msgid "modals.delete-acces-token.title" +msgstr "토큰 삭제" + +#: src/app/main/ui/settings/delete_account.cljs:56 +msgid "modals.delete-account.cancel" +msgstr "취소하고 계정 유지" + +#: src/app/main/ui/settings/delete_account.cljs:61 +msgid "modals.delete-account.confirm" +msgstr "네, 계정을 삭제합니다" + +#: src/app/main/ui/settings/delete_account.cljs:50 +msgid "modals.delete-account.info" +msgstr "계정을 삭제하면 현재의 모든 프로젝트와 아카이브를 잃게 됩니다." + +#: src/app/main/ui/settings/delete_account.cljs:43 +msgid "modals.delete-account.title" +msgstr "정말 계정을 삭제하시겠습니까?" + +#: src/app/main/ui/comments.cljs:888 +msgid "modals.delete-comment-thread.message" +msgstr "정말 이 대화를 삭제하시겠습니까? 이 스레드의 모든 댓글이 삭제됩니다." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:156 +msgid "modals.delete-component-annotation.message" +msgstr "이 주석을 삭제하시겠습니까?" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:155 +msgid "modals.delete-component-annotation.title" +msgstr "주석 삭제" + +#: src/app/main/ui/dashboard/file_menu.cljs:118 +msgid "modals.delete-file-multi-confirm.message" +msgstr "%s개의 파일을 삭제하시겠습니까?" + +#: src/app/main/ui/dashboard/file_menu.cljs:117 +msgid "modals.delete-file-multi-confirm.title" +msgstr "%s개 파일 삭제 중" + +#: src/app/main/ui/dashboard/fonts.cljs:356 +msgid "modals.delete-font-variant.message" +msgstr "" +"정말 이 글꼴 스타일을 삭제하시겠습니까? 파일에서 사용 중인 경우 로드되지 " +"않습니다." + +#: src/app/main/ui/dashboard/fonts.cljs:342 +msgid "modals.delete-font.message" +msgstr "이 글꼴을 삭제하시겠습니까? 파일에서 사용 중인 경우 로드되지 않습니다." + +#: src/app/main/ui/delete_shared.cljs:54 +msgid "modals.delete-shared-confirm.accept" +msgid_plural "modals.delete-shared-confirm.accept" +msgstr[0] "파일 삭제" + +#: src/app/main/ui/delete_shared.cljs:58 +msgid "modals.delete-shared-confirm.activated.no-files-message" +msgid_plural "modals.delete-shared-confirm.activated.no-files-message" +msgstr[0] "어떤 파일에서도 활성화되어 있지 않습니다." + +#: src/app/main/ui/delete_shared.cljs:60 +msgid "modals.delete-shared-confirm.activated.scd-message" +msgid_plural "modals.delete-shared-confirm.activated.scd-message" +msgstr[0] "다음 위치에서 이 라이브러리가 활성화되어 있습니다: " + +#: src/app/main/ui/delete_shared.cljs:49 +msgid "modals.delete-shared-confirm.message" +msgid_plural "modals.delete-shared-confirm.message" +msgstr[0] "이 파일들을 삭제하시겠습니까?" + +#: src/app/main/ui/delete_shared.cljs:44 +msgid "modals.delete-shared-confirm.title" +msgid_plural "modals.delete-shared-confirm.title" +msgstr[0] "파일 삭제 중" + +#: src/app/main/ui/dashboard/sidebar.cljs:443 +msgid "modals.delete-team-confirm.accept" +msgstr "팀 삭제" + +#: src/app/main/ui/dashboard/sidebar.cljs:442 +msgid "modals.delete-team-confirm.message" +msgstr "" +"이 팀을 삭제하시겠습니까? 팀과 관련된 모든 프로젝트와 파일이 영구적으로 " +"삭제됩니다." + +#: src/app/main/ui/dashboard/sidebar.cljs:441 +msgid "modals.delete-team-confirm.title" +msgstr "팀 삭제 중" + +#: src/app/main/ui/dashboard/team.cljs:461 +msgid "modals.delete-team-member-confirm.accept" +msgstr "구성원 삭제" + +#: src/app/main/ui/dashboard/team.cljs:460 +msgid "modals.delete-team-member-confirm.message" +msgstr "이 구성원을 팀에서 삭제하시겠습니까?" + +#: src/app/main/ui/dashboard/team.cljs:459 +msgid "modals.delete-team-member-confirm.title" +msgstr "팀 구성원 삭제" + +#: src/app/main/ui/delete_shared.cljs:62 +msgid "modals.delete-unpublish-shared-confirm.activated.hint" +msgid_plural "modals.delete-unpublish-shared-confirm.activated.hint" +msgstr[0] "" +"해당 파일에서 이미 사용된 에셋은 그대로 유지되며, 디자인에는 문제가 발생하지 " +"않습니다." + +#: src/app/main/ui/dashboard/team.cljs:1197 +msgid "modals.delete-webhook.accept" +msgstr "웹훅 삭제" + +#: src/app/main/ui/dashboard/team.cljs:1196 +msgid "modals.delete-webhook.message" +msgstr "이 웹훅을 삭제하시겠습니까?" + +#: src/app/main/ui/dashboard/team.cljs:1195 +msgid "modals.delete-webhook.title" +msgstr "웹훅 삭제 중" + +#: src/app/main/ui/dashboard/team.cljs:1126 +msgid "modals.edit-webhook.submit-label" +msgstr "웹훅 수정" + +#: src/app/main/ui/dashboard/team.cljs:1091 +msgid "modals.edit-webhook.title" +msgstr "웹훅 수정" + +#: src/app/main/ui/dashboard/team.cljs:249 +msgid "modals.invite-member-confirm.accept" +msgstr "초대 전송" + +#: src/app/main/ui/dashboard/team.cljs:245, src/app/main/ui/onboarding/team_choice.cljs:203 +msgid "modals.invite-member.emails" +msgstr "이메일 주소 (쉼표로 구분)" + +#: src/app/main/ui/dashboard/team.cljs:229 +msgid "modals.invite-member.repeated-invitation" +msgstr "일부 구성원은 이미 팀에 소속되어 있습니다. 나머지 구성원들만 초대합니다." + +#: src/app/main/ui/dashboard/team.cljs:222 +msgid "modals.invite-team-member.text" +msgstr "" +"팀에 구성원을 초대하여 이 파일과 모든 팀 파일에 접근할 수 있도록 할 수 " +"있습니다." + +#: src/app/main/ui/dashboard/team.cljs:218 +msgid "modals.invite-team-member.title" +msgstr "팀에 구성원 초대" + +#: src/app/main/ui/dashboard/sidebar.cljs:431, src/app/main/ui/dashboard/team.cljs:427 +msgid "modals.leave-and-close-confirm.hint" +msgstr "" +"귀하가 이 팀의 유일한 구성원이므로, 팀을 나가면 프로젝트 및 파일과 함께 팀이 " +"삭제됩니다." + +#: src/app/main/ui/dashboard/sidebar.cljs:430, src/app/main/ui/dashboard/team.cljs:426 +msgid "modals.leave-and-close-confirm.message" +msgstr "정말 %s 팀을 나가시겠습니까?" + +#: src/app/main/ui/dashboard/change_owner.cljs:54 +msgid "modals.leave-and-reassign.forbidden" +msgstr "" +"소유자(Owner) 역할을 위임할 다른 구성원이 없으면 팀을 나갈 수 없습니다. 팀 " +"삭제를 고려해보세요." + +#: src/app/main/ui/dashboard/change_owner.cljs:50 +msgid "modals.leave-and-reassign.hint1" +msgstr "" +"귀하는 이 팀의 소유자입니다. 나가기 전에 소유자 역할을 위임할 다른 구성원을 " +"선택해주세요." + +#: src/app/main/ui/dashboard/change_owner.cljs:73 +msgid "modals.leave-and-reassign.promote-and-leave" +msgstr "위임하고 나가기" + +#: src/app/main/ui/dashboard/change_owner.cljs:30 +msgid "modals.leave-and-reassign.select-member-to-promote" +msgstr "위임할 구성원 선택" + +#: src/app/main/ui/dashboard/change_owner.cljs:44 +msgid "modals.leave-and-reassign.title" +msgstr "팀을 나가기 전에" + +#: src/app/main/ui/dashboard/sidebar.cljs:410, src/app/main/ui/dashboard/sidebar.cljs:432, src/app/main/ui/dashboard/team.cljs:428, src/app/main/ui/dashboard/team.cljs:450 +msgid "modals.leave-confirm.accept" +msgstr "팀 나가기" + +#: src/app/main/ui/dashboard/sidebar.cljs:409, src/app/main/ui/dashboard/team.cljs:449 +msgid "modals.leave-confirm.message" +msgstr "이 팀을 나가시겠습니까?" + +#: src/app/main/ui/dashboard/sidebar.cljs:408, src/app/main/ui/dashboard/sidebar.cljs:429, src/app/main/ui/dashboard/team.cljs:425, src/app/main/ui/dashboard/team.cljs:448 +msgid "modals.leave-confirm.title" +msgstr "팀 나가는 중" + +#: src/app/main/ui/delete_shared.cljs:56 +msgid "modals.move-shared-confirm.accept" +msgid_plural "modals.move-shared-confirm.accept" +msgstr[0] "이동" + +#: src/app/main/ui/delete_shared.cljs:51 +msgid "modals.move-shared-confirm.message" +msgid_plural "modals.move-shared-confirm.message" +msgstr[0] "이 라이브러리를 이동하시겠습니까?" + +#: src/app/main/ui/delete_shared.cljs:46 +msgid "modals.move-shared-confirm.title" +msgid_plural "modals.move-shared-confirm.title" +msgstr[0] "라이브러리 이동" + +#: src/app/main/ui/workspace/main_menu.cljs:302, src/app/main/ui/workspace/nudge.cljs:46 +msgid "modals.nudge-title" +msgstr "이동 간격" + +#: src/app/main/ui/dashboard/team.cljs:380 +msgid "modals.promote-owner-confirm.accept" +msgstr "소유권 이전" + +#: src/app/main/ui/dashboard/team.cljs:379 +msgid "modals.promote-owner-confirm.hint" +msgstr "" +"소유권을 이전하면 귀하의 역할이 관리자(Admin)로 변경되며, 이 팀에 대한 일부 " +"권한을 잃게 됩니다. " + +#: src/app/main/ui/dashboard/team.cljs:378 +msgid "modals.promote-owner-confirm.message" +msgstr "" +"귀하는 현재 이 팀의 소유자입니다. 정말 %s님을 팀의 새로운 소유자로 " +"지정하시겠습니까?" + +#: src/app/main/ui/dashboard/team.cljs:377 +msgid "modals.promote-owner-confirm.title" +msgstr "새로운 팀 소유자" + +#: src/app/main/ui/workspace/libraries.cljs:286 +msgid "modals.publish-empty-library.accept" +msgstr "게시" + +#: src/app/main/ui/workspace/libraries.cljs:285 +msgid "modals.publish-empty-library.message" +msgstr "라이브러리가 비어 있습니다. 정말 게시하시겠습니까?" + +#: src/app/main/ui/workspace/libraries.cljs:284 +msgid "modals.publish-empty-library.title" +msgstr "빈 라이브러리 게시" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#, unused +msgid "modals.remove-shared-confirm.accept" +msgstr "공유 라이브러리에서 제거" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#, unused +msgid "modals.remove-shared-confirm.hint" +msgstr "" +"공유 라이브러리에서 제거하면, 이 파일의 라이브러리를 다른 파일에서 더 이상 " +"사용할 수 없게 됩니다." + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +#, unused +msgid "modals.remove-shared-confirm.message" +msgstr "\"%s\"를 공유 라이브러리에서 제거" + +#: src/app/main/ui/workspace/nudge.cljs:52 +msgid "modals.small-nudge" +msgstr "미세 이동" + +#: src/app/main/ui/delete_shared.cljs:55 +msgid "modals.unpublish-shared-confirm.accept" +msgid_plural "modals.unpublish-shared-confirm.accept" +msgstr[0] "게시 취소" + +#: src/app/main/ui/delete_shared.cljs:50 +msgid "modals.unpublish-shared-confirm.message" +msgid_plural "modals.unpublish-shared-confirm.message" +msgstr[0] "이 라이브러리의 게시를 취소하시겠습니까?" + +#: src/app/main/ui/delete_shared.cljs:45 +msgid "modals.unpublish-shared-confirm.title" +msgid_plural "modals.unpublish-shared-confirm.title" +msgstr[0] "라이브러리 게시 취소" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#, unused +msgid "modals.update-remote-component-in-bulk.hint" +msgstr "" +"공유 라이브러리의 컴포넌트를 업데이트하려고 합니다. 이를 사용하는 다른 " +"파일들에 영향을 줄 수 있습니다." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#, unused +msgid "modals.update-remote-component-in-bulk.message" +msgstr "공유 라이브러리의 컴포넌트 일괄 업데이트" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:417 +msgid "modals.update-remote-component.accept" +msgstr "업데이트" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:416 +msgid "modals.update-remote-component.cancel" +msgstr "취소" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:415 +msgid "modals.update-remote-component.hint" +msgstr "" +"공유 라이브러리의 컴포넌트를 업데이트하려고 합니다. 이를 사용하는 다른 " +"파일들에 영향을 줄 수 있습니다." + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:414 +msgid "modals.update-remote-component.message" +msgstr "공유 라이브러리의 컴포넌트 업데이트" + +#: src/app/main/ui/static.cljs:288 +msgid "not-found.desc-message.doesnt-exist" +msgstr "페이지가 존재하지 않습니다" + +#: src/app/main/ui/static.cljs:287 +msgid "not-found.desc-message.error" +msgstr "404 오류" + +#: src/app/main/ui/static.cljs:138 +msgid "not-found.login.free" +msgstr "Penpot은 디자인과 코드 사이의 협업을 위한 무료 오픈소스 디자인 툴입니다" + +#: src/app/main/ui/auth/recovery_request.cljs:114 +msgid "not-found.login.sent-recovery" +msgstr "다음을 통해 복구 이메일을 보냈습니다:" + +#: src/app/main/ui/auth/recovery_request.cljs:116 +msgid "not-found.login.sent-recovery-check" +msgstr "이메일을 확인하고 링크를 클릭하여 새 비밀번호를 생성하세요." + +#: src/app/main/ui/static.cljs:152 +msgid "not-found.login.signup-free" +msgstr "무료 회원가입" + +#: src/app/main/ui/static.cljs:153 +msgid "not-found.login.start-using" +msgstr "몇 초 안에 Penpot 사용을 시작해보세요!" + +#: src/app/main/ui/static.cljs:69 +msgid "not-found.made-with-love" +msgstr "사랑과 오픈 소스의 힘으로 만들어졌습니다" + +#: src/app/main/ui/static.cljs:248 +msgid "not-found.no-permission.already-requested.file" +msgstr "이미 이 파일에 대한 접근 권한을 요청하셨습니다." + +#: src/app/main/ui/static.cljs:249 +msgid "not-found.no-permission.already-requested.or-others.file" +msgstr "" +"이미 이 파일 또는 이 팀의 다른 파일/프로젝트에 대한 접근 권한을 " +"요청하셨습니다." + +#: src/app/main/ui/static.cljs:255 +msgid "not-found.no-permission.already-requested.or-others.project" +msgstr "" +"이미 이 프로젝트 또는 이 팀의 다른 프로젝트/파일에 대한 접근 권한을 " +"요청하셨습니다." + +#: src/app/main/ui/static.cljs:254 +msgid "not-found.no-permission.already-requested.project" +msgstr "이미 이 프로젝트에 대한 접근 권한을 요청하셨습니다." + +#: src/app/main/ui/static.cljs:269, src/app/main/ui/static.cljs:278 +msgid "not-found.no-permission.ask" +msgstr "접근 권한 요청" + +#: src/app/main/ui/static.cljs:261 +msgid "not-found.no-permission.done.remember" +msgstr "소유자가 승인하면 팀에 초대될 것임을 잊지 마세요." + +#: src/app/main/ui/static.cljs:260 +msgid "not-found.no-permission.done.success" +msgstr "요청이 성공적으로 전송되었습니다!" + +#: src/app/main/ui/static.cljs:266 +msgid "not-found.no-permission.file" +msgstr "이 파일에 대한 접근 권한이 없습니다." + +#: src/app/main/ui/static.cljs:56, src/app/main/ui/static.cljs:244, src/app/main/ui/static.cljs:250, src/app/main/ui/static.cljs:256, src/app/main/ui/static.cljs:262, src/app/main/ui/static.cljs:271, src/app/main/ui/static.cljs:280 +msgid "not-found.no-permission.go-dashboard" +msgstr "내 Penpot으로 이동" + +#: src/app/main/ui/static.cljs:268, src/app/main/ui/static.cljs:277 +msgid "not-found.no-permission.if-approves" +msgstr "소유자가 승인하면 팀에 초대됩니다." + +#: src/app/main/ui/static.cljs:496, src/app/main/ui/static.cljs:509 +msgid "not-found.no-permission.penpot-file" +msgstr "Penpot 파일" + +#: src/app/main/ui/static.cljs:243, src/app/main/ui/static.cljs:275 +msgid "not-found.no-permission.project" +msgstr "이 프로젝트에 대한 접근 권한이 없습니다." + +#: src/app/main/ui/static.cljs:495, src/app/main/ui/static.cljs:507 +msgid "not-found.no-permission.project-name" +msgstr "프로젝트" + +#: src/app/main/ui/static.cljs:267 +msgid "not-found.no-permission.you-can-ask.file" +msgstr "이 파일에 접근하려면 팀 소유자에게 문의하세요." + +#: src/app/main/ui/static.cljs:276 +msgid "not-found.no-permission.you-can-ask.project" +msgstr "이 프로젝트에 접근하려면 팀 소유자에게 문의하세요." + +#: src/app/main/data/common.cljs:89 +msgid "notifications.by-code.maintenance" +msgstr "시스템 점검 안내: 5분 이내에 짧은 점검을 위해 서비스가 중단될 예정입니다." + +#: src/app/main/data/common.cljs:82 +msgid "notifications.by-code.upgrade-version" +msgstr "새 버전이 출시되었습니다. 페이지를 새로고침해주세요" + +#: src/app/main/ui/dashboard/team.cljs:825 +msgid "notifications.invitation-deleted" +msgstr "초대가 성공적으로 삭제되었습니다" + +#: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 +msgid "notifications.invitation-email-sent" +msgstr "초대장이 성공적으로 전송되었습니다" + +#: src/app/main/ui/dashboard/team.cljs:635 +msgid "notifications.invitation-link-copied" +msgstr "초대 링크가 복사되었습니다" + +#: src/app/main/ui/settings/delete_account.cljs:24 +msgid "notifications.profile-deletion-not-allowed" +msgstr "프로필을 삭제할 수 없습니다. 진행하기 전에 팀 소유권을 먼저 위임해주세요." + +#: src/app/main/ui/settings/change_email.cljs:46 +msgid "notifications.validation-email-sent" +msgstr "인증 이메일을 %s로 보냈습니다. 이메일을 확인해주세요!" + +#, unused +msgid "onboarding-v2.before-start.desc1" +msgstr "" +"사용자 가이드와 YouTube 채널 등 Penpot 시작을 돕는 다양한 리소스가 준비되어 " +"있습니다." + +#, unused +msgid "onboarding-v2.before-start.desc2" +msgstr "" +"프로토타이핑부터 디자인 정리 및 공유까지, Penpot 사용법에 대한 자세한 " +"정보입니다." + +#, unused +msgid "onboarding-v2.before-start.desc2.title" +msgstr "사용자 가이드" + +#, unused +msgid "onboarding-v2.before-start.desc3" +msgstr "공식 튜토리얼 및 커뮤니티에서 제작한 튜토리얼 영상을 시청하실 수 있습니다." + +#, unused +msgid "onboarding-v2.before-start.desc3.title" +msgstr "비디오 튜토리얼" + +#, unused +msgid "onboarding-v2.before-start.title" +msgstr "시작하기 전에" + +#: src/app/main/ui/onboarding/newsletter.cljs:68 +msgid "onboarding-v2.newsletter.desc" +msgstr "Penpot 뉴스레터를 구독하고 제품 개발 현황 및 소식을 받아보세요." + +#: src/app/main/ui/onboarding/newsletter.cljs:88 +msgid "onboarding-v2.newsletter.news" +msgstr "Penpot 관련 소식 받기 (블로그 포스트, 비디오 튜토리얼, 스트리밍 등)." + +#: src/app/main/ui/onboarding/newsletter.cljs:96 +msgid "onboarding-v2.newsletter.privacy1" +msgstr "우리는 개인정보를 소중히 여깁니다. 다음을 확인해주세요: " + +#: src/app/main/ui/onboarding/newsletter.cljs:102 +msgid "onboarding-v2.newsletter.privacy2" +msgstr "" +"꼭 필요한 정보만 보내드립니다. 뉴스레터 하단의 수신 거부 링크를 통해 " +"언제든지 구독을 취소하실 수 있습니다." + +#: src/app/main/ui/auth/register.cljs:35, src/app/main/ui/onboarding/newsletter.cljs:76 +msgid "onboarding-v2.newsletter.updates" +msgstr ".제품 업데이트 소식 받기 (새로운 기능, 릴리즈, 수정 사항 등)." + +#, unused +msgid "onboarding-v2.welcome.desc1" +msgstr "" +"Penpot은 오픈소스이며 Kaleidos와 커뮤니티가 함께 만들어갑니다. 이미 많은 " +"분들이 서로 돕고 있으며, 누구나 다음과 같이 참여할 수 있습니다:" + +#, unused +msgid "onboarding-v2.welcome.desc2" +msgstr "" +"전체 커뮤니티 및 Penpot 핵심 팀과 함께 Penpot의 현재와 미래에 대해 배우고, " +"공유하고, 토론하는 공개 공간입니다." + +#, unused +msgid "onboarding-v2.welcome.desc2.title" +msgstr "커뮤니티 참여" + +#, unused +msgid "onboarding-v2.welcome.desc3" +msgstr "번역, 기능 제안, 코어 기여, 버그 찾기 등 협업 방법을 확인하실 수 있습니다." + +#, unused +msgid "onboarding-v2.welcome.desc3.title" +msgstr "기여 가이드" + +#: src/app/main/ui/onboarding/team_choice.cljs:241 +msgid "onboarding-v2.welcome.title" +msgstr "Penpot에 오신 것을 환영합니다!" + +#: src/app/main/ui/onboarding/team_choice.cljs:254 +#, unused +msgid "onboarding.choice.team-up.continue-creating-team" +msgstr "팀 생성 계속하기" + +#: src/app/main/ui/onboarding/team_choice.cljs:230 +msgid "onboarding.choice.team-up.continue-without-a-team" +msgstr "팀 없이 계속하기" + +#: src/app/main/ui/onboarding/team_choice.cljs:214 +msgid "onboarding.choice.team-up.create-team-and-invite" +msgstr "팀 생성 및 초대" + +#, unused +msgid "onboarding.choice.team-up.create-team-and-send-invites" +msgstr "팀 생성 및 초대장 전송" + +#: src/app/main/ui/onboarding/team_choice.cljs:219 +msgid "onboarding.choice.team-up.create-team-and-send-invites-description" +msgstr "나중에 초대할 수도 있습니다" + +#: src/app/main/ui/onboarding/team_choice.cljs:177 +msgid "onboarding.choice.team-up.create-team-desc" +msgstr "팀 이름을 정한 후, 구성원들을 초대할 수 있습니다." + +#: src/app/main/ui/onboarding/team_choice.cljs:185 +msgid "onboarding.choice.team-up.create-team-placeholder" +msgstr "팀 이름 입력" + +#: src/app/main/ui/onboarding/team_choice.cljs:215 +msgid "onboarding.choice.team-up.create-team-without-invite" +msgstr "팀 생성" + +#, unused +msgid "onboarding.choice.team-up.create-team-without-inviting" +msgstr "초대 없이 팀 생성" + +#: src/app/main/ui/dashboard/projects.cljs:97, src/app/main/ui/onboarding/team_choice.cljs:187 +msgid "onboarding.choice.team-up.invite-members" +msgstr "구성원 초대" + +#: src/app/main/ui/onboarding/team_choice.cljs:188 +msgid "onboarding.choice.team-up.invite-members-info" +msgstr "" +"모든 분들을 포함하는 것을 잊지 마세요. 개발자, 디자이너, 매니저... 다양성이 " +"힘이 됩니다 :)" + +#: src/app/main/ui/dashboard/team.cljs:234, src/app/main/ui/onboarding/team_choice.cljs:194 +msgid "onboarding.choice.team-up.roles" +msgstr "다음 역할로 초대:" + +#: src/app/main/ui/onboarding/team_choice.cljs:223 +msgid "onboarding.choice.team-up.start-without-a-team" +msgstr "팀 없이 시작하기" + +#: src/app/main/ui/onboarding/team_choice.cljs:225 +msgid "onboarding.choice.team-up.start-without-a-team-description" +msgstr "나중에 언제든지 팀을 생성할 수 있습니다." + +#, unused +msgid "onboarding.newsletter.accept" +msgstr "네, 구독합니다" + +#: src/app/main/ui/onboarding/newsletter.cljs:42 +msgid "onboarding.newsletter.acceptance-message" +msgstr "구독 요청이 전송되었습니다. 확인 메일을 보내드릴게요." + +#: src/app/main/ui/onboarding/newsletter.cljs:100 +msgid "onboarding.newsletter.policy" +msgstr "개인정보 처리방침." + +#: src/app/main/ui/onboarding/newsletter.cljs:65 +msgid "onboarding.newsletter.title" +msgstr "Penpot 소식을 받아보시겠어요?" + +#: src/app/main/ui/onboarding/questions.cljs:108 +msgid "onboarding.questions.lets-get-started" +msgstr "시작해볼까요!" + +#: src/app/main/ui/onboarding/questions.cljs:249 +msgid "onboarding.questions.reasons.alternative" +msgstr "Figma, XD 등의 대안을 찾고 있음" + +#: src/app/main/ui/onboarding/questions.cljs:243 +msgid "onboarding.questions.reasons.exploring" +msgstr "그냥 둘러보는 중" + +#: src/app/main/ui/onboarding/questions.cljs:246 +msgid "onboarding.questions.reasons.fit" +msgstr "Penpot이 우리 팀에 적합한지 검토 중" + +#: src/app/main/ui/onboarding/questions.cljs:252 +msgid "onboarding.questions.reasons.testing" +msgstr "셀프 호스팅 전 테스트 중" + +#: src/app/main/ui/onboarding/questions.cljs:407 +#, unused +msgid "onboarding.questions.referer.article" +msgstr "기사 (블로그, 포스트, 뉴스레터)" + +#: src/app/main/ui/onboarding/questions.cljs:405 +#, unused +msgid "onboarding.questions.referer.search" +msgstr "검색 엔진 (Google, Yahoo, Bing 등)" + +#: src/app/main/ui/onboarding/questions.cljs:406 +#, unused +msgid "onboarding.questions.referer.social" +msgstr "소셜 미디어 (X, LinkedIn, FB 등)" + +#: src/app/main/ui/onboarding/questions.cljs:322 +msgid "onboarding.questions.start-with.code" +msgstr "디자인에서 실제 코드 추출" + +#: src/app/main/ui/onboarding/questions.cljs:320 +msgid "onboarding.questions.start-with.ds" +msgstr "디자인 시스템 구축" + +#: src/app/main/ui/onboarding/questions.cljs:318 +msgid "onboarding.questions.start-with.prototyping" +msgstr "프로토타이핑" + +#: src/app/main/ui/onboarding/questions.cljs:314 +msgid "onboarding.questions.start-with.ui" +msgstr "앱의 UI/UX 디자인" + +#: src/app/main/ui/onboarding/questions.cljs:316 +msgid "onboarding.questions.start-with.wireframing" +msgstr "와이어프레임 제작" + +#: src/app/main/ui/onboarding/questions.cljs:116 +msgid "onboarding.questions.step1.question1" +msgstr "어떤 용도로 Penpot을 사용하실 예정인가요?" + +#: src/app/main/ui/onboarding/questions.cljs:273 +msgid "onboarding.questions.step1.question2" +msgstr "오늘 Penpot을 방문하신 이유는 무엇인가요?" + +#: src/app/main/ui/onboarding/questions.cljs:112 +msgid "onboarding.questions.step1.subtitle" +msgstr "" +"Penpot이 사용자분께 더 잘 맞도록 도와드리기 위해 정보를 조금만 알려주세요. " +"답변해주신 내용은 새로운 기능의 우선순위를 정하고 시작 가이드를 제공하는 데 " +"도움이 됩니다." + +#: src/app/main/ui/onboarding/questions.cljs:110 +msgid "onboarding.questions.step1.title" +msgstr "사용자분에 대해 알려주세요" + +#: src/app/main/ui/onboarding/questions.cljs:190 +msgid "onboarding.questions.step2.title" +msgstr "가장 많이 사용하는 디자인 도구는 무엇인가요?" + +#: src/app/main/ui/onboarding/questions.cljs:122 +msgid "onboarding.questions.step3.question1" +msgstr "주로 어떤 작업을 하시나요?" + +#: src/app/main/ui/onboarding/questions.cljs:303 +#, unused +msgid "onboarding.questions.step3.question2" +msgstr "사용자분의 역할은 무엇인가요?" + +#: src/app/main/ui/onboarding/questions.cljs:290 +msgid "onboarding.questions.step3.question3" +msgstr "회사의 규모는 어느 정도인가요?" + +#: src/app/main/ui/onboarding/questions.cljs:270 +msgid "onboarding.questions.step3.title" +msgstr "사용자분의 업무에 대해 알려주세요" + +#: src/app/main/ui/onboarding/questions.cljs:345 +msgid "onboarding.questions.step4.title" +msgstr "어디서부터 시작하고 싶으신가요?" + +#: src/app/main/ui/onboarding/questions.cljs:428 +#, unused +msgid "onboarding.questions.step5.title" +msgstr "어떻게 Penpot을 알게 되셨나요?" + +#: src/app/main/ui/onboarding/questions.cljs:233 +msgid "onboarding.questions.team-size.11-30" +msgstr "11-30명" + +#: src/app/main/ui/onboarding/questions.cljs:234 +msgid "onboarding.questions.team-size.2-10" +msgstr "2-10명" + +#: src/app/main/ui/onboarding/questions.cljs:232 +msgid "onboarding.questions.team-size.31-50" +msgstr "31-50명" + +#: src/app/main/ui/onboarding/questions.cljs:235 +msgid "onboarding.questions.team-size.freelancer" +msgstr "프리랜서입니다" + +#: src/app/main/ui/onboarding/questions.cljs:231 +msgid "onboarding.questions.team-size.more-than-50" +msgstr "50명 이상" + +#: src/app/main/ui/onboarding/questions.cljs:236 +msgid "onboarding.questions.team-size.personal-project" +msgstr "개인 프로젝트 진행 중입니다" + +#: src/app/main/ui/onboarding/questions.cljs:79 +msgid "onboarding.questions.use.education" +msgstr "교육용" + +#: src/app/main/ui/onboarding/questions.cljs:80 +msgid "onboarding.questions.use.personal" +msgstr "개인용" + +#: src/app/main/ui/onboarding/questions.cljs:78 +msgid "onboarding.questions.use.work" +msgstr "업무용" + +#: src/app/main/ui/onboarding/team_choice.cljs:175 +msgid "onboarding.team-modal.create-team" +msgstr "팀 생성" + +#: src/app/main/ui/onboarding/team_choice.cljs:31 +msgid "onboarding.team-modal.create-team-desc" +msgstr "" +"팀을 구성하면 동일한 파일과 프로젝트에서 다른 Penpot 사용자와 협업할 수 " +"있습니다." + +#: src/app/main/ui/onboarding/team_choice.cljs:36 +msgid "onboarding.team-modal.create-team-feature-1" +msgstr "무제한 파일 및 프로젝트" + +#: src/app/main/ui/onboarding/team_choice.cljs:40 +msgid "onboarding.team-modal.create-team-feature-2" +msgstr "실시간 동시 편집" + +#: src/app/main/ui/onboarding/team_choice.cljs:44 +msgid "onboarding.team-modal.create-team-feature-3" +msgstr "역할 관리" + +#: src/app/main/ui/onboarding/team_choice.cljs:48 +msgid "onboarding.team-modal.create-team-feature-4" +msgstr "무제한 구성원" + +#: src/app/main/ui/onboarding/team_choice.cljs:52 +msgid "onboarding.team-modal.create-team-feature-5" +msgstr "100% 무료!" + +#: src/app/main/ui/onboarding/team_choice.cljs:29 +msgid "onboarding.team-modal.team-definition" +msgstr "팀이란?" + +#: src/app/main/ui/onboarding/templates.cljs:77 +msgid "onboarding.templates.subtitle" +msgstr "제공되는 템플릿들입니다." + +#: src/app/main/ui/onboarding/templates.cljs:76 +msgid "onboarding.templates.title" +msgstr "디자인 시작하기" + +#, unused +msgid "onboarding.welcome.alt" +msgstr "Penpot" + +#: src/app/main/ui/auth/recovery.cljs:88 +msgid "profile.recovery.go-to-login" +msgstr "로그인으로 이동" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:373 +msgid "settings.detach" +msgstr "해제" + +#: src/app/main/ui/inspect/exports.cljs:148, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:196, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:213, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:215, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:240, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:260, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:278, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:295, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:342, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:496, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1062, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1302, src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:138, src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:149, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs:223, src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:221, src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.cljs:28, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:233, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:385, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:396, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:422, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:432, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:520, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:554, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:587, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:621, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:763, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:801, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:80, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:86, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:424, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:447, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:458, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:486, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:499, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:508, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:519, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:540, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:552, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs:155, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:200, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:336, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:391, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:410, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:422, src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:235 +msgid "settings.multiple" +msgstr "혼합됨" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:431 +msgid "settings.remove-color" +msgstr "색상 제거" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:436 +msgid "settings.select-this-color" +msgstr "이 스타일을 사용하는 항목 선택" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:409 +msgid "shortcut-section.basics" +msgstr "기본" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:415 +msgid "shortcut-section.dashboard" +msgstr "대시보드" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:418 +msgid "shortcut-section.viewer" +msgstr "뷰어" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:412 +msgid "shortcut-section.workspace" +msgstr "워크스페이스" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:58 +msgid "shortcut-subsection.alignment" +msgstr "정렬" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59 +msgid "shortcut-subsection.edit" +msgstr "편집" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 +msgid "shortcut-subsection.general-dashboard" +msgstr "일반" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 +msgid "shortcut-subsection.general-viewer" +msgstr "일반" + +#: src/app/main/ui/workspace/main_menu.cljs:857, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 +msgid "shortcut-subsection.main-menu" +msgstr "메인 메뉴" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:63 +msgid "shortcut-subsection.modify-layers" +msgstr "레이어 수정" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:64 +msgid "shortcut-subsection.navigation-dashboard" +msgstr "탐색" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:65 +msgid "shortcut-subsection.navigation-viewer" +msgstr "탐색" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:66 +msgid "shortcut-subsection.navigation-workspace" +msgstr "탐색" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:67 +msgid "shortcut-subsection.panels" +msgstr "패널" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:68 +msgid "shortcut-subsection.path-editor" +msgstr "패스" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:69 +msgid "shortcut-subsection.shape" +msgstr "도형" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:70 +msgid "shortcut-subsection.text-editor" +msgstr "텍스트" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:71 +msgid "shortcut-subsection.tools" +msgstr "도구" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:72 +msgid "shortcut-subsection.zoom-viewer" +msgstr "확대/축소" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:73 +msgid "shortcut-subsection.zoom-workspace" +msgstr "확대/축소" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:74 +msgid "shortcuts.add-comment" +msgstr "댓글" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:75 +msgid "shortcuts.add-node" +msgstr "노드 추가" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:76 +msgid "shortcuts.align-bottom" +msgstr "아래 정렬" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:77 +msgid "shortcuts.align-center" +msgstr "가운데 정렬" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:78 +msgid "shortcuts.align-hcenter" +msgstr "가로 가운데 정렬" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:79 +msgid "shortcuts.align-justify" +msgstr "양끝 정렬" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:80 +msgid "shortcuts.align-left" +msgstr "왼쪽 정렬" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:81 +msgid "shortcuts.align-right" +msgstr "오른쪽 정렬" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:82 +msgid "shortcuts.align-top" +msgstr "위쪽 정렬" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:83 +msgid "shortcuts.align-vcenter" +msgstr "세로 가운데 정렬" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:84 +msgid "shortcuts.artboard-selection" +msgstr "선택 영역을 보드로 만들기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:85 +msgid "shortcuts.bold" +msgstr "굵게 표시/해제" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:86 +msgid "shortcuts.bool-difference" +msgstr "빼기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:87 +msgid "shortcuts.bool-exclude" +msgstr "제외" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:88 +msgid "shortcuts.bool-intersection" +msgstr "교차" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:89 +msgid "shortcuts.bool-union" +msgstr "합치기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:90 +msgid "shortcuts.bring-back" +msgstr "맨 뒤로 보내기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:91 +msgid "shortcuts.bring-backward" +msgstr "뒤로 보내기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:92 +msgid "shortcuts.bring-forward" +msgstr "앞으로 가져오기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:93 +msgid "shortcuts.bring-front" +msgstr "맨 앞으로 가져오기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:94 +msgid "shortcuts.clear-undo" +msgstr "실행 취소 기록 삭제" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:95 +msgid "shortcuts.copy" +msgstr "복사" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:96 +msgid "shortcuts.copy-link" +msgstr "링크 복사" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:106 +#, unused +msgid "shortcuts.copy-props" +msgstr "속성 복사" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:97 +msgid "shortcuts.create-component-variant" +msgstr "컴포넌트 / 베리언트 생성" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:98 +msgid "shortcuts.create-new-project" +msgstr "새로 만들기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:99 +msgid "shortcuts.cut" +msgstr "잘라내기" + +#: src/app/main/ui/viewer/header.cljs:82, src/app/main/ui/workspace/right_header.cljs:82, src/app/main/ui/workspace/sidebar/shortcuts.cljs:100 +msgid "shortcuts.decrease-zoom" +msgstr "축소" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:101 +msgid "shortcuts.delete" +msgstr "삭제" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:102 +msgid "shortcuts.delete-node" +msgstr "노드 삭제" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:103 +msgid "shortcuts.detach-component" +msgstr "컴포넌트 해제" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:104 +msgid "shortcuts.draw-curve" +msgstr "곡선" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:105 +msgid "shortcuts.draw-ellipse" +msgstr "타원" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:106 +msgid "shortcuts.draw-frame" +msgstr "보드" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:107 +msgid "shortcuts.draw-nodes" +msgstr "패스 그리기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:108 +msgid "shortcuts.draw-path" +msgstr "패스" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:109 +msgid "shortcuts.draw-rect" +msgstr "사각형" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:110 +msgid "shortcuts.draw-text" +msgstr "텍스트" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:111 +msgid "shortcuts.duplicate" +msgstr "복제" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:112 +msgid "shortcuts.escape" +msgstr "취소" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:113 +msgid "shortcuts.export-shapes" +msgstr "도형 내보내기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:114 +msgid "shortcuts.fit-all" +msgstr "전체 맞춤" From aca63802e17a537e391cb84f59a50525285a3ffa Mon Sep 17 00:00:00 2001 From: deveronica Date: Thu, 5 Mar 2026 14:17:48 +0100 Subject: [PATCH 024/288] :globe_with_meridians: Add translations for: Korean Currently translated at 99.8% (2070 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/ --- frontend/translations/ko.po | 4472 ++++++++++++++++++++++++++++++++++- 1 file changed, 4435 insertions(+), 37 deletions(-) diff --git a/frontend/translations/ko.po b/frontend/translations/ko.po index fc087e4a11..12322ed57d 100644 --- a/frontend/translations/ko.po +++ b/frontend/translations/ko.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-03-04 20:10+0000\n" +"PO-Revision-Date: 2026-03-05 21:13+0000\n" "Last-Translator: deveronica \n" "Language-Team: Korean \n" @@ -145,7 +145,6 @@ msgid "auth.register-account-tagline" msgstr "대시보드와 이메일에 표시될 이름을 입력해주세요." #: src/app/main/ui/auth/register.cljs:350 -#, fuzzy msgid "auth.register-account-title" msgstr "당신의 이름" @@ -334,7 +333,7 @@ msgstr "토큰 복사됨" #: src/app/main/ui/settings/access_tokens.cljs:189 msgid "dashboard.access-tokens.create" -msgstr "새로운 토큰 생성" +msgstr "새 토큰 생성" #: src/app/main/ui/settings/access_tokens.cljs:64 msgid "dashboard.access-tokens.create.success" @@ -342,7 +341,7 @@ msgstr "액세스 토큰이 성공적으로 생성되었습니다." #: src/app/main/ui/settings/access_tokens.cljs:286 msgid "dashboard.access-tokens.empty.add-one" -msgstr "\"새로운 토큰 생성\" 버튼을 눌러 토큰을 생성하세요." +msgstr "\"새 토큰 생성\" 버튼을 눌러 토큰을 생성하세요." #: src/app/main/ui/settings/access_tokens.cljs:285 msgid "dashboard.access-tokens.empty.no-access-tokens" @@ -491,7 +490,7 @@ msgstr "아직 라이브러리가 없습니다." #: src/app/main/ui/dashboard/file_menu.cljs:280 msgid "dashboard.export-binary-multi" -msgstr "%s 펜팟 파일 (.penpot) 다운로드" +msgstr "%s Penpot 파일 (.penpot) 다운로드" #: src/app/main/ui/workspace/main_menu.cljs:698 msgid "dashboard.export-frames" @@ -507,7 +506,7 @@ msgstr "내보내기" #: src/app/main/ui/dashboard/sidebar.cljs:858 msgid "dashboard.no-projects-placeholder" -msgstr "고정된 프로젝트가 여기에 표시됩니다." +msgstr "고정된 프로젝트가 여기에 표시됩니다" #: src/app/main/ui/dashboard/deleted.cljs:62, src/app/main/ui/dashboard/projects.cljs:57 msgid "dashboard.projects-title" @@ -527,7 +526,7 @@ msgstr "검색…" #: src/app/main/ui/dashboard/search.cljs:72 msgid "dashboard.searching-for" -msgstr "\"%s\" 찾는 중…" +msgstr "\"%s\" 검색 중…" #: src/app/main/ui/dashboard/team.cljs:1344 msgid "dashboard.team-projects" @@ -665,7 +664,7 @@ msgstr "타이포그래피" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:308 msgid "inspect.attributes.typography.font-family" -msgstr "글꼴 패밀리" +msgstr "글꼴 모음" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:326, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:332 msgid "inspect.attributes.typography.font-size" @@ -722,7 +721,7 @@ msgstr "마스크" #: src/app/main/ui/inspect/right_sidebar.cljs:150 msgid "inspect.tabs.code.selected.path" -msgstr "패스" +msgstr "경로" #: src/app/main/ui/inspect/right_sidebar.cljs:151 msgid "inspect.tabs.code.selected.rect" @@ -867,7 +866,7 @@ msgstr "내보내기" #: src/app/main/ui/dashboard/fonts.cljs:432 msgid "labels.font-family" -msgstr "글꼴 패밀리" +msgstr "글꼴 모음" #, unused msgid "labels.font-providers" @@ -1023,11 +1022,11 @@ msgstr "프로필이 성공적으로 저장되었습니다!" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:115 msgid "shortcuts.flip-horizontal" -msgstr "가로로 뒤집기" +msgstr "좌우 반전" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:116 msgid "shortcuts.flip-vertical" -msgstr "세로로 뒤집기" +msgstr "상하 반전" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:119 msgid "shortcuts.go-to-drafts" @@ -1035,29 +1034,29 @@ msgstr "초안으로 가기" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:120 msgid "shortcuts.go-to-libs" -msgstr "공유된 라이브러리로 가기" +msgstr "공유 라이브러리로 이동" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:121 msgid "shortcuts.go-to-search" -msgstr "찾기" +msgstr "검색" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:122 msgid "shortcuts.group" -msgstr "그룹" +msgstr "그룹화" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:123 msgid "shortcuts.h-distribute" -msgstr "가로로 분배하기" +msgstr "수평 간격 동일하게" #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 msgid "color-token.empty-state" msgstr "" -"사용 가능한 색상 token이 없습니다. 활성 세트/테마를 확인하거나 새로운 " -"token을 추가하세요." +"사용 가능한 색상 token이 없습니다. 활성 세트/테마를 확인하거나 새 token을 " +"추가하세요." #: src/app/main/ui/dashboard/sidebar.cljs:347 msgid "dashboard.create-new-org" -msgstr "새 조직 만들기" +msgstr "새 조직 생성" #: src/app/main/ui/dashboard/grid.cljs:248 msgid "dashboard.deleted.will-be-deleted-at" @@ -1229,9 +1228,8 @@ msgstr[0] "글꼴 %s개 추가됨" msgid "dashboard.fonts.hero-text1" msgstr "" "여기에 업로드하는 모든 웹 글꼴은 이 팀의 파일 텍스트 속성에서 사용할 수 있는 " -"폰트 패밀리 목록에 추가됩니다. 동일한 폰트 패밀리 이름을 가진 " -"폰트들은**하나의 글꼴 패밀리**로 그룹화됩니다. 지원 형식: **TTF, OTF, WOFF** " -"(하나만 필요)." +"글꼴 모음 목록에 추가됩니다. 동일한 글꼴 모음 이름을 가진 글꼴들은**하나의 " +"글꼴 모음**으로 그룹화됩니다. 지원 형식: **TTF, OTF, WOFF** (하나만 필요)." #: src/app/main/ui/dashboard/fonts.cljs:194 #, markdown @@ -1420,7 +1418,7 @@ msgstr "새 프로젝트" #: src/app/main/ui/dashboard/search.cljs:77 msgid "dashboard.no-matches-for" -msgstr "\"%s\"에 대한 검색 결과가 없습니다." +msgstr "\"%s\"에 대한 검색 결과가 없습니다" #: src/app/main/ui/dashboard/comments.cljs:91 msgid "dashboard.notifications" @@ -2665,7 +2663,7 @@ msgstr "다음" #: src/app/main/ui/dashboard/comments.cljs:122, src/app/main/ui/workspace/comments.cljs:162 msgid "labels.no-comments-available" -msgstr "모든 알림을 확인했습니다! 새로운 댓글 알림이 여기에 표시됩니다." +msgstr "모든 알림을 확인했습니다! 새 댓글 알림이 여기에 표시됩니다." #: src/app/main/ui/dashboard/team.cljs:737 msgid "labels.no-invitations" @@ -3442,12 +3440,12 @@ msgstr "" #: src/app/main/ui/dashboard/team.cljs:378 msgid "modals.promote-owner-confirm.message" msgstr "" -"귀하는 현재 이 팀의 소유자입니다. 정말 %s님을 팀의 새로운 소유자로 " +"귀하는 현재 이 팀의 소유자입니다. 정말 %s님을 팀의 새 소유자로 " "지정하시겠습니까?" #: src/app/main/ui/dashboard/team.cljs:377 msgid "modals.promote-owner-confirm.title" -msgstr "새로운 팀 소유자" +msgstr "새 팀 소유자" #: src/app/main/ui/workspace/libraries.cljs:286 msgid "modals.publish-empty-library.accept" @@ -3637,7 +3635,7 @@ msgstr "초대가 성공적으로 삭제되었습니다" #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" -msgstr "초대장이 성공적으로 전송되었습니다" +msgstr "초대가 성공적으로 전송되었습니다" #: src/app/main/ui/dashboard/team.cljs:635 msgid "notifications.invitation-link-copied" @@ -3699,7 +3697,7 @@ msgstr "" #: src/app/main/ui/auth/register.cljs:35, src/app/main/ui/onboarding/newsletter.cljs:76 msgid "onboarding-v2.newsletter.updates" -msgstr ".제품 업데이트 소식 받기 (새로운 기능, 릴리즈, 수정 사항 등)." +msgstr ".제품 업데이트 소식 받기 (새 기능, 릴리즈, 수정 사항 등)." #, unused msgid "onboarding-v2.welcome.desc1" @@ -3744,7 +3742,7 @@ msgstr "팀 생성 및 초대" #, unused msgid "onboarding.choice.team-up.create-team-and-send-invites" -msgstr "팀 생성 및 초대장 전송" +msgstr "팀 생성 및 초대 전송" #: src/app/main/ui/onboarding/team_choice.cljs:219 msgid "onboarding.choice.team-up.create-team-and-send-invites-description" @@ -3871,7 +3869,7 @@ msgstr "오늘 Penpot을 방문하신 이유는 무엇인가요?" msgid "onboarding.questions.step1.subtitle" msgstr "" "Penpot이 사용자분께 더 잘 맞도록 도와드리기 위해 정보를 조금만 알려주세요. " -"답변해주신 내용은 새로운 기능의 우선순위를 정하고 시작 가이드를 제공하는 데 " +"답변해주신 내용은 새 기능의 우선순위를 정하고 시작 가이드를 제공하는 데 " "도움이 됩니다." #: src/app/main/ui/onboarding/questions.cljs:110 @@ -4052,15 +4050,15 @@ msgstr "레이어 수정" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:64 msgid "shortcut-subsection.navigation-dashboard" -msgstr "탐색" +msgstr "내비게이션" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:65 msgid "shortcut-subsection.navigation-viewer" -msgstr "탐색" +msgstr "내비게이션" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:66 msgid "shortcut-subsection.navigation-workspace" -msgstr "탐색" +msgstr "내비게이션" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:67 msgid "shortcut-subsection.panels" @@ -4068,7 +4066,7 @@ msgstr "패널" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:68 msgid "shortcut-subsection.path-editor" -msgstr "패스" +msgstr "경로" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:69 msgid "shortcut-subsection.shape" @@ -4193,7 +4191,7 @@ msgstr "컴포넌트 / 베리언트 생성" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:98 msgid "shortcuts.create-new-project" -msgstr "새로 만들기" +msgstr "새로 생성" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:99 msgid "shortcuts.cut" @@ -4229,11 +4227,11 @@ msgstr "보드" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:107 msgid "shortcuts.draw-nodes" -msgstr "패스 그리기" +msgstr "경로 그리기" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:108 msgid "shortcuts.draw-path" -msgstr "패스" +msgstr "경로" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:109 msgid "shortcuts.draw-rect" @@ -4258,3 +4256,4403 @@ msgstr "도형 내보내기" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:114 msgid "shortcuts.fit-all" msgstr "전체 맞춤" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:117 +msgid "shortcuts.font-size-dec" +msgstr "글꼴 크기 감소" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:118 +msgid "shortcuts.font-size-inc" +msgstr "글꼴 크기 증가" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:124 +msgid "shortcuts.hide-ui" +msgstr "UI 표시/숨기기" + +#: src/app/main/ui/viewer/header.cljs:88, src/app/main/ui/workspace/right_header.cljs:87, src/app/main/ui/workspace/sidebar/shortcuts.cljs:125 +msgid "shortcuts.increase-zoom" +msgstr "확대" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:126 +msgid "shortcuts.insert-image" +msgstr "이미지 삽입" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:127 +msgid "shortcuts.italic" +msgstr "기울임꼴 표시/해제" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:128 +msgid "shortcuts.join-nodes" +msgstr "노드 연결" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:129 +msgid "shortcuts.line-through" +msgstr "취소선 표시/해제" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:130 +msgid "shortcuts.make-corner" +msgstr "직각 노드로 변경" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:131 +msgid "shortcuts.make-curve" +msgstr "곡선 노드로 변경" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:132 +msgid "shortcuts.mask" +msgstr "마스크" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:133 +msgid "shortcuts.merge-nodes" +msgstr "노드 병합" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:134 +msgid "shortcuts.move" +msgstr "이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:135 +msgid "shortcuts.move-fast-down" +msgstr "아래로 크게 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:136 +msgid "shortcuts.move-fast-left" +msgstr "왼쪽으로 크게 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:137 +msgid "shortcuts.move-fast-right" +msgstr "오른쪽으로 크게 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:138 +msgid "shortcuts.move-fast-up" +msgstr "위로 크게 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:139 +msgid "shortcuts.move-nodes" +msgstr "노드 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:140 +msgid "shortcuts.move-unit-down" +msgstr "아래로 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:141 +msgid "shortcuts.move-unit-left" +msgstr "왼쪽으로 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:142 +msgid "shortcuts.move-unit-right" +msgstr "오른쪽으로 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:143 +msgid "shortcuts.move-unit-up" +msgstr "위로 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:144 +msgid "shortcuts.next-frame" +msgstr "다음 보드" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:509 +msgid "shortcuts.not-found" +msgstr "단축키를 찾을 수 없습니다" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:145 +msgid "shortcuts.opacity-0" +msgstr "불투명도 100% 설정" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:146 +msgid "shortcuts.opacity-1" +msgstr "불투명도 10% 설정" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:147 +msgid "shortcuts.opacity-2" +msgstr "불투명도 20% 설정" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:148 +msgid "shortcuts.opacity-3" +msgstr "불투명도 30% 설정" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:149 +msgid "shortcuts.opacity-4" +msgstr "불투명도 40% 설정" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:150 +msgid "shortcuts.opacity-5" +msgstr "불투명도 50% 설정" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:151 +msgid "shortcuts.opacity-6" +msgstr "불투명도 60% 설정" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:152 +msgid "shortcuts.opacity-7" +msgstr "불투명도 70% 설정" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:153 +msgid "shortcuts.opacity-8" +msgstr "불투명도 80% 설정" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:154 +msgid "shortcuts.opacity-9" +msgstr "불투명도 90% 설정" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:155 +msgid "shortcuts.open-color-picker" +msgstr "색상 선택기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:156 +msgid "shortcuts.open-comments" +msgstr "뷰어 댓글 섹션으로 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:157 +msgid "shortcuts.open-dashboard" +msgstr "대시보드로 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:158 +msgid "shortcuts.open-inspect" +msgstr "뷰어 검사 섹션으로 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:159 +msgid "shortcuts.open-interactions" +msgstr "뷰어 인터랙션 섹션으로 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:160 +msgid "shortcuts.open-viewer" +msgstr "뷰어 인터랙션 섹션으로 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:161 +msgid "shortcuts.open-workspace" +msgstr "워크스페이스로 이동" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:255 +msgid "shortcuts.or" +msgstr " 또는 " + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:162 +msgid "shortcuts.paste" +msgstr "붙여넣기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:111 +#, unused +msgid "shortcuts.paste-props" +msgstr "속성 붙여넣기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:604 +#, unused +msgid "shortcuts.plugins" +msgstr "플러그인 관리자" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:163 +msgid "shortcuts.prev-frame" +msgstr "이전 보드" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:164 +msgid "shortcuts.redo" +msgstr "다시 실행" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:165 +msgid "shortcuts.rename" +msgstr "이름 바꾸기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:166 +msgid "shortcuts.reset-zoom" +msgstr "확대/축소 초기화" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:167 +msgid "shortcuts.scale" +msgstr "스케일" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:168 +msgid "shortcuts.search-placeholder" +msgstr "단축키 검색" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:169 +msgid "shortcuts.select-all" +msgstr "전체 선택" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:170 +msgid "shortcuts.select-next" +msgstr "다음 레이어 선택" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:171 +msgid "shortcuts.select-parent-layer" +msgstr "상위 레이어 선택" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:172 +msgid "shortcuts.select-prev" +msgstr "이전 레이어 선택" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:173 +msgid "shortcuts.separate-nodes" +msgstr "노드 분리" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:174 +msgid "shortcuts.show-pixel-grid" +msgstr "픽셀 그리드 표시/숨기기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:175 +msgid "shortcuts.show-shortcuts" +msgstr "단축키 표시/숨기기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:176 +msgid "shortcuts.snap-nodes" +msgstr "노드에 스냅" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:177 +msgid "shortcuts.snap-pixel-grid" +msgstr "픽셀 그리드에 스냅" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:178 +msgid "shortcuts.start-editing" +msgstr "편집 시작" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:179 +msgid "shortcuts.start-measure" +msgstr "측정 시작" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:180 +msgid "shortcuts.stop-measure" +msgstr "측정 중지" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:181 +msgid "shortcuts.thumbnail-set" +msgstr "썸네일 설정" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:491, src/app/main/ui/workspace/sidebar/shortcuts.cljs:498 +msgid "shortcuts.title" +msgstr "키보드 단축키" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:182 +msgid "shortcuts.toggle-alignment" +msgstr "동적 정렬 전환" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:183 +msgid "shortcuts.toggle-assets" +msgstr "에셋 전환" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:184 +msgid "shortcuts.toggle-colorpalette" +msgstr "색상 선택자 켜기/끄기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:185 +msgid "shortcuts.toggle-focus-mode" +msgstr "포커스 모드 켜기/끄기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:186 +msgid "shortcuts.toggle-fullscreen" +msgstr "전체 화면 켜기/끄기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:187 +msgid "shortcuts.toggle-guides" +msgstr "가이드 표시 / 숨기기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:188 +msgid "shortcuts.toggle-history" +msgstr "히스토리 패널 전환" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:189 +msgid "shortcuts.toggle-layers" +msgstr "레이어 패널 전환" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:190 +msgid "shortcuts.toggle-layout-flex" +msgstr "플렉스 레이아웃 추가 / 제거" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:191 +msgid "shortcuts.toggle-layout-grid" +msgstr "그리드 레이아웃 추가 / 제거" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:192 +msgid "shortcuts.toggle-lock" +msgstr "잠금 / 잠금 해제" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:193 +msgid "shortcuts.toggle-lock-size" +msgstr "비율 잠금" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:194 +msgid "shortcuts.toggle-rulers" +msgstr "눈금자 표시 / 숨기기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:195 +msgid "shortcuts.toggle-snap-guides" +msgstr "가이드에 스냅" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:196 +msgid "shortcuts.toggle-snap-ruler-guide" +msgstr "눈금자 가이드에 스냅" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:197 +msgid "shortcuts.toggle-textpalette" +msgstr "텍스트 팔레트 전환" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:198 +msgid "shortcuts.toggle-theme" +msgstr "테마 변경" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:199 +msgid "shortcuts.toggle-visibility" +msgstr "표시 / 숨기기" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:200 +msgid "shortcuts.toggle-zoom-style" +msgstr "확대/축소 스타일 전환" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:201 +msgid "shortcuts.underline" +msgstr "밑줄 전환" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:202 +msgid "shortcuts.undo" +msgstr "실행 취소" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:203 +msgid "shortcuts.ungroup" +msgstr "그룹 해제" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:204 +msgid "shortcuts.unmask" +msgstr "마스크 해제" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:205 +msgid "shortcuts.v-distribute" +msgstr "세로로 균등 배분" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:206 +msgid "shortcuts.zoom-lense-decrease" +msgstr "줌 렌즈 축소" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:207 +msgid "shortcuts.zoom-lense-increase" +msgstr "줌 렌즈 확대" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:208 +msgid "shortcuts.zoom-selected" +msgstr "선택 항목에 맞추기" + +#: src/app/main/ui/dashboard/subscription.cljs:114, src/app/main/ui/dashboard/subscription.cljs:162 +msgid "subscription.dashboard.power-up.enterprise-plan" +msgstr "엔터프라이즈 플랜" + +#: src/app/main/ui/dashboard/subscription.cljs:109 +msgid "subscription.dashboard.power-up.enterprise-trial.top-title" +msgstr "엔터프라이즈 플랜 (체험판)" + +#: src/app/main/ui/dashboard/subscription.cljs:84 +msgid "subscription.dashboard.power-up.professional.bottom-button" +msgstr "업그레이드!" + +#: src/app/main/ui/dashboard/subscription.cljs:83 +msgid "subscription.dashboard.power-up.professional.bottom-description" +msgstr "팀을 위한 추가 저장 공간, 파일 복구 등의 기능을 이용해 보세요." + +#: src/app/main/ui/dashboard/subscription.cljs:82 +msgid "subscription.dashboard.power-up.professional.top-title" +msgstr "프로페셔널 플랜" + +#: src/app/main/ui/dashboard/subscription.cljs:64, src/app/main/ui/settings/subscription.cljs:107, src/app/main/ui/settings/subscription.cljs:131 +#, unused +msgid "subscription.dashboard.power-up.subscribe" +msgstr "구독하기" + +#: src/app/main/ui/dashboard/subscription.cljs:94 +#, markdown +msgid "subscription.dashboard.power-up.trial.bottom-description" +msgstr "" +"체험판은 만족스러우신가요? 모든 기능을 무제한으로 사용해 보세요. " +"[구독하기|target:self](%s)" + +#: src/app/main/ui/dashboard/subscription.cljs:93 +msgid "subscription.dashboard.power-up.trial.top-title" +msgstr "무제한 플랜 (체험판)" + +#: src/app/main/ui/dashboard/subscription.cljs:100, src/app/main/ui/dashboard/subscription.cljs:161 +msgid "subscription.dashboard.power-up.unlimited-plan" +msgstr "무제한 플랜" + +#: src/app/main/ui/dashboard/subscription.cljs:101 +#, markdown +msgid "subscription.dashboard.power-up.unlimited.bottom-text" +msgstr "" +"모든 팀에 대해 고정된 가격으로 무제한 저장 공간, 확장된 파일 복구 기능 및 " +"무제한 편집자 권한을 누리세요. [엔터프라이즈 플랜 살펴보기.|target:self](%s)" + +#: src/app/main/ui/dashboard/subscription.cljs:70 +#, unused +msgid "subscription.dashboard.power-up.unlimited.cta" +msgstr "자세히 보기" + +#: src/app/main/ui/dashboard/subscription.cljs:68 +#, unused +msgid "subscription.dashboard.power-up.unlimited.top-description" +msgstr "추가 편집자 시트, 저장 공간, 자동 저장 버전 및 파일 백업 등." + +#: src/app/main/ui/dashboard/subscription.cljs:81, src/app/main/ui/dashboard/subscription.cljs:92, src/app/main/ui/dashboard/subscription.cljs:99, src/app/main/ui/dashboard/subscription.cljs:108, src/app/main/ui/dashboard/subscription.cljs:113 +msgid "subscription.dashboard.power-up.your-subscription" +msgstr "현재 구독:" + +#: src/app/main/ui/dashboard/subscription.cljs:196 +msgid "subscription.dashboard.professional-dashboard-cta-title" +msgstr "" +"소유하신 팀 전체에 %s명의 편집자가 있으며, 프로페셔널 플랜은 최대 8명까지 " +"지원합니다." + +#: src/app/main/ui/dashboard/subscription.cljs:204 +#, markdown +msgid "subscription.dashboard.professional-dashboard-cta-upgrade-owner" +msgstr "" +"더 많은 편집자 시트와 저장 공간, 파일 복구 기능을 위해 무제한 또는 " +"엔터프라이즈 플랜으로 업그레이드해 주세요. [지금 구독하기.|target:self](%s)" + +#: src/app/main/ui/dashboard/subscription.cljs:137 +msgid "subscription.dashboard.team-plan" +msgstr "팀 플랜" + +#: src/app/main/ui/dashboard/subscription.cljs:199 +msgid "subscription.dashboard.unlimited-dashboard-cta-title" +msgstr "" +"팀이 계속 성장하고 있네요! 무제한 플랜은 최대 %s명의 편집자를 지원하지만, " +"현재 %s명이 소속되어 있습니다." + +#: src/app/main/ui/dashboard/subscription.cljs:207 +#, markdown +msgid "subscription.dashboard.unlimited-dashboard-cta-upgrade-owner" +msgstr "" +"현재 편집자 수에 맞춰 플랜을 업그레이드해 주세요. [지금 " +"구독하기.|target:self](%s)" + +#: src/app/main/ui/dashboard/subscription.cljs:184 +msgid "subscription.dashboard.unlimited-members-extra-editors-cta-text" +msgstr "" +"소유한 팀 전체에 추가된 새로운 편집자만 향후 요금에 합산됩니다. 25명 이상의 " +"편집자에 대해서는 월 $175의 고정 요금이 적용됩니다." + +#: src/app/main/ui/dashboard/sidebar.cljs:1073 +msgid "subscription.dashboard.upgrade-plan.power-up" +msgstr "업그레이드" + +#: src/app/main/ui/settings/sidebar.cljs:116, src/app/main/ui/settings/subscription.cljs:425, src/app/main/ui/settings/subscription.cljs:462 +msgid "subscription.labels" +msgstr "구독" + +#: src/app/main/ui/settings/subscription.cljs:484, src/app/main/ui/settings/subscription.cljs:508 +msgid "subscription.settings.add-payment-to-continue" +msgstr "체험판 종료 후에도 계속 이용하시려면 결제 수단을 추가해 주세요" + +#: src/app/main/ui/settings/subscription.cljs:478, src/app/main/ui/settings/subscription.cljs:554 +msgid "subscription.settings.benefits.all-professional-benefits" +msgstr "프로페셔널 플랜의 모든 혜택 및:" + +#: src/app/main/ui/settings/subscription.cljs:490, src/app/main/ui/settings/subscription.cljs:502, src/app/main/ui/settings/subscription.cljs:512, src/app/main/ui/settings/subscription.cljs:570 +msgid "subscription.settings.benefits.all-unlimited-benefits" +msgstr "무제한 플랜의 모든 혜택 및:" + +#: src/app/main/ui/settings/subscription.cljs:53 +msgid "subscription.settings.editors" +msgstr "(x %s명 편집자)" + +#: src/app/main/ui/dashboard/subscription.cljs:145, src/app/main/ui/settings/subscription.cljs:104, src/app/main/ui/settings/subscription.cljs:457, src/app/main/ui/settings/subscription.cljs:510, src/app/main/ui/settings/subscription.cljs:566 +msgid "subscription.settings.enterprise" +msgstr "엔터프라이즈" + +#: src/app/main/ui/settings/subscription.cljs:100, src/app/main/ui/settings/subscription.cljs:456, src/app/main/ui/settings/subscription.cljs:500 +msgid "subscription.settings.enterprise-trial" +msgstr "엔터프라이즈 (체험판)" + +#: src/app/main/ui/settings/subscription.cljs:504, src/app/main/ui/settings/subscription.cljs:514, src/app/main/ui/settings/subscription.cljs:572 +msgid "subscription.settings.enterprise.autosave" +msgstr "90일 자동 저장 버전 및 파일 복구" + +#: src/app/main/ui/settings/subscription.cljs:505, src/app/main/ui/settings/subscription.cljs:515, src/app/main/ui/settings/subscription.cljs:573 +msgid "subscription.settings.enterprise.capped-bill" +msgstr "월 정액제" + +#: src/app/main/ui/settings/subscription.cljs:503, src/app/main/ui/settings/subscription.cljs:513, src/app/main/ui/settings/subscription.cljs:571 +msgid "subscription.settings.enterprise.unlimited-storage-benefit" +msgstr "무제한 저장 공간" + +#: src/app/main/ui/dashboard/subscription.cljs:150, src/app/main/ui/settings/subscription.cljs:482, src/app/main/ui/settings/subscription.cljs:494, src/app/main/ui/settings/subscription.cljs:506, src/app/main/ui/settings/subscription.cljs:516 +msgid "subscription.settings.manage-your-subscription" +msgstr "나의 구독 관리" + +#: src/app/main/ui/settings/subscription.cljs:298 +msgid "subscription.settings.management-dialog.step-2-add-payment-button" +msgstr "결제 수단 추가" + +#: src/app/main/ui/settings/subscription.cljs:285 +msgid "subscription.settings.management-dialog.step-2-description" +msgstr "" +"결제 정보를 지금 추가하면 체험판 종료 후에도 구독이 끊김 없이 유지되며 " +"당사의 오픈소스 프로젝트를 후원하실 수 있습니다. 지금은 요금이 청구되지 " +"않습니다." + +#: src/app/main/ui/settings/subscription.cljs:293 +msgid "subscription.settings.management-dialog.step-2-skip-button" +msgstr "지금은 건너뛰고 체험판 시작" + +#: src/app/main/ui/settings/subscription.cljs:203 +msgid "subscription.settings.management-dialog.step-2-title" +msgstr "체험판을 더 원활하게 시작할 수 있도록 도와주세요" + +#: src/app/main/ui/settings/subscription.cljs:209 +msgid "subscription.settings.management.dialog.currently-editors-title" +msgid_plural "subscription.settings.management.dialog.currently-editors-title" +msgstr[0] "현재 팀 전체에서 편집할 수 있는 사람이 %s명 있습니다." + +#: src/app/main/ui/settings/subscription.cljs:230 +msgid "subscription.settings.management.dialog.downgrade" +msgstr "" +"알림: 하위 플랜으로 변경하면 저장 공간이 줄어들고 백업 및 버전 히스토리 보존 " +"기간이 단축됩니다." + +#: src/app/main/ui/settings/subscription.cljs:211 +msgid "subscription.settings.management.dialog.editors" +msgstr "편집자" + +#: src/app/main/ui/settings/subscription.cljs:218 +msgid "subscription.settings.management.dialog.editors-explanation" +msgstr "(소유자, 관리자, 편집자 포함. 뷰어는 편집자에 포함되지 않음)" + +#: src/app/main/ui/settings/subscription.cljs:263 +msgid "subscription.settings.management.dialog.input-error" +msgstr "" +"현재 인원보다 적은 편집자 수를 설정할 수 없습니다. 실제 편집을 하지 않는 " +"사용자는 팀 설정에서 역할(편집자/관리자에서 뷰어로)을 변경해 주세요." + +#: src/app/main/ui/settings/subscription.cljs:259 +msgid "subscription.settings.management.dialog.payment-explanation" +msgstr "체험판 종료 후 청구됩니다. 지금은 신용카드가 필요하지 않습니다." + +#: src/app/main/ui/settings/subscription.cljs:252, src/app/main/ui/settings/subscription.cljs:256 +#, markdown +msgid "subscription.settings.management.dialog.price-month" +msgstr "**$%s**/월" + +#: src/app/main/ui/settings/subscription.cljs:204 +msgid "subscription.settings.management.dialog.title" +msgstr "내 팀에 %s 적용" + +#: src/app/main/ui/settings/subscription.cljs:266 +msgid "subscription.settings.management.dialog.unlimited-capped-warning" +msgstr "" +"팁: 향후 초대를 고려하여 지금 시트 수를 늘려두실 수 있습니다. 팀 전체 " +"편집자가 25명 이상이면 월 $175의 고정 요금이 적용됩니다." + +#: src/app/main/ui/settings/subscription.cljs:533 +msgid "subscription.settings.member-since" +msgstr "Penpot 가입일: %s" + +#: src/app/main/ui/settings/subscription.cljs:546, src/app/main/ui/settings/subscription.cljs:560, src/app/main/ui/settings/subscription.cljs:576 +msgid "subscription.settings.more-information" +msgstr "자세한 정보" + +#: src/app/main/ui/settings/subscription.cljs:536 +msgid "subscription.settings.other-plans" +msgstr "다른 Penpot 플랜" + +#: src/app/main/ui/settings/subscription.cljs:540, src/app/main/ui/settings/subscription.cljs:553 +msgid "subscription.settings.price-editor-month" +msgstr "편집자당 월 비용" + +#: src/app/main/ui/settings/subscription.cljs:569 +msgid "subscription.settings.price-organization-month" +msgstr "조직/월" + +#: src/app/main/ui/dashboard/subscription.cljs:140, src/app/main/ui/settings/subscription.cljs:102, src/app/main/ui/settings/subscription.cljs:469, src/app/main/ui/settings/subscription.cljs:538 +msgid "subscription.settings.professional" +msgstr "프로페셔널" + +#: src/app/main/ui/settings/subscription.cljs:471, src/app/main/ui/settings/subscription.cljs:542 +msgid "subscription.settings.professional.autosave-benefit" +msgstr "7일 자동 저장 버전 및 파일 복구" + +#: src/app/main/ui/settings/subscription.cljs:470, src/app/main/ui/settings/subscription.cljs:541 +msgid "subscription.settings.professional.storage-benefit" +msgstr "10GB 저장 공간" + +#: src/app/main/ui/settings/subscription.cljs:472, src/app/main/ui/settings/subscription.cljs:543 +msgid "subscription.settings.professional.teams-editors-benefit" +msgstr "무제한 팀 생성. 소유한 팀 전체 합산 최대 8명의 편집자." + +#: src/app/main/ui/settings/subscription.cljs:50 +msgid "subscription.settings.recommended" +msgstr "추천" + +#: src/app/main/ui/settings/subscription.cljs:466 +msgid "subscription.settings.section-plan" +msgstr "내 구독 정보" + +#: src/app/main/ui/settings/subscription.cljs:313 +msgid "subscription.settings.start-trial" +msgstr "무료 체험판 시작" + +#: src/app/main/ui/settings/subscription.cljs:278, src/app/main/ui/settings/subscription.cljs:544, src/app/main/ui/settings/subscription.cljs:558, src/app/main/ui/settings/subscription.cljs:574 +msgid "subscription.settings.subscribe" +msgstr "구독하기" + +#: src/app/main/ui/settings/subscription.cljs:345 +msgid "subscription.settings.success.dialog.description" +msgstr "계정 상세 정보의 '구독' 페이지에서 언제든지 구독 내용을 수정하실 수 있습니다." + +#: src/app/main/ui/settings/subscription.cljs:343 +msgid "subscription.settings.success.dialog.thanks" +msgstr "Penpot %s 플랜을 선택해주셔서 감사합니다!" + +#: src/app/main/ui/settings/subscription.cljs:347 +msgid "subscription.settings.sucess.dialog.footer" +msgstr "플랜을 즐기세요!" + +#: src/app/main/ui/settings/subscription.cljs:340 +msgid "subscription.settings.sucess.dialog.title" +msgstr "%s이(가) 되셨습니다!" + +#: src/app/main/ui/settings/subscription.cljs:526 +msgid "subscription.settings.support-us-since" +msgstr "이 플랜으로 저희를 지원해주신 날짜: %s" + +#: src/app/main/ui/settings/subscription.cljs:558, src/app/main/ui/settings/subscription.cljs:574 +msgid "subscription.settings.try-it-free" +msgstr "14일 무료 체험" + +#: src/app/main/ui/dashboard/subscription.cljs:143, src/app/main/ui/settings/subscription.cljs:103, src/app/main/ui/settings/subscription.cljs:454, src/app/main/ui/settings/subscription.cljs:488, src/app/main/ui/settings/subscription.cljs:550 +msgid "subscription.settings.unlimited" +msgstr "무제한" + +#: src/app/main/ui/dashboard/subscription.cljs:142, src/app/main/ui/settings/subscription.cljs:99, src/app/main/ui/settings/subscription.cljs:453, src/app/main/ui/settings/subscription.cljs:476 +msgid "subscription.settings.unlimited-trial" +msgstr "무제한 (체험판)" + +#: src/app/main/ui/settings/subscription.cljs:480, src/app/main/ui/settings/subscription.cljs:492, src/app/main/ui/settings/subscription.cljs:556 +msgid "subscription.settings.unlimited.autosave-benefit" +msgstr "30일 자동 저장 버전 및 파일 복구" + +#: src/app/main/ui/settings/subscription.cljs:481, src/app/main/ui/settings/subscription.cljs:493, src/app/main/ui/settings/subscription.cljs:557 +msgid "subscription.settings.unlimited.bill" +msgstr "월 최대 $175 정액 청구" + +#: src/app/main/ui/settings/subscription.cljs:479, src/app/main/ui/settings/subscription.cljs:491, src/app/main/ui/settings/subscription.cljs:555 +msgid "subscription.settings.unlimited.storage-benefit" +msgstr "25GB 저장 공간" + +#: src/app/main/ui/dashboard/subscription.cljs:175, src/app/main/ui/workspace/main_menu.cljs:945 +msgid "subscription.workspace.header.menu.option.power-up" +msgstr "플랜 업그레이드" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:57 +#, markdown +msgid "subscription.workspace.versions.warning.enterprise.subtext-owner" +msgstr "이 한도를 늘리려면 [%s](mailto)로 문의하세요" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:59 +#, markdown +msgid "subscription.workspace.versions.warning.subtext-member" +msgstr "이 한도를 늘리려면 팀 소유자에게 문의하세요: [mailto:%s](%s)" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:58 +#, markdown +msgid "subscription.workspace.versions.warning.subtext-owner" +msgstr "이 한도를 늘리려면 [플랜을 업그레이드하세요|target:self](%s)" + +#: src/app/main/ui/dashboard/team.cljs:933 +msgid "team.invitations-selected" +msgid_plural "team.invitations-selected" +msgstr[0] "초대 %s개 선택됨" + +#: src/app/main/ui/dashboard/files.cljs:181 +msgid "title.dashboard.files" +msgstr "%s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs:46 +msgid "title.dashboard.font-providers" +msgstr "글꼴 제공자 - %s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs:45 +msgid "title.dashboard.fonts" +msgstr "글꼴 - %s - Penpot" + +#: src/app/main/ui/dashboard/projects.cljs:357 +msgid "title.dashboard.projects" +msgstr "프로젝트 - %s - Penpot" + +#: src/app/main/ui/dashboard/search.cljs:50 +msgid "title.dashboard.search" +msgstr "검색 - %s - Penpot" + +#: src/app/main/ui/dashboard/libraries.cljs:58 +msgid "title.dashboard.shared-libraries" +msgstr "공유 라이브러리 - %s - Penpot" + +#: src/app/main/ui/auth/verify_token.cljs:70, src/app/main/ui/auth.cljs:34 +msgid "title.default" +msgstr "Penpot - 팀을 위한 자유로운 디자인" + +#: src/app/main/ui/settings/access_tokens.cljs:278 +msgid "title.settings.access-tokens" +msgstr "프로필 - 액세스 토큰" + +#: src/app/main/ui/settings/feedback.cljs:161 +msgid "title.settings.feedback" +msgstr "의견 보내기 - Penpot" + +#: src/app/main/ui/settings/notifications.cljs:45 +msgid "title.settings.notifications" +msgstr "알림 - Penpot" + +#: src/app/main/ui/settings/options.cljs:83 +msgid "title.settings.options" +msgstr "설정 - Penpot" + +#: src/app/main/ui/settings/password.cljs:105 +msgid "title.settings.password" +msgstr "비밀번호 - Penpot" + +#: src/app/main/ui/settings/profile.cljs:124 +msgid "title.settings.profile" +msgstr "프로필 - Penpot" + +#: src/app/main/ui/dashboard/team.cljs:981 +msgid "title.team-invitations" +msgstr "초대 - %s - Penpot" + +#: src/app/main/ui/dashboard/team.cljs:535 +msgid "title.team-members" +msgstr "구성원 - %s - Penpot" + +#: src/app/main/ui/dashboard/team.cljs:1296 +msgid "title.team-settings" +msgstr "설정 - %s - Penpot" + +#: src/app/main/ui/dashboard/team.cljs:1249 +msgid "title.team-webhooks" +msgstr "웹훅 - %s - Penpot" + +#: src/app/main/ui/viewer.cljs:423 +msgid "title.viewer" +msgstr "%s - 보기 모드 - Penpot" + +#: src/app/main/ui/workspace.cljs:237 +msgid "title.workspace" +msgstr "%s - Penpot" + +#: src/app/main/ui.cljs:138 +#, unused +msgid "viewer.breaking-change.description" +msgstr "" +"공유 링크가 더 이상 유효하지 않습니다. 새 링크를 만들거나 소유자에게 " +"요청하세요." + +#: src/app/main/ui.cljs:137 +#, unused +msgid "viewer.breaking-change.message" +msgstr "죄송합니다!" + +#: src/app/main/ui/viewer.cljs:573 +msgid "viewer.empty-state" +msgstr "페이지에 보드가 없습니다." + +#: src/app/main/ui/viewer.cljs:578 +msgid "viewer.frame-not-found" +msgstr "보드를 찾을 수 없습니다." + +#: src/app/main/ui/viewer/header.cljs:336 +msgid "viewer.header.comments-section" +msgstr "댓글 (%s)" + +#: src/app/main/ui/viewer/interactions.cljs:298 +msgid "viewer.header.dont-show-interactions" +msgstr "인터랙션 숨기기" + +#: src/app/main/ui/viewer/header.cljs:187 +msgid "viewer.header.edit-in-workspace" +msgstr "워크스페이스에서 편집" + +#: src/app/main/ui/viewer/header.cljs:193 +msgid "viewer.header.fullscreen" +msgstr "전체 화면" + +#: src/app/main/ui/viewer/header.cljs:346 +msgid "viewer.header.inspect-section" +msgstr "검사 (%s)" + +#: src/app/main/ui/viewer/interactions.cljs:288 +msgid "viewer.header.interactions" +msgstr "인터랙션" + +#: src/app/main/ui/viewer/header.cljs:327 +msgid "viewer.header.interactions-section" +msgstr "인터랙션 (%s)" + +#: src/app/main/ui/viewer/share_link.cljs:193 +msgid "viewer.header.share.copy-link" +msgstr "링크 복사" + +#: src/app/main/ui/viewer/interactions.cljs:306 +msgid "viewer.header.show-interactions" +msgstr "인터랙션 표시" + +#: src/app/main/ui/viewer/interactions.cljs:317 +msgid "viewer.header.show-interactions-on-click" +msgstr "클릭 시 인터랙션 표시" + +#: src/app/main/ui/viewer/header.cljs:233 +msgid "viewer.header.sitemap" +msgstr "사이트맵" + +#: src/app/main/ui/dashboard/team.cljs:1203 +msgid "webhooks.last-delivery.success" +msgstr "마지막 전송이 성공했습니다." + +#: src/app/main/ui/workspace/sidebar/options/menus/align.cljs:55 +msgid "workspace.align.hcenter" +msgstr "가로 가운데 정렬 (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/align.cljs:69 +msgid "workspace.align.hdistribute" +msgstr "가로 간격 균등 (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/align.cljs:48 +msgid "workspace.align.hleft" +msgstr "왼쪽 정렬 (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/align.cljs:62 +msgid "workspace.align.hright" +msgstr "오른쪽 정렬 (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/align.cljs:91 +msgid "workspace.align.vbottom" +msgstr "아래쪽 정렬 (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/align.cljs:84 +msgid "workspace.align.vcenter" +msgstr "세로 가운데 정렬 (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/align.cljs:98 +msgid "workspace.align.vdistribute" +msgstr "세로 간격 균등 (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/align.cljs:77 +msgid "workspace.align.vtop" +msgstr "위쪽 정렬 (%s)" + +#: src/app/main/ui/workspace/sidebar/assets.cljs:172 +msgid "workspace.assets.add-library" +msgstr "라이브러리 추가" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +#, unused +msgid "workspace.assets.assets" +msgstr "에셋" + +#: src/app/main/ui/workspace/sidebar/assets.cljs:152 +msgid "workspace.assets.box-filter-all" +msgstr "모든 에셋" + +#: src/app/main/ui/dashboard/grid.cljs:161, src/app/main/ui/dashboard/grid.cljs:193, src/app/main/ui/workspace/sidebar/assets/colors.cljs:489, src/app/main/ui/workspace/sidebar/assets.cljs:158 +msgid "workspace.assets.colors" +msgstr "색상" + +#: src/app/main/ui/workspace/sidebar/assets/colors.cljs:497 +msgid "workspace.assets.colors.add-color" +msgstr "색상 추가" + +#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:81 +msgid "workspace.assets.component-group-options" +msgstr "컴포넌트 그룹 옵션" + +#: src/app/main/ui/dashboard/grid.cljs:157, src/app/main/ui/dashboard/grid.cljs:172, src/app/main/ui/workspace/sidebar/assets/components.cljs:559, src/app/main/ui/workspace/sidebar/assets.cljs:155 +msgid "workspace.assets.components" +msgstr "컴포넌트" + +#: src/app/main/ui/workspace/sidebar/assets/components.cljs:580 +msgid "workspace.assets.components.add-component" +msgstr "컴포넌트 추가" + +#: src/app/main/ui/workspace/sidebar/assets/components.cljs:177, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:602 +msgid "workspace.assets.components.num-variants" +msgstr "%s개 베리언트" + +#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:141 +msgid "workspace.assets.create-group" +msgstr "그룹 생성" + +#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:152 +msgid "workspace.assets.create-group-hint" +msgstr "항목 이름이 \"그룹 이름 / 항목 이름\" 형식으로 자동으로 지정됩니다" + +#: src/app/main/ui/workspace/context_menu.cljs:684, src/app/main/ui/workspace/sidebar/assets/colors.cljs:251, src/app/main/ui/workspace/sidebar/assets/components.cljs:640, src/app/main/ui/workspace/sidebar/assets/typographies.cljs:442 +msgid "workspace.assets.delete" +msgstr "삭제" + +#: src/app/main/ui/workspace/context_menu.cljs:689 +msgid "workspace.assets.duplicate" +msgstr "복제" + +#: src/app/main/ui/workspace/sidebar/assets/components.cljs:619 +msgid "workspace.assets.duplicate-main" +msgstr "메인 복제" + +#: src/app/main/ui/workspace/sidebar/assets/colors.cljs:247, src/app/main/ui/workspace/sidebar/assets/typographies.cljs:438 +msgid "workspace.assets.edit" +msgstr "편집" + +#: src/app/main/ui/workspace/sidebar/assets.cljs:186 +msgid "workspace.assets.filter" +msgstr "필터" + +#: src/app/main/ui/workspace/sidebar/assets/graphics.cljs:386, src/app/main/ui/workspace/sidebar/assets.cljs:152 +#, unused +msgid "workspace.assets.graphics" +msgstr "그래픽" + +#: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:189, src/app/main/ui/workspace/sidebar/assets/components.cljs:575, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:806 +msgid "workspace.assets.grid-view" +msgstr "그리드 보기" + +#: src/app/main/ui/workspace/sidebar/assets/colors.cljs:255, src/app/main/ui/workspace/sidebar/assets/components.cljs:624, src/app/main/ui/workspace/sidebar/assets/typographies.cljs:447 +msgid "workspace.assets.group" +msgstr "그룹" + +#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:151 +msgid "workspace.assets.group-name" +msgstr "그룹 이름" + +#: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:190, src/app/main/ui/workspace/sidebar/assets/components.cljs:571, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:802 +msgid "workspace.assets.list-view" +msgstr "목록 보기" + +#: src/app/main/ui/workspace/sidebar/assets/file_library.cljs:108, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:686 +msgid "workspace.assets.local-library" +msgstr "로컬 라이브러리" + +#: src/app/main/ui/workspace/sidebar/assets.cljs:176 +msgid "workspace.assets.manage-library" +msgstr "라이브러리 관리" + +#: src/app/main/ui/workspace/sidebar/assets/file_library.cljs:307 +msgid "workspace.assets.not-found" +msgstr "에셋을 찾을 수 없습니다" + +#: src/app/main/ui/workspace/sidebar/assets/file_library.cljs:113 +msgid "workspace.assets.open-library" +msgstr "라이브러리 파일 열기" + +#: src/app/main/ui/workspace/context_menu.cljs:687, src/app/main/ui/workspace/sidebar/assets/colors.cljs:243, src/app/main/ui/workspace/sidebar/assets/components.cljs:615, src/app/main/ui/workspace/sidebar/assets/groups.cljs:67, src/app/main/ui/workspace/sidebar/assets/typographies.cljs:433 +msgid "workspace.assets.rename" +msgstr "이름 바꾸기" + +#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:142 +msgid "workspace.assets.rename-group" +msgstr "그룹 이름 바꾸기" + +#: src/app/main/ui/workspace/sidebar/assets.cljs:181 +msgid "workspace.assets.search" +msgstr "에셋 검색" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +#, unused +msgid "workspace.assets.selected-count" +msgid_plural "workspace.assets.selected-count" +msgstr[0] "%s개 항목 선택됨" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +#, unused +msgid "workspace.assets.shared-library" +msgstr "공유 라이브러리" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:229 +msgid "workspace.assets.sidebar.components" +msgid_plural "workspace.assets.sidebar.components" +msgstr[0] "컴포넌트 %s개" + +#: src/app/main/ui/workspace/sidebar/assets.cljs:201 +msgid "workspace.assets.sort" +msgstr "정렬" + +#: src/app/main/ui/dashboard/grid.cljs:165, src/app/main/ui/dashboard/grid.cljs:220, src/app/main/ui/workspace/sidebar/assets/typographies.cljs:396, src/app/main/ui/workspace/sidebar/assets.cljs:161 +msgid "workspace.assets.typography" +msgstr "타이포그래피" + +#: src/app/main/ui/workspace/sidebar/assets/typographies.cljs:404 +msgid "workspace.assets.typography.add-typography" +msgstr "타이포그래피 추가" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#, unused +msgid "workspace.assets.typography.font-id" +msgstr "글꼴" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:528 +msgid "workspace.assets.typography.font-size" +msgstr "크기" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:524 +msgid "workspace.assets.typography.font-style" +msgstr "글꼴 스타일" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:546 +msgid "workspace.assets.typography.go-to-edit" +msgstr "스타일 라이브러리 파일로 이동하여 편집" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:536 +msgid "workspace.assets.typography.letter-spacing" +msgstr "자간" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:532 +msgid "workspace.assets.typography.line-height" +msgstr "행간" + +#: src/app/main/ui/dashboard/grid.cljs:230, src/app/main/ui/workspace/libraries.cljs:566, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:487, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:512, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:619, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:639 +msgid "workspace.assets.typography.sample" +msgstr "가나다" + +#, unused +msgid "workspace.assets.typography.text-styles" +msgstr "텍스트 스타일" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:540 +msgid "workspace.assets.typography.text-transform" +msgstr "텍스트 변환" + +#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:70 +msgid "workspace.assets.ungroup" +msgstr "그룹 해제" + +#: src/app/main/ui/workspace/colorpicker.cljs:428, src/app/main/ui/workspace/colorpicker.cljs:441 +msgid "workspace.colorpicker.color-tokens" +msgstr "색상 token" + +#: src/app/main/ui/workspace/colorpicker.cljs:434 +msgid "workspace.colorpicker.get-color" +msgstr "색상 가져오기" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:506 +msgid "workspace.component.swap.loop-error" +msgstr "컴포넌트는 자기 자신 안에 중첩될 수 없습니다." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:505 +msgid "workspace.component.switch.loop-error-multi" +msgstr "" +"일부 복사본을 전환할 수 없습니다. 컴포넌트는 자기 자신 안에 중첩될 수 " +"없습니다." + +#: src/app/main/ui/workspace/context_menu.cljs:796 +msgid "workspace.context-menu.grid-cells.area" +msgstr "영역 생성" + +#: src/app/main/ui/workspace/context_menu.cljs:799 +msgid "workspace.context-menu.grid-cells.create-board" +msgstr "보드 생성" + +#: src/app/main/ui/workspace/context_menu.cljs:791 +msgid "workspace.context-menu.grid-cells.merge" +msgstr "셀 병합" + +#: src/app/main/ui/workspace/context_menu.cljs:754 +msgid "workspace.context-menu.grid-track.column.add-after" +msgstr "오른쪽에 열 1개 추가" + +#: src/app/main/ui/workspace/context_menu.cljs:753 +msgid "workspace.context-menu.grid-track.column.add-before" +msgstr "왼쪽에 열 1개 추가" + +#: src/app/main/ui/workspace/context_menu.cljs:755 +msgid "workspace.context-menu.grid-track.column.delete" +msgstr "열 삭제" + +#: src/app/main/ui/workspace/context_menu.cljs:756 +msgid "workspace.context-menu.grid-track.column.delete-shapes" +msgstr "열 및 도형 삭제" + +#: src/app/main/ui/workspace/context_menu.cljs:752 +msgid "workspace.context-menu.grid-track.column.duplicate" +msgstr "열 복제" + +#: src/app/main/ui/workspace/context_menu.cljs:761 +msgid "workspace.context-menu.grid-track.row.add-after" +msgstr "아래에 행 1개 추가" + +#: src/app/main/ui/workspace/context_menu.cljs:760 +msgid "workspace.context-menu.grid-track.row.add-before" +msgstr "위에 행 1개 추가" + +#: src/app/main/ui/workspace/context_menu.cljs:762 +msgid "workspace.context-menu.grid-track.row.delete" +msgstr "행 삭제" + +#: src/app/main/ui/workspace/context_menu.cljs:763 +msgid "workspace.context-menu.grid-track.row.delete-shapes" +msgstr "행 및 도형 삭제" + +#: src/app/main/ui/workspace/context_menu.cljs:759 +msgid "workspace.context-menu.grid-track.row.duplicate" +msgstr "행 복제" + +#: src/app/main/ui/workspace/sidebar/debug.cljs:38 +msgid "workspace.debug.title" +msgstr "디버깅 도구" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:512 +msgid "workspace.focus.focus-mode" +msgstr "포커스 모드" + +#: src/app/main/ui/workspace/context_menu.cljs:396, src/app/main/ui/workspace/context_menu.cljs:711 +msgid "workspace.focus.focus-off" +msgstr "포커스 해제" + +#: src/app/main/ui/workspace/context_menu.cljs:395 +msgid "workspace.focus.focus-on" +msgstr "포커스 설정" + +#, unused +msgid "workspace.focus.selection" +msgstr "선택" + +#: src/app/util/color.cljs:34 +msgid "workspace.gradients.linear" +msgstr "선형 그래디언트" + +#: src/app/util/color.cljs:35 +msgid "workspace.gradients.radial" +msgstr "방사형 그래디언트" + +#: src/app/main/ui/workspace/main_menu.cljs:274 +msgid "workspace.header.menu.disable-dynamic-alignment" +msgstr "동적 정렬 비활성화" + +#: src/app/main/ui/workspace/main_menu.cljs:228 +msgid "workspace.header.menu.disable-scale-content" +msgstr "비율 유지 크기 조정 비활성화" + +#: src/app/main/ui/workspace/header.cljs +#, unused +msgid "workspace.header.menu.disable-scale-text" +msgstr "텍스트 크기 조정 비활성화" + +#: src/app/main/ui/workspace/main_menu.cljs:259 +msgid "workspace.header.menu.disable-snap-guides" +msgstr "가이드 스냅 비활성화" + +#: src/app/main/ui/workspace/main_menu.cljs:289 +msgid "workspace.header.menu.disable-snap-pixel-grid" +msgstr "픽셀 스냅 비활성화" + +#: src/app/main/ui/workspace/main_menu.cljs:243 +msgid "workspace.header.menu.disable-snap-ruler-guides" +msgstr "눈금자 가이드 스냅 비활성화" + +#: src/app/main/ui/workspace/main_menu.cljs:275 +msgid "workspace.header.menu.enable-dynamic-alignment" +msgstr "동적 정렬 활성화" + +#: src/app/main/ui/workspace/main_menu.cljs:229 +msgid "workspace.header.menu.enable-scale-content" +msgstr "비율 크기 조절 활성화" + +#: src/app/main/ui/workspace/header.cljs +#, unused +msgid "workspace.header.menu.enable-scale-text" +msgstr "텍스트 크기 조절 활성화" + +#: src/app/main/ui/workspace/main_menu.cljs:260 +msgid "workspace.header.menu.enable-snap-guides" +msgstr "가이드에 스냅" + +#: src/app/main/ui/workspace/main_menu.cljs:290 +msgid "workspace.header.menu.enable-snap-pixel-grid" +msgstr "픽셀 스냅 활성화" + +#: src/app/main/ui/workspace/main_menu.cljs:244 +msgid "workspace.header.menu.enable-snap-ruler-guides" +msgstr "눈금자 가이드에 스냅" + +#: src/app/main/ui/workspace/main_menu.cljs:422 +msgid "workspace.header.menu.hide-artboard-names" +msgstr "보드 이름 숨기기" + +#: src/app/main/ui/workspace/main_menu.cljs:376 +msgid "workspace.header.menu.hide-guides" +msgstr "가이드 숨기기" + +#: src/app/main/ui/workspace/main_menu.cljs:393 +msgid "workspace.header.menu.hide-palette" +msgstr "색상 팔레트 숨기기" + +#: src/app/main/ui/workspace/main_menu.cljs:434 +msgid "workspace.header.menu.hide-pixel-grid" +msgstr "픽셀 그리드 숨기기" + +#: src/app/main/ui/workspace/main_menu.cljs:360 +msgid "workspace.header.menu.hide-rules" +msgstr "눈금자 숨기기" + +#: src/app/main/ui/workspace/main_menu.cljs:407 +msgid "workspace.header.menu.hide-textpalette" +msgstr "글꼴 팔레트 숨기기" + +#: src/app/main/ui/workspace/main_menu.cljs:884 +msgid "workspace.header.menu.option.edit" +msgstr "편집" + +#: src/app/main/ui/workspace/main_menu.cljs:873 +msgid "workspace.header.menu.option.file" +msgstr "파일" + +#: src/app/main/ui/workspace/main_menu.cljs:930 +msgid "workspace.header.menu.option.help-info" +msgstr "도움말 및 정보" + +#: src/app/main/ui/workspace/main_menu.cljs:916 +#, unused +msgid "workspace.header.menu.option.power-up" +msgstr "플랜 업그레이드" + +#: src/app/main/ui/workspace/main_menu.cljs:906 +msgid "workspace.header.menu.option.preferences" +msgstr "환경 설정" + +#: src/app/main/ui/workspace/main_menu.cljs:895 +msgid "workspace.header.menu.option.view" +msgstr "보기" + +#: src/app/main/ui/workspace/main_menu.cljs:506 +msgid "workspace.header.menu.redo" +msgstr "다시 실행" + +#: src/app/main/ui/workspace/main_menu.cljs:477 +msgid "workspace.header.menu.select-all" +msgstr "모두 선택" + +#: src/app/main/ui/workspace/main_menu.cljs:423 +msgid "workspace.header.menu.show-artboard-names" +msgstr "보드 이름 표시" + +#: src/app/main/ui/workspace/main_menu.cljs:377 +msgid "workspace.header.menu.show-guides" +msgstr "가이드 표시" + +#: src/app/main/ui/workspace/main_menu.cljs:394 +msgid "workspace.header.menu.show-palette" +msgstr "색상 팔레트 표시" + +#: src/app/main/ui/workspace/main_menu.cljs:435 +msgid "workspace.header.menu.show-pixel-grid" +msgstr "픽셀 그리드 표시" + +#: src/app/main/ui/workspace/main_menu.cljs:361 +msgid "workspace.header.menu.show-rules" +msgstr "눈금자 표시" + +#: src/app/main/ui/workspace/main_menu.cljs:408 +msgid "workspace.header.menu.show-textpalette" +msgstr "글꼴 팔레트 표시" + +#: src/app/main/ui/workspace/main_menu.cljs:316 +msgid "workspace.header.menu.toggle-dark-theme" +msgstr "다크 테마로 전환" + +#: src/app/main/ui/workspace/main_menu.cljs:314, src/app/main/ui/workspace/main_menu.cljs:317 +msgid "workspace.header.menu.toggle-light-theme" +msgstr "라이트 테마로 전환" + +#: src/app/main/ui/workspace/main_menu.cljs:315 +msgid "workspace.header.menu.toggle-system-theme" +msgstr "시스템 테마로 전환" + +#: src/app/main/ui/workspace/main_menu.cljs:492 +msgid "workspace.header.menu.undo" +msgstr "실행 취소" + +#: src/app/main/ui/viewer/header.cljs:93, src/app/main/ui/workspace/right_header.cljs:92 +msgid "workspace.header.reset-zoom" +msgstr "초기화" + +#: src/app/main/ui/workspace/left_header.cljs:128 +msgid "workspace.header.save-error" +msgstr "저장 오류" + +#: src/app/main/ui/workspace/left_header.cljs:127 +msgid "workspace.header.saved" +msgstr "저장됨" + +#: src/app/main/ui/workspace/left_header.cljs:125, src/app/main/ui/workspace/left_header.cljs:126 +msgid "workspace.header.saving" +msgstr "저장 중" + +#: src/app/main/ui/workspace/right_header.cljs:232 +msgid "workspace.header.share" +msgstr "공유" + +#: src/app/main/ui/workspace/right_header.cljs:48, src/app/main/ui/workspace/right_header.cljs:53 +#, unused +msgid "workspace.header.unsaved" +msgstr "저장하지 않은 변경 사항" + +#: src/app/main/ui/workspace/right_header.cljs:237 +msgid "workspace.header.viewer" +msgstr "보기 모드 (%s)" + +#: src/app/main/ui/viewer/header.cljs:74, src/app/main/ui/workspace/right_header.cljs:74 +msgid "workspace.header.zoom" +msgstr "확대/축소" + +#: src/app/main/ui/viewer/header.cljs:104 +msgid "workspace.header.zoom-fill" +msgstr "채우기 - 채우기 맞춤" + +#: src/app/main/ui/viewer/header.cljs:97 +msgid "workspace.header.zoom-fit" +msgstr "맞추기 - 화면에 맞게 축소" + +#: src/app/main/ui/workspace/right_header.cljs:96 +msgid "workspace.header.zoom-fit-all" +msgstr "전체 맞추기" + +#: src/app/main/ui/viewer/header.cljs:111 +msgid "workspace.header.zoom-full-screen" +msgstr "전체 화면" + +#: src/app/main/ui/workspace/right_header.cljs:104 +msgid "workspace.header.zoom-selected" +msgstr "선택 항목에 맞추기" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:422 +msgid "workspace.layout-grid.editor.margin.expand" +msgstr "4면 여백 옵션 표시" + +#: src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs:275, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:859 +msgid "workspace.layout-grid.editor.options.edit-grid" +msgstr "그리드 편집" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1539 +msgid "workspace.layout-grid.editor.options.exit" +msgstr "종료" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:584, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:593, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:599 +msgid "workspace.layout-grid.editor.padding.bottom" +msgstr "아래쪽 패딩" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:669 +msgid "workspace.layout-grid.editor.padding.expand" +msgstr "4면 패딩 옵션 보기" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:416, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:427, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:433 +msgid "workspace.layout-grid.editor.padding.horizontal" +msgstr "가로 패딩" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:618, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:627, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:633 +msgid "workspace.layout-grid.editor.padding.left" +msgstr "왼쪽 패딩" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:551, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:560, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:566 +msgid "workspace.layout-grid.editor.padding.right" +msgstr "오른쪽 패딩" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:517, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:526, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:532 +msgid "workspace.layout-grid.editor.padding.top" +msgstr "위쪽 패딩" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:380, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:391, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:397 +msgid "workspace.layout-grid.editor.padding.vertical" +msgstr "세로 패딩" + +#: src/app/main/ui/workspace/viewport/grid_layout_editor.cljs:62 +msgid "workspace.layout-grid.editor.title" +msgstr "그리드 편집 중" + +#: src/app/main/ui/workspace/viewport/grid_layout_editor.cljs:70 +msgid "workspace.layout-grid.editor.top-bar.done" +msgstr "완료" + +#: src/app/main/ui/workspace/viewport/grid_layout_editor.cljs:66 +msgid "workspace.layout-grid.editor.top-bar.locate" +msgstr "위치 찾기" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1565 +msgid "workspace.layout-grid.editor.top-bar.locate.tooltip" +msgstr "그리드 레이아웃 위치 찾기" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:449 +msgid "workspace.layout-item.fit-content-horizontal" +msgstr "콘텐츠에 맞춤 (가로)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:477 +msgid "workspace.layout-item.fit-content-vertical" +msgstr "콘텐츠에 맞춤 (세로)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:465 +msgid "workspace.layout-item.fix-height" +msgstr "높이 고정" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:439 +msgid "workspace.layout-item.fix-width" +msgstr "너비 고정" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:471 +msgid "workspace.layout-item.height-100" +msgstr "높이 100%" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:444 +msgid "workspace.layout-item.width-100" +msgstr "너비 100%" + +#: src/app/main/ui/workspace/libraries.cljs +#, unused +msgid "workspace.libraries.add" +msgstr "추가" + +#: src/app/main/ui/workspace/libraries.cljs:100, src/app/main/ui/workspace/libraries.cljs:126 +msgid "workspace.libraries.colors" +msgid_plural "workspace.libraries.colors" +msgstr[0] "색상 %s개" + +#: src/app/main/ui/workspace/color_palette.cljs:147 +msgid "workspace.libraries.colors.empty-palette" +msgstr "라이브러리에 아직 색상 스타일이 없습니다" + +#: src/app/main/ui/workspace/text_palette.cljs:161 +msgid "workspace.libraries.colors.empty-typography-palette" +msgstr "라이브러리에 아직 타이포그래피 스타일이 없습니다" + +#: src/app/main/ui/workspace/color_palette_ctx_menu.cljs:88, src/app/main/ui/workspace/colorpicker/libraries.cljs:48, src/app/main/ui/workspace/text_palette_ctx_menu.cljs:49 +msgid "workspace.libraries.colors.file-library" +msgstr "파일 라이브러리" + +#: src/app/main/ui/workspace/colorpicker.cljs +#, unused +msgid "workspace.libraries.colors.hsv" +msgstr "HSV" + +#: src/app/main/ui/workspace/color_palette_ctx_menu.cljs:111, src/app/main/ui/workspace/colorpicker/libraries.cljs:47 +msgid "workspace.libraries.colors.recent-colors" +msgstr "최근 색상" + +#: src/app/main/ui/workspace/colorpicker.cljs +#, unused +msgid "workspace.libraries.colors.rgb-complementary" +msgstr "RGB 보색" + +#: src/app/main/ui/workspace/colorpicker.cljs:355 +msgid "workspace.libraries.colors.rgba" +msgstr "RGBA" + +#: src/app/main/ui/workspace/colorpicker.cljs:555 +msgid "workspace.libraries.colors.save-color" +msgstr "색상 스타일 저장" + +#: src/app/main/ui/workspace/libraries.cljs:94, src/app/main/ui/workspace/libraries.cljs:118 +msgid "workspace.libraries.components" +msgid_plural "workspace.libraries.components" +msgstr[0] "컴포넌트 %s개" + +#: src/app/main/ui/workspace/libraries.cljs:338 +msgid "workspace.libraries.connected-to" +msgstr "연결됨:" + +#: src/app/main/ui/workspace/libraries.cljs:392 +msgid "workspace.libraries.empty.add-some" +msgstr "또는 다음 중 하나를 추가해 보세요:" + +#: src/app/main/ui/workspace/libraries.cljs:386 +msgid "workspace.libraries.empty.no-libraries" +msgstr "팀에 공유 라이브러리가 없습니다. 여기서 찾아보세요" + +#: src/app/main/ui/workspace/libraries.cljs:390 +msgid "workspace.libraries.empty.some-templates" +msgstr "일부 템플릿 보기" + +#: src/app/main/ui/workspace/libraries.cljs:313 +msgid "workspace.libraries.file-library" +msgstr "파일 라이브러리" + +#: src/app/main/ui/workspace/libraries.cljs:97, src/app/main/ui/workspace/libraries.cljs:122 +msgid "workspace.libraries.graphics" +msgid_plural "workspace.libraries.graphics" +msgstr[0] "그래픽 %s개" + +#: src/app/main/ui/workspace/libraries.cljs:307 +msgid "workspace.libraries.in-this-file" +msgstr "이 파일의 라이브러리" + +#: src/app/main/ui/workspace/libraries.cljs:628, src/app/main/ui/workspace/libraries.cljs:648 +msgid "workspace.libraries.libraries" +msgstr "라이브러리" + +#: src/app/main/ui/workspace/libraries.cljs +#, unused +msgid "workspace.libraries.library" +msgstr "라이브러리" + +#: src/app/main/ui/workspace/libraries.cljs:487 +msgid "workspace.libraries.library-updates" +msgstr "라이브러리 업데이트" + +#: src/app/main/ui/workspace/libraries.cljs:381 +msgid "workspace.libraries.loading" +msgstr "로딩 중…" + +#: src/app/main/ui/workspace/libraries.cljs:387 +#, unused +msgid "workspace.libraries.more-templates" +msgstr "더 많은 템플릿을 찾아보세요 " + +#: src/app/main/ui/workspace/libraries.cljs:485 +msgid "workspace.libraries.no-libraries-need-sync" +msgstr "업데이트가 필요한 공유 라이브러리가 없습니다" + +#: src/app/main/ui/workspace/libraries.cljs:399 +msgid "workspace.libraries.no-matches-for" +msgstr "\"%s\"에 대한 검색 결과가 없습니다" + +#: src/app/main/ui/workspace/libraries.cljs:356 +msgid "workspace.libraries.search-shared-libraries" +msgstr "공유 라이브러리 검색" + +#: src/app/main/ui/workspace/libraries.cljs:352 +msgid "workspace.libraries.shared-libraries" +msgstr "공유 라이브러리" + +#: src/app/main/ui/workspace/libraries.cljs:372 +msgid "workspace.libraries.shared-library-btn" +msgstr "라이브러리 연결" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:332 +msgid "workspace.libraries.text.multiple-typography" +msgstr "여러 타이포그래피" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:335 +msgid "workspace.libraries.text.multiple-typography-tooltip" +msgstr "모든 타이포그래피 연결 해제" + +#: src/app/main/ui/workspace/libraries.cljs:103, src/app/main/ui/workspace/libraries.cljs:130 +msgid "workspace.libraries.typography" +msgid_plural "workspace.libraries.typography" +msgstr[0] "타이포그래피 %s개" + +#: src/app/main/ui/workspace/libraries.cljs:343 +msgid "workspace.libraries.unlink-library-btn" +msgstr "라이브러리 연결 해제" + +#: src/app/main/ui/workspace/libraries.cljs:507 +msgid "workspace.libraries.update" +msgstr "업데이트" + +#: src/app/main/ui/workspace/libraries.cljs:583 +msgid "workspace.libraries.update.see-all-changes" +msgstr "모든 변경 사항 보기" + +#: src/app/main/ui/workspace/libraries.cljs:630 +msgid "workspace.libraries.updates" +msgstr "업데이트" + +#: src/app/main/ui/ds/notifications/shared/notification_pill.cljs:67, src/app/main/ui/ds/notifications/shared/notification_pill.cljs:72 +msgid "workspace.notification-pill.detail" +msgstr "세부정보" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:784 +msgid "workspace.options.add-interaction" +msgstr "+ 버튼을 클릭하여 인터랙션을 추가하세요." + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:96 +msgid "workspace.options.blur-options.add-blur" +msgstr "블러 추가" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:119 +msgid "workspace.options.blur-options.remove-blur" +msgstr "블러 제거" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:92, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:112 +msgid "workspace.options.blur-options.title" +msgstr "블러" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:91 +msgid "workspace.options.blur-options.title.group" +msgstr "그룹 블러" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:90 +msgid "workspace.options.blur-options.title.multiple" +msgstr "선택 블러" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:115 +msgid "workspace.options.blur-options.toggle-blur" +msgstr "블러 전환" + +#: src/app/main/ui/workspace/sidebar/options/page.cljs:42, src/app/main/ui/workspace/sidebar/options/page.cljs:50 +msgid "workspace.options.canvas-background" +msgstr "캔버스 배경" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:567 +msgid "workspace.options.clip-content" +msgstr "콘텐츠 클리핑" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1027, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1033, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1282 +msgid "workspace.options.component" +msgstr "컴포넌트" + +#: src/app/main/ui/inspect/annotation.cljs:19, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:194 +msgid "workspace.options.component.annotation" +msgstr "주석" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1041 +msgid "workspace.options.component.copy" +msgstr "복사" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:187 +msgid "workspace.options.component.create-annotation" +msgstr "주석 만들기" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:186 +msgid "workspace.options.component.edit-annotation" +msgstr "주석 편집" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1040, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1286 +msgid "workspace.options.component.main" +msgstr "메인" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:781 +msgid "workspace.options.component.swap" +msgstr "컴포넌트 교체" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:820 +msgid "workspace.options.component.swap.empty" +msgstr "이 라이브러리에 에셋이 아직 없습니다" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1066 +msgid "workspace.options.component.unlinked" +msgstr "연결 해제됨" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:570 +msgid "workspace.options.component.variant.duplicated.copy.locate" +msgstr "충돌하는 베리언트 위치 찾기" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:567 +msgid "workspace.options.component.variant.duplicated.copy.title" +msgstr "" +"이 컴포넌트에 충돌하는 베리언트가 있습니다. 각 베리언트가 고유한 속성 조합을 " +"갖도록 하세요." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1341 +msgid "workspace.options.component.variant.duplicated.group.locate" +msgstr "중복된 베리언트 위치 찾기" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1338 +msgid "workspace.options.component.variant.duplicated.group.title" +msgstr "일부 베리언트의 속성과 값이 동일합니다" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:267 +msgid "workspace.options.component.variant.duplicated.single.all" +msgstr "" +"이 베리언트들은 속성과 값이 동일합니다. 각각 고유하게 식별될 수 있도록 값을 " +"조정하세요." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:264 +msgid "workspace.options.component.variant.duplicated.single.one" +msgstr "" +"이 베리언트는 다른 베리언트와 속성 및 값이 동일합니다. 고유하게 식별될 수 " +"있도록 값을 조정하세요." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:270 +msgid "workspace.options.component.variant.duplicated.single.some" +msgstr "" +"일부 베리언트의 속성과 값이 동일합니다. 각각 고유하게 식별될 수 있도록 값을 " +"조정하세요." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:557 +msgid "workspace.options.component.variant.malformed.copy" +msgstr "" +"이 컴포넌트에 이름이 유효하지 않은 베리언트가 있습니다. 올바른 형식을 " +"따르세요." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1331 +msgid "workspace.options.component.variant.malformed.group.locate" +msgstr "유효하지 않은 베리언트 위치 찾기" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1328 +msgid "workspace.options.component.variant.malformed.group.title" +msgstr "일부 베리언트의 이름이 유효하지 않습니다" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:560 +msgid "workspace.options.component.variant.malformed.locate" +msgstr "유효하지 않은 베리언트 위치 찾기" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:251 +msgid "workspace.options.component.variant.malformed.single.all" +msgstr "이 베리언트들의 이름이 유효하지 않습니다." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:248 +msgid "workspace.options.component.variant.malformed.single.one" +msgstr "이 베리언트의 이름이 유효하지 않습니다." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:254 +msgid "workspace.options.component.variant.malformed.single.some" +msgstr "일부 베리언트의 이름이 유효하지 않습니다." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:433 +msgid "workspace.options.component.variant.malformed.structure.example" +msgstr "[속성]=[값], [속성]=[값]" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:431 +msgid "workspace.options.component.variant.malformed.structure.title" +msgstr "다음 구조를 사용해 보세요:" + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:54 +msgid "workspace.options.component.variants-help-modal.intro" +msgstr "" +"베리언트 간 전환 시 변경 사항을 유지하려면, Penpot은 다음 조건을 만족하는 " +"레이어를 연결합니다:" + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:91 +msgid "workspace.options.component.variants-help-modal.outro" +msgstr "" +"이 중 하나를 변경(예: 레이어 이름 바꾸기 또는 그룹화)하면 연결이 끊어지지만 " +"변경을 되돌리면 복원됩니다." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:67 +msgid "workspace.options.component.variants-help-modal.rule1" +msgstr "같은 이름을 사용합니다." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:76 +msgid "workspace.options.component.variants-help-modal.rule2" +msgstr "같은 유형을 사용합니다." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:77 +msgid "workspace.options.component.variants-help-modal.rule2.detail" +msgstr "직사각형, 타원, 경로 및 불린 연산 항목들은 동일한 유형으로 간주됩니다." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:87 +msgid "workspace.options.component.variants-help-modal.rule3" +msgstr "계층 수준이 동일합니다." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:88 +msgid "workspace.options.component.variants-help-modal.rule3.detail" +msgstr "그룹, 보드, 레이아웃은 동일한 것으로 간주됩니다." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1045, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1289, src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:47 +msgid "workspace.options.component.variants-help-modal.title" +msgstr "베리언트 연결 방식" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:164 +msgid "workspace.options.constraints" +msgstr "제약 조건" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:151 +msgid "workspace.options.constraints.bottom" +msgstr "아래쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:142, src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:153 +msgid "workspace.options.constraints.center" +msgstr "중앙" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:224 +msgid "workspace.options.constraints.fix-when-scrolling" +msgstr "스크롤 시 고정" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:139 +msgid "workspace.options.constraints.left" +msgstr "왼쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:141 +msgid "workspace.options.constraints.leftright" +msgstr "좌우" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:140 +msgid "workspace.options.constraints.right" +msgstr "오른쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:143, src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:154 +msgid "workspace.options.constraints.scale" +msgstr "크기 조절" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:150 +msgid "workspace.options.constraints.top" +msgstr "위쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs:152 +msgid "workspace.options.constraints.topbottom" +msgstr "위아래" + +#: src/app/main/ui/workspace/sidebar/options.cljs:197 +msgid "workspace.options.design" +msgstr "디자인" + +#: src/app/main/ui/inspect/exports.cljs:140 +msgid "workspace.options.export" +msgstr "내보내기" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/inspect/exports.cljs +#, unused +msgid "workspace.options.export-multiple" +msgstr "선택 영역 내보내기" + +#: src/app/main/ui/inspect/exports.cljs:196, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs:273 +msgid "workspace.options.export-object" +msgid_plural "workspace.options.export-object" +msgstr[0] "요소 %s개 내보내기" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs:214 +msgid "workspace.options.export.add-export" +msgstr "내보내기 추가" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs:226, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs:261 +msgid "workspace.options.export.remove-export" +msgstr "내보내기 제거" + +#: src/app/main/ui/inspect/exports.cljs:179, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs:255 +msgid "workspace.options.export.suffix" +msgstr "접미사" + +#: src/app/main/ui/exports/assets.cljs:250 +msgid "workspace.options.exporting-complete" +msgstr "내보내기 완료" + +#: src/app/main/ui/exports/assets.cljs:171, src/app/main/ui/exports/assets.cljs:251, src/app/main/ui/inspect/exports.cljs:195, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs:272 +msgid "workspace.options.exporting-object" +msgstr "내보내는 중…" + +#: src/app/main/ui/exports/assets.cljs:249 +msgid "workspace.options.exporting-object-error" +msgstr "내보내기 실패" + +#: src/app/main/ui/exports/assets.cljs:252 +msgid "workspace.options.exporting-object-slow" +msgstr "내보내기가 예상보다 느립니다" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:107, src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:236 +msgid "workspace.options.fill" +msgstr "채우기" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:208 +msgid "workspace.options.fill.add-fill" +msgstr "채우기 추가" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:223 +msgid "workspace.options.fill.remove-fill" +msgstr "채우기 제거" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:405 +msgid "workspace.options.fit-content" +msgstr "콘텐츠에 맞게 보드 크기 조절" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:704 +msgid "workspace.options.flows.add-flow-start" +msgstr "플로우 시작 추가" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:700 +msgid "workspace.options.flows.flow" +msgstr "플로우" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:164 +msgid "workspace.options.flows.flow-start" +msgstr "플로우 시작" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:672 +msgid "workspace.options.flows.flow-starts" +msgstr "플로우 시작" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:155 +#, unused +msgid "workspace.options.flows.remove-flow" +msgstr "플로우 제거" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:32 +msgid "workspace.options.grid.auto" +msgstr "자동" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:163 +msgid "workspace.options.grid.column" +msgstr "열" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +#, unused +msgid "workspace.options.grid.grid-title" +msgstr "그리드" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:204, src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:247 +msgid "workspace.options.grid.params.color" +msgstr "색상" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +#, unused +msgid "workspace.options.grid.params.columns" +msgstr "열" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:270 +msgid "workspace.options.grid.params.gutter" +msgstr "거터" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:257 +msgid "workspace.options.grid.params.height" +msgstr "높이" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:281 +msgid "workspace.options.grid.params.margin" +msgstr "마진" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +#, unused +msgid "workspace.options.grid.params.rows" +msgstr "행" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:226, src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:302 +msgid "workspace.options.grid.params.set-default" +msgstr "기본값으로 설정" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +#, unused +msgid "workspace.options.grid.params.size" +msgstr "크기" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +#, unused +msgid "workspace.options.grid.params.type" +msgstr "유형" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:241 +msgid "workspace.options.grid.params.type.bottom" +msgstr "아래쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:239 +msgid "workspace.options.grid.params.type.center" +msgstr "중앙" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:238 +msgid "workspace.options.grid.params.type.left" +msgstr "왼쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:242 +msgid "workspace.options.grid.params.type.right" +msgstr "오른쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:235 +msgid "workspace.options.grid.params.type.stretch" +msgstr "늘이기" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:237 +msgid "workspace.options.grid.params.type.top" +msgstr "위쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:221, src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:300 +msgid "workspace.options.grid.params.use-default" +msgstr "기본값 사용" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:258 +msgid "workspace.options.grid.params.width" +msgstr "너비" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:164 +msgid "workspace.options.grid.row" +msgstr "행" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:162 +msgid "workspace.options.grid.square" +msgstr "사각형" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:106 +msgid "workspace.options.group-fill" +msgstr "그룹 채우기" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:45 +msgid "workspace.options.group-stroke" +msgstr "그룹 선" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:332 +msgid "workspace.options.guides.add-guide" +msgstr "가이드 추가" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:191 +msgid "workspace.options.guides.remove-guide" +msgstr "가이드 제거" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:329 +msgid "workspace.options.guides.title" +msgstr "가이드" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:187 +msgid "workspace.options.guides.toggle-guide" +msgstr "가이드 전환" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:435, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:454 +msgid "workspace.options.height" +msgstr "높이" + +#: src/app/main/ui/workspace/sidebar/options.cljs:201 +msgid "workspace.options.inspect" +msgstr "검사" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:438 +msgid "workspace.options.interaction-action" +msgstr "동작" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:43, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:344 +msgid "workspace.options.interaction-after-delay" +msgstr "지연 후" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:578 +msgid "workspace.options.interaction-animation" +msgstr "애니메이션" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:616 +msgid "workspace.options.interaction-animation-direction-down" +msgstr "아래" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:593 +msgid "workspace.options.interaction-animation-direction-in" +msgstr "들어오기" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:612 +msgid "workspace.options.interaction-animation-direction-left" +msgstr "왼쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:596 +msgid "workspace.options.interaction-animation-direction-out" +msgstr "나가기" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:608 +msgid "workspace.options.interaction-animation-direction-right" +msgstr "오른쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:620 +msgid "workspace.options.interaction-animation-direction-up" +msgstr "위로" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:383 +msgid "workspace.options.interaction-animation-dissolve" +msgstr "디졸브" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:382 +msgid "workspace.options.interaction-animation-none" +msgstr "없음" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:389 +msgid "workspace.options.interaction-animation-push" +msgstr "밀기" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:384 +msgid "workspace.options.interaction-animation-slide" +msgstr "슬라이드" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:368 +msgid "workspace.options.interaction-auto" +msgstr "자동" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:568 +msgid "workspace.options.interaction-background" +msgstr "배경 오버레이 추가" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:561 +msgid "workspace.options.interaction-close-outside" +msgstr "외부 클릭 시 닫기" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:349 +msgid "workspace.options.interaction-close-overlay" +msgstr "오버레이 닫기" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:58 +msgid "workspace.options.interaction-close-overlay-dest" +msgstr "오버레이 닫기: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:426 +msgid "workspace.options.interaction-delay" +msgstr "지연" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:449 +msgid "workspace.options.interaction-destination" +msgstr "목적지" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:628 +msgid "workspace.options.interaction-duration" +msgstr "지속 시간" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:641 +msgid "workspace.options.interaction-easing" +msgstr "이징" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:393 +msgid "workspace.options.interaction-easing-ease" +msgstr "기본(ease)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:394 +msgid "workspace.options.interaction-easing-ease-in" +msgstr "감가속(In)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:396 +msgid "workspace.options.interaction-easing-ease-in-out" +msgstr "감가감속(In-Out)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:395 +msgid "workspace.options.interaction-easing-ease-out" +msgstr "가감속(Out)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:392 +msgid "workspace.options.interaction-easing-linear" +msgstr "선형(Linear)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +#, unused +msgid "workspace.options.interaction-in" +msgstr "들어오기" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:41, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:341 +msgid "workspace.options.interaction-mouse-enter" +msgstr "마우스 진입 시" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:42, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:342 +msgid "workspace.options.interaction-mouse-leave" +msgstr "마우스 이탈 시" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:430, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:632 +msgid "workspace.options.interaction-ms" +msgstr "ms" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:346 +msgid "workspace.options.interaction-navigate-to" +msgstr "이동" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:52 +msgid "workspace.options.interaction-navigate-to-dest" +msgstr "이동: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:53, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:55, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:57, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:357 +msgid "workspace.options.interaction-none" +msgstr "(설정 안 함)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:654 +msgid "workspace.options.interaction-offset-effect" +msgstr "오프셋 효과" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:37, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:337 +msgid "workspace.options.interaction-on-click" +msgstr "클릭 시" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:347 +msgid "workspace.options.interaction-open-overlay" +msgstr "오버레이 열기" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:54 +msgid "workspace.options.interaction-open-overlay-dest" +msgstr "오버레이 열기: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:61, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:351 +msgid "workspace.options.interaction-open-url" +msgstr "URL 열기" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +#, unused +msgid "workspace.options.interaction-out" +msgstr "나가기" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:380, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:554 +msgid "workspace.options.interaction-pos-bottom-center" +msgstr "아래 가운데" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:378, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:538 +msgid "workspace.options.interaction-pos-bottom-left" +msgstr "아래 왼쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:379, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:546 +msgid "workspace.options.interaction-pos-bottom-right" +msgstr "아래 오른쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:374, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:506 +msgid "workspace.options.interaction-pos-center" +msgstr "중앙" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:373 +msgid "workspace.options.interaction-pos-manual" +msgstr "수동" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:377, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:530 +msgid "workspace.options.interaction-pos-top-center" +msgstr "위 가운데" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:375, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:514 +msgid "workspace.options.interaction-pos-top-left" +msgstr "위 왼쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:376, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:522 +msgid "workspace.options.interaction-pos-top-right" +msgstr "위 오른쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:492 +msgid "workspace.options.interaction-position" +msgstr "위 오른쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:460 +msgid "workspace.options.interaction-preserve-scroll" +msgstr "스크롤 위치 유지" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:60, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:350 +msgid "workspace.options.interaction-prev-screen" +msgstr "이전 화면" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:482 +msgid "workspace.options.interaction-relative-to" +msgstr "기준:" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:59, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:356, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:370, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:371 +msgid "workspace.options.interaction-self" +msgstr "자기 자신" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:348 +msgid "workspace.options.interaction-toggle-overlay" +msgstr "오버레이 전환" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:56 +msgid "workspace.options.interaction-toggle-overlay-dest" +msgstr "오버레이 전환: %s" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:415 +msgid "workspace.options.interaction-trigger" +msgstr "트리거" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:469 +msgid "workspace.options.interaction-url" +msgstr "URL" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:39, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:339 +msgid "workspace.options.interaction-while-hovering" +msgstr "호버 시" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:40, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:340 +msgid "workspace.options.interaction-while-pressing" +msgstr "누르는 동안" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:743 +msgid "workspace.options.interactions" +msgstr "인터랙션" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:746 +msgid "workspace.options.interactions.add-interaction" +msgstr "인터랙션 추가" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +#, unused +msgid "workspace.options.interactions.remove-interaction" +msgstr "인터랙션 제거" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:197 +msgid "workspace.options.layer-options.blend-mode.color" +msgstr "색상" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:186 +msgid "workspace.options.layer-options.blend-mode.color-burn" +msgstr "색상 번" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:189 +msgid "workspace.options.layer-options.blend-mode.color-dodge" +msgstr "색상 닷지" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:184 +msgid "workspace.options.layer-options.blend-mode.darken" +msgstr "어둡게" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:193 +msgid "workspace.options.layer-options.blend-mode.difference" +msgstr "차이" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:194 +msgid "workspace.options.layer-options.blend-mode.exclusion" +msgstr "제외" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:192 +msgid "workspace.options.layer-options.blend-mode.hard-light" +msgstr "하드 라이트" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:195 +msgid "workspace.options.layer-options.blend-mode.hue" +msgstr "색조" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:187 +msgid "workspace.options.layer-options.blend-mode.lighten" +msgstr "밝게" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:198 +msgid "workspace.options.layer-options.blend-mode.luminosity" +msgstr "광도" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:185 +msgid "workspace.options.layer-options.blend-mode.multiply" +msgstr "곱하기" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:183 +msgid "workspace.options.layer-options.blend-mode.normal" +msgstr "보통" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:190 +msgid "workspace.options.layer-options.blend-mode.overlay" +msgstr "오버레이" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:196 +msgid "workspace.options.layer-options.blend-mode.saturation" +msgstr "채도" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:188 +msgid "workspace.options.layer-options.blend-mode.screen" +msgstr "스크린" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:191 +msgid "workspace.options.layer-options.blend-mode.soft-light" +msgstr "소프트 라이트" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +#, unused +msgid "workspace.options.layer-options.title" +msgstr "레이어" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +#, unused +msgid "workspace.options.layer-options.title.group" +msgstr "레이어 그룹 만들기" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +#, unused +msgid "workspace.options.layer-options.title.multiple" +msgstr "선택된 레이어" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:255, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:261 +msgid "workspace.options.layer-options.toggle-layer" +msgstr "레이어 표시 전환" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout-item.advanced-ops" +msgstr "고급 옵션" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:686, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:693 +msgid "workspace.options.layout-item.layout-item-max-h" +msgstr "최대 높이" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:624, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:631 +msgid "workspace.options.layout-item.layout-item-max-w" +msgstr "최대 너비" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:655, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:663 +msgid "workspace.options.layout-item.layout-item-min-h" +msgstr "최소 높이" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:591, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:600 +msgid "workspace.options.layout-item.layout-item-min-w" +msgstr "최소 너비" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout-item.title.layout-item-max-h" +msgstr "최대 높이" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout-item.title.layout-item-max-w" +msgstr "최대 너비" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout-item.title.layout-item-min-h" +msgstr "최소 높이" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout-item.title.layout-item-min-w" +msgstr "최소 너비" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.bottom" +msgstr "아래쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.direction.column" +msgstr "열" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.direction.column-reverse" +msgstr "열 반전" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.direction.row" +msgstr "행" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.direction.row-reverse" +msgstr "행 반전" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.gap" +msgstr "간격" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.left" +msgstr "왼쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout.margin" +msgstr "마진" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout.margin-all" +msgstr "전체" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +#, unused +msgid "workspace.options.layout.margin-simple" +msgstr "단일 마진" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.packed" +msgstr "촘촘히 배치" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.padding" +msgstr "패딩" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.padding-all" +msgstr "전체" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.padding-simple" +msgstr "단일 패딩" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.right" +msgstr "오른쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.space-around" +msgstr "여백 포함 배분" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.space-between" +msgstr "균등 배분" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +#, unused +msgid "workspace.options.layout.top" +msgstr "위쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:241 +msgid "workspace.options.more-colors" +msgstr "색상 더보기" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:223 +msgid "workspace.options.more-lib-colors" +msgstr "라이브러리 색상 더보기" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:265 +msgid "workspace.options.more-token-colors" +msgstr "색상 token 더보기" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:229, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:241 +msgid "workspace.options.opacity" +msgstr "불투명도" + +#: src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs:108, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:401 +msgid "workspace.options.orientation.horizontal" +msgstr "가로" + +#: src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs:104, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:397 +msgid "workspace.options.orientation.vertical" +msgstr "세로" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +#, unused +msgid "workspace.options.position" +msgstr "위치" + +#: src/app/main/ui/workspace/sidebar/options.cljs:199 +msgid "workspace.options.prototype" +msgstr "프로토타입" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:182, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:206 +msgid "workspace.options.radius" +msgstr "반지름" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:271, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:323 +msgid "workspace.options.radius-bottom-left" +msgstr "왼쪽 아래" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:290, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:331 +msgid "workspace.options.radius-bottom-right" +msgstr "오른쪽 아래" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:234, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:307 +msgid "workspace.options.radius-top-left" +msgstr "왼쪽 위" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:253, src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:315 +msgid "workspace.options.radius-top-right" +msgstr "오른쪽 위" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:340 +msgid "workspace.options.radius.hide-all-corners" +msgstr "개별 반경 통합" + +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:341 +msgid "workspace.options.radius.show-single-corners" +msgstr "개별 반경 표시" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:191 +msgid "workspace.options.recent-fonts" +msgstr "최근" + +#: src/app/main/ui/exports/assets.cljs:298 +msgid "workspace.options.retry" +msgstr "다시 시도" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:536, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:544 +msgid "workspace.options.rotation" +msgstr "회전" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:188 +msgid "workspace.options.search-font" +msgstr "글꼴 검색" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:786 +msgid "workspace.options.select-a-shape" +msgstr "도형, 보드 또는 그룹을 선택하여 다른 보드로 연결하세요." + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:201 +msgid "workspace.options.selection-color" +msgstr "선택 색상" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:105 +msgid "workspace.options.selection-fill" +msgstr "선택 채우기" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:44 +msgid "workspace.options.selection-stroke" +msgstr "선택 선" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs:144 +msgid "workspace.options.shadow-options.add-shadow" +msgstr "그림자 추가" + +#: src/app/main/ui/inspect/attributes/shadow.cljs:47, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:181, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:183 +msgid "workspace.options.shadow-options.blur" +msgstr "블러" + +#: src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:211 +msgid "workspace.options.shadow-options.color" +msgstr "그림자 색상" + +#: src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:122 +msgid "workspace.options.shadow-options.drop-shadow" +msgstr "그림자 효과" + +#: src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:123 +msgid "workspace.options.shadow-options.inner-shadow" +msgstr "내부 그림자" + +#: src/app/main/ui/inspect/attributes/shadow.cljs:45, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:172 +msgid "workspace.options.shadow-options.offsetx" +msgstr "X" + +#: src/app/main/ui/inspect/attributes/shadow.cljs:46, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:201 +msgid "workspace.options.shadow-options.offsety" +msgstr "Y" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs:157, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:161 +msgid "workspace.options.shadow-options.remove-shadow" +msgstr "그림자 제거" + +#: src/app/main/ui/inspect/attributes/shadow.cljs:48, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:191, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:193 +msgid "workspace.options.shadow-options.spread" +msgstr "확산" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs:139 +msgid "workspace.options.shadow-options.title" +msgstr "그림자" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs:138 +msgid "workspace.options.shadow-options.title.group" +msgstr "그룹 그림자" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs:137 +msgid "workspace.options.shadow-options.title.multiple" +msgstr "선택 영역 그림자" + +#: src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:157 +msgid "workspace.options.shadow-options.toggle-shadow" +msgstr "그림자 전환" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:258 +msgid "workspace.options.show-fill-on-export" +msgstr "내보내기에 표시" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:574 +msgid "workspace.options.show-in-viewer" +msgstr "보기 모드에서 표시" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:168 +msgid "workspace.options.size" +msgstr "크기" + +#: src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs:71, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:364 +msgid "workspace.options.size-presets" +msgstr "크기 프리셋" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:469 +msgid "workspace.options.size.lock" +msgstr "비율 잠금" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:469 +msgid "workspace.options.size.unlock" +msgstr "비율 잠금 해제" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:44 +#, unused +msgid "workspace.options.stroke" +msgstr "선" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +#, unused +msgid "workspace.options.stroke-cap.circle-marker" +msgstr "원형 마커" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:175 +msgid "workspace.options.stroke-cap.circle-marker-short" +msgstr "원" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +#, unused +msgid "workspace.options.stroke-cap.diamond-marker" +msgstr "다이아몬드 마커" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:176 +msgid "workspace.options.stroke-cap.diamond-marker-short" +msgstr "다이아몬드" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +#, unused +msgid "workspace.options.stroke-cap.line-arrow" +msgstr "선 화살표" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:172 +msgid "workspace.options.stroke-cap.line-arrow-short" +msgstr "화살표" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:170 +msgid "workspace.options.stroke-cap.none" +msgstr "없음" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:178 +msgid "workspace.options.stroke-cap.round" +msgstr "둥근" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:179 +msgid "workspace.options.stroke-cap.square" +msgstr "사각형" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +#, unused +msgid "workspace.options.stroke-cap.square-marker" +msgstr "사각형 마커" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:174 +msgid "workspace.options.stroke-cap.square-marker-short" +msgstr "직사각형" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +#, unused +msgid "workspace.options.stroke-cap.triangle-arrow" +msgstr "삼각형 화살표" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:173 +msgid "workspace.options.stroke-cap.triangle-arrow-short" +msgstr "삼각형" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:210 +msgid "workspace.options.stroke-color" +msgstr "선 색상" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:225, src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:230 +msgid "workspace.options.stroke-width" +msgstr "선 두께" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:189 +msgid "workspace.options.stroke.add-stroke" +msgstr "선 색상 추가" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:111 +msgid "workspace.options.stroke.center" +msgstr "중앙" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:139 +msgid "workspace.options.stroke.dashed" +msgstr "점선" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:138 +msgid "workspace.options.stroke.dotted" +msgstr "점선" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:112 +msgid "workspace.options.stroke.inner" +msgstr "안쪽" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:140 +msgid "workspace.options.stroke.mixed" +msgstr "혼합" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:113 +msgid "workspace.options.stroke.outer" +msgstr "바깥쪽" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:202 +msgid "workspace.options.stroke.remove-stroke" +msgstr "선 제거" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:137 +msgid "workspace.options.stroke.solid" +msgstr "단색" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:127 +msgid "workspace.options.text-options.align-bottom" +msgstr "아래쪽 정렬" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:123 +msgid "workspace.options.text-options.align-middle" +msgstr "중간 정렬" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:119 +msgid "workspace.options.text-options.align-top" +msgstr "위쪽 정렬" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:92 +msgid "workspace.options.text-options.direction-ltr" +msgstr "LTR" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:96 +msgid "workspace.options.text-options.direction-rtl" +msgstr "RTL" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:165 +msgid "workspace.options.text-options.grow-auto-height" +msgstr "자동 높이" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:161 +msgid "workspace.options.text-options.grow-auto-width" +msgstr "자동 너비" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:157 +msgid "workspace.options.text-options.grow-fixed" +msgstr "고정됨" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:400 +msgid "workspace.options.text-options.letter-spacing" +msgstr "자간" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:381 +msgid "workspace.options.text-options.line-height" +msgstr "줄 높이" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#, unused +msgid "workspace.options.text-options.lowercase" +msgstr "소문자" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#, unused +msgid "workspace.options.text-options.none" +msgstr "없음" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:192 +msgid "workspace.options.text-options.strikethrough" +msgstr "취소선 (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:61 +msgid "workspace.options.text-options.text-align-center" +msgstr "중앙 정렬" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:69 +msgid "workspace.options.text-options.text-align-justify" +msgstr "양쪽 정렬" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:57 +msgid "workspace.options.text-options.text-align-left" +msgstr "왼쪽 정렬" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:65 +msgid "workspace.options.text-options.text-align-right" +msgstr "오른쪽 정렬" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:205 +msgid "workspace.options.text-options.title" +msgstr "텍스트" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:204 +msgid "workspace.options.text-options.title-group" +msgstr "그룹 텍스트" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:203 +msgid "workspace.options.text-options.title-selection" +msgstr "그룹 텍스트" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#, unused +msgid "workspace.options.text-options.titlecase" +msgstr "단어 첫 글자 대문자" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs:188 +msgid "workspace.options.text-options.underline" +msgstr "밑줄 (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +#, unused +msgid "workspace.options.text-options.uppercase" +msgstr "대문자" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:788 +msgid "workspace.options.use-play-button" +msgstr "헤더의 재생 버튼을 클릭해 프로토타입 보기를 실행하세요." + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:420, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:442 +msgid "workspace.options.width" +msgstr "너비" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:482, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:505 +msgid "workspace.options.x" +msgstr "X축" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:495, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:516 +msgid "workspace.options.y" +msgstr "Y축" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:140 +msgid "workspace.path.actions.add-node" +msgstr "노드 추가 (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:148 +msgid "workspace.path.actions.delete-node" +msgstr "노드 삭제 (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:123 +msgid "workspace.path.actions.draw-nodes" +msgstr "노드 그리기 (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:166 +msgid "workspace.path.actions.join-nodes" +msgstr "노드 연결 (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:184 +msgid "workspace.path.actions.make-corner" +msgstr "직선으로 변환 (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:192 +msgid "workspace.path.actions.make-curve" +msgstr "곡선으로 변환 (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:158 +msgid "workspace.path.actions.merge-nodes" +msgstr "노드 병합 (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:131 +msgid "workspace.path.actions.move-nodes" +msgstr "노드 이동 (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:174 +msgid "workspace.path.actions.separate-nodes" +msgstr "노드 분리 (%s)" + +#: src/app/main/ui/workspace/viewport/path_actions.cljs:203 +msgid "workspace.path.actions.snap-nodes" +msgstr "노드 스냅 (%s)" + +#: src/app/main/ui/workspace/plugins.cljs:85 +msgid "workspace.plugins.button-open" +msgstr "열기" + +#: src/app/main/ui/workspace/plugins.cljs:199 +#, markdown +msgid "workspace.plugins.discover" +msgstr "[더 많은 플러그인](%s) 찾아보기" + +#: src/app/main/ui/workspace/plugins.cljs:206 +msgid "workspace.plugins.empty-plugins" +msgstr "아직 설치된 플러그인이 없습니다" + +#: src/app/main/ui/workspace/plugins.cljs:193 +msgid "workspace.plugins.error.manifest" +msgstr "플러그인 매니페스트가 올바르지 않습니다." + +#: src/app/main/data/plugins.cljs:105, src/app/main/ui/workspace/main_menu.cljs:766, src/app/main/ui/workspace/plugins.cljs:84 +msgid "workspace.plugins.error.need-editor" +msgstr "이 플러그인을 사용하려면 편집자여야 합니다" + +#: src/app/main/ui/workspace/plugins.cljs:189 +msgid "workspace.plugins.error.url" +msgstr "플러그인이 존재하지 않거나 URL이 올바르지 않습니다." + +#: src/app/main/ui/workspace/plugins.cljs:185 +msgid "workspace.plugins.install" +msgstr "설치" + +#: src/app/main/ui/workspace/plugins.cljs:215 +msgid "workspace.plugins.installed-plugins" +msgstr "설치된 플러그인" + +#: src/app/main/ui/workspace/main_menu.cljs:721 +msgid "workspace.plugins.menu.plugins-manager" +msgstr "플러그인 관리자" + +#: src/app/main/ui/workspace/main_menu.cljs:918 +msgid "workspace.plugins.menu.title" +msgstr "플러그인" + +#: src/app/main/ui/workspace/plugins.cljs:376 +msgid "workspace.plugins.permissions-update.title" +msgstr "이 플러그인 업데이트" + +#: src/app/main/ui/workspace/plugins.cljs:380 +msgid "workspace.plugins.permissions-update.warning" +msgstr "" +"마지막으로 열었던 이후 플러그인이 수정되었습니다. 이제 다음에도 접근하려 " +"합니다:" + +#: src/app/main/ui/workspace/plugins.cljs:280 +msgid "workspace.plugins.permissions.allow-download" +msgstr "파일 다운로드 시작." + +#: src/app/main/ui/workspace/plugins.cljs:287 +msgid "workspace.plugins.permissions.allow-localstorage" +msgstr "브라우저에 데이터를 저장합니다." + +#: src/app/main/ui/workspace/plugins.cljs:273 +msgid "workspace.plugins.permissions.comment-read" +msgstr "댓글과 답글을 읽습니다." + +#: src/app/main/ui/workspace/plugins.cljs:267 +msgid "workspace.plugins.permissions.comment-write" +msgstr "댓글을 읽고 수정하며 귀하의 이름으로 답글을 답니다." + +#: src/app/main/ui/workspace/plugins.cljs:240 +msgid "workspace.plugins.permissions.content-read" +msgstr "사용자가 접근할 수 있는 파일의 내용을 읽습니다." + +#: src/app/main/ui/workspace/plugins.cljs:234 +msgid "workspace.plugins.permissions.content-write" +msgstr "사용자가 접근할 수 있는 파일의 내용을 읽고 수정합니다." + +#: src/app/main/ui/workspace/plugins.cljs:327 +msgid "workspace.plugins.permissions.disclaimer" +msgstr "" +"이 플러그인은 외부 개발자가 만들었습니다. 접근 권한을 부여하기 전에 신뢰할 " +"수 있는지 확인하세요. 개인정보 보호 및 보안이 중요합니다. 문의 사항은 " +"지원팀에 연락하세요." + +#: src/app/main/ui/workspace/plugins.cljs:260 +msgid "workspace.plugins.permissions.library-read" +msgstr "라이브러리와 에셋을 읽습니다." + +#: src/app/main/ui/workspace/plugins.cljs:254 +msgid "workspace.plugins.permissions.library-write" +msgstr "라이브러리와 에셋을 읽고 수정합니다." + +#: src/app/main/ui/workspace/plugins.cljs:320 +msgid "workspace.plugins.permissions.title" +msgstr "'%s' 플러그인이 다음에 접근하려 합니다:" + +#: src/app/main/ui/workspace/plugins.cljs:247 +msgid "workspace.plugins.permissions.user-read" +msgstr "현재 사용자의 프로필 정보를 읽습니다." + +#: src/app/main/ui/workspace/plugins.cljs:211 +msgid "workspace.plugins.plugin-list-link" +msgstr "플러그인 목록" + +#: src/app/main/ui/workspace/plugins.cljs:88 +msgid "workspace.plugins.remove-plugin" +msgstr "플러그인 제거" + +#: src/app/main/ui/workspace/plugins.cljs:180 +msgid "workspace.plugins.search-placeholder" +msgstr "플러그인 URL로 검색" + +#, unused +msgid "workspace.plugins.success" +msgstr "플러그인이 올바르게 로드되었습니다." + +#: src/app/main/ui/workspace/plugins.cljs:174 +msgid "workspace.plugins.title" +msgstr "플러그인" + +#: src/app/main/ui/workspace/plugins.cljs:440 +msgid "workspace.plugins.try-out.cancel" +msgstr "나중에" + +#: src/app/main/ui/workspace/plugins.cljs:433 +msgid "workspace.plugins.try-out.message" +msgstr "" +"살펴보시겠습니까? 현재 팀의 새 초안에서 열립니다. (그렇지 않으면 언제든지 " +"파일의 설치된 플러그인에서 찾을 수 있습니다.)" + +#: src/app/main/ui/workspace/plugins.cljs:429 +msgid "workspace.plugins.try-out.title" +msgstr "'%s' 플러그인이 설치되었습니다!" + +#: src/app/main/ui/workspace/plugins.cljs:446 +msgid "workspace.plugins.try-out.try" +msgstr "플러그인 사용해보기" + +#: src/app/main/ui/workspace/context_menu.cljs:559 +msgid "workspace.shape.menu.add-flex" +msgstr "플렉스 레이아웃 추가" + +#: src/app/main/ui/workspace/context_menu.cljs:563 +msgid "workspace.shape.menu.add-grid" +msgstr "그리드 레이아웃 추가" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1248, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1272 +msgid "workspace.shape.menu.add-layout" +msgstr "레이아웃 추가" + +#: src/app/main/ui/workspace/context_menu.cljs:612, src/app/main/ui/workspace/sidebar/assets/common.cljs:508, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1051, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1219, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1293 +msgid "workspace.shape.menu.add-variant" +msgstr "베리언트 생성" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:512, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1073, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1221, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1307 +msgid "workspace.shape.menu.add-variant-property" +msgstr "새 속성 추가" + +#: src/app/main/ui/workspace/context_menu.cljs:282 +msgid "workspace.shape.menu.back" +msgstr "맨 뒤로" + +#: src/app/main/ui/workspace/context_menu.cljs:279 +msgid "workspace.shape.menu.backward" +msgstr "뒤로" + +#: src/app/main/ui/workspace/context_menu.cljs:619, src/app/main/ui/workspace/sidebar/assets/components.cljs:633, src/app/main/ui/workspace/sidebar/assets/groups.cljs:75, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1106 +msgid "workspace.shape.menu.combine-as-variants" +msgstr "베리언트로 결합" + +#: src/app/main/ui/workspace/sidebar/assets/components.cljs:635 +msgid "workspace.shape.menu.combine-as-variants-error" +msgstr "컴포넌트가 같은 페이지에 있어야 합니다" + +#: src/app/main/ui/workspace/context_menu.cljs:200 +msgid "workspace.shape.menu.copy" +msgstr "복사" + +#: src/app/main/ui/workspace/context_menu.cljs:218 +msgid "workspace.shape.menu.copy-css" +msgstr "CSS로 복사" + +#: src/app/main/ui/workspace/context_menu.cljs:220 +msgid "workspace.shape.menu.copy-css-nested" +msgstr "CSS로 복사 (중첩된 레이어 포함)" + +#: src/app/main/ui/workspace/context_menu.cljs:203 +msgid "workspace.shape.menu.copy-link" +msgstr "링크 복사" + +#: src/app/main/ui/workspace/context_menu.cljs:216 +msgid "workspace.shape.menu.copy-paste-as" +msgstr "다른 형식으로 복사/붙여넣기." + +#: src/app/main/ui/workspace/context_menu.cljs:230 +msgid "workspace.shape.menu.copy-props" +msgstr "속성 복사" + +#: src/app/main/ui/workspace/context_menu.cljs:222 +msgid "workspace.shape.menu.copy-svg" +msgstr "SVG로 복사" + +#: src/app/main/ui/workspace/context_menu.cljs:227 +msgid "workspace.shape.menu.copy-text" +msgstr "텍스트로 복사" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:484 +msgid "workspace.shape.menu.create-annotation" +msgstr "주석 추가" + +#: src/app/main/ui/workspace/context_menu.cljs:382 +msgid "workspace.shape.menu.create-artboard-from-selection" +msgstr "선택 항목을 보드로" + +#: src/app/main/ui/workspace/context_menu.cljs:592 +msgid "workspace.shape.menu.create-component" +msgstr "컴포넌트 생성" + +#: src/app/main/ui/workspace/context_menu.cljs:596 +msgid "workspace.shape.menu.create-multiple-components" +msgstr "여러 컴포넌트 생성" + +#: src/app/main/ui/workspace/context_menu.cljs:206 +msgid "workspace.shape.menu.cut" +msgstr "잘라내기" + +#: src/app/main/ui/workspace/context_menu.cljs:629, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1001, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1290 +msgid "workspace.shape.menu.delete" +msgstr "삭제" + +#: src/app/main/ui/workspace/context_menu.cljs:506 +msgid "workspace.shape.menu.delete-flow-start" +msgstr "플로우 시작 삭제" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:489 +msgid "workspace.shape.menu.detach-instance" +msgstr "인스턴스 분리" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:488 +msgid "workspace.shape.menu.detach-instances-in-bulk" +msgstr "인스턴스 모두 분리" + +#: src/app/main/ui/workspace/context_menu.cljs:447, src/app/main/ui/workspace/sidebar/options/menus/bool.cljs:88 +msgid "workspace.shape.menu.difference" +msgstr "차이" + +#: src/app/main/ui/workspace/context_menu.cljs:212 +msgid "workspace.shape.menu.duplicate" +msgstr "복제복제" + +#: src/app/main/ui/workspace/context_menu.cljs:432 +msgid "workspace.shape.menu.edit" +msgstr "편집" + +#: src/app/main/ui/workspace/context_menu.cljs:453, src/app/main/ui/workspace/sidebar/options/menus/bool.cljs:98 +msgid "workspace.shape.menu.exclude" +msgstr "제외" + +#: src/app/main/ui/workspace/context_menu.cljs:437, src/app/main/ui/workspace/context_menu.cljs:460, src/app/main/ui/workspace/sidebar/options/menus/bool.cljs:104 +msgid "workspace.shape.menu.flatten" +msgstr "병합" + +#: src/app/main/ui/workspace/context_menu.cljs:299 +msgid "workspace.shape.menu.flip-horizontal" +msgstr "좌우 반전" + +#: src/app/main/ui/workspace/context_menu.cljs:295 +msgid "workspace.shape.menu.flip-vertical" +msgstr "상하 반전" + +#: src/app/main/ui/workspace/context_menu.cljs:508 +msgid "workspace.shape.menu.flow-start" +msgstr "플로우 시작" + +#: src/app/main/ui/workspace/context_menu.cljs:273 +msgid "workspace.shape.menu.forward" +msgstr "앞으로" + +#: src/app/main/ui/workspace/context_menu.cljs:276 +msgid "workspace.shape.menu.front" +msgstr "맨 앞으로" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +#, unused +msgid "workspace.shape.menu.go-main" +msgstr "메인 컴포넌트 파일로 이동" + +#: src/app/main/ui/workspace/context_menu.cljs:368 +msgid "workspace.shape.menu.group" +msgstr "그룹" + +#: src/app/main/ui/workspace/context_menu.cljs:477, src/app/main/ui/workspace/sidebar/layer_item.cljs:172 +msgid "workspace.shape.menu.hide" +msgstr "숨기기" + +#: src/app/main/ui/workspace/context_menu.cljs:706, src/app/main/ui/workspace/main_menu.cljs:448 +msgid "workspace.shape.menu.hide-ui" +msgstr "UI 표시/숨기기" + +#: src/app/main/ui/workspace/context_menu.cljs:450, src/app/main/ui/workspace/sidebar/options/menus/bool.cljs:93 +msgid "workspace.shape.menu.intersection" +msgstr "교차" + +#: src/app/main/ui/workspace/context_menu.cljs:485, src/app/main/ui/workspace/sidebar/layer_item.cljs:180, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:268 +msgid "workspace.shape.menu.lock" +msgstr "잠금" + +#: src/app/main/ui/workspace/context_menu.cljs:373 +msgid "workspace.shape.menu.mask" +msgstr "마스크" + +#: src/app/main/ui/workspace/context_menu.cljs:209, src/app/main/ui/workspace/context_menu.cljs:703 +msgid "workspace.shape.menu.paste" +msgstr "붙여넣기" + +#: src/app/main/ui/workspace/context_menu.cljs:234 +msgid "workspace.shape.menu.paste-props" +msgstr "속성 붙여넣기" + +#: src/app/main/ui/workspace/context_menu.cljs:443 +msgid "workspace.shape.menu.path" +msgstr "경로" + +#: src/app/main/ui/workspace/context_menu.cljs:549 +msgid "workspace.shape.menu.remove-flex" +msgstr "플렉스 레이아웃 제거" + +#: src/app/main/ui/workspace/context_menu.cljs:552 +msgid "workspace.shape.menu.remove-grid" +msgstr "그리드 레이아웃 제거" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1266 +msgid "workspace.shape.menu.remove-layout" +msgstr "레이아웃 제거" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1157 +msgid "workspace.shape.menu.remove-variant-property" +msgstr "속성 제거" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1156 +msgid "workspace.shape.menu.remove-variant-property.last-property" +msgstr "베리언트는 최소한 하나의 속성을 가져야 합니다" + +#: src/app/main/ui/workspace/context_menu.cljs:329 +msgid "workspace.shape.menu.rename" +msgstr "이름 바꾸기" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:493 +msgid "workspace.shape.menu.reset-overrides" +msgstr "오버라이드 초기화" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:499 +msgid "workspace.shape.menu.restore-main" +msgstr "메인 컴포넌트 복원" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:498 +msgid "workspace.shape.menu.restore-variant" +msgstr "베리언트 복원" + +#: src/app/main/ui/workspace/context_menu.cljs:263 +msgid "workspace.shape.menu.select-layer" +msgstr "레이어 선택" + +#: src/app/main/ui/workspace/context_menu.cljs:474, src/app/main/ui/workspace/sidebar/layer_item.cljs:171 +msgid "workspace.shape.menu.show" +msgstr "표시" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:481, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1217 +msgid "workspace.shape.menu.show-in-assets" +msgstr "에셋 패널에 표시" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:502, src/app/main/ui/workspace/sidebar/assets/components.cljs:629 +msgid "workspace.shape.menu.show-main" +msgstr "메인 컴포넌트 표시" + +#: src/app/main/ui/workspace/context_menu.cljs:314 +msgid "workspace.shape.menu.thumbnail-remove" +msgstr "썸네일 제거" + +#: src/app/main/ui/workspace/context_menu.cljs:316 +msgid "workspace.shape.menu.thumbnail-set" +msgstr "썸네일로 설정" + +#: src/app/main/ui/workspace/context_menu.cljs:436 +#, unused +msgid "workspace.shape.menu.transform-to-path" +msgstr "경로로 변환" + +#: src/app/main/ui/workspace/context_menu.cljs:364 +msgid "workspace.shape.menu.ungroup" +msgstr "그룹 해제" + +#: src/app/main/ui/workspace/context_menu.cljs:444, src/app/main/ui/workspace/sidebar/options/menus/bool.cljs:83 +msgid "workspace.shape.menu.union" +msgstr "합집합" + +#: src/app/main/ui/workspace/context_menu.cljs:482, src/app/main/ui/workspace/sidebar/layer_item.cljs:179, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:274 +msgid "workspace.shape.menu.unlock" +msgstr "잠금 해제" + +#: src/app/main/ui/workspace/context_menu.cljs:378 +msgid "workspace.shape.menu.unmask" +msgstr "마스크 해제" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +#, unused +msgid "workspace.shape.menu.update-components-in-bulk" +msgstr "메인 컴포넌트 업데이트" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:505 +msgid "workspace.shape.menu.update-main" +msgstr "메인 컴포넌트 업데이트" + +#: src/app/main/ui/components/tab_container.cljs:52, src/app/main/ui/workspace/sidebar.cljs:62 +msgid "workspace.sidebar.collapse" +msgstr "사이드바 접기" + +#: src/app/main/ui/workspace/sidebar.cljs:73, src/app/main/ui/workspace/sidebar.cljs:77 +msgid "workspace.sidebar.expand" +msgstr "사이드바 펼치기" + +#: src/app/main/ui/workspace/right_header.cljs:226 +msgid "workspace.sidebar.history" +msgstr "히스토리" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:509, src/app/main/ui/workspace/sidebar.cljs:154, src/app/main/ui/workspace/sidebar.cljs:157, src/app/main/ui/workspace/sidebar.cljs:164 +msgid "workspace.sidebar.layers" +msgstr "레이어" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:313, src/app/main/ui/workspace/sidebar/layers.cljs:374 +msgid "workspace.sidebar.layers.components" +msgstr "컴포넌트" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:297 +msgid "workspace.sidebar.layers.filter" +msgstr "필터" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:310, src/app/main/ui/workspace/sidebar/layers.cljs:338 +msgid "workspace.sidebar.layers.frames" +msgstr "보드" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:311, src/app/main/ui/workspace/sidebar/layers.cljs:350 +msgid "workspace.sidebar.layers.groups" +msgstr "그룹" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:315, src/app/main/ui/workspace/sidebar/layers.cljs:398 +msgid "workspace.sidebar.layers.images" +msgstr "이미지" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:312, src/app/main/ui/workspace/sidebar/layers.cljs:362 +msgid "workspace.sidebar.layers.masks" +msgstr "마스크" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:293 +msgid "workspace.sidebar.layers.search" +msgstr "레이어 검색" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:316, src/app/main/ui/workspace/sidebar/layers.cljs:410 +msgid "workspace.sidebar.layers.shapes" +msgstr "도형" + +#: src/app/main/ui/workspace/sidebar/layers.cljs:314, src/app/main/ui/workspace/sidebar/layers.cljs:386 +msgid "workspace.sidebar.layers.texts" +msgstr "텍스트" + +#: src/app/main/ui/inspect/attributes/svg.cljs:56, src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs:101 +msgid "workspace.sidebar.options.svg-attrs.title" +msgstr "가져온 SVG 속성" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs:264 +msgid "workspace.sidebar.sitemap" +msgstr "페이지" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs:274 +msgid "workspace.sidebar.sitemap.add-page" +msgstr "페이지 추가" + +#: src/app/main/ui/workspace/left_header.cljs:98 +msgid "workspace.sitemap" +msgstr "사이트맵" + +#: src/app/main/ui/workspace/tokens/themes/theme_selector.cljs:86 +msgid "workspace.tokens.active-themes" +msgstr "활성 테마 %s개" + +#: src/app/main/ui/workspace/tokens/sidebar.cljs +#, unused +msgid "workspace.tokens.add set" +msgstr "세트 추가" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:66, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:151, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:347 +msgid "workspace.tokens.add-new-theme" +msgstr "새 테마 추가" + +#: src/app/main/ui/workspace/tokens/sets/context_menu.cljs:62 +msgid "workspace.tokens.add-set-to-group" +msgstr "이 그룹에 세트 추가" + +#: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:196, src/app/main/ui/workspace/tokens/management/group.cljs:156 +msgid "workspace.tokens.add-token" +msgstr "token 추가: %s" + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:137 +msgid "workspace.tokens.applied-to" +msgstr "적용됨" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:330 +msgid "workspace.tokens.axis" +msgstr "축" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:356 +msgid "workspace.tokens.back-to-themes" +msgstr "테마 목록으로" + +#: src/app/main/ui/workspace/tokens/settings/menu.cljs:89 +msgid "workspace.tokens.base-font-size" +msgstr "기본 글꼴 크기" + +#: src/app/main/ui/workspace/tokens/settings/menu.cljs:43 +msgid "workspace.tokens.base-font-size.error" +msgstr "기본 글꼴 크기는 픽셀 또는 단위가 없는 값이어야 합니다." + +#: src/app/main/ui/workspace/tokens/modals/import.cljs:127 +#, unused +msgid "workspace.tokens.choose-file" +msgstr "파일 선택" + +#: src/app/main/ui/workspace/tokens/modals/import.cljs:132 +#, unused +msgid "workspace.tokens.choose-folder" +msgstr "폴더 선택" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:299, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:130 +msgid "workspace.tokens.color" +msgstr "색상" + +#: src/app/main/data/workspace/tokens/errors.cljs:101, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:251 +msgid "workspace.tokens.composite-line-height-needs-font-size" +msgstr "" +"행간은 글꼴 크기에 따라 달라집니다. 확인된 값을 얻으려면 글꼴 크기를 " +"추가하세요." + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:57 +msgid "workspace.tokens.create-new-theme" +msgstr "첫 번째 테마를 만들어 보세요." + +#: src/app/main/ui/workspace/tokens/sets/lists.cljs:96, src/app/main/ui/workspace/tokens/themes.cljs:44 +msgid "workspace.tokens.create-one" +msgstr "하나 생성하기." + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:235 +msgid "workspace.tokens.create-token" +msgstr "새 %s token 생성" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:353 +msgid "workspace.tokens.delete" +msgstr "token 삭제" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:140 +msgid "workspace.tokens.delete-theme-title" +msgstr "테마 삭제" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:350 +msgid "workspace.tokens.duplicate" +msgstr "token 복제" + +#: src/app/main/data/workspace/tokens/library_edit.cljs:240, src/app/main/data/workspace/tokens/library_edit.cljs:509 +msgid "workspace.tokens.duplicate-suffix" +msgstr "복사" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:337 +msgid "workspace.tokens.edit" +msgstr "token 편집" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:346 +msgid "workspace.tokens.edit-theme-title" +msgstr "테마 편집" + +#: src/app/main/ui/workspace/tokens/themes/theme_selector.cljs:74 +msgid "workspace.tokens.edit-themes" +msgstr "테마 편집" + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:234 +msgid "workspace.tokens.edit-token" +msgstr "%s token 편집" + +#: src/app/main/data/workspace/tokens/errors.cljs:41 +msgid "workspace.tokens.empty-input" +msgstr "token 값은 비워둘 수 없습니다" + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 +msgid "workspace.tokens.enter-token-name" +msgstr "%s token 이름 입력" + +#: src/app/main/data/workspace/tokens/errors.cljs:15 +msgid "workspace.tokens.error-parse" +msgstr "가져오기 오류: JSON을 구문 분석할 수 없습니다." + +#: src/app/main/ui/workspace/tokens/export/modal.cljs:49 +msgid "workspace.tokens.export" +msgstr "내보내기" + +#: src/app/main/ui/workspace/tokens/export/modal.cljs:125 +msgid "workspace.tokens.export-tokens" +msgstr "token 내보내기" + +#: src/app/main/ui/workspace/tokens/export/modal.cljs:118 +msgid "workspace.tokens.export.multiple-files" +msgstr "여러 파일" + +#: src/app/main/ui/workspace/tokens/export/modal.cljs:38 +msgid "workspace.tokens.export.no-tokens-themes-sets" +msgstr "내보낼 token, 테마 또는 세트가 없습니다." + +#: src/app/main/ui/workspace/tokens/export/modal.cljs:35 +msgid "workspace.tokens.export.preview" +msgstr "미리보기:" + +#: src/app/main/ui/workspace/tokens/export/modal.cljs:116 +msgid "workspace.tokens.export.single-file" +msgstr "단일 파일" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:129 +msgid "workspace.tokens.font-size-value-enter" +msgstr "글꼴 크기 또는 {alias} 입력" + +#: src/app/main/data/workspace/tokens/application.cljs:325 +msgid "workspace.tokens.font-variant-not-found" +msgstr "글꼴 굵기/스타일 설정 오류. 현재 글꼴에 이 스타일이 없습니다" + +#: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:42, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:137 +msgid "workspace.tokens.font-weight-value-enter" +msgstr "글꼴 굵기(300, Bold Italic 등) 또는 {alias} 입력" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:240 +msgid "workspace.tokens.gaps" +msgstr "간격" + +#: src/app/main/ui/workspace/tokens/style_dictionary.cljs +#, unused +msgid "workspace.tokens.generic-error" +msgstr "오류: " + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:93 +msgid "workspace.tokens.group-name" +msgstr "그룹 이름" + +#: src/app/main/ui/workspace/tokens/sets.cljs +#, unused +msgid "workspace.tokens.grouping-set-alert" +msgstr "token 세트 그룹화는 아직 지원되지 않습니다." + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:233 +msgid "workspace.tokens.import-button-prefix" +msgstr "%s 가져오기" + +#: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 +msgid "workspace.tokens.import-error" +msgstr "가져오기 오류:" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:273 +msgid "workspace.tokens.import-menu-folder-option" +msgstr "폴더" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:271 +msgid "workspace.tokens.import-menu-json-option" +msgstr "단일 JSON 파일" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:272 +msgid "workspace.tokens.import-menu-zip-option" +msgstr "ZIP 파일" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:241 +msgid "workspace.tokens.import-multiple-files" +msgstr "여러 파일의 경우 파일 이름/경로가 세트 이름입니다." + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:240 +msgid "workspace.tokens.import-single-file" +msgstr "단일 JSON 파일에서 최상위 키는 token 세트 이름이어야 합니다." + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:237 +msgid "workspace.tokens.import-tokens" +msgstr "token 가져오기" + +#: src/app/main/ui/workspace/tokens/sidebar.cljs:414, src/app/main/ui/workspace/tokens/sidebar.cljs:415 +#, unused +msgid "workspace.tokens.import-tooltip" +msgstr "JSON 파일을 가져오면 현재 token, 세트 및 테마가 모두 교체됩니다" + +#: src/app/main/ui/workspace/tokens/import/modal.cljs:247 +msgid "workspace.tokens.import-warning" +msgstr "token을 가져오면 현재 token, 세트 및 테마가 모두 덮어써집니다." + +#: src/app/main/ui/workspace/tokens/management.cljs:78 +msgid "workspace.tokens.inactive-set" +msgstr "비활성" + +#: src/app/main/ui/workspace/tokens/management.cljs:70 +msgid "workspace.tokens.inactive-set-description" +msgstr "" +"이 세트가 활성화되지 않았습니다. 테마를 변경하거나 이 세트를 활성화하여 변경 " +"사항을 확인하세요" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:240, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:195 +msgid "workspace.tokens.individual-tokens" +msgstr "개별 token 사용" + +#: src/app/main/data/workspace/tokens/errors.cljs:49 +msgid "workspace.tokens.invalid-color" +msgstr "유효하지 않은 색상 값: %s" + +#: src/app/main/data/workspace/tokens/errors.cljs:93 +msgid "workspace.tokens.invalid-font-family-token-value" +msgstr "잘못된 token 값: 글꼴 모음 token만 참조할 수 있습니다" + +#: src/app/main/data/workspace/tokens/errors.cljs:89 +msgid "workspace.tokens.invalid-font-weight-token-value" +msgstr "" +"유효하지 않은 글꼴 굵기 값입니다. 숫자 값(100~950) 또는 표준 이름(thin, " +"light, regular, bold 등)을 사용하세요. 필요하면 뒤에 'Italic'을 붙일 수 " +"있습니다" + +#: src/app/main/data/workspace/tokens/errors.cljs:23 +msgid "workspace.tokens.invalid-json" +msgstr "가져오기 오류: JSON 내 token 데이터가 유효하지 않습니다." + +#: src/app/main/data/workspace/tokens/errors.cljs:27 +msgid "workspace.tokens.invalid-json-token-name" +msgstr "가져오기 오류: JSON 내 token 이름이 유효하지 않습니다." + +#: src/app/main/data/workspace/tokens/errors.cljs:28 +msgid "workspace.tokens.invalid-json-token-name-detail" +msgstr "" +"\"%s\"은(는) 유효한 token 이름이 아닙니다.\n" +"token 이름은 .으로 구분된 문자와 숫자만 포함할 수 있으며 $ 기호로 시작할 수 " +"없습니다." + +#: src/app/main/data/workspace/tokens/errors.cljs:105 +msgid "workspace.tokens.invalid-shadow-type-token-value" +msgstr "유효하지 않은 그림자 유형: '내부 그림자' 또는 '그림자 효과'만 가능합니다" + +#: src/app/main/data/workspace/tokens/errors.cljs:81 +msgid "workspace.tokens.invalid-text-case-token-value" +msgstr "잘못된 token 값: none, Uppercase, Lowercase, Capitalize만 가능합니다" + +#: src/app/main/data/workspace/tokens/errors.cljs:85 +msgid "workspace.tokens.invalid-text-decoration-token-value" +msgstr "잘못된 token 값: none, underline, strike-through만 가능합니다" + +#: src/app/main/data/workspace/tokens/errors.cljs:117 +msgid "workspace.tokens.invalid-token-value-shadow" +msgstr "잘못된 값: 복합 그림자 token을 참조해야 합니다." + +#: src/app/main/data/workspace/tokens/errors.cljs:97 +msgid "workspace.tokens.invalid-token-value-typography" +msgstr "잘못된 값: 복합 타이포그래피 token을 참조해야 합니다." + +#: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 +msgid "workspace.tokens.invalid-value" +msgstr "잘못된 token 값: %s" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 +msgid "workspace.tokens.label.group" +msgstr "그룹" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:207 +msgid "workspace.tokens.label.group-placeholder" +msgstr "그룹 추가 (예: 모드)" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:214 +msgid "workspace.tokens.label.theme" +msgstr "테마" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:215 +msgid "workspace.tokens.label.theme-placeholder" +msgstr "테마 추가 (예: 라이트)" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:153 +msgid "workspace.tokens.letter-spacing-value-enter-composite" +msgstr "자간 또는 {alias} 입력" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:145 +msgid "workspace.tokens.line-height-value-enter" +msgstr "행간(배수, px, %) 또는 {alias} 입력" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:232 +msgid "workspace.tokens.margins" +msgstr "마진" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:268 +msgid "workspace.tokens.max-size" +msgstr "최대 크기" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:262 +msgid "workspace.tokens.min-size" +msgstr "최소 크기" + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:303 +msgid "workspace.tokens.missing-reference" +msgstr "누락된 참조" + +#: src/app/main/data/workspace/tokens/errors.cljs:57 +msgid "workspace.tokens.missing-references" +msgstr "누락된 token 참조: " + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 +msgid "workspace.tokens.more-options" +msgstr "우클릭으로 옵션 보기" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:135 +msgid "workspace.tokens.no-active-sets" +msgstr "활성 세트 없음" + +#: src/app/main/ui/workspace/tokens/themes/theme_selector.cljs:91 +msgid "workspace.tokens.no-active-theme" +msgstr "활성 테마 없음" + +#: src/app/main/ui/workspace/tokens/sets/lists.cljs:72 +msgid "workspace.tokens.no-permisions-set" +msgstr "세트를 활성화 / 비활성화하려면 편집자여야 합니다" + +#: src/app/main/ui/workspace/tokens/themes.cljs:54 +msgid "workspace.tokens.no-permission-themes" +msgstr "테마를 사용하려면 편집자여야 합니다" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:87 +#, unused +msgid "workspace.tokens.no-references-found" +msgstr "참조 없음" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +#, unused +msgid "workspace.tokens.no-remap-needed" +msgstr "이 token은 현재 디자인에서 사용되지 않으므로 재매핑이 필요 없습니다." + +#: src/app/main/ui/workspace/tokens/sets/lists.cljs:485 +msgid "workspace.tokens.no-sets-create" +msgstr "아직 정의된 세트가 없습니다. 먼저 하나를 만드세요." + +#: src/app/main/ui/workspace/tokens/sets/lists.cljs:93, src/app/main/ui/workspace/tokens/sets/lists.cljs:99 +msgid "workspace.tokens.no-sets-yet" +msgstr "아직 세트가 없습니다." + +#: src/app/main/ui/workspace/tokens/themes.cljs:40 +msgid "workspace.tokens.no-themes" +msgstr "테마가 없습니다." + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:53 +msgid "workspace.tokens.no-themes-currently" +msgstr "현재 테마가 없습니다." + +#: src/app/main/data/workspace/tokens/errors.cljs:19 +msgid "workspace.tokens.no-token-files-found" +msgstr "이 파일에서 token, 세트 또는 테마를 찾을 수 없습니다." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:95 +msgid "workspace.tokens.not-remap" +msgstr "재매핑 안 함" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:134 +msgid "workspace.tokens.num-active-sets" +msgstr "활성 세트 %s개" + +#: src/app/main/data/workspace/tokens/errors.cljs:53 +msgid "workspace.tokens.number-too-large" +msgstr "잘못된 token 값. 확인된 값이 너무 큽니다: %s" + +#: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 +msgid "workspace.tokens.opacity-range" +msgstr "불투명도는 0~100% 또는 0~1 사이의 값이어야 합니다(예: 50% 또는 0.5)." + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 +msgid "workspace.tokens.original-value" +msgstr "원래 값: %s" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:216 +msgid "workspace.tokens.paddings" +msgstr "패딩" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:292 +msgid "workspace.tokens.radius" +msgstr "반지름" + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:129 +msgid "workspace.tokens.ref-not-valid" +msgstr "참조가 유효하지 않거나 활성 세트에 없습니다" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:178 +msgid "workspace.tokens.reference-composite" +msgstr "타이포그래피 token 별칭 입력" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:204 +msgid "workspace.tokens.reference-composite-shadow" +msgstr "그림자 token 별칭 입력" + +#: src/app/main/ui/workspace/tokens/style_dictionary.cljs +#, unused +msgid "workspace.tokens.reference-error" +msgstr "참조 오류: " + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:99 +msgid "workspace.tokens.remap" +msgstr "토큰 재매핑" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:86 +msgid "workspace.tokens.remap-token-references-title" +msgstr "`%s`을 사용하는 모든 토큰을 `%s`로 재매핑하시겠습니까?" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88 +msgid "workspace.tokens.remap-warning-effects" +msgstr "이 작업은 이전 token 이름을 사용하는 모든 레이어와 참조를 변경합니다." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89 +msgid "workspace.tokens.remap-warning-time" +msgstr "이 작업은 시간이 조금 걸릴 수 있습니다." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:92 +#, unused +msgid "workspace.tokens.remapping-in-progress" +msgstr "token 참조 재매핑 중..." + +#: src/app/main/data/workspace/tokens/warnings.cljs:15, src/app/main/data/workspace/tokens/warnings.cljs:19, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:56, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:84, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:103, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:285, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:459, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:176, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:311, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:251, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:364, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:465, src/app/main/ui/workspace/tokens/management/token_pill.cljs:122 +msgid "workspace.tokens.resolved-value" +msgstr "확인된 값: %s" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:251 +msgid "workspace.tokens.save-theme" +msgstr "테마 저장" + +#: src/app/main/ui/workspace/tokens/sets/lists.cljs:204, src/app/main/ui/workspace/tokens/sets/lists.cljs:309 +msgid "workspace.tokens.select-set" +msgstr "세트 선택." + +#: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 +msgid "workspace.tokens.self-reference" +msgstr "token이 자기 자신을 참조하고 있습니다" + +#: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 +msgid "workspace.tokens.set-edit-placeholder" +msgstr "이름 입력 (그룹화하려면 '/' 사용)" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:361 +msgid "workspace.tokens.set-selection-theme" +msgstr "이 테마에 포함할 token 세트를 정의하세요:" + +#: src/app/main/ui/workspace/tokens/token_pill.cljs:47 +#, unused +msgid "workspace.tokens.set.not-active" +msgstr "token 세트가 활성화되지 않았습니다" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:129 +msgid "workspace.tokens.sets-hint" +msgstr "테마 편집 및 세트 관리" + +#: src/app/main/ui/workspace/tokens/settings/menu.cljs:91 +msgid "workspace.tokens.setting-description" +msgstr "여기서 1rem의 값을 정의하는 기본 글꼴 크기를 설정할 수 있습니다:" + +#: src/app/main/ui/workspace/tokens/settings/menu.cljs:84 +msgid "workspace.tokens.settings" +msgstr "token 설정" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:232 +msgid "workspace.tokens.shadow-add-shadow" +msgstr "그림자 추가" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:161, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:162, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:165 +msgid "workspace.tokens.shadow-blur" +msgstr "블러" + +#: src/app/main/data/workspace/tokens/errors.cljs:109 +msgid "workspace.tokens.shadow-blur-range" +msgstr "그림자 블러는 0 이상이어야 합니다." + +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988 +#, unused +msgid "workspace.tokens.shadow-color" +msgstr "색상" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:114 +msgid "workspace.tokens.shadow-inset" +msgstr "내부" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:123 +msgid "workspace.tokens.shadow-remove-shadow" +msgstr "그림자 제거" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:173, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:174, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:177 +msgid "workspace.tokens.shadow-spread" +msgstr "확산" + +#: src/app/main/data/workspace/tokens/errors.cljs:113 +msgid "workspace.tokens.shadow-spread-range" +msgstr "그림자 확산은 0 이상이어야 합니다." + +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1215 +#, unused +msgid "workspace.tokens.shadow-title" +msgstr "그림자" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:281 +msgid "workspace.tokens.shadow-token-blur-value-error" +msgstr "블러 값은 음수일 수 없습니다" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:287 +#, unused +msgid "workspace.tokens.shadow-token-spread-value-error" +msgstr "확산 값은 음수일 수 없습니다" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:139, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:141 +msgid "workspace.tokens.shadow-x" +msgstr "X" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:150, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:152 +msgid "workspace.tokens.shadow-y" +msgstr "Y" + +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:256 +msgid "workspace.tokens.size" +msgstr "크기" + +#: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 +msgid "workspace.tokens.stroke-width-range" +msgstr "선 두께는 0 이상이어야 합니다." + +#: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 +msgid "workspace.tokens.text-case-value-enter" +msgstr "없음 | 대문자 | 소문자 | 각 단어 첫 글자 대문자 또는 {alias}" + +#: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:41, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:169 +msgid "workspace.tokens.text-decoration-value-enter" +msgstr "없음 | 밑줄 | 취소선 또는 {alias}" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:154 +#, unused +msgid "workspace.tokens.theme-name" +msgstr "테마 %s" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:51 +#, unused +msgid "workspace.tokens.theme-name-already-exists" +msgstr "이미 같은 이름의 테마가 있습니다" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:100 +#, unused +msgid "workspace.tokens.theme.disable" +msgstr "비활성화" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:96 +#, unused +msgid "workspace.tokens.theme.enable" +msgstr "활성화" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:85 +msgid "workspace.tokens.themes-description" +msgstr "" +"여기서 테마를 관리하고, 활성화 / 비활성화하고, 활성 세트를 설정할 수 " +"있습니다." + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:49, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:83 +msgid "workspace.tokens.themes-list" +msgstr "테마 목록" + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:275, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:276 +msgid "workspace.tokens.token-description" +msgstr "설명" + +#: src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:122, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:246 +msgid "workspace.tokens.token-font-family-select" +msgstr "글꼴 모음 선택" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:121 +msgid "workspace.tokens.token-font-family-value" +msgstr "글꼴 모음" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:120 +msgid "workspace.tokens.token-font-family-value-enter" +msgstr "글꼴 모음 또는 쉼표(,)로 구분된 글꼴 목록" + +#: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:83, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:112, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:240 +msgid "workspace.tokens.token-name" +msgstr "이름" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:268, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:223 +msgid "workspace.tokens.token-name-duplication-validation-error" +msgstr "해당 경로에 이미 token이 있습니다: %s" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:265, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:220 +msgid "workspace.tokens.token-name-length-validation-error" +msgstr "이름은 최소 1자 이상이어야 합니다" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:267, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:222 +msgid "workspace.tokens.token-name-validation-error" +msgstr "" +" 은(는) 유효한 token 이름이 아닙니다.\n" +"token 이름은 .으로 구분된 문자와 숫자만 포함할 수 있으며 $ 기호로 시작할 수 " +"없습니다." + +#: src/app/main/ui/workspace/tokens/style_dictionary.cljs:259 +#, unused +msgid "workspace.tokens.token-not-resolved" +msgstr "이름으로 참조 token을 확인할 수 없습니다: %s" + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:267 +msgid "workspace.tokens.token-value" +msgstr "값" + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:266, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:129 +msgid "workspace.tokens.token-value-enter" +msgstr "값 또는 {alias}를 사용한 별칭을 입력하세요" + +#: src/app/main/ui/workspace/tokens/management.cljs:67 +msgid "workspace.tokens.tokens-section-title" +msgstr "token - %s" + +#: src/app/main/ui/workspace/tokens/sidebar.cljs:122 +msgid "workspace.tokens.tools" +msgstr "도구" + +#: src/app/main/data/workspace/tokens/import_export.cljs:46 +msgid "workspace.tokens.unknown-token-type-message" +msgstr "가져오기가 성공적으로 완료되었습니다. 일부 token이 포함되지 않았습니다." + +#: src/app/main/data/workspace/tokens/import_export.cljs:48 +msgid "workspace.tokens.unknown-token-type-section" +msgstr "유형 '%s'가 지원되지 않습니다 (%s)\n" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 +msgid "workspace.tokens.use-reference" +msgstr "참조 사용" + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:132 +msgid "workspace.tokens.value-not-valid" +msgstr "값이 유효하지 않습니다" + +#: src/app/main/data/workspace/tokens/errors.cljs:69 +msgid "workspace.tokens.value-with-percent" +msgstr "잘못된 값: %가 허용되지 않습니다." + +#: src/app/main/data/workspace/tokens/errors.cljs:65 +msgid "workspace.tokens.value-with-units" +msgstr "잘못된 값: 단위가 허용되지 않습니다." + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +#, unused +msgid "workspace.tokens.warning-name-change" +msgstr "이 token의 이름을 바꾸면 기존 이름에 대한 참조가 끊어집니다" + +#: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 +msgid "workspace.toolbar.assets" +msgstr "에셋" + +#: src/app/main/ui/workspace/palette.cljs:184 +msgid "workspace.toolbar.color-palette" +msgstr "색상 팔레트 (%s)" + +#: src/app/main/ui/workspace/right_header.cljs:217 +msgid "workspace.toolbar.comments" +msgstr "댓글 (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:195 +msgid "workspace.toolbar.curve" +msgstr "곡선 (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:231 +msgid "workspace.toolbar.debug" +msgstr "디버깅 도구" + +#: src/app/main/ui/workspace/top_toolbar.cljs:172 +msgid "workspace.toolbar.ellipse" +msgstr "타원 (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:130 +msgid "workspace.toolbar.frame" +msgstr "보드 (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:129 +msgid "workspace.toolbar.frame-first-time" +msgstr "보드를 만듭니다. 클릭하고 드래그하여 크기를 정의합니다. (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:60 +msgid "workspace.toolbar.image" +msgstr "이미지 (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:143 +msgid "workspace.toolbar.move" +msgstr "이동 (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:205 +msgid "workspace.toolbar.path" +msgstr "경로 (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:216 +msgid "workspace.toolbar.plugins" +msgstr "플러그인 (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:162 +msgid "workspace.toolbar.rect" +msgstr "직사각형 (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +#, unused +msgid "workspace.toolbar.shortcuts" +msgstr "단축키 (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:182 +msgid "workspace.toolbar.text" +msgstr "텍스트 (%s)" + +#: src/app/main/ui/workspace/palette.cljs:190 +msgid "workspace.toolbar.text-palette" +msgstr "타이포그래피 (%s)" + +#: src/app/main/ui/workspace/top_toolbar.cljs:235, src/app/main/ui/workspace/top_toolbar.cljs:236 +msgid "workspace.toolbar.toggle-toolbar" +msgstr "도구 모음 켜기/끄기" + +#: src/app/main/ui/workspace/viewport/top_bar.cljs:41 +msgid "workspace.top-bar.read-only.done" +msgstr "완료" + +#: src/app/main/ui/workspace/viewport/top_bar.cljs:37 +#, markdown +msgid "workspace.top-bar.view-only" +msgstr "**코드 검사 중** (보기 전용)" + +#: src/app/main/ui/workspace/sidebar/history.cljs:333 +msgid "workspace.undo.empty" +msgstr "아직 기록 변경사항이 없습니다" + +#: src/app/main/ui/workspace/sidebar/history.cljs:147 +msgid "workspace.undo.entry.delete" +msgstr "삭제됨 %s" + +#: src/app/main/ui/workspace/sidebar/history.cljs:146 +msgid "workspace.undo.entry.modify" +msgstr "수정됨 %s" + +#: src/app/main/ui/workspace/sidebar/history.cljs:148 +msgid "workspace.undo.entry.move" +msgstr "객체 이동됨" + +#: src/app/main/ui/workspace/sidebar/history.cljs:111 +msgid "workspace.undo.entry.multiple.circle" +msgstr "원" + +#: src/app/main/ui/workspace/sidebar/history.cljs:112 +msgid "workspace.undo.entry.multiple.color" +msgstr "색상 에셋" + +#: src/app/main/ui/workspace/sidebar/history.cljs:113 +msgid "workspace.undo.entry.multiple.component" +msgstr "컴포넌트" + +#: src/app/main/ui/workspace/sidebar/history.cljs:114 +msgid "workspace.undo.entry.multiple.curve" +msgstr "곡선" + +#: src/app/main/ui/workspace/sidebar/history.cljs:115 +msgid "workspace.undo.entry.multiple.frame" +msgstr "보드" + +#: src/app/main/ui/workspace/sidebar/history.cljs:116 +msgid "workspace.undo.entry.multiple.group" +msgstr "그룹" + +#: src/app/main/ui/workspace/sidebar/history.cljs:117 +msgid "workspace.undo.entry.multiple.media" +msgstr "그래픽 에셋" + +#: src/app/main/ui/workspace/sidebar/history.cljs:118 +msgid "workspace.undo.entry.multiple.multiple" +msgstr "객체" + +#: src/app/main/ui/workspace/sidebar/history.cljs:119 +msgid "workspace.undo.entry.multiple.page" +msgstr "페이지" + +#: src/app/main/ui/workspace/sidebar/history.cljs:120 +msgid "workspace.undo.entry.multiple.path" +msgstr "경로" + +#: src/app/main/ui/workspace/sidebar/history.cljs:121 +msgid "workspace.undo.entry.multiple.rect" +msgstr "직사각형" + +#: src/app/main/ui/workspace/sidebar/history.cljs:122 +msgid "workspace.undo.entry.multiple.shape" +msgstr "도형" + +#: src/app/main/ui/workspace/sidebar/history.cljs:123 +msgid "workspace.undo.entry.multiple.text" +msgstr "텍스트" + +#: src/app/main/ui/workspace/sidebar/history.cljs:124 +msgid "workspace.undo.entry.multiple.typography" +msgstr "타이포그래피 에셋" + +#: src/app/main/ui/workspace/sidebar/history.cljs:145 +msgid "workspace.undo.entry.new" +msgstr "새 %s" + +#: src/app/main/ui/workspace/sidebar/history.cljs:125 +msgid "workspace.undo.entry.single.circle" +msgstr "원" + +#: src/app/main/ui/workspace/sidebar/history.cljs:126 +msgid "workspace.undo.entry.single.color" +msgstr "색상 에셋" + +#: src/app/main/ui/workspace/sidebar/history.cljs:127 +msgid "workspace.undo.entry.single.component" +msgstr "컴포넌트" + +#: src/app/main/ui/workspace/sidebar/history.cljs:128 +msgid "workspace.undo.entry.single.curve" +msgstr "곡선" + +#: src/app/main/ui/workspace/sidebar/history.cljs:129 +msgid "workspace.undo.entry.single.frame" +msgstr "보드" + +#: src/app/main/ui/workspace/sidebar/history.cljs:130 +msgid "workspace.undo.entry.single.group" +msgstr "그룹" + +#: src/app/main/ui/workspace/sidebar/history.cljs:131 +msgid "workspace.undo.entry.single.image" +msgstr "이미지" + +#: src/app/main/ui/workspace/sidebar/history.cljs:132 +msgid "workspace.undo.entry.single.media" +msgstr "그래픽 에셋" + +#: src/app/main/ui/workspace/sidebar/history.cljs:133 +msgid "workspace.undo.entry.single.multiple" +msgstr "객체" + +#: src/app/main/ui/workspace/sidebar/history.cljs:134 +msgid "workspace.undo.entry.single.page" +msgstr "페이지" + +#: src/app/main/ui/workspace/sidebar/history.cljs:135 +msgid "workspace.undo.entry.single.path" +msgstr "경로" + +#: src/app/main/ui/workspace/sidebar/history.cljs:136 +msgid "workspace.undo.entry.single.rect" +msgstr "직사각형" + +#: src/app/main/ui/workspace/sidebar/history.cljs:137 +msgid "workspace.undo.entry.single.shape" +msgstr "도형" + +#: src/app/main/ui/workspace/sidebar/history.cljs:138 +msgid "workspace.undo.entry.single.text" +msgstr "텍스트" + +#: src/app/main/ui/workspace/sidebar/history.cljs:139 +msgid "workspace.undo.entry.single.typography" +msgstr "타이포그래피 에셋" + +#: src/app/main/ui/workspace/sidebar/history.cljs:149 +msgid "workspace.undo.entry.unknown" +msgstr "%s에 대한 작업" + +#: src/app/main/ui/workspace/sidebar/history.cljs:335 +#, unused +msgid "workspace.undo.title" +msgstr "히스토리" + +#: src/app/main/data/workspace/libraries.cljs:1247, src/app/main/ui/workspace/sidebar/versions.cljs:85 +msgid "workspace.updates.dismiss" +msgstr "해제" + +#: src/app/main/data/workspace/libraries.cljs:1245 +msgid "workspace.updates.more-info" +msgstr "자세히" + +#: src/app/main/data/workspace/libraries.cljs:1243 +msgid "workspace.updates.there-are-updates" +msgstr "공유 라이브러리에 업데이트가 있습니다" + +#: src/app/main/data/workspace/libraries.cljs:1249 +msgid "workspace.updates.update" +msgstr "업데이트" + +#: src/app/main/ui/ds/product/milestone_group.cljs:73 +msgid "workspace.versions.autosaved.entry" +msgstr "%s 자동 저장 버전" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:260 +msgid "workspace.versions.autosaved.version" +msgstr "자동 저장됨 %s" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:278 +msgid "workspace.versions.button.pin" +msgstr "버전 고정" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:273 +msgid "workspace.versions.button.restore" +msgstr "버전 복원" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:396, src/app/main/ui/workspace/sidebar/versions.cljs:398 +msgid "workspace.versions.button.save" +msgstr "버전 저장" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:405 +msgid "workspace.versions.empty" +msgstr "아직 버전이 없습니다" + +#: src/app/main/ui/ds/product/milestone_group.cljs:67 +msgid "workspace.versions.expand-snapshot" +msgstr "스냅샷 확장" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:369 +msgid "workspace.versions.filter.all" +msgstr "모든 버전" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:383 +msgid "workspace.versions.filter.label" +msgstr "버전 필터" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:370 +msgid "workspace.versions.filter.mine" +msgstr "내 버전" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:374 +msgid "workspace.versions.filter.user" +msgstr "%s의 버전" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:391 +msgid "workspace.versions.loading" +msgstr "로드 중..." + +#, unused +msgid "workspace.versions.locked-by-other" +msgstr "이 버전은 %s에 의해 잠겨있으며 수정할 수 없습니다" + +#, unused +msgid "workspace.versions.locked-by-you" +msgstr "이 버전은 당신에 의해 잠겨있습니다" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:83 +msgid "workspace.versions.restore-warning" +msgstr "이 버전을 복원하시겠습니까?" + +#, unused +msgid "workspace.versions.snapshot-menu" +msgstr "스냅샷 메뉴 열기" + +#: src/app/main/ui/workspace/sidebar.cljs:257 +msgid "workspace.versions.tab.actions" +msgstr "동작" + +#: src/app/main/ui/workspace/sidebar.cljs:255 +msgid "workspace.versions.tab.history" +msgstr "히스토리" + +#, unused +msgid "workspace.versions.tooltip.locked-version" +msgstr "잠긴 버전 - 생성자만 수정할 수 있습니다" + +#: src/app/main/ui/ds/product/milestone.cljs:84, src/app/main/ui/ds/product/milestone_group.cljs:86 +msgid "workspace.versions.version-menu" +msgstr "버전 메뉴 열기" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:60 +#, markdown +msgid "workspace.versions.warning.subtext" +msgstr "이 제한을 늘리고 싶으시면, [support@penpot.app](%s)으로 문의하세요" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:431 +msgid "workspace.versions.warning.text" +msgstr "자동 저장 버전은 %s일 동안 유지됩니다." + +#, unused +msgid "workspace.viewport.click-to-close-path" +msgstr "경로를 닫으려면 클릭하세요" + +#: src/app/main/ui/inspect/styles/panels/tokens_panel.cljs:26 +msgid "inspect.tabs.styles.active-sets" +msgstr "활성 세트" + +#: src/app/main/ui/inspect/styles/panels/tokens_panel.cljs:21 +msgid "inspect.tabs.styles.active-themes" +msgstr "활성 테마" + +#: src/app/main/ui/dashboard/subscription.cljs:180 +msgid "subscription.dashboard.unlimited-members-extra-editors-cta-title" +msgstr "무제한 플랜 이용 중 사람 초대하기" From 0719e4fa706032e48b8be5a2040a3d85687abdeb Mon Sep 17 00:00:00 2001 From: Denys Kisil Date: Sun, 8 Mar 2026 20:50:31 +0100 Subject: [PATCH 025/288] :globe_with_meridians: Add translations for: Ukrainian (ukr_UA) Currently translated at 99.7% (2068 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/ --- frontend/translations/ukr_UA.po | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/translations/ukr_UA.po b/frontend/translations/ukr_UA.po index 9a3dc3a3b8..3f338f8ae3 100644 --- a/frontend/translations/ukr_UA.po +++ b/frontend/translations/ukr_UA.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-03-02 18:09+0000\n" +"PO-Revision-Date: 2026-03-09 20:09+0000\n" "Last-Translator: Denys Kisil \n" "Language-Team: Ukrainian \n" @@ -5433,7 +5433,7 @@ msgstr "Деякі з цих варіантів мають помилкові і #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:433 msgid "workspace.options.component.variant.malformed.structure.example" -msgstr "[property] = [value], [property] = [value]" +msgstr "[property] = [value], [property] = [value]" #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:431 msgid "workspace.options.component.variant.malformed.structure.title" @@ -6491,7 +6491,7 @@ msgstr "Підкреслення (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs #, unused msgid "workspace.options.text-options.uppercase" -msgstr "ВЕРХНІЙ РЕГІСТР" +msgstr "Верхній Регістр" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:788 msgid "workspace.options.use-play-button" @@ -8743,9 +8743,8 @@ msgid "workspace.tokens.shadow-x" msgstr "Х" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:150, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:152 -#, fuzzy msgid "workspace.tokens.shadow-y" -msgstr "У" +msgstr "" #: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 msgid "workspace.tokens.text-case-value-enter" From 6e19548bacfc9b5be3d34bd5df04c186327fd55e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 16 Mar 2026 09:38:23 +0100 Subject: [PATCH 026/288] :paperclip: Update changelog --- CHANGES.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 538c8994f4..da8dcab15f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,24 @@ # CHANGELOG +## 2.16.0 (Unreleased) + +### :boom: Breaking changes & Deprecations + +### :rocket: Epics and highlights + +### :sparkles: New features & Enhancements + +- Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912) +- Import Tokens from linked library [Github #8391](https://github.com/penpot/penpot/pull/8391) +- Option to download custom fonts (by @dfelinto) [Github #8320](https://github.com/penpot/penpot/issues/8320) +- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248) +- Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313) +- Add Tab/Shift+Tab navigation to rename layers sequentially (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8474) + + +### :bug: Bugs fixed + + ## 2.15.0 (Unreleased) ### :boom: Breaking changes & Deprecations From 8f35e451e6209aff2d23855fbf625419f334dc04 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 16 Mar 2026 10:36:32 +0100 Subject: [PATCH 027/288] :sparkles: Add notification for nitrate when creating a team inside an organization (#8639) --- backend/src/app/nitrate.clj | 74 +++++++++++++++---- backend/src/app/rpc/commands/teams.clj | 7 +- backend/src/app/rpc/management/nitrate.clj | 6 +- frontend/src/app/main/data/team.cljs | 5 +- .../src/app/main/ui/dashboard/sidebar.cljs | 16 +++- .../src/app/main/ui/dashboard/team_form.cljs | 24 ++++-- 6 files changed, 102 insertions(+), 30 deletions(-) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index cfa0ff9014..dfd034026e 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -18,16 +18,16 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- request-builder - [cfg method uri shared-key profile-id] + [cfg method uri shared-key profile-id request-params] (fn [] - (http/req! cfg {:method method - :headers {"content-type" "application/json" - "accept" "application/json" - "x-shared-key" shared-key - "x-profile-id" (str profile-id)} - :uri uri - :version :http1.1}))) - + (http/req! cfg (cond-> {:method method + :headers {"content-type" "application/json" + "accept" "application/json" + "x-shared-key" shared-key + "x-profile-id" (str profile-id)} + :uri uri + :version :http1.1} + (= method :post) (assoc :body (json/encode request-params)))))) (defn- with-retries [handler max-retries] @@ -60,9 +60,9 @@ nil))))) (defn- request-to-nitrate - [cfg method uri schema {:keys [::rpc/profile-id] :as params}] + [cfg method uri schema {:keys [::rpc/profile-id request-params] :as params}] (let [shared-key (-> cfg ::setup/shared-keys :nitrate) - full-http-call (-> (request-builder cfg method uri shared-key profile-id) + full-http-call (-> (request-builder cfg method uri shared-key profile-id request-params) (with-retries 3) (with-validate uri schema))] (full-http-call))) @@ -86,6 +86,12 @@ [:name ::sm/text] [:slug ::sm/text]]) +(def ^:private schema:team + [:map + [:id ::sm/uuid] + [:organizationId ::sm/uuid] + [:yourPenpot :boolean]]) + ;; TODO Unify with schemas on backend/src/app/http/management.clj (def ^:private schema:timestamp (sm/type-schema @@ -161,17 +167,40 @@ (defn- get-team-org [cfg {:keys [team-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] - (request-to-nitrate cfg :get (str baseuri "/api/teams/" (str team-id)) schema:organization params))) + (request-to-nitrate cfg :get + (str baseuri + "/api/teams/" + team-id) + schema:organization params))) + +(defn- set-team-org + [cfg {:keys [organization-id team-id is-default] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri) + params (assoc params :request-params {:teamId team-id + :yourPenpot (true? is-default)})] + (request-to-nitrate cfg :post + (str baseuri + "/api/organizations/" + organization-id + "/addTeam") + schema:team params))) (defn- get-subscription [cfg {:keys [profile-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] - (request-to-nitrate cfg :get (str baseuri "/api/subscriptions/" (str profile-id)) schema:subscription params))) + (request-to-nitrate cfg :get + (str baseuri + "/api/subscriptions/" + profile-id) + schema:subscription params))) (defn- get-connectivity [cfg params] (let [baseuri (cf/get :nitrate-backend-uri)] - (request-to-nitrate cfg :get (str baseuri "/api/connectivity") schema:connectivity params))) + (request-to-nitrate cfg :get + (str baseuri + "/api/connectivity") + schema:connectivity params))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; INITIALIZATION @@ -181,6 +210,7 @@ [_ cfg] (when (contains? cf/flags :nitrate) {:get-team-org (partial get-team-org cfg) + :set-team-org (partial set-team-org cfg) :get-subscription (partial get-subscription cfg) :connectivity (partial get-connectivity cfg)})) @@ -224,6 +254,22 @@ :cause cause) team))) +(defn set-team-organization + "Associates a team with an organization in Nitrate. + Requires organization-id and is-default in params. + Throws an exception if the request fails." + [cfg team params] + (let [params (assoc (or params {}) + :team-id (:id team) + :organization-id (:organization-id params) + :is-default (:is-default params)) + result (call cfg :set-team-org params)] + (when (nil? result) + (throw (ex-info "Failed to set team organization" + {:team-id (:id team) + :organization-id (:organization-id params)}))) + team)) + (defn connectivity [cfg] (call cfg :connectivity {})) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 220602d4e9..55836cb5b6 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -499,7 +499,9 @@ [:map {:title "create-team"} [:name [:string {:max 250}]] [:features {:optional true} ::cfeat/features] - [:id {:optional true} ::sm/uuid]]) + [:id {:optional true} ::sm/uuid] + [:organization-id {:optional true} ::sm/uuid] + [:is-default {:optional true} :boolean]]) (sv/defmethod ::create-team {::doc/added "1.17" @@ -531,6 +533,9 @@ :role :owner) project (create-team-default-project conn params)] (create-team-role conn params) + ;; Set team organization in Nitrate if organization-id is provided + (when (and (contains? cf/flags :nitrate) (:organization-id params)) + (nitrate/set-team-organization cfg-or-conn team params)) (assoc team :default-project-id (:id project)))) (defn- create-team* diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 455b96705b..c70b617c7c 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -113,7 +113,7 @@ {::doc/added "2.14" ::sm/params schema:notify-user-added-to-organization ::rpc/auth false} - [cfg {:keys [profile-id]}] + [cfg {:keys [profile-id organization-id]}] (quotes/check! cfg {::quotes/id ::quotes/teams-per-profile ::quotes/profile-id profile-id}) @@ -122,7 +122,9 @@ (set/difference cfeat/no-team-inheritable-features)) params {:profile-id profile-id :name "Default" - :features features} + :features features + :organization-id organization-id + :is-default true} team (db/tx-run! cfg teams/create-team params)] (select-keys team [:id]))) diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs index 60846d88bd..2f6c03f68e 100644 --- a/frontend/src/app/main/data/team.cljs +++ b/frontend/src/app/main/data/team.cljs @@ -255,7 +255,7 @@ (-deref [_] team))) (defn create-team - [{:keys [name] :as params}] + [{:keys [name organization-id] :as params}] (dm/assert! (string? name)) (ptk/reify ::create-team ptk/WatchEvent @@ -264,7 +264,8 @@ :or {on-success identity on-error rx/throw}} (meta params) features features/global-enabled-features - params {:name name :features features}] + params (cond-> {:name name :features features} + organization-id (assoc :organization-id organization-id))] (->> (rp/cmd! :create-team (with-meta params (meta it))) (rx/tap on-success) (rx/map team-created) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index c0d9b46496..491b0d9800 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -314,7 +314,9 @@ (mf/use-fn (mf/deps organization) (fn [] - (dnt/go-to-nitrate-cc organization))) + (if (:organization-id organization) + (dnt/go-to-nitrate-cc organization) + (dnt/go-to-nitrate-cc)))) default-team-id (or (->> organizations vals @@ -371,7 +373,13 @@ teams (dissoc teams default-team-id) on-create-team-click - (mf/use-fn #(st/emit! (modal/show :team-form {}))) + (mf/use-fn + (mf/deps team) + (fn [] + (let [params (if (and (contains? cf/flags :nitrate) (:organization-id team)) + {:organization-id (:organization-id team)} + {})] + (st/emit! (modal/show :team-form params))))) on-team-click (mf/use-fn @@ -383,12 +391,12 @@ [:> dropdown-menu* props [:> dropdown-menu-item* {:on-click on-team-click - :data-value (:default-team-id profile) + :data-value default-team-id :class (stl/css :team-dropdown-item)} [:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon] [:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")] - (when (= (:default-team-id profile) (:id team)) + (when (= default-team-id (:id team)) tick-icon)] (for [team-item (remove :is-default (vals teams))] diff --git a/frontend/src/app/main/ui/dashboard/team_form.cljs b/frontend/src/app/main/ui/dashboard/team_form.cljs index 5f6fdaae90..9856a3ec1e 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.cljs +++ b/frontend/src/app/main/ui/dashboard/team_form.cljs @@ -24,7 +24,8 @@ (def ^:private schema:team-form [:map {:title "TeamForm"} - [:name [::sm/text {:max 250}]]]) + [:name [::sm/text {:max 250}]] + [:organization-id {:optional true} [:maybe ::sm/uuid]]]) (defn- on-create-success [_form response] @@ -50,7 +51,9 @@ [form] (let [mdata {:on-success (partial on-create-success form) :on-error (partial on-error form)} - params {:name (get-in @form [:clean-data :name])}] + data (:clean-data @form) + params (cond-> {:name (:name data)} + (:organization-id data) (assoc :organization-id (:organization-id data)))] (st/emit! (-> (dtm/create-team (with-meta params mdata)) (with-meta {::ev/origin :dashboard}))))) @@ -58,7 +61,8 @@ [form] (let [mdata {:on-success (partial on-update-success form) :on-error (partial on-error form)} - team (get @form :clean-data)] + data (:clean-data @form) + team (select-keys data [:id :name])] ;; Only send name and id for updates (st/emit! (dtm/update-team (with-meta team mdata)) (modal/hide)))) @@ -72,10 +76,16 @@ (mf/defc team-form-modal {::mf/register modal/components ::mf/register-as :team-form} - [{:keys [team] :as props}] - (let [initial (mf/use-memo (fn [] - (or (some-> team (select-keys [:name :id])) - {}))) + [{:keys [team organization-id] :as props}] + (let [initial (mf/use-memo + (mf/deps team organization-id) + (fn [] + (if team + ;; For existing teams, only include name and id (no organization changes) + (select-keys team [:name :id]) + ;; For new teams, include organization-id if provided + (cond-> {} + organization-id (assoc :organization-id organization-id))))) form (fm/use-form :schema schema:team-form :initial initial) handle-keydown From f566c1950fef324f597dd9fc700be54b13824b89 Mon Sep 17 00:00:00 2001 From: "Dr. Dominik Jain" Date: Mon, 16 Mar 2026 10:38:25 +0100 Subject: [PATCH 028/288] :sparkles: Account for changed interfaces of addToken and addSet (#8614) Resolves #8613 --- mcp/packages/server/data/initial_instructions.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mcp/packages/server/data/initial_instructions.md b/mcp/packages/server/data/initial_instructions.md index 0dfa15d6c9..c212bb408b 100644 --- a/mcp/packages/server/data/initial_instructions.md +++ b/mcp/packages/server/data/initial_instructions.md @@ -312,18 +312,18 @@ Design tokens are reusable design values (colors, dimensions, typography, etc.) The token library: `penpot.library.local.tokens` (type: `TokenCatalog`) * `sets: TokenSet[]` - Token collections (order matters for precedence) * `themes: TokenTheme[]` - Presets that activate specific sets - * `addSet(name: string): TokenSet` - Create new set + * `addSet({name: string}): TokenSet` - Create new set * `addTheme(group: string, name: string): TokenTheme` - Create new theme `TokenSet` contains tokens with unique names: * `active: boolean` - Only active sets affect shapes; use `set.toggleActive()` to change: `if (!set.active) set.toggleActive();` * `tokens: Token[]` - All tokens in set - * `addToken(type: TokenType, name: string, value: TokenValueString): Token` - Creates a token, adding it to the set. + * `addToken({type: TokenType, name: string, value: TokenValueString}): Token` - Creates a token, adding it to the set. - `TokenType`: "color" | "dimension" | "spacing" | "typography" | "shadow" | "opacity" | "borderRadius" | "borderWidth" | "fontWeights" | "fontSizes" | "fontFamilies" | "letterSpacing" | "textDecoration" | "textCase" - `value`: depends on the type of token (inspect `Token` and related types) - Examples: - const token = set.addToken("color", "color.primary", "#0066FF"); // direct value - const token2 = set.addToken("color", "color.accent", "{color.primary}"); // reference to another token + const token = set.addToken({type: "color", name: "color.primary", value: "#0066FF"}); // direct value + const token2 = set.addToken({type: "color", name: "color.accent", value: "{color.primary}"}); // reference to another token `Token`: union type encompassing various token types, with common properties: * `name: string` - Token name (typically structured, e.g. "color.base.white") From 98e989d7f38dec7aaccd06053d5dfb755e69036d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?= Date: Mon, 16 Mar 2026 10:51:10 +0100 Subject: [PATCH 029/288] :books: Adjust MCP presence in changelog (#8642) --- CHANGES.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index da8dcab15f..91bdfea215 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,11 +5,12 @@ ### :boom: Breaking changes & Deprecations ### :rocket: Epics and highlights +- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112) ### :sparkles: New features & Enhancements - Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912) -- Import Tokens from linked library [Github #8391](https://github.com/penpot/penpot/pull/8391) +- Import Tokens from linked library (by @dfelinto) [Github #8391](https://github.com/penpot/penpot/pull/8391) - Option to download custom fonts (by @dfelinto) [Github #8320](https://github.com/penpot/penpot/issues/8320) - Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248) - Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313) @@ -27,7 +28,7 @@ ### :sparkles: New features & Enhancements -- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112), [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114) +- Access Tokens look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114) ### :bug: Bugs fixed From ce04780b6cd8d2dfbe4d11b39b06addcc22057fd Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka928@users.noreply.github.com> Date: Mon, 16 Mar 2026 06:03:49 -0400 Subject: [PATCH 030/288] :bug: Make collapsible sidebar titles clickable to toggle (#8547) Fixes #5168 --- CHANGES.md | 1 + .../src/app/main/ui/components/title_bar.cljs | 25 ++++++------------- .../src/app/main/ui/components/title_bar.scss | 9 ------- .../ui/workspace/sidebar/assets/common.cljs | 1 - .../sidebar/assets/file_library.cljs | 1 - .../ui/workspace/sidebar/assets/groups.cljs | 1 - .../main/ui/workspace/sidebar/sitemap.cljs | 1 - 7 files changed, 9 insertions(+), 30 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 91bdfea215..7a118fe18e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -34,6 +34,7 @@ - Fix Alt/Option to draw shapes from center point (by @offreal) [Github #8361](https://github.com/penpot/penpot/pull/8361) - Add token name on broken token pill on sidebar [Taiga #13527](https://tree.taiga.io/project/penpot/issue/13527) +- Fix collapsible sidebar property titles not toggling on click [Github #5168](https://github.com/penpot/penpot/issues/5168) ## 2.14.0 (Unreleased) diff --git a/frontend/src/app/main/ui/components/title_bar.cljs b/frontend/src/app/main/ui/components/title_bar.cljs index 432936b0b3..56c74696c3 100644 --- a/frontend/src/app/main/ui/components/title_bar.cljs +++ b/frontend/src/app/main/ui/components/title_bar.cljs @@ -13,30 +13,21 @@ (mf/defc title-bar* [{:keys [class collapsable collapsed title children - btn-icon btn-title all-clickable add-icon-gap + btn-icon btn-title add-icon-gap title-class on-collapsed on-btn-click]}] - [:div {:class [(stl/css-case :title-bar true - :all-clickable all-clickable) + [:div {:class [(stl/css :title-bar) class]} (if ^boolean collapsable [:div {:class [(stl/css :title-wrapper) title-class]} (let [icon-id (if collapsed "arrow-right" "arrow-down")] - (if ^boolean all-clickable - [:button {:class (stl/css :icon-text-btn) - :on-click on-collapsed} - [:> icon* {:icon-id icon-id - :size "s" - :class (stl/css :icon)}] - [:div {:class (stl/css :title)} title]] - [:* - [:button {:class (stl/css :icon-btn) - :on-click on-collapsed} - [:> icon* {:icon-id icon-id - :size "s" - :class (stl/css :icon)}]] - [:div {:class (stl/css :title)} title]]))] + [:button {:class (stl/css :icon-text-btn) + :on-click on-collapsed} + [:> icon* {:icon-id icon-id + :size "s" + :class (stl/css :icon)}] + [:div {:class (stl/css :title)} title]])] [:div {:class [(stl/css-case :title-only true :title-only-icon-gap add-icon-gap) diff --git a/frontend/src/app/main/ui/components/title_bar.scss b/frontend/src/app/main/ui/components/title_bar.scss index b4b5b84554..f985736946 100644 --- a/frontend/src/app/main/ui/components/title_bar.scss +++ b/frontend/src/app/main/ui/components/title_bar.scss @@ -75,12 +75,3 @@ --title-color: var(--title-foreground-color-hover); } } - -.icon-btn { - @include deprecated.buttonStyle; - @include deprecated.flexCenter; - - &:hover { - --arrow-icon-color: var(--icon-foreground-hover); - } -} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs index 9f7762b861..0c0b839864 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs @@ -172,7 +172,6 @@ [:> title-bar* {:collapsable (< 0 assets-count) :collapsed (not is-open) - :all-clickable true :on-collapsed on-collapsed :add-icon-gap (= 0 assets-count) :title title} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs index 2fc62e001d..0cb88cd0df 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs @@ -101,7 +101,6 @@ :open is-open)} [:> title-bar* {:collapsable true :collapsed (not is-open) - :all-clickable true :on-collapsed toggle-open :title (if is-local (mf/html [:div {:class (stl/css :special-title)} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs index 72a1be6996..fa81b21157 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs @@ -51,7 +51,6 @@ :on-context-menu on-context-menu} [:> title-bar* {:collapsable true :collapsed (not is-group-open) - :all-clickable true :on-collapsed on-fold-group :title (mf/html [:* (when-not (empty? other-path) [:span {:class (stl/css :pre-path) diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index a58e1512ba..9c62ed393f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -260,7 +260,6 @@ [:> title-bar* {:collapsable true :collapsed collapsed :on-collapsed on-toggle-collapsed - :all-clickable true :title (tr "workspace.sidebar.sitemap") :class (stl/css :title-spacing-sitemap)} From 8cb5c23a2938c4f2320dfc56c50c433d7123696c Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 16 Mar 2026 13:34:32 +0100 Subject: [PATCH 031/288] :bug: Fix nitrate url --- backend/src/app/nitrate.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index dfd034026e..36c2bd0b04 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -182,7 +182,7 @@ (str baseuri "/api/organizations/" organization-id - "/addTeam") + "/add-team") schema:team params))) (defn- get-subscription From 1b8871df8e3f04183212a72be967c37e8dbc7881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Fri, 13 Mar 2026 12:29:48 +0100 Subject: [PATCH 032/288] :sparkles: Update image nitrate modal --- .../images/assets/nitrate-welcome.svg | 52 +++++++++++++++++++ .../ui/ds/foundations/assets/raw_svg.cljs | 1 + frontend/src/app/main/ui/icons.cljs | 1 + .../src/app/main/ui/nitrate/nitrate_form.cljs | 2 +- .../src/app/main/ui/nitrate/nitrate_form.scss | 12 +++-- .../app/main/ui/settings/subscription.scss | 2 +- 6 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 frontend/resources/images/assets/nitrate-welcome.svg diff --git a/frontend/resources/images/assets/nitrate-welcome.svg b/frontend/resources/images/assets/nitrate-welcome.svg new file mode 100644 index 0000000000..18ced86fa1 --- /dev/null +++ b/frontend/resources/images/assets/nitrate-welcome.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs b/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs index 428d582e97..e744cfe292 100644 --- a/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs +++ b/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs @@ -20,6 +20,7 @@ (def ^:svg-id logo-error-screen "logo-error-screen") (def ^:svg-id logo-subscription "logo-subscription") (def ^:svg-id logo-subscription-light "logo-subscription-light") +(def ^:svg-id nitrate-welcome "nitrate-welcome") (def ^:svg-id marketing-arrows "marketing-arrows") (def ^:svg-id marketing-exchange "marketing-exchange") (def ^:svg-id marketing-file "marketing-file") diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index f5ca0d9117..1cc3569881 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -19,6 +19,7 @@ (def ^:icon logo-error-screen (icon-xref :logo-error-screen)) (def ^:icon logo-subscription (icon-xref :logo-subscription)) (def ^:icon logo-subscription-light (icon-xref :logo-subscription-light)) +(def ^:icon nitrate-welcome (icon-xref :nitrate-welcome)) (def ^:icon brand-openid (icon-xref :brand-openid)) (def ^:icon brand-github (icon-xref :brand-github)) diff --git a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs index de55959c6f..6e763a1568 100644 --- a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs +++ b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs @@ -45,7 +45,7 @@ [:div {:class (stl/css :modal-success-content)} [:div {:class (stl/css :modal-start)} ;; TODO this svg is a placeholder. Use the proper one when created - [:> raw-svg* {:id "logo-subscription"}]] + [:> raw-svg* {:id "nitrate-welcome"}]] [:div {:class (stl/css :modal-end)} [:div {:class (stl/css :modal-title)} diff --git a/frontend/src/app/main/ui/nitrate/nitrate_form.scss b/frontend/src/app/main/ui/nitrate/nitrate_form.scss index bc48fe7a6d..bb2cfe2475 100644 --- a/frontend/src/app/main/ui/nitrate/nitrate_form.scss +++ b/frontend/src/app/main/ui/nitrate/nitrate_form.scss @@ -19,7 +19,13 @@ .modal-dialog { @extend .modal-container-base; max-block-size: initial; - min-inline-size: px2rem(648); + min-inline-size: px2rem(1021); + padding: px2rem(80); + + @media (max-width: 1024px) { + min-inline-size: px2rem(712); + padding: var(--sp-xxxl); + } } .close-btn { @@ -66,14 +72,14 @@ .modal-start { display: flex; justify-content: center; - max-inline-size: $sz-224; + min-inline-size: $sz-284; svg { inline-size: 100%; block-size: auto; } - @media (max-inline-size: 992px) { + @media (max-width: 992px) { display: none; } } diff --git a/frontend/src/app/main/ui/settings/subscription.scss b/frontend/src/app/main/ui/settings/subscription.scss index f98c1caef3..7355b3e109 100644 --- a/frontend/src/app/main/ui/settings/subscription.scss +++ b/frontend/src/app/main/ui/settings/subscription.scss @@ -270,7 +270,7 @@ block-size: auto; } - @media (max-inline-size: 992px) { + @media (max-width: 992px) { display: none; } } From 31696de4746c98aa63f086c119788654b56e25ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 16 Mar 2026 13:55:27 +0100 Subject: [PATCH 033/288] :wrench: GitHub Actions worker tasks updated --- .github/workflows/build-bundle.yml | 2 +- .github/workflows/build-docker-devenv.yml | 8 ++-- .github/workflows/build-docker.yml | 20 ++++---- .github/workflows/plugins-deploy-api-doc.yml | 4 +- .github/workflows/plugins-deploy-package.yml | 4 +- .github/workflows/plugins-deploy-packages.yml | 4 +- .../workflows/plugins-deploy-styles-doc.yml | 4 +- .github/workflows/release.yml | 4 +- .github/workflows/tests-mcp.yml | 2 +- .github/workflows/tests.yml | 46 +++++++++---------- 10 files changed, 49 insertions(+), 49 deletions(-) diff --git a/.github/workflows/build-bundle.yml b/.github/workflows/build-bundle.yml index 4626c4dc2e..8f8b40007a 100644 --- a/.github/workflows/build-bundle.yml +++ b/.github/workflows/build-bundle.yml @@ -48,7 +48,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ inputs.gh_ref }} diff --git a/.github/workflows/build-docker-devenv.yml b/.github/workflows/build-docker-devenv.yml index a2aaee24c0..3ba45267a5 100644 --- a/.github/workflows/build-docker-devenv.yml +++ b/.github/workflows/build-docker-devenv.yml @@ -16,19 +16,19 @@ jobs: echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to Docker Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.PUB_DOCKER_USERNAME }} password: ${{ secrets.PUB_DOCKER_PASSWORD }} - name: Build and push DevEnv Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 env: DOCKER_IMAGE: 'penpotapp/devenv' with: diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index c1901c9114..18ac6aec9f 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -28,7 +28,7 @@ jobs: echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ inputs.gh_ref }} @@ -63,10 +63,10 @@ jobs: popd - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to Docker Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ secrets.DOCKER_REGISTRY }} username: ${{ secrets.DOCKER_USERNAME }} @@ -76,14 +76,14 @@ jobs: # images from DockerHub for unregistered users. # https://docs.docker.com/docker-hub/usage/ - name: Login to DockerHub Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.PUB_DOCKER_USERNAME }} password: ${{ secrets.PUB_DOCKER_PASSWORD }} - name: Extract metadata (tags, labels) id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: frontend @@ -95,7 +95,7 @@ jobs: bundle_version=${{ steps.bundles.outputs.bundle_version }} - name: Build and push Backend Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 env: DOCKER_IMAGE: 'backend' BUNDLE_PATH: './bundle-backend' @@ -110,7 +110,7 @@ jobs: cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max - name: Build and push Frontend Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 env: DOCKER_IMAGE: 'frontend' BUNDLE_PATH: './bundle-frontend' @@ -125,7 +125,7 @@ jobs: cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max - name: Build and push Exporter Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 env: DOCKER_IMAGE: 'exporter' BUNDLE_PATH: './bundle-exporter' @@ -140,7 +140,7 @@ jobs: cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max - name: Build and push Storybook Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 env: DOCKER_IMAGE: 'storybook' BUNDLE_PATH: './bundle-storybook' @@ -155,7 +155,7 @@ jobs: cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max - name: Build and push MCP Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 env: DOCKER_IMAGE: 'mcp' BUNDLE_PATH: './bundle-mcp' diff --git a/.github/workflows/plugins-deploy-api-doc.yml b/.github/workflows/plugins-deploy-api-doc.yml index 1842a61b16..51be85e45e 100644 --- a/.github/workflows/plugins-deploy-api-doc.yml +++ b/.github/workflows/plugins-deploy-api-doc.yml @@ -37,7 +37,7 @@ jobs: echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ steps.vars.outputs.gh_ref }} @@ -62,7 +62,7 @@ jobs: run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - name: Cache pnpm store - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.pnpm-store.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }} diff --git a/.github/workflows/plugins-deploy-package.yml b/.github/workflows/plugins-deploy-package.yml index ae61d3105d..137ba6f7fa 100644 --- a/.github/workflows/plugins-deploy-package.yml +++ b/.github/workflows/plugins-deploy-package.yml @@ -37,7 +37,7 @@ jobs: runs-on: penpot-runner-01 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ inputs.gh_ref }} @@ -62,7 +62,7 @@ jobs: run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - name: Cache pnpm store - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.pnpm-store.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }} diff --git a/.github/workflows/plugins-deploy-packages.yml b/.github/workflows/plugins-deploy-packages.yml index 3223bc52a6..943e4b790d 100644 --- a/.github/workflows/plugins-deploy-packages.yml +++ b/.github/workflows/plugins-deploy-packages.yml @@ -36,9 +36,9 @@ jobs: # [For new plugins] # Add more outputs here steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: filter - uses: dorny/paths-filter@v3 + uses: dorny/paths-filter@v4 with: filters: | colors_to_tokens: diff --git a/.github/workflows/plugins-deploy-styles-doc.yml b/.github/workflows/plugins-deploy-styles-doc.yml index f8e43899b8..47f0d1cc24 100644 --- a/.github/workflows/plugins-deploy-styles-doc.yml +++ b/.github/workflows/plugins-deploy-styles-doc.yml @@ -35,7 +35,7 @@ jobs: echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ steps.vars.outputs.gh_ref }} @@ -60,7 +60,7 @@ jobs: run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - name: Cache pnpm store - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.pnpm-store.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d79842480c..21c0eb6de2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,7 +31,7 @@ jobs: echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ steps.vars.outputs.gh_ref }} @@ -93,7 +93,7 @@ jobs: # --- Create GitHub release --- - name: Create GitHub release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/tests-mcp.yml b/.github/workflows/tests-mcp.yml index a733a76d0f..9f2a4ed589 100644 --- a/.github/workflows/tests-mcp.yml +++ b/.github/workflows/tests-mcp.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup working-directory: ./mcp diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fa21646cac..61c0778cbb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Lint Common working-directory: ./common @@ -85,7 +85,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Run tests working-directory: ./common @@ -98,7 +98,7 @@ jobs: container: penpotapp/devenv:latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Node id: setup-node @@ -150,10 +150,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Restore shared.js - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: key: "render-wasm-shared-js-${{ github.sha }}" path: frontend/src/app/render_wasm/api/shared.js @@ -177,7 +177,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Format working-directory: ./render-wasm @@ -202,7 +202,7 @@ jobs: cp $SHARED_FILE ../frontend/src/app/render_wasm/api/shared.js; - name: Cache shared.js - uses: actions/cache@v4 + uses: actions/cache@v5 with: key: "render-wasm-shared-js-${{ github.sha }}" path: frontend/src/app/render_wasm/api/shared.js @@ -233,7 +233,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Run tests working-directory: ./backend @@ -253,7 +253,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Run tests working-directory: ./library @@ -267,7 +267,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Build Bundle working-directory: ./frontend @@ -275,7 +275,7 @@ jobs: ./scripts/build 0.0.0 - name: Store Bundle Cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: key: "integration-bundle-${{ github.sha }}" path: frontend/resources/public @@ -289,10 +289,10 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Restore Cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: key: "integration-bundle-${{ github.sha }}" path: frontend/resources/public @@ -303,7 +303,7 @@ jobs: ./scripts/test-e2e --shard="1/4"; - name: Upload test result - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: integration-tests-result-1 @@ -319,10 +319,10 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Restore Cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: key: "integration-bundle-${{ github.sha }}" path: frontend/resources/public @@ -333,7 +333,7 @@ jobs: ./scripts/test-e2e --shard="2/4"; - name: Upload test result - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: integration-tests-result-2 @@ -349,10 +349,10 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Restore Cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: key: "integration-bundle-${{ github.sha }}" path: frontend/resources/public @@ -363,7 +363,7 @@ jobs: ./scripts/test-e2e --shard="3/4"; - name: Upload test result - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: integration-tests-result-3 @@ -379,10 +379,10 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Restore Cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: key: "integration-bundle-${{ github.sha }}" path: frontend/resources/public @@ -393,7 +393,7 @@ jobs: ./scripts/test-e2e --shard="4/4"; - name: Upload test result - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: integration-tests-result-4 From acc383ba315f561eb7d2d715dfbf8af97ef2dc25 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 16 Mar 2026 17:08:18 +0100 Subject: [PATCH 034/288] :sparkles: Improve nitrate module JSON handling and error management --- backend/src/app/nitrate.clj | 47 +++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 36c2bd0b04..4cb422389f 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -1,6 +1,7 @@ (ns app.nitrate "Module that make calls to the external nitrate aplication" (:require + [app.common.json :as json] [app.common.logging :as l] [app.common.schema :as sm] [app.common.schema.generators :as sg] @@ -9,7 +10,6 @@ [app.http.client :as http] [app.rpc :as-alias rpc] [app.setup :as-alias setup] - [app.util.json :as json] [clojure.core :as c] [integrant.core :as ig])) @@ -27,7 +27,7 @@ "x-profile-id" (str profile-id)} :uri uri :version :http1.1} - (= method :post) (assoc :body (json/encode request-params)))))) + (= method :post) (assoc :body (json/encode request-params :key-fn json/write-camel-key)))))) (defn- with-retries [handler max-retries] @@ -49,15 +49,27 @@ (defn- with-validate [handler uri schema] (fn [] - (let [coercer-http (sm/coercer schema - :type :validation - :hint (str "invalid data received calling " uri))] - (try - (coercer-http (-> (handler) :body json/decode)) - (catch Exception e - ;; TODO Error handling - (l/error :hint "error validating json response" :cause e) - nil))))) + (let [response (handler) + status (:status response)] + (if (>= status 400) + ;; For error status codes (4xx, 5xx), fail immediately without validation + (do + (l/error :hint "nitrate request failed with error status" + :uri uri + :status status + :body (:body response)) + nil) + ;; For success status codes, validate the response + (let [coercer-http (sm/coercer schema + :type :validation + :hint (str "invalid data received calling " uri)) + data (-> response :body (json/decode :key-fn json/read-kebab-key))] + (try + (coercer-http data) + (catch Exception e + ;; TODO Error handling + (l/error :hint "error validating json response" :cause e) + nil))))))) (defn- request-to-nitrate [cfg method uri schema {:keys [::rpc/profile-id request-params] :as params}] @@ -84,13 +96,14 @@ [:map [:id ::sm/uuid] [:name ::sm/text] - [:slug ::sm/text]]) + [:slug ::sm/text] + [:is-your-penpot :boolean]]) (def ^:private schema:team [:map [:id ::sm/uuid] - [:organizationId ::sm/uuid] - [:yourPenpot :boolean]]) + [:organization-id ::sm/uuid] + [:is-your-penpot :boolean]]) ;; TODO Unify with schemas on backend/src/app/http/management.clj (def ^:private schema:timestamp @@ -176,8 +189,8 @@ (defn- set-team-org [cfg {:keys [organization-id team-id is-default] :as params}] (let [baseuri (cf/get :nitrate-backend-uri) - params (assoc params :request-params {:teamId team-id - :yourPenpot (true? is-default)})] + params (assoc params :request-params {:team-id team-id + :is-your-penpot (true? is-default)})] (request-to-nitrate cfg :post (str baseuri "/api/organizations/" @@ -246,7 +259,7 @@ :organization-id (:id org) :organization-name (:name org) :organization-slug (:slug org) - :is-default (or (:is-default team) (true? (:isYourPenpot org)))) + :is-default (or (:is-default team) (true? (:is-your-penpot org)))) team)) (catch Throwable cause (l/error :hint "failed to get team organization info" From 27a934dcfd579093b066c78d67eba782ba6229cb Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 10 Mar 2026 11:20:45 +0000 Subject: [PATCH 035/288] :bug: Fix plugin sandbox freezing CLJS Proxy constructor breaking Transit encoding When the plugin sandbox calls harden() (SES lockdown) on any proxy object returned from the penpot.* API, SES traverses the prototype chain up to Proxy.prototype and freezes the CLJS Proxy constructor function. Transit's typeTag helper later fails with "object is not extensible" when trying to set its cache property on that frozen constructor. Fix by deleting the constructor data property from Proxy.prototype so that harden never traverses to the CLJS Proxy constructor function. Signed-off-by: Andrey Antukh --- frontend/src/app/util/object.cljc | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/util/object.cljc b/frontend/src/app/util/object.cljc index 090effd710..9ff10e7b9f 100644 --- a/frontend/src/app/util/object.cljc +++ b/frontend/src/app/util/object.cljc @@ -466,10 +466,17 @@ #?(:cljs (def Proxy - (app.util.object/class - :name "Proxy" - :extends js/Object - :constructor (constantly nil)))) + (let [ctor (app.util.object/class + :name "Proxy" + :extends js/Object + :constructor (constantly nil))] + ;; Remove the `constructor` data property from the prototype so that + ;; SES `harden` (used by the plugin sandbox) does not traverse from a + ;; proxy instance back to this constructor function and freeze it. + ;; If the constructor is frozen before Transit's `typeTag` helper sets + ;; its cache property, Transit throws "object is not extensible". + (js-delete (.-prototype ctor) "constructor") + ctor))) (defmacro reify "A domain specific variation of reify that creates anonymous objects From f796f7ccb9dcf9e4f927450550920ad63f1de08d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 13 Mar 2026 20:22:15 +0000 Subject: [PATCH 036/288] :bug: Fix "Cannot assign to read only property toString" error in plugins runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The error "Cannot assign to read only property 'toString' of function" occurs during React's commit phase after a plugin is loaded. The root cause is an initialization ordering issue in the SES (Secure EcmaScript) lockdown sequence. When loadPlugin() is called, ses.harden(context) runs first, which transitively freezes everything reachable from the context object — including Function.prototype and Object.prototype — via prototype chain traversal of getter functions. Later, createSandbox() calls ses.hardenIntrinsics(), which attempts to run enablePropertyOverrides() to convert frozen data properties (like Function.prototype.toString) into accessor pairs that work around JavaScript's "override mistake". However, enablePropertyOverrides checks "if (configurable)" before converting, and since Function.prototype is already frozen (all properties have configurable: false), the override taming is silently skipped. This leaves Function.prototype.toString as a frozen non-writable data property, causing any subsequent code that assigns .toString to a function instance in strict mode to throw a TypeError. The fix calls ses.hardenIntrinsics() before ses.harden(context) in loadPlugin(), ensuring override taming installs the accessor pairs on prototype properties before they get frozen. The existing hardenIntrinsics() call in createSandbox() becomes a harmless no-op thanks to the idempotency guard. Signed-off-by: Andrey Antukh --- .../libs/plugins-runtime/src/lib/load-plugin.spec.ts | 1 + plugins/libs/plugins-runtime/src/lib/load-plugin.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts index a46bbc8573..c8d31131a0 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts @@ -22,6 +22,7 @@ vi.mock('./create-plugin', () => ({ vi.mock('./ses.js', () => ({ ses: { harden: vi.fn().mockImplementation((obj) => obj), + hardenIntrinsics: vi.fn(), }, })); diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts index 990a9148a1..df62cd4fca 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts @@ -52,6 +52,16 @@ export const loadPlugin = async function ( closeAllPlugins(); + // hardenIntrinsics must be called BEFORE harden() to ensure that + // override taming (enablePropertyOverrides) converts prototype + // properties like Function.prototype.toString into accessor pairs. + // Without this, harden() would freeze Function.prototype with plain + // data properties, making them non-configurable, which causes + // enablePropertyOverrides to silently skip them when hardenIntrinsics + // runs later — resulting in "Cannot assign to read only property + // 'toString'" errors. + ses.hardenIntrinsics(); + const plugin = await createPlugin( ses.harden(context) as Context, manifest, From 1b223359d9a5b98d270ff9344f0e1145d9e4ba52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 16 Mar 2026 11:12:11 +0100 Subject: [PATCH 037/288] :wrench: Remove staging-render bundle github workflow --- .github/workflows/build-staging-render.yml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .github/workflows/build-staging-render.yml diff --git a/.github/workflows/build-staging-render.yml b/.github/workflows/build-staging-render.yml deleted file mode 100644 index 7e65a518a9..0000000000 --- a/.github/workflows/build-staging-render.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: _STAGING RENDER - -on: - workflow_dispatch: - schedule: - - cron: '36 5-20 * * 1-5' - -jobs: - build-bundle: - uses: ./.github/workflows/build-bundle.yml - secrets: inherit - with: - gh_ref: "staging-render" - build_wasm: "yes" - build_storybook: "yes" From e018253c6b27be507e786c836253796cc4622c4c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 17 Mar 2026 14:28:30 +0100 Subject: [PATCH 038/288] :sparkles: Make mcp plugin always ready to be in multiuser --- frontend/scripts/build | 2 +- frontend/src/debug.cljs | 6 ++++++ mcp/package.json | 2 -- mcp/packages/plugin/package.json | 2 -- mcp/packages/plugin/src/main.ts | 6 +----- mcp/packages/plugin/src/plugin.ts | 6 +----- mcp/packages/plugin/vite.config.ts | 3 --- mcp/packages/server/package.json | 1 - 8 files changed, 9 insertions(+), 19 deletions(-) diff --git a/frontend/scripts/build b/frontend/scripts/build index 6f63930a9a..2cc812e228 100755 --- a/frontend/scripts/build +++ b/frontend/scripts/build @@ -36,7 +36,7 @@ popd pushd ../mcp; rm -rf node_modules; ./scripts/setup -WS_URI="/mcp/ws" pnpm run --filter "mcp-plugin" build:multi-user +WS_URI="/mcp/ws" pnpm run --filter "mcp-plugin" build popd; pushd ../plugins diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index fa2641c7b3..e586dc3d1a 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -462,3 +462,9 @@ (defn print-last-exception [] (some-> errors/last-exception ex/print-throwable)) + + +(defn ^:export dbg + [o] + (app.common.pprint/pprint o {:level 100 :length 100})) + diff --git a/mcp/package.json b/mcp/package.json index e6b7fbe744..c9204b108f 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -10,9 +10,7 @@ "build:multi-user": "pnpm -r run build:multi-user", "build:types": "bash ./scripts/build-types", "start": "pnpm -r --parallel run start", - "start:multi-user": "pnpm -r --parallel --filter \"./packages/*\" run start:multi-user", "bootstrap": "pnpm -r install && pnpm run build && pnpm run start", - "bootstrap:multi-user": "pnpm -r install && pnpm run build:multi-user && pnpm run start:multi-user", "fmt": "prettier --write packages/", "fmt:check": "prettier --check packages/" }, diff --git a/mcp/packages/plugin/package.json b/mcp/packages/plugin/package.json index 2fca8aeaae..2fc0506508 100644 --- a/mcp/packages/plugin/package.json +++ b/mcp/packages/plugin/package.json @@ -5,9 +5,7 @@ "type": "module", "scripts": { "start": "vite build --watch --config vite.config.ts", - "start:multi-user": "cross-env MULTI_USER_MODE=true vite build --watch --config vite.config.ts", "build": "tsc && vite build --config vite.release.config.ts", - "build:multi-user": "tsc && cross-env MULTI_USER_MODE=true vite build --config vite.release.config.ts", "types:check": "tsc --noEmit", "clean": "rm -rf dist/" }, diff --git a/mcp/packages/plugin/src/main.ts b/mcp/packages/plugin/src/main.ts index 13342069a6..1d6c7edf3b 100644 --- a/mcp/packages/plugin/src/main.ts +++ b/mcp/packages/plugin/src/main.ts @@ -4,10 +4,6 @@ import "./style.css"; const searchParams = new URLSearchParams(window.location.search); document.body.dataset.theme = searchParams.get("theme") ?? "light"; -// Determine whether multi-user mode is enabled based on URL parameters -const isMultiUserMode = searchParams.get("multiUser") === "true"; -console.log("Penpot MCP multi-user mode:", isMultiUserMode); - // WebSocket connection management let ws: WebSocket | null = null; const statusElement = document.getElementById("connection-status"); @@ -59,7 +55,7 @@ function connectToMcpServer(baseUrl?: string, token?: string): void { try { let wsUrl = baseUrl || PENPOT_MCP_WEBSOCKET_URL; - if (isMultiUserMode && token) { + if (token) { wsUrl += `?userToken=${encodeURIComponent(token)}`; } diff --git a/mcp/packages/plugin/src/plugin.ts b/mcp/packages/plugin/src/plugin.ts index d579e4e86d..e113f7adc3 100644 --- a/mcp/packages/plugin/src/plugin.ts +++ b/mcp/packages/plugin/src/plugin.ts @@ -8,12 +8,8 @@ mcp?.setMcpStatus("connecting"); */ const taskHandlers: TaskHandler[] = [new ExecuteCodeTaskHandler()]; -// Determine whether multi-user mode is enabled based on build-time configuration -declare const IS_MULTI_USER_MODE: boolean; -const isMultiUserMode = typeof IS_MULTI_USER_MODE !== "undefined" ? IS_MULTI_USER_MODE : false; - // Open the plugin UI (main.ts) -penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}&multiUser=${isMultiUserMode}`, { +penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}`, { width: 158, height: 200, hidden: !!mcp, diff --git a/mcp/packages/plugin/vite.config.ts b/mcp/packages/plugin/vite.config.ts index 38e610f247..128a0f554f 100644 --- a/mcp/packages/plugin/vite.config.ts +++ b/mcp/packages/plugin/vite.config.ts @@ -2,9 +2,7 @@ import { defineConfig } from "vite"; import livePreview from "vite-live-preview"; let WS_URI = process.env.WS_URI || "http://localhost:4402"; -let MULTI_USER_MODE = process.env.MULTI_USER_MODE === "true"; -console.log("Will define IS_MULTI_USER_MODE as:", JSON.stringify(MULTI_USER_MODE)); console.log("Will define PENPOT_MCP_WEBSOCKET_URL as:", JSON.stringify(WS_URI)); export default defineConfig({ @@ -37,7 +35,6 @@ export default defineConfig({ allowedHosts: [], }, define: { - IS_MULTI_USER_MODE: JSON.stringify(process.env.MULTI_USER_MODE === "true"), PENPOT_MCP_WEBSOCKET_URL: JSON.stringify(WS_URI), }, }); diff --git a/mcp/packages/server/package.json b/mcp/packages/server/package.json index edbbe5747f..c64de4c303 100644 --- a/mcp/packages/server/package.json +++ b/mcp/packages/server/package.json @@ -7,7 +7,6 @@ "scripts": { "build:server": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --external:@modelcontextprotocol/* --external:ws --external:express --external:class-transformer --external:class-validator --external:reflect-metadata --external:pino --external:pino-pretty --external:js-yaml --external:sharp", "build": "pnpm run build:server && node scripts/copy-resources.js", - "build:multi-user": "pnpm run build", "build:types": "tsc --emitDeclarationOnly --outDir dist", "start": "node dist/index.js", "start:multi-user": "node dist/index.js --multi-user", From b86898eaf94c046e20dcffd6a72ba2d49c5db08d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 17 Mar 2026 14:28:40 +0100 Subject: [PATCH 039/288] Revert ":bug: Fix "Cannot assign to read only property toString" error in plugins runtime" This reverts commit f796f7ccb9dcf9e4f927450550920ad63f1de08d. --- .../libs/plugins-runtime/src/lib/load-plugin.spec.ts | 1 - plugins/libs/plugins-runtime/src/lib/load-plugin.ts | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts index c8d31131a0..a46bbc8573 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts @@ -22,7 +22,6 @@ vi.mock('./create-plugin', () => ({ vi.mock('./ses.js', () => ({ ses: { harden: vi.fn().mockImplementation((obj) => obj), - hardenIntrinsics: vi.fn(), }, })); diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts index df62cd4fca..990a9148a1 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts @@ -52,16 +52,6 @@ export const loadPlugin = async function ( closeAllPlugins(); - // hardenIntrinsics must be called BEFORE harden() to ensure that - // override taming (enablePropertyOverrides) converts prototype - // properties like Function.prototype.toString into accessor pairs. - // Without this, harden() would freeze Function.prototype with plain - // data properties, making them non-configurable, which causes - // enablePropertyOverrides to silently skip them when hardenIntrinsics - // runs later — resulting in "Cannot assign to read only property - // 'toString'" errors. - ses.hardenIntrinsics(); - const plugin = await createPlugin( ses.harden(context) as Context, manifest, From 7480be0bdaa347319afe1d3381825d578a1bb882 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 17 Mar 2026 14:51:51 +0100 Subject: [PATCH 040/288] :bug: Fix mcp bundle build issue introduced in previous commits --- mcp/scripts/build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp/scripts/build b/mcp/scripts/build index 41af7a4684..84181f2d84 100755 --- a/mcp/scripts/build +++ b/mcp/scripts/build @@ -25,7 +25,7 @@ set -e popd pnpm -r --filter "!mcp-plugin" install; -pnpm -r --filter "mcp-server" run build:multi-user; +pnpm -r --filter "mcp-server" run build; rsync -avr packages/server/dist/ ./dist/; From d6cc46902779a34ccbaba1144c667f67c2820f75 Mon Sep 17 00:00:00 2001 From: girafic Date: Tue, 17 Mar 2026 15:05:26 +0100 Subject: [PATCH 041/288] :bug: Fix permission message and update ruler guide proxy name on plugins api (#8632) - Updated the error message for missing content write permission in the removeRulerGuide function. - Renamed the ruler guide proxy from "RuleGuideProxy" to "RulerGuideProxy" for consistency. - Adjusted variable naming in the addRulerGuide function for clarity. Signed-off-by: Stas Haas --- frontend/src/app/plugins/page.cljs | 2 +- frontend/src/app/plugins/ruler_guides.cljs | 2 +- frontend/src/app/plugins/shape.cljs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/plugins/page.cljs b/frontend/src/app/plugins/page.cljs index b0302a1939..c7a946d348 100644 --- a/frontend/src/app/plugins/page.cljs +++ b/frontend/src/app/plugins/page.cljs @@ -331,7 +331,7 @@ (u/display-not-valid :removeRulerGuide "Guide not provided") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :removeRulerGuide "Plugin doesn't have 'comment:write' permission") + (u/display-not-valid :removeRulerGuide "Plugin doesn't have 'content:write' permission") :else (let [guide (u/proxy->ruler-guide value)] diff --git a/frontend/src/app/plugins/ruler_guides.cljs b/frontend/src/app/plugins/ruler_guides.cljs index d9c8e7c6c4..c42e148e56 100644 --- a/frontend/src/app/plugins/ruler_guides.cljs +++ b/frontend/src/app/plugins/ruler_guides.cljs @@ -24,7 +24,7 @@ (defn ruler-guide-proxy [plugin-id file-id page-id id] - (obj/reify {:name "RuleGuideProxy"} + (obj/reify {:name "RulerGuideProxy"} :$plugin {:enumerable false :get (constantly plugin-id)} :$file {:enumerable false :get (constantly file-id)} :$page {:enumerable false :get (constantly page-id)} diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index c7730d8a36..29e560bf35 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -1267,7 +1267,7 @@ (u/display-not-valid :addRulerGuide "Plugin doesn't have 'content:write' permission") :else - (let [id (uuid/next) + (let [ruler-id (uuid/next) axis (parser/orientation->axis orientation) objects (u/locate-objects file-id page-id) frame (get objects id) @@ -1275,11 +1275,11 @@ position (+ board-pos value)] (st/emit! (dwgu/update-guides - {:id id + {:id ruler-id :axis axis :position position :frame-id id})) - (rg/ruler-guide-proxy plugin-id file-id page-id id))))) + (rg/ruler-guide-proxy plugin-id file-id page-id ruler-id))))) :removeRulerGuide (fn [_ value] From 3c92c98c94392db2058925a3953cc728cd180b43 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 17 Mar 2026 15:30:26 +0100 Subject: [PATCH 042/288] :sparkles: Revert several changes to mcp scripts introduced in previous commits --- mcp/package.json | 2 ++ mcp/packages/plugin/package.json | 1 + 2 files changed, 3 insertions(+) diff --git a/mcp/package.json b/mcp/package.json index c9204b108f..31764661b9 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -10,7 +10,9 @@ "build:multi-user": "pnpm -r run build:multi-user", "build:types": "bash ./scripts/build-types", "start": "pnpm -r --parallel run start", + "start:multi-user": "pnpm -r --parallel run start:multi-user", "bootstrap": "pnpm -r install && pnpm run build && pnpm run start", + "bootstrap:multi-user": "pnpm -r install && pnpm run build && pnpm run start:multi-user", "fmt": "prettier --write packages/", "fmt:check": "prettier --check packages/" }, diff --git a/mcp/packages/plugin/package.json b/mcp/packages/plugin/package.json index 2fc0506508..0ccf276181 100644 --- a/mcp/packages/plugin/package.json +++ b/mcp/packages/plugin/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "start": "vite build --watch --config vite.config.ts", + "start:multi-user": "pnpm run start", "build": "tsc && vite build --config vite.release.config.ts", "types:check": "tsc --noEmit", "clean": "rm -rf dist/" From 5eecd52743099eafa53bd189993593918cf07538 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Tue, 17 Mar 2026 18:25:18 +0100 Subject: [PATCH 043/288] :sparkles: Add get-teams-summary to nitrate api (#8662) --- backend/src/app/rpc/management/nitrate.clj | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index c70b617c7c..563653c65d 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -160,3 +160,55 @@ (let [current-user-id (-> (profile/get-profile cfg profile-id) :id)] (db/exec! cfg [sql:get-managed-profiles current-user-id current-user-id]))) +;; ---- API: get-teams-summary + +(def ^:private sql:get-teams-summary + "SELECT t.id, t.name + FROM team AS t + WHERE t.id = ANY(?) + AND t.deleted_at IS NULL;") + +(def ^:private sql:get-files-count + "SELECT COUNT(f.*) AS count + FROM file AS f + JOIN project AS p ON f.project_id = p.id + JOIN team AS t ON t.id = p.team_id + WHERE p.team_id = ANY(?) + AND t.deleted_at IS NULL + AND p.deleted_at IS NULL + AND f.deleted_at IS NULL;") + +(def ^:private schema:get-teams-summary-params + [:map + [:ids [:or ::sm/uuid [:vector ::sm/uuid]]]]) + +(def ^:private schema:get-teams-summary-result + [:map + [:teams [:vector [:map + [:id ::sm/uuid] + [:name ::sm/text]]]] + [:num-files ::sm/int]]) + +(sv/defmethod ::get-teams-summary + "Get summary information for a list of teams" + {::doc/added "2.15" + ::sm/params schema:get-teams-summary-params + ::sm/result schema:get-teams-summary-result} + [cfg {:keys [ids]}] + (let [;; Handle one or multiple params + ids (cond + (uuid? ids) + [ids] + + (and (vector? ids) (every? uuid? ids)) + ids + + :else + [])] + (db/run! cfg (fn [{:keys [::db/conn]}] + (let [ids-array (db/create-array conn "uuid" ids) + teams (db/exec! conn [sql:get-teams-summary ids-array]) + files-count (-> (db/exec-one! conn [sql:get-files-count ids-array]) :count)] + {:teams (mapv #(select-keys % [:id :name]) teams) + :num-files files-count}))))) + From 0f24cf26f6fec8104e398290919cc4ffe2f4e4f2 Mon Sep 17 00:00:00 2001 From: "Dr. Dominik Jain" Date: Tue, 17 Mar 2026 18:48:06 +0100 Subject: [PATCH 044/288] :sparkles: Reduce instructions transferred at MCP connection to a minimum (#8649) * :sparkles: Reduce instructions transferred at MCP connection to a minimum Force on-demand loading of the 'Penpot High-Level Overview', which was previously transferred in the MCP server's instructions. This greatly reduces the number of tokens for users who will not actually interact with Penpot, allowing the MCP server to remain enabled for such users without wasting too many tokens. Resolves #8647 * :paperclip: Update Serena project --- mcp/.serena/project.yml | 9 ++++++++ mcp/packages/server/data/base_instructions.md | 2 ++ .../server/src/ConfigurationLoader.ts | 23 +++++++++++++------ mcp/packages/server/src/PenpotMcpServer.ts | 17 +++++++++----- .../server/src/tools/HighLevelOverviewTool.ts | 2 +- 5 files changed, 39 insertions(+), 14 deletions(-) create mode 100644 mcp/packages/server/data/base_instructions.md diff --git a/mcp/.serena/project.yml b/mcp/.serena/project.yml index a0a980e806..abb5cab52e 100644 --- a/mcp/.serena/project.yml +++ b/mcp/.serena/project.yml @@ -141,3 +141,12 @@ symbol_info_budget: # Note: the backend is fixed at startup. If a project with a different backend # is activated post-init, an error will be returned. language_backend: + +# line ending convention to use when writing source files. +# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default) +# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings. +line_ending: + +# list of regex patterns which, when matched, mark a memory entry as read‑only. +# Extends the list from the global configuration, merging the two lists. +read_only_memory_patterns: [] diff --git a/mcp/packages/server/data/base_instructions.md b/mcp/packages/server/data/base_instructions.md new file mode 100644 index 0000000000..10245a2b45 --- /dev/null +++ b/mcp/packages/server/data/base_instructions.md @@ -0,0 +1,2 @@ +You have access to Penpot tools in order to interact with Penpot designs. +Before working with these tools, be sure to read the 'Penpot High-Level Overview' via the `high_level_overview` tool. diff --git a/mcp/packages/server/src/ConfigurationLoader.ts b/mcp/packages/server/src/ConfigurationLoader.ts index 390522ff24..2b4b11288e 100644 --- a/mcp/packages/server/src/ConfigurationLoader.ts +++ b/mcp/packages/server/src/ConfigurationLoader.ts @@ -4,15 +4,12 @@ import { createLogger } from "./logger.js"; /** * Configuration loader for prompts and server settings. - * - * Handles loading and parsing of YAML configuration files, - * providing type-safe access to configuration values with - * appropriate fallbacks for missing files or values. */ export class ConfigurationLoader { private readonly logger = createLogger("ConfigurationLoader"); private readonly baseDir: string; - private initialInstructions: string; + private readonly initialInstructions: string; + private readonly baseInstructions: string; /** * Creates a new configuration loader instance. @@ -22,6 +19,7 @@ export class ConfigurationLoader { constructor(baseDir: string) { this.baseDir = baseDir; this.initialInstructions = this.loadFileContent(join(this.baseDir, "data", "initial_instructions.md")); + this.baseInstructions = this.loadFileContent(join(this.baseDir, "data", "base_instructions.md")); } private loadFileContent(filePath: string): string { @@ -32,11 +30,22 @@ export class ConfigurationLoader { } /** - * Gets the initial instructions for the MCP server. + * Gets the initial instructions for the MCP server corresponding to the + * 'Penpot High-Level Overview' * - * @returns The initial instructions string, or undefined if not configured + * @returns The initial instructions string */ public getInitialInstructions(): string { return this.initialInstructions; } + + /** + * Gets the base instructions which shall be provided to clients when connecting to + * the MCP server + * + * @returns The initial instructions string + */ + public getBaseInstructions(): string { + return this.baseInstructions; + } } diff --git a/mcp/packages/server/src/PenpotMcpServer.ts b/mcp/packages/server/src/PenpotMcpServer.ts index 2c4a2cc792..4cc08e43b3 100644 --- a/mcp/packages/server/src/PenpotMcpServer.ts +++ b/mcp/packages/server/src/PenpotMcpServer.ts @@ -56,7 +56,8 @@ export class PenpotMcpServer { public readonly pluginBridge: PluginBridge; private readonly replServer: ReplServer; private apiDocs: ApiDocs; - private initialInstructions: string; + private readonly penpotHighLevelOverview: string; + private readonly connectionInstructions: string; /** * Manages session-specific context, particularly user tokens for each request. @@ -82,10 +83,11 @@ export class PenpotMcpServer { this.configLoader = new ConfigurationLoader(process.cwd()); this.apiDocs = new ApiDocs(); - // prepare initial instructions + // prepare instructions let instructions = this.configLoader.getInitialInstructions(); instructions = instructions.replace("$api_types", this.apiDocs.getTypeNames().join(", ")); - this.initialInstructions = instructions; + this.penpotHighLevelOverview = instructions; + this.connectionInstructions = this.configLoader.getBaseInstructions(); this.tools = this.initTools(); @@ -124,8 +126,11 @@ export class PenpotMcpServer { return !this.isRemoteMode(); } - public getInitialInstructions(): string { - return this.initialInstructions; + /** + * Retrieves the high-level overview instructions explaining core Penpot usage. + */ + public getHighLevelOverviewInstructions(): string { + return this.penpotHighLevelOverview; } /** @@ -163,7 +168,7 @@ export class PenpotMcpServer { private createMcpServer(): McpServer { const server = new McpServer( { name: "penpot", version: "1.0.0" }, - { instructions: this.getInitialInstructions() } + { instructions: this.connectionInstructions } ); for (const tool of this.tools) { diff --git a/mcp/packages/server/src/tools/HighLevelOverviewTool.ts b/mcp/packages/server/src/tools/HighLevelOverviewTool.ts index ada8829771..b16edcb68b 100644 --- a/mcp/packages/server/src/tools/HighLevelOverviewTool.ts +++ b/mcp/packages/server/src/tools/HighLevelOverviewTool.ts @@ -21,6 +21,6 @@ export class HighLevelOverviewTool extends Tool { } protected async executeCore(args: EmptyToolArgs): Promise { - return new TextResponse(this.mcpServer.getInitialInstructions()); + return new TextResponse(this.mcpServer.getHighLevelOverviewInstructions()); } } From 5482ee211e8de4578dc17c4c8207902192874764 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 18 Mar 2026 09:53:22 +0100 Subject: [PATCH 045/288] :bug: Fix unexpected corner case between SES hardening and transit (#8663) * Revert ":bug: Fix plugin sandbox freezing CLJS Proxy constructor breaking Transit encoding" This reverts commit 27a934dcfd579093b066c78d67eba782ba6229cb. * :bug: Fix unexpected corner case between SES hardening and transit The cause of the issue is a race condition between plugin loading and the first time js/Date objects are encoded using transit. Transit encoder populates the prototype of the Date object the first time a Date instance is encoded, but if SES freezes the Date prototype before transit, an strange exception will be raised on encoding any object that contains Date instances. Example of the exception: Cannot define property transit$guid$4a57baf3-8824-4930-915a-fa905479a036, object is not extensible --- frontend/src/app/main.cljs | 12 +++++++++++- frontend/src/app/util/object.cljc | 15 ++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 870f8a82bb..7bf4afecc7 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -8,6 +8,8 @@ (:require [app.common.data.macros :as dm] [app.common.logging :as log] + [app.common.time :as ct] + [app.common.transit :as t] [app.common.types.objects-map] [app.common.uuid :as uuid] [app.config :as cf] @@ -100,6 +102,15 @@ (defn ^:export init [options] + ;; WORKAROUND: we set this really not usefull property for signal a + ;; sideffect and prevent GCC remove it. We need it because we need + ;; to populate the Date prototype with transit related properties + ;; before SES hardning is applied on loading MCP plugin + (unchecked-set js/globalThis "penpotStartDate" + (-> (ct/now) + (t/encode-str) + (t/decode-str))) + ;; Before initializing anything, check if the browser has loaded ;; stale JS from a previous deployment. If so, do a hard reload so ;; the browser fetches fresh assets matching the current index.html. @@ -110,7 +121,6 @@ (do (some-> (unchecked-get options "defaultTranslations") (i18n/set-default-translations)) - (mw/init!) (i18n/init) (cur/init-styles) diff --git a/frontend/src/app/util/object.cljc b/frontend/src/app/util/object.cljc index 9ff10e7b9f..090effd710 100644 --- a/frontend/src/app/util/object.cljc +++ b/frontend/src/app/util/object.cljc @@ -466,17 +466,10 @@ #?(:cljs (def Proxy - (let [ctor (app.util.object/class - :name "Proxy" - :extends js/Object - :constructor (constantly nil))] - ;; Remove the `constructor` data property from the prototype so that - ;; SES `harden` (used by the plugin sandbox) does not traverse from a - ;; proxy instance back to this constructor function and freeze it. - ;; If the constructor is frozen before Transit's `typeTag` helper sets - ;; its cache property, Transit throws "object is not extensible". - (js-delete (.-prototype ctor) "constructor") - ctor))) + (app.util.object/class + :name "Proxy" + :extends js/Object + :constructor (constantly nil)))) (defmacro reify "A domain specific variation of reify that creates anonymous objects From 04a3e236fed878ce85331cb5012f7ec1204cd7e7 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Wed, 18 Mar 2026 10:15:31 +0100 Subject: [PATCH 046/288] :sparkles: Add a callback-url parameter to login (#8655) --- frontend/src/app/main/ui/auth/login.cljs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index 63becbc0a6..7812c56634 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -46,12 +46,12 @@ (st/emit! (da/create-demo-profile))) (defn- store-login-redirect - [] + [callback-url] (binding [s/*sync* true] ;; Save the current login raw uri for later redirect user back to ;; the same page, we need it to be synchronous because the user is ;; going to be redirected instantly to the oidc provider uri - (swap! s/session assoc :login-redirect (rt/get-current-href)))) + (swap! s/session assoc :login-redirect (or callback-url (rt/get-current-href))))) (defn- clear-login-redirect [] @@ -76,6 +76,7 @@ error (mf/use-state false) form (fm/use-form :schema schema:login-form :initial initial) + callback-url (:callback-url params) on-error (fn [cause] (let [cause (ex-data cause)] @@ -158,9 +159,9 @@ #(st/emit! (rt/nav :auth-recovery-request)))] - (mf/with-effect [handle-redirect] - (if handle-redirect - (store-login-redirect) + (mf/with-effect [handle-redirect callback-url] + (if (or handle-redirect callback-url) + (store-login-redirect callback-url) (clear-login-redirect))) [:* From df8194acf59643f8fd96ee3c82512b995c14a9f0 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 18 Mar 2026 12:52:58 +0100 Subject: [PATCH 047/288] :bug: Fix several bugs (#8604) * :bug: Fix console warning * :recycle: Use DS buttons and remove deprecated CSS * :bug: Fix copy on update library message * :bug: Fix id prop on switch component * :bug: Fix tooltip shown after tab change --- CHANGES.md | 6 +- .../src/app/main/ui/ds/controls/switch.cljs | 5 +- .../src/app/main/ui/ds/tooltip/tooltip.cljs | 34 ++-- .../src/app/main/ui/workspace/libraries.cljs | 116 +++++++------ .../src/app/main/ui/workspace/libraries.scss | 152 +++++------------- .../workspace/tokens/themes/create_modal.cljs | 3 +- frontend/translations/cs.po | 2 +- frontend/translations/de.po | 2 +- frontend/translations/en.po | 2 +- frontend/translations/es.po | 2 +- frontend/translations/fr.po | 2 +- frontend/translations/fr_CA.po | 2 +- frontend/translations/ha.po | 2 +- frontend/translations/he.po | 4 - frontend/translations/hi.po | 4 - frontend/translations/hr.po | 2 +- frontend/translations/id.po | 2 +- frontend/translations/ig.po | 2 +- frontend/translations/it.po | 2 +- frontend/translations/lv.po | 2 +- frontend/translations/ms.po | 2 +- frontend/translations/nl.po | 2 +- frontend/translations/pt_PT.po | 2 +- frontend/translations/ro.po | 2 +- frontend/translations/ru.po | 4 - frontend/translations/sr.po | 4 - frontend/translations/sv.po | 2 +- frontend/translations/tr.po | 2 +- frontend/translations/ukr_UA.po | 4 - frontend/translations/yo.po | 4 - frontend/translations/zh_CN.po | 4 - frontend/translations/zh_Hant.po | 4 - 32 files changed, 144 insertions(+), 240 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7a118fe18e..e15be66cd8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,7 +35,11 @@ - Fix Alt/Option to draw shapes from center point (by @offreal) [Github #8361](https://github.com/penpot/penpot/pull/8361) - Add token name on broken token pill on sidebar [Taiga #13527](https://tree.taiga.io/project/penpot/issue/13527) - Fix collapsible sidebar property titles not toggling on click [Github #5168](https://github.com/penpot/penpot/issues/5168) - +- Fix `penpot.openPage()` plugin API not navigating in the same tab; change default to same-tab navigation and allow passing a UUID string instead of a Page object [Github #8520](https://github.com/penpot/penpot/issues/8520) +- Fix scroll on library modal [Taiga #13639](https://tree.taiga.io/project/penpot/issue/13639) +- Update copy on penpot update message [Taiga #12924](https://tree.taiga.io/project/penpot/issue/12924) +- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534) +- Fix tooltip shown on tab change [Taiga #13627](https://tree.taiga.io/project/penpot/issue/13627) ## 2.14.0 (Unreleased) diff --git a/frontend/src/app/main/ui/ds/controls/switch.cljs b/frontend/src/app/main/ui/ds/controls/switch.cljs index 8786c8809f..c673497008 100644 --- a/frontend/src/app/main/ui/ds/controls/switch.cljs +++ b/frontend/src/app/main/ui/ds/controls/switch.cljs @@ -16,7 +16,6 @@ (def ^:private schema:switch [:map - [:id {:optional true} :string] [:class {:optional true} :string] [:label {:optional true} [:maybe :string]] [:aria-label {:optional true} [:maybe :string]] @@ -26,10 +25,12 @@ (mf/defc switch* {::mf/schema schema:switch} - [{:keys [id class label aria-label default-checked on-change disabled] :rest props} ref] + [{:keys [class label aria-label default-checked on-change disabled] :rest props} ref] (let [checked* (mf/use-state default-checked) checked? (deref checked*) + id (mf/use-id) + disabled? (d/nilv disabled false) has-label? (not (str/blank? label)) diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs index 087649ca63..968361f865 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs @@ -185,17 +185,18 @@ (mf/use-fn (mf/deps tooltip-id delay) (fn [_] - (let [trigger-el (mf/ref-val trigger-ref)] - (clear-schedule schedule-ref) - (add-schedule schedule-ref (d/nilv delay 300) - (fn [] - (when-let [active @active-tooltip] - (when (not= (:id active) tooltip-id) - (when-let [tooltip-el (dom/get-element (:id active))] - (dom/set-css-property! tooltip-el "display" "none")) - (reset! active-tooltip nil))) - (reset! active-tooltip {:id tooltip-id :trigger trigger-el}) - (reset! visible* true)))))) + (when-not (.-hidden js/document) + (let [trigger-el (mf/ref-val trigger-ref)] + (clear-schedule schedule-ref) + (add-schedule schedule-ref (d/nilv delay 300) + (fn [] + (when-let [active @active-tooltip] + (when (not= (:id active) tooltip-id) + (when-let [tooltip-el (dom/get-element (:id active))] + (dom/set-css-property! tooltip-el "display" "none")) + (reset! active-tooltip nil))) + (reset! active-tooltip {:id tooltip-id :trigger trigger-el}) + (reset! visible* true))))))) on-hide (mf/use-fn @@ -243,6 +244,17 @@ content aria-label)})] + (mf/use-effect + (mf/deps tooltip-id) + (fn [] + (let [handle-visibility-change + (fn [] + (when (.-hidden js/document) + (on-hide)))] + (js/document.addEventListener "visibilitychange" handle-visibility-change) + ;; cleanup + #(js/document.removeEventListener "visibilitychange" handle-visibility-change)))) + (mf/use-effect (mf/deps visible placement offset) (fn [] diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index 99d0e9c3de..6b9532225a 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -32,6 +32,7 @@ [app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.context :as ctx] + [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]] @@ -47,12 +48,6 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) -(def ^:private close-icon - (deprecated-icon/icon-xref :close (stl/css :close-icon))) - -(def ^:private add-icon - (deprecated-icon/icon-xref :add (stl/css :add-icon))) - (defn- get-library-summary "Given a library data return a summary representation of this library" [data] @@ -171,12 +166,12 @@ [:div {:class (stl/css :sample-library-item) :key (dm/str id)} [:div {:class (stl/css :sample-library-item-name)} (:name library)] - [:input {:class (stl/css-case :sample-library-button true - :sample-library-add (nil? importing?) - :sample-library-adding (some? importing?)) - :type "button" - :value (if (= importing? id) (tr "labels.adding") (tr "labels.add")) - :on-click import-library}]])) + + [:> button* {:variant "secondary" + :disabled (some? importing?) + :on-click import-library + :class (stl/css :sample-library-button)} + (if (= importing? id) (tr "labels.adding") (tr "labels.add"))]])) (defn- empty-library? "Check if currentt library summary has elements or not" @@ -341,31 +336,31 @@ [:div {:class (stl/css :section-list-item)} [:div {:class (stl/css :item-content)} - [:div {:class (stl/css :item-name)} (tr "workspace.libraries.file-library")] + [:div {:class (stl/css :item-title)} (tr "workspace.libraries.file-library")] [:ul {:class (stl/css :item-contents)} [:> library-description* {:summary summary}]]] (if ^boolean is-shared - [:input {:class (stl/css :item-unpublish) - :type "button" - :value (tr "common.unpublish") - :on-click unpublish}] - [:input {:class (stl/css :item-publish) - :type "button" - :value (tr "common.publish") - :on-click publish}])] + [:> button* {:variant "secondary" + :type "button" + :on-click unpublish} + (tr "common.unpublish")] + + [:> button* {:variant "primary" + :type "button" + :on-click publish} + (tr "common.publish")])] (for [{:keys [id name data connected-to connected-to-names] :as library} linked-libraries] (let [disabled? (some #(contains? linked-libraries-ids %) connected-to) has-tokens? (and (has-tokens? library) (contains? cf/flags :token-import-from-library))] - [:div {:class (if has-tokens? - (stl/css :section-list-item-double-icon) - (stl/css :section-list-item)) + [:div {:class (stl/css :section-list-item) :key (dm/str id) :data-testid "library-item"} [:div {:class (stl/css :item-content)} - [:div {:class (stl/css :item-name)} name] + [:div {:class (stl/css-case :item-name true + :item-name-short has-tokens?)} name] [:ul {:class (stl/css :item-contents)} (let [summary (get-library-summary data)] [:* @@ -375,23 +370,23 @@ [:span "(" (tr "workspace.libraries.connected-to") " "] [:span {:class (stl/css :connected-to-values)} (str/join ", " connected-to-names)] [:span ")"]])])]] + [:div {:class (stl/css :library-actions)} + (when ^boolean has-tokens? + [:> icon-button* + {:type "button" + :aria-label (tr "workspace.tokens.import-tokens") + :icon i/import-export + :data-library-id (dm/str id) + :variant "secondary" + :on-click import-tokens}]) - (when ^boolean has-tokens? - [:> icon-button* - {:type "button" - :aria-label (tr "workspace.tokens.import-tokens") - :icon i/import-export - :data-library-id (dm/str id) - :variant "secondary" - :on-click import-tokens}]) - - [:> icon-button* {:type "button" - :aria-label (tr "workspace.libraries.unlink-library-btn") - :icon i/detach - :data-library-id (dm/str id) - :variant "secondary" - :disabled disabled? - :on-click unlink-library}]]))]] + [:> icon-button* {:type "button" + :aria-label (tr "workspace.libraries.unlink-library-btn") + :icon i/detach + :data-library-id (dm/str id) + :variant "secondary" + :disabled disabled? + :on-click unlink-library}]]]))]] [:div {:class (stl/css :shared-section)} [:> title-bar* {:collapsable false @@ -415,11 +410,12 @@ (adapt-backend-summary))] [:> library-description* {:summary summary}])]] - [:button {:class (stl/css :item-button-shared) - :data-library-id (dm/str id) - :title (tr "workspace.libraries.shared-library-btn") - :on-click link-library} - add-icon]])] + [:> icon-button* {:class (stl/css :item-button-shared) + :variant "secondary" + :data-library-id (dm/str id) + :icon "add" + :aria-label (tr "workspace.libraries.shared-library-btn") + :on-click link-library}]])] (when (empty? shared-libraries) [:div {:class (stl/css :section-list-empty)} @@ -440,6 +436,7 @@ (for [library sample-libraries] [:> sample-library-entry* {:library library + :key (dm/str (:id library)) :importing importing*}])]] :else @@ -540,17 +537,17 @@ [:div {:class (stl/css :section-list-item) :key (dm/str id)} [:div {:class (stl/css :item-content)} - [:div {:class (stl/css :item-name)} name] + [:div {:class (stl/css :item-name-long)} name] [:ul {:class (stl/css :item-contents)} (describe-library (count components) 0 (count colors) (count typographies))]] - [:button {:type "button" - :class (stl/css :item-update) - :disabled updating? - :data-library-id (dm/str id) - :on-click update} + [:> button* {:class (stl/css :item-update) + :disabled updating? + :variant "primary" + :data-library-id (dm/str id) + :on-click update} (tr "workspace.libraries.update")] [:div {:class (stl/css :libraries-updates)} @@ -684,11 +681,11 @@ :on-click close-dialog-outside :data-testid "libraries-modal"} [:div {:class (stl/css :modal-dialog)} - [:button {:class (stl/css :close-btn) - :on-click close-dialog - :aria-label (tr "labels.close") - :data-testid "close-libraries"} - close-icon] + [:> icon-button* {:class (stl/css :close-btn) + :on-click close-dialog + :aria-label (tr "labels.close") + :variant "ghost" + :icon i/close}] [:div {:class (stl/css :modal-title)} (tr "workspace.libraries.libraries")] @@ -760,5 +757,6 @@ "created in your files previously to this new version."]]] [:div {:class (stl/css :info-bottom)} - [:button {:class (stl/css :primary-button) - :on-click handle-gotit-click} "I GOT IT"]]]]])) + [:> button* {:class (stl/css :primary-button) + :variant "primary" + :on-click handle-gotit-click} "I GOT IT"]]]]])) diff --git a/frontend/src/app/main/ui/workspace/libraries.scss b/frontend/src/app/main/ui/workspace/libraries.scss index 978c2cbcba..599cc8afde 100644 --- a/frontend/src/app/main/ui/workspace/libraries.scss +++ b/frontend/src/app/main/ui/workspace/libraries.scss @@ -4,7 +4,6 @@ // // Copyright (c) KALEIDOS INC -@use "refactor/common-refactor.scss" as deprecated; @use "ds/_sizes.scss" as *; @use "ds/_borders.scss" as *; @use "ds/_utils.scss" as *; @@ -33,7 +32,7 @@ background-color: var(--modal-background-color); border: $b-2 solid var(--modal-border-color); display: grid; - grid-template-rows: auto 1fr; + grid-template-rows: 0 auto 1fr; min-width: $sz-364; min-height: $sz-192; height: $sz-520; @@ -42,23 +41,6 @@ max-width: $sz-712; } -// TODO: Remove this extended creating modal component -.close-btn { - @extend .modal-close-btn-base; -} - -.close-icon { - display: flex; - justify-content: center; - align-items: center; - height: $sz-16; - width: $sz-16; - color: transparent; - fill: none; - stroke-width: $b-1; - stroke: var(--icon-foreground); -} - .modal-title { @include t.use-typography("headline-medium"); margin-block-end: var(--sp-l); @@ -81,12 +63,6 @@ display: grid; grid-template-rows: auto 1fr; gap: var(--sp-s); - - .section-list { - .section-list-item:first-child { - border: none; - } - } } .shared-section { @@ -116,6 +92,10 @@ border-radius: $br-8; } +.section-list-item:first-child { + border: none; +} + .section-list-item-double-icon { @extend .section-list-item; grid-template-columns: 1fr auto auto; @@ -125,44 +105,10 @@ height: fit-content; } -.item-publish, -.item-unpublish { - // TODO: remove this extended by using DS button component - @extend .button-primary; - @include t.use-typography("headline-small"); - height: $sz-32; - min-width: px2rem(92); - padding: var(--sp-s) var(--sp-xxl); - margin: 0; - border-radius: $br-8; -} - -.item-unpublish { - // TODO: remove this extended by using DS button component - @extend .button-secondary; -} - -.item-button, -.item-button-shared { - // TODO: remove this extended by using DS button component - @extend .button-secondary; - height: $sz-32; - width: $sz-32; - margin-inline-start: var(--sp-xxs); - padding: var(--sp-s); -} - -.detach-icon, -.add-icon { - display: flex; - justify-content: center; - align-items: center; - height: $sz-16; - width: $sz-16; - color: transparent; - fill: none; - stroke-width: $b-1; - stroke: var(--icon-foreground); +.close-btn { + position: absolute; + inset-block-start: var(--sp-s); + inset-inline-end: var(--sp-s); } .section-list-shared { @@ -175,26 +121,6 @@ color: var(--title-foreground-color); } -.search-icon { - display: flex; - justify-content: center; - align-items: center; - width: px2rem(20); - padding: 0 0 0 var(--sp-s); - - svg { - display: flex; - justify-content: center; - align-items: center; - color: transparent; - fill: none; - height: px2rem(12); - width: px2rem(12); - stroke-width: 1.33px; - stroke: var(--icon-foreground); - } -} - // empty state .section-list-empty { display: grid; @@ -206,21 +132,13 @@ margin-block: var(--sp-l); } -.library-icon { - display: flex; - justify-content: center; - align-items: center; - color: transparent; - fill: none; - stroke-width: $b-1; - stroke: var(--icon-foreground); - height: $sz-32; - width: $sz-32; -} - // Update library tab .libraries-updates-see-all { - @extend .link; + background: unset; + border: none; + color: var(--link-foreground-color); + cursor: pointer; + text-decoration: none; direction: rtl; grid-column: span 3; margin-block-start: var(--sp-s); @@ -236,7 +154,7 @@ display: grid; grid-column: span 3; grid-template-columns: repeat(auto-fill, minmax(px2rem(160), 1fr)); - gap: deprecated.$s-24; + gap: var(--sp-xxl); margin-block-start: var(--sp-l); } @@ -246,7 +164,7 @@ } .libraries-updates-item { - @include deprecated.bodyLargeTypography; + @include t.use-typography("body-large"); display: grid; grid-template-columns: auto 1fr; align-items: start; @@ -277,22 +195,32 @@ @include t.use-typography("body-large"); @include textEllipsis; margin: 0; - max-width: px2rem(216); + max-width: px2rem(236); + color: var(--library-name-foreground-color); +} + +.item-name-short { + max-width: px2rem(206); +} + +.item-name-long { + @extend .item-name; + max-width: px2rem(450); +} + +.item-title { + @include t.use-typography("body-large"); + margin: 0; color: var(--library-name-foreground-color); } .item-update { - @extend .button-primary; @include t.use-typography("headline-small"); height: $sz-32; min-width: px2rem(92); padding: var(--sp-s) var(--sp-xxl); margin-inline-end: var(--sp-xxs); border-radius: $br-8; - - &:disabled { - @extend .button-disabled; - } } .item-contents { @@ -303,6 +231,11 @@ margin: 0; } +.library-actions { + display: flex; + gap: var(--sp-xs); +} + .element-count { white-space: nowrap; @@ -386,7 +319,6 @@ } .primary-button { - @extend .button-primary; @include t.use-typography("headline-small"); padding: 0 var(--sp-l); } @@ -434,16 +366,6 @@ max-width: px2rem(232); } -// TODO: Remove this extended using a DS component -.sample-library-add { - @extend .button-secondary; -} - -// TODO: Remove this extended using a DS component -.sample-library-adding { - @extend .button-disabled; -} - .sample-library-button { @include t.use-typography("headline-small"); height: $sz-32; diff --git a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs index a9fa7e5998..32abd69298 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs @@ -114,8 +114,7 @@ :class (stl/css :theme-row)} [:div {:class (stl/css :theme-switch-row)} - [:> switch* {:id name - :label name + [:> switch* {:label name :on-change on-switch-theme :default-checked selected?}]] diff --git a/frontend/translations/cs.po b/frontend/translations/cs.po index a39f7dc50f..3657d89e77 100644 --- a/frontend/translations/cs.po +++ b/frontend/translations/cs.po @@ -2916,7 +2916,7 @@ msgstr "Přestávka na údržbu: do 5 minut budeme mimo provoz na krátkou údr #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "K dispozici je nová verze, obnovte prosím stránku" +msgstr "K dispozici je nová verze." #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" diff --git a/frontend/translations/de.po b/frontend/translations/de.po index d3b1388070..1a35b21e8e 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -3661,7 +3661,7 @@ msgstr "" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "Eine neue Version ist verfügbar, bitte aktualisieren Sie die Seite" +msgstr "Eine neue Version ist verfügbar." #: src/app/main/ui/dashboard/team.cljs:825 msgid "notifications.invitation-deleted" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index ebe68d176b..1d30cbb441 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3819,7 +3819,7 @@ msgstr "Maintenance break: we will be down for a short maintenance within 5 minu #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "A new version is available, please refresh the page" +msgstr "A new version is available." #: src/app/main/ui/dashboard/team.cljs:825 msgid "notifications.invitation-deleted" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 2721f60649..ee377bef15 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3776,7 +3776,7 @@ msgstr "" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "Una nueva versión está disponible, por favor actualiza la página" +msgstr "Una nueva versión está disponible." #: src/app/main/ui/dashboard/team.cljs:825 msgid "notifications.invitation-deleted" diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 98b90ac8a9..29010e9d1c 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -3717,7 +3717,7 @@ msgstr "" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "Il y a une nouvelle version disponible. Rafraîchissez la page" +msgstr "Il y a une nouvelle version disponible." #: src/app/main/ui/dashboard/team.cljs:825 msgid "notifications.invitation-deleted" diff --git a/frontend/translations/fr_CA.po b/frontend/translations/fr_CA.po index 999ea56579..c592b77f2b 100644 --- a/frontend/translations/fr_CA.po +++ b/frontend/translations/fr_CA.po @@ -3690,7 +3690,7 @@ msgstr "" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "Une nouvelle version est disponible. Merci de rafraîchir la page" +msgstr "Une nouvelle version est disponible." #: src/app/main/ui/dashboard/team.cljs:825 msgid "notifications.invitation-deleted" diff --git a/frontend/translations/ha.po b/frontend/translations/ha.po index ef5bec5a9c..19e377aa3d 100644 --- a/frontend/translations/ha.po +++ b/frontend/translations/ha.po @@ -2269,7 +2269,7 @@ msgstr "sabunta sashe a babbar taska" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "akwai sabon yayi, fatan za a sabunta fage" +msgstr "akwai sabon yayi." #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" diff --git a/frontend/translations/he.po b/frontend/translations/he.po index d7fe28a169..a288628e94 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -3465,10 +3465,6 @@ msgstr "כדי לגשת למיזם הזה, אפשר לבקש מבעלי הצוו msgid "notifications.by-code.maintenance" msgstr "הפסקת תחזוקה: המערכת תושבת לעבודת תחזוקה קצרה תוך 5 דקות." -#: src/app/main/data/common.cljs:82 -msgid "notifications.by-code.upgrade-version" -msgstr "יש גרסה חדשה, נא לרענן את העמוד" - #: src/app/main/ui/dashboard/team.cljs:825 msgid "notifications.invitation-deleted" msgstr "ההזמנה נמחקה בהצלחה" diff --git a/frontend/translations/hi.po b/frontend/translations/hi.po index 78d73a4605..77507cfd03 100644 --- a/frontend/translations/hi.po +++ b/frontend/translations/hi.po @@ -3578,10 +3578,6 @@ msgstr "इस परियोजना तक पहुँचने के ल msgid "notifications.by-code.maintenance" msgstr "रखरखाव विराम: हम 5 मिनट के भीतर एक छोटे रखरखाव के लिए बंद रहेंगे।" -#: src/app/main/data/common.cljs:82 -msgid "notifications.by-code.upgrade-version" -msgstr "एक नया संस्करण उपलब्ध है, कृपया पृष्ठ को रिफ्रेश करें" - #: src/app/main/ui/dashboard/team.cljs:825 msgid "notifications.invitation-deleted" msgstr "आमंत्रण सफलतापूर्वक हटा दिया गया" diff --git a/frontend/translations/hr.po b/frontend/translations/hr.po index c9e39a10a3..3420ac275f 100644 --- a/frontend/translations/hr.po +++ b/frontend/translations/hr.po @@ -2913,7 +2913,7 @@ msgstr "" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "Dostupna je nova verzija, molimo osvježite stranicu" +msgstr "Dostupna je nova verzija." #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" diff --git a/frontend/translations/id.po b/frontend/translations/id.po index 199ff13314..8e610f193e 100644 --- a/frontend/translations/id.po +++ b/frontend/translations/id.po @@ -3088,7 +3088,7 @@ msgstr "" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "Versi baru sudah tersedia, silakan muat ulang laman" +msgstr "Versi baru sudah tersedia." #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" diff --git a/frontend/translations/ig.po b/frontend/translations/ig.po index 3519dd29cd..348a6881ad 100644 --- a/frontend/translations/ig.po +++ b/frontend/translations/ig.po @@ -1935,7 +1935,7 @@ msgstr "Kagbuo" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "A new version is available, please refresh the page" +msgstr "A new version is available." #: src/app/main/ui/settings/delete_account.cljs:24 msgid "notifications.profile-deletion-not-allowed" diff --git a/frontend/translations/it.po b/frontend/translations/it.po index efe561e12d..cac516f2bb 100644 --- a/frontend/translations/it.po +++ b/frontend/translations/it.po @@ -3686,7 +3686,7 @@ msgstr "" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "Una nuova versione è disponibile, si prega di ricaricare la pagina" +msgstr "Una nuova versione è disponibile." #: src/app/main/ui/dashboard/team.cljs:825 msgid "notifications.invitation-deleted" diff --git a/frontend/translations/lv.po b/frontend/translations/lv.po index 94b374aa1d..48ae267a5a 100644 --- a/frontend/translations/lv.po +++ b/frontend/translations/lv.po @@ -3389,7 +3389,7 @@ msgstr "" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "Ir pieejama jauna versija, lūgums atsvaidzināt lapu" +msgstr "Ir pieejama jauna versija." #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" diff --git a/frontend/translations/ms.po b/frontend/translations/ms.po index c23e749435..cdfd2be839 100644 --- a/frontend/translations/ms.po +++ b/frontend/translations/ms.po @@ -2332,7 +2332,7 @@ msgstr "Kemas kini komponen dalam pustaka kongsi" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "Versi baharu tersedia, sila muat semula halaman" +msgstr "Versi baharu tersedia." #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" diff --git a/frontend/translations/nl.po b/frontend/translations/nl.po index 68fd89b0d8..ea4c4ba523 100644 --- a/frontend/translations/nl.po +++ b/frontend/translations/nl.po @@ -3711,7 +3711,7 @@ msgstr "" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "Er is een nieuwe versie beschikbaar, vernieuw de pagina" +msgstr "Er is een nieuwe versie beschikbaar." #: src/app/main/ui/dashboard/team.cljs:825 msgid "notifications.invitation-deleted" diff --git a/frontend/translations/pt_PT.po b/frontend/translations/pt_PT.po index c44f021561..c839a9496e 100644 --- a/frontend/translations/pt_PT.po +++ b/frontend/translations/pt_PT.po @@ -2968,7 +2968,7 @@ msgstr "" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "Está disponível uma nova versão, por favor atualiza a página" +msgstr "Está disponível uma nova versão." #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index 9f8c230987..9ec2ddf587 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -3446,7 +3446,7 @@ msgstr "" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "O versiune nouă este valabilă, te rugăm să reîncarci pagina" +msgstr "O versiune nouă este valabilă." #: src/app/main/ui/dashboard/team.cljs:825 msgid "notifications.invitation-deleted" diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index 8b107f92b7..e4d471bde5 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -3108,10 +3108,6 @@ msgstr "" "Технический перерыв: сервис будет недоступен короткое время в течение 5 " "минут." -#: src/app/main/data/common.cljs:82 -msgid "notifications.by-code.upgrade-version" -msgstr "Доступна новая версия, обновите страницу" - #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" msgstr "Приглашение успешно отправлено" diff --git a/frontend/translations/sr.po b/frontend/translations/sr.po index aeea104729..325ad703c3 100644 --- a/frontend/translations/sr.po +++ b/frontend/translations/sr.po @@ -2495,10 +2495,6 @@ msgstr "" msgid "modals.update-remote-component.message" msgstr "Ажурирајте компоненту у дељеној библиотеци" -#: src/app/main/data/common.cljs:82 -msgid "notifications.by-code.upgrade-version" -msgstr "Доступна је нова верзија, молимо Вас да освежите страницу" - #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" msgstr "Позивница је успешно послата" diff --git a/frontend/translations/sv.po b/frontend/translations/sv.po index c55a2c8824..bdf1d0ea0e 100644 --- a/frontend/translations/sv.po +++ b/frontend/translations/sv.po @@ -3501,7 +3501,7 @@ msgstr "" #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "En ny version är tillgänglig, uppdatera sidan" +msgstr "En ny version är tillgänglig." #: src/app/main/ui/dashboard/team.cljs:825 msgid "notifications.invitation-deleted" diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index 96a7594ff9..f8d364d3bf 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -3673,7 +3673,7 @@ msgstr "Bakım arası: 5 dakika içinde kısa bir bakım için kapalı olacağı #: src/app/main/data/common.cljs:82 msgid "notifications.by-code.upgrade-version" -msgstr "Yeni bir sürüm mevcut, lütfen sayfayı yenileyin" +msgstr "Yeni bir sürüm mevcut." #: src/app/main/ui/dashboard/team.cljs:825 msgid "notifications.invitation-deleted" diff --git a/frontend/translations/ukr_UA.po b/frontend/translations/ukr_UA.po index 2e7b736c7f..4769f8e70a 100644 --- a/frontend/translations/ukr_UA.po +++ b/frontend/translations/ukr_UA.po @@ -3262,10 +3262,6 @@ msgstr "" "Перерва на технічне обслуговування: ми закінчимо технічне обслуговування " "протягом 5 хвилин." -#: src/app/main/data/common.cljs:82 -msgid "notifications.by-code.upgrade-version" -msgstr "Нова версія доступна, будь ласка, оновіть сторінку" - #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" msgstr "Запрощення успішно надіслано" diff --git a/frontend/translations/yo.po b/frontend/translations/yo.po index 5714d3f655..494b2d6ace 100644 --- a/frontend/translations/yo.po +++ b/frontend/translations/yo.po @@ -2151,10 +2151,6 @@ msgstr "" msgid "modals.update-remote-component.message" msgstr "Mú ẹ̀yà iyàrá ìkàwé pípín kan dójú ìwọ̀n" -#: src/app/main/data/common.cljs:82 -msgid "notifications.by-code.upgrade-version" -msgstr "Ẹ̀yà tuntun ti wà, jọ̀wọ́ tún sọ ọ́ jí" - #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" msgstr "Ìfipè tí a fi ránńṣẹ́ ti lọ dáadáa" diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po index c32fd42b02..19707c26a6 100644 --- a/frontend/translations/zh_CN.po +++ b/frontend/translations/zh_CN.po @@ -3140,10 +3140,6 @@ msgstr "要访问此项目,您可以询问团队拥有者。" msgid "notifications.by-code.maintenance" msgstr "维护中断:我们将在5分钟内进行短暂维护。" -#: src/app/main/data/common.cljs:82 -msgid "notifications.by-code.upgrade-version" -msgstr "有新版本可用,请刷新页面" - #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" msgstr "成功发送邀请" diff --git a/frontend/translations/zh_Hant.po b/frontend/translations/zh_Hant.po index d5861efe24..2620c9ec0d 100644 --- a/frontend/translations/zh_Hant.po +++ b/frontend/translations/zh_Hant.po @@ -2770,10 +2770,6 @@ msgstr "要存取該項目,您可以詢問團隊老大。" msgid "notifications.by-code.maintenance" msgstr "中斷維護:我們將在5分鐘內進行短暫維護。" -#: src/app/main/data/common.cljs:82 -msgid "notifications.by-code.upgrade-version" -msgstr "有新版本可用,請重新整理頁面" - #: src/app/main/ui/dashboard/team.cljs:170, src/app/main/ui/dashboard/team.cljs:867 msgid "notifications.invitation-email-sent" msgstr "邀請已成功發送" From 2a09f301995d04d55e5190f6276f085c0f0f2a91 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Wed, 18 Mar 2026 12:31:39 +0100 Subject: [PATCH 048/288] :sparkles: Add nitrate endpoint to delete teams keeping your-penpot projects --- backend/src/app/rpc/commands/management.clj | 15 +++ backend/src/app/rpc/management/nitrate.clj | 127 +++++++++++++++++++- 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index 0908b358d7..c4d580c37d 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -339,6 +339,21 @@ ;; --- COMMAND: Move project (defn move-project + "Moves a project from one team to another. + + Performs comprehensive validation including: + - Permission checks on both source and destination teams + - Team compatibility verification between source and destination + - File features compatibility with destination team + + The operation also: + - Updates the project's team assignment + - Cleans up any broken library relations after the move + + Throws: + - :cant-move-to-same-team if trying to move project to its current team + - Permission exceptions if user lacks required permissions + - Team compatibility exceptions if teams are incompatible" [{:keys [::db/conn] :as cfg} {:keys [profile-id team-id project-id] :as params}] (let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]}) pids (->> (db/query conn :project {:team-id (:team-id project)} {:columns [:id]}) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 563653c65d..08fa458dcb 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -8,6 +8,8 @@ "Internal Nitrate HTTP RPC API. Provides authenticated access to organization management and token validation endpoints." (:require + [app.common.data :as d] + [app.common.exceptions :as ex] [app.common.features :as cfeat] [app.common.schema :as sm] [app.common.types.profile :refer [schema:profile, schema:basic-profile]] @@ -18,12 +20,14 @@ [app.msgbus :as mbus] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] + [app.rpc.commands.management :as management] [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.doc :as doc] [app.rpc.quotes :as quotes] [app.util.services :as sv] - [clojure.set :as set])) + [clojure.set :as set] + [cuerdas.core :as str])) ;; ---- API: authenticate @@ -212,3 +216,124 @@ {:teams (mapv #(select-keys % [:id :name]) teams) :num-files files-count}))))) + +;; ---- API: delete-teams-keeping-your-penpot-projects + +(def ^:private sql:get-projects-and-default-teams + "Get projects from specified teams along with their team owner's default team information. + This query: + - Selects projects (id, team_id, name) from teams in the provided list + - Gets the profile_id of each team owner + - Gets the default_team_id where projects should be moved + - Only includes teams where the user is owner + - Only includes projects that contain at least one non-deleted file + - Excludes deleted projects and teams" + "SELECT p.id AS project_id, + p.team_id AS source_team_id, + p.name AS project_name, + tpr.profile_id, + pr.default_team_id + FROM project AS p + JOIN team AS tm ON p.team_id = tm.id + JOIN team_profile_rel AS tpr ON tm.id = tpr.team_id + JOIN profile AS pr ON tpr.profile_id = pr.id + WHERE p.team_id = ANY(?) + AND p.deleted_at IS NULL + AND tm.deleted_at IS NULL + AND tpr.is_owner IS TRUE + AND EXISTS (SELECT 1 FROM file f WHERE f.project_id = p.id AND f.deleted_at IS NULL);") + +(def ^:private sql:delete-teams + "UPDATE team SET deleted_at = ? WHERE id = ANY(?)") + +(def ^:private schema:delete-teams-keeping-your-penpot-projects + [:map + [:org-name ::sm/text] + [:teams [:vector [:map + [:id ::sm/uuid] + [:is-your-penpot ::sm/boolean]]]]]) + +(def ^:private schema:delete-teams-error + [:map + [:error ::sm/keyword] + [:message ::sm/text] + [:cause ::sm/text] + [:project-id {:optional true} ::sm/uuid] + [:project-name {:optional true} ::sm/text] + [:team-id {:optional true} ::sm/uuid] + [:phase {:optional true} [:enum :move-projects :delete-teams]]]) + +(def ^:private schema:delete-teams-result + [:or [:= nil] schema:delete-teams-error]) + +(defn- ^:private clean-org-name + "Clean and sanitize organization name to remove emojis, special characters, + and prevent potential injections. Only allows alphanumeric characters, + spaces, hyphens, underscores, and parentheses." + [org-name] + (when org-name + (-> org-name + str + str/trim + (str/replace #"[^\w\s\-_()]+" "") + (str/replace #"\s+" " ") + str/trim))) + +(sv/defmethod ::delete-teams-keeping-your-penpot-projects + "For a list of teams, move the projects of your-penpot teams to the + default team of each team owner, then delete all provided teams." + {::doc/added "2.15" + ::sm/params schema:delete-teams-keeping-your-penpot-projects + ::sm/result schema:delete-teams-result} + [cfg {:keys [teams org-name ::rpc/request-at]}] + + (let [your-penpot-team-ids (into [] (comp (filter :is-your-penpot) d/xf:map-id) teams) + all-team-ids (into [] d/xf:map-id teams) + cleaned-org-name (clean-org-name org-name) + org-prefix (if (str/empty? cleaned-org-name) + "imported: " + (str cleaned-org-name " imported: "))] + + (when (seq all-team-ids) + + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + + ;; ---- Move projects ---- + (when (seq your-penpot-team-ids) + (let [ids-array (db/create-array conn "uuid" your-penpot-team-ids) + projects (db/exec! conn [sql:get-projects-and-default-teams ids-array])] + + (doseq [{:keys [default-team-id profile-id project-id project-name source-team-id]} projects + :when default-team-id] + + (try + (management/move-project cfg {:profile-id profile-id + :team-id default-team-id + :project-id project-id}) + + (db/update! conn :project + {:is-default false + :name (str org-prefix project-name)} + {:id project-id}) + + (catch Throwable cause + (ex/raise :type :internal + :code :nitrate-project-move-failed + :context {:project-id project-id + :project-name project-name + :team-id source-team-id} + :cause cause)))))) + + ;; ---- Delete teams ---- + (try + (let [team-ids-array (db/create-array conn "uuid" all-team-ids)] + (db/exec-one! conn [sql:delete-teams request-at team-ids-array])) + (catch Throwable cause + (ex/raise :type :internal + :code :nitrate-team-deletion-failed + :context {:team-ids all-team-ids} + :cause cause)))))) + + nil)) + From b876417d5b76c553443f6ddc04879ab4c832db48 Mon Sep 17 00:00:00 2001 From: BitToby <218712309+bittoby@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:19:15 +0200 Subject: [PATCH 049/288] =?UTF-8?q?:sparkles:=20Add=20copy=20and=20paste?= =?UTF-8?q?=20for=20grid=20layout=20rows=20and=20columns=20via=20co?= =?UTF-8?q?=E2=80=A6=20(#8498)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: Add copy and paste for grid layout rows and columns via context menu * :wrench: Use grid-id instead of grid in context menu deps --------- Co-authored-by: bittoby --- common/src/app/common/types/shape/layout.cljc | 36 +++++ .../app/main/data/workspace/shape_layout.cljs | 132 ++++++++++++++++++ .../app/main/ui/workspace/context_menu.cljs | 59 +++++++- frontend/translations/en.po | 9 ++ 4 files changed, 231 insertions(+), 5 deletions(-) diff --git a/common/src/app/common/types/shape/layout.cljc b/common/src/app/common/types/shape/layout.cljc index 384029a688..79aa661755 100644 --- a/common/src/app/common/types/shape/layout.cljc +++ b/common/src/app/common/types/shape/layout.cljc @@ -874,6 +874,42 @@ (duplicate-cells :column index (inc index) ids-map) (assign-cells objects)))) +(defn duplicate-row-at + "Duplicate source row and insert the copy at target-index (0-indexed). + Like `duplicate-row` but inserts at an arbitrary position. + Note: after add-grid-row, if target <= source the source cells shift + by +1, so we must adjust the from-index for duplicate-cells." + [shape objects source-index target-index ids-map] + (let [value (dm/get-in shape [:layout-grid-rows source-index]) + ;; After inserting at target-index, cells at rows >= (inc target-index) + ;; get shifted +1. If target <= source, the source row shifts. + adjusted-source (if (<= target-index source-index) + (inc source-index) + source-index)] + (-> shape + (remove-cell-areas-after :row source-index) + (add-grid-row value target-index) + (duplicate-cells :row adjusted-source target-index ids-map) + (assign-cells objects)))) + +(defn duplicate-column-at + "Duplicate source column and insert the copy at target-index (0-indexed). + Like `duplicate-column` but inserts at an arbitrary position. + Note: after add-grid-column, if target <= source the source cells shift + by +1, so we must adjust the from-index for duplicate-cells." + [shape objects source-index target-index ids-map] + (let [value (dm/get-in shape [:layout-grid-columns source-index]) + ;; After inserting at target-index, cells at columns >= (inc target-index) + ;; get shifted +1. If target <= source, the source column shifts. + adjusted-source (if (<= target-index source-index) + (inc source-index) + source-index)] + (-> shape + (remove-cell-areas-after :column source-index) + (add-grid-column value target-index) + (duplicate-cells :column adjusted-source target-index ids-map) + (assign-cells objects)))) + (defn make-remove-cell [attr span-attr track-num] (fn [[_ cell]] diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index 163195f11f..bbff39663a 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -787,3 +787,135 @@ (dch/commit-changes changes) (ptk/data-event :layout/update {:ids [layout-id]}) (dwu/commit-undo-transaction undo-id)))))) + +(defn complete-rows? + "Check if the selected cells cover complete row(s) — all columns must be included." + [grid cells] + (let [{:keys [first-column last-column]} (ctl/cells-coordinates cells) + num-columns (count (:layout-grid-columns grid))] + (and (= first-column 1) + (= last-column num-columns)))) + +(defn complete-columns? + "Check if the selected cells cover complete column(s) — all rows must be included." + [grid cells] + (let [{:keys [first-row last-row]} (ctl/cells-coordinates cells) + num-rows (count (:layout-grid-rows grid))] + (and (= first-row 1) + (= last-row num-rows)))) + +(defn copy-grid-tracks + "Store the selected track indices for later paste. Works for both + complete rows and complete columns." + [grid-id type] + (assert (#{:row :column} type)) + (ptk/reify ::copy-grid-tracks + ptk/UpdateEvent + (update [_ state] + (let [objects (dsh/lookup-page-objects state) + grid (get objects grid-id) + selected (get-in state [:workspace-grid-edition grid-id :selected]) + cells (->> selected (map #(get-in grid [:layout-grid-cells %]))) + {:keys [first-row last-row first-column last-column]} (ctl/cells-coordinates cells) + ;; Convert 1-indexed cell positions to 0-indexed track indices + track-indices (if (= type :row) + (vec (range (dec first-row) last-row)) + (vec (range (dec first-column) last-column)))] + (assoc-in state [:workspace-grid-edition grid-id :copied-tracks] + {:track-indices track-indices + :type type + :grid-id grid-id}))))) + +(defn paste-grid-tracks + "Paste previously copied tracks at the end of the grid. + Each source track is duplicated and appended after the last + existing track. All operations are grouped in a single undo + transaction. Follows the same pattern as `duplicate-layout-track`." + [grid-id] + (ptk/reify ::paste-grid-tracks + ptk/WatchEvent + (watch [it state _] + (let [file-id (:current-file-id state) + page (dsh/lookup-page state) + objects (:objects page) + libraries (dsh/lookup-libraries state) + library-data (dsh/lookup-file state file-id) + grid (get objects grid-id) + + copied (get-in state [:workspace-grid-edition grid-id :copied-tracks]) + track-indices (:track-indices copied) + type (:type copied) + undo-id (js/Symbol)] + + (when (and (seq track-indices) (some? type)) + (let [shapes-by-track-fn + (if (= type :row) + ctl/shapes-by-row + ctl/shapes-by-column) + + ;; Collect shapes from all source tracks + all-shapes + (->> track-indices + (mapcat #(shapes-by-track-fn grid % false)) + (set)) + + ;; Generate duplication changes for all shapes at once + changes + (-> (pcb/empty-changes it) + (cll/generate-duplicate-changes objects page all-shapes (gpt/point 0 0) libraries library-data file-id) + (cll/generate-duplicate-changes-update-indices objects all-shapes)) + + ;; Build ids-map: old-shape-id -> new-shape-id + ids-map + (->> changes + :redo-changes + (filter #(= (:type %) :add-obj)) + (filter #(all-shapes (:old-id %))) + (map #(vector (:old-id %) (get-in % [:obj :id]))) + (into {})) + + duplicate-at-fn + (if (= type :row) + ctl/duplicate-row-at + ctl/duplicate-column-at) + + tracks-prop + (if (= type :row) + :layout-grid-rows + :layout-grid-columns) + + ;; Sort source indices ascending — we'll append each + ;; copy at the end in order, preserving the original + ;; track ordering in the appended block. + sorted-indices (vec (sort track-indices)) + + changes + (-> changes + (pcb/update-shapes + [grid-id] + (fn [shape objects] + ;; Restore grid structure (duplication may have altered it) + (let [shape (merge shape (select-keys grid [:layout-grid-cells :layout-grid-columns :layout-grid-rows]))] + ;; Append each source track at the end. + ;; Process in ascending order so the copies + ;; appear in the same order as the originals. + ;; Each insertion adds one track, so both the + ;; target index and the source index (if it + ;; comes after the target) shift by 1. + (reduce + (fn [s [offset src-idx]] + (let [;; Source tracks don't shift because we + ;; append after them (target > source). + actual-src src-idx + ;; Append at the end (which grows by + ;; one with each iteration). + target-idx (+ (count (get grid tracks-prop)) offset)] + (duplicate-at-fn s objects actual-src target-idx ids-map))) + shape + (map-indexed vector sorted-indices)))) + {:with-objects? true}))] + + (rx/of (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (ptk/data-event :layout/update {:ids [grid-id]}) + (dwu/commit-undo-transaction undo-id)))))))) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 1fedf00e64..ca426f6e9e 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -779,6 +779,7 @@ [{:keys [mdata]}] (let [{:keys [grid cells]} mdata + grid-id (:id grid) single? (= (count cells) 1) can-merge? @@ -786,17 +787,53 @@ (mf/deps cells) #(ctl/valid-area-cells? cells)) + can-copy-rows? + (mf/use-memo + (mf/deps grid cells) + #(dwsl/complete-rows? grid cells)) + + can-copy-columns? + (mf/use-memo + (mf/deps grid cells) + #(dwsl/complete-columns? grid cells)) + + grid-edition-ref + (mf/use-memo + (mf/deps grid-id) + #(refs/workspace-grid-edition-id grid-id)) + + grid-edition (mf/deref grid-edition-ref) + has-copied-tracks? (some? (:copied-tracks grid-edition)) + do-merge-cells (mf/use-fn - (mf/deps grid cells) + (mf/deps grid-id cells) (fn [] - (st/emit! (dwsl/merge-cells (:id grid) (map :id cells))))) + (st/emit! (dwsl/merge-cells grid-id (map :id cells))))) do-create-board (mf/use-fn - (mf/deps grid cells) + (mf/deps grid-id cells) (fn [] - (st/emit! (dwsl/create-cell-board (:id grid) (map :id cells)))))] + (st/emit! (dwsl/create-cell-board grid-id (map :id cells))))) + + do-copy-rows + (mf/use-fn + (mf/deps grid-id) + (fn [] + (st/emit! (dwsl/copy-grid-tracks grid-id :row)))) + + do-copy-columns + (mf/use-fn + (mf/deps grid-id) + (fn [] + (st/emit! (dwsl/copy-grid-tracks grid-id :column)))) + + do-paste-tracks + (mf/use-fn + (mf/deps grid-id) + (fn [] + (st/emit! (dwsl/paste-grid-tracks grid-id))))] [:* (when (not single?) [:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.merge") @@ -809,7 +846,19 @@ [:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.create-board") :on-click do-create-board - :disabled (and (not single?) (not can-merge?))}]])) + :disabled (and (not single?) (not can-merge?))}] + + [:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.copy-rows") + :on-click do-copy-rows + :disabled (not can-copy-rows?)}] + + [:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.copy-columns") + :on-click do-copy-columns + :disabled (not can-copy-columns?)}] + + [:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.paste-tracks") + :on-click do-paste-tracks + :disabled (not has-copied-tracks?)}]])) ;; FIXME: optimize because it is rendered always diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 1d30cbb441..eeff80eeed 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5589,6 +5589,15 @@ msgstr "Create board" msgid "workspace.context-menu.grid-cells.merge" msgstr "Merge cells" +msgid "workspace.context-menu.grid-cells.copy-rows" +msgstr "Copy rows" + +msgid "workspace.context-menu.grid-cells.copy-columns" +msgstr "Copy columns" + +msgid "workspace.context-menu.grid-cells.paste-tracks" +msgstr "Paste" + #: src/app/main/ui/workspace/context_menu.cljs:754 msgid "workspace.context-menu.grid-track.column.add-after" msgstr "Add 1 column to the right" From 8e7e6ffc2fc540417e8f2c3a7502098320e58c82 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 19 Mar 2026 16:46:18 +0100 Subject: [PATCH 050/288] :recycle: Design review for numeric inputs (#8630) * :recycle: Update tooltip position on icon buttons * :recycle: Sort token groups by priority not alphabetically * :recycle: Add proper padding on text-icon-inputs * :recycle: Hide detach button when dropdown is open * :bug: Fix detach stroke width * :bug: Fix strokes applied on all rows * :bug: Fix nillable inputs * :bug: Fix comments on PR --- common/src/app/common/types/token.cljc | 50 +++++------ .../main/ui/ds/controls/numeric_input.cljs | 86 ++----------------- .../main/ui/ds/controls/numeric_input.scss | 5 +- .../ui/ds/controls/utilities/token_field.cljs | 7 +- .../ui/ds/controls/utilities/token_field.scss | 3 + .../src/app/main/ui/ds/tooltip/tooltip.cljs | 4 +- .../ui/workspace/sidebar/options/common.cljs | 16 ++++ .../sidebar/options/menus/border_radius.cljs | 26 +++--- .../options/menus/input_wrapper_tokens.cljs | 9 +- .../options/menus/layout_container.cljs | 23 ++--- .../sidebar/options/menus/layout_item.cljs | 40 ++++----- .../sidebar/options/menus/stroke.cljs | 5 +- .../sidebar/options/rows/color_row.cljs | 11 +-- .../sidebar/options/rows/stroke_row.cljs | 16 ++-- .../management/forms/controls/utils.cljs | 30 ++++--- 15 files changed, 131 insertions(+), 200 deletions(-) diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 55ecc842e7..15e5168a0b 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -522,31 +522,31 @@ (def tokens-by-input "A map from input name to applicable token for that input." - {:width #{:sizing :dimensions} - :height #{:sizing :dimensions} - :max-width #{:sizing :dimensions} - :max-height #{:sizing :dimensions} - :min-width #{:sizing :dimensions} - :min-height #{:sizing :dimensions} - :x #{:dimensions} - :y #{:dimensions} - :rotation #{:number :rotation} - :border-radius #{:border-radius :dimensions} - :row-gap #{:spacing :dimensions} - :column-gap #{:spacing :dimensions} - :horizontal-padding #{:spacing :dimensions} - :vertical-padding #{:spacing :dimensions} - :sided-paddings #{:spacing :dimensions} - :horizontal-margin #{:spacing :dimensions} - :vertical-margin #{:spacing :dimensions} - :sided-margins #{:spacing :dimensions} - :line-height #{:line-height :number} - :opacity #{:opacity} - :stroke-width #{:stroke-width :dimensions} - :font-size #{:font-size} - :letter-spacing #{:letter-spacing} - :fill #{:color} - :stroke-color #{:color}}) + {:width [:sizing :dimensions] + :height [:sizing :dimensions] + :max-width [:sizing :dimensions] + :max-height [:sizing :dimensions] + :min-width [:sizing :dimensions] + :min-height [:sizing :dimensions] + :x [:dimensions] + :y [:dimensions] + :rotation [:rotation :number] + :border-radius [:border-radius :dimensions] + :row-gap [:spacing :dimensions] + :column-gap [:spacing :dimensions] + :horizontal-padding [:spacing :dimensions] + :vertical-padding [:spacing :dimensions] + :sided-paddings [:spacing :dimensions] + :horizontal-margin [:spacing :dimensions] + :vertical-margin [:spacing :dimensions] + :sided-margins [:spacing :dimensions] + :line-height [:line-height :number] + :opacity [:opacity] + :stroke-width [:stroke-width :dimensions] + :font-size [:font-size] + :letter-spacing [:letter-spacing] + :fill [:color] + :stroke-color [:color]}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HELPERS for tokens application diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs index 5ffed93c6c..cb86f6bdab 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs @@ -19,6 +19,7 @@ [app.main.ui.ds.controls.utilities.token-field :refer [token-field*]] [app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i] [app.main.ui.formats :as fmt] + [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.keyboard :as kbd] @@ -83,48 +84,6 @@ (str/replace #"^\{" "") (str/replace #"\}$" ""))) -(defn- token->dropdown-option - [token] - {:id (str (get token :id)) - :type :token - :resolved-value (get token :resolved-value) - :name (get token :name)}) - -(defn- generate-dropdown-options - [tokens no-sets] - (if (empty? tokens) - [{:type :empty - :label (if no-sets - (tr "ds.inputs.numeric-input.no-applicable-tokens") - (tr "ds.inputs.numeric-input.no-matches"))}] - (->> tokens - (map (fn [[type items]] - (cons {:group true - :type :group - :id (dm/str "group-" (name type)) - :name (name type)} - (map token->dropdown-option items)))) - (interpose [{:separator true - :id "separator" - :type :separator}]) - (apply concat) - (vec) - (not-empty)))) - -(defn- extract-partial-brace-text - [s] - (when-let [start (str/last-index-of s "{")] - (subs s (inc start)))) - -(defn- filter-token-groups-by-name - [tokens filter-text] - (let [lc-filter (str/lower filter-text)] - (into {} - (keep (fn [[group tokens]] - (let [filtered (filter #(str/includes? (str/lower (:name %)) lc-filter) tokens)] - (when (seq filtered) - [group filtered])))) - tokens))) (defn- focusable-option? [option] @@ -149,31 +108,6 @@ j))) indices))) -(defn- sort-groups-and-tokens - "Sorts both the groups and the tokens inside them alphabetically. - - Input: - A map where: - - keys are groups (keywords or strings, e.g. :dimensions, :colors) - - values are vectors of token maps, each containing at least a :name key - - Example input: - {:dimensions [{:name \"tres\"} {:name \"quini\"}] - :colors [{:name \"azul\"} {:name \"rojo\"}]} - - Output: - A sorted map where: - - groups are ordered alphabetically by key - - tokens inside each group are sorted alphabetically by :name - - Example output: - {:colors [{:name \"azul\"} {:name \"rojo\"}] - :dimensions [{:name \"quini\"} {:name \"tres\"}]}" - - [groups->tokens] - (into (sorted-map) ;; ensure groups are ordered alphabetically by their key - (for [[group tokens] groups->tokens] - [group (sort-by :name tokens)]))) (def ^:private schema:icon [:and :string [:fn #(contains? icon-list %)]]) @@ -288,16 +222,7 @@ dropdown-options (mf/with-memo [tokens filter-id] - (delay - (let [tokens (if (delay? tokens) @tokens tokens) - - sorted-tokens (sort-groups-and-tokens tokens) - partial (extract-partial-brace-text filter-id) - options (if (seq partial) - (filter-token-groups-by-name sorted-tokens partial) - sorted-tokens) - no-sets? (nil? sorted-tokens)] - (generate-dropdown-options options no-sets?)))) + (csu/get-token-dropdown-options tokens filter-id)) selected-id* (mf/use-state (fn [] @@ -649,6 +574,7 @@ :icon i/tokens :tooltip-class (stl/css :button-tooltip) :class (stl/css :invisible-button) + :tooltip-placement "top-left" :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown") :ref open-dropdown-ref :on-click open-dropdown}]))) @@ -676,6 +602,7 @@ :on-blur on-blur :class inner-class :property property + :is-open is-open :slot-start (when (or icon text-icon) (mf/html (cond @@ -714,6 +641,11 @@ (when-let [node (mf/ref-val ref)] (dom/set-value! node value')))) + (mf/with-effect [applied-token] + (when (nil? applied-token) + (reset! token-applied* nil) + (reset! selected-id* nil))) + (mf/with-layout-effect [on-mouse-wheel] (when-let [node (mf/ref-val ref)] (let [key (events/listen node "wheel" on-mouse-wheel #js {:passive false})] diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.scss b/frontend/src/app/main/ui/ds/controls/numeric_input.scss index 7825f6a7fb..bbec005618 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.scss +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.scss @@ -35,9 +35,10 @@ .text-icon { color: var(--color-foreground-secondary); - @include t.use-typography("code-font"); + @include t.use-typography("body-small"); inline-size: fit-content; - min-inline-size: px2rem(40); + min-inline-size: px2rem(46); + padding-inline-start: var(--sp-xs); } .invisible-button { diff --git a/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs b/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs index 7af90350e4..3e825f2b38 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs +++ b/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs @@ -25,6 +25,7 @@ [:property {:optional true} [:maybe :string]] [:value :any] [:disabled {:optional true} :boolean] + [:is-open {:optional true} :boolean] [:slot-start {:optional true} [:maybe some?]] [:on-click {:optional true} fn?] [:on-token-key-down fn?] @@ -36,7 +37,7 @@ {::mf/schema schema:token-field} [{:keys [id label value slot-start disabled class on-click on-token-key-down on-blur detach-token - token-wrapper-ref token-detach-btn-ref on-focus property]}] + token-wrapper-ref token-detach-btn-ref on-focus property is-open]}] (let [set-active? (some? id) content (if set-active? label @@ -88,9 +89,11 @@ (when-not ^boolean disabled [:> icon-button* {:variant "ghost" - :class (stl/css :invisible-button) + :class (stl/css-case :invisible-button true + :invisible-btn-dropdown-open is-open) :tooltip-class (stl/css :button-tooltip) :icon i/broken-link :ref token-detach-btn-ref + :tooltip-placement "top-left" :aria-label (tr "ds.inputs.token-field.detach-token") :on-click detach-token}])]])) diff --git a/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss b/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss index e96c5b583e..4233b2305d 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss +++ b/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss @@ -136,6 +136,9 @@ --opacity-button: 1; } } +.invisible-btn-dropdown-open { + --opacity-button: 0; +} .content-wrapper { inline-size: 100%; diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs index 968361f865..9fe9ff3fbf 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs @@ -172,7 +172,7 @@ (deref placement*) delay - (d/nilv delay 300) + (d/nilv delay 700) schedule-ref (mf/use-ref nil) @@ -188,7 +188,7 @@ (when-not (.-hidden js/document) (let [trigger-el (mf/ref-val trigger-ref)] (clear-schedule schedule-ref) - (add-schedule schedule-ref (d/nilv delay 300) + (add-schedule schedule-ref delay (fn [] (when-let [active @active-tooltip] (when (not= (:id active) tooltip-id) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs index 7ea8e42132..065984f819 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs @@ -7,6 +7,8 @@ (ns app.main.ui.workspace.sidebar.options.common (:require-macros [app.main.style :as stl]) (:require + [app.main.data.workspace.tokens.application :as dwta] + [app.main.store :as st] [app.util.dom :as dom] [rumext.v2 :as mf])) @@ -24,3 +26,17 @@ :ref ref} children]))) +(defn emit-value-or-token [value emit-value-fn ids attrs] + (cond + (nil? value) + (emit-value-fn nil) + + (or (string? value) (number? value)) + (emit-value-fn value) + + :else + (st/emit! + (dwta/toggle-token {:token (first value) + :attrs attrs + :shape-ids ids})))) + diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs index d1d7a55fc5..13c260726f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs @@ -11,6 +11,7 @@ [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.hooks :as hooks] + [app.main.ui.workspace.sidebar.options.common :as soc] [app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]] [app.util.i18n :as i18n :refer [tr]] [beicon.v2.core :as rx] @@ -130,26 +131,21 @@ (mf/use-fn (mf/deps change-radius ids) (fn [value] - (if (or (string? value) (number? value)) - (st/emit! - (change-radius (fn [shape] - (ctsr/set-radius-to-all-corners shape value)))) - (st/emit! - (dwta/toggle-token {:token (first value) - :attrs #{:r1 :r2 :r3 :r4} - :shape-ids ids}))))) - + (soc/emit-value-or-token + value + #(st/emit! (change-radius (fn [shape] (ctsr/set-radius-to-all-corners shape %)))) + ids + #{:r1 :r2 :r3 :r4}))) on-single-radius-change (mf/use-fn (mf/deps change-one-radius ids) (fn [value attr] - (if (or (string? value) (number? value)) - (st/emit! (change-one-radius #(ctsr/set-radius-to-single-corner % attr value) attr)) - (st/emit! (st/emit! - (dwta/toggle-token {:token (first value) - :attrs #{attr} - :shape-ids ids})))))) + (soc/emit-value-or-token + value + #(st/emit! (change-one-radius (fn [shape] (ctsr/set-radius-to-single-corner shape attr %)) attr)) + ids + #{attr}))) on-radius-r1-change #(on-single-radius-change % :r1) on-radius-r2-change #(on-single-radius-change % :r2) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.cljs index be7c58ebb2..099a38b5c9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.cljs @@ -1,9 +1,9 @@ (ns app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens (:require-macros [app.main.style :as stl]) (:require - [app.common.types.token :as tk] [app.main.ui.context :as muc] [app.main.ui.ds.controls.numeric-input :refer [numeric-input*]] + [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) @@ -11,11 +11,8 @@ [{:keys [value attr applied-token align on-detach placeholder input-type class] :rest props}] (let [tokens (mf/use-ctx muc/active-tokens-by-type) - tokens (mf/with-memo [tokens input-type] - (delay - (-> (deref tokens) - (select-keys (get tk/tokens-by-input (or input-type attr))) - (not-empty)))) + tokens (mf/with-memo [tokens input-type attr] + (csu/filter-tokens-for-input tokens (or input-type attr))) on-detach-attr (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs index e07c3cd958..568bea26da 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs @@ -30,6 +30,7 @@ [app.main.ui.formats :as fmt] [app.main.ui.hooks :as h] [app.main.ui.icons :as deprecated-icon] + [app.main.ui.workspace.sidebar.options.common :as soc] [app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -335,15 +336,8 @@ (mf/use-fn (mf/deps on-change ids) (fn [value attr event] - (if (or (string? value) (number? value)) - (on-change :simple attr value event) - (do - (st/emit! - (dwta/toggle-token {:token (first value) - :attrs (if (= :p1 attr) - #{:p1 :p3} - #{:p2 :p4}) - :shape-ids ids})))))) + (let [on-change-fn #(on-change :simple attr % event)] + (soc/emit-value-or-token value on-change-fn ids #{attr})))) on-detach-token (mf/use-fn @@ -719,15 +713,8 @@ (mf/use-fn (mf/deps on-change wrap-type ids) (fn [value event attr] - (if (or (string? value) (number? value)) - (on-change (= "nowrap" wrap-type) attr value event) - (do - (st/emit! - (dwta/toggle-token {:token (first value) - :attrs (if (= "nowrap" wrap-type) - #{:row-gap :colum-gap} - #{attr}) - :shape-ids ids})))))) + (let [on-change-fn #((on-change (= "nowrap" wrap-type) attr % event))] + (soc/emit-value-or-token value on-change-fn ids #{attr})))) on-detach-token (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs index 8af6905212..d30939e51c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs @@ -21,6 +21,7 @@ [app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.icons :as deprecated-icon] + [app.main.ui.workspace.sidebar.options.common :as soc] [app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]] [app.main.ui.workspace.sidebar.options.menus.layout-container :refer [get-layout-flex-icon]] [app.util.dom :as dom] @@ -117,15 +118,11 @@ (mf/use-fn (mf/deps on-change ids) (fn [value attr] - (if (or (string? value) (number? value)) - (on-change :simple attr value) - (do - (st/emit! - (dwta/toggle-token {:token (first value) - :attrs (if (= :m1 attr) - #{:m1 :m3} - #{:m2 :m4}) - :shape-ids ids})))))) + (soc/emit-value-or-token + value + #(on-change :simple attr %) + ids + (if (= :m1 attr) #{:m1 :m3} #{:m2 :m4})))) on-focus-m1 (mf/use-fn (mf/deps on-focus) #(on-focus :m1)) @@ -247,14 +244,11 @@ (mf/use-fn (mf/deps on-change ids) (fn [value attr] - (if (or (string? value) (number? value)) - (on-change :multiple attr value) - (do - (st/emit! - (dwta/toggle-token {:token (first value) - :attrs #{attr} - :shape-ids ids})))))) - + (soc/emit-value-or-token + value + #(on-change :multiple attr %) + ids + #{attr}))) on-m1-change (mf/use-fn (mf/deps on-change') #(on-change' % :m1)) @@ -577,13 +571,11 @@ (mf/use-fn (mf/deps ids) (fn [value attr] - (if (or (string? value) (number? value)) - (st/emit! (dwsl/update-layout-child ids {attr value})) - (do - (st/emit! - (dwta/toggle-token {:token (first value) - :attrs #{attr} - :shape-ids ids})))))) + (soc/emit-value-or-token + value + #(st/emit! (dwsl/update-layout-child ids {attr %})) + ids + #{attr}))) on-layout-item-min-w-change (mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-min-w)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index c35bac471e..96cf70f8f5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -168,6 +168,7 @@ on-blur (fn [_] (reset! disable-drag false)) + on-detach-token (mf/use-fn (mf/deps ids) @@ -205,7 +206,7 @@ (seq strokes) [:> h/sortable-container* {} (for [[index value] (d/enumerate (:strokes values []))] - [:> stroke-row* {:key (dm/str "stroke-" index) + [:> stroke-row* {:key (dm/str "stroke-" index "-" (hash applied-tokens)) :stroke value :title (tr "workspace.options.stroke-color") :index index @@ -222,7 +223,7 @@ :on-stroke-cap-start-change on-stroke-cap-start-change :on-stroke-cap-end-change on-stroke-cap-end-change :on-stroke-cap-switch on-stroke-cap-switch - :applied-tokens applied-tokens + :applied-tokens (when (= 0 index) applied-tokens) :on-detach-token on-detach-token :on-remove on-remove :on-reorder handle-reorder diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index ce2a8ae403..f2f4df6423 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -11,7 +11,6 @@ [app.common.data.macros :as dm] [app.common.types.color :as clr] [app.common.types.shape.attrs :refer [default-color]] - [app.common.types.token :as tk] [app.config :as cfg] [app.main.data.modal :as modal] [app.main.data.workspace.colors :as dwc] @@ -27,6 +26,7 @@ [app.main.ui.ds.utilities.swatch :refer [swatch*]] [app.main.ui.formats :as fmt] [app.main.ui.hooks :as h] + [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] [app.util.color :as uc] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -176,12 +176,9 @@ active-tokens* (mf/use-ctx ctx/active-tokens-by-type) - tokens (mf/with-memo [active-tokens* origin] - (let [origin (if (= :color-selection origin) :fill origin)] - (delay - (-> (deref active-tokens*) - (select-keys (get tk/tokens-by-input origin)) - (not-empty))))) + tokens (mf/with-memo [active-tokens* origin] + (csu/filter-tokens-for-input active-tokens* origin)) + on-focus' (mf/use-fn (mf/deps on-focus) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index ec5770eabb..9fe822f9df 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -18,6 +18,7 @@ [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.hooks :as h] + [app.main.ui.workspace.sidebar.options.common :as soc] [app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row*]] [app.util.i18n :as i18n :refer [tr]] @@ -92,14 +93,13 @@ on-width-change (mf/use-fn - (mf/deps index on-stroke-width-change) + (mf/deps index on-stroke-width-change ids) (fn [value] - (if (or (string? value) (number? value)) - (on-stroke-width-change index value) - - (st/emit! (dwta/toggle-token {:token (first value) - :attrs #{:stroke-width} - :shape-ids ids}))))) + (soc/emit-value-or-token + value + #(on-stroke-width-change index %) + ids + #{:stroke-width}))) stroke-alignment (or (:stroke-alignment stroke) :center) @@ -164,7 +164,7 @@ (mf/use-fn (mf/deps on-detach-token) (fn [token] - (on-detach-token (first token) #{:stroke-width}))) + (on-detach-token token #{:stroke-width}))) stroke-caps-options [{:value nil :label (tr "workspace.options.stroke-cap.none")} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs index f29c348e9d..e75989cdc8 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs @@ -9,7 +9,7 @@ [token] {:id (str (get token :id)) :type :token - :resolved-value (get token :value) + :resolved-value (get token :resolved-value) :name (get token :name)}) (defn- generate-dropdown-options @@ -53,7 +53,7 @@ tokens))) (defn- sort-groups-and-tokens - "Sorts both the groups and the tokens inside them alphabetically. + "Sorts the tokens inside the groups alphabetically. Input: A map where: @@ -65,18 +65,18 @@ :colors [{:name \"azul\"} {:name \"rojo\"}]} Output: - A sorted map where: - - groups are ordered alphabetically by key + A map which: - tokens inside each group are sorted alphabetically by :name Example output: - {:colors [{:name \"azul\"} {:name \"rojo\"}] - :dimensions [{:name \"quini\"} {:name \"tres\"}]}" + {:dimensions [{:name \"quini\"} {:name \"tres\"}] + :colors [{:name \"azul\"} {:name \"rojo\"}]}" [groups->tokens] - (into (sorted-map) ;; ensure groups are ordered alphabetically by their key - (for [[group tokens] groups->tokens] - [group (sort-by :name tokens)]))) + (reduce (fn [acc [group tokens]] + (assoc acc group (sort-by :name tokens))) + {} + groups->tokens)) (defn get-token-dropdown-options [tokens filter-term] @@ -94,9 +94,15 @@ (defn filter-tokens-for-input [raw-tokens input-type] (delay - (-> (deref raw-tokens) - (select-keys (get cto/tokens-by-input input-type)) - (not-empty)))) + (let [raw-tokens (deref raw-tokens) + key-order (get cto/tokens-by-input input-type)] + (-> (reduce (fn [acc k] + (if (contains? raw-tokens k) + (assoc acc k (get raw-tokens k)) + acc)) + (array-map) + key-order) + (not-empty))))) (defn focusable-options [options] (filter #(= (:type %) :token) options)) \ No newline at end of file From f8913c755dcf3880dc316d5eb8c6dbf5ad7696ce Mon Sep 17 00:00:00 2001 From: Xaviju Date: Thu, 19 Mar 2026 22:54:21 +0100 Subject: [PATCH 051/288] :tada: Rename token group (#8275) * :tada: Rename token group * :paperclip: Add to CHANGES --- CHANGES.md | 4 +- common/src/app/common/files/tokens.cljc | 26 ++++ common/src/app/common/types/token.cljc | 11 ++ common/src/app/common/types/tokens_lib.cljc | 36 +++++ frontend/playwright/ui/pages/WorkspacePage.js | 36 +++-- .../playwright/ui/specs/tokens/crud.spec.js | 11 +- .../playwright/ui/specs/tokens/helpers.js | 1 + .../ui/specs/tokens/remapping.spec.js | 147 +++++++++++++++++- .../data/workspace/tokens/library_edit.cljs | 28 ++++ .../main/data/workspace/tokens/remapping.cljs | 12 ++ frontend/src/app/main/ui/forms.cljs | 34 ++-- frontend/src/app/main/ui/workspace.cljs | 1 + .../workspace/sidebar/options/menus/fill.cljs | 2 +- .../main/ui/workspace/tokens/management.cljs | 105 ++++++++++++- .../tokens/management/forms/generic_form.cljs | 15 +- .../management/forms/rename_node_modal.cljs | 117 ++++++++++++++ .../management/forms/rename_node_modal.scss | 46 ++++++ .../tokens/management/node_context_menu.cljs | 16 +- .../ui/workspace/tokens/remapping_modal.cljs | 40 +++-- frontend/translations/ca.po | 20 +++ frontend/translations/en.po | 22 ++- frontend/translations/es.po | 16 +- 22 files changed, 687 insertions(+), 59 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss diff --git a/CHANGES.md b/CHANGES.md index e15be66cd8..99e393bc5b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### :boom: Breaking changes & Deprecations ### :rocket: Epics and highlights + - Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112) ### :sparkles: New features & Enhancements @@ -15,11 +16,10 @@ - Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248) - Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313) - Add Tab/Shift+Tab navigation to rename layers sequentially (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8474) - +- Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137) ### :bug: Bugs fixed - ## 2.15.0 (Unreleased) ### :boom: Breaking changes & Deprecations diff --git a/common/src/app/common/files/tokens.cljc b/common/src/app/common/files/tokens.cljc index e8f1208058..071b28a4e7 100644 --- a/common/src/app/common/files/tokens.cljc +++ b/common/src/app/common/files/tokens.cljc @@ -147,6 +147,27 @@ #(and (some? tokens-tree) (not (ctob/token-name-path-exists? % tokens-tree)))]]) +(defn make-node-token-name-schema + "Dynamically generates a schema to check a token node name, adding translated error messages + and two additional validations: + - Min and max length. + - Checks if other token with a path derived from the name already exists at `tokens-tree`. + e.g. it's not allowed to create a token `foo.bar` if a token `foo` already exists." + [active-tokens tokens-tree node] + [:and + [:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] + (-> cto/schema:token-node-name + (sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))) + [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} + (fn [name] + (let [current-path (:path node) + current-name (:name node) + new-tokens (ctob/update-tokens-group active-tokens current-path current-name name)] + (and (some? new-tokens) + (some (fn [[token-name _]] + (not (ctob/token-name-path-exists? token-name tokens-tree))) + new-tokens))))]]) + (def schema:token-description [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]) @@ -165,6 +186,11 @@ (when (and name value) (not (cto/token-value-self-reference? name value))))]]) +(defn make-node-token-schema + [active-tokens tokens-tree node] + [:map + [:name (make-node-token-name-schema active-tokens tokens-tree node)]]) + (defn convert-dtcg-token "Convert token attributes as they come from a decoded json, with DTCG types, to internal types. Eg. From this: diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 15e5168a0b..2d4b5b0395 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -136,6 +136,9 @@ (def token-name-validation-regex #"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$") +(def token-node-name-validation-regex + #"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$") + (def schema:token-name "A token name can contains letters, numbers, underscores the character $ and dots, but not start with $ or end with a dot. The $ character does not have any special meaning, @@ -153,6 +156,14 @@ :gen/gen sg/text} token-ref-validation-regex]) +(def schema:token-node-name + "A token node name can contains letters, numbers, underscores and the character $, but + not start with $ or a dot, or end with a dot. The $ character does not have any special meaning, + but dots separate token groups (e.g. color.primary.background)." + [:re {:title "TokenNodeName" + :gen/gen sg/text} + token-node-name-validation-regex]) + (def schema:token-type [::sm/one-of {:decode/json (fn [type] (if (string? type) diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 63cd87e393..b219e60e01 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -153,6 +153,18 @@ tokens)] (group-by :type tokens'))) +(defn rename-path + "Renames a node or token path segment with a new name. + If token is provided, it renames a token path, otherwise it renames a node path." + ([node new-name] + (rename-path node nil new-name)) + ([node token new-name] + (let [element (if token (:name token) (:path node)) + split-path (cpn/split-path element :separator ".") + updated-split-element-name (assoc split-path (:depth node) new-name) + new-element-path (cpn/join-path updated-split-element-name :separator "." :with-spaces? false)] + new-element-path))) + ;; === Token Set (defprotocol ITokenSet @@ -1490,6 +1502,30 @@ Will return a value that matches this schema: (seq) (boolean))))) +(defn update-tokens-group + "Updates the active tokens path when renaming a group node. + - Filters tokens whose path matches the current path prefix + - Replaces the token name with the new name + - Updates the :path value in the token object + + active-tokens: map of token-name to token-object for all active tokens in the set + current-path: the path of the group being renamed, e.g. \"foo.bar\" + current-name: the current name of the group being renamed, e.g. \"bar\" + new-name: the new name for the group being renamed, e.g. \"baz\"" + + [active-tokens current-path current-name new-name] + (let [path-prefix (str/replace current-path current-name "")] + (mapv (fn [[token-path token-obj]] + (if (str/starts-with? token-path path-prefix) + (let [new-token-path (str/replace token-path current-name new-name) + new-token-obj (-> token-obj + (assoc :name new-token-path) + (cond-> (:path token-obj) + (assoc :path (str/replace (:path token-obj) current-name new-name))))] + [new-token-path new-token-obj]) + [token-path token-obj])) + active-tokens))) + ;; === Import / Export from JSON format ;; Supported formats: diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 92f9b9bef2..b74341c965 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -108,7 +108,9 @@ export class WorkspacePage extends BaseWebSocketPage { } async waitForIdle() { - await this.page.evaluate(() => new Promise((resolve) => globalThis.requestIdleCallback(resolve))); + await this.page.evaluate( + () => new Promise((resolve) => globalThis.requestIdleCallback(resolve)), + ); } }; @@ -190,6 +192,7 @@ export class WorkspacePage extends BaseWebSocketPage { this.tokensUpdateCreateModal = page.getByTestId( "token-update-create-modal", ); + this.tokenRenameNodeModal = page.getByTestId("token-rename-node-modal"); this.tokenThemeUpdateCreateModal = page.getByTestId( "token-theme-update-create-modal", ); @@ -224,7 +227,7 @@ export class WorkspacePage extends BaseWebSocketPage { async #waitForWebSocketReadiness() { // TODO: find a better event to settle whether the app is ready to receive notifications via ws - await expect(this.pageName).toHaveText("Page 1", { timeout: 30000 }) + await expect(this.pageName).toHaveText("Page 1", { timeout: 30000 }); } async sendPresenceMessage(fixture) { @@ -309,7 +312,7 @@ export class WorkspacePage extends BaseWebSocketPage { async clickWithDragViewportAt(x, y, width, height) { await this.page.waitForTimeout(100); const box = await this.viewport.boundingBox(); - if (!box) throw new Error('Viewport not visible'); + if (!box) throw new Error("Viewport not visible"); const startX = box.x + x; const startY = box.y + y; @@ -362,7 +365,9 @@ export class WorkspacePage extends BaseWebSocketPage { await this.page.keyboard.press("T"); await this.page.waitForTimeout(timeToWait); - const layersCountBefore = await this.layers.getByTestId("layer-row").count(); + const layersCountBefore = await this.layers + .getByTestId("layer-row") + .count(); await this.clickAndMove(x1, y1, x2, y2); if (initialText) { @@ -385,10 +390,13 @@ export class WorkspacePage extends BaseWebSocketPage { await this.page.keyboard.press("ControlOrMeta+C"); } // wait for the clipboard to be updated - await this.page.waitForFunction(async () => { - const content = await navigator.clipboard.readText() - return content !== ""; - }, { timeout: 1000 }); + await this.page.waitForFunction( + async () => { + const content = await navigator.clipboard.readText(); + return content !== ""; + }, + { timeout: 1000 }, + ); } async cut(kind = "keyboard", locator = undefined) { @@ -399,13 +407,15 @@ export class WorkspacePage extends BaseWebSocketPage { await this.page.keyboard.press("ControlOrMeta+X"); } // wait for the clipboard to be updated - await this.page.waitForFunction(async () => { - const content = await navigator.clipboard.readText() - return content !== ""; - }, { timeout: 1000 }); + await this.page.waitForFunction( + async () => { + const content = await navigator.clipboard.readText(); + return content !== ""; + }, + { timeout: 1000 }, + ); await this.page.waitForTimeout(3000); - } /** diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index ac72a2cc7c..153f1dc25d 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -914,7 +914,9 @@ test.describe("Tokens - creation", () => { const emptyNameError = "Name should be at least 1 character"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] }); + await setupEmptyTokensFileRender(page, { + flags: ["enable-token-shadow"], + }); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -1130,7 +1132,9 @@ test.describe("Tokens - creation", () => { const emptyNameError = "Name should be at least 1 character"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] }); + await setupEmptyTokensFileRender(page, { + flags: ["enable-token-shadow"], + }); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -1576,7 +1580,8 @@ test.describe("Tokens - creation", () => { const nameField = tokensUpdateCreateModal.getByLabel("Name"); await nameField.fill(newTokenTitle); - const referenceTabButton = tokensUpdateCreateModal.getByTestId("reference-opt"); + const referenceTabButton = + tokensUpdateCreateModal.getByTestId("reference-opt"); await referenceTabButton.click(); const referenceField = tokensUpdateCreateModal.getByRole("textbox", { diff --git a/frontend/playwright/ui/specs/tokens/helpers.js b/frontend/playwright/ui/specs/tokens/helpers.js index 63c54af0f9..8f8974e40f 100644 --- a/frontend/playwright/ui/specs/tokens/helpers.js +++ b/frontend/playwright/ui/specs/tokens/helpers.js @@ -161,6 +161,7 @@ const setupTokensFileRender = async (page, options = {}) => { workspacePage, tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal, tokenThemeUpdateCreateModal: workspacePage.tokenThemeUpdateCreateModal, + tokensRenameNodeModal: workspacePage.tokensRenameNodeModal, tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar, tokenSetItems: workspacePage.tokenSetItems, tokenSetGroupItems: workspacePage.tokenSetGroupItems, diff --git a/frontend/playwright/ui/specs/tokens/remapping.spec.js b/frontend/playwright/ui/specs/tokens/remapping.spec.js index 4563b491b3..90eb658a77 100644 --- a/frontend/playwright/ui/specs/tokens/remapping.spec.js +++ b/frontend/playwright/ui/specs/tokens/remapping.spec.js @@ -123,7 +123,7 @@ const createCompositeDerivedToken = async (page, type, name, reference) => { await expect(tokensUpdateCreateModal).not.toBeVisible(); }; -test.describe("Remapping Tokens", () => { +test.describe("Remapping a single token", () => { test.describe("Box Shadow Token Remapping", () => { test("User renames box shadow token with alias references", async ({ page, @@ -634,3 +634,148 @@ test.describe("Remapping Tokens", () => { }); }); }); + +test.describe("Remapping group of tokens", () => { + test("User renames a group - no remap", async ({ page }) => { + const { tokensSidebar } = await setupTokensFileRender(page); + + // Create multiple tokens in a group + await createToken(page, "Color", "dark.primary", "Value", "#000000"); + await createToken(page, "Color", "dark.secondary", "Value", "#111111"); + + // Verify that the node and child token are visible before deletion + const darkNode = tokensSidebar.getByRole("button", { + name: "dark", + exact: true, + }); + const darkNodeToken = tokensSidebar.getByRole("button", { + name: "primary", + }); + + // Select a node and right click on it to open context menu + await expect(darkNode).toBeVisible(); + await expect(darkNodeToken).toBeVisible(); + await darkNode.click({ button: "right" }); + + // select "Rename" from the context menu + const renameNodeButton = page.getByRole("button", { + name: "Rename", + exact: true, + }); + await expect(renameNodeButton).toBeVisible(); + await renameNodeButton.click(); + + // Expect the rename modal to be visible, fill in the new name and submit + const tokenRenameNodeModal = page.getByTestId("token-rename-node-modal"); + await expect(tokenRenameNodeModal).toBeVisible(); + + const nameField = tokenRenameNodeModal.getByRole("textbox", { + name: "Name", + }); + await nameField.fill("darker"); + + const submitButton = tokenRenameNodeModal.getByRole("button", { + name: "Rename", + }); + await submitButton.click(); + + // Ensure that the remapping modal does not appear + const remappingModal = page.getByTestId("token-remapping-modal"); + await expect(remappingModal).not.toBeVisible(); + + // Verify that the node has been renamed and tokens are still visible + const darkerNode = tokensSidebar.getByRole("button", { + name: "darker", + exact: true, + }); + + await expect(darkerNode).toBeVisible(); + }); + + test("User renames a group - and remaps", async ({ page }) => { + const { tokensSidebar } = await setupTokensFileRender(page); + const workspacePage = new WasmWorkspacePage(page); + const rightSidebar = workspacePage.rightSidebar; + + // Create multiple tokens in a group + await createToken(page, "Color", "light.primary", "Value", "#FFFFFF"); + await createToken(page, "Color", "light.secondary", "Value", "#EEEEEE"); + + // Verify that the node and child token are visible before deletion + const lightNode = tokensSidebar.getByRole("button", { + name: "light", + exact: true, + }); + const lightNodeToken = tokensSidebar.getByRole("button", { + name: "primary", + }); + + // Select a node and right click on it to open context menu + await expect(lightNode).toBeVisible(); + await expect(lightNodeToken).toBeVisible(); + + // Apply token to a shape to ensure remapping modal appears with applied token reference + await page.getByRole("tab", { name: "Layers" }).click(); + await page + .getByTestId("layer-row") + .filter({ hasText: "Rectangle" }) + .first() + .click(); + + await page.getByRole("tab", { name: "Tokens" }).click(); + const lightPrimaryToken = tokensSidebar.getByRole("button", { + name: "primary", + }); + await lightPrimaryToken.click(); + + // Right click on the node to rename + + await lightNode.click({ button: "right" }); + const renameNodeButton = page.getByRole("button", { + name: "Rename", + exact: true, + }); + await expect(renameNodeButton).toBeVisible(); + await renameNodeButton.click(); + + // Expect the rename modal to be visible, fill in the new name and submit + const tokenRenameNodeModal = page.getByTestId("token-rename-node-modal"); + await expect(tokenRenameNodeModal).toBeVisible(); + + const nameField = tokenRenameNodeModal.getByRole("textbox", { + name: "Name", + }); + await nameField.fill("lighter"); + + const submitButton = tokenRenameNodeModal.getByRole("button", { + name: "Rename", + }); + await submitButton.click(); + + // Ensure that the remapping modal appears and confirm remap + const remappingModal = page.getByTestId("token-remapping-modal"); + await expect(remappingModal).toBeVisible({ timeout: 5000 }); + + const confirmButton = remappingModal.getByRole("button", { + name: "remap tokens", + }); + await confirmButton.click(); + + // Verify that the node has been renamed and tokens are still visible + const lighterNode = tokensSidebar.getByRole("button", { + name: "lighter", + exact: true, + }); + + await expect(lighterNode).toBeVisible(); + + // Verify that the applied token reference has been updated in the right sidebar for the selected shape + const fillSection = rightSidebar.getByTestId("fill-section"); + await expect(fillSection).toBeVisible(); + + const tokenReference = fillSection.getByLabel("lighter.primary", { + exact: true, + }); + await expect(tokenReference).toBeVisible(); + }); +}); diff --git a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs index 4daccd05b8..22bd7789fb 100644 --- a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs +++ b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs @@ -456,6 +456,34 @@ (rx/of (dch/commit-changes changes) (ptk/data-event ::ev/event {::ev/name "edit-token" :type token-type}))))))) +(defn bulk-update-tokens + [set-id token-ids type old-path new-path] + (dm/assert! (uuid? set-id)) + (dm/assert! (every? uuid? token-ids)) + (ptk/reify ::bulk-update-tokens + ptk/WatchEvent + (watch [it state _] + (let [token-set (if set-id + (lookup-token-set state set-id) + (lookup-token-set state)) + data (dsh/lookup-file-data state) + changes (reduce (fn [changes token-id] + (let [token (-> (get-tokens-lib state) + (ctob/get-token (ctob/get-id token-set) token-id)) + new-name (str/replace (:name token) old-path new-path) + token' (->> (merge token {:name new-name}) + (into {}) + (ctob/make-token))] + (pcb/set-token changes (ctob/get-id token-set) token-id token'))) + (-> (pcb/empty-changes it) + (pcb/with-library-data data)) + + token-ids)] + (toggle-token-path (str (name type) "." old-path)) + (toggle-token-path (str (name type) "." new-path)) + (rx/of (dch/commit-changes changes) + (ptk/data-event ::ev/event {::ev/name "bulk-update-tokens" :type type})))))) + (defn delete-token [set-id token-id] (dm/assert! (uuid? set-id)) diff --git a/frontend/src/app/main/data/workspace/tokens/remapping.cljs b/frontend/src/app/main/data/workspace/tokens/remapping.cljs index fac4eeb40e..0992501f4c 100644 --- a/frontend/src/app/main/data/workspace/tokens/remapping.cljs +++ b/frontend/src/app/main/data/workspace/tokens/remapping.cljs @@ -150,6 +150,18 @@ (rx/of (dch/commit-changes token-changes)))))) +(defn bulk-remap-tokens + "Helper function to remap a batch of tokens, used for node renaming" + [tokens-in-path new-tokens] + (ptk/reify ::bulk-remap-tokens + ptk/WatchEvent + (watch [_ _ _] + (rx/concat + (map (fn [old-token new-token] + (remap-tokens (:name old-token) (:name new-token))) + tokens-in-path + new-tokens))))) + (defn validate-token-remapping "Validate that a token remapping operation is safe to perform" [old-name new-name] diff --git a/frontend/src/app/main/ui/forms.cljs b/frontend/src/app/main/ui/forms.cljs index 9aede980cf..c0426dcfaa 100644 --- a/frontend/src/app/main/ui/forms.cljs +++ b/frontend/src/app/main/ui/forms.cljs @@ -67,24 +67,38 @@ (mf/defc form-submit* [{:keys [disabled on-submit] :rest props}] + (let [form (mf/use-ctx context) - disabled? (or (and (some? form) - (or (not (:valid @form)) - (seq (:async-errors @form)) - (seq (:extra-errors @form)))) - (true? disabled)) + form-state (when form @form) + + disabled? (mf/use-memo + (mf/deps form form-state disabled) + (fn [] + (boolean + (or (nil? form) + (true? disabled) + (not (:valid form-state)) + (seq (:async-errors form-state)) + (seq (:extra-errors form-state)))))) + handle-key-down-save (mf/use-fn - (mf/deps on-submit form) + (mf/deps on-submit form disabled?) (fn [e] - (when (or (k/enter? e) (k/space? e)) + (when (and (or (k/enter? e) (k/space? e)) (not disabled?)) (dom/prevent-default e) (on-submit form e)))) props - (mf/spread-props props {:disabled disabled? - :on-key-down handle-key-down-save - :type "submit"})] + (mf/spread-props props {:on-key-down handle-key-down-save + :type "submit"}) + + props + (if disabled? + (mf/spread-props props {:disabled true + :on-key-down handle-key-down-save + :type "submit"}) + props)] [:> button* props])) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 6014b0614a..bc96444370 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -36,6 +36,7 @@ [app.main.ui.workspace.tokens.import] [app.main.ui.workspace.tokens.import.modal] [app.main.ui.workspace.tokens.management.forms.modals] + [app.main.ui.workspace.tokens.management.forms.rename-node-modal] [app.main.ui.workspace.tokens.remapping-modal] [app.main.ui.workspace.tokens.settings] [app.main.ui.workspace.tokens.themes.create-modal] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index ba6ea893f2..20a4198431 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -195,7 +195,7 @@ (dom/set-attribute! checkbox "indeterminate" true) (dom/remove-attribute! checkbox "indeterminate")))) - [:div {:class (stl/css :fill-section)} + [:div {:class (stl/css :fill-section) :data-testid "fill-section"} [:div {:class (stl/css :fill-title)} [:> title-bar* {:collapsable has-fills? :collapsed (not open?) diff --git a/frontend/src/app/main/ui/workspace/tokens/management.cljs b/frontend/src/app/main/ui/workspace/tokens/management.cljs index 0ff4deafa4..7e077cc1f4 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management.cljs @@ -2,12 +2,17 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.path-names :as cpn] [app.common.types.shape.layout :as ctsl] [app.common.types.tokens-lib :as ctob] [app.config :as cf] + [app.main.data.helpers :as dh] + [app.main.data.modal :as modal] [app.main.data.style-dictionary :as sd] [app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.library-edit :as dwtl] + [app.main.data.workspace.tokens.propagation :as dwtp] + [app.main.data.workspace.tokens.remapping :as remap] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] @@ -17,6 +22,7 @@ [app.main.ui.workspace.tokens.management.node-context-menu :refer [token-node-context-menu*]] [app.util.array :as array] [app.util.i18n :refer [tr]] + [cljs.pprint :as pp] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -124,6 +130,17 @@ (mf/with-memo [tokens-by-type] (get-sorted-token-groups tokens-by-type)) + ;; Filter tokens by their path and return the tokens + filter-tokens-by-path + (mf/use-fn + (fn [tokens-filtered-by-type node] + (->> tokens-filtered-by-type + (filter (fn [token] + (let [token-path (cpn/split-path (:name token) :separator ".") + _ (pp/pprint {:token-path token-path :count (count token-path)})] + (and (> (count token-path) 0) + (str/starts-with? (:name token) (str (:path node) "."))))))))) + ;; Filter tokens by their path and return their ids filter-tokens-by-path-ids (mf/use-fn @@ -132,7 +149,7 @@ (->> selected-token-set-tokens (filter (fn [token] (let [[_ token-value] token] - (and (= (:type token-value) type) (str/starts-with? (:name token-value) path))))) + (and (= (:type token-value) type) (str/starts-with? (:name token-value) (str path ".")))))) (mapv (fn [token] (let [[_ token-value] token] (:id token-value))))))) @@ -176,7 +193,88 @@ ;; Remove from unfolded tree path (if remaining-tokens? (st/emit! (dwtl/toggle-token-path (str (name type) "." path))) - (st/emit! (dwtl/toggle-token-path (name type)))))))] + (st/emit! (dwtl/toggle-token-path (name type))))))) + + bulk-rename-tokens-in-path + ;; Rename tokens in bulk affected by a node rename. + (mf/use-fn + (mf/deps filter-tokens-by-path-ids selected-token-set-id) + (fn [node type new-node-name] + (let [old-path (:path node) + new-path (ctob/rename-path node new-node-name) + tokens-in-path-ids (filter-tokens-by-path-ids type old-path)] + (st/emit! + (modal/hide) + (dwtl/bulk-update-tokens selected-token-set-id tokens-in-path-ids type old-path new-path))))) + + bulk-remap-tokens-in-path + ;; Remap tokens in bulk affected by a node rename. + ;; It will update the token names and propagate the changes to the workspace. + (mf/use-fn + (mf/deps filter-tokens-by-path filter-tokens-by-path-ids selected-token-set-tokens selected-token-set-id) + (fn [node type new-node-name] + (let [old-path (:path node) + ;; Get tokens in path to remap their names after remapping the node + tokens-by-type (ctob/group-by-type selected-token-set-tokens) + tokens-filtered-by-type (get tokens-by-type type) + tokens-in-path (filter-tokens-by-path tokens-filtered-by-type node) + tokens-in-path-ids (filter-tokens-by-path-ids type old-path) + new-node-path (ctob/rename-path node new-node-name) + new-tokens (map (fn [token] + (let [new-token-path (ctob/rename-path node token new-node-name)] + (assoc token :name new-token-path))) + tokens-in-path)] + (st/emit! + (dwtl/bulk-update-tokens selected-token-set-id tokens-in-path-ids type old-path new-node-path) + (remap/bulk-remap-tokens tokens-in-path new-tokens) + (dwtp/propagate-workspace-tokens) + (modal/hide))))) + + on-remap-node-warning + ;; If there are tokens that will be affected by the node rename, we show the remap modal + (mf/use-fn + (mf/deps bulk-remap-tokens-in-path bulk-rename-tokens-in-path) + (fn [node type new-node-name] + (let [remap-data {:new-name new-node-name + :old-name (:name node) + :type "node"} + remap-handler #(bulk-remap-tokens-in-path node type new-node-name) + rename-handler #(bulk-rename-tokens-in-path node type new-node-name)] + (st/emit! + (modal/hide) + (modal/show :tokens/remapping-confirmation {:remap-data remap-data + :on-remap remap-handler + :on-rename rename-handler}))))) + + on-rename-node + ;; When user renames a node, we need to check if there are tokens that will be affected by this change. + ;; If there are, we display the remap modal, otherwise, we rename the tokens directly. + (mf/use-fn + (mf/deps selected-token-set-tokens filter-tokens-by-path on-remap-node-warning bulk-rename-tokens-in-path) + (fn [node type new-node-name] + (let [state @st/state + file-data (dh/lookup-file-data state) + tokens-by-type (ctob/group-by-type selected-token-set-tokens) + tokens-filtered-by-type (get tokens-by-type type) + tokens-in-current-path (filter-tokens-by-path tokens-filtered-by-type node) + _ (pp/pprint {:tokens-in-current-path tokens-in-current-path}) + token-references-count (reduce (fn [count token] + (+ count (remap/count-token-references file-data (:name token)))) + 0 + tokens-in-current-path)] + (if (> token-references-count 0) + (on-remap-node-warning node type new-node-name) + (bulk-rename-tokens-in-path node type new-node-name))))) + + open-rename-node-modal + ;; When user renames a node, we display a form modal + (mf/use-fn + (mf/deps selected-token-set-tokens on-rename-node) + (fn [node type] + (let [on-rename-node-handler #(on-rename-node node type %)] + (st/emit! (modal/show :tokens/rename-node {:node node + :tokens-in-active-set selected-token-set-tokens + :on-rename on-rename-node-handler})))))] (mf/with-effect [tokens-lib selected-token-set-id] (when (and tokens-lib @@ -190,7 +288,8 @@ [:* [:& token-context-menu {:on-delete-token delete-token}] - [:> token-node-context-menu* {:on-delete-node delete-node}] + [:> token-node-context-menu* {:on-rename-node open-rename-node-modal + :on-delete-node delete-node}] [:> selected-set-info* {:tokens-lib tokens-lib :selected-token-set-id selected-token-set-id}] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs index 8225a52887..b968395e7c 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs @@ -160,13 +160,13 @@ on-remap-token (mf/use-fn (mf/deps token) - (fn [valid-token name old-name description] + (fn [valid-token new-name old-name description] (st/emit! (dwtl/update-token (:id token) - {:name name + {:name new-name :value (:value valid-token) :description description}) - (remap/remap-tokens old-name name) + (remap/remap-tokens old-name new-name) (dwtp/propagate-workspace-tokens) (modal/hide!)))) @@ -203,11 +203,12 @@ is-rename (and (= action "edit") (not= name old-name)) references-count (remap/count-token-references file-data old-name) on-remap #(on-remap-token valid-token name old-name description) - on-rename #(on-rename-token valid-token name description)] + on-rename #(on-rename-token valid-token name description) + remap-data {:new-name name + :old-name old-name + :type "token"}] (if (and is-rename (> references-count 0)) - (st/emit! (modal/show :tokens/remapping-confirmation {:old-token-name old-name - :new-token-name name - :references-count references-count + (st/emit! (modal/show :tokens/remapping-confirmation {:remap-data remap-data :on-remap on-remap :on-rename on-rename})) (st/emit! diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs new file mode 100644 index 0000000000..c58d244b6a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs @@ -0,0 +1,117 @@ +(ns app.main.ui.workspace.tokens.management.forms.rename-node-modal + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.files.tokens :as cfo] + [app.common.types.tokens-lib :as ctob] + [app.main.data.modal :as modal] + [app.main.store :as st] + [app.main.ui.ds.buttons.button :refer [button*]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.ds.foundations.typography.heading :refer [heading*]] + [app.main.ui.forms :as fc] + [app.util.forms :as fm] + [app.util.i18n :refer [tr]] + [app.util.keyboard :as kbd] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(mf/defc rename-node-form* + [{:keys [node active-tokens tokens-tree on-close on-submit]}] + (let [make-schema #(cfo/make-node-token-schema active-tokens tokens-tree node) + + schema + (mf/with-memo [active-tokens] + (make-schema)) + + initial (mf/with-memo [node] + {:name (:name node)}) + + form (fm/use-form :schema schema + :initial initial) + + on-submit (mf/use-fn + (mf/deps form on-submit) + (fn [] + (let [name (get-in @form [:clean-data :name])] + (when (and (get-in @form [:touched :name]) (not= name (:name node))) + (on-submit name))))) + + is-disabled? (or (not (:valid @form)) + (not (get-in @form [:touched :name])) + (= (get-in @form [:clean-data :name]) (:name node))) + + new-path (mf/with-memo [@form node] + (let [new-name (get-in @form [:clean-data :name]) + path (str (:path node)) + new-path (str/replace path (:name node) new-name)] + new-path))] + + [:* + [:> heading* {:level 2 + :typography "headline-medium" + :class (stl/css :form-modal-title)} + (tr "workspace.tokens.rename-group")] + [:> fc/form* {:class (stl/css :form-wrapper) + :form form + :on-submit on-submit} + [:> fc/form-input* {:id "rename-node" + :name :name + :label (tr "workspace.tokens.token-name") + :placeholder (tr "workspace.tokens.token-name") + :max-length 255 + :variant "comfortable" + :hint-type "hint" + :hint-message (tr "workspace.tokens.rename-group-name-hint" new-path) + :auto-focus true}] + [:div {:class (stl/css :form-actions)} + [:> button* {:variant "secondary" + :name "cancel" + :on-click on-close} (tr "labels.cancel")] + [:> fc/form-submit* {:variant "primary" + :disabled is-disabled? + :name "rename"} (tr "labels.rename")]]]])) + +(mf/defc rename-node-modal + {::mf/register modal/components + ::mf/register-as :tokens/rename-node} + [{:keys [node tokens-in-active-set on-rename]}] + + (let [tokens-tree-in-selected-set + (mf/with-memo [tokens-in-active-set node] + (-> (ctob/tokens-tree tokens-in-active-set) + (d/dissoc-in (:name node)))) + + close-modal + (mf/use-fn + (fn [] + (st/emit! (modal/hide)))) + + rename + (mf/use-fn + (mf/deps on-rename) + (fn [new-name] + (on-rename new-name))) + + on-key-down + (mf/use-fn + (mf/deps [close-modal]) + (fn [event] + (when (kbd/esc? event) + (close-modal))))] + + [:div {:class (stl/css :modal-overlay) + :on-key-down on-key-down + :data-testid "token-rename-node-modal"} + [:div {:class (stl/css :modal-dialog)} + [:> icon-button* {:class (stl/css :close-btn) + :on-click close-modal + :aria-label (tr "labels.close") + :variant "ghost" + :icon i/close}] + [:> rename-node-form* {:node node + :active-tokens tokens-in-active-set + :tokens-tree tokens-tree-in-selected-set + :on-close close-modal + :on-submit rename}]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss new file mode 100644 index 0000000000..71e0cda690 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss @@ -0,0 +1,46 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "ds/_sizes.scss" as *; +@use "ds/typography.scss" as t; + +@use "refactor/common-refactor.scss" as deprecated; + +.modal-overlay { + --modal-title-foreground-color: var(--color-foreground-primary); + --modal-text-foreground-color: var(--color-foreground-secondary); + + @extend .modal-overlay-base; + display: flex; + justify-content: center; + align-items: center; + position: fixed; + inset-inline-start: 0; + inset-block-start: 0; + block-size: 100%; + inline-size: 100%; + background-color: var(--overlay-color); +} + +.close-btn { + position: absolute; + inset-block-start: $sz-6; + inset-inline-end: $sz-6; +} + +.modal-dialog { + @extend .modal-container-base; + inline-size: 100%; + max-inline-size: 32rem; + max-block-size: unset; + user-select: none; + position: relative; +} + +.form-modal-title { + @include t.use-typography("headline-medium"); + color: var(--color-foreground-primary); +} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs index 4e272f7bdd..f98e761203 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs @@ -13,6 +13,7 @@ (def ^:private schema:token-node-context-menu [:map + [:on-rename-node fn?] [:on-delete-node fn?]]) (def ^:private tokens-node-menu-ref @@ -25,7 +26,7 @@ (mf/defc token-node-context-menu* {::mf/schema schema:token-node-context-menu} - [{:keys [on-delete-node]}] + [{:keys [on-rename-node on-delete-node]}] (let [mdata (mf/deref tokens-node-menu-ref) is-open? (boolean mdata) dropdown-ref (mf/use-ref) @@ -35,7 +36,13 @@ dropdown-direction-change* (mf/use-ref 0) top (+ (get-in mdata [:position :y]) 5) left (+ (get-in mdata [:position :x]) 5) - + rename-node (mf/use-fn + (mf/deps mdata on-rename-node) + (fn [] + (let [node (get mdata :node) + type (get mdata :type)] + (when node + (on-rename-node node type))))) delete-node (mf/use-fn (mf/deps mdata) (fn [] @@ -75,6 +82,11 @@ :on-context-menu prevent-default} (when mdata [:ul {:class (stl/css :token-node-context-menu-list)} + [:li {:class (stl/css :token-node-context-menu-listitem)} + [:button {:class (stl/css :token-node-context-menu-action) + :type "button" + :on-click rename-node} + (tr "labels.rename")]] [:li {:class (stl/css :token-node-context-menu-listitem)} [:button {:class (stl/css :token-node-context-menu-action) :type "button" diff --git a/frontend/src/app/main/ui/workspace/tokens/remapping_modal.cljs b/frontend/src/app/main/ui/workspace/tokens/remapping_modal.cljs index 198972a193..1ac692c484 100644 --- a/frontend/src/app/main/ui/workspace/tokens/remapping_modal.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/remapping_modal.cljs @@ -20,27 +20,41 @@ [app.util.keyboard :as kbd] [rumext.v2 :as mf])) -(defn hide-remapping-modal +(defn- hide-remapping-modal "Hide the token remapping confirmation modal" [] (st/emit! (modal/hide))) +;; TODO: Uncomment when modal components support schema validation + +;; (def ^:private schema:remap-data +;; [:map +;; [:old-name :string] +;; [:new-name :string] +;; [:type [:enum "token" "node"]]]) + +;; (def ^:private schema:token-remapping-modal +;; [:map +;; [:remap-data [:maybe schema:remap-data]] +;; [:on-remap {:optional true} [:maybe fn?]] +;; [:on-rename {:optional true} [:maybe fn?]]]) + ;; Remapping Modal Component (mf/defc token-remapping-modal {::mf/register modal/components - ::mf/register-as :tokens/remapping-confirmation} - [{:keys [old-token-name new-token-name on-remap on-rename]}] - (let [remap-modal (get @st/state :remap-modal) + ::mf/register-as :tokens/remapping-confirmation + ;; TODO: Uncomment when modal components support schema validation + ;; ::mf/schema schema:token-remapping-modal + } + [{:keys [remap-data on-remap on-rename]}] + (let [old-name (:old-name remap-data) + new-name (:new-name remap-data) ;; Remap logic on confirm confirm-remap (mf/use-fn - (mf/deps on-remap remap-modal) + (mf/deps on-remap old-name new-name) (fn [] - ;; Call shared remapping logic - (let [old-token-name (:old-token-name remap-modal) - new-token-name (:new-token-name remap-modal)] - (st/emit! [:tokens/remap-tokens old-token-name new-token-name])) (when (fn? on-remap) (on-remap)))) @@ -83,9 +97,13 @@ :id "modal-title" :typography "headline-large" :class (stl/css :modal-title)} - (tr "workspace.tokens.remap-token-references-title" old-token-name new-token-name)]] + (if (= (:type remap-data) "token") + (tr "workspace.tokens.remap-token-references-title" old-name new-name) + (tr "workspace.tokens.remap-node-references-title" old-name new-name))]] [:div {:class (stl/css :modal-content)} - [:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-effects")] + (if (= (:type remap-data) "token") + [:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-token-warning-effects")] + [:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-node-warning-effects")]) [:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-time")]] [:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :action-buttons)} diff --git a/frontend/translations/ca.po b/frontend/translations/ca.po index 586495e0ad..59edb27cf1 100644 --- a/frontend/translations/ca.po +++ b/frontend/translations/ca.po @@ -4272,6 +4272,26 @@ msgstr "Pàgines" msgid "workspace.sitemap" msgstr "Mapa del lloc" +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78 +msgid "workspace.tokens.remap-node-references-title" +msgstr "Canviar el nom de `%s` a `%s` i remapejar tots els tokens d'aquest grup?" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78 +msgid "workspace.tokens.remap-token-warning-effects" +msgstr "Això canviarà totes les capes i referències que utilitzen el token antic." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89 +msgid "workspace.tokens.remap-warning-time" +msgstr "Aquest procés pot trigar una mica" + +#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs +msgid "workspace.tokens.rename-group" +msgstr "Canviar nom del grup de tokens" + +#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs +msgid "workspace.tokens.rename-group-name-hint" +msgstr "Els teus tokens es renomenaran automàticament a %s.(sufix).(token)" + #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 msgid "workspace.toolbar.assets" msgstr "Recursos" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index eeff80eeed..6b6516709e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -8301,11 +8301,19 @@ msgstr "Remap tokens" msgid "workspace.tokens.remap-token-references-title" msgstr "Remap all tokens that use `%s` to `%s`?" -#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88 -msgid "workspace.tokens.remap-warning-effects" +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78 +msgid "workspace.tokens.remap-node-references-title" +msgstr "Rename `%s` to `%s` and remap all tokens in this group?" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78 +msgid "workspace.tokens.remap-token-warning-effects" msgstr "This will change all layers and references that use the old token name." -#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89 +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78 +msgid "workspace.tokens.remap-node-warning-effects" +msgstr "This will update all tokens and references that use the old tokens name." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78 msgid "workspace.tokens.remap-warning-time" msgstr "This action could take a while." @@ -8547,6 +8555,14 @@ msgstr "Renaming this token will break any reference to its old name" msgid "workspace.tokens.error-text-edition" msgstr "Tokens can't be applied while editing text. Select the text layer instead." +#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs +msgid "workspace.tokens.rename-group" +msgstr "Rename Tokens Group" + +#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs +msgid "workspace.tokens.rename-group-name-hint" +msgstr "Your tokens will automatically be renamed to %s.(suffix).(tokenName)" + #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 msgid "workspace.toolbar.assets" msgstr "Assets" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index ee377bef15..69e1aeb0bf 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -8175,11 +8175,13 @@ msgstr "Actualizar tokens" msgid "workspace.tokens.remap-token-references-title" msgstr "¿Actualizar todas las referencias de `%s` a `%s`?" +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78 +msgid "workspace.tokens.remap-node-references-title" +msgstr "¿Renombrar `%s` to `%s` y remapear todos los tokens de este grupo?" + #: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88 msgid "workspace.tokens.remap-warning-effects" -msgstr "" -"Esta acción actualizará todas las capas y referencias que usen el token " -"antiguo" +msgstr "Esta acción actualizará todas las capas y referencias que usen el token antiguo" #: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89 msgid "workspace.tokens.remap-warning-time" @@ -8404,6 +8406,14 @@ msgstr "" msgid "workspace.tokens.error-text-edition" msgstr "No se pueden aplicar tokens mientras se edita texto. Seleccione la capa de texto en su lugar." +#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs +msgid "workspace.tokens.rename-group" +msgstr "Renombrar grupo de tokens" + +#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs +msgid "workspace.tokens.rename-group-name-hint" +msgstr "Tus tokens serán automáticamente renombrados a %s.(sufijo).(token)" + #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 msgid "workspace.toolbar.assets" msgstr "Recursos" From 8ad62c6800100fd4fdd665b6b9c5c21ed4bc497b Mon Sep 17 00:00:00 2001 From: Xaviju Date: Thu, 19 Mar 2026 23:20:18 +0100 Subject: [PATCH 052/288] :bug: Add export menu to inspect styles tab (#8645) * :bug: Add export menu to inspect styles tab * :paperclip: Add to CHANGES --- CHANGES.md | 2 + .../app/main/ui/inspect/right_sidebar.cljs | 4 +- frontend/src/app/main/ui/inspect/styles.cljs | 268 ++++++++++-------- frontend/src/app/main/ui/inspect/styles.scss | 5 + 4 files changed, 155 insertions(+), 124 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 99e393bc5b..0e46a9c0da 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,8 @@ ### :bug: Bugs fixed +- Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582) + ## 2.15.0 (Unreleased) ### :boom: Breaking changes & Deprecations diff --git a/frontend/src/app/main/ui/inspect/right_sidebar.cljs b/frontend/src/app/main/ui/inspect/right_sidebar.cljs index 5e205b502a..9d4dfa0a1b 100644 --- a/frontend/src/app/main/ui/inspect/right_sidebar.cljs +++ b/frontend/src/app/main/ui/inspect/right_sidebar.cljs @@ -188,7 +188,9 @@ :shapes shapes :from from :libraries libraries - :file-id file-id}] + :page-id page-id + :file-id file-id + :share-id share-id}] :computed [:> attributes* {:color-space color-space :page-id page-id diff --git a/frontend/src/app/main/ui/inspect/styles.cljs b/frontend/src/app/main/ui/inspect/styles.cljs index 3794ba61c7..21bc681ec5 100644 --- a/frontend/src/app/main/ui/inspect/styles.cljs +++ b/frontend/src/app/main/ui/inspect/styles.cljs @@ -15,6 +15,7 @@ [app.common.types.tokens-lib :as ctob] [app.main.data.style-dictionary :as sd] [app.main.refs :as refs] + [app.main.ui.inspect.exports :as exports] [app.main.ui.inspect.styles.panels.blur :refer [blur-panel*]] [app.main.ui.inspect.styles.panels.fill :refer [fill-panel*]] [app.main.ui.inspect.styles.panels.geometry :refer [geometry-panel*]] @@ -89,8 +90,20 @@ (:type first-shape)) :multiple)) +(def ^:private schema:styles-tab + [:map + [:color-space {:optional true} :string] ;; color format, e.g., "hex", "rgba", etc. + [:shapes :any] + [:libraries :map] + [:objects :map] + [:file-id :uuid] + [:page-id :uuid] + [:share-id {:optional true} [:maybe :uuid]] + [:from {:optional true} [:enum :workspace :viewer]]]) + (mf/defc styles-tab* - [{:keys [color-space shapes libraries objects file-id from]}] + {::mf/schema schema:styles-tab} + [{:keys [color-space shapes libraries objects file-id page-id share-id from]}] (let [data (dm/get-in libraries [file-id :data]) first-shape (first shapes) first-component (ctkl/get-component data (:component-id first-shape)) @@ -131,130 +144,139 @@ (mf/deps shorthands*) (fn [shorthand] (swap! shorthands* assoc (:panel shorthand) (:property shorthand))))] - [:ol {:class (stl/css-case :styles-tab true - :styles-tab-workspace (= from :workspace)) :aria-label (tr "labels.styles")} - ;; TOKENS PANEL - (when (or (seq active-themes) (seq active-sets)) - [:li - [:> style-box* {:panel :token} - [:> tokens-panel* {:theme-paths active-themes :set-names active-sets}]]]) - (for [panel panels] - [:li {:key (d/name panel)} - (case panel - ;; VARIANTS PANEL - :variant - [:> style-box* {:panel :variant} - [:> variants-panel* {:component first-component - :objects objects - :shape first-shape - :data data}]] - ;; GEOMETRY PANEL - :geometry - [:> style-box* {:panel :geometry - :shorthand (:geometry shorthands)} - [:> geometry-panel* {:shapes shapes - :objects objects - :resolved-tokens resolved-active-tokens - :on-geometry-shorthand set-shorthands}]] - ;; LAYOUT PANEL - :layout - (let [layout-shapes (->> shapes (filter ctl/any-layout?))] - (when (seq layout-shapes) - [:> style-box* {:panel :layout - :shorthand (:layout shorthands)} - [:> layout-panel* {:shapes layout-shapes - :objects objects - :resolved-tokens resolved-active-tokens - :on-layout-shorthand set-shorthands}]])) - ;; LAYOUT ELEMENT PANEL - :layout-element - (let [shapes (->> shapes (filter #(ctl/any-layout-immediate-child? objects %))) - some-layout-prop? (->> shapes - (mapcat (fn [shape] - (keep #(css/get-css-value objects shape %) layout-element-properties))) - (seq))] - (when some-layout-prop? - (let [only-flex? (every? #(ctl/flex-layout-immediate-child? objects %) shapes) - only-grid? (every? #(ctl/grid-layout-immediate-child? objects %) shapes) - panel (if only-flex? - :flex-element - (if only-grid? - :grid-element - :layout-element))] - [:> style-box* {:panel panel - :shorthand (:layout-element shorthands)} - [:> layout-element-panel* {:shapes shapes - :objects objects - :resolved-tokens resolved-active-tokens - :layout-element-properties layout-element-properties - :on-layout-element-shorthand set-shorthands}]]))) - ;; FILL PANEL - :fill - (let [shapes (filter has-fill? shapes)] - (when (seq shapes) - [:> style-box* {:panel :fill - :shorthand (:fill shorthands)} - [:> fill-panel* {:color-space color-space - :shapes shapes - :resolved-tokens resolved-active-tokens - :on-fill-shorthand set-shorthands}]])) + [:section {:class (stl/css-case :styles-tab true + :styles-tab-workspace (= from :workspace)) + :aria-label (tr "labels.styles")} + [:ol + ;; TOKENS PANEL + (when (or (seq active-themes) (seq active-sets)) + [:li + [:> style-box* {:panel :token} + [:> tokens-panel* {:theme-paths active-themes :set-names active-sets}]]]) + (for [panel panels] + [:li {:key (d/name panel)} + (case panel + ;; VARIANTS PANEL + :variant + [:> style-box* {:panel :variant} + [:> variants-panel* {:component first-component + :objects objects + :shape first-shape + :data data}]] + ;; GEOMETRY PANEL + :geometry + [:> style-box* {:panel :geometry + :shorthand (:geometry shorthands)} + [:> geometry-panel* {:shapes shapes + :objects objects + :resolved-tokens resolved-active-tokens + :on-geometry-shorthand set-shorthands}]] + ;; LAYOUT PANEL + :layout + (let [layout-shapes (->> shapes (filter ctl/any-layout?))] + (when (seq layout-shapes) + [:> style-box* {:panel :layout + :shorthand (:layout shorthands)} + [:> layout-panel* {:shapes layout-shapes + :objects objects + :resolved-tokens resolved-active-tokens + :on-layout-shorthand set-shorthands}]])) + ;; LAYOUT ELEMENT PANEL + :layout-element + (let [shapes (->> shapes (filter #(ctl/any-layout-immediate-child? objects %))) + some-layout-prop? (->> shapes + (mapcat (fn [shape] + (keep #(css/get-css-value objects shape %) layout-element-properties))) + (seq))] + (when some-layout-prop? + (let [only-flex? (every? #(ctl/flex-layout-immediate-child? objects %) shapes) + only-grid? (every? #(ctl/grid-layout-immediate-child? objects %) shapes) + panel (if only-flex? + :flex-element + (if only-grid? + :grid-element + :layout-element))] + [:> style-box* {:panel panel + :shorthand (:layout-element shorthands)} + [:> layout-element-panel* {:shapes shapes + :objects objects + :resolved-tokens resolved-active-tokens + :layout-element-properties layout-element-properties + :on-layout-element-shorthand set-shorthands}]]))) + ;; FILL PANEL + :fill + (let [shapes (filter has-fill? shapes)] + (when (seq shapes) + [:> style-box* {:panel :fill + :shorthand (:fill shorthands)} + [:> fill-panel* {:color-space color-space + :shapes shapes + :resolved-tokens resolved-active-tokens + :on-fill-shorthand set-shorthands}]])) - ;; STROKE PANEL - :stroke - (let [shapes (filter has-stroke? shapes)] - (when (seq shapes) - [:> style-box* {:panel :stroke - :shorthand (:stroke shorthands)} - [:> stroke-panel* {:color-space color-space - :shapes shapes - :objects objects - :resolved-tokens resolved-active-tokens - :on-stroke-shorthand set-shorthands}]])) + ;; STROKE PANEL + :stroke + (let [shapes (filter has-stroke? shapes)] + (when (seq shapes) + [:> style-box* {:panel :stroke + :shorthand (:stroke shorthands)} + [:> stroke-panel* {:color-space color-space + :shapes shapes + :objects objects + :resolved-tokens resolved-active-tokens + :on-stroke-shorthand set-shorthands}]])) - ;; VISIBILITY PANEL - :visibility - (let [shapes (filter has-visibility-props? shapes)] - (when (seq shapes) - [:> style-box* {:panel :visibility} - [:> visibility-panel* {:shapes shapes - :objects objects - :resolved-tokens resolved-active-tokens}]])) - ;; SVG PANEL - :svg - (let [shape (first shapes)] - (when (seq (:svg-attrs shape)) - [:> style-box* {:panel :svg} - [:> svg-panel* {:shape shape - :objects objects}]])) - ;; BLUR PANEL - :blur - (let [shapes (->> shapes (filter has-blur?))] - (when (seq shapes) - [:> style-box* {:panel :blur} - [:> blur-panel* {:shapes shapes + ;; VISIBILITY PANEL + :visibility + (let [shapes (filter has-visibility-props? shapes)] + (when (seq shapes) + [:> style-box* {:panel :visibility} + [:> visibility-panel* {:shapes shapes + :objects objects + :resolved-tokens resolved-active-tokens}]])) + ;; SVG PANEL + :svg + (let [shape (first shapes)] + (when (seq (:svg-attrs shape)) + [:> style-box* {:panel :svg} + [:> svg-panel* {:shape shape :objects objects}]])) - ;; TEXT PANEL - :text - (let [shapes (filter has-text? shapes)] - (when (seq shapes) - [:> style-box* {:panel :text - :shorthand (:text shorthands)} - [:> text-panel* {:shapes shapes - :color-space color-space - :resolved-tokens resolved-active-tokens - :on-font-shorthand set-shorthands}]])) + ;; BLUR PANEL + :blur + (let [shapes (->> shapes (filter has-blur?))] + (when (seq shapes) + [:> style-box* {:panel :blur} + [:> blur-panel* {:shapes shapes + :objects objects}]])) + ;; TEXT PANEL + :text + (let [shapes (filter has-text? shapes)] + (when (seq shapes) + [:> style-box* {:panel :text + :shorthand (:text shorthands)} + [:> text-panel* {:shapes shapes + :color-space color-space + :resolved-tokens resolved-active-tokens + :on-font-shorthand set-shorthands}]])) - ;; SHADOW PANEL - :shadow - (let [shapes (filter has-shadow? shapes)] - (when (seq shapes) - [:> style-box* {:panel :shadow - :shorthand (:shadow shorthands)} - [:> shadow-panel* {:shapes shapes - :resolved-tokens resolved-active-tokens - :color-space color-space - :on-shadow-shorthand set-shorthands}]])) + ;; SHADOW PANEL + :shadow + (let [shapes (filter has-shadow? shapes)] + (when (seq shapes) + [:> style-box* {:panel :shadow + :shorthand (:shadow shorthands)} + [:> shadow-panel* {:shapes shapes + :resolved-tokens resolved-active-tokens + :color-space color-space + :on-shadow-shorthand set-shorthands}]])) - ;; DEFAULT WIP - [:> style-box* {:panel panel} - [:div color-space]])])])) + ;; DEFAULT WIP + [:> style-box* {:panel panel} + [:div color-space]])])] + [:div {:class (stl/css :exports-wrapper)} + [:& exports/exports + {:shapes shapes + :type type + :page-id page-id + :file-id file-id + :share-id share-id}]]])) diff --git a/frontend/src/app/main/ui/inspect/styles.scss b/frontend/src/app/main/ui/inspect/styles.scss index 0680351132..d78617bb6b 100644 --- a/frontend/src/app/main/ui/inspect/styles.scss +++ b/frontend/src/app/main/ui/inspect/styles.scss @@ -13,3 +13,8 @@ .styles-tab-workspace { block-size: calc(100vh - px2rem(180)); // TODO: Fix this hardcoded value } + +.exports-wrapper { + padding-block: var(--sp-s); + padding-inline: var(--sp-m); +} From ee1dd80b6e3b18bd28058ca195058d7dc8c34c5d Mon Sep 17 00:00:00 2001 From: Xaviju Date: Thu, 19 Mar 2026 23:22:44 +0100 Subject: [PATCH 053/288] :sparkles: Copy token name from contextual menu (#8566) --- CHANGES.md | 1 + .../playwright/ui/specs/tokens/apply.spec.js | 43 ++++++++++++------- .../tokens/management/context_menu.cljs | 5 +++ frontend/translations/en.po | 4 ++ frontend/translations/es.po | 4 ++ 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0e46a9c0da..702384e36e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ - Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313) - Add Tab/Shift+Tab navigation to rename layers sequentially (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8474) - Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137) +- Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568) ### :bug: Bugs fixed diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js index b52de56e16..a8401e2d51 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -84,8 +84,9 @@ test.describe("Tokens: Apply token", () => { await brTokenPillSM.click(); // Change token from dropdown - const brTokenOptionXl = borderRadiusSection - .getByRole("option", { name: "borderRadius.xl" }); + const brTokenOptionXl = borderRadiusSection.getByRole("option", { + name: "borderRadius.xl", + }); await expect(brTokenOptionXl).toBeVisible(); await brTokenOptionXl.click(); @@ -151,7 +152,9 @@ test.describe("Tokens: Apply token", () => { await detachButton.click(); // Open dropdown from input - const dropdownBtn = layerMenuSection.getByRole('button', { name: 'Open token list' }) + const dropdownBtn = layerMenuSection.getByRole("button", { + name: "Open token list", + }); await expect(dropdownBtn).toBeVisible(); await dropdownBtn.click(); @@ -227,8 +230,12 @@ test.describe("Tokens: Apply token", () => { await expect(firstShadowFields).toBeVisible(); // Fill in the shadow values - const offsetXInput = firstShadowFields.getByRole('textbox', { name: 'X' }); - const offsetYInput = firstShadowFields.getByRole('textbox', { name: 'Y' }); + const offsetXInput = firstShadowFields.getByRole("textbox", { + name: "X", + }); + const offsetYInput = firstShadowFields.getByRole("textbox", { + name: "Y", + }); const blurInput = firstShadowFields.getByRole("textbox", { name: "Blur", }); @@ -301,8 +308,12 @@ test.describe("Tokens: Apply token", () => { await expect(thirdShadowFields).toBeVisible(); // User adds values for the third shadow - const thirdOffsetXInput = thirdShadowFields.getByRole('textbox', { name: 'X' }); - const thirdOffsetYInput = thirdShadowFields.getByRole('textbox', { name: 'Y' }); + const thirdOffsetXInput = thirdShadowFields.getByRole("textbox", { + name: "X", + }); + const thirdOffsetYInput = thirdShadowFields.getByRole("textbox", { + name: "Y", + }); const thirdBlurInput = thirdShadowFields.getByRole("textbox", { name: "Blur", }); @@ -330,10 +341,10 @@ test.describe("Tokens: Apply token", () => { // Verify that the first shadow kept its values const firstOffsetXValue = await firstShadowFields - .getByRole('textbox', { name: 'X' }) + .getByRole("textbox", { name: "X" }) .inputValue(); const firstOffsetYValue = await firstShadowFields - .getByRole('textbox', { name: 'Y' }) + .getByRole("textbox", { name: "Y" }) .inputValue(); const firstBlurValue = await firstShadowFields .getByRole("textbox", { name: "Blur" }) @@ -359,10 +370,10 @@ test.describe("Tokens: Apply token", () => { await expect(newSecondShadowFields).toBeVisible(); const secondOffsetXValue = await newSecondShadowFields - .getByRole('textbox', { name: 'X' }) + .getByRole("textbox", { name: "X" }) .inputValue(); const secondOffsetYValue = await newSecondShadowFields - .getByRole('textbox', { name: 'Y' }) + .getByRole("textbox", { name: "Y" }) .inputValue(); const secondBlurValue = await newSecondShadowFields .getByRole("textbox", { name: "Blur" }) @@ -412,10 +423,10 @@ test.describe("Tokens: Apply token", () => { // Verify first shadow values are still there const restoredFirstOffsetX = await firstShadowFields - .getByRole('textbox', { name: 'X' }) + .getByRole("textbox", { name: "X" }) .inputValue(); const restoredFirstOffsetY = await firstShadowFields - .getByRole('textbox', { name: 'Y' }) + .getByRole("textbox", { name: "Y" }) .inputValue(); const restoredFirstBlur = await firstShadowFields .getByRole("textbox", { name: "Blur" }) @@ -435,10 +446,10 @@ test.describe("Tokens: Apply token", () => { // Verify second shadow values are still there const restoredSecondOffsetX = await newSecondShadowFields - .getByRole('textbox', { name: 'X' }) + .getByRole("textbox", { name: "X" }) .inputValue(); const restoredSecondOffsetY = await newSecondShadowFields - .getByRole('textbox', { name: 'Y' }) + .getByRole("textbox", { name: "Y" }) .inputValue(); const restoredSecondBlur = await newSecondShadowFields .getByRole("textbox", { name: "Blur" }) @@ -616,7 +627,7 @@ test.describe("Tokens: Apply token", () => { await tokensSidebar .getByRole("button", { name: "dimension.sm" }) .click({ button: "right" }); - await tokenContextMenuForToken.getByText("Y").click(); + await tokenContextMenuForToken.getByText("Y", { exact: true }).click(); // Check if measures sections is visible on right sidebar const measuresSection = page.getByRole("region", { diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs index bcb44b83c5..621f8f2f4e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs @@ -20,6 +20,7 @@ [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] + [app.util.clipboard :as clipboard] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.timers :as timers] @@ -333,6 +334,7 @@ (defn default-actions [{:keys [token selected-token-set-id on-delete-token]}] (let [{:keys [modal]} (dwta/get-token-properties token) + on-copy-name #(clipboard/to-clipboard (:name token)) on-duplicate-token #(st/emit! (dwtl/duplicate-token (:id token)))] [{:title (tr "workspace.tokens.edit") :no-selectable true @@ -350,6 +352,9 @@ {:title (tr "workspace.tokens.duplicate") :no-selectable true :action on-duplicate-token} + {:title (tr "workspace.tokens.copy-name") + :no-selectable true + :action on-copy-name} {:title (tr "workspace.tokens.delete") :no-selectable true :action #(on-delete-token token)}])) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 6b6516709e..4a7eb227f9 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7955,6 +7955,10 @@ msgstr "Delete theme" msgid "workspace.tokens.duplicate" msgstr "Duplicate token" +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:350 +msgid "workspace.tokens.copy-name" +msgstr "Copy token path" + #: src/app/main/data/workspace/tokens/library_edit.cljs:240, src/app/main/data/workspace/tokens/library_edit.cljs:509 msgid "workspace.tokens.duplicate-suffix" msgstr "copy" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 69e1aeb0bf..50678fd0c8 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -7866,6 +7866,10 @@ msgstr "Borrar theme" msgid "workspace.tokens.duplicate" msgstr "Duplicar token" +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:350 +msgid "workspace.tokens.copy-name" +msgstr "Copiar ruta de token" + #: src/app/main/data/workspace/tokens/library_edit.cljs:240, src/app/main/data/workspace/tokens/library_edit.cljs:509 msgid "workspace.tokens.duplicate-suffix" msgstr "copiar" From fb5ac5cd8b3550901c0b44b531a8517b65b8c653 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Fri, 20 Mar 2026 09:02:27 +0100 Subject: [PATCH 054/288] :bug: Add box shadow to token dropdowns (#8685) --- .../src/app/main/ui/ds/controls/shared/options_dropdown.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss index b7c3d2e40a..f5a164efb5 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss +++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss @@ -28,6 +28,7 @@ overflow-y: auto; overflow-x: hidden; z-index: var(--z-index-dropdown); + box-shadow: 0px 0px $sz-12 0px var(--color-shadow-dark); } .left-align { From 71b32b97f07a4fdbc7e2aa963c9b0838e4870cbc Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Fri, 20 Mar 2026 10:13:05 +0100 Subject: [PATCH 055/288] :wrench: Activate flag on dev enviroment (#8706) --- common/src/app/common/flags.cljc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index 23ef653592..91f1558531 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -190,8 +190,7 @@ :enable-inspect-styles :enable-feature-fdata-objects-map :enable-feature-render-wasm - ;; Temporary deactivated - #_:enable-token-import-from-library]) + :enable-token-import-from-library]) (defn parse [& flags] From 9c3fbc59b93009fe071713e4941b732ffc9bf623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Fri, 20 Mar 2026 13:42:45 +0100 Subject: [PATCH 056/288] :bug: Fix visibility of go to nitrate cc option --- backend/src/app/nitrate.clj | 6 ++++-- .../src/app/main/ui/dashboard/sidebar.cljs | 21 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 4cb422389f..32e2b7ce5e 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -97,7 +97,8 @@ [:id ::sm/uuid] [:name ::sm/text] [:slug ::sm/text] - [:is-your-penpot :boolean]]) + [:is-your-penpot :boolean] + [:owner-id ::sm/uuid]]) (def ^:private schema:team [:map @@ -248,7 +249,7 @@ (defn add-org-info-to-team "Enriches a team map with organization information from Nitrate. - Adds organization-id, organization-name, organization-slug, and your-penpot fields. + Adds organization-id, organization-name, organization-slug, organization-owner-id, and your-penpot fields. Returns the original team unchanged if the request fails or org data is nil." [cfg team params] (try @@ -259,6 +260,7 @@ :organization-id (:id org) :organization-name (:name org) :organization-slug (:slug org) + :organization-owner-id (:owner-id org) :is-default (or (:is-default team) (true? (:is-your-penpot org)))) team)) (catch Throwable cause diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 491b0d9800..b392d2abca 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -312,9 +312,11 @@ on-go-to-cc-click (mf/use-fn - (mf/deps organization) + (mf/deps organization profile) (fn [] - (if (:organization-id organization) + ;; Navigate to active org if user owns it, otherwise to last visited org + (if (and (:organization-id organization) + (= (:id profile) (:organization-owner-id organization))) (dnt/go-to-nitrate-cc organization) (dnt/go-to-nitrate-cc)))) @@ -324,7 +326,9 @@ first :id) (:default-team-id profile)) - organizations (dissoc organizations default-team-id)] + organizations (dissoc organizations default-team-id) + + is-valid-license? (dnt/is-valid-license? profile)] [:> dropdown-menu* props @@ -356,10 +360,11 @@ :class (stl/css :org-dropdown-item :action)} [:span {:class (stl/css :icon-wrapper)} add-org-icon] [:span {:class (stl/css :team-text)} (tr "dashboard.create-new-org")]] - [:> dropdown-menu-item* {:on-click on-go-to-cc-click - :class (stl/css :org-dropdown-item :action)} - [:span {:class (stl/css :icon-wrapper)} arrow-up-right-icon] - [:span {:class (stl/css :team-text)} (tr "dashboard.go-to-control-center")]]])) + (when is-valid-license? + [:> dropdown-menu-item* {:on-click on-go-to-cc-click + :class (stl/css :org-dropdown-item :action)} + [:span {:class (stl/css :icon-wrapper)} arrow-up-right-icon] + [:span {:class (stl/css :team-text)} (tr "dashboard.go-to-control-center")]])])) (mf/defc teams-selector-dropdown* {::mf/private true} @@ -575,7 +580,7 @@ (defn- team->org [team] - (assoc (dm/select-keys team [:id :organization-id :organization-slug]) + (assoc (dm/select-keys team [:id :organization-id :organization-slug :organization-owner-id]) :name (:organization-name team))) (mf/defc sidebar-org-switch* From e53ff6d20b639bef207e1a0354daaadd17c539e3 Mon Sep 17 00:00:00 2001 From: Juanfran Date: Fri, 20 Mar 2026 12:30:15 +0100 Subject: [PATCH 057/288] :sparkles: Open create org modal in Nitrate --- frontend/src/app/main/data/nitrate.cljs | 4 ++++ frontend/src/app/main/ui/dashboard/sidebar.cljs | 8 +++++--- frontend/src/app/main/ui/dashboard/subscription.cljs | 7 +++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index d9743c3543..8670833bdf 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -29,6 +29,10 @@ (u/percent-encode (str organization-id)))] (st/emit! (rt/nav-raw :href href))))) +(defn go-to-nitrate-cc-create-org + [] + (st/emit! (rt/nav-raw :href "/control-center/?action=create-org"))) + (defn go-to-nitrate-billing [] (st/emit! (rt/nav-raw :href "/control-center/licenses/billing"))) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index b392d2abca..eaf52430e3 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -307,7 +307,7 @@ (mf/deps profile) (fn [] (if (dnt/is-valid-license? profile) - (dnt/go-to-nitrate-cc) + (dnt/go-to-nitrate-cc-create-org) (st/emit! (dnt/show-nitrate-popup :nitrate-form))))) on-go-to-cc-click @@ -595,7 +595,9 @@ (map team->org) (d/index-by :id))) - no-orgs? (= (count orgs) 0) + ;; There is always at least one default organization + ;; so no-orgs? is true when only that default one exists (count <= 1). + no-orgs? (<= (count orgs) 1) current-org (team->org team) @@ -630,7 +632,7 @@ (mf/deps profile) (fn [] (if (dnt/is-valid-license? profile) - (dnt/go-to-nitrate-cc) + (dnt/go-to-nitrate-cc-create-org) (st/emit! (dnt/show-nitrate-popup :nitrate-form)))))] (if no-orgs? [:div {:class (stl/css :nitrate-selected-org)} diff --git a/frontend/src/app/main/ui/dashboard/subscription.cljs b/frontend/src/app/main/ui/dashboard/subscription.cljs index 8749305c1d..043046f08d 100644 --- a/frontend/src/app/main/ui/dashboard/subscription.cljs +++ b/frontend/src/app/main/ui/dashboard/subscription.cljs @@ -135,7 +135,10 @@ handle-click (mf/use-fn (fn [] - (st/emit! (dnt/show-nitrate-popup :nitrate-form))))] + (st/emit! (dnt/show-nitrate-popup :nitrate-form)))) + + handle-go-to-cc + (mf/use-fn dnt/go-to-nitrate-cc-create-org)] ;; TODO add translations for this texts when we have the definitive ones (if (and nitrate? no-orgs-created?) @@ -148,7 +151,7 @@ [:> button* {:variant "primary" :type "button" :class (stl/css :nitrate-bottom-button) - :on-click dnt/go-to-nitrate-cc} "CREATE ORGANIZATION"]]] + :on-click handle-go-to-cc} "CREATE ORGANIZATION"]]] ;; Banner for users without nitrate license (when (not nitrate?) From 62b36f015302c6ed9ac991e7975fe58383bba5e2 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 20 Mar 2026 16:48:37 +0100 Subject: [PATCH 058/288] :bug: Restore correct branches in finalize-editor-state for text --- .../src/app/main/data/workspace/texts.cljs | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 6dc5618f66..0d11245453 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -121,7 +121,7 @@ new-shape? (contains? (:workspace-new-text-shapes state) id)] (if (ted/content-has-text? content) - (when (features/active-feature? state "render-wasm/v1") + (if (features/active-feature? state "render-wasm/v1") (let [content (d/merge (ted/export-content content) (dissoc (:content shape) :children)) new-size (dwwt/get-wasm-text-new-size shape content)] @@ -141,34 +141,34 @@ (cond-> (some? new-size) (gsh/transform-shape (ctm/change-size shape (:width new-size) (:height new-size)))))) + {:undo-group (when new-shape? id)}))))) + + (let [content (d/merge (ted/export-content content) + (dissoc (:content shape) :children)) + modifiers (get-in state [:workspace-text-modifier id])] + (rx/merge + (rx/of (update-editor-state shape nil)) + (when (and (not= content (:content shape)) + (some? (:current-page-id state)) + (some? shape)) + (rx/of + (dwsh/update-shapes + [id] + (fn [shape] + (let [{:keys [width height position-data]} modifiers] + (-> shape + (assoc :content content) + (cond-> position-data + (assoc :position-data position-data)) + (cond-> (and update-name? (some? name)) + (assoc :name name)) + (cond-> (or (some? width) (some? height)) + (gsh/transform-shape (ctm/change-size shape width height)))))) {:undo-group (when new-shape? id)})))))) - (let [content (d/merge (ted/export-content content) - (dissoc (:content shape) :children)) - modifiers (get-in state [:workspace-text-modifier id])] - (rx/merge - (rx/of (update-editor-state shape nil)) - (when (and (not= content (:content shape)) - (some? (:current-page-id state)) - (some? shape)) - (rx/of - (dwsh/update-shapes - [id] - (fn [shape] - (let [{:keys [width height position-data]} modifiers] - (-> shape - (assoc :content content) - (cond-> position-data - (assoc :position-data position-data)) - (cond-> (and update-name? (some? name)) - (assoc :name name)) - (cond-> (or (some? width) (some? height)) - (gsh/transform-shape (ctm/change-size shape width height)))))) - {:undo-group (when new-shape? id)})))))) - - (when (some? id) - (rx/of (dws/deselect-shape id) - (dwsh/delete-shapes #{id})))))))) + (when (some? id) + (rx/of (dws/deselect-shape id) + (dwsh/delete-shapes #{id}))))))))) (defn initialize-editor-state [{:keys [id name content] :as shape} decorator] From 35125dfd79ca64bf6f7ac03d79d36c48ed0b8f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?= Date: Mon, 23 Mar 2026 08:48:22 +0100 Subject: [PATCH 059/288] :sparkles: Update changelog (#8703) --- CHANGES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 86c0362128..1dda87e5ee 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,11 +11,12 @@ ### :sparkles: New features & Enhancements - Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912) +- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248) - Import Tokens from linked library (by @dfelinto) [Github #8391](https://github.com/penpot/penpot/pull/8391) - Option to download custom fonts (by @dfelinto) [Github #8320](https://github.com/penpot/penpot/issues/8320) -- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248) - Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313) - Add Tab/Shift+Tab navigation to rename layers sequentially (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8474) +- Copy and paste entire rows in existing table (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8498) - Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137) - Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568) From b637f0a917fadb6c1b00ddd41d85ff8eef20998b Mon Sep 17 00:00:00 2001 From: Andres Gonzalez Date: Mon, 23 Mar 2026 08:55:31 +0100 Subject: [PATCH 060/288] :bug: Remove wrong lines from changelog --- CHANGES.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1dda87e5ee..eeaf76071f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,8 +32,6 @@ ### :sparkles: New features & Enhancements -- Access Tokens look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114) -- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112), [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114) - Access Tokens look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114) - Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714) From 8406b5e9f86420f902320c1a4ecfbb428bd336fc Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 23 Mar 2026 09:59:57 +0100 Subject: [PATCH 061/288] :sparkles: Add nitrate api for notify org deletion (#8697) --- backend/src/app/rpc/management/nitrate.clj | 167 +++++------------- frontend/src/app/main/data/dashboard.cljs | 21 ++- .../src/app/main/ui/dashboard/sidebar.cljs | 2 +- frontend/translations/en.po | 3 + frontend/translations/es.po | 3 + 5 files changed, 70 insertions(+), 126 deletions(-) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 08fa458dcb..b6ac38a4d4 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -8,8 +8,6 @@ "Internal Nitrate HTTP RPC API. Provides authenticated access to organization management and token validation endpoints." (:require - [app.common.data :as d] - [app.common.exceptions :as ex] [app.common.features :as cfeat] [app.common.schema :as sm] [app.common.types.profile :refer [schema:profile, schema:basic-profile]] @@ -20,7 +18,6 @@ [app.msgbus :as mbus] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] - [app.rpc.commands.management :as management] [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.doc :as doc] @@ -88,21 +85,28 @@ [:organization-id ::sm/uuid] [:organization-name ::sm/text]]) -(sv/defmethod ::notify-team-change - "Notify to Penpot a team change from nitrate" - {::doc/added "2.14" - ::sm/params schema:notify-team-change - ::rpc/auth false} - [cfg {:keys [id organization-id organization-name]}] +(defn notify-team-change + [cfg team-id team-name organization-id organization-name notification] (let [msgbus (::mbus/msgbus cfg)] (mbus/pub! msgbus ;;TODO There is a bug on dashboard with teams notifications. ;;For now we send it to uuid/zero instead of team-id :topic uuid/zero :message {:type :team-org-change - :team-id id + :team-id team-id + :team-name team-name :organization-id organization-id - :organization-name organization-name}))) + :organization-name organization-name + :notification notification}))) + + +(sv/defmethod ::notify-team-change + "Notify to Penpot a team change from nitrate" + {::doc/added "2.14" + ::sm/params schema:notify-team-change + ::rpc/auth false} + [cfg {:keys [id organization-id organization-name]}] + (notify-team-change cfg id nil organization-id organization-name nil)) ;; ---- API: notify-user-added-to-organization @@ -219,121 +223,42 @@ ;; ---- API: delete-teams-keeping-your-penpot-projects -(def ^:private sql:get-projects-and-default-teams - "Get projects from specified teams along with their team owner's default team information. - This query: - - Selects projects (id, team_id, name) from teams in the provided list - - Gets the profile_id of each team owner - - Gets the default_team_id where projects should be moved - - Only includes teams where the user is owner - - Only includes projects that contain at least one non-deleted file - - Excludes deleted projects and teams" - "SELECT p.id AS project_id, - p.team_id AS source_team_id, - p.name AS project_name, - tpr.profile_id, - pr.default_team_id - FROM project AS p - JOIN team AS tm ON p.team_id = tm.id - JOIN team_profile_rel AS tpr ON tm.id = tpr.team_id - JOIN profile AS pr ON tpr.profile_id = pr.id - WHERE p.team_id = ANY(?) - AND p.deleted_at IS NULL - AND tm.deleted_at IS NULL - AND tpr.is_owner IS TRUE - AND EXISTS (SELECT 1 FROM file f WHERE f.project_id = p.id AND f.deleted_at IS NULL);") +(def ^:private sql:add-prefix-to-teams + "UPDATE team + SET name = ? || name + WHERE id = ANY(?) +RETURNING id, name;") -(def ^:private sql:delete-teams - "UPDATE team SET deleted_at = ? WHERE id = ANY(?)") -(def ^:private schema:delete-teams-keeping-your-penpot-projects +(def ^:private schema:notify-org-deletion [:map [:org-name ::sm/text] - [:teams [:vector [:map - [:id ::sm/uuid] - [:is-your-penpot ::sm/boolean]]]]]) + [:teams [:vector ::sm/uuid]]]) -(def ^:private schema:delete-teams-error - [:map - [:error ::sm/keyword] - [:message ::sm/text] - [:cause ::sm/text] - [:project-id {:optional true} ::sm/uuid] - [:project-name {:optional true} ::sm/text] - [:team-id {:optional true} ::sm/uuid] - [:phase {:optional true} [:enum :move-projects :delete-teams]]]) - -(def ^:private schema:delete-teams-result - [:or [:= nil] schema:delete-teams-error]) - -(defn- ^:private clean-org-name - "Clean and sanitize organization name to remove emojis, special characters, - and prevent potential injections. Only allows alphanumeric characters, - spaces, hyphens, underscores, and parentheses." - [org-name] - (when org-name - (-> org-name - str - str/trim - (str/replace #"[^\w\s\-_()]+" "") - (str/replace #"\s+" " ") - str/trim))) - -(sv/defmethod ::delete-teams-keeping-your-penpot-projects - "For a list of teams, move the projects of your-penpot teams to the - default team of each team owner, then delete all provided teams." +(sv/defmethod ::notify-org-deletion + "For a list of teams, rename them with the name of the deleted org, and notify + of the deletion to the connected users" {::doc/added "2.15" - ::sm/params schema:delete-teams-keeping-your-penpot-projects - ::sm/result schema:delete-teams-result} - [cfg {:keys [teams org-name ::rpc/request-at]}] + ::sm/params schema:notify-org-deletion} + [cfg {:keys [teams org-name]}] + (when (seq teams) + (let [cleaned-org-name (if org-name + (-> org-name + str + str/trim + (str/replace #"[^\w\s\-_()]+" "") + (str/replace #"\s+" " ") + str/trim) + "") + org-prefix (str "[" cleaned-org-name "] ")] + (db/tx-run! + cfg + (fn [{:keys [::db/conn] :as cfg}] + (let [ids-array (db/create-array conn "uuid" teams) + ;; ---- Rename projects ---- + updated-teams (db/exec! conn [sql:add-prefix-to-teams org-prefix ids-array])] - (let [your-penpot-team-ids (into [] (comp (filter :is-your-penpot) d/xf:map-id) teams) - all-team-ids (into [] d/xf:map-id teams) - cleaned-org-name (clean-org-name org-name) - org-prefix (if (str/empty? cleaned-org-name) - "imported: " - (str cleaned-org-name " imported: "))] - - (when (seq all-team-ids) - - (db/tx-run! cfg - (fn [{:keys [::db/conn] :as cfg}] - - ;; ---- Move projects ---- - (when (seq your-penpot-team-ids) - (let [ids-array (db/create-array conn "uuid" your-penpot-team-ids) - projects (db/exec! conn [sql:get-projects-and-default-teams ids-array])] - - (doseq [{:keys [default-team-id profile-id project-id project-name source-team-id]} projects - :when default-team-id] - - (try - (management/move-project cfg {:profile-id profile-id - :team-id default-team-id - :project-id project-id}) - - (db/update! conn :project - {:is-default false - :name (str org-prefix project-name)} - {:id project-id}) - - (catch Throwable cause - (ex/raise :type :internal - :code :nitrate-project-move-failed - :context {:project-id project-id - :project-name project-name - :team-id source-team-id} - :cause cause)))))) - - ;; ---- Delete teams ---- - (try - (let [team-ids-array (db/create-array conn "uuid" all-team-ids)] - (db/exec-one! conn [sql:delete-teams request-at team-ids-array])) - (catch Throwable cause - (ex/raise :type :internal - :code :nitrate-team-deletion-failed - :context {:team-ids all-team-ids} - :cause cause)))))) - - nil)) + ;; ---- Notify users ---- + (doseq [team updated-teams] + (notify-team-change cfg (:id team) (:name team) nil org-name "dashboard.org-deleted")))))))) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index a5ce2cd2c3..6a1d8d1646 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -685,14 +685,27 @@ (modal/hide))))) (defn handle-change-team-org - [{:keys [team-id organization-id organization-name]}] + [{:keys [team-id team-name organization-id organization-name notification]}] (ptk/reify ::handle-change-team-org + ptk/WatchEvent + (watch [_ state _] + (let [current-team-id (:current-team-id state)] + (when (and (contains? cf/flags :nitrate) + notification + (= team-id current-team-id)) + (rx/of (ntf/show {:content (tr notification organization-name) + :type :toast + :level :info + :timeout nil}))))) ptk/UpdateEvent (update [_ state] (if (contains? cf/flags :nitrate) - (d/update-in-when state [:teams team-id] assoc - :organization-id organization-id - :organization-name organization-name) + (d/update-in-when state [:teams team-id] + (fn [team] + (cond-> (assoc team + :organization-id organization-id + :organization-name organization-name) + team-name (assoc :name team-name)))) state)))) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index eaf52430e3..4b9a1aaaa4 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -601,7 +601,7 @@ current-org (team->org team) - default-org? (= (:default-team-id profile) (:id current-org)) + default-org? (nil? (:organization-id current-org)) show-orgs-menu* (mf/use-state false) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index fdcb1cac5f..bc9944638c 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -338,6 +338,9 @@ msgstr "You're going to restore %s." msgid "dashboard-restore-file-confirmation.title" msgstr "Restore file" +msgid "dashboard.org-deleted" +msgstr "The %s organization has been deleted." + #: src/app/main/ui/dashboard/placeholder.cljs:41 msgid "dashboard.add-file" msgstr "Add file" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 5f630941fe..3b12ef2a44 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -347,6 +347,9 @@ msgstr "Vas a restaurar %s." msgid "dashboard-restore-file-confirmation.title" msgstr "Restaurar archivo" +msgid "dashboard.org-deleted" +msgstr "La organización %s se ha borrado." + #: src/app/main/ui/dashboard/placeholder.cljs:41 msgid "dashboard.add-file" msgstr "Añadir archivo" From 72fd637ec22f9183a94ecb2370d7b338eb0667ee Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Mon, 23 Mar 2026 11:00:29 +0100 Subject: [PATCH 062/288] :recycle: Refactor small numeric inputs (#8660) * :recycle: Refactor individual border radius inputs * :recycle: Refactor layer opacity input * :recycle: Refactor stroke width inputs and add icon only selects * :recycle: Fix comments on PR --- .../playwright/ui/specs/tokens/apply.spec.js | 8 +- .../resources/images/icons/stroke-center.svg | 5 + .../resources/images/icons/stroke-dashed.svg | 3 + .../resources/images/icons/stroke-dotted.svg | 3 + .../resources/images/icons/stroke-inside.svg | 5 + .../resources/images/icons/stroke-mixed.svg | 4 + .../resources/images/icons/stroke-outside.svg | 5 + .../resources/images/icons/stroke-solid.svg | 3 + .../data/workspace/tokens/application.cljs | 45 +++++- .../main/ui/ds/controls/numeric_input.cljs | 7 +- .../src/app/main/ui/ds/controls/select.cljs | 41 +++-- .../src/app/main/ui/ds/controls/select.scss | 5 + .../main/ui/ds/controls/select.stories.jsx | 9 +- .../ds/controls/shared/options_dropdown.cljs | 1 + .../ui/ds/controls/utilities/token_field.cljs | 6 +- .../main/ui/ds/foundations/assets/icon.cljs | 7 + .../ui/workspace/sidebar/common/sidebar.scss | 38 +++-- .../sidebar/options/menus/border_radius.cljs | 140 ++++++++++-------- .../sidebar/options/menus/border_radius.scss | 31 ++-- .../options/menus/color_selection.cljs | 2 +- .../workspace/sidebar/options/menus/fill.cljs | 8 +- .../options/menus/input_wrapper_tokens.scss | 2 +- .../sidebar/options/menus/layer.cljs | 132 +++++++++++------ .../sidebar/options/menus/layer.scss | 52 ++++++- .../options/menus/layout_container.cljs | 6 +- .../sidebar/options/menus/measures.cljs | 18 +-- .../sidebar/options/menus/measures.scss | 2 +- .../sidebar/options/rows/stroke_row.cljs | 68 +++++---- .../sidebar/options/rows/stroke_row.scss | 20 ++- frontend/translations/en.po | 8 + frontend/translations/es.po | 8 + 31 files changed, 484 insertions(+), 208 deletions(-) create mode 100644 frontend/resources/images/icons/stroke-center.svg create mode 100644 frontend/resources/images/icons/stroke-dashed.svg create mode 100644 frontend/resources/images/icons/stroke-dotted.svg create mode 100644 frontend/resources/images/icons/stroke-inside.svg create mode 100644 frontend/resources/images/icons/stroke-mixed.svg create mode 100644 frontend/resources/images/icons/stroke-outside.svg create mode 100644 frontend/resources/images/icons/stroke-solid.svg diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js index a8401e2d51..69ed14f051 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -72,7 +72,7 @@ test.describe("Tokens: Apply token", () => { // Check if border radius sections is visible on right sidebar const borderRadiusSection = page.getByRole("region", { - name: "border-radius-section", + name: "Border radius section", }); await expect(borderRadiusSection).toBeVisible(); @@ -135,7 +135,7 @@ test.describe("Tokens: Apply token", () => { // Check if opacity sections is visible on right sidebar const layerMenuSection = page.getByRole("region", { - name: "layer-menu-section", + name: "Layer menu section", }); await expect(layerMenuSection).toBeVisible(); @@ -688,7 +688,7 @@ test.describe("Tokens: Apply token", () => { // Check if border radius sections is visible on right sidebar const borderRadiusSection = page.getByRole("region", { - name: "border-radius-section", + name: "Border radius section", }); await expect(borderRadiusSection).toBeVisible(); @@ -897,7 +897,7 @@ test.describe("Tokens: Detach token", () => { // Check if border radius sections is visible on right sidebar const borderRadiusSection = page.getByRole("region", { - name: "border-radius-section", + name: "Border radius section", }); await expect(borderRadiusSection).toBeVisible(); diff --git a/frontend/resources/images/icons/stroke-center.svg b/frontend/resources/images/icons/stroke-center.svg new file mode 100644 index 0000000000..a00cdf58df --- /dev/null +++ b/frontend/resources/images/icons/stroke-center.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/resources/images/icons/stroke-dashed.svg b/frontend/resources/images/icons/stroke-dashed.svg new file mode 100644 index 0000000000..40c3bdcae1 --- /dev/null +++ b/frontend/resources/images/icons/stroke-dashed.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/resources/images/icons/stroke-dotted.svg b/frontend/resources/images/icons/stroke-dotted.svg new file mode 100644 index 0000000000..8b3c1940e3 --- /dev/null +++ b/frontend/resources/images/icons/stroke-dotted.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/resources/images/icons/stroke-inside.svg b/frontend/resources/images/icons/stroke-inside.svg new file mode 100644 index 0000000000..21f2eb1c52 --- /dev/null +++ b/frontend/resources/images/icons/stroke-inside.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/resources/images/icons/stroke-mixed.svg b/frontend/resources/images/icons/stroke-mixed.svg new file mode 100644 index 0000000000..56070d56ee --- /dev/null +++ b/frontend/resources/images/icons/stroke-mixed.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/resources/images/icons/stroke-outside.svg b/frontend/resources/images/icons/stroke-outside.svg new file mode 100644 index 0000000000..0f4dec0924 --- /dev/null +++ b/frontend/resources/images/icons/stroke-outside.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/resources/images/icons/stroke-solid.svg b/frontend/resources/images/icons/stroke-solid.svg new file mode 100644 index 0000000000..a9bba0e9b9 --- /dev/null +++ b/frontend/resources/images/icons/stroke-solid.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index 3ee7758284..9c79d40260 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -822,9 +822,50 @@ :shape-ids shape-ids :on-update-shape on-update-shape})))))))) -(defn apply-token-on-selected +(defn apply-token-from-input + [{:keys [token attrs shape-ids expand-with-children]}] + (ptk/reify ::apply-token-from-input + ptk/WatchEvent + (watch [_ state _] + (let [objects (dsh/lookup-page-objects state) + shapes (into [] (keep (d/getf objects)) shape-ids) + + shapes + (if expand-with-children + (into [] + (mapcat (fn [shape] + (if (= (:type shape) :group) + (keep objects (:shapes shape)) + [shape]))) + shapes) + shapes) + + {:keys [attributes _ on-update-shape]} + (get token-properties (:type token)) + + on-update-shape + (if (seq attrs) + (or (get attr->shape-update (first attrs)) on-update-shape) + on-update-shape)] + + (rx/of + (cond + (and (= (:type token) :spacing) + (nil? attrs)) + (apply-spacing-token-separated {:token token + :attr attrs + :shapes shapes}) + + :else + (apply-token {:attributes (if (empty? attrs) attributes attrs) + :token token + :shape-ids shape-ids + :on-update-shape on-update-shape}))))))) + + +(defn apply-token-on-color-selected [color-operations token] - (ptk/reify ::apply-token-on-selected + (ptk/reify ::apply-token-on-color-selected ptk/WatchEvent (watch [_ _ _] (let [undo-id (js/Symbol)] diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs index cb86f6bdab..a98dba1c8d 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs @@ -140,6 +140,8 @@ [:on-focus {:optional true} fn?] [:on-detach {:optional true} fn?] [:property {:optional true} :string] + [:tooltip-placement {:optional true} + [:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]] [:align {:optional true} [:maybe [:enum :left :right]]]]) (mf/defc numeric-input* @@ -151,7 +153,7 @@ tokens applied-token empty-to-end on-change on-blur on-focus on-detach property align ref name - text-icon] + tooltip-placement text-icon] :rest props}] (let [;; NOTE: we use mfu/bean here for transparently handle @@ -574,9 +576,9 @@ :icon i/tokens :tooltip-class (stl/css :button-tooltip) :class (stl/css :invisible-button) - :tooltip-placement "top-left" :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown") :ref open-dropdown-ref + :tooltip-placement tooltip-placement :on-click open-dropdown}]))) :max-length max-length}) @@ -603,6 +605,7 @@ :class inner-class :property property :is-open is-open + :tooltip-placement tooltip-placement :slot-start (when (or icon text-icon) (mf/html (cond diff --git a/frontend/src/app/main/ui/ds/controls/select.cljs b/frontend/src/app/main/ui/ds/controls/select.cljs index d40d7275b8..c31fb45264 100644 --- a/frontend/src/app/main/ui/ds/controls/select.cljs +++ b/frontend/src/app/main/ui/ds/controls/select.cljs @@ -9,8 +9,10 @@ [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown* schema:option]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] + [app.main.ui.ds.tooltip.tooltip :refer [tooltip*]] [app.util.dom :as dom] [app.util.keyboard :as kbd] [app.util.object :as obj] @@ -50,15 +52,17 @@ [:map [:options [:vector {:min 1} schema:option]] [:class {:optional true} :string] + [:wrapper-class {:optional true} :string] [:disabled {:optional true} :boolean] [:default-selected {:optional true} :string] [:empty-to-end {:optional true} [:maybe :boolean]] [:on-change {:optional true} fn?] - [:variant {:optional true} [:maybe [:enum "default" "ghost"]]]]) + [:dropdown-alignment {:optional true} [:maybe [:enum :left :right]]] + [:variant {:optional true} [:maybe [:enum "default" "ghost" "icon-only"]]]]) (mf/defc select* {::mf/schema schema:select} - [{:keys [options class disabled default-selected empty-to-end on-change variant] :rest props}] + [{:keys [options class disabled default-selected empty-to-end on-change variant wrapper-class dropdown-alignment] :rest props}] (let [;; NOTE: we use mfu/bean here for transparently handle ;; options provide as clojure data structures or javascript ;; plain objects and lists. @@ -192,26 +196,40 @@ (some? icon) dimmed? - (:dimmed selected-option)] + (:dimmed selected-option) + + icon-ref (mf/use-ref nil) + icon-id (mf/use-id)] (mf/with-effect [options] (mf/set-ref-val! options-ref options)) - [:div {:class (stl/css :select-wrapper) + [:div {:class [wrapper-class (stl/css :select-wrapper)] :on-click on-click :ref select-ref :on-blur on-blur} [:> :button props [:span {:class (stl/css-case :select-header true - :header-icon has-icon?)} + :header-icon has-icon? + :header-icon-only (= variant "icon-only"))} (when ^boolean has-icon? - [:> icon* {:icon-id icon - :size "s" - :aria-hidden true}]) - [:span {:class (stl/css-case :header-label true - :header-label-dimmed (or empty-selected-id? dimmed?))} - (if ^boolean empty-selected-id? "--" label)]] + (if (= variant "icon-only") + [:> tooltip* {:content label + :trigger-ref icon-ref + :id (dm/str icon-id "-name") + :class (stl/css :option-text)} + [:> icon* {:icon-id icon + :ref icon-ref + :aria-labelledby (dm/str icon-id "-name")}]] + [:> icon* {:icon-id icon + :size "s" + :aria-hidden true}])) + + (when-not ^boolean (= variant "icon-only") + [:span {:class (stl/css-case :header-label true + :header-label-dimmed (or empty-selected-id? dimmed?))} + (if ^boolean empty-selected-id? "--" label)])] [:> icon* {:icon-id i/arrow-down :class (stl/css :arrow) @@ -224,5 +242,6 @@ :options options :selected selected-id :focused focused-id + :align dropdown-alignment :empty-to-end empty-to-end :ref set-option-ref}])])) diff --git a/frontend/src/app/main/ui/ds/controls/select.scss b/frontend/src/app/main/ui/ds/controls/select.scss index d52be44549..3b96b1b7e8 100644 --- a/frontend/src/app/main/ui/ds/controls/select.scss +++ b/frontend/src/app/main/ui/ds/controls/select.scss @@ -109,3 +109,8 @@ grid-template-columns: auto 1fr; color: var(--select-icon-color); } + +.header-icon-only { + grid-template-columns: 1fr; + color: var(--select-icon-color); +} diff --git a/frontend/src/app/main/ui/ds/controls/select.stories.jsx b/frontend/src/app/main/ui/ds/controls/select.stories.jsx index 3cf750d5d7..8a2005cd32 100644 --- a/frontend/src/app/main/ui/ds/controls/select.stories.jsx +++ b/frontend/src/app/main/ui/ds/controls/select.stories.jsx @@ -9,7 +9,7 @@ import Components from "@target/components"; const { Select } = Components; -const variants = ["default", "ghost"]; +const variants = ["default", "ghost", "icon-only"]; const options = [ { id: "option-code", label: "Code" }, @@ -75,3 +75,10 @@ export const EmptyToEnd = { emptyToEnd: true, }, }; + +export const OnlyWithIcons = { + args: { + options: optionsWithIcons, + variant: variants[2], + }, +}; diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs index 0191891398..7d566f7d63 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs @@ -28,6 +28,7 @@ [:resolved-value {:optional true} [:or :int :string :float]] [:name {:optional true} :string] + [:value {:optional true} :keyword] [:icon {:optional true} schema:icon-list] [:label {:optional true} :string] [:aria-label {:optional true} :string]]) diff --git a/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs b/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs index 3e825f2b38..8b67fa82f8 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs +++ b/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs @@ -31,12 +31,14 @@ [:on-token-key-down fn?] [:on-blur {:optional true} fn?] [:on-focus {:optional true} fn?] + [:tooltip-placement {:optional true} + [:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]] [:detach-token fn?]]) (mf/defc token-field* {::mf/schema schema:token-field} [{:keys [id label value slot-start disabled class - on-click on-token-key-down on-blur detach-token + on-click on-token-key-down on-blur detach-token tooltip-placement token-wrapper-ref token-detach-btn-ref on-focus property is-open]}] (let [set-active? (some? id) content (if set-active? @@ -92,8 +94,8 @@ :class (stl/css-case :invisible-button true :invisible-btn-dropdown-open is-open) :tooltip-class (stl/css :button-tooltip) + :tooltip-placement tooltip-placement :icon i/broken-link :ref token-detach-btn-ref - :tooltip-placement "top-left" :aria-label (tr "ds.inputs.token-field.detach-token") :on-click detach-token}])]])) diff --git a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs index 0e83173bee..0203bf9999 100644 --- a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs +++ b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs @@ -245,12 +245,19 @@ (def ^:icon-id status-update "status-update") (def ^:icon-id status-wrong "status-wrong") (def ^:icon-id stroke-arrow "stroke-arrow") +(def ^:icon-id stroke-center "stroke-center") (def ^:icon-id stroke-circle "stroke-circle") +(def ^:icon-id stroke-dashed "stroke-dashed") (def ^:icon-id stroke-diamond "stroke-diamond") +(def ^:icon-id stroke-dotted "stroke-dotted") +(def ^:icon-id stroke-inside "stroke-inside") +(def ^:icon-id stroke-mixed "stroke-mixed") +(def ^:icon-id stroke-outside "stroke-outside") (def ^:icon-id stroke-rectangle "stroke-rectangle") (def ^:icon-id stroke-rounded "stroke-rounded") (def ^:icon-id stroke-size "stroke-size") (def ^:icon-id stroke-squared "stroke-squared") +(def ^:icon-id stroke-solid "stroke-solid") (def ^:icon-id stroke-triangle "stroke-triangle") (def ^:icon-id svg "svg") (def ^:icon-id swatches "swatches") diff --git a/frontend/src/app/main/ui/workspace/sidebar/common/sidebar.scss b/frontend/src/app/main/ui/workspace/sidebar/common/sidebar.scss index b5a372a431..a20ad8d615 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/common/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/common/sidebar.scss @@ -43,11 +43,15 @@ $column-number: 8; // total number of columns // -> 8 columns (32px each) + 7 gaps (4px each) = 284px // Derived widths -$options-width: calc(#{$column-width} * #{$column-number} + #{$column-gap} * calc(#{$column-number} - 1)); -$seven-column-width: calc( - #{$column-width} * calc(#{$column-number} - 1) + #{$column-gap} * calc(#{$column-number} - 2) -); +@function grid-width($cols) { + @return calc(#{$column-width} * #{$cols} + #{$column-gap} * #{$cols - 1}); +} +$options-width: grid-width($column-number); +$two-column-width: grid-width(2); +$three-column-width: grid-width(3); +$four-column-width: grid-width(4); +$seven-column-width: grid-width(7); // ------------------------------------------------------------ // Grid mixin — applies the standard structure to any container // ------------------------------------------------------------ @@ -73,16 +77,28 @@ $seven-column-width: calc( // |___|-|___|-|___|-|___|-|___|-|___|-|___|-|___| // -> 8 columns (32px each) + 7 gaps (4px each) = 284px // -// But one block (grid-exception-input) doesn’t fit perfectly: +// But two blocks don’t fit perfectly: +// First (grid-exception-input-width) // |__________________|-|__________________|-|___| -// -// We calculate the width of that grid-exception-input as: +// We calculate the width of that grid-exception-input-width as: // // - 3.5 columns of base grid width // - + 3 inter-column gaps // - − half a gap (because it’s visually shared with the next block) - $grid-exception-input-width: calc(#{$sz-32} * 3.5 + 3 * var(--sp-xs) - (var(--sp-xs) / 2)); +// +// |___|-|___|-|___|-|___|-|___|-|___|-|___|-|___| +// +// Second (grid-exception-input-width-small) +// |__________________|-|____________|-|___|-|___| +// +// We calculate the width of that grid-exception-input-width-small as: +// +// - 2.5 columns of base grid width +// - + 2 inter-column gaps +// - − half a gap (because it’s visually shared with the next block) + +$grid-exception-input-width-small: calc(#{$sz-32} * 2.5 + 2 * var(--sp-xs) - (var(--sp-xs) / 2)); // ============================================================ // CSS VARIABLES (exposed for runtime use) @@ -95,7 +111,11 @@ $grid-exception-input-width: calc(#{$sz-32} * 3.5 + 3 * var(--sp-xs) - (var(--sp --left-sidebar-width-max: #{$left-sidebar-width-max}; --right-sidebar-width: #{$right-sidebar-width}; --right-sidebar-width-max: #{$right-sidebar-width-max}; - --7-columns-dropdown-width: #{$seven-column-width}; + --2-columns-width: #{$two-column-width}; + --3-columns-width: #{$three-column-width}; + --4-columns-width: #{$four-column-width}; + --7-columns-width: #{$seven-column-width}; --options-width: #{$options-width}; --grid-exception-input-width: #{$grid-exception-input-width}; + --grid-exception-input-width-small: #{$grid-exception-input-width-small}; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs index 13c260726f..16feaa68d0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs @@ -164,64 +164,50 @@ (mf/with-effect [ids] (reset! radius-expanded* false)) - [:section {:class (dm/str class " " (stl/css :radius)) - :aria-label "border-radius-section"} - (if (not radius-expanded) - (if token-numeric-inputs - [:> numeric-input-wrapper* - {:on-change on-all-radius-change - :on-detach on-detach-all - :icon i/corner-radius - :min 0 - :attr :border-radius - :nillable true - :property (tr "workspace.options.radius") - :applied-token (cond - (not (seq applied-tokens)) - nil + (if token-numeric-inputs + [:section {:class (dm/str class " " (stl/css :radius-token)) + :aria-label (tr "workspace.options.radius.radius-section")} + [:div {:class (stl/css :radius-first-row)} + [:> numeric-input-wrapper* + {:on-change on-all-radius-change + :on-detach on-detach-all + :icon i/corner-radius + :min 0 + :attr :border-radius + :nillable true + :property (tr "workspace.options.radius") + :applied-token (cond + (not (seq applied-tokens)) + nil - (or (not all-values-equal?) (not all-token-equal?)) - :multiple + (or (not all-values-equal?) (not all-token-equal?)) + :multiple - :else - (get applied-tokens :r1)) - :align :right - :placeholder (cond - (or (not all-values-equal?) - (not all-token-equal?)) - (tr "settings.multiple") - :else - "--") - :value (if all-values-equal? - (if (nil? (:r1 values)) - 0 - (:r1 values)) - nil)}] - - [:div {:class (stl/css :radius-1) - :title (tr "workspace.options.radius")} - [:> icon* {:icon-id i/corner-radius - :size "s" - :class (stl/css :icon)}] - [:> deprecated-input/numeric-input* - {:placeholder (cond - (not all-values-equal?) - (tr "settings.multiple") - (= :multiple (:r1 values)) - (tr "settings.multiple") :else - "--") - :min 0 - :nillable true - :on-change on-all-radius-change - :value (if all-values-equal? - (if (nil? (:r1 values)) - 0 - (:r1 values)) - nil)}]]) + (get applied-tokens :r1)) + :align :right + :placeholder (cond + (or (not all-values-equal?) + (not all-token-equal?)) + (tr "settings.multiple") + :else + "--") + :value (if all-values-equal? + (if (nil? (:r1 values)) + 0 + (:r1 values)) + nil)}] + [:> icon-button* {:class (stl/css-case :selected radius-expanded) + :variant "ghost" + :tooltip-placement "top-left" + :on-click toggle-radius-mode + :aria-label (if radius-expanded + (tr "workspace.options.radius.hide-all-corners") + (tr "workspace.options.radius.show-single-corners")) + :icon i/corner-radius}]] - (if token-numeric-inputs - [:div {:class (stl/css :radius-4)} + (when radius-expanded + [:div {:class (stl/css :radius-4-token)} [:> numeric-input-wrapper* {:on-change on-radius-r1-change :on-detach on-detach-r1 @@ -249,6 +235,7 @@ :property (tr "workspace.options.radius-top-right") :applied-token (get applied-tokens :r2) :align :right + :tooltip-placement "top-left" :inner-class (stl/css :no-icon-input) :placeholder (cond (or (= :multiple (get applied-tokens :r2)) @@ -293,9 +280,33 @@ "--") :align :right :class (stl/css :radius-wrapper) + :tooltip-placement "top-left" :inner-class (stl/css :no-icon-input) - :value (:r3 values)}]] - + :value (:r3 values)}]])] + [:section {:class (dm/str class " " (stl/css :radius)) + :aria-label (tr "workspace.options.radius.radius-section")} + (if (not radius-expanded) + [:div {:class (stl/css :radius-1) + :title (tr "workspace.options.radius")} + [:> icon* {:icon-id i/corner-radius + :size "s" + :class (stl/css :icon)}] + [:> deprecated-input/numeric-input* + {:placeholder (cond + (not all-values-equal?) + (tr "settings.multiple") + (= :multiple (:r1 values)) + (tr "settings.multiple") + :else + "--") + :min 0 + :nillable true + :on-change on-all-radius-change + :value (if all-values-equal? + (if (nil? (:r1 values)) + 0 + (:r1 values)) + nil)}]] [:div {:class (stl/css :radius-4)} [:div {:class (stl/css :small-input)} [:> deprecated-input/numeric-input* @@ -327,12 +338,11 @@ :title (tr "workspace.options.radius-bottom-right") :min 0 :on-change on-radius-r3-change - :value (:r3 values)}]]])) - - [:> icon-button* {:class (stl/css-case :selected radius-expanded) - :variant "ghost" - :on-click toggle-radius-mode - :aria-label (if radius-expanded - (tr "workspace.options.radius.hide-all-corners") - (tr "workspace.options.radius.show-single-corners")) - :icon i/corner-radius}]])) + :value (:r3 values)}]]]) + [:> icon-button* {:class (stl/css-case :selected radius-expanded) + :variant "ghost" + :on-click toggle-radius-mode + :aria-label (if radius-expanded + (tr "workspace.options.radius.hide-all-corners") + (tr "workspace.options.radius.show-single-corners")) + :icon i/corner-radius}]]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss index 5473314c67..222dc5bd03 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss @@ -14,7 +14,8 @@ gap: var(--sp-xs); } -.radius-1 { +.radius-1, +.small-input { @extend .input-element; @include t.use-typography("body-small"); } @@ -25,23 +26,12 @@ gap: var(--sp-xs); } -.small-input { - @extend .input-element; - @include t.use-typography("body-small"); -} - .selected { border-color: var(--button-icon-border-color-selected); background-color: var(--button-icon-background-color-selected); color: var(--color-accent-primary); } -.selected { - border-color: var(--button-icon-border-color-selected); - background-color: var(--button-icon-background-color-selected); - color: var(--button-icon-foreground-color-selected); -} - .icon { margin-inline: var(--sp-xs); } @@ -53,3 +43,20 @@ .dropdown-offset { --dropdown-offset: #{px2rem(-65)}; } + +.radius-token { + display: grid; + gap: var(--sp-xs); +} + +.radius-first-row { + display: grid; + grid-template-columns: 1fr auto; + gap: var(--sp-xs); +} + +.radius-4-token { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--sp-xs); +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs index eac46af116..31dd4ad3a5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs @@ -190,7 +190,7 @@ [color-operations _] (retrieve-color-operations groups old-color prev-colors)] (mf/set-ref-val! prev-colors-ref (conj prev-colors color)) - (st/emit! (dwta/apply-token-on-selected color-operations token)))))] + (st/emit! (dwta/apply-token-on-color-selected color-operations token)))))] [:div {:class (stl/css :element-set)} [:div {:class (stl/css :element-title)} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 20a4198431..b3509487e2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -174,10 +174,10 @@ (mf/deps ids) (fn [_ token] (st/emit! - (dwta/toggle-token {:token token - :attrs #{:fill} - :shape-ids ids - :expand-with-children true})))) + (dwta/apply-token-from-input {:token token + :attrs #{:fill} + :shape-ids ids + :expand-with-children true})))) on-detach-token (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.scss index b5d6de75d1..aeeaa99191 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.scss @@ -5,5 +5,5 @@ // Copyright (c) KALEIDOS INC .numeric-input-wrapper { - --dropdown-width: var(--7-columns-dropdown-width); + --dropdown-width: var(--7-columns-width); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs index 3e5cfe9921..3c08d9d6d8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs @@ -138,14 +138,13 @@ on-opacity-change (mf/use-fn - (mf/deps on-change handle-opacity-change) + (mf/deps handle-opacity-change) (fn [value] (if (or (string? value) (number? value)) (handle-opacity-change value) - (do - (st/emit! (dwta/toggle-token {:token (first value) - :attrs #{:opacity} - :shape-ids ids})))))) + (st/emit! (dwta/apply-token-from-input {:token (first value) + :attrs #{:opacity} + :shape-ids ids}))))) handle-set-hidden (mf/use-fn @@ -205,20 +204,25 @@ preview-complete?)) (swap! state* assoc :selected-blend-mode current-blend-mode))) - [:section {:class (stl/css-case :element-set-content true - :hidden hidden?) - :aria-label "layer-menu-section"} - [:div {:class (stl/css :select)} - [:& select - {:default-value selected-blend-mode - :options options - :on-change handle-change-blend-mode - :is-open? option-highlighted? - :class (stl/css-case :hidden-select hidden?) - :on-pointer-enter-option handle-blend-mode-enter - :on-pointer-leave-option handle-blend-mode-leave}]] + ;; NOTE: + ;; This code is temporarily duplicated because the UI is changing with a new feature. + ;; The new implementation is currently behind a feature/config flag and not yet released. + ;; Once the feature is released, the duplicated ClojureScript and SCSS code should be removed. + ;; https://tree.taiga.io/project/penpot/task/13704 + + (if token-numeric-inputs + ;; TODO: When duplicated code is remove rename this class removing the "token" reference from it + [:section {:class (stl/css :element-set-content-token) + :aria-label (tr "workspace.options.layer-options.layer-section")} + [:& select + {:default-value selected-blend-mode + :options options + :on-change handle-change-blend-mode + :is-open? option-highlighted? + :class (stl/css-case :hidden-select hidden?) + :on-pointer-enter-option handle-blend-mode-enter + :on-pointer-leave-option handle-blend-mode-leave}] - (if token-numeric-inputs [:> numeric-input-wrapper* {:on-change on-opacity-change :on-detach on-detach-token @@ -233,10 +237,54 @@ (tr "settings.multiple") "--") :align :right + :disabled hidden? :class (stl/css :numeric-input-wrapper) :value (* 100 (or (get values :opacity) 1))}] + (cond + (or (= :multiple hidden?) (not hidden?)) + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.options.layer-options.toggle-layer") + :on-click handle-set-hidden + :tooltip-placement "top-left" + :icon i/shown}] + + :else + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.options.layer-options.toggle-layer") + :on-click handle-set-visible + :tooltip-placement "top-left" + :icon i/hide}]) + + (cond + (or (= :multiple blocked?) (not blocked?)) + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.shape.menu.lock") + :on-click handle-set-blocked + :tooltip-placement "top-left" + :icon i/unlock}] + + :else + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.shape.menu.unlock") + :on-click handle-set-unblocked + :tooltip-placement "top-left" + :icon i/lock}])] + + [:section {:class (stl/css-case :element-set-content true + :hidden hidden?) + :aria-label (tr "workspace.options.layer-options.layer-section")} + [:div {:class (stl/css :select)} + [:& select + {:default-value selected-blend-mode + :options options + :on-change handle-change-blend-mode + :is-open? option-highlighted? + :class (stl/css-case :hidden-select hidden?) + :on-pointer-enter-option handle-blend-mode-enter + :on-pointer-leave-option handle-blend-mode-leave}]] + [:div {:class (stl/css :input) :title (tr "workspace.options.opacity")} [:span {:class (stl/css :icon)} "%"] @@ -246,31 +294,31 @@ :on-change handle-opacity-change :min 0 :max 100 - :className (stl/css :numeric-input)}]]) + :className (stl/css :numeric-input)}]] - [:div {:class (stl/css :actions)} - (cond - (or (= :multiple hidden?) (not hidden?)) - [:> icon-button* {:variant "ghost" - :aria-label (tr "workspace.options.layer-options.toggle-layer") - :on-click handle-set-hidden - :icon i/shown}] + [:div {:class (stl/css :actions)} + (cond + (or (= :multiple hidden?) (not hidden?)) + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.options.layer-options.toggle-layer") + :on-click handle-set-hidden + :icon i/shown}] - :else - [:> icon-button* {:variant "ghost" - :aria-label (tr "workspace.options.layer-options.toggle-layer") - :on-click handle-set-visible - :icon i/hide}]) + :else + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.options.layer-options.toggle-layer") + :on-click handle-set-visible + :icon i/hide}]) - (cond - (or (= :multiple blocked?) (not blocked?)) - [:> icon-button* {:variant "ghost" - :aria-label (tr "workspace.shape.menu.lock") - :on-click handle-set-blocked - :icon i/unlock}] + (cond + (or (= :multiple blocked?) (not blocked?)) + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.shape.menu.lock") + :on-click handle-set-blocked + :icon i/unlock}] - :else - [:> icon-button* {:variant "ghost" - :aria-label (tr "workspace.shape.menu.unlock") - :on-click handle-set-unblocked - :icon i/lock}])]])) + :else + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.shape.menu.unlock") + :on-click handle-set-unblocked + :icon i/lock}])]]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss index 637cf9a090..21fe114676 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss @@ -7,18 +7,23 @@ @use "refactor/common-refactor.scss" as deprecated; @use "../../../sidebar/common/sidebar.scss" as sidebar; @use "ds/_utils.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/typography.scss" as t; +// This code should be remove when numeric-input-tokens are activated +// https://tree.taiga.io/project/penpot/task/13704 .element-set-content { @include sidebar.option-grid-structure; - height: deprecated.$s-32; - margin-bottom: deprecated.$s-8; + block-size: $sz-32; + margin-block-end: var(--sp-s); .select { grid-column: span 4; padding: 0; } .input { @extend .input-element; - @include deprecated.bodySmallTypography; + @include t.use-typography("body-small"); grid-column: span 2; } .actions { @@ -29,12 +34,22 @@ &.hidden { .hidden-select { - @include deprecated.hiddenElement; - border: deprecated.$s-1 solid var(--input-border-color-disabled); + cursor: default; + pointer-events: none; + box-sizing: border-box; + color: var(--input-foreground-color-disabled); + stroke: var(--input-foreground-color-disabled); + background-color: transparent; + border: $b-1 solid var(--input-border-color-disabled); } .input { - @include deprecated.hiddenElement; - border: deprecated.$s-1 solid var(--input-border-color-disabled); + cursor: default; + pointer-events: none; + box-sizing: border-box; + color: var(--input-foreground-color-disabled); + stroke: var(--input-foreground-color-disabled); + background-color: transparent; + border: $b-1 solid var(--input-border-color-disabled); .icon { stroke: var(--input-foreground-color-disabled); } @@ -45,7 +60,28 @@ } } +// This code should remain when numeric-input-tokens are activated +// https://tree.taiga.io/project/penpot/task/13704 + +// This rule should be rename when numeric-input-tokens are +// activated removing the token reference on the class +.element-set-content-token { + @include sidebar.option-grid-structure; + block-size: $sz-32; + margin-block-end: var(--sp-s); + grid-template-columns: var(--grid-exception-input-width) var(--grid-exception-input-width-small) auto auto; +} + +.hidden-select { + cursor: default; + pointer-events: none; + box-sizing: border-box; + color: var(--input-foreground-color-disabled); + stroke: var(--input-foreground-color-disabled); + background-color: transparent; + border: $b-1 solid var(--input-border-color-disabled); +} + .numeric-input-wrapper { - grid-column: span 2; --dropdown-offset: #{px2rem(-35)}; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs index 568bea26da..7ad7c0186a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs @@ -463,9 +463,9 @@ (if (or (string? value) (number? value)) (on-change :multiple attr value event) (do - (st/emit! (dwta/toggle-token {:token (first value) - :attrs #{attr} - :shape-ids ids})))))) + (st/emit! (dwta/apply-token-from-input {:token (first value) + :attrs #{attr} + :shape-ids ids})))))) on-focus (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index 9c5030e99c..ef9936d90e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -283,9 +283,9 @@ (st/emit! (udw/trigger-bounding-box-cloaking ids) (udw/update-dimensions ids attr value)) (st/emit! (udw/trigger-bounding-box-cloaking ids) - (dwta/toggle-token {:token (first value) - :attrs #{attr} - :shape-ids ids}))))) + (dwta/apply-token-from-input {:token (first value) + :attrs #{attr} + :shape-ids ids}))))) on-proportion-lock-change (mf/use-fn @@ -304,9 +304,9 @@ (st/emit! (udw/trigger-bounding-box-cloaking ids)) (st/emit! (udw/update-positions ids {attr value}))) (st/emit! (udw/trigger-bounding-box-cloaking ids) - (dwta/toggle-token {:token (first value) - :attrs #{attr} - :shape-ids ids}))))) + (dwta/apply-token-from-input {:token (first value) + :attrs #{attr} + :shape-ids ids}))))) on-rotation-change (mf/use-fn @@ -317,9 +317,9 @@ (st/emit! (udw/trigger-bounding-box-cloaking ids)) (st/emit! (udw/increase-rotation ids value))) (st/emit! (udw/trigger-bounding-box-cloaking ids) - (dwta/toggle-token {:token (first value) - :attrs #{:rotation} - :shape-ids ids}))))) + (dwta/apply-token-from-input {:token (first value) + :attrs #{:rotation} + :shape-ids ids}))))) on-width-change (mf/use-fn (mf/deps on-size-change) #(on-size-change % :width)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss index e214e0f636..c75c161942 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss @@ -191,5 +191,5 @@ // TODO: Add a proper variable to this sizing .numeric-input-measures { - --dropdown-width: var(--7-columns-dropdown-width); + --dropdown-width: var(--7-columns-width); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index 9fe822f9df..955e69f26c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -16,6 +16,7 @@ [app.main.ui.components.reorder-handler :refer [reorder-handler*]] [app.main.ui.components.select :refer [select]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.controls.select :refer [select*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.hooks :as h] [app.main.ui.workspace.sidebar.options.common :as soc] @@ -108,9 +109,9 @@ (d/concat-vec (when (= :multiple stroke-alignment) [{:value :multiple :label "--"}]) - [{:value :center :label (tr "workspace.options.stroke.center")} - {:value :inner :label (tr "workspace.options.stroke.inner")} - {:value :outer :label (tr "workspace.options.stroke.outer")}])) + [{:value :center :label (tr "workspace.options.stroke.center") :id "center" :icon "stroke-center"} + {:value :inner :label (tr "workspace.options.stroke.inner") :id "inner" :icon "stroke-inside"} + {:value :outer :label (tr "workspace.options.stroke.outer") :id "outer" :icon "stroke-outside"}])) on-alignment-change (mf/use-fn @@ -122,10 +123,10 @@ (mf/deps ids) (fn [_ token] (st/emit! - (dwta/toggle-token {:token token - :attrs #{:stroke-color} - :shape-ids ids - :expand-with-children true})))) + (dwta/apply-token-from-input {:token token + :attrs #{:stroke-color} + :shape-ids ids + :expand-with-children true})))) stroke-style (or (:stroke-style stroke) :solid) @@ -134,10 +135,10 @@ (d/concat-vec (when (= :multiple stroke-style) [{:value :multiple :label "--"}]) - [{:value :solid :label (tr "workspace.options.stroke.solid")} - {:value :dotted :label (tr "workspace.options.stroke.dotted")} - {:value :dashed :label (tr "workspace.options.stroke.dashed")} - {:value :mixed :label (tr "workspace.options.stroke.mixed")}])) + [{:value :solid :label (tr "workspace.options.stroke.solid") :id "solid" :icon "stroke-solid"} + {:value :dotted :label (tr "workspace.options.stroke.dotted") :id "dotted" :icon "stroke-dotted"} + {:value :dashed :label (tr "workspace.options.stroke.dashed") :id "dashed" :icon "stroke-dashed"} + {:value :mixed :label (tr "workspace.options.stroke.mixed") :id "mixed" :icon "stroke-mixed"}])) on-style-change (mf/use-fn @@ -212,8 +213,8 @@ :on-blur on-blur}] ;; Stroke Width, Alignment & Style - [:div {:class (stl/css :stroke-options)} - (if token-numeric-inputs + (if token-numeric-inputs + [:div {:class (stl/css :stroke-options-tokens)} [:> numeric-input-wrapper* {:on-change on-width-change :on-detach on-detach-token-width :icon i/stroke-size @@ -225,7 +226,23 @@ :property (tr "workspace.options.stroke-width") :applied-token (get applied-tokens :stroke-width) :value stroke-width}] + [:> select* {:default-selected (d/name stroke-alignment) + :options stroke-alignment-options + :variant "icon-only" + :data-testid "stroke.alignment" + :wrapper-class (stl/css :stroke-align-icon-select) + :on-change on-alignment-change}] + (when-not disable-stroke-style + [:> select* {:default-selected (d/name stroke-style) + :options stroke-style-options + :wrapper-class (stl/css :stroke-style-icon-select) + :data-testid "stroke.style" + :variant "icon-only" + :dropdown-alignment :right + :on-change on-style-change}])] + + [:div {:class (stl/css :stroke-options)} [:div {:class (stl/css :stroke-width-input) :title (tr "workspace.options.stroke-width")} [:> icon* {:icon-id i/stroke-size @@ -236,20 +253,19 @@ :on-change on-width-change :on-focus on-focus :select-on-focus select-on-focus - :on-blur on-blur}]]) + :on-blur on-blur}]] + [:div {:class (stl/css :stroke-alignment-select) + :data-testid "stroke.alignment"} + [:& select {:default-value stroke-alignment + :options stroke-alignment-options + :on-change on-alignment-change}]] - [:div {:class (stl/css :stroke-alignment-select) - :data-testid "stroke.alignment"} - [:& select {:default-value stroke-alignment - :options stroke-alignment-options - :on-change on-alignment-change}]] - - (when-not disable-stroke-style - [:div {:class (stl/css :stroke-style-select) - :data-testid "stroke.style"} - [:& select {:default-value stroke-style - :options stroke-style-options - :on-change on-style-change}]])] + (when-not disable-stroke-style + [:div {:class (stl/css :stroke-style-select) + :data-testid "stroke.style"} + [:& select {:default-value stroke-style + :options stroke-style-options + :on-change on-style-change}]])]) ;; Stroke Caps (when show-caps diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss index 19f81ac9c2..830acdab03 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss @@ -38,17 +38,12 @@ .stroke-width-input { grid-column: span 2; - // TODO replace with numeric-input* from DS @extend .input-element; @include t.use-typography("body-small"); padding-inline-start: var(--sp-xs); } -.numeric-input-wrapper { - grid-column: span 2; -} - .stroke-alignment-select { grid-column: span 3; } @@ -62,3 +57,18 @@ grid-template-columns: 1fr auto 1fr; column-gap: var(--sp-xs); } + +.stroke-options-tokens { + @include sidebar.option-grid-structure; + grid-template-columns: var(--3-columns-width) var(--grid-exception-input-width-small) var( + --grid-exception-input-width-small + ); +} + +.stroke-align-icon-select { + --dropdown-width: var(--4-columns-width); +} + +.stroke-style-icon-select { + --dropdown-width: var(--4-columns-width); +} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index bc9944638c..624b8a2acb 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -6845,6 +6845,10 @@ msgstr "Selected layers" msgid "workspace.options.layer-options.toggle-layer" msgstr "Toggle layer visibility" +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:255 +msgid "workspace.options.layer-options.layer-section" +msgstr "Layer menu section" + #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs #, unused msgid "workspace.options.layout-item.advanced-ops" @@ -7029,6 +7033,10 @@ msgstr "Collapse independent radius" msgid "workspace.options.radius.show-single-corners" msgstr "Show independent radius" +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:341 +msgid "workspace.options.radius.radius-section" +msgstr "Border radius section" + #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:191 msgid "workspace.options.recent-fonts" msgstr "Recent" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 3b12ef2a44..6c378c4985 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -6762,6 +6762,10 @@ msgstr "Capas seleccionadas" msgid "workspace.options.layer-options.toggle-layer" msgstr "Mostrar/ocultar capa" +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:255 +msgid "workspace.options.layer-options.layer-section" +msgstr "Sección del menú de capas" + #: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs #, unused msgid "workspace.options.layout-item.advanced-ops" @@ -6946,6 +6950,10 @@ msgstr "Colapsar radios individuales" msgid "workspace.options.radius.show-single-corners" msgstr "Mostrar radios individuales" +#: src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs:341 +msgid "workspace.options.radius.radius-section" +msgstr "Sección de radios de borde" + #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:191 msgid "workspace.options.recent-fonts" msgstr "Recientes" From bfb331d230fc73127c126af7738066cc35776ed7 Mon Sep 17 00:00:00 2001 From: Roland Date: Mon, 23 Mar 2026 11:24:29 +0100 Subject: [PATCH 063/288] :bug: Fix pluings API theme.addSet() crash caused by async state race in token-set proxy (#8700) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `catalog.addSet()` creates a new token set, `st/emit!` is async — the set is not yet in `@st/state` when the returned proxy is used. Calling `theme.addSet(proxy)` immediately after reads `.name` from the proxy, which calls `locate-token-set` on stale state → returns nil → `enable-set` conjs nil into the theme's `:sets` → backend rejects with 400 (`:sets #{nil}`) → workspace reloads → plugin disconnects. Fix: store `initial-name` in the proxy at construction time as a fallback for the `:name` getter during the async propagation window. Also add nil guards in `addSet`/`removeSet` as defense-in-depth. Closes #8698 Signed-off-by: rodo --- frontend/src/app/plugins/tokens.cljs | 241 +++++++++++++++------------ 1 file changed, 131 insertions(+), 110 deletions(-) diff --git a/frontend/src/app/plugins/tokens.cljs b/frontend/src/app/plugins/tokens.cljs index ec1ebf6b71..41f37476b0 100644 --- a/frontend/src/app/plugins/tokens.cljs +++ b/frontend/src/app/plugins/tokens.cljs @@ -203,123 +203,130 @@ (obj/type-of? p "TokenSetProxy")) (defn token-set-proxy - [plugin-id file-id id] - (obj/reify {:name "TokenSetProxy" - :on-error (u/handle-error plugin-id)} - :$plugin {:enumerable false :get (constantly plugin-id)} - :$file-id {:enumerable false :get (constantly file-id)} - :$id {:enumerable false :get (constantly id)} + ([plugin-id file-id id] + (token-set-proxy plugin-id file-id id nil)) + ([plugin-id file-id id initial-name] + (obj/reify {:name "TokenSetProxy" + :on-error (u/handle-error plugin-id)} + :$plugin {:enumerable false :get (constantly plugin-id)} + :$file-id {:enumerable false :get (constantly file-id)} + :$id {:enumerable false :get (constantly id)} - :id - {:get #(dm/str id)} + :id + {:get #(dm/str id)} - :name - {:this true - :get + :name + {:this true + :get + (fn [_] + ;; Prefer the authoritative state lookup; fall back to initial-name + ;; when the async state update from `catalog.addSet()` hasn't + ;; propagated yet. + (let [set (u/locate-token-set file-id id)] + (if (some? set) + (ctob/get-name set) + initial-name))) + :schema (cfo/make-token-set-name-schema + (u/locate-tokens-lib file-id) + id) + :set + (fn [_ name] + (let [set (u/locate-token-set file-id id)] + (st/emit! (dwtl/rename-token-set set name))))} + + :active + {:this true + :enumerable false + :get + (fn [_] + (let [tokens-lib (u/locate-tokens-lib file-id) + set (u/locate-token-set file-id id)] + (ctob/token-set-active? tokens-lib (ctob/get-name set)))) + :schema ::sm/boolean + :set + (fn [_ value] + (let [set (u/locate-token-set file-id id)] + (st/emit! (dwtl/set-enabled-token-set (ctob/get-name set) value))))} + + :toggleActive (fn [_] (let [set (u/locate-token-set file-id id)] - (ctob/get-name set))) - :schema (cfo/make-token-set-name-schema - (u/locate-tokens-lib file-id) - id) - :set - (fn [_ name] - (let [set (u/locate-token-set file-id id)] - (st/emit! (dwtl/rename-token-set set name))))} + (st/emit! (dwtl/toggle-token-set (ctob/get-name set))))) - :active - {:this true - :enumerable false - :get - (fn [_] - (let [tokens-lib (u/locate-tokens-lib file-id) - set (u/locate-token-set file-id id)] - (ctob/token-set-active? tokens-lib (ctob/get-name set)))) - :schema ::sm/boolean - :set - (fn [_ value] - (let [set (u/locate-token-set file-id id)] - (st/emit! (dwtl/set-enabled-token-set (ctob/get-name set) value))))} + :tokens + {:this true + :enumerable false + :get + (fn [_] + (let [tokens-lib (u/locate-tokens-lib file-id)] + (->> (ctob/get-tokens tokens-lib id) + (vals) + (map #(token-proxy plugin-id file-id id (:id %))) + (apply array))))} - :toggleActive - (fn [_] - (let [set (u/locate-token-set file-id id)] - (st/emit! (dwtl/toggle-token-set (ctob/get-name set))))) + :tokensByType + {:this true + :enumerable false + :get + (fn [_] + (let [tokens-lib (u/locate-tokens-lib file-id) + tokens (ctob/get-tokens tokens-lib id)] + (->> tokens + (vals) + (sort-by :name) + (group-by #(cto/token-type->dtcg-token-type (:type %))) + (into []) + (mapv (fn [[type tokens]] + #js [(name type) + (->> tokens + (map #(token-proxy plugin-id file-id id (:id %))) + (apply array))])) + (apply array))))} - :tokens - {:this true - :enumerable false - :get - (fn [_] - (let [tokens-lib (u/locate-tokens-lib file-id)] - (->> (ctob/get-tokens tokens-lib id) - (vals) - (map #(token-proxy plugin-id file-id id (:id %))) - (apply array))))} + :getTokenById + {:enumerable false + :schema [:tuple ::sm/uuid] + :fn (fn [token-id] + (let [token (u/locate-token file-id id token-id)] + (when (some? token) + (token-proxy plugin-id file-id id token-id))))} - :tokensByType - {:this true - :enumerable false - :get - (fn [_] - (let [tokens-lib (u/locate-tokens-lib file-id) - tokens (ctob/get-tokens tokens-lib id)] - (->> tokens - (vals) - (sort-by :name) - (group-by #(cto/token-type->dtcg-token-type (:type %))) - (into []) - (mapv (fn [[type tokens]] - #js [(name type) - (->> tokens - (map #(token-proxy plugin-id file-id id (:id %))) - (apply array))])) - (apply array))))} + :addToken + {:enumerable false + :schema (fn [args] + [:tuple (-> (cfo/make-token-schema + (-> (u/locate-tokens-lib file-id) (ctob/get-tokens id)) + (cto/dtcg-token-type->token-type (-> args (first) (get "type")))) + ;; Don't allow plugins to set the id + (sm/dissoc-key :id) + ;; Instruct the json decoder in obj/reify not to process map keys (:key-fn below) + ;; and set a converter that changes DTCG types to internal types (:decode/json). + ;; E.g. "FontFamilies" -> :font-family or "BorderWidth" -> :stroke-width + (sm/update-properties assoc :decode/json cfo/convert-dtcg-token))]) + :decode/options {:key-fn identity} + :fn (fn [attrs] + (let [tokens-lib (u/locate-tokens-lib file-id) + token (ctob/make-token attrs) + tokens-tree (-> (ctob/get-tokens-in-active-sets tokens-lib) + (assoc (:name token) token)) + resolved-tokens (ts/resolve-tokens tokens-tree) - :getTokenById - {:enumerable false - :schema [:tuple ::sm/uuid] - :fn (fn [token-id] - (let [token (u/locate-token file-id id token-id)] - (when (some? token) - (token-proxy plugin-id file-id id token-id))))} + {:keys [errors resolved-value] :as resolved-token} + (get resolved-tokens (:name token))] - :addToken - {:enumerable false - :schema (fn [args] - [:tuple (-> (cfo/make-token-schema - (-> (u/locate-tokens-lib file-id) (ctob/get-tokens id)) - (cto/dtcg-token-type->token-type (-> args (first) (get "type")))) - ;; Don't allow plugins to set the id - (sm/dissoc-key :id) - ;; Instruct the json decoder in obj/reify not to process map keys (:key-fn below) - ;; and set a converter that changes DTCG types to internal types (:decode/json). - ;; E.g. "FontFamilies" -> :font-family or "BorderWidth" -> :stroke-width - (sm/update-properties assoc :decode/json cfo/convert-dtcg-token))]) - :decode/options {:key-fn identity} - :fn (fn [attrs] - (let [tokens-lib (u/locate-tokens-lib file-id) - token (ctob/make-token attrs) - tokens-tree (-> (ctob/get-tokens-in-active-sets tokens-lib) - (assoc (:name token) token)) - resolved-tokens (ts/resolve-tokens tokens-tree) + (if resolved-value + (do (st/emit! (dwtl/create-token id token)) + (token-proxy plugin-id file-id id (:id token))) + (do (u/not-valid plugin-id :addToken (str errors)) + nil))))} - {:keys [errors resolved-value] :as resolved-token} - (get resolved-tokens (:name token))] + :duplicate + (fn [] + (st/emit! (dwtl/duplicate-token-set id))) - (if resolved-value - (do (st/emit! (dwtl/create-token id token)) - (token-proxy plugin-id file-id id (:id token))) - (do (u/not-valid plugin-id :addToken (str errors)) - nil))))} - - :duplicate - (fn [] - (st/emit! (dwtl/duplicate-token-set id))) - - :remove - (fn [] - (st/emit! (dwtl/delete-token-set id))))) + :remove + (fn [] + (st/emit! (dwtl/delete-token-set id)))))) (defn token-theme-proxy? [p] (obj/type-of? p "TokenThemeProxy")) @@ -408,15 +415,26 @@ {:enumerable false :schema [:tuple [:fn token-set-proxy?]] :fn (fn [token-set] - (let [theme (u/locate-token-theme file-id id)] - (st/emit! (dwtl/update-token-theme id (ctob/enable-set theme (obj/get token-set :name))))))} + ;; Resolve the set name before the theme lookup. The proxy's :name + ;; getter now falls back to `initial-name` when state hasn't + ;; propagated, so this is safe even for freshly created sets. + ;; Guard against nil to prevent `enable-set` from conj'ing nil + ;; into the theme's :sets — which would send `:sets #{nil}` to the + ;; backend and crash the workspace. + (let [set-name (obj/get token-set :name) + theme (u/locate-token-theme file-id id)] + (when (and (some? set-name) (some? theme)) + (st/emit! (dwtl/update-token-theme id (ctob/enable-set theme set-name))))))} :removeSet {:enumerable false :schema [:tuple [:fn token-set-proxy?]] :fn (fn [token-set] - (let [theme (u/locate-token-theme file-id id)] - (st/emit! (dwtl/update-token-theme id (ctob/disable-set theme (obj/get token-set :name))))))} + ;; Same nil guard as addSet — see comment above. + (let [set-name (obj/get token-set :name) + theme (u/locate-token-theme file-id id)] + (when (and (some? set-name) (some? theme)) + (st/emit! (dwtl/update-token-theme id (ctob/disable-set theme set-name))))))} :duplicate (fn [] @@ -484,7 +502,10 @@ (let [attrs (update attrs :name ctob/normalize-set-name) set (ctob/make-token-set attrs)] (st/emit! (dwtl/create-token-set set)) - (token-set-proxy plugin-id file-id (ctob/get-id set))))} + ;; Pass the set name as `initial-name` so the proxy can resolve + ;; it immediately, before the async `st/emit!` above propagates + ;; the new set into `@st/state`. + (token-set-proxy plugin-id file-id (ctob/get-id set) (ctob/get-name set))))} :getThemeById {:enumerable false From 4345cfaec784e832cb5cc00b9ca21fe21f6c65e6 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Mon, 23 Mar 2026 11:24:59 +0100 Subject: [PATCH 064/288] :tada: Add natural sort on token names (#8672) --- CHANGES.md | 1 + common/src/app/common/data.cljc | 45 +++++++++++++++++++ common/test/common_tests/data_test.cljc | 43 ++++++++++++++++++ .../tokens/management/token_tree.cljs | 8 ++-- 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 943e6afba4..dc1616d59c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,7 @@ - Copy and paste entire rows in existing table (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8498) - Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137) - Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568) +- Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713) ### :bug: Bugs fixed diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 414179753c..2b9748183d 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -1126,6 +1126,51 @@ (let [value (format-precision value precision)] (str value)))))) +(defn- natural-sort-key + "Splits a string into a sequence of alternating string and number segments, + converting numeric segments to longs/ints so they compare by value rather + than lexicographically. e.g. \"size10b\" => (\"size\" 10 \"b\")" + [s] + (map (fn [part] + (if (re-matches #"\d+" part) + #?(:clj (Long/parseLong part) + :cljs (js/parseInt part)) + part)) + (re-seq #"\d+|\D+" s))) + +(defn- natural-compare + "Comparator that orders strings naturally, sorting numeric segments by value + rather than lexicographically. Returns a negative number, zero, or positive + number when a is before, equal to, or after b respectively. + e.g. \"size2\" < \"size10\" instead of \"size10\" < \"size2\"." + [a b] + (loop [ka (natural-sort-key a) + kb (natural-sort-key b)] + (cond + (and (empty? ka) (empty? kb)) 0 + (empty? ka) -1 + (empty? kb) 1 + :else + (let [pa (first ka) + pb (first kb) + result (cond + (and (number? pa) (number? pb)) (compare pa pb) + (and (string? pa) (string? pb)) (compare pa pb) + (number? pa) -1 + :else 1)] + (if (zero? result) + (recur (rest ka) (rest kb)) + result))))) + +(defn natural-sort-by + "Sorts coll by extracting a string key with keyfn and ordering elements + using natural sort order, where embedded numbers are compared by value + rather than lexicographically. + e.g. (natural-sort-by :name [{:name \"size10\"} {:name \"size2\"}]) + => [{:name \"size2\"} {:name \"size10\"}]" + [key coll] + (sort-by key natural-compare coll)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Util protocols ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/common/test/common_tests/data_test.cljc b/common/test/common_tests/data_test.cljc index c4cbe4c100..cdc01a077c 100644 --- a/common/test/common_tests/data_test.cljc +++ b/common/test/common_tests/data_test.cljc @@ -137,3 +137,46 @@ (t/is (= (d/nth-index-of "abc*def*ghi" "*" 1) 3)) (t/is (= (d/nth-index-of "abc*def*ghi" "*" 2) 7)) (t/is (= (d/nth-index-of "abc*def*ghi" "*" 3) nil))) + +(t/deftest natural-sort-by-test + (t/is (= (d/natural-sort-by identity ["10" "2" "1" "11" "3" "30"]) + ["1" "2" "3" "10" "11" "30"])) + (t/is (= (d/natural-sort-by identity ["banana" "apple" "cherry"]) + ["apple" "banana" "cherry"])) + (t/is (= (d/natural-sort-by identity ["size10" "size2" "size1" "size20" "size3"]) + ["size1" "size2" "size3" "size10" "size20"])) + (t/is (= (d/natural-sort-by identity ["b1" "a2" "a10" "a1"]) + ["a1" "a2" "a10" "b1"])) + (t/is (= (d/natural-sort-by identity []) [])) + (t/is (= (d/natural-sort-by identity ["solo"]) ["solo"])) + (t/is (= (d/natural-sort-by identity ["b" "a" "a" "c"]) + ["a" "a" "b" "c"])) + (t/is (= (d/natural-sort-by :name + [{:name "big"} {:name "small"} {:name "medium"}]) + [{:name "big"} {:name "medium"} {:name "small"}])) + (t/is (= (d/natural-sort-by :name + [{:name "size10"} {:name "size2"} {:name "size1"}]) + [{:name "size1"} {:name "size2"} {:name "size10"}])) + (t/is (= (d/natural-sort-by :name + [{:name "border-radius-10"} + {:name "border-radius-2"} + {:name "border-radius-1"}]) + [{:name "border-radius-1"} + {:name "border-radius-2"} + {:name "border-radius-10"}])) + (t/is (= (d/natural-sort-by :name + [{:name "border-10-radius"} + {:name "border-2-radius"} + {:name "border-1-radius"}]) + [{:name "border-1-radius"} + {:name "border-2-radius"} + {:name "border-10-radius"}])) + (t/is (= (d/natural-sort-by :name + [{:name "border-10-radius"} + {:name "border-2-extra"} + {:name "border-2-radius"} + {:name "border-1-radius"}]) + [{:name "border-1-radius"} + {:name "border-2-extra"} + {:name "border-2-radius"} + {:name "border-10-radius"}]))) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs index c08c1cd618..3fdc067bd0 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.tokens.management.token-tree (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] [app.common.path-names :as cpn] [app.common.types.tokens-lib :as ctob] [app.main.data.workspace.tokens.library-edit :as dwtl] @@ -69,8 +70,8 @@ [:div {:class (stl/css :folder-children-wrapper) :id (str "folder-children-" (:path node))} (when children-fn - (let [children (children-fn)] - (for [child children] + (let [sorted-children (d/natural-sort-by :name (children-fn))] + (for [child sorted-children] (if (not (:leaf child)) [:ul {:class (stl/css :node-parent) :key (:path child)} @@ -127,7 +128,8 @@ tree (mf/use-memo (mf/deps tokens) (fn [] - (cpn/build-tree-root tokens separator))) + (->> (cpn/build-tree-root tokens separator) + (d/natural-sort-by :name)))) can-edit? (:can-edit (deref refs/permissions)) on-node-context-menu (mf/use-fn (mf/deps can-edit? on-node-context-menu) From 11ed09f4316643cbee20555173fcc51ffa4beecc Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 23 Mar 2026 12:29:08 +0100 Subject: [PATCH 065/288] :bug: Fix link to nitrate create org --- frontend/src/app/main/ui/settings/subscription.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index 65c1eccf95..f8fc390a3c 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -385,7 +385,7 @@ {:class (stl/css :primary-button) :type "button" :value "CREATE ORGANIZATION" - :on-click dnt/go-to-nitrate-cc}]]]]]])) + :on-click dnt/go-to-nitrate-cc-create-org}]]]]]])) (mf/defc subscription-page* [{:keys [profile]}] From 7adac6df40f853de5a2fb2d8e796c20d09717a6a Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Mon, 23 Mar 2026 16:06:23 +0100 Subject: [PATCH 066/288] :bug: Fix review comments (#8708) * :bug: Fix focus option only on arrowdown not at open * :bug: Fix focus on input when visible focus should be on options * :recycle: Improve nativation, adding tab control and moving throught options is now cyclic * :sparkles: Add selected option when inside cursor is inside option * :bug: Dropdown is positioned nex to the input alwais --- .../ds/controls/shared/options_dropdown.cljs | 23 +++++----- .../ui/ds/controls/utilities/input_field.cljs | 6 ++- .../ui/ds/controls/utilities/input_field.scss | 5 +++ .../management/forms/controls/combobox.cljs | 29 ++++++++++-- .../forms/controls/combobox_navigation.cljs | 45 +++++++++++-------- .../forms/controls/floating_dropdown.cljs | 44 +++++++++++------- .../forms/controls/token_parsing.cljs | 12 +++++ .../management/forms/controls/utils.cljs | 2 +- 8 files changed, 116 insertions(+), 50 deletions(-) diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs index 7d566f7d63..60de5830a0 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs @@ -33,17 +33,7 @@ [:label {:optional true} :string] [:aria-label {:optional true} :string]]) -(def ^:private schema:options-dropdown - [:map - [:ref {:optional true} fn?] - [:class {:optional true} :string] - [:wrapper-ref {:optional true} :any] - [:on-click fn?] - [:options [:vector schema:option]] - [:selected {:optional true} :any] - [:focused {:optional true} :any] - [:empty-to-end {:optional true} [:maybe :boolean]] - [:align {:optional true} [:maybe [:enum :left :right]]]]) + (def ^:private xf:filter-blank-id @@ -104,6 +94,17 @@ :dimmed (true? (:dimmed option)) :on-click on-click}])))) +(def ^:private schema:options-dropdown + [:map + [:ref {:optional true} fn?] + [:class {:optional true} :string] + [:wrapper-ref {:optional true} :any] + [:on-click fn?] + [:options [:vector schema:option]] + [:selected {:optional true} :any] + [:focused {:optional true} :any] + [:empty-to-end {:optional true} [:maybe :boolean]] + [:align {:optional true} [:maybe [:enum :left :right]]]]) (mf/defc options-dropdown* {::mf/schema schema:options-dropdown} diff --git a/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs b/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs index 58a3202c80..c0ee6f245e 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs +++ b/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs @@ -37,6 +37,8 @@ has-hint hint-type max-length variant slot-start slot-end + data-option-focused + input-wrapper-ref aria-label] :rest props} ref] (let [input-ref (mf/use-ref) type (d/nilv type "text") @@ -74,7 +76,9 @@ (dom/select-node input-node) (dom/focus! input-node))))] - [:div {:class [inside-class class]} + [:div {:class [inside-class class] + :ref input-wrapper-ref + :data-option-focused data-option-focused} (when (some? slot-start) slot-start) (when (some? icon) diff --git a/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss b/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss index 80068f0c2b..20533664ef 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss +++ b/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss @@ -41,6 +41,11 @@ --input-bg-color: var(--color-background-primary); --input-outline-color: var(--color-background-quaternary); } + + &[data-option-focused="true"]:has(*:focus-visible) { + --input-bg-color: var(--color-background-tertiary); + --input-outline-color: none; + } } .variant-dense, diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs index d53b8d0d60..f1ef466929 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs @@ -84,11 +84,15 @@ filter-term* (mf/use-state "") filter-term (deref filter-term*) + selected-id* (mf/use-state nil) + selected-id (deref selected-id*) + options-ref (mf/use-ref nil) dropdown-ref (mf/use-ref nil) internal-ref (mf/use-ref nil) nodes-ref (mf/use-ref nil) wrapper-ref (mf/use-ref nil) + input-wrapper-ref (mf/use-ref nil) icon-button-ref (mf/use-ref nil) ref (or ref internal-ref) @@ -117,12 +121,28 @@ state (obj/set! state id node)] (mf/set-ref-val! nodes-ref state)))) + get-selected-id + (mf/use-fn + (mf/deps dropdown-options) + (fn [] + (let [input-node (mf/ref-val ref) + value (dom/get-input-value input-node) + cursor (dom/selection-start input-node) + token-name (tp/token-at-cursor value cursor) + options (if (delay? dropdown-options) @dropdown-options dropdown-options)] + (when token-name + (->> options + (filter #(= (:name %) token-name)) + first + :id))))) + toggle-dropdown (mf/use-fn (mf/deps is-open) (fn [event] (dom/prevent-default event) (swap! is-open* not) + (reset! selected-id* (get-selected-id)) (let [input-node (mf/ref-val ref)] (dom/focus! input-node)))) @@ -157,7 +177,8 @@ :options dropdown-options :toggle-dropdown toggle-dropdown :is-open* is-open* - :on-enter on-option-enter}) + :on-enter on-option-enter + :get-selected-id get-selected-id}) on-change (mf/use-fn @@ -216,11 +237,13 @@ :hint-message (:message hint) :on-key-down on-key-down :hint-type (:type hint) + :input-wrapper-ref input-wrapper-ref :ref ref :role "combobox" :aria-activedescendant focused-id :aria-controls listbox-id :aria-expanded is-open + :data-option-focused (boolean focused-id) :slot-end (when (some? @filtered-tokens-by-type) (mf/html @@ -241,7 +264,7 @@ props) - {:keys [style ready?]} (use-floating-dropdown is-open wrapper-ref dropdown-ref)] + {:keys [style ready?]} (use-floating-dropdown is-open input-wrapper-ref wrapper-ref dropdown-ref)] (mf/with-effect [resolve-stream tokens token name token-name] (let [subs (->> resolve-stream @@ -300,7 +323,7 @@ :id listbox-id :options options :focused focused-id - :selected nil + :selected selected-id :align :right :empty-to-end empty-to-end :wrapper-ref dropdown-ref diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox_navigation.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox_navigation.cljs index b8be6dad81..3508833797 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox_navigation.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox_navigation.cljs @@ -26,14 +26,13 @@ [focusables focused-id direction] (let [ids (vec (map :id focusables)) idx (.indexOf (clj->js ids) focused-id) - idx (if (= idx -1) -1 idx) - next-idx (case direction - :down (min (dec (count ids)) (inc idx)) - :up (max 0 (dec (if (= idx -1) 0 idx))))] - (nth ids next-idx nil))) + count (count ids)] + (case direction + :down (nth ids (mod (inc idx) count) nil) + :up (nth ids (mod (if (= idx -1) 0 (dec idx)) count) nil)))) (defn use-navigation - [{:keys [is-open options nodes-ref is-open* toggle-dropdown on-enter]}] + [{:keys [is-open options nodes-ref is-open* toggle-dropdown on-enter get-selected-id]}] (let [focused-id* (mf/use-state nil) focused-id (deref focused-id*) @@ -46,6 +45,7 @@ down? (kbd/down-arrow? event) enter? (kbd/enter? event) esc? (kbd/esc? event) + tab? (kbd/tab? event) open-dropdown (kbd/is-key? event "{") close-dropdown (kbd/is-key? event "}") options (if (delay? options) @options options)] @@ -56,18 +56,21 @@ (dom/prevent-default event) (let [focusables (focusable-options options)] (cond + ;; Dropdown open: move focus to next option is-open (when (seq focusables) (let [next-id (next-focus-id focusables focused-id :down)] (reset! focused-id* next-id))) + ;; Dropdown closed with options: open and focus first (seq focusables) (do (toggle-dropdown event) + (when get-selected-id + (get-selected-id)) (reset! focused-id* (first-focusable-id focusables))) - :else - nil))) + :else nil))) up? (when is-open @@ -77,7 +80,9 @@ (reset! focused-id* next-id))) open-dropdown - (reset! is-open* true) + (do + (reset! is-open* true) + (reset! focused-id* nil)) close-dropdown (reset! is-open* false) @@ -89,21 +94,23 @@ (dom/prevent-default event) (when (some #(= (:id %) focused-id) focusables) (on-enter focused-id))))) + esc? - (do + (when is-open (dom/prevent-default event) + (dom/stop-propagation event) (reset! is-open* false)) + + tab? + (when is-open + (reset! is-open* false) + (reset! focused-id* nil)) + :else nil))))] - ;; Initial focus on first option - (mf/with-effect [is-open options] - (when is-open - (let [opts (if (delay? options) @options options) - focusables (focusable-options opts) - ids (set (map :id focusables))] - (when (and (seq focusables) - (not (contains? ids focused-id))) - (reset! focused-id* (:id (first focusables))))))) + (mf/with-effect [is-open] + (when (not is-open) + (reset! focused-id* nil))) ;; auto scroll when key down (mf/with-effect [focused-id nodes-ref] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/floating_dropdown.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/floating_dropdown.cljs index 739ca0628c..d7cf90a3f3 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/floating_dropdown.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/floating_dropdown.cljs @@ -9,7 +9,7 @@ [app.util.dom :as dom] [rumext.v2 :as mf])) -(defn use-floating-dropdown [is-open wrapper-ref dropdown-ref] +(defn use-floating-dropdown [is-open input-wrapper-ref outer-wrapper-ref dropdown-ref] (let [position* (mf/use-state nil) position (deref position*) ready* (mf/use-state false) @@ -32,7 +32,7 @@ (> dropdown-height space-below)) position (if open-up? - {:bottom (str (- windows-height (:top combobox-rect) 12) "px") + {:bottom (str (- windows-height (:top combobox-rect) -8) "px") :left (str (:left combobox-rect) "px") :width (str (:width combobox-rect) "px") :placement :top} @@ -44,27 +44,41 @@ (reset! ready* true) (reset! position* position)))] - (mf/with-effect [is-open dropdown-ref wrapper-ref] + (mf/with-effect [is-open dropdown-ref input-wrapper-ref outer-wrapper-ref] (when is-open - (let [handler (fn [event] - (let [dropdown-node (mf/ref-val dropdown-ref) - target (dom/get-target event)] - (when (or (nil? dropdown-node) - (not (instance? js/Node target)) - (not (.contains dropdown-node target))) - (js/requestAnimationFrame - (fn [] - (let [wrapper-node (mf/ref-val wrapper-ref)] - (reset! ready* true) - (calculate-position wrapper-node)))))))] + (let [recalculate + (fn [] + (js/requestAnimationFrame + (fn [] + (let [input-node (mf/ref-val input-wrapper-ref)] + (calculate-position input-node))))) + + handler + (fn [event] + (let [dropdown-node (mf/ref-val dropdown-ref) + target (dom/get-target event)] + (when (or (nil? dropdown-node) + (not (instance? js/Node target)) + (not (.contains dropdown-node target))) + (recalculate)))) + + resize-observer (js/ResizeObserver. (fn [_] (recalculate))) + outer-node (mf/ref-val outer-wrapper-ref) + dropdown-node (mf/ref-val dropdown-ref)] + (handler nil) (.addEventListener js/window "resize" handler) (.addEventListener js/window "scroll" handler true) + (when outer-node + (.observe resize-observer outer-node)) + (when dropdown-node + (.observe resize-observer dropdown-node)) (fn [] (.removeEventListener js/window "resize" handler) - (.removeEventListener js/window "scroll" handler true))))) + (.removeEventListener js/window "scroll" handler true) + (.disconnect resize-observer))))) {:style position :ready? ready diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/token_parsing.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/token_parsing.cljs index 2308bef9c7..1c317ff3e3 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/token_parsing.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/token_parsing.cljs @@ -22,6 +22,18 @@ :end (or (str/index-of value "}" last-open) cursor) :partial (subs text-before (inc last-open))}))) +(defn token-at-cursor + "Returns the full token name at the cursor position if cursor is + inside a complete {token-name} reference, nil otherwise." + [value cursor] + (let [last-open (str/last-index-of (subs value 0 cursor) "{") + last-close (str/index-of value "}" (or last-open 0))] + (when (and last-open last-close (> last-close last-open)) + (let [token-name (subs value (inc last-open) last-close)] + (when (and (seq token-name) + (not (str/includes? token-name " "))) + token-name))))) + (defn active-token [value input-node] (let [cursor (dom/selection-start input-node)] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs index e75989cdc8..8127dc2f1e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs @@ -9,7 +9,7 @@ [token] {:id (str (get token :id)) :type :token - :resolved-value (get token :resolved-value) + :resolved-value (get token :value) :name (get token :name)}) (defn- generate-dropdown-options From 852f9ce07fcc433af15847a378736ddb871592e7 Mon Sep 17 00:00:00 2001 From: Renzo <170978465+RenzoMXD@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:01:32 +0100 Subject: [PATCH 067/288] :tada: Add drag-to-change for numeric inputs (#8536) Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com> --- CHANGES.md | 1 + frontend/resources/styles/common/base.scss | 7 ++ .../styles/common/refactor/basic-rules.scss | 6 ++ .../app/main/ui/components/numeric_input.cljs | 94 ++++++++++++++-- .../main/ui/ds/controls/numeric_input.cljs | 102 ++++++++++++++++-- .../main/ui/ds/controls/numeric_input.scss | 3 + .../sidebar/options/rows/color_row.scss | 7 ++ 7 files changed, 201 insertions(+), 19 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index dc1616d59c..afb57f166a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,6 +35,7 @@ - Access Tokens look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114) - Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714) +- Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466) ### :bug: Bugs fixed diff --git a/frontend/resources/styles/common/base.scss b/frontend/resources/styles/common/base.scss index 0d61ab7ebb..eea8d93655 100644 --- a/frontend/resources/styles/common/base.scss +++ b/frontend/resources/styles/common/base.scss @@ -27,6 +27,13 @@ body { width: 100vw; height: 100vh; overflow: hidden; + + &.cursor-drag-scrub { + cursor: ew-resize !important; + * { + cursor: ew-resize !important; + } + } } #app { diff --git a/frontend/resources/styles/common/refactor/basic-rules.scss b/frontend/resources/styles/common/refactor/basic-rules.scss index c82907a5b6..07f6b82dc9 100644 --- a/frontend/resources/styles/common/refactor/basic-rules.scss +++ b/frontend/resources/styles/common/refactor/basic-rules.scss @@ -338,6 +338,12 @@ background-color: var(--input-background-color); border: $s-1 solid var(--input-border-color); color: var(--input-foreground-color); + &:not(:focus-within) { + cursor: ew-resize; + input { + cursor: ew-resize; + } + } span, label { @extend .input-label; diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index 510738b0bd..796f79e40e 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.components.numeric-input (:require [app.common.data :as d] + [app.common.math :as mth] [app.common.schema :as sm] [app.main.ui.formats :as fmt] [app.main.ui.hooks :as h] @@ -61,6 +62,11 @@ ;; Last value input by the user we need to store to save on unmount last-value* (mf/use-var value) + ;; Drag scrubbing state + drag-state* (mf/use-ref :idle) + drag-start-x* (mf/use-ref 0) + drag-start-val* (mf/use-ref 0) + parse-value (mf/use-fn (mf/deps min-value max-value value nillable? default) @@ -213,16 +219,80 @@ (mf/use-callback (mf/deps on-focus select-on-focus?) (fn [event] - (reset! last-value* (parse-value)) - (let [target (dom/get-target event)] - (when on-focus - (mf/set-ref-val! dirty-ref true) - (on-focus event)) + (when-not (= :dragging (mf/ref-val drag-state*)) + (reset! last-value* (parse-value)) + (let [target (dom/get-target event)] + (when on-focus + (mf/set-ref-val! dirty-ref true) + (on-focus event)) - (when select-on-focus? - (dom/select-text! target) - ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect - (.addEventListener target "mouseup" dom/prevent-default #js {:once true}))))) + (when select-on-focus? + (dom/select-text! target) + ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect + (.addEventListener target "mouseup" dom/prevent-default #js {:once true})))))) + + on-scrub-pointer-down + (mf/use-fn + (mf/deps value value-str min-value max-value default) + (fn [event] + (let [disabled? (unchecked-get props "disabled") + node (mf/ref-val ref) + is-focused (and (some? node) (dom/active? node))] + (when-not (or disabled? is-focused (= :multiple value-str)) + (let [client-x (.-clientX event) + start-val (or value default 0)] + (mf/set-ref-val! drag-state* :maybe-dragging) + (mf/set-ref-val! drag-start-x* client-x) + (mf/set-ref-val! drag-start-val* start-val) + (dom/capture-pointer event)))))) + + on-scrub-pointer-move + (mf/use-fn + (mf/deps apply-value update-input step-value min-value max-value) + (fn [event] + (let [state (mf/ref-val drag-state*)] + (when (or (= state :maybe-dragging) (= state :dragging)) + (let [client-x (.-clientX event) + start-x (mf/ref-val drag-start-x*) + delta-x (- client-x start-x)] + (when (and (= state :maybe-dragging) + (>= (js/Math.abs delta-x) 3)) + (mf/set-ref-val! drag-state* :dragging) + (dom/add-class! (dom/get-body) "cursor-drag-scrub")) + (when (= (mf/ref-val drag-state*) :dragging) + (let [effective-step (cond + (.-shiftKey event) (* step-value 10) + (.-ctrlKey event) (* step-value 0.1) + :else step-value) + steps (js/Math.round (/ delta-x 1)) + new-val (+ (mf/ref-val drag-start-val*) + (* steps effective-step)) + new-val (cond-> new-val + (d/num? min-value) (mth/max min-value) + (d/num? max-value) (mth/min max-value))] + (update-input new-val) + (apply-value event new-val)))))))) + + on-scrub-pointer-up + (mf/use-fn + (mf/deps ref) + (fn [event] + (let [state (mf/ref-val drag-state*)] + (when (= state :maybe-dragging) + (mf/set-ref-val! drag-state* :idle) + (dom/release-pointer event) + (when-let [node (mf/ref-val ref)] + (dom/focus! node))) + (when (= state :dragging) + (mf/set-ref-val! drag-state* :idle) + (dom/remove-class! (dom/get-body) "cursor-drag-scrub") + (dom/release-pointer event))))) + + on-scrub-lost-pointer-capture + (mf/use-fn + (fn [_event] + (mf/set-ref-val! drag-state* :idle) + (dom/remove-class! (dom/get-body) "cursor-drag-scrub"))) props (-> (obj/clone props) (obj/unset! "selectOnFocus") @@ -236,7 +306,11 @@ (obj/set! "title" title) (obj/set! "onKeyDown" handle-key-down) (obj/set! "onBlur" handle-blur) - (obj/set! "onFocus" handle-focus))] + (obj/set! "onFocus" handle-focus) + (obj/set! "onPointerDown" on-scrub-pointer-down) + (obj/set! "onPointerMove" on-scrub-pointer-move) + (obj/set! "onPointerUp" on-scrub-pointer-up) + (obj/set! "onLostPointerCapture" on-scrub-lost-pointer-capture))] (mf/with-effect [value] (when-let [input-node (mf/ref-val ref)] diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs index a98dba1c8d..5ad6492d1e 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs @@ -136,6 +136,8 @@ [:applied-token {:optional true} [:maybe [:or :string [:= :multiple]]]] [:empty-to-end {:optional true} :boolean] [:on-change {:optional true} fn?] + [:on-change-start {:optional true} fn?] + [:on-change-end {:optional true} fn?] [:on-blur {:optional true} fn?] [:on-focus {:optional true} fn?] [:on-detach {:optional true} fn?] @@ -151,7 +153,8 @@ min max max-length step is-selected-on-focus nillable tokens applied-token empty-to-end - on-change on-blur on-focus on-detach + on-change on-change-start on-change-end + on-blur on-focus on-detach property align ref name tooltip-placement text-icon] :rest props}] @@ -222,6 +225,11 @@ open-dropdown-ref (mf/use-ref nil) token-detach-btn-ref (mf/use-ref nil) + ;; Drag scrubbing state + drag-state* (mf/use-ref :idle) + drag-start-x* (mf/use-ref 0) + drag-start-val* (mf/use-ref 0) + dropdown-options (mf/with-memo [tokens filter-id] (csu/get-token-dropdown-options tokens filter-id)) @@ -442,13 +450,14 @@ (mf/use-fn (mf/deps on-focus select-on-focus) (fn [event] - (when (fn? on-focus) - (on-focus event)) - (let [target (dom/get-target event)] - (when select-on-focus - (dom/select-text! target) - ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect - (.addEventListener target "mouseup" dom/prevent-default #js {:once true}))))) + (when-not (= :dragging (mf/ref-val drag-state*)) + (when (fn? on-focus) + (on-focus event)) + (let [target (dom/get-target event)] + (when select-on-focus + (dom/select-text! target) + ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect + (.addEventListener target "mouseup" dom/prevent-default #js {:once true})))))) on-mouse-wheel (mf/use-fn @@ -468,6 +477,77 @@ (dom/stop-propagation event) (apply-value (dm/str new-val))))))) + on-scrub-pointer-down + (mf/use-fn + (mf/deps disabled is-open is-multiple? ref min max nillable default) + (fn [event] + (when-not (or disabled is-open is-multiple?) + (let [node (mf/ref-val ref) + is-focused (and (some? node) (dom/active? node)) + has-token (some? (deref token-applied*))] + (when-not (or is-focused has-token) + (let [client-x (.-clientX event) + parsed (parse-value (mf/ref-val raw-value*) (mf/ref-val last-value*) min max nillable) + start-val (or parsed default 0)] + (mf/set-ref-val! drag-state* :maybe-dragging) + (mf/set-ref-val! drag-start-x* client-x) + (mf/set-ref-val! drag-start-val* start-val) + (dom/capture-pointer event))))))) + + on-scrub-pointer-move + (mf/use-fn + (mf/deps apply-value update-input step min max on-change-start) + (fn [event] + (let [state (mf/ref-val drag-state*)] + (when (or (= state :maybe-dragging) (= state :dragging)) + (let [client-x (.-clientX event) + start-x (mf/ref-val drag-start-x*) + delta-x (- client-x start-x)] + (when (and (= state :maybe-dragging) + (>= (js/Math.abs delta-x) 3)) + (mf/set-ref-val! drag-state* :dragging) + (dom/add-class! (dom/get-body) "cursor-drag-scrub") + (when (fn? on-change-start) + (on-change-start))) + (when (= (mf/ref-val drag-state*) :dragging) + (let [effective-step (cond + (.-shiftKey event) (* step 10) + (.-ctrlKey event) (* step 0.1) + :else step) + steps (js/Math.round (/ delta-x 1)) + new-val (mth/clamp (+ (mf/ref-val drag-start-val*) + (* steps effective-step)) + min max)] + (update-input (fmt/format-number new-val)) + (apply-value (dm/str new-val))))))))) + + on-scrub-pointer-up + (mf/use-fn + (mf/deps ref on-change-end) + (fn [event] + (let [state (mf/ref-val drag-state*)] + (when (= state :maybe-dragging) + (mf/set-ref-val! drag-state* :idle) + (dom/release-pointer event) + (when-let [node (mf/ref-val ref)] + (dom/focus! node))) + (when (= state :dragging) + (mf/set-ref-val! drag-state* :idle) + (dom/remove-class! (dom/get-body) "cursor-drag-scrub") + (dom/release-pointer event) + (when (fn? on-change-end) + (on-change-end)))))) + + on-scrub-lost-pointer-capture + (mf/use-fn + (mf/deps on-change-end) + (fn [_event] + (let [was-dragging (= :dragging (mf/ref-val drag-state*))] + (mf/set-ref-val! drag-state* :idle) + (dom/remove-class! (dom/get-body) "cursor-drag-scrub") + (when (and was-dragging (fn? on-change-end)) + (on-change-end))))) + open-dropdown (mf/use-fn (mf/deps disabled ref) @@ -658,7 +738,11 @@ (mf/set-ref-val! options-ref dropdown-options)) [:div {:class [class (stl/css :input-wrapper)] - :ref wrapper-ref} + :ref wrapper-ref + :on-pointer-down on-scrub-pointer-down + :on-pointer-move on-scrub-pointer-move + :on-pointer-up on-scrub-pointer-up + :on-lost-pointer-capture on-scrub-lost-pointer-capture} (if (and (some? token-applied) (not= :multiple token-applied)) diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.scss b/frontend/src/app/main/ui/ds/controls/numeric_input.scss index bbec005618..b45ff9fe15 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.scss +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.scss @@ -20,6 +20,9 @@ inline-size: 100%; position: relative; + &:not(:focus-within) { + cursor: ew-resize; + } &:hover { --opacity-button: 1; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss index 8e88e54ed6..42f5dceefe 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss @@ -184,6 +184,9 @@ @include t.use-typography("body-small"); display: flex; align-items: center; + &:not(:focus-within) { + cursor: ew-resize; + } block-size: $sz-32; inline-size: px2rem(60); padding-inline-start: var(--sp-xs); @@ -240,6 +243,10 @@ margin: var(--sp-xxs) 0; padding: 0 0 0 px2rem(6); color: var(--color-foreground-primary); + cursor: ew-resize; + &:focus { + cursor: text; + } &[disabled] { opacity: 0.5; pointer-events: none; From 1442e4c24660dceec71b38b0f25d6a16d2292a5f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 23 Mar 2026 19:16:48 +0100 Subject: [PATCH 068/288] :paperclip: Update changelog --- CHANGES.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index afb57f166a..adcd95025a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,11 +20,13 @@ - Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137) - Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568) - Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713) +- Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466) ### :bug: Bugs fixed - Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582) + ## 2.15.0 (Unreleased) ### :boom: Breaking changes & Deprecations @@ -35,14 +37,11 @@ - Access Tokens look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114) - Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714) -- Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466) ### :bug: Bugs fixed - Fix Alt/Option to draw shapes from center point (by @offreal) [Github #8361](https://github.com/penpot/penpot/pull/8361) - Add token name on broken token pill on sidebar [Taiga #13527](https://tree.taiga.io/project/penpot/issue/13527) -- Fix collapsible sidebar property titles not toggling on click [Github #5168](https://github.com/penpot/penpot/issues/5168) -- Fix `penpot.openPage()` plugin API not navigating in the same tab; change default to same-tab navigation and allow passing a UUID string instead of a Page object [Github #8520](https://github.com/penpot/penpot/issues/8520) - Fix scroll on library modal [Taiga #13639](https://tree.taiga.io/project/penpot/issue/13639) - Update copy on penpot update message [Taiga #12924](https://tree.taiga.io/project/penpot/issue/12924) - Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534) From 65ea27cbacaa288ddcbefd98e467b461cb8a3358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Mon, 23 Mar 2026 20:05:13 +0100 Subject: [PATCH 069/288] :lipstick: Fix styles between grid layout inputs (#8673) --- CHANGES.md | 1 + .../ui/workspace/sidebar/options/menus/layout_container.scss | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index adcd95025a..d81e6ace21 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,7 @@ ### :bug: Bugs fixed - Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582) +- Fix styles between grid layout inputs [Taiga #13526](https://tree.taiga.io/project/penpot/issue/13526) ## 2.15.0 (Unreleased) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss index 10247a86b1..a0311d38a6 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss @@ -188,6 +188,7 @@ align-items: flex-start; position: relative; gap: var(--sp-xs); + margin-block-end: var(--sp-s); } .locate-button { @@ -424,6 +425,7 @@ var(--grid-exception-input-width) /* second input block */ var(--sp-xxxl); /* action button */ gap: var(--sp-xs); + margin-block-end: var(--sp-xs); } .grid-first-row { From be437fbfa195062891644b5a55af55cc74bf0a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Mon, 23 Mar 2026 11:36:21 +0100 Subject: [PATCH 070/288] :lipstick: Fix styles from select organization --- frontend/src/app/main/ui/dashboard/sidebar.scss | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index 5adcb89a04..64375cdf6e 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -548,15 +548,14 @@ height: calc(2 * var(--sp-xxxl)); max-height: calc(2 * var(--sp-xxxl)); justify-content: space-between; - padding: var(--sp-xs) var(--sp-l) var(--sp-xs) var(--sp-s); - // border-block-end: $b-1 solid var(--color-background-quaternary); + padding: 0 var(--sp-l); } .nitrate-selected-org { @include t.use-typography("body-medium"); color: var(--color-foreground-primary); width: 100%; - margin: var(--sp-xs) 0 var(--sp-xs) var(--sp-l); + padding-inline-start: var(--sp-s); display: flex; align-items: center; gap: var(--sp-s); @@ -612,5 +611,9 @@ gap: var(--sp-s); height: 100%; width: 100%; - padding: 0 var(--sp-m); + padding: 0 var(--sp-s); +} + +.current-org .arrow-icon { + margin-inline-end: var(--sp-xs); } From ccd28140bc9f8b27587f315e2fce7dd18c14006f Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 24 Mar 2026 12:03:56 +0100 Subject: [PATCH 071/288] :paperclip: Update changelog (#8744) --- CHANGES.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d81e6ace21..427c6aad56 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,7 +26,9 @@ - Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582) - Fix styles between grid layout inputs [Taiga #13526](https://tree.taiga.io/project/penpot/issue/13526) - +- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534) +- Update copy on penpot update message [Taiga #12924](https://tree.taiga.io/project/penpot/issue/12924) +- Fix scroll on library modal [Taiga #13639](https://tree.taiga.io/project/penpot/issue/13639) ## 2.15.0 (Unreleased) @@ -43,10 +45,6 @@ - Fix Alt/Option to draw shapes from center point (by @offreal) [Github #8361](https://github.com/penpot/penpot/pull/8361) - Add token name on broken token pill on sidebar [Taiga #13527](https://tree.taiga.io/project/penpot/issue/13527) -- Fix scroll on library modal [Taiga #13639](https://tree.taiga.io/project/penpot/issue/13639) -- Update copy on penpot update message [Taiga #12924](https://tree.taiga.io/project/penpot/issue/12924) -- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534) -- Fix tooltip shown on tab change [Taiga #13627](https://tree.taiga.io/project/penpot/issue/13627) - Fix tooltip activated when tab change [Taiga #13627](https://tree.taiga.io/project/penpot/issue/13627) From a59bd05c4fc9d0c94c8ebf8f24bfae67c821ba12 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Wed, 25 Mar 2026 09:51:32 +0100 Subject: [PATCH 072/288] :bug: Update visual regression tests (#8730) --- .gitignore | 1 + frontend/playwright/ui/pages/DashboardPage.js | 1 + .../ui/visual-specs/visual-dashboard.spec.js | 13 ++++++++++--- .../ui/visual-specs/visual-viewer.spec.js | 2 +- .../playwright/ui/visual-specs/workspace.spec.js | 4 +++- frontend/src/app/main/ui/dashboard/grid.cljs | 1 + 6 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 15e8533e3f..100be94717 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ /frontend/.storybook/preview-body.html /frontend/.storybook/preview-head.html /frontend/playwright-report/ +/frontend/playwright/ui/visual-specs/ /frontend/text-editor/src/wasm/ /frontend/dist/ /frontend/npm-debug.log diff --git a/frontend/playwright/ui/pages/DashboardPage.js b/frontend/playwright/ui/pages/DashboardPage.js index f7e4df2582..4dee04f18e 100644 --- a/frontend/playwright/ui/pages/DashboardPage.js +++ b/frontend/playwright/ui/pages/DashboardPage.js @@ -147,6 +147,7 @@ export class DashboardPage extends BaseWebSocketPage { "get-projects?team-id=*", "dashboard/get-projects-full.json", ); + await this.mockRPC( "get-project-files?project-id=*", "dashboard/get-project-files.json", diff --git a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js index 5e3f1a5eff..9a61a8ae97 100644 --- a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js +++ b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js @@ -9,6 +9,7 @@ test("User goes to an empty dashboard", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.goToDashboard(); + await expect(dashboardPage.page).toHaveURL(/dashboard/); await expect(dashboardPage.mainHeading).toBeVisible(); await expect(dashboardPage.page).toHaveScreenshot(); @@ -122,9 +123,7 @@ test("User goes to a full search page", async ({ page }) => { await dashboardPage.searchInput.fill("3"); await expect(dashboardPage.mainHeading).toHaveText("Search results"); - await expect( - dashboardPage.page.getByRole("button", { name: "New File 3" }), - ).toBeVisible(); + await expect(page.getByRole("button", { name: "New File 3" })).toBeVisible(); await expect(dashboardPage.page).toHaveScreenshot(); }); @@ -202,6 +201,10 @@ test("User opens teams selector with more than one team", async ({ page }) => { test("User goes to second team", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.setupDashboardFull(); + await dashboardPage.mockRPC( + `get-projects?team-id=${DashboardPage.secondTeamId}`, + "dashboard/get-projects-second-team.json", + ); await dashboardPage.goToDashboard(); await dashboardPage.teamDropdown.click(); @@ -216,6 +219,10 @@ test("User goes to second team", async ({ page }) => { test("User opens team management dropdown", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.setupDashboardFull(); + await dashboardPage.mockRPC( + `get-projects?team-id=${DashboardPage.secondTeamId}`, + "dashboard/get-projects-second-team.json", + ); await dashboardPage.goToSecondTeamDashboard(); await expect(page.getByText("Team Up")).toBeVisible(); diff --git a/frontend/playwright/ui/visual-specs/visual-viewer.spec.js b/frontend/playwright/ui/visual-specs/visual-viewer.spec.js index 977eb57fcb..8361c263fb 100644 --- a/frontend/playwright/ui/visual-specs/visual-viewer.spec.js +++ b/frontend/playwright/ui/visual-specs/visual-viewer.spec.js @@ -103,7 +103,7 @@ test("User goes to the Viewer Inspect code", async ({ page }) => { await expect( viewerPage.page.getByRole("button", { - name: "Toggle panel Size & Position", + name: "Toggle panel Size and position", }), ).toBeVisible(); diff --git a/frontend/playwright/ui/visual-specs/workspace.spec.js b/frontend/playwright/ui/visual-specs/workspace.spec.js index c1cc4ecbcc..df766736a0 100644 --- a/frontend/playwright/ui/visual-specs/workspace.spec.js +++ b/frontend/playwright/ui/visual-specs/workspace.spec.js @@ -158,7 +158,9 @@ test.describe("Palette", () => { .getByRole("button", { name: "Color Palette" }) .click(); await expect( - workspace.palette.getByRole("button", { name: "#7798ff" }), + workspace.palette.getByText( + "There are no color styles in your library yet", + ), ).toBeVisible(); }); }); diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index a24490af59..345a8a6075 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -413,6 +413,7 @@ :ref node-ref :role "button" :title (:name file) + :aria-label (:name file) :draggable (dm/str can-edit) :on-click on-select :on-key-down on-key-down From cd67dc42c43cebce3670886c8ed928de8854172d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Thu, 26 Mar 2026 09:33:13 +0100 Subject: [PATCH 073/288] :bug: Fix dates to avoid show them in english when browser is in auto (#8775) --- CHANGES.md | 1 + frontend/src/app/util/i18n.cljs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 710fa230b9..820d8f19a1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -29,6 +29,7 @@ - Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534) - Update copy on penpot update message [Taiga #12924](https://tree.taiga.io/project/penpot/issue/12924) - Fix scroll on library modal [Taiga #13639](https://tree.taiga.io/project/penpot/issue/13639) +- Fix dates to avoid show them in english when browser is in auto [Taiga #13786](https://tree.taiga.io/project/penpot/issue/13786) ## 2.15.0 (Unreleased) diff --git a/frontend/src/app/util/i18n.cljs b/frontend/src/app/util/i18n.cljs index 2f93c48129..9bc5415de8 100644 --- a/frontend/src/app/util/i18n.cljs +++ b/frontend/src/app/util/i18n.cljs @@ -213,6 +213,9 @@ (when (not= pv cv) (ct/set-default-locale cv)))) +;; Initialize date-fns locale on startup, the watch above only fires on changes +(ct/set-default-locale *current-locale*) + ;; We set the real translation function in the common i18n namespace, ;; so that when common code calls (tr ...) it uses this function. (set! app.common.i18n/tr tr) From 713ff6190bdb5cc91ef89fe688851b2ee9f6c486 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Thu, 26 Mar 2026 10:09:54 +0100 Subject: [PATCH 074/288] :wrench: Add SCSS linter (stylelint) (#8592) * :wrench: Add SCSS linter (stylelint) * :sparkles: Fix default standard scss errors with extends - WIP * :sparkles: Fix default standard scss errors * :sparkles: Update and cleanup * :sparkles: Update and cleanup * :sparkles: Update and cleanup * :bug: Fix broken visual regression tests * :paperclip: Add to CHANGES * :recycle: Remove unused class --- CHANGES.md | 3 +- frontend/package.json | 7 +- frontend/pnpm-lock.yaml | 573 ++++++++++++++++++ .../styles/common/refactor/basic-rules.scss | 112 ++-- .../common/refactor/common-dashboard.scss | 4 +- frontend/src/app/main/ui/alert.scss | 17 +- frontend/src/app/main/ui/auth.scss | 4 +- frontend/src/app/main/ui/auth/common.scss | 17 +- frontend/src/app/main/ui/auth/login.scss | 2 +- frontend/src/app/main/ui/auth/recovery.scss | 2 +- .../app/main/ui/auth/recovery_request.scss | 3 +- frontend/src/app/main/ui/auth/register.scss | 9 +- frontend/src/app/main/ui/comments.scss | 29 +- .../app/main/ui/components/button_link.scss | 3 +- .../app/main/ui/components/code_block.scss | 1 + .../app/main/ui/components/color_bullet.scss | 13 + .../main/ui/components/context_menu_a11y.scss | 16 +- .../app/main/ui/components/copy_button.scss | 19 +- .../main/ui/components/editable_label.scss | 2 + .../main/ui/components/editable_select.scss | 27 +- .../src/app/main/ui/components/forms.scss | 105 +++- .../src/app/main/ui/components/progress.scss | 13 +- .../app/main/ui/components/radio_buttons.scss | 16 +- .../main/ui/components/reorder_handler.scss | 1 + .../app/main/ui/components/search_bar.scss | 7 +- .../src/app/main/ui/components/select.scss | 32 +- .../app/main/ui/components/tab_container.scss | 12 +- .../src/app/main/ui/components/title_bar.scss | 5 + frontend/src/app/main/ui/confirm.scss | 12 +- frontend/src/app/main/ui/dashboard.scss | 3 +- .../app/main/ui/dashboard/change_owner.scss | 19 +- .../src/app/main/ui/dashboard/comments.scss | 10 +- .../src/app/main/ui/dashboard/deleted.scss | 11 +- frontend/src/app/main/ui/dashboard/files.scss | 4 +- frontend/src/app/main/ui/dashboard/fonts.scss | 35 +- frontend/src/app/main/ui/dashboard/grid.scss | 11 +- .../src/app/main/ui/dashboard/import.scss | 72 ++- .../app/main/ui/dashboard/inline_edition.scss | 2 +- .../app/main/ui/dashboard/placeholder.scss | 6 +- .../src/app/main/ui/dashboard/projects.scss | 29 +- .../src/app/main/ui/dashboard/search.scss | 4 +- .../src/app/main/ui/dashboard/sidebar.scss | 84 ++- .../app/main/ui/dashboard/subscription.scss | 20 +- frontend/src/app/main/ui/dashboard/team.scss | 63 +- .../src/app/main/ui/dashboard/team_form.scss | 22 +- .../src/app/main/ui/dashboard/templates.scss | 28 +- .../src/app/main/ui/debug/icons_preview.scss | 5 +- .../src/app/main/ui/debug/playground.scss | 0 frontend/src/app/main/ui/delete_shared.scss | 21 +- frontend/src/app/main/ui/ds/_borders.scss | 1 - frontend/src/app/main/ui/ds/_utils.scss | 5 +- .../src/app/main/ui/ds/buttons/_buttons.scss | 29 +- .../src/app/main/ui/ds/buttons/button.scss | 3 +- .../app/main/ui/ds/buttons/icon_button.scss | 5 - frontend/src/app/main/ui/ds/colors.scss | 28 +- .../src/app/main/ui/ds/controls/checkbox.scss | 4 +- .../src/app/main/ui/ds/controls/combobox.scss | 4 +- .../main/ui/ds/controls/numeric_input.scss | 10 + .../src/app/main/ui/ds/controls/select.scss | 3 +- .../main/ui/ds/controls/shared/option.scss | 4 +- .../ds/controls/shared/options_dropdown.scss | 9 +- .../ui/ds/controls/shared/token_option.scss | 9 +- .../src/app/main/ui/ds/controls/switch.scss | 11 +- .../ds/controls/utilities/hint_message.scss | 1 + .../ui/ds/controls/utilities/input_field.scss | 4 - .../main/ui/ds/controls/utilities/label.scss | 1 + .../ui/ds/controls/utilities/token_field.scss | 21 +- .../utilities/token/token_status.scss | 2 +- .../app/main/ui/ds/layers/layer_button.cljs | 3 +- .../app/main/ui/ds/layers/layer_button.scss | 6 +- .../app/main/ui/ds/layout/tab_switcher.scss | 11 +- frontend/src/app/main/ui/ds/mixins.scss | 10 +- .../shared/notification_pill.scss | 2 - .../app/main/ui/ds/notifications/toast.scss | 1 - .../src/app/main/ui/ds/product/avatar.scss | 1 + .../main/ui/ds/product/empty_placeholder.scss | 3 +- .../app/main/ui/ds/product/empty_state.scss | 1 + .../main/ui/ds/product/input_with_meta.scss | 2 + .../src/app/main/ui/ds/product/loader.scss | 2 +- .../src/app/main/ui/ds/product/milestone.scss | 13 +- .../main/ui/ds/product/milestone_group.scss | 15 +- .../app/main/ui/ds/product/panel_title.scss | 1 + .../src/app/main/ui/ds/tooltip/tooltip.scss | 6 +- frontend/src/app/main/ui/ds/typography.scss | 22 +- .../src/app/main/ui/ds/utilities/swatch.scss | 6 +- frontend/src/app/main/ui/exports/assets.scss | 95 ++- frontend/src/app/main/ui/exports/files.scss | 81 ++- .../src/app/main/ui/inspect/annotation.scss | 3 +- .../src/app/main/ui/inspect/attributes.scss | 3 +- .../app/main/ui/inspect/attributes/blur.scss | 6 +- .../main/ui/inspect/attributes/common.scss | 16 +- .../app/main/ui/inspect/attributes/fill.scss | 1 + .../main/ui/inspect/attributes/geometry.scss | 6 +- .../main/ui/inspect/attributes/layout.scss | 6 +- .../ui/inspect/attributes/layout_element.scss | 7 +- .../main/ui/inspect/attributes/shadow.scss | 6 +- .../main/ui/inspect/attributes/stroke.scss | 6 +- .../app/main/ui/inspect/attributes/svg.scss | 9 +- .../app/main/ui/inspect/attributes/text.scss | 10 +- .../main/ui/inspect/attributes/variant.scss | 9 +- .../ui/inspect/attributes/visibility.scss | 6 +- frontend/src/app/main/ui/inspect/code.scss | 28 +- frontend/src/app/main/ui/inspect/exports.scss | 25 +- .../app/main/ui/inspect/right_sidebar.scss | 3 + .../main/ui/inspect/styles/panels/text.scss | 3 +- .../styles/property_detail_copiable.scss | 3 + .../styles/rows/color_properties_row.scss | 3 + .../inspect/styles/rows/properties_row.scss | 2 + .../app/main/ui/inspect/styles/style_box.scss | 3 +- frontend/src/app/main/ui/modal.scss | 2 +- .../src/app/main/ui/nitrate/nitrate_form.scss | 14 +- .../src/app/main/ui/notifications/badge.scss | 3 + .../notifications/context_notification.scss | 6 +- .../app/main/ui/onboarding/newsletter.scss | 17 +- .../src/app/main/ui/onboarding/questions.scss | 20 +- .../app/main/ui/onboarding/team_choice.scss | 34 +- frontend/src/app/main/ui/releases.scss | 0 frontend/src/app/main/ui/releases/common.scss | 3 +- frontend/src/app/main/ui/releases/v2_0.scss | 10 +- frontend/src/app/main/ui/releases/v2_1.scss | 8 +- frontend/src/app/main/ui/releases/v2_10.scss | 10 +- frontend/src/app/main/ui/releases/v2_11.scss | 10 +- frontend/src/app/main/ui/releases/v2_12.scss | 10 +- frontend/src/app/main/ui/releases/v2_13.scss | 10 +- frontend/src/app/main/ui/releases/v2_14.scss | 10 +- frontend/src/app/main/ui/releases/v2_2.scss | 8 +- frontend/src/app/main/ui/releases/v2_3.scss | 10 +- frontend/src/app/main/ui/releases/v2_4.scss | 10 +- frontend/src/app/main/ui/releases/v2_5.scss | 10 +- frontend/src/app/main/ui/releases/v2_6.scss | 10 +- frontend/src/app/main/ui/releases/v2_7.scss | 10 +- frontend/src/app/main/ui/releases/v2_8.scss | 10 +- frontend/src/app/main/ui/releases/v2_9.scss | 10 +- frontend/src/app/main/ui/settings.scss | 27 +- .../app/main/ui/settings/change_email.scss | 17 +- .../app/main/ui/settings/delete_account.scss | 19 +- .../src/app/main/ui/settings/feedback.scss | 8 +- .../app/main/ui/settings/integrations.scss | 10 +- .../app/main/ui/settings/notifications.scss | 4 +- .../src/app/main/ui/settings/password.scss | 4 +- .../src/app/main/ui/settings/profile.scss | 31 +- .../src/app/main/ui/settings/sidebar.scss | 8 +- .../app/main/ui/settings/subscription.scss | 43 +- frontend/src/app/main/ui/static.scss | 20 +- frontend/src/app/main/ui/viewer.scss | 29 +- frontend/src/app/main/ui/viewer/comments.scss | 30 +- frontend/src/app/main/ui/viewer/header.scss | 85 ++- frontend/src/app/main/ui/viewer/inspect.scss | 2 +- .../src/app/main/ui/viewer/interactions.scss | 22 +- frontend/src/app/main/ui/viewer/login.scss | 12 +- .../src/app/main/ui/viewer/share_link.scss | 52 +- .../src/app/main/ui/viewer/thumbnails.scss | 20 +- frontend/src/app/main/ui/workspace.scss | 12 +- .../app/main/ui/workspace/color_palette.scss | 12 +- .../ui/workspace/color_palette_ctx_menu.scss | 20 +- .../app/main/ui/workspace/colorpicker.scss | 42 +- .../workspace/colorpicker/color_inputs.scss | 12 +- .../workspace/colorpicker/color_tokens.scss | 6 + .../ui/workspace/colorpicker/gradients.scss | 12 +- .../ui/workspace/colorpicker/harmony.scss | 5 +- .../main/ui/workspace/colorpicker/hsva.scss | 4 +- .../ui/workspace/colorpicker/libraries.scss | 6 +- .../main/ui/workspace/colorpicker/ramp.scss | 9 +- .../colorpicker/slider_selector.scss | 12 +- .../src/app/main/ui/workspace/comments.scss | 24 +- .../app/main/ui/workspace/context_menu.scss | 20 +- .../app/main/ui/workspace/coordinates.scss | 2 +- .../app/main/ui/workspace/left_header.scss | 10 + .../src/app/main/ui/workspace/libraries.scss | 45 +- .../src/app/main/ui/workspace/main_menu.scss | 7 +- frontend/src/app/main/ui/workspace/nudge.scss | 16 +- .../src/app/main/ui/workspace/palette.scss | 50 +- .../src/app/main/ui/workspace/plugins.scss | 34 +- .../src/app/main/ui/workspace/presence.scss | 4 +- .../app/main/ui/workspace/right_header.scss | 38 +- .../ui/workspace/shapes/text/v2_editor.scss | 16 +- .../ui/workspace/shapes/text/v3_editor.scss | 1 - .../src/app/main/ui/workspace/sidebar.scss | 20 +- .../app/main/ui/workspace/sidebar/assets.scss | 13 +- .../ui/workspace/sidebar/assets/colors.scss | 6 + .../ui/workspace/sidebar/assets/common.scss | 9 +- .../workspace/sidebar/assets/components.scss | 7 +- .../sidebar/assets/file_library.scss | 17 +- .../ui/workspace/sidebar/assets/groups.scss | 20 +- .../ui/workspace/sidebar/common/sidebar.scss | 10 +- .../app/main/ui/workspace/sidebar/debug.scss | 6 +- .../workspace/sidebar/debug_shape_info.scss | 2 +- .../main/ui/workspace/sidebar/history.scss | 18 +- .../main/ui/workspace/sidebar/layer_item.scss | 35 +- .../main/ui/workspace/sidebar/layer_name.scss | 2 +- .../app/main/ui/workspace/sidebar/layers.scss | 49 +- .../main/ui/workspace/sidebar/options.scss | 5 +- .../sidebar/options/drawing/frame.scss | 27 +- .../sidebar/options/menus/align.scss | 16 +- .../workspace/sidebar/options/menus/blur.scss | 22 +- .../workspace/sidebar/options/menus/bool.scss | 10 +- .../sidebar/options/menus/border_radius.scss | 2 +- .../options/menus/color_selection.scss | 11 +- .../sidebar/options/menus/component.scss | 47 +- .../sidebar/options/menus/constraints.scss | 32 +- .../sidebar/options/menus/exports.scss | 13 +- .../workspace/sidebar/options/menus/fill.scss | 10 +- .../sidebar/options/menus/frame_grid.scss | 80 ++- .../sidebar/options/menus/grid_cell.scss | 14 +- .../options/menus/input_wrapper_tokens.scss | 2 +- .../sidebar/options/menus/interactions.scss | 32 +- .../sidebar/options/menus/layer.scss | 11 +- .../options/menus/layout_container.scss | 67 +- .../sidebar/options/menus/layout_item.scss | 25 +- .../sidebar/options/menus/measures.scss | 52 +- .../sidebar/options/menus/shadow.scss | 2 +- .../sidebar/options/menus/stroke.scss | 2 +- .../sidebar/options/menus/svg_attrs.scss | 12 +- .../workspace/sidebar/options/menus/text.scss | 11 +- .../sidebar/options/menus/typography.scss | 120 +++- .../options/menus/variants_help_modal.scss | 6 +- .../sidebar/options/rows/color_row.scss | 36 +- .../sidebar/options/rows/shadow_row.scss | 3 +- .../sidebar/options/rows/stroke_row.scss | 13 +- .../main/ui/workspace/sidebar/shortcuts.scss | 10 + .../main/ui/workspace/sidebar/sitemap.scss | 47 +- .../main/ui/workspace/sidebar/versions.scss | 9 +- .../app/main/ui/workspace/text_palette.scss | 20 +- .../ui/workspace/text_palette_ctx_menu.scss | 17 +- .../app/main/ui/workspace/tokens/export.scss | 6 +- .../ui/workspace/tokens/export/modal.scss | 13 +- .../app/main/ui/workspace/tokens/import.scss | 5 +- .../ui/workspace/tokens/import/modal.scss | 4 +- .../workspace/tokens/import_from_library.scss | 14 +- .../main/ui/workspace/tokens/management.scss | 4 +- .../tokens/management/context_menu.scss | 5 + .../management/forms/controls/combobox.scss | 3 +- .../tokens/management/forms/generic_form.scss | 1 + .../tokens/management/forms/modals.scss | 7 +- .../management/forms/rename_node_modal.scss | 8 +- .../tokens/management/forms/shadow.scss | 2 + .../tokens/management/forms/typography.scss | 1 + .../tokens/management/node_context_menu.scss | 3 +- .../tokens/management/token_pill.scss | 12 +- .../tokens/management/token_tree.scss | 1 + .../ui/workspace/tokens/remapping_modal.scss | 15 +- .../app/main/ui/workspace/tokens/sets.scss | 10 +- .../workspace/tokens/sets/context_menu.scss | 2 + .../main/ui/workspace/tokens/sets/lists.scss | 10 +- .../ui/workspace/tokens/settings/menu.scss | 7 +- .../app/main/ui/workspace/tokens/sidebar.scss | 16 +- .../app/main/ui/workspace/tokens/themes.scss | 3 +- .../workspace/tokens/themes/create_modal.scss | 8 +- .../tokens/themes/theme_selector.scss | 19 +- .../app/main/ui/workspace/top_toolbar.scss | 11 +- .../src/app/main/ui/workspace/viewport.scss | 5 +- .../viewport/grid_layout_editor.scss | 19 +- .../ui/workspace/viewport/path_actions.scss | 8 +- .../ui/workspace/viewport/pixel_overlay.scss | 5 +- .../main/ui/workspace/viewport/presence.scss | 2 +- .../main/ui/workspace/viewport/top_bar.scss | 5 +- .../main/ui/workspace/viewport/widgets.scss | 7 +- .../app/main/ui/workspace/viewport_wasm.scss | 7 +- frontend/stylelint.config.mjs | 67 ++ 259 files changed, 3426 insertions(+), 1110 deletions(-) delete mode 100644 frontend/src/app/main/ui/debug/playground.scss delete mode 100644 frontend/src/app/main/ui/releases.scss create mode 100644 frontend/stylelint.config.mjs diff --git a/CHANGES.md b/CHANGES.md index 820d8f19a1..6dfcb81b1e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ - Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568) - Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713) - Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466) +- Add CSS linter [Taiga #13790](https://tree.taiga.io/project/penpot/us/13790) ### :bug: Bugs fixed @@ -48,7 +49,6 @@ - Add token name on broken token pill on sidebar [Taiga #13527](https://tree.taiga.io/project/penpot/issue/13527) - Fix tooltip activated when tab change [Taiga #13627](https://tree.taiga.io/project/penpot/issue/13627) - ## 2.14.0 ### :boom: Breaking changes & Deprecations @@ -116,6 +116,7 @@ ## 2.13.0 ### :heart: Community contributions (Thank you!) + - Add 'page' special shapeId to MCP export_shape tool for full-page snapshots [Github #8689](https://github.com/penpot/penpot/issues/8689) - Fix mask issues with component swap (by @dfelinto) [Github #7675](https://github.com/penpot/penpot/issues/7675) diff --git a/frontend/package.json b/frontend/package.json index 7bfe1fc39c..271c962168 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,7 +31,7 @@ "fmt:scss": "prettier -c resources/styles -c src/**/*.scss -w", "lint:clj": "clj-kondo --parallel --lint ../common/src src/", "lint:js": "exit 0", - "lint:scss": "exit 0", + "lint:scss": "pnpx stylelint 'src/**/*.scss'", "build:test": "clojure -M:dev:shadow-cljs compile test", "test": "pnpm run build:wasm && pnpm run build:test && node target/tests/test.js", "test:storybook": "vitest run --project=storybook", @@ -94,6 +94,7 @@ "postcss": "^8.5.4", "postcss-clean": "^1.2.2", "postcss-modules": "^6.0.1", + "postcss-scss": "^4.0.9", "prettier": "3.5.3", "pretty-time": "^1.1.0", "prop-types": "^15.8.1", @@ -111,6 +112,10 @@ "source-map-support": "^0.5.21", "storybook": "10.1.11", "style-dictionary": "5.0.0-rc.1", + "stylelint": "^17.4.0", + "stylelint-config-standard-scss": "^17.0.0", + "stylelint-scss": "^7.0.0", + "stylelint-use-logical-spec": "^5.0.1", "svg-sprite": "^2.0.4", "tdigest": "^0.1.2", "tinycolor2": "^1.6.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index d00c38a2cf..ac78b07588 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -148,6 +148,9 @@ importers: postcss-modules: specifier: ^6.0.1 version: 6.0.1(postcss@8.5.6) + postcss-scss: + specifier: ^4.0.9 + version: 4.0.9(postcss@8.5.6) prettier: specifier: 3.5.3 version: 3.5.3 @@ -199,6 +202,18 @@ importers: style-dictionary: specifier: 5.0.0-rc.1 version: 5.0.0-rc.1(tslib@2.8.1) + stylelint: + specifier: ^17.4.0 + version: 17.4.0(typescript@5.9.3) + stylelint-config-standard-scss: + specifier: ^17.0.0 + version: 17.0.0(postcss@8.5.6)(stylelint@17.4.0(typescript@5.9.3)) + stylelint-scss: + specifier: ^7.0.0 + version: 7.0.0(stylelint@17.4.0(typescript@5.9.3)) + stylelint-use-logical-spec: + specifier: ^5.0.1 + version: 5.0.1(stylelint@17.4.0(typescript@5.9.3)) svg-sprite: specifier: ^2.0.4 version: 2.0.4 @@ -543,6 +558,12 @@ packages: '@bundled-es-modules/postcss-calc-ast-parser@0.1.6': resolution: {integrity: sha512-y65TM5zF+uaxo9OeekJ3rxwTINlQvrkbZLogYvQYVoLtxm4xEiHfZ7e/MyiWbStYyWZVZkVqsaVU6F4SUK5XUA==} + '@cacheable/memory@2.0.8': + resolution: {integrity: sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==} + + '@cacheable/utils@2.4.0': + resolution: {integrity: sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ==} + '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} @@ -558,6 +579,13 @@ packages: '@csstools/css-parser-algorithms': ^4.0.0 '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-calc@3.1.1': + resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-color-parser@4.0.1': resolution: {integrity: sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==} engines: {node: '>=20.19.0'} @@ -574,10 +602,32 @@ packages: '@csstools/css-syntax-patches-for-csstree@1.0.26': resolution: {integrity: sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==} + '@csstools/css-syntax-patches-for-csstree@1.1.0': + resolution: {integrity: sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==} + '@csstools/css-tokenizer@4.0.0': resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} + '@csstools/media-query-list-parser@5.0.0': + resolution: {integrity: sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/selector-resolve-nested@4.0.0': + resolution: {integrity: sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss-selector-parser: ^7.1.1 + + '@csstools/selector-specificity@6.0.0': + resolution: {integrity: sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss-selector-parser: ^7.1.1 + '@dabh/diagnostics@2.0.8': resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} @@ -1127,6 +1177,15 @@ packages: peerDependencies: tslib: '2' + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@mdx-js/react@3.1.1': resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} peerDependencies: @@ -1527,6 +1586,10 @@ packages: '@sinclair/typebox@0.27.10': resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@so-ric/colorspace@1.1.6': resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} @@ -2094,6 +2157,10 @@ packages: ast-v8-to-istanbul@0.3.11: resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==} + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -2206,6 +2273,9 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + cacheable@2.3.3: + resolution: {integrity: sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2341,6 +2411,9 @@ packages: resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} engines: {node: '>=18'} + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + colorjs.io@0.4.5: resolution: {integrity: sha512-yCtUNCmge7llyfd/Wou19PMAcf5yC3XXhgFoAh6zsO2pGswhUPBaaUh8jzgHnXtXuZyFKzXZNAnyF5i+apICow==} @@ -2424,6 +2497,15 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cosmiconfig@9.0.1: + resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + cross-fetch@3.2.0: resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} @@ -2435,6 +2517,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-functions-list@3.3.3: + resolution: {integrity: sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==} + engines: {node: '>=12'} + css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} @@ -2715,6 +2801,10 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -2938,6 +3028,10 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -2962,6 +3056,9 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@11.1.2: + resolution: {integrity: sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2985,6 +3082,9 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} + flat-cache@6.1.20: + resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==} + flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -3076,6 +3176,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} @@ -3138,6 +3242,14 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -3146,6 +3258,13 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} + globby@16.1.1: + resolution: {integrity: sha512-dW7vl+yiAJSp6aCekaVnVJxurRv7DCOLyXqEG3RYMYUg7AuJ2jCqPkZTA8ooqC2vtnkaMcV5WfFBMuEnTu1OQg==} + engines: {node: '>=20'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3165,6 +3284,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-flag@5.0.1: + resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} + engines: {node: '>=12'} + has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -3180,6 +3303,10 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hashery@1.5.0: + resolution: {integrity: sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==} + engines: {node: '>=20'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -3198,6 +3325,9 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -3208,6 +3338,10 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-tags@5.1.0: + resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==} + engines: {node: '>=20.10'} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -3252,6 +3386,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + immutable@3.7.6: resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==} engines: {node: '>=0.8.0'} @@ -3267,6 +3405,9 @@ packages: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -3397,10 +3538,18 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -3547,6 +3696,9 @@ packages: json-parse-better-errors@1.0.2: resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3582,9 +3734,19 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + klaw-sync@6.0.0: resolution: {integrity: sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==} + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} @@ -3602,6 +3764,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + load-json-file@4.0.0: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} @@ -3634,6 +3799,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} @@ -3694,6 +3862,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mathml-tag-names@4.0.0: + resolution: {integrity: sha512-aa6AU2Pcx0VP/XWnh8IGL0SYSgQHDT6Ucror2j2mXeFAlN3ahaNs8EZtG1YiticMkSLj3Gt6VPFfZogt7G5iFQ==} + mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} @@ -3703,6 +3874,9 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -3716,6 +3890,10 @@ packages: resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} engines: {node: '>= 0.10.0'} + meow@14.1.0: + resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} + engines: {node: '>=20'} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -4001,6 +4179,10 @@ packages: resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} engines: {node: '>=4'} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} @@ -4132,6 +4314,9 @@ packages: resolution: {integrity: sha512-DpuMWW19Dd2K9KY4wknMz3khq9q2yZYa2U37bnhzdtBdBv0ggIfUj5T2XD3ir6gKVlDkb5OtOqw1iQJWq6qvpw==} engines: {node: '>=4.0.0'} + postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + postcss-modules-extract-imports@3.1.0: resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} engines: {node: ^10 || ^12 || >= 14} @@ -4161,6 +4346,21 @@ packages: peerDependencies: postcss: ^8.0.0 + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + postcss-selector-parser@7.1.1: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} @@ -4182,6 +4382,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: @@ -4255,6 +4456,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qified@0.6.0: + resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==} + engines: {node: '>=20'} + qs@6.14.1: resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} @@ -4704,6 +4909,14 @@ packages: resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} engines: {node: '>=6'} + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4787,6 +5000,10 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + string.prototype.codepointat@0.2.1: resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} @@ -4866,6 +5083,59 @@ packages: engines: {node: '>=22.0.0'} hasBin: true + stylelint-config-recommended-scss@17.0.0: + resolution: {integrity: sha512-VkVD9r7jfUT/dq3mA3/I1WXXk2U71rO5wvU2yIil9PW5o1g3UM7Xc82vHmuVJHV7Y8ok5K137fmW5u3HbhtTOA==} + engines: {node: '>=20'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^17.0.0 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-recommended@18.0.0: + resolution: {integrity: sha512-mxgT2XY6YZ3HWWe3Di8umG6aBmWmHTblTgu/f10rqFXnyWxjKWwNdjSWkgkwCtxIKnqjSJzvFmPT5yabVIRxZg==} + engines: {node: '>=20.19.0'} + peerDependencies: + stylelint: ^17.0.0 + + stylelint-config-standard-scss@17.0.0: + resolution: {integrity: sha512-uLJS6xgOCBw5EMsDW7Ukji8l28qRoMnkRch15s0qwZpskXvWt9oPzMmcYM307m9GN4MxuWLsQh4I6hU9yI53cQ==} + engines: {node: '>=20'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^17.0.0 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-standard@40.0.0: + resolution: {integrity: sha512-EznGJxOUhtWck2r6dJpbgAdPATIzvpLdK9+i5qPd4Lx70es66TkBPljSg4wN3Qnc6c4h2n+WbUrUynQ3fanjHw==} + engines: {node: '>=20.19.0'} + peerDependencies: + stylelint: ^17.0.0 + + stylelint-scss@7.0.0: + resolution: {integrity: sha512-H88kCC+6Vtzj76NsC8rv6x/LW8slBzIbyeSjsKVlS+4qaEJoDrcJR4L+8JdrR2ORdTscrBzYWiiT2jq6leYR1Q==} + engines: {node: '>=20.19.0'} + peerDependencies: + stylelint: ^16.8.2 || ^17.0.0 + + stylelint-use-logical-spec@5.0.1: + resolution: {integrity: sha512-UfLB4LW6iG4r3cXxjxkiHQrFyhWFqt8FpNNngD+TyvgMWSokk5TYwTvBHS3atUvZhOogllTOe/PUrGE+4z84AA==} + engines: {node: '>=8.0.0'} + peerDependencies: + stylelint: '>=11 < 17' + + stylelint@17.4.0: + resolution: {integrity: sha512-3kQ2/cHv3Zt8OBg+h2B8XCx9evEABQIrv4hh3uXahGz/ZEHrTR80zxBiK2NfXNaSoyBzxO1pjsz1Vhdzwn5XSw==} + engines: {node: '>=20.19.0'} + hasBin: true + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -4878,6 +5148,10 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} + supports-hyperlinks@4.4.0: + resolution: {integrity: sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==} + engines: {node: '>=20'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -4887,6 +5161,9 @@ packages: engines: {node: '>=12'} hasBin: true + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + svgo@2.8.0: resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} engines: {node: '>=10.13.0'} @@ -4908,6 +5185,10 @@ packages: resolution: {integrity: sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==} engines: {node: '>=16.0.0'} + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} @@ -5113,6 +5394,10 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -5410,6 +5695,10 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@7.0.1: + resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} + engines: {node: ^20.17.0 || >=22.9.0} + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -5724,6 +6013,18 @@ snapshots: dependencies: postcss-calc-ast-parser: 0.1.4 + '@cacheable/memory@2.0.8': + dependencies: + '@cacheable/utils': 2.4.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.4.0': + dependencies: + hashery: 1.5.0 + keyv: 5.6.0 + '@colors/colors@1.6.0': {} '@csstools/color-helpers@6.0.1': {} @@ -5733,6 +6034,11 @@ snapshots: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-color-parser@4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/color-helpers': 6.0.1 @@ -5746,8 +6052,23 @@ snapshots: '@csstools/css-syntax-patches-for-csstree@1.0.26': {} + '@csstools/css-syntax-patches-for-csstree@1.1.0': {} + '@csstools/css-tokenizer@4.0.0': {} + '@csstools/media-query-list-parser@5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/selector-resolve-nested@4.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + '@dabh/diagnostics@2.0.8': dependencies: '@so-ric/colorspace': 1.1.6 @@ -6153,6 +6474,14 @@ snapshots: '@jsonjoy.com/codegen': 17.65.0(tslib@2.8.1) tslib: 2.8.1 + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + '@mdx-js/react@3.1.1(@types/react@19.2.13)(react@19.2.3)': dependencies: '@types/mdx': 2.0.13 @@ -6458,6 +6787,8 @@ snapshots: '@sinclair/typebox@0.27.10': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@so-ric/colorspace@1.1.6': dependencies: color: 5.0.3 @@ -7189,6 +7520,8 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 + astral-regex@2.0.0: {} + async-function@1.0.0: {} async@3.2.6: {} @@ -7317,6 +7650,14 @@ snapshots: cac@6.7.14: {} + cacheable@2.3.3: + dependencies: + '@cacheable/memory': 2.0.8 + '@cacheable/utils': 2.4.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.6.0 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -7459,6 +7800,8 @@ snapshots: color-convert: 3.1.3 color-string: 2.1.4 + colord@2.9.3: {} + colorjs.io@0.4.5: {} colorjs.io@0.5.2: {} @@ -7529,6 +7872,15 @@ snapshots: core-util-is@1.0.3: {} + cosmiconfig@9.0.1(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + cross-fetch@3.2.0(encoding@0.1.13): dependencies: node-fetch: 2.7.0(encoding@0.1.13) @@ -7549,6 +7901,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-functions-list@3.3.3: {} + css-select@4.3.0: dependencies: boolbase: 1.0.0 @@ -7811,6 +8165,8 @@ snapshots: entities@7.0.1: {} + env-paths@2.2.1: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -8231,6 +8587,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fastest-levenshtein@1.0.16: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -8257,6 +8615,10 @@ snapshots: fflate@0.8.2: {} + file-entry-cache@11.1.2: + dependencies: + flat-cache: 6.1.20 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -8290,6 +8652,12 @@ snapshots: flatted: 3.3.3 keyv: 4.5.4 + flat-cache@6.1.20: + dependencies: + cacheable: 2.3.3 + flatted: 3.3.3 + hookified: 1.15.1 + flatted@3.3.3: {} fn.name@1.1.0: {} @@ -8368,6 +8736,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.5.0: {} + get-func-name@2.0.2: {} get-intrinsic@1.3.0: @@ -8452,6 +8822,16 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + globals@14.0.0: {} globalthis@1.0.4: @@ -8459,6 +8839,17 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 + globby@16.1.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + fast-glob: 3.3.3 + ignore: 7.0.5 + is-path-inside: 4.0.0 + slash: 5.1.0 + unicorn-magic: 0.4.0 + + globjoin@0.1.4: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -8469,6 +8860,8 @@ snapshots: has-flag@4.0.0: {} + has-flag@5.0.1: {} + has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.1 @@ -8483,6 +8876,10 @@ snapshots: dependencies: has-symbols: 1.1.0 + hashery@1.5.0: + dependencies: + hookified: 1.15.1 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -8497,6 +8894,8 @@ snapshots: highlight.js@11.11.1: {} + hookified@1.15.1: {} + hosted-git-info@2.8.9: {} html-encoding-sniffer@6.0.0: @@ -8507,6 +8906,8 @@ snapshots: html-escaper@2.0.2: {} + html-tags@5.1.0: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -8551,6 +8952,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + immutable@3.7.6: {} immutable@5.1.4: {} @@ -8562,6 +8965,8 @@ snapshots: import-lazy@4.0.0: {} + import-meta-resolve@4.2.0: {} + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -8682,8 +9087,12 @@ snapshots: is-number@7.0.0: {} + is-path-inside@4.0.0: {} + is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} + is-potential-custom-element-name@1.0.1: {} is-promise@4.0.0: {} @@ -8855,6 +9264,8 @@ snapshots: json-parse-better-errors@1.0.2: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -8894,10 +9305,18 @@ snapshots: dependencies: json-buffer: 3.0.1 + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + + kind-of@6.0.3: {} + klaw-sync@6.0.0: dependencies: graceful-fs: 4.2.11 + known-css-properties@0.37.0: {} + kolorist@1.8.0: {} kuler@2.0.0: {} @@ -8913,6 +9332,8 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lines-and-columns@1.2.4: {} + load-json-file@4.0.0: dependencies: graceful-fs: 4.2.11 @@ -8945,6 +9366,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash.truncate@4.4.2: {} + lodash@4.17.23: {} logform@2.7.0: @@ -9006,12 +9429,16 @@ snapshots: math-intrinsics@1.1.0: {} + mathml-tag-names@4.0.0: {} + mdn-data@2.0.14: {} mdn-data@2.0.28: {} mdn-data@2.12.2: {} + mdn-data@2.27.1: {} + media-typer@1.1.0: {} memfs@4.56.10(tslib@2.8.1): @@ -9033,6 +9460,8 @@ snapshots: memorystream@0.3.1: {} + meow@14.1.0: {} + merge-descriptors@2.0.0: {} merge-stream@2.0.0: {} @@ -9314,6 +9743,13 @@ snapshots: error-ex: 1.3.4 json-parse-better-errors: 1.0.2 + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + parse5@8.0.0: dependencies: entities: 6.0.1 @@ -9431,6 +9867,8 @@ snapshots: clean-css: 4.2.4 postcss: 6.0.23 + postcss-media-query-parser@0.2.3: {} + postcss-modules-extract-imports@3.1.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -9464,6 +9902,16 @@ snapshots: postcss-modules-values: 4.0.0(postcss@8.5.6) string-hash: 1.1.3 + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 @@ -9562,6 +10010,10 @@ snapshots: punycode@2.3.1: {} + qified@0.6.0: + dependencies: + hookified: 1.15.1 + qs@6.14.1: dependencies: side-channel: 1.1.0 @@ -10063,6 +10515,14 @@ snapshots: slash@2.0.0: {} + slash@5.1.0: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -10169,6 +10629,11 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 + string-width@8.2.0: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.1.2 + string.prototype.codepointat@0.2.1: {} string.prototype.includes@2.0.1: @@ -10283,6 +10748,93 @@ snapshots: transitivePeerDependencies: - tslib + stylelint-config-recommended-scss@17.0.0(postcss@8.5.6)(stylelint@17.4.0(typescript@5.9.3)): + dependencies: + postcss-scss: 4.0.9(postcss@8.5.6) + stylelint: 17.4.0(typescript@5.9.3) + stylelint-config-recommended: 18.0.0(stylelint@17.4.0(typescript@5.9.3)) + stylelint-scss: 7.0.0(stylelint@17.4.0(typescript@5.9.3)) + optionalDependencies: + postcss: 8.5.6 + + stylelint-config-recommended@18.0.0(stylelint@17.4.0(typescript@5.9.3)): + dependencies: + stylelint: 17.4.0(typescript@5.9.3) + + stylelint-config-standard-scss@17.0.0(postcss@8.5.6)(stylelint@17.4.0(typescript@5.9.3)): + dependencies: + stylelint: 17.4.0(typescript@5.9.3) + stylelint-config-recommended-scss: 17.0.0(postcss@8.5.6)(stylelint@17.4.0(typescript@5.9.3)) + stylelint-config-standard: 40.0.0(stylelint@17.4.0(typescript@5.9.3)) + optionalDependencies: + postcss: 8.5.6 + + stylelint-config-standard@40.0.0(stylelint@17.4.0(typescript@5.9.3)): + dependencies: + stylelint: 17.4.0(typescript@5.9.3) + stylelint-config-recommended: 18.0.0(stylelint@17.4.0(typescript@5.9.3)) + + stylelint-scss@7.0.0(stylelint@17.4.0(typescript@5.9.3)): + dependencies: + css-tree: 3.1.0 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mdn-data: 2.27.1 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + stylelint: 17.4.0(typescript@5.9.3) + + stylelint-use-logical-spec@5.0.1(stylelint@17.4.0(typescript@5.9.3)): + dependencies: + stylelint: 17.4.0(typescript@5.9.3) + + stylelint@17.4.0(typescript@5.9.3): + dependencies: + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-syntax-patches-for-csstree': 1.1.0 + '@csstools/css-tokenizer': 4.0.0 + '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) + '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) + colord: 2.9.3 + cosmiconfig: 9.0.1(typescript@5.9.3) + css-functions-list: 3.3.3 + css-tree: 3.1.0 + debug: 4.4.3(supports-color@5.5.0) + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 11.1.2 + global-modules: 2.0.0 + globby: 16.1.1 + globjoin: 0.1.4 + html-tags: 5.1.0 + ignore: 7.0.5 + import-meta-resolve: 4.2.0 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + mathml-tag-names: 4.0.0 + meow: 14.1.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-safe-parser: 7.0.1(postcss@8.5.6) + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + string-width: 8.2.0 + supports-hyperlinks: 4.4.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 7.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + supports-color@10.2.2: {} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -10295,6 +10847,11 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-hyperlinks@4.4.0: + dependencies: + has-flag: 5.0.1 + supports-color: 10.2.2 + supports-preserve-symlinks-flag@1.0.0: {} svg-sprite@2.0.4: @@ -10317,6 +10874,8 @@ snapshots: xpath: 0.0.34 yargs: 17.7.2 + svg-tags@1.0.0: {} + svgo@2.8.0: dependencies: '@trysound/sax': 0.2.0 @@ -10343,6 +10902,14 @@ snapshots: sync-message-port@1.2.0: {} + table@6.9.0: + dependencies: + ajv: 8.13.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + tar-fs@2.1.4: dependencies: chownr: 1.1.4 @@ -10543,6 +11110,8 @@ snapshots: undici-types@7.16.0: {} + unicorn-magic@0.4.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -10884,6 +11453,10 @@ snapshots: wrappy@1.0.2: {} + write-file-atomic@7.0.1: + dependencies: + signal-exit: 4.1.0 + ws@8.19.0: {} wsl-utils@0.1.0: diff --git a/frontend/resources/styles/common/refactor/basic-rules.scss b/frontend/resources/styles/common/refactor/basic-rules.scss index 07f6b82dc9..6fa5d52d8f 100644 --- a/frontend/resources/styles/common/refactor/basic-rules.scss +++ b/frontend/resources/styles/common/refactor/basic-rules.scss @@ -12,7 +12,7 @@ @use "./z-index.scss" as *; // SCROLLBAR -.new-scrollbar { +%new-scrollbar { scrollbar-width: thin; scrollbar-color: rgba(170, 181, 186, 0.3) transparent; &:hover { @@ -56,7 +56,7 @@ } // BUTTONS -.button-primary { +%button-primary { @include buttonStyle; @include flexCenter; @include headlineSmallTypography; @@ -100,7 +100,7 @@ } } -.button-secondary { +%button-secondary { @include buttonStyle; @include flexCenter; border-radius: $br-8; @@ -142,7 +142,7 @@ } } -.button-tertiary { +%button-tertiary { @include buttonStyle; @include flexCenter; --button-tertiary-border-width: #{$s-2}; @@ -191,7 +191,7 @@ } } -.button-icon-selected { +%button-icon-selected { outline: none; border-color: var(--button-icon-border-color-selected); background-color: var(--button-icon-background-color-selected); @@ -241,7 +241,7 @@ color: var(--button-warning-foreground-color-rest); } -.button-disabled { +%button-disabled { @include buttonStyle; @include flexCenter; background-color: var(--button-background-color-disabled); @@ -250,7 +250,7 @@ cursor: unset; } -.button-tag { +%button-tag { @include buttonStyle; @include flexCenter; @include focus; @@ -265,7 +265,7 @@ } } -.button-icon { +%button-icon { @include flexCenter; height: $s-16; width: $s-16; @@ -274,8 +274,8 @@ stroke-width: 1px; } -.button-icon-small { - @extend .button-icon; +%button-icon-small { + @extend %button-icon; height: $s-12; width: $s-12; stroke-width: 1.33px; @@ -296,7 +296,7 @@ } // INPUTS -.input-base { +%input-base { @include removeInputStyle; @include textEllipsis; height: $s-28; @@ -317,7 +317,7 @@ min-width: $s-12; height: $s-32; svg { - @extend .button-icon-small; + @extend %button-icon-small; } } @@ -330,7 +330,7 @@ color: var(--input-foreground-color); } -.input-element { +%input-element { display: flex; align-items: center; height: $s-32; @@ -348,13 +348,13 @@ label { @extend .input-label; svg { - @extend .button-icon-small; + @extend %button-icon-small; stroke: var(--input-foreground-color); } } input { - @extend .input-base; + @extend %input-base; } ::placeholder { @@ -405,13 +405,13 @@ } } -.input-element-label { +%input-element-label { @include bodySmallTypography; display: flex; align-items: flex-start; padding: 0; input { - @extend .input-base; + @extend %input-base; padding-left: $s-8; display: flex; align-items: flex-start; @@ -445,7 +445,7 @@ } } -.disabled-input { +%disabled-input { background-color: var(--input-background-color-disabled); border: $s-1 solid var(--input-border-color-disabled); color: var(--input-foreground-color-disabled); @@ -459,7 +459,7 @@ } } -.checkbox-icon { +%checkbox-icon { @include flexCenter; width: $s-16; height: $s-16; @@ -485,7 +485,7 @@ border-color: var(--input-checkbox-border-color-active); background-color: var(--input-checkbox-background-color-active); svg { - @extend .button-icon-small; + @extend %button-icon-small; stroke: var(--input-checkbox-foreground-color-active); } } @@ -494,7 +494,7 @@ background-color: var(--input-checkbox-background-color-intermediate); border-color: var(--input-checkbox-border-color-intermediate); svg { - @extend .button-icon-small; + @extend %button-icon-small; stroke: var(--input-checkbox-foreground-color-intermediate); } } @@ -508,7 +508,7 @@ } } -.input-checkbox { +%input-checkbox { display: flex; align-items: center; label { @@ -519,7 +519,7 @@ cursor: pointer; color: var(--input-checkbox-text-foreground-color); span { - @extend .checkbox-icon; + @extend %checkbox-icon; } input { margin: 0; @@ -539,7 +539,7 @@ } } -.input-with-label { +%input-with-label { display: flex; flex-direction: column; label { @@ -552,7 +552,7 @@ } input { - @extend .input-base; + @extend %input-base; @include bodySmallTypography; border-radius: $br-8; height: $s-32; @@ -571,7 +571,7 @@ } } &:global(.disabled) { - @extend .disabled-input; + @extend %disabled-input; } &:global(.invalid) { @@ -582,7 +582,7 @@ } //MODALS -.modal-background { +%modal-background { @include menuShadow; position: absolute; display: flex; @@ -594,7 +594,7 @@ background-color: var(--modal-background-color); } -.modal-overlay-base { +%modal-overlay-base { @include flexCenter; position: fixed; left: 0; @@ -605,7 +605,7 @@ background-color: var(--overlay-color); } -.modal-container-base { +%modal-container-base { position: relative; padding: $s-32; border-radius: $br-8; @@ -617,15 +617,15 @@ max-height: $s-512; } -.modal-close-btn-base { - @extend .button-tertiary; +%modal-close-btn-base { + @extend %button-tertiary; position: absolute; top: $s-8; right: $s-6; height: $s-32; width: $s-28; svg { - @extend .button-icon; + @extend %button-icon; } } @@ -636,14 +636,14 @@ border-bottom: $s-1 solid var(--modal-hint-border-color); } -.modal-action-btns { +%modal-action-btns { display: flex; justify-content: flex-end; gap: $s-16; } -.modal-cancel-btn { - @extend .button-secondary; +%modal-cancel-btn { + @extend %button-secondary; @include uppercaseTitleTipography; padding: $s-8 $s-24; border-radius: $br-8; @@ -651,8 +651,8 @@ margin: 0; } -.modal-accept-btn { - @extend .button-primary; +%modal-accept-btn { + @extend %button-primary; @include uppercaseTitleTipography; padding: $s-8 $s-24; border-radius: $br-8; @@ -660,8 +660,8 @@ margin: 0; } -.modal-danger-btn { - @extend .button-primary; +%modal-danger-btn { + @extend %button-primary; @include uppercaseTitleTipography; padding: $s-8 $s-24; border-radius: $br-8; @@ -676,7 +676,7 @@ // FIXME: This is used multiple times accross the app. We should design this in // the DS and create a proper component for it. -.asset-element { +%asset-element { @include bodySmallTypography; display: flex; align-items: center; @@ -691,13 +691,13 @@ } } -.shortcut-base { +%shortcut-base { @include flexCenter; gap: $s-2; color: var(--menu-shortcut-foreground-color); } -.shortcut-key-base { +%shortcut-key-base { @include bodySmallTypography; @include flexCenter; height: $s-20; @@ -706,7 +706,7 @@ background-color: var(--menu-shortcut-background-color); } -.mixed-bar { +%mixed-bar { @include bodySmallTypography; display: flex; align-items: center; @@ -718,7 +718,7 @@ color: var(--input-foreground-color-active); } -.link { +%link { background: unset; border: none; color: var(--link-foreground-color); @@ -726,7 +726,7 @@ text-decoration: none; } -.colorpicker-handler { +%colorpicker-handler { position: absolute; left: 50%; top: 50%; @@ -742,16 +742,16 @@ } } -.attr-title { +%attr-title { div { margin-left: 0; color: var(--entry-foreground-color-hover); } button { - @extend .button-tertiary; + @extend %button-tertiary; display: none; svg { - @extend .button-icon-small; + @extend %button-icon-small; stroke: var(--icon-foreground); } } @@ -762,7 +762,7 @@ } } -.attr-row { +%attr-row { display: grid; grid-template-areas: "name content"; grid-template-columns: 1fr 3fr; @@ -785,7 +785,7 @@ } } -.copy-button-children { +%copy-button-children { @include bodySmallTypography; color: var(--color-foreground-primary); text-align: left; @@ -800,7 +800,7 @@ } // SELECTS AND DROPDOWNS -.menu-dropdown { +%menu-dropdown { @include menuShadow; @include flexColumn; position: absolute; @@ -813,7 +813,7 @@ margin: 0; } -.menu-item-base { +%menu-item-base { @include bodySmallTypography; display: flex; align-items: center; @@ -828,7 +828,7 @@ } } -.dropdown-element-base { +%dropdown-element-base { @include bodySmallTypography; display: flex; align-items: center; @@ -843,7 +843,7 @@ @include flexCenter; @include textEllipsis; svg { - @extend .button-icon-small; + @extend %button-icon-small; stroke: var(--icon-foreground); } } @@ -856,7 +856,7 @@ } } -.dropdown-wrapper { +%dropdown-wrapper { @include menuShadow; position: absolute; top: $s-32; @@ -875,7 +875,7 @@ border: $s-2 solid var(--panel-border-color); } -.select-wrapper { +%select-wrapper { @include bodySmallTypography; position: relative; display: flex; diff --git a/frontend/resources/styles/common/refactor/common-dashboard.scss b/frontend/resources/styles/common/refactor/common-dashboard.scss index ed30f20a2e..708e83eb6b 100644 --- a/frontend/resources/styles/common/refactor/common-dashboard.scss +++ b/frontend/resources/styles/common/refactor/common-dashboard.scss @@ -138,14 +138,14 @@ } .btn-primary { - @extend .button-primary; + @extend %button-primary; text-transform: uppercase; font-size: $fs-14; font-weight: $fw400; } .btn-secondary { - @extend .button-secondary; + @extend %button-secondary; color: var(--color-foreground-primary); font-size: $fs-12; text-transform: uppercase; diff --git a/frontend/src/app/main/ui/alert.scss b/frontend/src/app/main/ui/alert.scss index f50ee50d41..0776a29250 100644 --- a/frontend/src/app/main/ui/alert.scss +++ b/frontend/src/app/main/ui/alert.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; &.transparent { background-color: transparent; @@ -15,7 +15,7 @@ } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; } .modal-header { @@ -24,31 +24,33 @@ .modal-title { @include deprecated.headlineMediumTypography; + color: var(--modal-title-foreground-color); } .modal-close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content { @include deprecated.bodyLargeTypography; + margin-bottom: deprecated.$s-24; } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; } .cancel-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } .accept-btn { - @extend .modal-accept-btn; + @extend %modal-accept-btn; &.danger { - @extend .modal-danger-btn; + @extend %modal-danger-btn; } } @@ -56,6 +58,7 @@ .modal-subtitle, .modal-msg { @include deprecated.bodyLargeTypography; + color: var(--modal-text-foreground-color); line-height: 1.5; } diff --git a/frontend/src/app/main/ui/auth.scss b/frontend/src/app/main/ui/auth.scss index 62ba1a1830..40f2076fec 100644 --- a/frontend/src/app/main/ui/auth.scss +++ b/frontend/src/app/main/ui/auth.scss @@ -18,7 +18,7 @@ width: 100%; overflow: auto; - @media (max-width: 992px) { + @media (width <= 992px) { display: flex; justify-content: center; } @@ -53,7 +53,7 @@ height: auto; justify-self: center; - @media (max-width: 992px) { + @media (width <= 992px) { display: none; } } diff --git a/frontend/src/app/main/ui/auth/common.scss b/frontend/src/app/main/ui/auth/common.scss index eedfa34da1..048aef58ec 100644 --- a/frontend/src/app/main/ui/auth/common.scss +++ b/frontend/src/app/main/ui/auth/common.scss @@ -11,6 +11,7 @@ padding-block-end: 0; display: grid; gap: deprecated.$s-12; + form { display: flex; flex-direction: column; @@ -33,16 +34,19 @@ .auth-title { @include deprecated.bigTitleTipography; + color: var(--title-foreground-color-hover); } .auth-subtitle { @include deprecated.smallTitleTipography; + color: var(--title-foreground-color); } .auth-tagline { @include deprecated.smallTitleTipography; + margin: 0; color: var(--title-foreground-color); } @@ -60,8 +64,9 @@ .login-button, .login-ldap-button { - @extend .button-primary; + @extend %button-primary; @include deprecated.uppercaseTitleTipography; + height: deprecated.$s-40; width: 100%; } @@ -75,8 +80,9 @@ } .go-back-link { - @extend .button-secondary; + @extend %button-secondary; @include deprecated.uppercaseTitleTipography; + height: deprecated.$s-40; } @@ -100,6 +106,7 @@ .recovery-text, .demo-account-text { @include deprecated.smallTitleTipography; + text-align: right; color: var(--title-foreground-color); } @@ -110,6 +117,7 @@ .forgot-pass-link, .demo-account-link { @include deprecated.smallTitleTipography; + text-align: left; background-color: transparent; border: none; @@ -129,14 +137,16 @@ .submit-btn, .register-btn, .recover-btn { - @extend .button-primary; + @extend %button-primary; @include deprecated.uppercaseTitleTipography; + height: deprecated.$s-40; width: 100%; } .login-btn { @include deprecated.smallTitleTipography; + display: flex; align-items: center; gap: deprecated.$s-6; @@ -144,6 +154,7 @@ border-radius: deprecated.$br-8; background-color: var(--button-secondary-background-color-rest); color: var(--button-foreground-color-focus); + span { padding-block-start: deprecated.$s-2; } diff --git a/frontend/src/app/main/ui/auth/login.scss b/frontend/src/app/main/ui/auth/login.scss index b0002114f9..4f4aa3dd9d 100644 --- a/frontend/src/app/main/ui/auth/login.scss +++ b/frontend/src/app/main/ui/auth/login.scss @@ -4,4 +4,4 @@ // // Copyright (c) KALEIDOS INC -@use "./common.scss"; +@use "./common"; diff --git a/frontend/src/app/main/ui/auth/recovery.scss b/frontend/src/app/main/ui/auth/recovery.scss index a89055b061..6da351a238 100644 --- a/frontend/src/app/main/ui/auth/recovery.scss +++ b/frontend/src/app/main/ui/auth/recovery.scss @@ -5,7 +5,7 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; -@use "./common.scss"; +@use "./common"; .submit-btn { margin-top: deprecated.$s-16; diff --git a/frontend/src/app/main/ui/auth/recovery_request.scss b/frontend/src/app/main/ui/auth/recovery_request.scss index c774a575a3..bad82e2767 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.scss +++ b/frontend/src/app/main/ui/auth/recovery_request.scss @@ -5,7 +5,7 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; -@use "./common.scss"; +@use "./common"; .fields-row { margin-bottom: deprecated.$s-8; @@ -13,6 +13,7 @@ .notification-text-email { @include deprecated.medTitleTipography; + font-size: deprecated.$fs-20; color: var(--register-confirmation-color); margin-inline: deprecated.$s-36; diff --git a/frontend/src/app/main/ui/auth/register.scss b/frontend/src/app/main/ui/auth/register.scss index 182dfddbaa..c6525ed145 100644 --- a/frontend/src/app/main/ui/auth/register.scss +++ b/frontend/src/app/main/ui/auth/register.scss @@ -5,7 +5,7 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; -@use "./common.scss"; +@use "./common"; .accept-terms-and-privacy-wrapper { :global(a) { @@ -25,6 +25,7 @@ .register-success { gap: deprecated.$s-24; + .auth-title { @include deprecated.medTitleTipography; } @@ -35,6 +36,7 @@ display: flex; justify-content: center; margin-bottom: deprecated.$s-32; + svg { width: deprecated.$s-92; height: deprecated.$s-92; @@ -43,11 +45,13 @@ .notification-text { @include deprecated.bodyMediumTypography; + color: var(--title-foreground-color); } .notification-text-email { @include deprecated.medTitleTipography; + font-size: deprecated.$fs-20; color: var(--register-confirmation-color); margin-inline: deprecated.$s-36; @@ -55,6 +59,7 @@ .logo-btn { height: deprecated.$s-40; + svg { width: deprecated.$s-120; height: deprecated.$s-40; @@ -71,6 +76,7 @@ .terms-register { @include deprecated.bodySmallTypography; + display: flex; gap: deprecated.$s-4; justify-content: center; @@ -84,6 +90,7 @@ .auth-link { color: var(--link-foreground-color); + &:hover { text-decoration: underline; } diff --git a/frontend/src/app/main/ui/comments.scss b/frontend/src/app/main/ui/comments.scss index 9da4eef616..546cfa0af4 100644 --- a/frontend/src/app/main/ui/comments.scss +++ b/frontend/src/app/main/ui/comments.scss @@ -24,6 +24,7 @@ .error-text { @include deprecated.bodySmallTypography; + color: var(--color-foreground-error); } @@ -44,6 +45,7 @@ .author { @include deprecated.bodySmallTypography; + display: flex; align-items: center; gap: deprecated.$s-8; @@ -55,11 +57,13 @@ .author-fullname { @include deprecated.textEllipsis; + color: var(--comment-title-color); } .author-timeago { @include deprecated.textEllipsis; + color: var(--comment-subtitle-color); } @@ -112,11 +116,12 @@ } .avatar-darken { - background: rgba(0, 0, 0, 0.5); + background: rgb(0 0 0 / 50%); } .cover { @include deprecated.bodySmallTypography; + cursor: pointer; display: flex; flex-direction: column; @@ -127,8 +132,8 @@ .item { @include deprecated.bodySmallTypography; + color: var(--color-foreground-primary); - word-wrap: break-word; overflow-wrap: break-word; hyphens: auto; white-space: pre-wrap; @@ -136,6 +141,7 @@ .replies { @include deprecated.bodySmallTypography; + display: flex; gap: deprecated.$s-8; } @@ -143,6 +149,7 @@ .replies-total { color: var(--color-foreground-secondary); } + .replies-unread { color: var(--color-accent-primary); } @@ -168,15 +175,18 @@ --translate-x: 0%; --translate-y: 0%; + transform: translate(var(--translate-x), var(--translate-y)); &.left { --translate-x: -100%; + flex-direction: row-reverse; } &.top { --translate-y: -100%; + align-items: flex-end; } } @@ -214,10 +224,13 @@ --translate-x: 0%; --translate-y: 0%; + transform: translate(var(--translate-x), var(--translate-y)); + &.left { --translate-x: -100%; } + &.top { --translate-y: -100%; } @@ -233,6 +246,7 @@ .floating-thread-header-left { @include deprecated.bodySmallTypography; + color: var(--color-foreground-primary); } @@ -257,22 +271,25 @@ display: flex; flex-direction: column; gap: deprecated.$s-8; + @include deprecated.bodySmallTypography; } .checkbox-wrapper { @include deprecated.flexCenter; + width: deprecated.$s-16; height: deprecated.$s-24; margin-right: deprecated.$s-8; } .checkbox { - @extend .checkbox-icon; + @extend %checkbox-icon; } .dropdown-menu { - @extend .dropdown-wrapper; + @extend %dropdown-wrapper; + position: absolute; width: fit-content; max-width: deprecated.$s-200; @@ -282,7 +299,7 @@ } .dropdown-menu-option { - @extend .dropdown-element-base; + @extend %dropdown-element-base; } .form { @@ -365,7 +382,7 @@ .comment-input { @include deprecated.bodySmallTypography; - white-space: pre-line; + background: var(--input-background-color); border-radius: deprecated.$br-8; border: deprecated.$s-1 solid var(--input-border-color); diff --git a/frontend/src/app/main/ui/components/button_link.scss b/frontend/src/app/main/ui/components/button_link.scss index bb58dcc4a5..b8b598f305 100644 --- a/frontend/src/app/main/ui/components/button_link.scss +++ b/frontend/src/app/main/ui/components/button_link.scss @@ -12,13 +12,12 @@ border: none; cursor: pointer; display: flex; - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; justify-content: center; min-width: 25px; padding: 0 1rem; transition: all 0.4s; text-decoration: none !important; - height: 40px; svg { diff --git a/frontend/src/app/main/ui/components/code_block.scss b/frontend/src/app/main/ui/components/code_block.scss index 69b4658f0f..dd8d79680e 100644 --- a/frontend/src/app/main/ui/components/code_block.scss +++ b/frontend/src/app/main/ui/components/code_block.scss @@ -9,6 +9,7 @@ .code-display { @include t.use-typography("code-font"); + user-select: text; border-radius: $br-8; margin-top: var(--sp-s); diff --git a/frontend/src/app/main/ui/components/color_bullet.scss b/frontend/src/app/main/ui/components/color_bullet.scss index 52fc242fac..6cab83cb79 100644 --- a/frontend/src/app/main/ui/components/color_bullet.scss +++ b/frontend/src/app/main/ui/components/color_bullet.scss @@ -16,9 +16,11 @@ min-height: var(--bullet-size, deprecated.$s-24); border: deprecated.$s-2 solid var(--color-bullet-border-color); border-radius: deprecated.$br-circle; + &.grid-area { grid-area: color; } + &.mini { width: var(--bullet-size, deprecated.$s-16); height: var(--bullet-size, deprecated.$s-16); @@ -31,24 +33,29 @@ &.is-not-library-color { overflow: hidden; border-radius: deprecated.$br-8; + & .color-bullet-wrapper { clip-path: none; } + &.mini { border-radius: deprecated.$br-4; } } + &.is-gradient { background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAExJREFUSIljvHnz5n8GLEBNTQ2bMMOtW7ewiuNSz4RVlIpg1IKBt4Dx////WFMRqakFl/qhH0SjFhAELNRKLaNl0Qi2YLQsGrWAcgAA0gAgQPhT2rAAAAAASUVORK5CYII=") left center; background-color: var(--color-bullet-background-color); transform: rotate(-90deg); } + &.is-transparent { background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAExJREFUSIljvHnz5n8GLEBNTQ2bMMOtW7ewiuNSz4RVlIpg1IKBt4Dx////WFMRqakFl/qhH0SjFhAELNRKLaNl0Qi2YLQsGrWAcgAA0gAgQPhT2rAAAAAASUVORK5CYII=") left center; background-color: var(--color-bullet-background-color); } + .color-bullet-wrapper { display: flex; flex-direction: row; @@ -59,11 +66,13 @@ background-repeat: no-repeat; background-position: center; } + .color-bullet-wrapper > * { width: 100%; height: 100%; background-color: var(--color-bullet-background-color); } + &:hover:not(.read-only) { border: deprecated.$s-2 solid var(--color-bullet-border-color-selected); } @@ -72,13 +81,16 @@ .color-text { @include deprecated.twoLineTextEllipsis; @include deprecated.bodySmallTypography; + width: deprecated.$s-80; text-align: center; margin-top: deprecated.$s-2; max-height: deprecated.$s-28; color: var(--palette-text-color); + &.small-text { @include deprecated.textEllipsis; + max-height: deprecated.$s-16; } } @@ -86,6 +98,7 @@ .big-text { @include deprecated.inspectValue; @include deprecated.twoLineTextEllipsis; + line-height: 1; color: var(--palette-text-color); text-align: center; diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.scss b/frontend/src/app/main/ui/components/context_menu_a11y.scss index e0fc29989e..8f38c78277 100644 --- a/frontend/src/app/main/ui/components/context_menu_a11y.scss +++ b/frontend/src/app/main/ui/components/context_menu_a11y.scss @@ -25,6 +25,7 @@ .context-menu-items { @include deprecated.menuShadow; + position: absolute; top: deprecated.$s-12; left: calc(-1 * deprecated.$s-6); @@ -50,6 +51,7 @@ .context-menu-action { @include deprecated.bodySmallTypography; + display: flex; align-items: center; justify-content: flex-start; @@ -69,7 +71,8 @@ margin-left: 0.5rem; svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--menu-foreground-color); } } @@ -84,7 +87,8 @@ cursor: pointer; .submenu-icon-back svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--menu-foreground-color); transform: rotate(180deg); } @@ -140,12 +144,14 @@ } .selected-icon { - @extend .button-tag; + @extend %button-tag; + border-radius: deprecated.$br-8; height: 100%; svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--menu-foreground-color-focus); } } @@ -154,7 +160,7 @@ .is-selected .context-menu-action { padding-left: deprecated.$s-28; - background-image: url(/images/icons/tick.svg); + background-image: url("/images/icons/tick.svg"); background-repeat: no-repeat; background-position: 5% 48%; background-size: deprecated.$s-12; diff --git a/frontend/src/app/main/ui/components/copy_button.scss b/frontend/src/app/main/ui/components/copy_button.scss index 0239900938..b8f8f67789 100644 --- a/frontend/src/app/main/ui/components/copy_button.scss +++ b/frontend/src/app/main/ui/components/copy_button.scss @@ -8,19 +8,24 @@ .copy-button { @include deprecated.buttonStyle; + width: 100%; height: deprecated.$s-32; border: deprecated.$s-1 solid transparent; border-radius: deprecated.$br-8; background-color: transparent; box-sizing: border-box; + .icon-btn { @include deprecated.flexCenter; + height: deprecated.$s-32; min-width: deprecated.$s-28; width: deprecated.$s-28; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } @@ -29,18 +34,21 @@ background-color: var(--color-background-tertiary); color: var(--color-foreground-primary); border: deprecated.$s-1 solid var(--color-background-tertiary); + .icon-btn { svg { stroke: var(--button-tertiary-foreground-color-active); } } } + &:focus, &:focus-visible { outline: none; border: deprecated.$s-1 solid var(--button-tertiary-border-color-focus); background-color: transparent; color: var(--button-tertiary-foreground-color-focus); + .icon-btn svg { stroke: var(--button-tertiary-foreground-color-active); } @@ -50,27 +58,34 @@ .copy-wrapper { @include deprecated.buttonStyle; @include deprecated.copyWrapperBase; + width: 100%; height: fit-content; text-align: left; border: deprecated.$s-1 solid transparent; + .icon-btn { @include deprecated.flexCenter; + position: absolute; top: 0; right: 0; height: deprecated.$s-32; width: deprecated.$s-28; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--button-tertiary-foreground-color-focus); display: none; } } + &:hover { background-color: var(--button-tertiary-background-color-focus); color: var(--button-tertiary-foreground-color-focus); border: deprecated.$s-1 solid var(--button-tertiary-background-color-focus); + .icon-btn svg { display: flex; } diff --git a/frontend/src/app/main/ui/components/editable_label.scss b/frontend/src/app/main/ui/components/editable_label.scss index 29a57d551d..46ff586f95 100644 --- a/frontend/src/app/main/ui/components/editable_label.scss +++ b/frontend/src/app/main/ui/components/editable_label.scss @@ -11,6 +11,7 @@ .editable-label-input { @include t.use-typography("body-small"); + outline: none; width: 100%; height: 100%; @@ -23,6 +24,7 @@ .editable-label-text { @include t.use-typography("body-small"); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/frontend/src/app/main/ui/components/editable_select.scss b/frontend/src/app/main/ui/components/editable_select.scss index ef874df8c0..07a1272095 100644 --- a/frontend/src/app/main/ui/components/editable_select.scss +++ b/frontend/src/app/main/ui/components/editable_select.scss @@ -4,18 +4,17 @@ // // Copyright (c) KALEIDOS INC -// FIXME: we need this import for .asset-element +// FIXME: we need this import for %asset-element @use "refactor/basic-rules.scss" as deprecated; - @use "ds/_borders.scss" as *; @use "ds/_sizes.scss" as *; @use "ds/_utils.scss" as *; @use "ds/spacing.scss" as *; .editable-select { - @extend .asset-element; + @extend %asset-element; + margin: 0; - padding: 0; border: $b-1 solid var(--input-border-color); position: relative; display: flex; @@ -24,27 +23,34 @@ padding: var(--sp-s); border-radius: $br-8; cursor: pointer; + .dropdown-button { display: flex; place-content: center; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + transform: rotate(90deg); stroke: var(--icon-foreground); } } .custom-select-dropdown { - @extend .dropdown-wrapper; + @extend %dropdown-wrapper; + width: fit-content; max-height: px2rem(320); // TODO: when this gets addressed in the DS, use a token .separator { margin: 0; height: $sz-12; } + .dropdown-element { - @extend .dropdown-element-base; + @extend %dropdown-element-base; + color: var(--menu-foreground-color-rest); + .label { flex-grow: 1; width: 100%; @@ -53,8 +59,10 @@ .check-icon { display: flex; place-content: center; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + visibility: hidden; stroke: var(--icon-foreground); } @@ -62,14 +70,17 @@ &.is-selected { color: var(--menu-foreground-color); + .check-icon svg { stroke: var(--menu-foreground-color); visibility: visible; } } + &:hover { background-color: var(--menu-background-color-hover); color: var(--menu-foreground-color-hover); + .check-icon svg { stroke: var(--menu-foreground-color-hover); } diff --git a/frontend/src/app/main/ui/components/forms.scss b/frontend/src/app/main/ui/components/forms.scss index 6139098e5f..6db91af254 100644 --- a/frontend/src/app/main/ui/components/forms.scss +++ b/frontend/src/app/main/ui/components/forms.scss @@ -9,30 +9,38 @@ // INPUT .input-wrapper { --input-icon-padding: var(--sp-l); + display: flex; flex-direction: column; align-items: center; position: relative; + &.valid { input { border: deprecated.$s-1 solid var(--input-border-color-success); - @extend .disabled-input; + + @extend %disabled-input; + &:hover, &:focus { border: deprecated.$s-1 solid var(--input-border-color-success); } } } + &.invalid { input { border: deprecated.$s-1 solid var(--input-border-color-error); - @extend .disabled-input; + + @extend %disabled-input; + &:hover, &:focus { border: deprecated.$s-1 solid var(--input-border-color-error); } } } + &.valid .help-icon, &.invalid .help-icon { right: deprecated.$s-40; @@ -41,6 +49,7 @@ .input-with-label-form { @include deprecated.flexColumn; + gap: deprecated.$s-8; justify-content: flex-start; align-items: flex-start; @@ -50,8 +59,10 @@ cursor: pointer; color: var(--modal-title-foreground-color); text-transform: uppercase; + input { - @extend .input-element; + @extend %input-element; + color: var(--input-foreground-color-active); margin-top: 0; width: 100%; @@ -72,9 +83,9 @@ input:-webkit-autofill:focus, input:-webkit-autofill:active { -webkit-text-fill-color: var(--input-foreground-color-active); - -webkit-box-shadow: inset 0 0 20px 20px var(--input-background-color); + box-shadow: inset 0 0 20px 20px var(--input-background-color); border: deprecated.$s-1 solid var(--input-border-color); - -webkit-background-clip: text; + background-clip: text; transition: background-color 5000s ease-in-out 0s; caret-color: var(--input-foreground-color-active); } @@ -92,8 +103,10 @@ position: absolute; right: deprecated.$s-16; top: calc(50% - deprecated.$s-8); + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--color-foreground-secondary); width: deprecated.$s-16; height: deprecated.$s-16; @@ -111,6 +124,7 @@ position: absolute; right: var(--input-icon-padding); top: calc(50% - deprecated.$s-8); + svg { width: deprecated.$s-12; height: deprecated.$s-12; @@ -129,6 +143,7 @@ position: absolute; right: deprecated.$s-16; top: calc(50% - deprecated.$s-8); + svg { width: deprecated.$s-12; height: deprecated.$s-12; @@ -145,32 +160,39 @@ .hint { @include deprecated.bodySmallTypography; + width: 99%; margin-block-start: deprecated.$s-8; color: var(--modal-text-foreground-color); } .checkbox { - @extend .input-checkbox; + @extend %input-checkbox; + .checkbox-label { @include deprecated.bodySmallTypography; + display: flex; align-items: center; flex-direction: row-reverse; gap: deprecated.$s-6; min-height: deprecated.$s-32; cursor: pointer; + span { - @extend .checkbox-icon; + @extend %checkbox-icon; } + input { display: none !important; } + &:hover { span { border-color: var(--input-checkbox-border-color-hover); } } + a { // Need for terms and conditions links on register checkbox color: var(--link-foreground-color); @@ -180,19 +202,24 @@ // SELECT .custom-select { - @extend .select-wrapper; + @extend %select-wrapper; + height: deprecated.$s-32; + .input-container { @include deprecated.flexRow; + height: deprecated.$s-32; width: 100%; border-radius: deprecated.$br-8; border: deprecated.$s-1 solid var(--input-border-color); color: var(--input-foreground-color-active); background-color: var(--input-background-color); + .main-content { @include deprecated.flexColumn; @include deprecated.bodySmallTypography; + position: relative; justify-content: center; flex-grow: 1; @@ -202,21 +229,26 @@ .label { color: var(--input-foreground-color); } + .value { width: 100%; - padding: 0px; - margin: 0px; - border: 0px; + padding: 0; + margin: 0; + border: 0; color: var(--input-foreground-color-active); } } + .icon { @include deprecated.flexCenter; + height: deprecated.$s-32; width: deprecated.$s-24; pointer-events: none; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); transform: rotate(90deg); } @@ -227,6 +259,7 @@ border: deprecated.$s-1 solid var(--input-border-color-disabled); color: var(--input-foreground-color-disabled); } + &.focus { outline: none; color: var(--input-foreground-color-active); @@ -236,8 +269,9 @@ } select { - @extend .menu-dropdown; + @extend %menu-dropdown; @include deprecated.bodySmallTypography; + box-sizing: border-box; position: absolute; top: 0; @@ -252,8 +286,10 @@ z-index: deprecated.$z-index-10; background-color: transparent; cursor: pointer; + option { @include deprecated.bodySmallTypography; + color: var(--title-foreground-color-hover); background-color: var(--menu-background-color); appearance: none; @@ -264,9 +300,11 @@ // SUBMIT-BUTTON .button-submit { - @extend .button-primary; + @extend %button-primary; + &:disabled { - @extend .button-disabled; + @extend %button-disabled; + min-height: deprecated.$s-32; } } @@ -280,35 +318,41 @@ max-height: deprecated.$s-180; width: 100%; overflow-y: hidden; + .inside-input { @include deprecated.removeInputStyle; @include deprecated.bodySmallTypography; @include deprecated.textEllipsis; + width: 100%; max-width: calc(100% - deprecated.$s-1); min-height: deprecated.$s-32; - padding-top: 0; height: deprecated.$s-32; padding: deprecated.$s-8; margin: 0; border-radius: deprecated.$br-8; color: var(--input-foreground-color-active); background-color: var(--input-background-color); + &:focus { outline: none; border: deprecated.$s-1 solid var(--input-border-color-focus); } + &.invalid { border: deprecated.$s-1 solid var(--input-border-color-error); + &:hover, &:focus { border: deprecated.$s-1 solid var(--input-border-color-error); } } } + label { display: none; } + .selected-items { display: flex; flex-wrap: wrap; @@ -320,6 +364,7 @@ .selected-item { .around { @include deprecated.flexRow; + height: deprecated.$s-24; width: fit-content; padding-left: deprecated.$s-6; @@ -327,8 +372,10 @@ background-color: var(--pill-background-color); border: deprecated.$s-1 solid var(--pill-background-color); box-sizing: border-box; + .text { @include deprecated.bodySmallTypography; + padding-right: deprecated.$s-8; color: var(--pill-foreground-color); } @@ -336,18 +383,24 @@ .icon { @include deprecated.flexCenter; @include deprecated.buttonStyle; + height: deprecated.$s-32; width: deprecated.$s-24; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } + &.invalid { background-color: var(--status-widget-background-color-error); + .text { color: var(--alert-text-foreground-color-error); } + .icon svg { stroke: var(--alert-text-foreground-color-error); } @@ -367,6 +420,7 @@ .radio-label { @include deprecated.bodySmallTypography; @include deprecated.flexRow; + align-items: flex-start; gap: deprecated.$s-8; min-height: deprecated.$s-32; @@ -375,6 +429,7 @@ padding: deprecated.$s-8; color: var(--input-foreground-color-rest); border: deprecated.$s-1 solid transparent; + &:focus, &:focus-within { outline: none; @@ -395,28 +450,33 @@ } .radio-icon { - @extend .checkbox-icon; + @extend %checkbox-icon; + border-radius: deprecated.$br-circle; } .radio-label-image { @include deprecated.smallTitleTipography; + display: grid; - grid-template-rows: auto auto 0px; + grid-template-rows: auto auto 0; justify-items: center; gap: 0; border-radius: deprecated.$br-8; margin: 0; border: 1px solid var(--color-background-tertiary); cursor: pointer; + &:global(.checked) { border: 1px solid var(--color-accent-primary); } + &:focus, &:focus-within { outline: none; border: deprecated.$s-1 solid var(--input-border-color-active); } + .image-text { color: var(--input-foreground-color-rest); display: grid; @@ -436,7 +496,9 @@ .icon-inside { margin: deprecated.$s-16; + @include deprecated.flexCenter; + svg { width: 40px; height: 60px; @@ -445,10 +507,11 @@ } } -//TEXTAREA +// TEXTAREA .textarea-label { @include deprecated.uppercaseTitleTipography; + color: var(--modal-title-foreground-color); text-transform: uppercase; margin-bottom: deprecated.$s-8; diff --git a/frontend/src/app/main/ui/components/progress.scss b/frontend/src/app/main/ui/components/progress.scss index 0ef02d0f17..dcdf538c8a 100644 --- a/frontend/src/app/main/ui/components/progress.scss +++ b/frontend/src/app/main/ui/components/progress.scss @@ -9,6 +9,7 @@ // PROGRESS WIDGET .progress-widget { @include deprecated.flexCenter; + width: deprecated.$s-28; height: deprecated.$s-28; } @@ -19,6 +20,7 @@ --export-modal-fg-color: var(--alert-text-foreground-color-default); --export-modal-icon-color: var(--alert-icon-foreground-color-default); --export-modal-border-color: var(--alert-border-color-default); + position: absolute; right: deprecated.$s-16; top: deprecated.$s-48; @@ -41,13 +43,15 @@ --export-modal-fg-color: var(--alert-text-foreground-color-error); --export-modal-icon-color: var(--alert-icon-foreground-color-error); --export-modal-border-color: var(--alert-border-color-error); + grid-template-areas: "icon text close"; gap: deprecated.$s-8; padding-block: deprecated.$s-8; } .icon { - @extend .button-icon; + @extend %button-icon; + grid-area: icon; align-self: center; margin-inline-start: deprecated.$s-8; @@ -56,6 +60,7 @@ .title { @include deprecated.bodyMediumTypography; + display: grid; grid-template-columns: auto 1fr; gap: deprecated.$s-8; @@ -68,6 +73,7 @@ .progress { @include deprecated.bodyMediumTypography; + padding-left: deprecated.$s-8; margin: 0; align-self: center; @@ -77,6 +83,7 @@ .retry-btn { @include deprecated.buttonStyle; @include deprecated.bodySmallTypography; + display: inline; text-align: left; color: var(--modal-link-foreground-color); @@ -86,12 +93,14 @@ .progress-close-button { @include deprecated.buttonStyle; + padding: 0; margin-inline-end: deprecated.$s-8; } .close-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--export-modal-icon-color); } diff --git a/frontend/src/app/main/ui/components/radio_buttons.scss b/frontend/src/app/main/ui/components/radio_buttons.scss index 6ef73339ad..51ca7a4c81 100644 --- a/frontend/src/app/main/ui/components/radio_buttons.scss +++ b/frontend/src/app/main/ui/components/radio_buttons.scss @@ -8,6 +8,7 @@ .radio-btn-wrapper { @include deprecated.flexCenter; + border-radius: deprecated.$br-8; height: deprecated.$s-32; background-color: var(--input-background-color); @@ -20,6 +21,7 @@ @include deprecated.buttonStyle; @include deprecated.flexCenter; @include deprecated.focusRadio; + height: deprecated.$s-32; flex-grow: 1; border-radius: deprecated.$s-8; @@ -29,14 +31,19 @@ input { display: none; } + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--radio-btn-foreground-color); } + .title-name { @include deprecated.uppercaseTitleTipography; + color: var(--radio-btn-foreground-color); } + &:hover { svg { stroke: var(--radio-btn-foreground-color-selected); @@ -48,9 +55,11 @@ --radio-icon-border-color: var(--radio-btn-border-color-selected); background-color: var(--radio-btn-background-color-selected); + svg { stroke: var(--radio-btn-foreground-color-selected); } + .title-name { color: var(--radio-btn-foreground-color-selected); } @@ -60,18 +69,23 @@ cursor: default; background-color: transparent; border: deprecated.$s-2 solid transparent; + svg { stroke: var(--button-foreground-color-disabled); } + .title-name { color: var(--button-foreground-color-disabled); } + &:hover { background-color: transparent; border: deprecated.$s-2 solid transparent; + svg { stroke: var(--button-foreground-color-disabled); } + .title-name { color: var(--button-foreground-color-disabled); } diff --git a/frontend/src/app/main/ui/components/reorder_handler.scss b/frontend/src/app/main/ui/components/reorder_handler.scss index 499ff56ad5..8991efb661 100644 --- a/frontend/src/app/main/ui/components/reorder_handler.scss +++ b/frontend/src/app/main/ui/components/reorder_handler.scss @@ -20,6 +20,7 @@ block-size: var(--sp-l); pointer-events: none; visibility: var(--reorder-icon-visibility, hidden); + --icon-stroke-color: var(--color-foreground-secondary); } diff --git a/frontend/src/app/main/ui/components/search_bar.scss b/frontend/src/app/main/ui/components/search_bar.scss index 96855005a1..4534d76345 100644 --- a/frontend/src/app/main/ui/components/search_bar.scss +++ b/frontend/src/app/main/ui/components/search_bar.scss @@ -23,6 +23,7 @@ .search-input-wrapper { @include deprecated.flexCenter; + height: deprecated.$s-32; width: 100%; border: deprecated.$s-1 solid var(--search-bar-input-border-color); @@ -32,6 +33,7 @@ &:hover { border: deprecated.$s-1 solid var(--input-border-color-hover); background-color: var(--input-background-color-hover); + .search-input { background-color: var(--input-background-color-hover); } @@ -41,6 +43,7 @@ background-color: var(--input-background-color-active); color: var(--input-foreground-color-active); border: deprecated.$s-1 solid var(--input-border-color-focus); + .search-input { background-color: var(--input-background-color-active); } @@ -56,13 +59,15 @@ font-size: deprecated.$fs-12; color: var(--input-foreground-color); border-radius: deprecated.$br-8; + &:focus { outline: none; } } .clear-icon { - @extend .button-tag; + @extend %button-tag; + flex: 0 0 deprecated.$s-32; height: 100%; color: var(--color-icon-default); diff --git a/frontend/src/app/main/ui/components/select.scss b/frontend/src/app/main/ui/components/select.scss index ba01e42e08..72eae37864 100644 --- a/frontend/src/app/main/ui/components/select.scss +++ b/frontend/src/app/main/ui/components/select.scss @@ -11,8 +11,10 @@ --bg-color: var(--menu-background-color); --icon-color: var(--icon-foreground); --text-color: var(--menu-foreground-color); - @extend .new-scrollbar; + + @extend %new-scrollbar; @include deprecated.bodySmallTypography; + position: relative; display: grid; grid-template-columns: 1fr auto; @@ -48,15 +50,19 @@ --border-color: var(--menu-border-color-disabled); --icon-color: var(--menu-foreground-color-disabled); --text-color: var(--menu-foreground-color-disabled); + pointer-events: none; cursor: default; } .dropdown-button { @include deprecated.flexCenter; + margin-inline-end: var(--sp-xxs); + svg { - @extend .button-icon-small; + @extend %button-icon-small; + transform: rotate(90deg); stroke: var(--icon-color); } @@ -64,16 +70,20 @@ .current-icon { @include deprecated.flexCenter; + width: deprecated.$s-24; padding-right: deprecated.$s-4; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } .custom-select-dropdown { - @extend .dropdown-wrapper; + @extend %dropdown-wrapper; + .separator { margin: 0; height: deprecated.$s-12; @@ -87,14 +97,18 @@ } .checked-element { - @extend .dropdown-element-base; + @extend %dropdown-element-base; + .icon { @include deprecated.flexCenter; + height: deprecated.$s-24; width: deprecated.$s-24; padding-right: deprecated.$s-4; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } } @@ -106,8 +120,10 @@ .check-icon { @include deprecated.flexCenter; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + visibility: hidden; stroke: var(--icon-foreground); } @@ -115,11 +131,13 @@ &.is-selected { color: var(--menu-foreground-color); + .check-icon svg { stroke: var(--menu-foreground-color); visibility: visible; } } + &.disabled { display: none; } diff --git a/frontend/src/app/main/ui/components/tab_container.scss b/frontend/src/app/main/ui/components/tab_container.scss index aab1d5ffdd..71cc4371ad 100644 --- a/frontend/src/app/main/ui/components/tab_container.scss +++ b/frontend/src/app/main/ui/components/tab_container.scss @@ -32,6 +32,7 @@ .tab-container-tab-title { @include deprecated.flexCenter; + height: 100%; width: 100%; padding: 0 deprecated.$s-8; @@ -43,12 +44,14 @@ min-width: 0; svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--tab-foreground-color); } .content { @include deprecated.headlineSmallTypography; + text-align: center; white-space: nowrap; overflow: hidden; @@ -78,6 +81,7 @@ .collapse-sidebar { @include deprecated.flexCenter; @include deprecated.buttonStyle; + height: 100%; width: deprecated.$s-24; min-width: deprecated.$s-24; @@ -86,6 +90,7 @@ svg { @include deprecated.flexCenter; + height: deprecated.$s-16; width: deprecated.$s-16; stroke: var(--icon-foreground); @@ -109,13 +114,12 @@ } .tab-container-content { - overflow-y: auto; - overflow-x: hidden; + overflow: hidden auto; display: flex; flex-direction: column; } -//Firefox doesn't respect scrollbar-gutter +// Firefox doesn't respect scrollbar-gutter @supports (-moz-appearance: none) { .tab-container-content { padding-right: deprecated.$s-8; diff --git a/frontend/src/app/main/ui/components/title_bar.scss b/frontend/src/app/main/ui/components/title_bar.scss index f985736946..1ba37cde67 100644 --- a/frontend/src/app/main/ui/components/title_bar.scss +++ b/frontend/src/app/main/ui/components/title_bar.scss @@ -14,6 +14,7 @@ height: deprecated.$s-32; width: 100%; min-height: deprecated.$s-32; + --arrow-icon-color: var(--icon-foreground); --title-color: var(--title-foreground-color); } @@ -32,12 +33,15 @@ .title { @include t.use-typography("headline-small"); + color: var(--title-color); } .title-only { @include t.use-typography("headline-small"); + --title-bar-title-margin: #{deprecated.$s-8}; + color: var(--title-color); margin-inline-start: var(--title-bar-title-margin); } @@ -64,6 +68,7 @@ .icon-text-btn { @include deprecated.buttonStyle; + display: flex; align-items: center; flex-grow: 1; diff --git a/frontend/src/app/main/ui/confirm.scss b/frontend/src/app/main/ui/confirm.scss index 09b23426f3..f5261af38d 100644 --- a/frontend/src/app/main/ui/confirm.scss +++ b/frontend/src/app/main/ui/confirm.scss @@ -7,14 +7,16 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; + &.transparent { background-color: transparent; } } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; + display: flex; flex-direction: column; gap: var(--sp-xxl); @@ -22,6 +24,7 @@ .modal-title { @include deprecated.headlineMediumTypography; + color: var(--modal-title-foreground-color); } @@ -41,21 +44,24 @@ .modal-component-icon { @include deprecated.flexCenter; + color: var(--color-foreground-secondary); } .modal-component-name { @include deprecated.bodyLargeTypography; + color: var(--color-foreground-secondary); } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; } .modal-scd-msg, .modal-subtitle, .modal-msg { @include deprecated.bodyLargeTypography; + color: var(--modal-text-foreground-color); } diff --git a/frontend/src/app/main/ui/dashboard.scss b/frontend/src/app/main/ui/dashboard.scss index 994f56a723..045d817316 100644 --- a/frontend/src/app/main/ui/dashboard.scss +++ b/frontend/src/app/main/ui/dashboard.scss @@ -7,7 +7,8 @@ @use "refactor/common-refactor.scss" as deprecated; .dashboard { - @extend .new-scrollbar; + @extend %new-scrollbar; + background-color: var(--app-background); display: grid; grid-template-columns: deprecated.$s-40 deprecated.$s-256 1fr; diff --git a/frontend/src/app/main/ui/dashboard/change_owner.scss b/frontend/src/app/main/ui/dashboard/change_owner.scss index 40e8387274..fadf30e3f6 100644 --- a/frontend/src/app/main/ui/dashboard/change_owner.scss +++ b/frontend/src/app/main/ui/dashboard/change_owner.scss @@ -7,11 +7,11 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; } .modal-header { @@ -20,35 +20,38 @@ .modal-title { @include deprecated.uppercaseTitleTipography; + color: var(--modal-title-foreground-color); } .modal-close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content { @include deprecated.bodySmallTypography; + margin-bottom: deprecated.$s-24; } .input-wrapper { - @extend .input-with-label; + @extend %input-with-label; @include deprecated.bodySmallTypography; } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; } .cancel-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } .accept-btn { - @extend .modal-accept-btn; + @extend %modal-accept-btn; + &.danger { - @extend .modal-danger-btn; + @extend %modal-danger-btn; } } diff --git a/frontend/src/app/main/ui/dashboard/comments.scss b/frontend/src/app/main/ui/dashboard/comments.scss index c81fd1ee71..9c1fc9b244 100644 --- a/frontend/src/app/main/ui/dashboard/comments.scss +++ b/frontend/src/app/main/ui/dashboard/comments.scss @@ -8,6 +8,7 @@ .dashboard-comments-section { @include deprecated.flexCenter; + position: relative; border-radius: deprecated.$br-8; } @@ -35,7 +36,8 @@ } .comments-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); height: deprecated.$s-24; width: deprecated.$s-24; @@ -44,6 +46,7 @@ .comment-button { position: relative; + .unread { position: absolute; width: deprecated.$s-8; @@ -57,12 +60,14 @@ } .comments-icon-small { - @extend .button-icon; + @extend %button-icon; + stroke: var(--comment-icon-small-foreground-color); } .dropdown { @include deprecated.menuShadow; + background-color: var(--color-background-tertiary); border-radius: deprecated.$br-8; border: deprecated.$s-1 solid transparent; @@ -101,6 +106,7 @@ &:hover { cursor: pointer; } + &.mark-all-as-read-button { border-radius: deprecated.$s-8; border: deprecated.$s-1 solid; diff --git a/frontend/src/app/main/ui/dashboard/deleted.scss b/frontend/src/app/main/ui/dashboard/deleted.scss index 3d8d5bf8c7..9a1ee20ac7 100644 --- a/frontend/src/app/main/ui/dashboard/deleted.scss +++ b/frontend/src/app/main/ui/dashboard/deleted.scss @@ -29,9 +29,10 @@ .deleted-info { display: block; - height: fit-content; color: var(--color-foreground-secondary); + @include t.use-typography("body-large"); + line-height: 0.8; height: var(--sp-xl); } @@ -63,7 +64,6 @@ .nav-option { color: var(--color-foreground-secondary); padding: 0.5rem; - display: flex; align-items: center; justify-content: center; @@ -100,6 +100,7 @@ .project-name { @include t.use-typography("body-large"); + width: fit-content; margin-inline-end: var(--sp-m); line-height: 0.8; @@ -115,7 +116,8 @@ .add-file-btn, .options-btn { - @extend .button-tertiary; + @extend %button-tertiary; + height: var(--sp-xxxl); width: var(--sp-xxxl); margin: 0 var(--sp-s); @@ -130,6 +132,7 @@ .add-icon, .menu-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } diff --git a/frontend/src/app/main/ui/dashboard/files.scss b/frontend/src/app/main/ui/dashboard/files.scss index 79f3563168..0a5fe94dbd 100644 --- a/frontend/src/app/main/ui/dashboard/files.scss +++ b/frontend/src/app/main/ui/dashboard/files.scss @@ -17,6 +17,7 @@ &.dashboard-projects { user-select: none; } + &.dashboard-shared { width: calc(100vw - deprecated.$s-320); margin-right: deprecated.$s-52; @@ -32,7 +33,8 @@ } .menu-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } diff --git a/frontend/src/app/main/ui/dashboard/fonts.scss b/frontend/src/app/main/ui/dashboard/fonts.scss index f3d195b35b..f277fbe517 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.scss +++ b/frontend/src/app/main/ui/dashboard/fonts.scss @@ -5,7 +5,6 @@ // Copyright (c) KALEIDOS INC @use "common/refactor/common-dashboard"; - @use "ds/_utils.scss" as *; @use "ds/_sizes.scss" as *; @use "ds/_borders.scss" as *; @@ -37,6 +36,7 @@ h3 { @include t.use-typography("title-small"); + color: var(--color-foreground-secondary); margin: var(--sp-xs); } @@ -48,6 +48,7 @@ .installed-fonts-header { @include t.use-typography("headline-small"); + align-items: center; color: var(--color-foreground-secondary); display: flex; @@ -55,7 +56,8 @@ padding-left: var(--sp-xxl); > .family { - @include twoLineTextEllipsis; + @include two-line-text-ellipsis; + min-width: $sz-200; width: $sz-200; } @@ -72,8 +74,8 @@ input { @include t.use-typography("body-medium"); + background-color: var(--color-background-tertiary); - border-color: transparent; border-radius: $br-8; border: $b-1 solid transparent; color: var(--color-foreground-primary); @@ -85,6 +87,7 @@ &:focus { outline: $b-1 solid var(--color-accent-primary); } + &::placeholder { color: var(--color-foreground-secondary); } @@ -93,6 +96,7 @@ .font-item { @include t.use-typography("body-medium"); + align-items: center; background-color: var(--color-background-tertiary); border-radius: $br-4; @@ -106,11 +110,11 @@ input { @include t.use-typography("body-medium"); - @include textEllipsis; + @include text-ellipsis; + border: $b-1 solid transparent; margin: 0; padding: var(--sp-s); - background-color: var(--color-background-tertiary); border-radius: $br-8; color: var(--color-foreground-primary); @@ -123,21 +127,25 @@ } > .family { - @include twoLineTextEllipsis; + @include two-line-text-ellipsis; + min-width: $sz-200; width: $sz-200; + &.is-edition { overflow: visible; } } > .filenames { - @include textEllipsis; + @include text-ellipsis; + min-width: $sz-200; } > .variants { @include t.use-typography("body-medium"); + display: flex; flex-wrap: wrap; flex-grow: 1; @@ -151,12 +159,14 @@ padding: var(--sp-s) var(--sp-m); cursor: pointer; gap: var(--sp-xs); + .icon { display: flex; align-items: center; justify-content: center; height: $sz-16; width: $sz-16; + svg { fill: none; width: $sz-12; @@ -171,6 +181,7 @@ } } } + .inhert-variant { cursor: default; } @@ -178,6 +189,7 @@ .table-field { color: var(--color-foreground-primary); + .variant { background-color: var(--color-background-quaternary); border-radius: $br-8; @@ -186,7 +198,8 @@ .filenames { @include t.use-typography("body-small"); - @include textEllipsis; + @include text-ellipsis; + min-width: $sz-400; padding-left: var(--sp-xxxl); } @@ -203,6 +216,7 @@ margin-left: var(--sp-m); justify-content: center; align-items: center; + svg { width: $sz-16; height: $sz-16; @@ -212,6 +226,7 @@ &.failure { margin-right: var(--sp-m); + svg { stroke: var(--element-foreground-warning); } @@ -220,6 +235,7 @@ &.close { background: none; border: none; + svg { stroke: var(--color-foreground-secondary); } @@ -245,6 +261,7 @@ .dashboard-fonts-hero { @include t.use-typography("body-medium"); + padding: var(--sp-xxxl) 0; margin-top: px2rem(80); display: flex; @@ -269,6 +286,7 @@ p { @include t.use-typography("body-large"); + color: var(--color-foreground-secondary); } } @@ -299,6 +317,7 @@ .label { @include t.use-typography("body-medium"); + color: var(--color-foreground-secondary); } } diff --git a/frontend/src/app/main/ui/dashboard/grid.scss b/frontend/src/app/main/ui/dashboard/grid.scss index 3f4189c729..f0bf82dac2 100644 --- a/frontend/src/app/main/ui/dashboard/grid.scss +++ b/frontend/src/app/main/ui/dashboard/grid.scss @@ -8,15 +8,13 @@ // TODO: Legacy sass variables. We should remove them in favor of DS tokens. $bp-max-1366: "(max-width: 1366px)"; - $thumbnail-default-width: deprecated.$s-252; // Default width $thumbnail-default-height: deprecated.$s-168; // Default width .dashboard-grid { font-size: deprecated.$fs-14; height: 100%; - overflow-y: auto; - overflow-x: hidden; + overflow: hidden auto; padding: 0 deprecated.$s-16; } @@ -42,6 +40,7 @@ $thumbnail-default-height: deprecated.$s-168; // Default width width: 100%; font-weight: deprecated.$fw400; } + button { background-color: transparent; border: none; @@ -108,7 +107,6 @@ $thumbnail-default-height: deprecated.$s-168; // Default width line-height: 1.92; max-width: deprecated.$s-260; overflow: hidden; - padding-right: deprecated.$s-8; padding: 0; text-overflow: ellipsis; white-space: nowrap; @@ -126,9 +124,11 @@ $thumbnail-default-height: deprecated.$s-168; // Default width width: 100%; white-space: nowrap; max-width: deprecated.$s-260; + &::first-letter { text-transform: capitalize; } + @media #{$bp-max-1366} { max-width: deprecated.$s-232; } @@ -198,9 +198,11 @@ $thumbnail-default-height: deprecated.$s-168; // Default width &:focus, &:focus-within { background-color: var(--color-background-tertiary); + .project-th-actions { opacity: 1; } + a { text-decoration: none; } @@ -243,6 +245,7 @@ $thumbnail-default-height: deprecated.$s-168; // Default width margin-right: 0; margin-top: deprecated.$s-20; width: 100%; + --menu-icon-color: var(--button-tertiary-foreground-color-rest); &:hover, diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss index cf7cea13ae..133d0c8dbe 100644 --- a/frontend/src/app/main/ui/dashboard/import.scss +++ b/frontend/src/app/main/ui/dashboard/import.scss @@ -7,11 +7,12 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; + display: flex; flex-direction: column; } @@ -22,18 +23,19 @@ .modal-title { @include deprecated.uppercaseTitleTipography; + color: var(--modal-title-foreground-color); } .modal-close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content { @include deprecated.bodySmallTypography; + flex: 1; - overflow-y: auto; - overflow-x: hidden; + overflow: hidden auto; display: grid; grid-template-columns: 1fr; gap: deprecated.$s-16; @@ -42,16 +44,18 @@ } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; } .cancel-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } + .accept-btn { - @extend .modal-accept-btn; + @extend %modal-accept-btn; + &.danger { - @extend .modal-danger-btn; + @extend %modal-danger-btn; } } @@ -59,60 +63,81 @@ .modal-subtitle, .modal-msg { @include deprecated.bodySmallTypography; + color: var(--modal-text-foreground-color); line-height: 1.5; } .file-entry { display: flex; + .file-name { @include deprecated.flexRow; + .file-icon { @include deprecated.flexCenter; + height: deprecated.$s-24; width: deprecated.$s-16; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } + &.icon-fill svg { fill: var(--icon-foreground); } } + .file-name-edit { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + flex-grow: 1; } + .file-name-label { @include deprecated.bodySmallTypography; + display: flex; align-items: center; gap: deprecated.$s-12; flex-grow: 1; + .icon { @include deprecated.flexCenter; + height: deprecated.$s-16; width: deprecated.$s-16; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } } + .edit-entry-buttons { @include deprecated.flexRow; + button { - @extend .button-tertiary; + @extend %button-tertiary; + width: deprecated.$s-28; height: deprecated.$s-32; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } } } } + .error-message, .progress-message { display: flex; @@ -126,14 +151,19 @@ align-items: center; gap: deprecated.$s-12; color: var(--modal-text-foreground-color); + .linked-library-tag { @include deprecated.flexCenter; + height: deprecated.$s-24; width: deprecated.$s-24; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } + &.error { svg { stroke: var(--element-foreground-error); @@ -147,45 +177,57 @@ color: var(--modal-text-foreground-color); } } + &.warning { .file-name { color: var(--element-foreground-warning); + .file-icon svg { stroke: var(--element-foreground-warning); } + .file-icon.icon-fill svg { fill: var(--element-foreground-warning); } } } + &.success { .file-name { color: var(--modal-text-foreground-color); + .file-icon svg { stroke: var(--modal-text-foreground-color); } + .file-icon.icon-fill svg { fill: var(--modal-text-foreground-color); } } } + &.error { .file-name { color: var(--modal-text-foreground-color); + .file-icon svg { stroke: var(--modal-text-foreground-color); } + .file-icon.icon-fill svg { fill: var(--modal-text-foreground-color); } } } + &.editable { .file-name { color: var(--modal-text-foreground-color); + .file-icon svg { stroke: var(--modal-text-foreground-color); } + .file-icon.icon-fill svg { fill: var(--modal-text-foreground-color); } diff --git a/frontend/src/app/main/ui/dashboard/inline_edition.scss b/frontend/src/app/main/ui/dashboard/inline_edition.scss index 4f62033011..d07e701fee 100644 --- a/frontend/src/app/main/ui/dashboard/inline_edition.scss +++ b/frontend/src/app/main/ui/dashboard/inline_edition.scss @@ -34,7 +34,6 @@ input.element-title { .close { cursor: pointer; position: absolute; - top: deprecated.$s-1; right: calc(-1 * deprecated.$s-8); @@ -45,6 +44,7 @@ input.element-title { width: deprecated.$s-16; margin: 0; } + &:hover { svg { fill: var(--element-foreground-warning); diff --git a/frontend/src/app/main/ui/dashboard/placeholder.scss b/frontend/src/app/main/ui/dashboard/placeholder.scss index ca47399436..6254535b58 100644 --- a/frontend/src/app/main/ui/dashboard/placeholder.scss +++ b/frontend/src/app/main/ui/dashboard/placeholder.scss @@ -14,7 +14,7 @@ padding: deprecated.$s-12 0; &.libs { - background-image: url(/images/ph-left.svg), url(/images/ph-right.svg); + background-image: url("/images/ph-left.svg"), url("/images/ph-right.svg"); background-position: 15% bottom, 85% top; @@ -47,7 +47,6 @@ border-radius: deprecated.$br-8; color: var(--color-foreground-primary); cursor: pointer; - height: deprecated.$s-160; margin: deprecated.$s-8; border: deprecated.$s-2 solid transparent; width: var(--th-width, #{g.$thumbnail-default-width}); @@ -113,9 +112,11 @@ .empty-project-card { @include t.use-typography("body-small"); + --color-card-background: var(--color-background-tertiary); --color-card-title: var(--color-foreground-primary); --color-card-subtitle: var(--color-foreground-secondary); + display: flex; flex-direction: column; justify-content: center; @@ -129,6 +130,7 @@ --color-card-background: var(--color-accent-primary); --color-card-title: var(--color-background-secondary); --color-card-subtitle: var(--color-background-secondary); + cursor: pointer; .empty-project-card-title { diff --git a/frontend/src/app/main/ui/dashboard/projects.scss b/frontend/src/app/main/ui/dashboard/projects.scss index 7df6a0f9c9..d910ffb0d2 100644 --- a/frontend/src/app/main/ui/dashboard/projects.scss +++ b/frontend/src/app/main/ui/dashboard/projects.scss @@ -42,6 +42,7 @@ .dashboard-project-row { --actions-opacity: 0; + margin-block-end: var(--sp-xxl); position: relative; @@ -84,8 +85,9 @@ } .project-name { - @include textEllipsis; + @include text-ellipsis; @include t.use-typography("body-large"); + color: var(--title-foreground-color-hover); cursor: pointer; block-size: $sz-16; @@ -102,8 +104,10 @@ .info, .recent-files-row-title-info { @include t.use-typography("body-medium"); + color: var(--title-foreground-color); - @media (max-width: 760px) { + + @media (width <= 760px) { display: none; } } @@ -116,7 +120,8 @@ .add-file-btn, .options-btn { - @extend .button-tertiary; + @extend %button-tertiary; + block-size: $sz-32; inline-size: $sz-32; margin: 0 var(--sp-s); @@ -125,7 +130,8 @@ .add-icon, .menu-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } @@ -140,7 +146,9 @@ .show-more { --show-more-color: var(--button-secondary-foreground-color-rest); + @include t.use-typography("body-medium"); + border: none; background: none; cursor: pointer; @@ -179,7 +187,7 @@ border-radius: $br-4; inline-size: auto; - @media (max-width: 1200px) { + @media (width <= 1200px) { display: none; inline-size: 0; } @@ -202,18 +210,22 @@ .info { flex: 1; font-size: $sz-16; + span { color: var(--color-foreground-secondary); display: block; } + a { color: var(--color-accent-primary); } + padding: var(--sp-s) 0; } .close { --close-icon-foreground-color: var(--icon-foreground); + position: absolute; top: var(--sp-xl); inset-inline-end: var(--sp-xxl); @@ -221,13 +233,15 @@ background-color: transparent; border: none; cursor: pointer; + &:hover { --close-icon-foreground-color: var(--button-icon-foreground-color-selected); } } .close-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--close-icon-foreground-color); } @@ -244,7 +258,8 @@ block-size: var(--sp-xl) 0; overflow: hidden; border-radius: $br-4; - @media (max-width: 1200px) { + + @media (width <= 1200px) { display: none; inline-size: 0; } diff --git a/frontend/src/app/main/ui/dashboard/search.scss b/frontend/src/app/main/ui/dashboard/search.scss index c360ac61ac..f514fb4ba0 100644 --- a/frontend/src/app/main/ui/dashboard/search.scss +++ b/frontend/src/app/main/ui/dashboard/search.scss @@ -6,7 +6,7 @@ @use "refactor/common-refactor.scss" as deprecated; @use "common/refactor/common-dashboard"; -@use "./placeholder.scss"; +@use "./placeholder"; .dashboard-container { flex: 1 0 0; @@ -18,6 +18,7 @@ &.dashboard-projects { user-select: none; } + &.dashboard-shared { width: calc(100vw - deprecated.$s-320); margin-right: deprecated.$s-52; @@ -41,6 +42,7 @@ .text { color: var(--color-foreground-primary); } + .icon svg { stroke: var(--color-foreground-secondary); width: deprecated.$s-32; diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index 64375cdf6e..52a17762e3 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -29,15 +29,14 @@ z-index: var(--z-index-dropdown); } -//SIDEBAR CONTENT COMPONENT +// SIDEBAR CONTENT COMPONENT .sidebar-content { display: grid; grid-template-rows: auto auto auto auto 1fr; gap: var(--sp-xxl); height: 100%; padding: 0; - overflow-x: hidden; - overflow-y: auto; + overflow: hidden auto; } .sidebar-content-nitrate { @@ -57,6 +56,7 @@ .sidebar-section-title { @include t.use-typography("headline-small"); + padding: 0 var(--sp-s) var(--sp-s) var(--sp-xxl); color: var(--color-foreground-secondary); } @@ -80,6 +80,7 @@ .current-team { @include deprecated.buttonStyle; + display: grid; align-items: center; grid-template-columns: 1fr auto; @@ -99,6 +100,7 @@ .team-text { @include deprecated.textEllipsis; @include t.use-typography("title-small"); + width: auto; text-align: left; color: var(--menu-foreground-color-hover); @@ -124,13 +126,15 @@ .team-picture { @include deprecated.flexCenter; + border-radius: 50%; height: var(--sp-xxl); width: var(--sp-xxl); } .arrow-icon { - @extend .button-icon; + @extend %button-icon; + transform: rotate(90deg); stroke: var(--icon-foreground); } @@ -138,6 +142,7 @@ .switch-options { @include deprecated.buttonStyle; @include deprecated.flexCenter; + max-width: var(--sp-xxl); min-width: deprecated.$s-28; height: 100%; @@ -146,26 +151,28 @@ } .menu-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } // DROPDOWNS .teams-dropdown { - @extend .menu-dropdown; + @extend %menu-dropdown; + left: 0; top: deprecated.$s-52; height: fit-content; max-height: $sz-480; min-width: deprecated.$s-248; width: 100%; - overflow-x: hidden; - overflow-y: auto; + overflow: hidden auto; } .team-dropdown-item { - @extend .menu-item-base; + @extend %menu-item-base; + display: grid; grid-template-columns: var(--sp-xxl) 1fr auto; gap: var(--sp-s); @@ -173,7 +180,8 @@ } .org-dropdown-item { - @extend .menu-item-base; + @extend %menu-item-base; + display: grid; grid-template-columns: var(--sp-xxxl) 1fr auto; gap: var(--sp-s); @@ -192,6 +200,7 @@ .icon-wrapper { @include deprecated.flexCenter; + width: var(--sp-xxl); height: var(--sp-xxl); margin-right: var(--sp-m); @@ -200,7 +209,8 @@ } .add-icon { - @extend .button-icon; + @extend %button-icon; + width: var(--sp-xxl); height: var(--sp-xxl); stroke: var(--sidebar-action-icon-color); @@ -212,12 +222,14 @@ } .tick-icon { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } .options-dropdown { - @extend .menu-dropdown; + @extend %menu-dropdown; + right: var(--sp-xxs); top: deprecated.$s-52; max-height: $sz-480; @@ -228,7 +240,8 @@ } .team-options-item { - @extend .menu-item-base; + @extend %menu-item-base; + height: $sz-40; } @@ -290,6 +303,7 @@ .element-title { @include deprecated.textEllipsis; + width: deprecated.$s-256; color: var(--color-foreground-primary); font-size: deprecated.$fs-14; @@ -305,7 +319,8 @@ } .pin-icon { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); margin: 0 var(--sp-m); } @@ -329,6 +344,7 @@ .input-text { @include t.use-typography("title-small"); + height: $sz-40; width: 100%; padding: $sz-6 var(--sp-m); @@ -353,6 +369,7 @@ .search-btn { @include deprecated.buttonStyle; @include deprecated.flexCenter; + position: absolute; right: 0; height: var(--sp-xxl); @@ -362,8 +379,10 @@ .search-icon, .clear-search-btn { - @extend .button-icon; + @extend %button-icon; + --sidebar-search-foreground-color: var(--search-bar-icon-foreground-color); + stroke: var(--sidebar-search-foreground-color); } @@ -384,6 +403,7 @@ .profile { @include deprecated.buttonStyle; + display: grid; grid-template-columns: auto 1fr; gap: var(--sp-s); @@ -394,6 +414,7 @@ .profile-fullname { @include t.use-typography("title-small"); @include deprecated.textEllipsis; + align-self: center; max-width: var(--sp-l) 0; color: var(--profile-foreground-color); @@ -406,16 +427,19 @@ } .profile-dropdown { - @extend .menu-dropdown; + @extend %menu-dropdown; + inset-inline-start: var(--sp-s); inset-block-end: px2rem(72); // 72 is the height of the profile button min-width: calc(100% - var(--sp-s)); + // TODO ADD animation fadeInUp } .profile-dropdown-item { - @extend .menu-item-base; + @extend %menu-item-base; @include t.use-typography("body-medium"); + block-size: $sz-40; margin-block-end: var(--sp-xs); padding: var(--sp-s); @@ -435,12 +459,14 @@ } .profile-dropdown-item .open-arrow svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } .sub-menu { - @extend .menu-dropdown; + @extend %menu-dropdown; + inset-inline-start: calc(deprecated.$s-292 + var(--sp-s)); min-width: deprecated.$s-192; } @@ -458,8 +484,9 @@ } .submenu-item { - @extend .menu-item-base; + @extend %menu-item-base; @include t.use-typography("body-medium"); + block-size: $sz-40; margin-block-end: var(--sp-xs); padding-block: var(--sp-s); @@ -479,6 +506,7 @@ .menu-version { @include t.use-typography("code-font"); @include deprecated.textEllipsis; + color: var(--color-foreground-secondary); margin-inline-start: var(--sp-s); text-transform: uppercase; @@ -496,19 +524,22 @@ } .exit-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } .add-org-icon { - @extend .button-icon; + @extend %button-icon; + width: var(--sp-l); height: var(--sp-l); stroke: var(--sidebar-action-icon-color); } .arrow-up-right-icon { - @extend .button-icon; + @extend %button-icon; + width: var(--sp-m); height: var(--sp-m); stroke: var(--sidebar-action-icon-color); @@ -516,6 +547,7 @@ .upgrade-plan-section { @include deprecated.buttonStyle; + display: flex; justify-content: space-between; border: $b-1 solid var(--color-background-quaternary); @@ -528,6 +560,7 @@ .penpot-free { @include t.use-typography("body-medium"); + display: flex; flex-direction: column; text-align: left; @@ -539,6 +572,7 @@ .power-up { @include t.use-typography("body-small"); + color: var(--color-accent-tertiary); } @@ -553,6 +587,7 @@ .nitrate-selected-org { @include t.use-typography("body-medium"); + color: var(--color-foreground-primary); width: 100%; padding-inline-start: var(--sp-s); @@ -605,6 +640,7 @@ .current-org { @include deprecated.buttonStyle; + display: grid; align-items: center; grid-template-columns: 1fr auto; diff --git a/frontend/src/app/main/ui/dashboard/subscription.scss b/frontend/src/app/main/ui/dashboard/subscription.scss index e9439558f1..24f92ab331 100644 --- a/frontend/src/app/main/ui/dashboard/subscription.scss +++ b/frontend/src/app/main/ui/dashboard/subscription.scss @@ -27,6 +27,7 @@ .cta-top-section { @include deprecated.buttonStyle; + display: grid; color: var(--color-foreground-secondary); grid-template-columns: 1fr auto; @@ -44,12 +45,14 @@ .icon-dropdown { @include deprecated.flexCenter; + height: 100%; width: var(--sp-l); } .icon-dropdown svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); transform: rotate(90deg); } @@ -68,6 +71,7 @@ .cta-bottom-section .content { @include t.use-typography("body-medium"); @include deprecated.buttonStyle; + color: var(--color-foreground-secondary); display: inline-block; text-align: left; @@ -88,11 +92,13 @@ .cta-title { @include t.use-typography("body-small"); + margin-block-end: var(--sp-xs); } .highlighted .cta-title { @include t.use-typography("body-medium"); + margin-block-end: 0; } @@ -102,17 +108,20 @@ .highlighted .cta-text { @include t.use-typography("body-large"); + color: var(--color-foreground-primary); } .cta-bottom-section .content a { @include t.use-typography("body-medium"); + color: var(--color-accent-tertiary); margin-inline-start: var(--sp-xs); } .cta-link { @include deprecated.buttonStyle; + align-self: end; margin-inline-start: var(--sp-xs); } @@ -127,17 +136,20 @@ .team-label { @include t.use-typography("headline-small"); + color: var(--title-foreground-color); } .team-text { @include t.use-typography("title-medium"); + color: var(--color-foreground-primary); } .manage-subscription-link { @include deprecated.buttonStyle; @include t.use-typography("body-medium"); + color: var(--color-accent-tertiary); display: flex; margin-block-start: -8px; @@ -161,7 +173,8 @@ } .menu-item { - @extend .menu-item-base; + @extend %menu-item-base; + cursor: pointer; &:hover { @@ -197,6 +210,7 @@ .cta-message { @include t.use-typography("body-small"); + color: var(--color-foreground-secondary); line-height: 1; @@ -218,11 +232,13 @@ .nitrate-title { @include t.use-typography("body-large"); + color: var(--color-foreground-primary); } .nitrate-info { @include t.use-typography("body-medium"); + color: var(--color-foreground-secondary); margin-block: var(--sp-s) var(--sp-xxl); } diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss index 259fdeb565..6d6d0a369b 100644 --- a/frontend/src/app/main/ui/dashboard/team.scss +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -45,6 +45,7 @@ .block-label { @include t.use-typography("headline-small"); + color: var(--color-foreground-secondary); } @@ -82,6 +83,7 @@ .team-icon { --update-button-opacity: 0; + position: relative; height: $sz-120; width: $sz-120; @@ -162,6 +164,7 @@ .table-header { @include t.use-typography("headline-small"); + display: grid; align-items: center; grid-template-columns: 43% 1fr px2rem(108) var(--sp-m); @@ -245,12 +248,13 @@ .member-name, .member-email { - @include textEllipsis; + @include text-ellipsis; @include t.use-typography("body-large"); } .member-email { @include t.use-typography("body-small"); + color: var(--color-foreground-secondary); } @@ -262,6 +266,7 @@ // ROL INFO .rol-selector { @include t.use-typography("body-medium"); + position: relative; display: grid; grid-template-columns: 1fr auto; @@ -303,6 +308,7 @@ .rol-dropdown-item { @include t.use-typography("body-small"); + display: flex; align-items: center; justify-content: space-between; @@ -311,6 +317,7 @@ padding: px2rem(6); border-radius: $br-8; cursor: pointer; + &:hover { background-color: var(--color-background-quaternary); } @@ -337,7 +344,8 @@ .input-checkbox { // TODO: remove this extended class. - @extend .input-checkbox; + @extend %input-checkbox; + cursor: pointer; } @@ -363,6 +371,7 @@ .action-dropdown-item { @include t.use-typography("body-small"); + display: flex; align-items: center; justify-content: space-between; @@ -371,6 +380,7 @@ padding: px2rem(6); border-radius: $br-8; cursor: pointer; + &:hover { background-color: var(--color-background-quaternary); } @@ -399,6 +409,7 @@ .invitations-actions { @include t.use-typography("body-medium"); + display: flex; justify-content: end; align-items: center; @@ -432,7 +443,8 @@ .btn-empty-invitations { // TODO: Remove this extend add DS component - @extend .button-primary; + @extend %button-primary; + margin-block-start: var(--sp-l); padding-inline: var(--sp-m); } @@ -451,8 +463,9 @@ } .field-email { - @include textEllipsis; + @include text-ellipsis; @include t.use-typography("body-large"); + display: flex; gap: var(--sp-l); align-items: center; @@ -499,10 +512,10 @@ .webhooks-hero { @include t.use-typography("body-medium"); + display: grid; grid-template-rows: auto 1fr auto; gap: var(--sp-xxxl); - margin-top: var(--sp-xxxl); margin: 0; padding: var(--sp-xxxl); padding: 0; @@ -511,19 +524,22 @@ .hero-title { @include t.use-typography("title-large"); + color: var(--color-foreground-primary); } .hero-desc { @include t.use-typography("body-large"); + color: var(--color-foreground-secondary); margin-bottom: 0; max-width: $sz-512; } .hero-btn { - //TODO: Remove this extended class using a DS component - @extend .button-primary; + // TODO: Remove this extended class using a DS component + @extend %button-primary; + height: $sz-32; max-width: $sz-512; } @@ -572,6 +588,7 @@ .webhook-dropdown-item { @include t.use-typography("body-small"); + display: flex; align-items: center; justify-content: space-between; @@ -580,6 +597,7 @@ padding: px2rem(6); border-radius: $br-8; cursor: pointer; + &:hover { background-color: var(--color-background-quaternary); } @@ -611,10 +629,7 @@ // INVITE MEMBERS MODAL .modal-team-container { - position: relative; - padding: var(--sp-xxxl); border-radius: $br-8; - background-color: var(--color-background-primary); border: $b-2 solid var(--color-background-quaternary); min-width: $sz-364; min-height: $sz-192; @@ -643,6 +658,7 @@ .modal-title { @include t.use-typography("headline-medium"); + height: $sz-32; color: var(--color-foreground-primary); } @@ -669,12 +685,14 @@ .invite-team-member-text { @include t.use-typography("body-large"); + margin: 0 0 var(--sp-l) 0; color: var(--color-foreground-primary); } .role-title { @include t.use-typography("body-large"); + margin: 0; color: var(--color-foreground-primary); } @@ -691,7 +709,7 @@ .accept-btn { // TODO: remove this extend class creating a modal component - @extend .modal-accept-btn; + @extend %modal-accept-btn; } // WEBHOOKS MODAL @@ -727,16 +745,18 @@ .modal-title { @include t.use-typography("title-small"); + color: var(--color-foreground-primary); } .modal-close-btn { // TODO remove extended class creating a modal component - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content { @include t.use-typography("body-small"); + display: flex; flex-direction: column; gap: var(--sp-xxl); @@ -751,6 +771,7 @@ .select-title { @include t.use-typography("body-small"); + color: var(--color-foreground-primary); } @@ -764,28 +785,31 @@ // TODO: Remove this extended classes creating a modal component .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; button { - @extend .modal-accept-btn; + @extend %modal-accept-btn; } .cancel-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } } // TODO: Remove this extended class using input component .email-input { @include t.use-typography("body-small"); - @extend .input-base; + @extend %input-base; + height: auto; } + // FIXME: This does not conform to our CSS Guidelines. Need to unnest and to use // custom properties to handle state changes. .input-wrapper { display: flex; align-items: center; + @include t.use-typography("body-large"); label { @@ -801,6 +825,7 @@ border-color: var(--color-accent-primary); } } + &:hover { span { border-color: var(--color-accent-primary-muted); @@ -809,13 +834,17 @@ } span { - @extend .checkbox-icon; + @extend %checkbox-icon; @include t.use-typography("body-small"); + color: var(--color-foreground-secondary); } + input { margin: 0; + @include t.use-typography("body-small"); + color: var(--color-foreground-secondary); } } diff --git a/frontend/src/app/main/ui/dashboard/team_form.scss b/frontend/src/app/main/ui/dashboard/team_form.scss index eba9c361d0..354a177dd0 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.scss +++ b/frontend/src/app/main/ui/dashboard/team_form.scss @@ -7,11 +7,11 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; } .modal-header { @@ -20,11 +20,12 @@ .modal-title { @include deprecated.uppercaseTitleTipography; + color: var(--modal-title-foreground-color); } .modal-close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content { @@ -36,12 +37,15 @@ } .group-name-input { - @extend .input-element-label; + @extend %input-element-label; @include deprecated.bodySmallTypography; + margin-bottom: deprecated.$s-8; + label { @include deprecated.flexColumn; @include deprecated.bodySmallTypography; + align-items: flex-start; width: 100%; border: none; @@ -55,15 +59,17 @@ } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; } .cancel-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } + .accept-btn { - @extend .modal-accept-btn; + @extend %modal-accept-btn; + &.danger { - @extend .modal-danger-btn; + @extend %modal-danger-btn; } } diff --git a/frontend/src/app/main/ui/dashboard/templates.scss b/frontend/src/app/main/ui/dashboard/templates.scss index 8a5ab660c0..583e87d6ad 100644 --- a/frontend/src/app/main/ui/dashboard/templates.scss +++ b/frontend/src/app/main/ui/dashboard/templates.scss @@ -19,21 +19,23 @@ flex-direction: column; height: px2rem(244); justify-content: flex-end; - margin-inline-start: px2rem(6); - margin-inline-end: px2rem(6); + margin-inline: px2rem(6); margin-block-end: px2rem(6); position: absolute; transition: bottom 300ms; width: calc(100% - $sz-12); pointer-events: none; + &.collapsed { inset-block-end: calc(-1 * px2rem(228)); background-color: transparent; transition: bottom 300ms; + .title-btn { border-end-end-radius: $br-8; border-end-start-radius: $br-8; } + .content, .content-description { visibility: hidden; @@ -66,26 +68,24 @@ .title-text { @include t.use-typography("body-large"); + display: inline-block; vertical-align: middle; - margin-inline-start: var(--sp-m); - margin-inline-end: var(--sp-s); + margin-inline: var(--sp-m) var(--sp-s); color: var(--color-foreground-primary); } .title-icon-container { display: inline-block; vertical-align: middle; - margin-inline-start: auto; - margin-inline-end: var(--sp-s); + margin-inline: auto var(--sp-s); color: var(--color-foreground-primary); } .title-icon { display: inline-block; vertical-align: middle; - margin-inline-start: auto; - margin-inline-end: var(--sp-s); + margin-inline: auto var(--sp-s); transform: rotate(90deg); } @@ -127,6 +127,7 @@ &:hover { border: $b-2 solid var(--color-background-tertiary); background-color: var(--color-accent-primary); + .arrow-icon { stroke: var(--color-background-tertiary); } @@ -146,9 +147,9 @@ .content-description { @include t.use-typography("body-medium"); + color: var(--color-foreground-primary); - margin-block-end: calc(-1 * var(--sp-s)); - margin-block-start: var(--sp-l); + margin-block: var(--sp-l) calc(-1 * var(--sp-s)); margin-inline-start: var(--sp-l); visibility: visible; } @@ -179,6 +180,7 @@ .template-card { @include t.use-typography("body-large"); + display: inline-block; width: px2rem(256); cursor: pointer; @@ -186,12 +188,15 @@ padding: 0 var(--sp-xs) var(--sp-s) var(--sp-xs); border-radius: $br-8; border: $b-2 solid transparent; + &:hover { text-decoration: none; border-color: var(--color-accent-primary); + .download-icon { stroke: var(--color-accent-primary); } + .card-text { color: var(--color-accent-primary); } @@ -222,6 +227,7 @@ .card-text { @include t.use-typography("body-large"); + white-space: nowrap; overflow: hidden; width: 90%; @@ -249,11 +255,13 @@ .template-link-title { @include t.use-typography("body-medium"); + color: var(--color-foreground-primary); } .template-link-text { @include t.use-typography("body-small"); + margin-block-start: var(--sp-s); color: var(--color-foreground-secondary); } diff --git a/frontend/src/app/main/ui/debug/icons_preview.scss b/frontend/src/app/main/ui/debug/icons_preview.scss index a8493ed42b..082723835e 100644 --- a/frontend/src/app/main/ui/debug/icons_preview.scss +++ b/frontend/src/app/main/ui/debug/icons_preview.scss @@ -10,6 +10,7 @@ .title { @include deprecated.bigTitleTipography; + color: var(--color-foreground-primary); } @@ -28,9 +29,9 @@ row-gap: 0.5rem; grid-template-rows: var(--cell-size) 1fr; padding: 0.5rem; - color: var(--color-foreground-primary); - word-break: break-word; + overflow-wrap: break-word; + @include deprecated.bodySmallTypography; svg { diff --git a/frontend/src/app/main/ui/debug/playground.scss b/frontend/src/app/main/ui/debug/playground.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/src/app/main/ui/delete_shared.scss b/frontend/src/app/main/ui/delete_shared.scss index d0f08a50e8..c3487c3aa2 100644 --- a/frontend/src/app/main/ui/delete_shared.scss +++ b/frontend/src/app/main/ui/delete_shared.scss @@ -8,14 +8,16 @@ @use "ds/typography.scss" as t; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; + &.transparent { background-color: transparent; } } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; + display: grid; gap: var(--sp-xxl); grid-template-rows: auto minmax(0, 1fr) auto; @@ -29,38 +31,42 @@ .modal-title { @include t.use-typography("headline-medium"); + color: var(--modal-title-foreground-color); } .modal-close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content { @include t.use-typography("body-small"); + display: grid; gap: var(--sp-s); } .element-list { @include t.use-typography("body-large"); + color: var(--modal-text-foreground-color); overflow-y: auto; margin-block: 0; } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; } .cancel-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } .accept-btn { - @extend .modal-accept-btn; + @extend %modal-accept-btn; + &.danger { - @extend .modal-danger-btn; + @extend %modal-danger-btn; } } @@ -72,6 +78,7 @@ .modal-subtitle, .modal-msg { @include t.use-typography("body-large"); + color: var(--modal-text-foreground-color); line-height: 1.5; } diff --git a/frontend/src/app/main/ui/ds/_borders.scss b/frontend/src/app/main/ui/ds/_borders.scss index 9fff3615ac..070f0b9abb 100644 --- a/frontend/src/app/main/ui/ds/_borders.scss +++ b/frontend/src/app/main/ui/ds/_borders.scss @@ -12,6 +12,5 @@ $br-6: px2rem(6); $br-8: px2rem(8); $br-12: px2rem(12); $br-circle: 50%; - $b-1: px2rem(1); $b-2: px2rem(2); diff --git a/frontend/src/app/main/ui/ds/_utils.scss b/frontend/src/app/main/ui/ds/_utils.scss index 248d43d002..a455cb0318 100644 --- a/frontend/src/app/main/ui/ds/_utils.scss +++ b/frontend/src/app/main/ui/ds/_utils.scss @@ -7,6 +7,7 @@ @use "sass:math"; @function px2rem($value) { - $remValue: math.div($value, 16) * 1rem; - @return $remValue; + $rem-value: math.div($value, 16) * 1rem; + + @return $rem-value; } diff --git a/frontend/src/app/main/ui/ds/buttons/_buttons.scss b/frontend/src/app/main/ui/ds/buttons/_buttons.scss index 433495c300..a3001d8311 100644 --- a/frontend/src/app/main/ui/ds/buttons/_buttons.scss +++ b/frontend/src/app/main/ui/ds/buttons/_buttons.scss @@ -11,29 +11,21 @@ %base-button { --button-bg-color: initial; --button-fg-color: initial; - --button-hover-bg-color: initial; --button-hover-fg-color: initial; - --button-active-bg-color: initial; --button-active-fg-color: initial; - --button-disabled-bg-color: initial; --button-disabled-fg-color: initial; - --button-border-color: var(--button-bg-color); - --button-focus-inner-ring-color: initial; --button-focus-outer-ring-color: initial; - --button-width: initial; --button-height: #{$sz-32}; appearance: none; - width: var(--button-width); height: var(--button-height); - background: var(--button-bg-color); color: var(--button-fg-color); border: $b-1 solid var(--button-border-color); @@ -53,6 +45,7 @@ &:focus-visible { outline: var(--button-focus-inner-ring-color) solid #{px2rem(2)}; outline-offset: -#{px2rem(3)}; + --button-border-color: var(--button-focus-outer-ring-color); --button-fg-color: var(--button-focus-fg-color); } @@ -66,16 +59,12 @@ %base-button-primary { --button-bg-color: var(--color-accent-primary); --button-fg-color: var(--color-background-secondary); - --button-hover-bg-color: var(--color-accent-tertiary); --button-hover-fg-color: var(--color-background-secondary); - --button-active-bg-color: var(--color-accent-tertiary); --button-active-fg-color: var(--color-background-secondary); - --button-disabled-bg-color: var(--color-accent-primary-muted); --button-disabled-fg-color: var(--color-background-secondary); - --button-focus-bg-color: var(--color-accent-primary); --button-focus-fg-color: var(--color-background-secondary); --button-focus-inner-ring-color: var(--color-background-secondary); @@ -83,23 +72,19 @@ &:active, &[aria-pressed="true"] { - box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2); + box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgb(0 0 0 / 20%); } } %base-button-secondary { --button-bg-color: var(--color-background-tertiary); --button-fg-color: var(--color-foreground-secondary); - --button-hover-bg-color: var(--color-background-tertiary); --button-hover-fg-color: var(--color-accent-primary); - --button-active-bg-color: var(--color-background-quaternary); --button-active-fg-color: var(--color-accent-primary); - --button-disabled-bg-color: transparent; --button-disabled-fg-color: var(--color-foreground-secondary); - --button-focus-bg-color: var(--color-background-tertiary); --button-focus-fg-color: var(--color-foreground-primary); --button-focus-inner-ring-color: var(--color-background-secondary); @@ -109,16 +94,12 @@ %base-button-ghost { --button-bg-color: transparent; --button-fg-color: var(--color-foreground-secondary); - --button-hover-bg-color: var(--color-background-tertiary); --button-hover-fg-color: var(--color-accent-primary); - --button-active-bg-color: var(--color-background-quaternary); --button-active-fg-color: var(--color-accent-primary); - --button-disabled-bg-color: transparent; --button-disabled-fg-color: var(--color-accent-primary-muted); - --button-focus-bg-color: transparent; --button-focus-fg-color: var(--color-foreground-secondary); --button-focus-inner-ring-color: transparent; @@ -128,16 +109,12 @@ %base-button-destructive { --button-bg-color: var(--color-accent-error); --button-fg-color: var(--color-foreground-primary); - --button-hover-bg-color: var(--color-background-error); --button-hover-fg-color: var(--color-foreground-primary); - --button-active-bg-color: var(--color-accent-error); --button-active-fg-color: var(--color-foreground-primary); - --button-disabled-bg-color: var(--color-background-error); --button-disabled-fg-color: var(--color-accent-error); - --button-focus-bg-color: var(--color-accent-error); --button-focus-fg-color: var(--color-foreground-primary); --button-focus-inner-ring-color: var(--color-background-primary); @@ -145,6 +122,6 @@ &:active, &[aria-pressed="true"] { - box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2); + box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgb(0 0 0 / 20%); } } diff --git a/frontend/src/app/main/ui/ds/buttons/button.scss b/frontend/src/app/main/ui/ds/buttons/button.scss index dd8c720559..5885f881c8 100644 --- a/frontend/src/app/main/ui/ds/buttons/button.scss +++ b/frontend/src/app/main/ui/ds/buttons/button.scss @@ -9,10 +9,9 @@ .button { @extend %base-button; - @include use-typography("headline-small"); - padding: 0 var(--sp-m); + padding: 0 var(--sp-m); display: inline-flex; align-items: center; column-gap: var(--sp-xs); diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.scss b/frontend/src/app/main/ui/ds/buttons/icon_button.scss index 26c8692558..40b422168a 100644 --- a/frontend/src/app/main/ui/ds/buttons/icon_button.scss +++ b/frontend/src/app/main/ui/ds/buttons/icon_button.scss @@ -37,20 +37,15 @@ .icon-button-action { --button-bg-color: transparent; --button-fg-color: var(--color-foreground-secondary); - --button-hover-bg-color: transparent; --button-hover-fg-color: var(--color-accent-primary); - --button-active-bg-color: var(--color-background-quaternary); - --button-disabled-bg-color: transparent; --button-disabled-fg-color: var(--color-accent-primary-muted); - --button-focus-bg-color: transparent; --button-focus-fg-color: var(--color-accent-primary); --button-focus-inner-ring-color: transparent; --button-focus-outer-ring-color: var(--color-accent-primary); - --button-width: #{$sz-24}; --button-height: #{$sz-24}; } diff --git a/frontend/src/app/main/ui/ds/colors.scss b/frontend/src/app/main/ui/ds/colors.scss index e5c1525e10..67358d3096 100644 --- a/frontend/src/app/main/ui/ds/colors.scss +++ b/frontend/src/app/main/ui/ds/colors.scss @@ -12,22 +12,17 @@ $mint-700: #426158; $mint-150-60: #7efff599; $mint-250-10: #00d1b81a; $mint-250-70: #00d1b8b3; - $green-200: #a7e8d9; $green-500: #2d9f8f; $green-950: #0a2927; - $orange-200: #fedeac; $orange-500: #fe9c07; $orange-950: #3d2501; - $red-200: #ffcada; $red-400: #c80857; $red-500: #ff3277; $red-950: #500124; - $pink-400: #ff6fe0; - $purple-200: #e1d2f5; $purple-400: #bb97d8; $purple-500: #a977d1; @@ -36,23 +31,18 @@ $purple-700: #6911d4; $purple-600-10: #8c33eb1a; $purple-600-70: #8c33ebb3; $purple-700-60: #6911d499; - $aqua-200: #ddf7ff; $aqua-400: #77e1f3; $aqua-600: #59acbb; $aqua-800: #1d4464; - $violet-300: #a7a9ff; $violet-600: #6c6dad; $violet-700: #484c74; $violet-800: #272941; - $blue-200: #bae3fd; $blue-500: #0e9be9; $blue-950: #082c49; - $cobalt-700: #1345aa; - $black: #000; $gray-950: #18181a; $gray-950-60: #18181a99; @@ -63,12 +53,10 @@ $gray-200: #e8eaee; $gray-100: #eef0f2; $gray-50: #f3f4f6; $white: #fff; -$white-60: #ffffff99; +$white-60: #fff9; $white-90: #ffffffe6; - $blue-teal-700: #495e74; $grayish-blue-500: #8f9da3; - $grayish-red: #bfbfbf; :global(.light) { @@ -83,7 +71,6 @@ $grayish-red: #bfbfbf; --color-accent-action: #{$purple-400}; --color-accent-action-hover: #{$purple-500}; --color-accent-off: #{$gray-50}; - --color-accent-success: #{$green-500}; --color-background-success: #{$green-200}; --color-accent-warning: #{$orange-500}; @@ -97,29 +84,23 @@ $grayish-red: #bfbfbf; --color-accent-default: #{$gray-100}; --color-icon-default: #{$blue-teal-700}; --color-background-disabled: #{$gray-200}; - --color-background-primary: #{$white}; --color-background-secondary: #{$gray-200}; --color-background-tertiary: #{$gray-50}; --color-background-quaternary: #{$gray-100}; - --color-foreground-primary: #{$black}; --color-foreground-secondary: #{$blue-teal-700}; - --color-static-white: #{$white}; --color-static-black: #{$black}; - --color-shadow-dark: #{color.change($gray-200, $alpha: 0.6)}; --color-shadow-light: #{color.change($black, $alpha: 0.3)}; --color-overlay-default: #{$white-60}; --color-overlay-onboarding: #{$white-90}; --color-canvas: #{$grayish-red}; - --color-token-background: #{$aqua-200}; --color-token-border: #{$aqua-400}; --color-token-accent: #{$aqua-600}; --color-token-foreground: #{$aqua-800}; - --color-badge-premium: #{$orange-500}; } @@ -135,7 +116,6 @@ $grayish-red: #bfbfbf; --color-accent-action: #{$purple-400}; --color-accent-action-hover: #{$purple-500}; --color-accent-off: #{$gray-50}; - --color-accent-success: #{$green-500}; --color-background-success: #{$green-950}; --color-accent-warning: #{$orange-500}; @@ -149,28 +129,22 @@ $grayish-red: #bfbfbf; --color-accent-default: #{$gray-800}; --color-icon-default: #{$grayish-blue-500}; --color-background-disabled: #{$gray-800}; - --color-background-primary: #{$gray-950}; --color-background-secondary: #{$black}; --color-background-tertiary: #{$gray-900}; --color-background-quaternary: #{$gray-800}; - --color-foreground-primary: #{$white}; --color-foreground-secondary: #{$grayish-blue-500}; - --color-static-white: #{$white}; --color-static-black: #{$black}; - --color-shadow-dark: #{color.change($black, $alpha: 0.6)}; --color-shadow-light: #{color.change($black, $alpha: 0.3)}; --color-overlay-default: #{$gray-950-60}; --color-overlay-onboarding: #{$gray-950-90}; --color-canvas: #{$grayish-red}; - --color-token-background: #{$violet-800}; --color-token-border: #{$violet-700}; --color-token-accent: #{$violet-600}; --color-token-foreground: #{$violet-300}; - --color-badge-premium: #{$orange-200}; } diff --git a/frontend/src/app/main/ui/ds/controls/checkbox.scss b/frontend/src/app/main/ui/ds/controls/checkbox.scss index 81eda1fd47..de272a654f 100644 --- a/frontend/src/app/main/ui/ds/controls/checkbox.scss +++ b/frontend/src/app/main/ui/ds/controls/checkbox.scss @@ -14,14 +14,11 @@ --input-checkbox-border-color-hover: var(--color-accent-primary-muted); --input-checkbox-foreground-color: var(--color-foreground-primary); --input-checkbox-background-color: var(--color-background-quaternary); - --input-checkbox-border-color-checked: var(--color-background-quaternary); --input-checkbox-foreground-color-checked: var(--color-background-primary); --input-checkbox-background-color-checked: var(--color-accent-primary); - --input-checkbox-foreground-color-disabled: var(--color-background-primary); --input-checkbox-background-color-disabled: var(--color-foreground-secondary); - --input-checkbox-text-color: var(--color-foreground-secondary); } @@ -73,6 +70,7 @@ .checkbox-text { @include use-typography("body-small"); + padding-inline-start: var(--sp-s); color: var(--input-checkbox-text-color); } diff --git a/frontend/src/app/main/ui/ds/controls/combobox.scss b/frontend/src/app/main/ui/ds/controls/combobox.scss index 3df8586715..70ad818514 100644 --- a/frontend/src/app/main/ui/ds/controls/combobox.scss +++ b/frontend/src/app/main/ui/ds/controls/combobox.scss @@ -26,6 +26,7 @@ } @include use-typography("body-small"); + position: relative; display: grid; grid-template-rows: auto; @@ -40,7 +41,6 @@ height: $sz-32; width: 100%; padding: var(--sp-s); - border: none; border-radius: $br-8; outline: $b-1 solid var(--combobox-outline-color); border: $b-1 solid var(--combobox-border-color); @@ -68,6 +68,7 @@ all: unset; @include use-typography("body-small"); + background-color: transparent; overflow: hidden; text-align: left; @@ -88,6 +89,7 @@ .disabled { cursor: default; + --combobox-background-color: var(--color-background-primary); --combobox-border-color: var(--color-background-quaternary); --combobox-text-color: var(--color-foreground-secondary); diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.scss b/frontend/src/app/main/ui/ds/controls/numeric_input.scss index b45ff9fe15..1c2db43243 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.scss +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.scss @@ -13,7 +13,9 @@ .input-wrapper { --input-padding-size: var(--sp-xs); --opacity-button: 0; + @include t.use-typography("code-font"); + display: flex; flex-direction: column; gap: var(--sp-xs); @@ -23,9 +25,11 @@ &:not(:focus-within) { cursor: ew-resize; } + &:hover { --opacity-button: 1; } + &:focus-within { --opacity-button: 1; } @@ -38,7 +42,9 @@ .text-icon { color: var(--color-foreground-secondary); + @include t.use-typography("body-small"); + inline-size: fit-content; min-inline-size: px2rem(46); padding-inline-start: var(--sp-xs); @@ -50,12 +56,16 @@ inset-block-start: 0; opacity: var(--opacity-button); background-color: var(--color-background-quaternary); + &:hover { background-color: var(--color-background-quaternary); + --opacity-button: 1; } + &:focus { background-color: var(--color-background-quaternary); + --opacity-button: 1; } } diff --git a/frontend/src/app/main/ui/ds/controls/select.scss b/frontend/src/app/main/ui/ds/controls/select.scss index 3b96b1b7e8..0cb48866ca 100644 --- a/frontend/src/app/main/ui/ds/controls/select.scss +++ b/frontend/src/app/main/ui/ds/controls/select.scss @@ -22,6 +22,7 @@ } @include use-typography("body-small"); + position: relative; display: grid; grid-template-rows: auto; @@ -47,7 +48,6 @@ block-size: $sz-32; inline-size: 100%; padding: var(--sp-s); - border: none; border-radius: $br-8; outline: $b-1 solid var(--select-outline-color); border: $b-1 solid var(--select-border-color); @@ -91,6 +91,7 @@ .header-label { @include use-typography("body-small"); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/frontend/src/app/main/ui/ds/controls/shared/option.scss b/frontend/src/app/main/ui/ds/controls/shared/option.scss index 0c2462206b..ae38b73b01 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/option.scss +++ b/frontend/src/app/main/ui/ds/controls/shared/option.scss @@ -14,8 +14,7 @@ --options-empty: var(--color-canvas); display: grid; - align-items: center; - justify-items: start; + place-items: center start; grid-template-columns: 1fr auto; gap: var(--sp-xs); width: 100%; @@ -56,6 +55,7 @@ .option-current { --options-outline-color: var(--color-accent-primary); + outline: $b-1 solid var(--options-outline-color); } diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss index f5a164efb5..4af9bb4793 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss +++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss @@ -25,10 +25,9 @@ padding-block: var(--sp-xs); margin-block-end: 0; max-block-size: $sz-400; - overflow-y: auto; - overflow-x: hidden; + overflow: hidden auto; z-index: var(--z-index-dropdown); - box-shadow: 0px 0px $sz-12 0px var(--color-shadow-dark); + box-shadow: 0 0 $sz-12 0 var(--color-shadow-dark); } .left-align { @@ -41,13 +40,13 @@ .option-separator { border: $b-1 solid var(--options-dropdown-border-color); - margin-block-start: var(--sp-xs); - margin-block-end: var(--sp-xs); + margin-block: var(--sp-xs) var(--sp-xs); } .group-option, .option-empty { @include use-typography("body-small"); + display: flex; align-items: center; gap: var(--sp-xs); diff --git a/frontend/src/app/main/ui/ds/controls/shared/token_option.scss b/frontend/src/app/main/ui/ds/controls/shared/token_option.scss index 884ddfea54..b05b6e8e66 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/token_option.scss +++ b/frontend/src/app/main/ui/ds/controls/shared/token_option.scss @@ -12,10 +12,11 @@ --token-options-fg-color: var(--color-foreground-primary); --token-options-bg-color: unset; --token-options-empty: var(--color-canvas); + @include use-typography("body-small"); + display: grid; - align-items: center; - justify-items: start; + place-items: center start; grid-template-columns: 1fr auto; gap: $sz-6; width: 100%; @@ -26,10 +27,10 @@ outline-offset: calc(-1 * $b-1); background-color: var(--token-options-bg-color); color: var(--token-options-fg-color); - overflow: hidden; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + &:hover, &[aria-selected="true"] { --token-options-bg-color: var(--color-background-quaternary); @@ -51,11 +52,13 @@ .option-current { --token-options-outline-color: var(--color-accent-primary); + outline: $b-1 solid var(--token-options-outline-color); } .option-pill { @include use-typography("code-font"); + color: var(--color-foreground-secondary); background-color: var(--color-background-primary); border-radius: $br-6; diff --git a/frontend/src/app/main/ui/ds/controls/switch.scss b/frontend/src/app/main/ui/ds/controls/switch.scss index c5abbf3a00..01eae97f00 100644 --- a/frontend/src/app/main/ui/ds/controls/switch.scss +++ b/frontend/src/app/main/ui/ds/controls/switch.scss @@ -13,10 +13,8 @@ .switch { --switch-label-foreground-color: var(--color-foreground-primary); - --switch-track-outline-color: none; --switch-track-shadow: inset 0 1px 2px var(--color-shadow-light); - --switch-thumb-shadow: 0 1px 2px var(--color-shadow-light); display: grid; @@ -29,7 +27,6 @@ &.off { --switch-track-justify-content: start; --switch-track-background-color: var(--color-foreground-secondary); - --switch-thumb-width: #{px2rem(14)}; --switch-thumb-height: #{px2rem(14)}; --switch-thumb-background-color: var(--color-accent-off); @@ -39,7 +36,6 @@ &.neutral { --switch-track-justify-content: center; --switch-track-background-color: var(--color-accent-tertiary); - --switch-thumb-width: #{px2rem(14)}; --switch-thumb-height: #{px2rem(4)}; --switch-thumb-background-color: var(--color-accent-off); @@ -49,7 +45,6 @@ &.on { --switch-track-justify-content: end; --switch-track-background-color: var(--color-accent-tertiary); - --switch-thumb-width: #{px2rem(14)}; --switch-thumb-height: #{px2rem(14)}; --switch-thumb-background-color: var(--color-accent-off); @@ -58,24 +53,21 @@ &[disabled] { pointer-events: none; + --switch-label-foreground-color: var(--color-foreground-secondary); - --switch-track-shadow: none; - --switch-thumb-shadow: none; } &.off[disabled] { --switch-track-background-color: var(--color-background-primary); --switch-track-border-color: var(--color-background-disabled); - --switch-thumb-background-color: var(--color-background-disabled); } &.on[disabled], &.neutral[disabled] { --switch-track-background-color: var(--color-background-disabled); - --switch-thumb-background-color: var(--color-background-primary); } @@ -90,6 +82,7 @@ .switch-label { @include t.use-typography("body-small"); + color: var(--switch-label-foreground-color); user-select: none; } diff --git a/frontend/src/app/main/ui/ds/controls/utilities/hint_message.scss b/frontend/src/app/main/ui/ds/controls/utilities/hint_message.scss index 1112a3f816..97f1a12fda 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/hint_message.scss +++ b/frontend/src/app/main/ui/ds/controls/utilities/hint_message.scss @@ -11,6 +11,7 @@ --hint-color: var(--color-foreground-secondary); @include use-typography("body-small"); + color: var(--hint-color); } diff --git a/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss b/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss index 20533664ef..adc4f5a301 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss +++ b/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss @@ -22,7 +22,6 @@ align-items: center; position: relative; inline-size: 100%; - background: var(--input-bg-color); border-radius: $br-8; padding: 0 var(--input-padding-size, var(--sp-s)); @@ -85,12 +84,10 @@ border: none; background: none; inline-size: 100%; - font-family: inherit; font-size: inherit; font-weight: inherit; line-height: inherit; - color: var(--input-fg-color); &:focus-visible { @@ -107,7 +104,6 @@ &:is(:autofill, :autofill:hover, :autofill:focus, :autofill:active) { -webkit-text-fill-color: var(--input-fg-color); - -webkit-background-clip: text; background-clip: text; caret-color: var(--input-bg-color); } diff --git a/frontend/src/app/main/ui/ds/controls/utilities/label.scss b/frontend/src/app/main/ui/ds/controls/utilities/label.scss index 405beb6c6f..4ba6a988dc 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/label.scss +++ b/frontend/src/app/main/ui/ds/controls/utilities/label.scss @@ -13,6 +13,7 @@ --label-optional-color: var(--color-foreground-secondary); @include use-typography("body-small"); + color: var(--label-color); display: flex; gap: var(--sp-xs); diff --git a/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss b/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss index 4233b2305d..4cb3a61a0d 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss +++ b/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss @@ -18,10 +18,10 @@ --token-field-outline-color: none; --token-field-height: var(--sp-xxxl); --token-field-margin: unset; + display: inline-flex; column-gap: var(--sp-xs); align-items: center; - position: relative; inline-size: 100%; background: var(--token-field-bg-color); border-radius: $br-8; @@ -38,6 +38,7 @@ --token-field-outline-color: var(--color-accent-primary); } } + .token-field-wrapper { inline-size: 100%; } @@ -48,8 +49,10 @@ .token-field-disabled { user-select: none; + --token-field-bg-color: var(--color-background-primary); --token-field-outline-color: var(--color-background-quaternary); + &:hover { --token-field-bg-color: var(--color-background-primary); --token-field-outline-color: var(--color-background-quaternary); @@ -60,8 +63,10 @@ --pill-border-color: var(--color-token-border); --pill-bg-color: var(--color-background-tertiary); --pill-fg-color: var(--color-token-foreground); + @include t.use-typography("code-font"); - @include textEllipsis; + @include text-ellipsis; + display: block; block-size: var(--sp-xxl); inline-size: fit-content; @@ -72,24 +77,29 @@ border-radius: $br-6; padding-inline: $sz-6; max-inline-size: 100%; + &:hover { --pill-bg-color: var(--color-token-background); --pill-fg-color: var(--color-foreground-primary); --pill-border-color: var(--color-token-foreground); } + &:focus-visible { --pill-bg-color: var(--color-token-background); --pill-fg-color: var(--color-foreground-primary); --pill-border-color: var(--color-accent-primary); + outline: none; } } .pill-disabled { user-select: none; + --pill-bg-color: none; --pill-fg-color: var(--color-foreground-secondary); --pill-border-color: var(--color-token-border); + &:hover { --pill-bg-color: none; --pill-fg-color: var(--color-foreground-secondary); @@ -101,7 +111,9 @@ --pill-bg-color: none; --pill-fg-color: var(--color-foreground-secondary); --pill-border-color: var(--color-token-border); + position: relative; + &:hover { --pill-bg-color: none; --pill-fg-color: var(--color-foreground-secondary); @@ -127,15 +139,20 @@ inset-block-start: 0; opacity: var(--opacity-button); background-color: var(--color-background-quaternary); + &:hover { background-color: var(--color-background-quaternary); + --opacity-button: 1; } + &:focus { background-color: var(--color-background-quaternary); + --opacity-button: 1; } } + .invisible-btn-dropdown-open { --opacity-button: 0; } diff --git a/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.scss b/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.scss index 207b2236af..a459eb5e74 100644 --- a/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.scss +++ b/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.scss @@ -5,6 +5,6 @@ // Copyright (c) KALEIDOS INC .token-icon { - fill: currentColor; + fill: currentcolor; stroke: none; } diff --git a/frontend/src/app/main/ui/ds/layers/layer_button.cljs b/frontend/src/app/main/ui/ds/layers/layer_button.cljs index 315ad56e88..268e33f961 100644 --- a/frontend/src/app/main/ui/ds/layers/layer_button.cljs +++ b/frontend/src/app/main/ui/ds/layers/layer_button.cljs @@ -27,8 +27,7 @@ [{:keys [label description class is-expandable expanded icon on-toggle-expand on-context-menu children] :rest props}] (let [button-props (mf/spread-props props {:class [class (stl/css-case :layer-button true - :layer-button--expandable is-expandable - :layer-button--expanded expanded)] + :layer-button-expanded expanded)] :type "button" :on-click on-toggle-expand :on-context-menu on-context-menu})] diff --git a/frontend/src/app/main/ui/ds/layers/layer_button.scss b/frontend/src/app/main/ui/ds/layers/layer_button.scss index 56e59e8acf..2850af269f 100644 --- a/frontend/src/app/main/ui/ds/layers/layer_button.scss +++ b/frontend/src/app/main/ui/ds/layers/layer_button.scss @@ -16,9 +16,7 @@ display: flex; justify-content: space-between; - block-size: var(--layer-button-block-size); - background: var(--layer-button-background); color: var(--layer-button-text); } @@ -27,17 +25,15 @@ @include use-typography("body-small"); appearance: none; - flex: 1; display: flex; align-items: center; - border: none; background: none; color: inherit; } -.layer-button--expanded { +.layer-button-expanded { & .layer-button-name { color: var(--color-foreground-primary); } diff --git a/frontend/src/app/main/ui/ds/layout/tab_switcher.scss b/frontend/src/app/main/ui/ds/layout/tab_switcher.scss index 90af8cf778..b8e7dd8f8d 100644 --- a/frontend/src/app/main/ui/ds/layout/tab_switcher.scss +++ b/frontend/src/app/main/ui/ds/layout/tab_switcher.scss @@ -10,15 +10,14 @@ .tabs { --tabs-bg-color: var(--color-background-secondary); + display: grid; grid-template-rows: auto 1fr; } .padding-wrapper { - padding-inline-start: var(--tabs-nav-padding-inline-start, 0); - padding-inline-end: var(--tabs-nav-padding-inline-end, 0); - padding-block-start: var(--tabs-nav-padding-block-start, 0); - padding-block-end: var(--tabs-nav-padding-block-end, 0); + padding-inline: var(--tabs-nav-padding-inline-start, 0) var(--tabs-nav-padding-inline-end, 0); + padding-block: var(--tabs-nav-padding-block-start, 0) var(--tabs-nav-padding-block-end, 0); } // TAB NAV @@ -44,6 +43,7 @@ grid-auto-flow: column; gap: var(--sp-xxs); width: 100%; + // Removing margin bottom from default ul margin-block-end: 0; border-radius: $br-8; @@ -68,7 +68,6 @@ height: $sz-32; border: none; border-radius: $br-8; - padding: 0 var(--sp-s); outline: $b-1 solid var(--tabs-item-outline-color); display: grid; grid-auto-flow: column; @@ -89,6 +88,7 @@ .tab-text { @include use-typography("headline-small"); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -102,6 +102,7 @@ .tab-panel { --tab-panel-outline-color: none; + &:focus { outline: none; } diff --git a/frontend/src/app/main/ui/ds/mixins.scss b/frontend/src/app/main/ui/ds/mixins.scss index 32e2dce255..f43b690c02 100644 --- a/frontend/src/app/main/ui/ds/mixins.scss +++ b/frontend/src/app/main/ui/ds/mixins.scss @@ -8,7 +8,7 @@ @use "ds/_borders.scss" as *; @use "ds/_sizes.scss" as *; -@mixin textEllipsis { +@mixin text-ellipsis { display: block; max-width: 99%; overflow: hidden; @@ -16,7 +16,7 @@ white-space: nowrap; } -@mixin twoLineTextEllipsis { +@mixin two-line-text-ellipsis { max-width: 99%; overflow: hidden; text-overflow: ellipsis; @@ -33,6 +33,7 @@ /// @param {Length} $border - Inner transparent border size /// @param {Bool} $include-selection - Include ::selection styles /// @param {Bool} $include-placeholder - Include placeholder styles + @mixin custom-scrollbar( $thumb-color: #aab5ba4d, $thumb-hover-color: #aab5bab3, @@ -84,12 +85,7 @@ @if $include-placeholder { &::placeholder { @include t.use-typography("body-small"); - color: var(--color-foreground-secondary); - } - // Legacy webkit - &::-webkit-input-placeholder { - @include t.use-typography("body-small"); color: var(--color-foreground-secondary); } } diff --git a/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.scss b/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.scss index 3cf7b1bd2c..3974567f7c 100644 --- a/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.scss +++ b/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.scss @@ -22,10 +22,8 @@ border: $b-1 solid var(--notification-border-color); border-radius: $br-8; padding: var(--notification-padding); - display: flex; gap: var(--sp-s); - color: var(--notification-fg-color); // Targets the potential links included by the creator in the children props. diff --git a/frontend/src/app/main/ui/ds/notifications/toast.scss b/frontend/src/app/main/ui/ds/notifications/toast.scss index c09629bfdd..9a7728d75c 100644 --- a/frontend/src/app/main/ui/ds/notifications/toast.scss +++ b/frontend/src/app/main/ui/ds/notifications/toast.scss @@ -18,7 +18,6 @@ min-inline-size: $sz-224; max-inline-size: $sz-480; - display: block; position: fixed; inset-block-start: var(--toast-inset-block-start-position); diff --git a/frontend/src/app/main/ui/ds/product/avatar.scss b/frontend/src/app/main/ui/ds/product/avatar.scss index 36952c13d7..a84777a71f 100644 --- a/frontend/src/app/main/ui/ds/product/avatar.scss +++ b/frontend/src/app/main/ui/ds/product/avatar.scss @@ -36,6 +36,7 @@ .is-selected { --border-color: var(--color-accent-primary); + padding: var(--sp-xxs); } diff --git a/frontend/src/app/main/ui/ds/product/empty_placeholder.scss b/frontend/src/app/main/ui/ds/product/empty_placeholder.scss index 2850b67eb5..d0e2a8dfa1 100644 --- a/frontend/src/app/main/ui/ds/product/empty_placeholder.scss +++ b/frontend/src/app/main/ui/ds/product/empty_placeholder.scss @@ -22,8 +22,7 @@ .text-wrapper { display: grid; grid-auto-rows: auto; - align-self: center; - justify-self: center; + place-self: center center; max-width: $sz-400; } diff --git a/frontend/src/app/main/ui/ds/product/empty_state.scss b/frontend/src/app/main/ui/ds/product/empty_state.scss index b0612ecec0..60f05e85b9 100644 --- a/frontend/src/app/main/ui/ds/product/empty_state.scss +++ b/frontend/src/app/main/ui/ds/product/empty_state.scss @@ -31,6 +31,7 @@ .text { @include t.use-typography("body-small"); + text-align: center; color: var(--color-foreground-secondary); } diff --git a/frontend/src/app/main/ui/ds/product/input_with_meta.scss b/frontend/src/app/main/ui/ds/product/input_with_meta.scss index a01190d120..17b7e8c65a 100644 --- a/frontend/src/app/main/ui/ds/product/input_with_meta.scss +++ b/frontend/src/app/main/ui/ds/product/input_with_meta.scss @@ -15,6 +15,7 @@ --input-meta-background: var(--color-background-tertiary); @include t.use-typography("body-small"); + border-radius: $br-8; background-color: var(--input-meta-background); padding: var(--sp-s); @@ -28,6 +29,7 @@ &:hover { --input-meta-background: var(--color-background-quaternary); + cursor: text; } } diff --git a/frontend/src/app/main/ui/ds/product/loader.scss b/frontend/src/app/main/ui/ds/product/loader.scss index 772049d573..f552bad7be 100644 --- a/frontend/src/app/main/ui/ds/product/loader.scss +++ b/frontend/src/app/main/ui/ds/product/loader.scss @@ -78,7 +78,7 @@ } .loader { - fill: currentColor; + fill: currentcolor; width: var(--icon-width); } diff --git a/frontend/src/app/main/ui/ds/product/milestone.scss b/frontend/src/app/main/ui/ds/product/milestone.scss index 5e276d23a0..6a60a52804 100644 --- a/frontend/src/app/main/ui/ds/product/milestone.scss +++ b/frontend/src/app/main/ui/ds/product/milestone.scss @@ -11,19 +11,13 @@ .milestone { border: $b-1 solid var(--border-color, transparent); border-radius: $br-8; - background: var(--color-background-primary); - display: grid; - grid-template-areas: - "avatar name button" - "avatar content button"; - grid-template-rows: auto 1fr; - grid-template-columns: calc(var(--sp-xxl) + var(--sp-l)) 1fr auto; - + grid-template: + "avatar name button" auto "avatar content button" 1fr / calc(var(--sp-xxl) + var(--sp-l)) + 1fr auto; padding: var(--sp-s) 0; align-items: center; - column-gap: var(--sp-s); &.is-selected, @@ -60,6 +54,7 @@ .date { @include t.use-typography("body-small"); + grid-area: content; color: var(--color-foreground-secondary); } diff --git a/frontend/src/app/main/ui/ds/product/milestone_group.scss b/frontend/src/app/main/ui/ds/product/milestone_group.scss index 43c71ce334..0903a24921 100644 --- a/frontend/src/app/main/ui/ds/product/milestone_group.scss +++ b/frontend/src/app/main/ui/ds/product/milestone_group.scss @@ -11,19 +11,13 @@ .milestone { border: $b-1 solid var(--border-color, transparent); border-radius: $br-8; - background: var(--color-background-primary); - display: grid; - grid-template-areas: - "avatar name button" - "avatar content button"; - grid-template-rows: auto 1fr; - grid-template-columns: calc(var(--sp-xxl) + var(--sp-l)) 1fr auto; - + grid-template: + "avatar name button" auto "avatar content button" 1fr / calc(var(--sp-xxl) + var(--sp-l)) + 1fr auto; padding: var(--sp-s) 0; align-items: center; - column-gap: var(--sp-s); &.is-selected, @@ -39,12 +33,14 @@ .name { @include t.use-typography("body-small"); + grid-area: name; color: var(--color-foreground-primary); } .toggle-message { @include t.use-typography("body-small"); + grid-area: name; } @@ -95,6 +91,7 @@ &:hover { color: var(--color-accent-primary); + --icon-stroke-color: var(--color-accent-primary); } } diff --git a/frontend/src/app/main/ui/ds/product/panel_title.scss b/frontend/src/app/main/ui/ds/product/panel_title.scss index e0419221e4..fcb59ea43b 100644 --- a/frontend/src/app/main/ui/ds/product/panel_title.scss +++ b/frontend/src/app/main/ui/ds/product/panel_title.scss @@ -19,6 +19,7 @@ .panel-title-text { @include t.use-typography("headline-small"); + flex-grow: 1; text-align: center; color: var(--color-foreground-primary); diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.scss b/frontend/src/app/main/ui/ds/tooltip/tooltip.scss index 79fe80f774..dcb01cb95a 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.scss +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.scss @@ -55,6 +55,7 @@ $arrow-side: 12px; "arrow" "content"; } + .tooltip-bottom .tooltip-arrow { justify-self: center; border-radius: var(--sp-xs) 0; @@ -111,7 +112,7 @@ $arrow-side: 12px; } .tooltip-bottom-right .tooltip-arrow { - margin: 0px var(--sp-s); + margin: 0 var(--sp-s); transform: rotate(45deg) translateX(var(--sp-s)); border-radius: var(--sp-xs) 0; border-block-start: $b-1 solid var(--color-accent-primary-muted); @@ -123,6 +124,7 @@ $arrow-side: 12px; "arrow" "content"; } + .tooltip-bottom-left .tooltip-arrow { justify-self: end; margin: 0 var(--sp-s); @@ -137,6 +139,7 @@ $arrow-side: 12px; "content" "arrow"; } + .tooltip-top-left .tooltip-arrow { margin: 0 var(--sp-s); justify-self: end; @@ -148,6 +151,7 @@ $arrow-side: 12px; .tooltip-content { @include t.use-typography("body-small"); + background-color: var(--color-background-primary); color: var(--color-foreground-secondary); border-radius: var(--sp-xs); diff --git a/frontend/src/app/main/ui/ds/typography.scss b/frontend/src/app/main/ui/ds/typography.scss index 6ca2fd6670..7f27419cd0 100644 --- a/frontend/src/app/main/ui/ds/typography.scss +++ b/frontend/src/app/main/ui/ds/typography.scss @@ -8,11 +8,9 @@ $_font-weight-regular: 400; $_font-weight-medium: 500; - $_font-lineheight-dense: 1.2; $_font-lineheight-compact: 1.3; $_font-lineheight-normal: 1.4; - $_fs-12: px2rem(12); $_fs-14: px2rem(14); $_fs-16: px2rem(16); @@ -22,7 +20,7 @@ $_fs-24: px2rem(24); $_fs-36: px2rem(36); @mixin _font-style-display { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -30,7 +28,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-title-large { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -38,7 +36,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-title-medium { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -46,7 +44,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-title-small { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-dense; @@ -54,7 +52,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-headline-large { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -63,7 +61,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-headline-medium { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -72,7 +70,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-headline-small { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-medium; line-height: $_font-lineheight-dense; @@ -81,7 +79,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-body-large { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -89,7 +87,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-body-medium { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -97,7 +95,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-body-small { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-compact; diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.scss b/frontend/src/app/main/ui/ds/utilities/swatch.scss index a9eb3b6936..9daa4d9141 100644 --- a/frontend/src/app/main/ui/ds/utilities/swatch.scss +++ b/frontend/src/app/main/ui/ds/utilities/swatch.scss @@ -11,7 +11,7 @@ @property --solid-color-overlay { syntax: ""; inherits: false; - initial-value: rgba(0, 0, 0, 0); + initial-value: rgb(0 0 0 / 0%); } .swatch { @@ -19,13 +19,13 @@ --border-radius: #{$br-4}; --border-color-active: var(--color-foreground-primary); --border-color-active-inset: var(--color-background-primary); - --checkerboard-background: repeating-conic-gradient(lightgray 0% 25%, white 0% 50%); --checkerboard-size: 0.5rem 0.5rem; border: $b-1 solid var(--border-color); border-radius: var(--border-radius); overflow: hidden; + &:focus-visible { --border-color: var(--color-accent-primary); } @@ -80,6 +80,7 @@ &:hover { --border-color: var(--color-accent-primary-muted); + border-width: $b-2; } } @@ -114,7 +115,6 @@ /* solid‑colour overlay */ /* checkerboard pattern */ linear-gradient(var(--solid-color-overlay), var(--solid-color-overlay)), var(--checkerboard-background); - background-size: cover, var(--checkerboard-size); background-position: center, center; background-repeat: no-repeat, repeat; diff --git a/frontend/src/app/main/ui/exports/assets.scss b/frontend/src/app/main/ui/exports/assets.scss index 8bc4737a20..4a8b358930 100644 --- a/frontend/src/app/main/ui/exports/assets.scss +++ b/frontend/src/app/main/ui/exports/assets.scss @@ -9,6 +9,7 @@ // PROGRESS WIDGET .export-progress-widget { @include deprecated.flexCenter; + width: deprecated.$s-28; height: deprecated.$s-28; } @@ -19,6 +20,7 @@ --export-modal-fg-color: var(--alert-text-foreground-color-default); --export-modal-icon-color: var(--alert-icon-foreground-color-default); --export-modal-border-color: var(--alert-border-color-default); + position: absolute; right: deprecated.$s-16; top: deprecated.$s-48; @@ -41,13 +43,15 @@ --export-modal-fg-color: var(--alert-text-foreground-color-error); --export-modal-icon-color: var(--alert-icon-foreground-color-error); --export-modal-border-color: var(--alert-border-color-error); + grid-template-areas: "icon text close"; gap: deprecated.$s-8; padding-block: deprecated.$s-8; } .icon { - @extend .button-icon; + @extend %button-icon; + grid-area: icon; align-self: center; margin-inline-start: deprecated.$s-8; @@ -56,6 +60,7 @@ .export-progress-title { @include deprecated.bodyMediumTypography; + display: grid; grid-template-columns: auto 1fr; gap: deprecated.$s-8; @@ -68,6 +73,7 @@ .progress { @include deprecated.bodyMediumTypography; + padding-left: deprecated.$s-8; margin: 0; align-self: center; @@ -77,6 +83,7 @@ .retry-btn { @include deprecated.buttonStyle; @include deprecated.bodySmallTypography; + display: inline; text-align: left; color: var(--modal-link-foreground-color); @@ -86,12 +93,14 @@ .progress-close-button { @include deprecated.buttonStyle; + padding: 0; margin-inline-end: deprecated.$s-8; } .close-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--export-modal-icon-color); } @@ -102,14 +111,16 @@ // EXPORT MODAL .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; + &.transparent { background-color: transparent; } } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; + max-height: calc(10 * deprecated.$s-80); } @@ -119,75 +130,95 @@ .modal-title { @include deprecated.headlineMediumTypography; + color: var(--modal-title-foreground-color); } .modal-close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content, .no-selection { @include deprecated.bodySmallTypography; + margin-bottom: deprecated.$s-24; + .modal-link { @include deprecated.bodyLargeTypography; + text-decoration: none; cursor: pointer; color: var(--modal-link-foreground-color); } + .selection-header { @include deprecated.flexRow; + height: deprecated.$s-32; margin-bottom: deprecated.$s-4; + .selection-btn { @include deprecated.buttonStyle; - @extend .input-checkbox; + @extend %input-checkbox; @include deprecated.flexCenter; + height: deprecated.$s-24; width: deprecated.$s-24; padding: 0; margin-left: deprecated.$s-16; + span { - @extend .checkbox-icon; + @extend %checkbox-icon; } } + .selection-title { @include deprecated.bodyLargeTypography; + color: var(--modal-text-foreground-color); } } + .selection-wrapper { position: relative; width: 100%; height: fit-content; } + .selection-shadow { width: 100%; height: 100%; - &:after { + + &::after { position: absolute; bottom: 0; left: 0; width: 100%; height: 50px; - background: linear-gradient(to top, rgba(24, 24, 26, 1) 0%, rgba(24, 24, 26, 0) 100%); + background: linear-gradient(to top, rgb(24 24 26 / 100%) 0%, rgb(24 24 26 / 0%) 100%); content: ""; pointer-events: none; } } + .selection-list { @include deprecated.flexColumn; + max-height: deprecated.$s-400; overflow-y: auto; padding-bottom: deprecated.$s-12; + .selection-row { @include deprecated.flexRow; + background-color: var(--entry-background-color); min-height: deprecated.$s-40; border-radius: deprecated.$br-8; + .selection-btn { @include deprecated.buttonStyle; + display: grid; grid-template-columns: min-content auto 1fr auto auto; align-items: center; @@ -195,45 +226,57 @@ height: 10%; gap: deprecated.$s-8; padding: 0 deprecated.$s-16; + .checkbox-wrapper { - @extend .input-checkbox; + @extend %input-checkbox; @include deprecated.flexCenter; + height: deprecated.$s-24; width: deprecated.$s-24; padding: 0; + .checkobox-tick { - @extend .checkbox-icon; + @extend %checkbox-icon; } } + .selection-name { @include deprecated.bodyLargeTypography; @include deprecated.textEllipsis; + flex-grow: 1; color: var(--modal-text-foreground-color); text-align: start; } + .selection-scale { @include deprecated.bodyLargeTypography; @include deprecated.textEllipsis; + min-width: deprecated.$s-108; padding: deprecated.$s-12; color: var(--modal-text-foreground-color); } + .selection-extension { @include deprecated.bodyLargeTypography; @include deprecated.textEllipsis; + min-width: deprecated.$s-72; padding: deprecated.$s-12; color: var(--modal-text-foreground-color); } } + .image-wrapper { @include deprecated.flexCenter; + min-height: deprecated.$s-32; min-width: deprecated.$s-32; background-color: var(--app-white); border-radius: deprecated.$br-6; margin: auto 0; + img, svg { object-fit: contain; @@ -245,15 +288,18 @@ } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; } + .cancel-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } + .accept-btn { - @extend .modal-accept-btn; + @extend %modal-accept-btn; + &.danger { - @extend .modal-danger-btn; + @extend %modal-danger-btn; } } @@ -261,20 +307,26 @@ .modal-subtitle, .modal-msg { @include deprecated.bodyLargeTypography; + color: var(--modal-text-foreground-color); } .export-option { - @extend .input-checkbox; + @extend %input-checkbox; + width: 100%; align-items: flex-start; + label { align-items: flex-start; + .modal-subtitle { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } } + span { margin-top: deprecated.$s-8; } @@ -288,37 +340,46 @@ .file-entry { .file-name { @include deprecated.flexRow; + .file-icon { @include deprecated.flexCenter; + height: deprecated.$s-16; width: deprecated.$s-16; svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--input-foreground); } } + .file-name-label { @include deprecated.bodyLargeTypography; @include deprecated.textEllipsis; } } + &.loading { .file-name { color: var(--modal-text-foreground-color); } } + &.error { .file-name { color: var(--modal-text-foreground-color); + .file-icon svg { stroke: var(--modal-text-foreground-color); } } } + &.success { .file-name { color: var(--modal-text-foreground-color); + .file-icon svg { stroke: var(--modal-text-foreground-color); } diff --git a/frontend/src/app/main/ui/exports/files.scss b/frontend/src/app/main/ui/exports/files.scss index d6055ed184..9a149abc4c 100644 --- a/frontend/src/app/main/ui/exports/files.scss +++ b/frontend/src/app/main/ui/exports/files.scss @@ -8,14 +8,16 @@ // EXPORT MODAL .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; + &.transparent { background-color: transparent; } } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; + max-height: calc(10 * deprecated.$s-80); } @@ -25,74 +27,94 @@ .modal-title { @include deprecated.headlineMediumTypography; + color: var(--modal-title-foreground-color); } .modal-close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content { @include deprecated.bodySmallTypography; + margin-bottom: deprecated.$s-24; + .modal-link { @include deprecated.bodyLargeTypography; + text-decoration: none; cursor: pointer; color: var(--modal-link-foreground-color); } + .selection-header { @include deprecated.flexRow; + height: deprecated.$s-32; margin-bottom: deprecated.$s-4; + .selection-btn { @include deprecated.buttonStyle; - @extend .input-checkbox; + @extend %input-checkbox; @include deprecated.flexCenter; + height: deprecated.$s-24; width: deprecated.$s-24; padding: 0; margin-left: deprecated.$s-16; + span { - @extend .checkbox-icon; + @extend %checkbox-icon; } } + .selection-title { @include deprecated.bodyLargeTypography; + color: var(--modal-text-foreground-color); } } + .selection-wrapper { position: relative; width: 100%; height: fit-content; } + .selection-shadow { width: 100%; height: 100%; - &:after { + + &::after { position: absolute; bottom: 0; left: 0; width: 100%; height: 50px; - background: linear-gradient(to top, rgba(24, 24, 26, 1) 0%, rgba(24, 24, 26, 0) 100%); + background: linear-gradient(to top, rgb(24 24 26 / 100%) 0%, rgb(24 24 26 / 0%) 100%); content: ""; pointer-events: none; } } + .selection-list { @include deprecated.flexColumn; + max-height: deprecated.$s-400; overflow-y: auto; padding-bottom: deprecated.$s-12; + .selection-row { @include deprecated.flexRow; + background-color: var(--entry-background-color); min-height: deprecated.$s-40; border-radius: deprecated.$br-8; + .selection-btn { @include deprecated.buttonStyle; + display: grid; grid-template-columns: min-content auto 1fr auto auto; align-items: center; @@ -100,45 +122,57 @@ height: 10%; gap: deprecated.$s-8; padding: 0 deprecated.$s-16; + .checkbox-wrapper { - @extend .input-checkbox; + @extend %input-checkbox; @include deprecated.flexCenter; + height: deprecated.$s-24; width: deprecated.$s-24; padding: 0; + .checkobox-tick { - @extend .checkbox-icon; + @extend %checkbox-icon; } } + .selection-name { @include deprecated.bodyLargeTypography; @include deprecated.textEllipsis; + flex-grow: 1; color: var(--modal-text-foreground-color); text-align: start; } + .selection-scale { @include deprecated.bodyLargeTypography; @include deprecated.textEllipsis; + min-width: deprecated.$s-108; padding: deprecated.$s-12; color: var(--modal-text-foreground-color); } + .selection-extension { @include deprecated.bodyLargeTypography; @include deprecated.textEllipsis; + min-width: deprecated.$s-72; padding: deprecated.$s-12; color: var(--modal-text-foreground-color); } } + .image-wrapper { @include deprecated.flexCenter; + min-height: deprecated.$s-32; min-width: deprecated.$s-32; background-color: var(--app-white); border-radius: deprecated.$br-6; margin: auto 0; + img, svg { object-fit: contain; @@ -150,15 +184,18 @@ } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; } + .cancel-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } + .accept-btn { - @extend .modal-accept-btn; + @extend %modal-accept-btn; + &.danger { - @extend .modal-danger-btn; + @extend %modal-danger-btn; } } @@ -166,21 +203,27 @@ .modal-subtitle, .modal-msg { @include deprecated.bodyLargeTypography; + color: var(--modal-text-foreground-color); } .export-option { - @extend .input-checkbox; + @extend %input-checkbox; + width: 100%; align-items: flex-start; + label { align-items: flex-start; + .modal-subtitle { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); padding: 0.25rem 0; } } + span { margin-top: deprecated.$s-8; } @@ -197,35 +240,43 @@ .file-icon { @include deprecated.flexCenter; + height: deprecated.$s-16; width: deprecated.$s-16; svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--input-foreground); } } + .file-name-label { @include deprecated.bodyLargeTypography; @include deprecated.textEllipsis; } } + &.loading { .file-name { color: var(--modal-text-foreground-color); } } + &.error { .file-name { color: var(--modal-text-foreground-color); + .file-icon svg { stroke: var(--modal-text-foreground-color); } } } + &.success { .file-name { color: var(--modal-text-foreground-color); + .file-icon svg { stroke: var(--modal-text-foreground-color); } diff --git a/frontend/src/app/main/ui/inspect/annotation.scss b/frontend/src/app/main/ui/inspect/annotation.scss index 431754d330..863d3f6ede 100644 --- a/frontend/src/app/main/ui/inspect/annotation.scss +++ b/frontend/src/app/main/ui/inspect/annotation.scss @@ -11,11 +11,12 @@ } .title-spacing-annotation { - @extend .attr-title; + @extend %attr-title; } .annotation-content { @include deprecated.bodySmallTypography; + color: var(--entry-foreground-color); } diff --git a/frontend/src/app/main/ui/inspect/attributes.scss b/frontend/src/app/main/ui/inspect/attributes.scss index 4eafa389eb..2fbcbd3e06 100644 --- a/frontend/src/app/main/ui/inspect/attributes.scss +++ b/frontend/src/app/main/ui/inspect/attributes.scss @@ -15,8 +15,7 @@ max-height: calc(100vh - px2rem(128)); // TODO: Fix this hardcoded value padding-top: var(--sp-s); padding-inline: var(--sp-m); - overflow-y: auto; - overflow-x: hidden; + overflow: hidden auto; scrollbar-gutter: stable; background-color: var(--low-emphasis-background); } diff --git a/frontend/src/app/main/ui/inspect/attributes/blur.scss b/frontend/src/app/main/ui/inspect/attributes/blur.scss index 9ae8c464eb..240b8f8c20 100644 --- a/frontend/src/app/main/ui/inspect/attributes/blur.scss +++ b/frontend/src/app/main/ui/inspect/attributes/blur.scss @@ -10,6 +10,7 @@ .attributes-block { --box-border-color: var(--color-background-primary); + display: flex; flex-direction: column; border-block-end: $b-2 solid var(--box-border-color); @@ -25,12 +26,13 @@ } .blur-row { - @extend .attr-row; + @extend %attr-row; + block-size: $sz-36; } .button-children { - @extend .copy-button-children; + @extend %copy-button-children; } .copy-btn-title { diff --git a/frontend/src/app/main/ui/inspect/attributes/common.scss b/frontend/src/app/main/ui/inspect/attributes/common.scss index 79d7b9a410..569af36772 100644 --- a/frontend/src/app/main/ui/inspect/attributes/common.scss +++ b/frontend/src/app/main/ui/inspect/attributes/common.scss @@ -5,7 +5,6 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; - @use "ds/_utils.scss" as *; @use "ds/_sizes.scss" as *; @use "ds/_borders.scss" as *; @@ -24,7 +23,7 @@ } .attributes-color-row { - @extend .attr-row; + @extend %attr-row; } .bullet-wrapper { @@ -41,6 +40,7 @@ .image-format { @include use-typography("headline-small"); + block-size: $sz-32; padding: var(--sp-s) 0; color: var(--color-foreground-secondary); @@ -56,6 +56,7 @@ .format-info { @include use-typography("body-small"); + padding-left: var(--sp-xxs); color: var(--color-foreground-secondary); } @@ -66,10 +67,12 @@ gap: var(--sp-xs); flex-grow: 1; max-inline-size: px2rem(144); + button { visibility: hidden; min-inline-size: px2rem(28); } + &:hover button { visibility: visible; } @@ -87,6 +90,7 @@ .color-name-wrapper { @include use-typography("body-small"); + display: flex; flex-direction: column; gap: var(--sp-xs); @@ -109,7 +113,8 @@ .color-value-wrapper { @include use-typography("body-small"); - @include textEllipsis; + @include text-ellipsis; + color: var(--menu-foreground-color); text-transform: uppercase; } @@ -120,6 +125,7 @@ .opacity-info { @include use-typography("body-small"); + color: var(--menu-foreground-color); text-transform: uppercase; inline-size: 100%; @@ -127,7 +133,6 @@ .second-row { min-block-size: $sz-16; - padding-right: var(--sp-s); inline-size: 100%; text-align: left; margin: 0; @@ -136,8 +141,9 @@ .color-name-library { @include use-typography("body-small"); + color: var(--color-foreground-secondary); - word-break: break-word; + overflow-wrap: break-word; } .image-download { diff --git a/frontend/src/app/main/ui/inspect/attributes/fill.scss b/frontend/src/app/main/ui/inspect/attributes/fill.scss index 3cede83d81..bb6fa49a90 100644 --- a/frontend/src/app/main/ui/inspect/attributes/fill.scss +++ b/frontend/src/app/main/ui/inspect/attributes/fill.scss @@ -10,6 +10,7 @@ .attributes-block { --box-border-color: var(--color-background-primary); + display: flex; flex-direction: column; border-block-end: $b-2 solid var(--box-border-color); diff --git a/frontend/src/app/main/ui/inspect/attributes/geometry.scss b/frontend/src/app/main/ui/inspect/attributes/geometry.scss index f1a90db1e3..40777a76d7 100644 --- a/frontend/src/app/main/ui/inspect/attributes/geometry.scss +++ b/frontend/src/app/main/ui/inspect/attributes/geometry.scss @@ -10,6 +10,7 @@ .attributes-block { --box-border-color: var(--color-background-primary); + display: flex; flex-direction: column; border-block-end: $b-2 solid var(--box-border-color); @@ -25,12 +26,13 @@ } .geometry-row { - @extend .attr-row; + @extend %attr-row; + block-size: $sz-36; } .button-children { - @extend .copy-button-children; + @extend %copy-button-children; } .copy-btn-title { diff --git a/frontend/src/app/main/ui/inspect/attributes/layout.scss b/frontend/src/app/main/ui/inspect/attributes/layout.scss index 2164e152fc..4918983aba 100644 --- a/frontend/src/app/main/ui/inspect/attributes/layout.scss +++ b/frontend/src/app/main/ui/inspect/attributes/layout.scss @@ -10,6 +10,7 @@ .attributes-block { --box-border-color: var(--color-background-primary); + display: flex; flex-direction: column; border-block-end: $b-2 solid var(--box-border-color); @@ -25,12 +26,13 @@ } .layout-row { - @extend .attr-row; + @extend %attr-row; + block-size: $sz-36; } .button-children { - @extend .copy-button-children; + @extend %copy-button-children; } .copy-btn-title { diff --git a/frontend/src/app/main/ui/inspect/attributes/layout_element.scss b/frontend/src/app/main/ui/inspect/attributes/layout_element.scss index a51009ab53..0e4cc28285 100644 --- a/frontend/src/app/main/ui/inspect/attributes/layout_element.scss +++ b/frontend/src/app/main/ui/inspect/attributes/layout_element.scss @@ -10,6 +10,7 @@ .attributes-block { --box-border-color: var(--color-background-primary); + display: flex; flex-direction: column; border-block-end: $b-2 solid var(--box-border-color); @@ -25,15 +26,15 @@ } .layout-element-row { - @extend .attr-row; + @extend %attr-row; + block-size: $sz-36; } .button-children { - @extend .copy-button-children; + @extend %copy-button-children; } .copy-btn-title { max-inline-size: $sz-28; - max-inline-size: $sz-28; } diff --git a/frontend/src/app/main/ui/inspect/attributes/shadow.scss b/frontend/src/app/main/ui/inspect/attributes/shadow.scss index 8cfb86f0c3..d4e04e5b1b 100644 --- a/frontend/src/app/main/ui/inspect/attributes/shadow.scss +++ b/frontend/src/app/main/ui/inspect/attributes/shadow.scss @@ -10,6 +10,7 @@ .attributes-block { --box-border-color: var(--color-background-primary); + display: flex; flex-direction: column; border-block-end: $b-2 solid var(--box-border-color); @@ -25,10 +26,11 @@ } .shadow-row { - @extend .attr-row; + @extend %attr-row; + block-size: $sz-36; } .button-children { - @extend .copy-button-children; + @extend %copy-button-children; } diff --git a/frontend/src/app/main/ui/inspect/attributes/stroke.scss b/frontend/src/app/main/ui/inspect/attributes/stroke.scss index dd5bf8d4b4..70bd2a5ef5 100644 --- a/frontend/src/app/main/ui/inspect/attributes/stroke.scss +++ b/frontend/src/app/main/ui/inspect/attributes/stroke.scss @@ -10,6 +10,7 @@ .attributes-block { --box-border-color: var(--color-background-primary); + display: flex; flex-direction: column; border-block-end: $b-2 solid var(--box-border-color); @@ -31,12 +32,13 @@ } .stroke-row { - @extend .attr-row; + @extend %attr-row; + block-size: $sz-36; } .button-children { - @extend .copy-button-children; + @extend %copy-button-children; } .attributes-content { diff --git a/frontend/src/app/main/ui/inspect/attributes/svg.scss b/frontend/src/app/main/ui/inspect/attributes/svg.scss index 1b7495e61d..dd50046496 100644 --- a/frontend/src/app/main/ui/inspect/attributes/svg.scss +++ b/frontend/src/app/main/ui/inspect/attributes/svg.scss @@ -11,6 +11,7 @@ .attributes-block { --box-border-color: var(--color-background-primary); + display: flex; flex-direction: column; border-block-end: $b-2 solid var(--box-border-color); @@ -26,24 +27,28 @@ } .svg-row { - @extend .attr-row; + @extend %attr-row; + block-size: $sz-36; } .button-children { - @extend .copy-button-children; + @extend %copy-button-children; } .attributes-subtitle { @include use-typography("headline-small"); + display: flex; justify-content: space-between; block-size: $sz-32; + span { block-size: $sz-32; display: flex; align-items: center; } + button { display: none; } diff --git a/frontend/src/app/main/ui/inspect/attributes/text.scss b/frontend/src/app/main/ui/inspect/attributes/text.scss index 9f3ecf1808..6a81b7bcd0 100644 --- a/frontend/src/app/main/ui/inspect/attributes/text.scss +++ b/frontend/src/app/main/ui/inspect/attributes/text.scss @@ -12,6 +12,7 @@ .attributes-block { --box-border-color: var(--color-background-primary); + display: flex; flex-direction: column; border-block-end: $b-2 solid var(--box-border-color); @@ -33,16 +34,18 @@ } .text-row { - @extend .attr-row; + @extend %attr-row; + block-size: unset; min-block-size: $sz-36; + :global(.attr-value) { align-items: center; } } .button-children { - @extend .copy-button-children; + @extend %copy-button-children; } .attributes-content-row { @@ -51,8 +54,10 @@ border-radius: $br-8; border: $b-1 solid var(--menu-border-color-disabled); margin-block-start: var(--sp-xs); + .content { @include use-typography("body-small"); + width: 100%; padding: var(--sp-xs) 0; color: var(--color-foreground-secondary); @@ -61,6 +66,7 @@ &:hover { border: $b-1 solid var(--color-background-tertiary); background-color: var(--menu-background-color); + .content { color: var(--menu-foreground-color-hover); } diff --git a/frontend/src/app/main/ui/inspect/attributes/variant.scss b/frontend/src/app/main/ui/inspect/attributes/variant.scss index 3d0df70402..050826a5db 100644 --- a/frontend/src/app/main/ui/inspect/attributes/variant.scss +++ b/frontend/src/app/main/ui/inspect/attributes/variant.scss @@ -10,6 +10,7 @@ .attributes-block { --box-border-color: var(--color-background-primary); + display: flex; flex-direction: column; border-block-end: $b-2 solid var(--box-border-color); @@ -25,12 +26,14 @@ } .variant-row { - @extend .attr-row; + @extend %attr-row; + block-size: fit-content; min-block-size: $sz-36; } .button-children { - @extend .copy-button-children; - word-break: break-word; + @extend %copy-button-children; + + overflow-wrap: break-word; } diff --git a/frontend/src/app/main/ui/inspect/attributes/visibility.scss b/frontend/src/app/main/ui/inspect/attributes/visibility.scss index c888735ff1..a4b20d8700 100644 --- a/frontend/src/app/main/ui/inspect/attributes/visibility.scss +++ b/frontend/src/app/main/ui/inspect/attributes/visibility.scss @@ -10,6 +10,7 @@ .attributes-block { --box-border-color: var(--color-background-primary); + display: flex; flex-direction: column; border-block-end: $b-2 solid var(--box-border-color); @@ -25,12 +26,13 @@ } .visibility-row { - @extend .attr-row; + @extend %attr-row; + block-size: $sz-36; } .button-children { - @extend .copy-button-children; + @extend %copy-button-children; } .copy-btn-title { diff --git a/frontend/src/app/main/ui/inspect/code.scss b/frontend/src/app/main/ui/inspect/code.scss index 7f88871edc..bf9bd04168 100644 --- a/frontend/src/app/main/ui/inspect/code.scss +++ b/frontend/src/app/main/ui/inspect/code.scss @@ -13,7 +13,6 @@ overflow: hidden; padding-bottom: deprecated.$s-16; overflow-y: auto; - overflow-x: hidden; padding-inline: var(--sp-m); } @@ -22,8 +21,9 @@ } .download-button { - @extend .button-secondary; + @extend %button-secondary; @include deprecated.uppercaseTitleTipography; + height: deprecated.$s-32; width: 100%; margin: deprecated.$s-8 0; @@ -31,6 +31,7 @@ .code-block { @include deprecated.codeTypography; + display: flex; flex-direction: column; height: 100%; @@ -63,6 +64,7 @@ .code-lang { @include deprecated.uppercaseTitleTipography; + display: flex; align-items: center; } @@ -76,11 +78,14 @@ .expand-button, .css-copy-btn, .html-copy-btn { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-32; width: deprecated.$s-28; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } } @@ -88,15 +93,19 @@ .code-lang-options { max-width: deprecated.$s-108; } + .code-lang-select { @include deprecated.uppercaseTitleTipography; + width: deprecated.$s-72; border: deprecated.$s-1 solid transparent; background-color: transparent; color: var(--menu-foreground-color-disabled); } + .code-lang-option { @include deprecated.uppercaseTitleTipography; + width: deprecated.$s-72; height: deprecated.$s-32; padding: deprecated.$s-8; @@ -112,31 +121,40 @@ .toggle-btn { @include deprecated.buttonStyle; + display: flex; align-items: center; padding: 0; color: var(--title-foreground-color); stroke: var(--title-foreground-color); + .collapsabled-icon { @include deprecated.flexCenter; + height: deprecated.$s-24; border-radius: deprecated.$br-8; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + transform: rotate(90deg); stroke: var(--icon-foreground); } + &.rotated svg { transform: rotate(0deg); } } + &:hover { color: var(--title-foreground-color-hover); stroke: var(--title-foreground-color-hover); + .title { color: var(--title-foreground-color-hover); stroke: var(--title-foreground-color-hover); } + .collapsabled-icon svg { stroke: var(--title-foreground-color-hover); } diff --git a/frontend/src/app/main/ui/inspect/exports.scss b/frontend/src/app/main/ui/inspect/exports.scss index 4ca98720a8..c3b5318d0e 100644 --- a/frontend/src/app/main/ui/inspect/exports.scss +++ b/frontend/src/app/main/ui/inspect/exports.scss @@ -23,27 +23,32 @@ } .add-export { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-32; width: deprecated.$s-28; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } } .element-set-content { @include deprecated.flexColumn; + margin-bottom: deprecated.$s-4; } .multiple-exports { @include deprecated.flexRow; + grid-column: 1 / span 9; } .label { - @extend .mixed-bar; + @extend %mixed-bar; } .actions { @@ -54,12 +59,15 @@ display: grid; grid-template-columns: repeat(9, 1fr); column-gap: deprecated.$s-4; + .action-btn { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-32; width: deprecated.$s-28; + svg { - @extend .button-icon; + @extend %button-icon; } } } @@ -84,6 +92,7 @@ .size-select { grid-column: span 2; padding: 0; + .dropdown-upwards { bottom: deprecated.$s-36; top: unset; @@ -92,14 +101,16 @@ } .suffix-input { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + grid-column: span 3; } .export-btn { - @extend .button-secondary; + @extend %button-secondary; @include deprecated.uppercaseTitleTipography; + height: deprecated.$s-32; width: 100%; } diff --git a/frontend/src/app/main/ui/inspect/right_sidebar.scss b/frontend/src/app/main/ui/inspect/right_sidebar.scss index ca57b53f1c..b0327b4673 100644 --- a/frontend/src/app/main/ui/inspect/right_sidebar.scss +++ b/frontend/src/app/main/ui/inspect/right_sidebar.scss @@ -58,6 +58,7 @@ .layer-title { @include deprecated.bodySmallTypography; @include deprecated.textEllipsis; + block-size: $sz-32; padding: var(--sp-s) 0; color: var(--color-foreground-primary); @@ -71,6 +72,7 @@ .layer-subtitle { @include deprecated.bodySmallTypography; @include deprecated.textEllipsis; + color: var(--assets-item-name-foreground-color-rest); } @@ -97,6 +99,7 @@ .inspect-tab-switcher-label { @include use-typography("body-medium"); + color: var(--color-foreground-primary); flex: 0 1 40%; } diff --git a/frontend/src/app/main/ui/inspect/styles/panels/text.scss b/frontend/src/app/main/ui/inspect/styles/panels/text.scss index 0b1bbdd05c..3c68ea52c9 100644 --- a/frontend/src/app/main/ui/inspect/styles/panels/text.scss +++ b/frontend/src/app/main/ui/inspect/styles/panels/text.scss @@ -8,7 +8,7 @@ .text-content-wrapper { --border-color: var(--color-background-quaternary); - --border-radius: ${$br-8}; + --border-radius: #{$br-8}; border: $b-1 solid var(--border-color); border-radius: var(--border-radius); @@ -16,5 +16,6 @@ .text-content { --detail-color: var(--color-foreground-secondary); + color: var(--detail-color); } diff --git a/frontend/src/app/main/ui/inspect/styles/property_detail_copiable.scss b/frontend/src/app/main/ui/inspect/styles/property_detail_copiable.scss index c1ddecdf4e..23c7fd9b52 100644 --- a/frontend/src/app/main/ui/inspect/styles/property_detail_copiable.scss +++ b/frontend/src/app/main/ui/inspect/styles/property_detail_copiable.scss @@ -46,6 +46,7 @@ .property-detail-copied { --button-border-active: var(--color-accent-tertiary); + border: $b-1 solid var(--button-border-active); } @@ -61,11 +62,13 @@ .property-detail-text { @include use-typography("body-small"); + color: var(--detail-color); } .property-detail-text-token { @include use-typography("code-font"); + --detail-color: var(--color-token-foreground); line-height: 1.4; diff --git a/frontend/src/app/main/ui/inspect/styles/rows/color_properties_row.scss b/frontend/src/app/main/ui/inspect/styles/rows/color_properties_row.scss index d5b8497c5c..44d90d99b1 100644 --- a/frontend/src/app/main/ui/inspect/styles/rows/color_properties_row.scss +++ b/frontend/src/app/main/ui/inspect/styles/rows/color_properties_row.scss @@ -50,6 +50,7 @@ .color-image-preview-wrapper { --image-background: var(--color-background-secondary); + background: var(--image-background); } @@ -69,11 +70,13 @@ .tooltip-token-title { @include use-typography("body-small"); + color: var(--title-color); } .tooltip-token-value { @include use-typography("body-small"); + color: var(--title-value); } diff --git a/frontend/src/app/main/ui/inspect/styles/rows/properties_row.scss b/frontend/src/app/main/ui/inspect/styles/rows/properties_row.scss index 19287bc219..0dace4fcc7 100644 --- a/frontend/src/app/main/ui/inspect/styles/rows/properties_row.scss +++ b/frontend/src/app/main/ui/inspect/styles/rows/properties_row.scss @@ -45,11 +45,13 @@ .tooltip-token-title { @include use-typography("body-small"); + color: var(--title-color); } .tooltip-token-value { @include use-typography("body-small"); + color: var(--title-value); } diff --git a/frontend/src/app/main/ui/inspect/styles/style_box.scss b/frontend/src/app/main/ui/inspect/styles/style_box.scss index a55a6b5fc4..7965049e59 100644 --- a/frontend/src/app/main/ui/inspect/styles/style_box.scss +++ b/frontend/src/app/main/ui/inspect/styles/style_box.scss @@ -25,7 +25,6 @@ padding-block: var(--sp-s); padding-inline: var(--sp-m); background-color: var(--low-emphasis-background); - border-block-end: 2px solid var(--box-border-color); } @@ -39,7 +38,6 @@ display: grid; place-items: center; color: var(--arrow-color); - appearance: none; background: none; padding: 0; @@ -49,6 +47,7 @@ .panel-title { @include use-typography("headline-small"); + flex: 1; color: var(--title-color); padding-block: var(--title-padding); diff --git a/frontend/src/app/main/ui/modal.scss b/frontend/src/app/main/ui/modal.scss index b78ff64bf4..9068cc6eda 100644 --- a/frontend/src/app/main/ui/modal.scss +++ b/frontend/src/app/main/ui/modal.scss @@ -11,5 +11,5 @@ } .modal-wrapper { - @extend .new-scrollbar; + @extend %new-scrollbar; } diff --git a/frontend/src/app/main/ui/nitrate/nitrate_form.scss b/frontend/src/app/main/ui/nitrate/nitrate_form.scss index bb2cfe2475..363297493c 100644 --- a/frontend/src/app/main/ui/nitrate/nitrate_form.scss +++ b/frontend/src/app/main/ui/nitrate/nitrate_form.scss @@ -12,28 +12,31 @@ @use "ds/_utils.scss" as *; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; + z-index: var(--z-index-notifications); } .modal-dialog { - @extend .modal-container-base; + @extend %modal-container-base; + max-block-size: initial; min-inline-size: px2rem(1021); padding: px2rem(80); - @media (max-width: 1024px) { + @media (width <= 1024px) { min-inline-size: px2rem(712); padding: var(--sp-xxxl); } } .close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-title { @include t.use-typography("title-large"); + margin-block-end: var(--sp-xxxl); color: var(--modal-title-foreground-color); display: flex; @@ -79,7 +82,7 @@ block-size: auto; } - @media (max-width: 992px) { + @media (width <= 992px) { display: none; } } @@ -87,6 +90,7 @@ .radio-btns { label { @include t.use-typography("body-large"); + padding: 0; display: flex; align-items: center; diff --git a/frontend/src/app/main/ui/notifications/badge.scss b/frontend/src/app/main/ui/notifications/badge.scss index 99941b8fb4..62fb7ee090 100644 --- a/frontend/src/app/main/ui/notifications/badge.scss +++ b/frontend/src/app/main/ui/notifications/badge.scss @@ -8,9 +8,11 @@ .badge-notification { @include deprecated.smallTitleTipography; + --badge-notification-bg-color: var(--alert-background-color-default); --badge-notification-fg-color: var(--alert-text-foreground-color-default); --badge-notification-border-color: var(--alert-border-color-default); + box-sizing: border-box; display: grid; place-items: center; @@ -30,6 +32,7 @@ .small { @include deprecated.bodySmallTypography; + min-height: deprecated.$s-20; border-radius: deprecated.$br-6; } diff --git a/frontend/src/app/main/ui/notifications/context_notification.scss b/frontend/src/app/main/ui/notifications/context_notification.scss index 1b14e33cea..c455fca32b 100644 --- a/frontend/src/app/main/ui/notifications/context_notification.scss +++ b/frontend/src/app/main/ui/notifications/context_notification.scss @@ -11,6 +11,7 @@ --context-notification-fg-color: var(--alert-text-foreground-color-default); --context-notification-icon-color: var(--alert-icon-foreground-color-default); --context-notification-border-color: var(--alert-border-color-default); + box-sizing: border-box; display: grid; grid-template-columns: deprecated.$s-16 1fr; @@ -60,13 +61,15 @@ } .icon { - @extend .button-icon; + @extend %button-icon; + align-self: self-start; stroke: var(--context-notification-icon-color); } .context-text { @include deprecated.bodySmallTypography; + align-self: center; color: var(--context-notification-fg-color); margin: auto 0; @@ -79,6 +82,7 @@ .link, .contain-html .context-text a { @include deprecated.bodySmallTypography; + align-self: center; display: inline; text-align: left; diff --git a/frontend/src/app/main/ui/onboarding/newsletter.scss b/frontend/src/app/main/ui/onboarding/newsletter.scss index 34abf708bd..cb128fda7a 100644 --- a/frontend/src/app/main/ui/onboarding/newsletter.scss +++ b/frontend/src/app/main/ui/onboarding/newsletter.scss @@ -7,18 +7,18 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; + position: relative; display: grid; grid-template-columns: auto auto; gap: deprecated.$s-32; padding-inline: deprecated.$s-100; - padding-block-start: deprecated.$s-100; - padding-block-end: deprecated.$s-72; + padding-block: deprecated.$s-100 deprecated.$s-72; margin: 0; width: deprecated.$s-960; height: deprecated.$s-632; @@ -29,6 +29,7 @@ .modal-left { width: deprecated.$s-172; margin-block-end: deprecated.$s-64; + img { width: deprecated.$s-172; border-radius: deprecated.$br-8 0 0 deprecated.$br-8; @@ -45,11 +46,13 @@ .modal-title { @include deprecated.bigTitleTipography; + color: var(--modal-title-foreground-color); } .modal-text { @include deprecated.bodyLargeTypography; + color: var(--modal-text-foreground-color); margin: 0; } @@ -61,16 +64,18 @@ } .input-wrapper { - @extend .input-checkbox; + @extend %input-checkbox; } .modal-link { @include deprecated.bodyLargeTypography; + color: var(--modal-link-foreground-color); margin: 0; } .accept-btn { - @extend .modal-accept-btn; + @extend %modal-accept-btn; + justify-self: flex-end; } diff --git a/frontend/src/app/main/ui/onboarding/questions.scss b/frontend/src/app/main/ui/onboarding/questions.scss index 94444a493b..909f08b75a 100644 --- a/frontend/src/app/main/ui/onboarding/questions.scss +++ b/frontend/src/app/main/ui/onboarding/questions.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -15,8 +15,7 @@ max-height: fit-content; width: fit-content; padding-inline: deprecated.$s-100; - padding-block-start: deprecated.$s-40; - padding-block-end: deprecated.$s-72; + padding-block: deprecated.$s-40 deprecated.$s-72; border-radius: deprecated.$br-8; border: deprecated.$s-2 solid var(--modal-border-color); background-color: var(--modal-background-color); @@ -31,20 +30,22 @@ // STEP CONTAINER .paginator { @include deprecated.smallTitleTipography; + height: deprecated.$s-20; text-align: right; color: var(--modal-text-foreground-color); } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; } + .next-button { - @extend .modal-accept-btn; + @extend %modal-accept-btn; } .prev-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } .radio-btns label, @@ -62,6 +63,7 @@ .modal-title { @include deprecated.bigTitleTipography; + color: var(--modal-title-foreground-color); min-height: deprecated.$s-32; margin-block: auto; @@ -69,6 +71,7 @@ .modal-subtitle { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); margin: 0; padding: 0; @@ -76,6 +79,7 @@ .modal-text { @include deprecated.bodyLargeTypography; + color: var(--modal-text-foreground-color); margin: 0; } @@ -88,6 +92,7 @@ max-width: deprecated.$s-540; width: deprecated.$s-540; } + .step-2 { grid-template-rows: deprecated.$s-20 auto auto deprecated.$s-32; } @@ -121,8 +126,7 @@ display: grid; grid-template-rows: 1fr 1fr; grid-template-columns: deprecated.$s-92 deprecated.$s-92 deprecated.$s-92; - row-gap: deprecated.$s-16; - column-gap: deprecated.$s-24; + gap: deprecated.$s-16 deprecated.$s-24; justify-content: center; } diff --git a/frontend/src/app/main/ui/onboarding/team_choice.scss b/frontend/src/app/main/ui/onboarding/team_choice.scss index 067a1f1346..ade731846f 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.scss +++ b/frontend/src/app/main/ui/onboarding/team_choice.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -16,8 +16,7 @@ max-height: deprecated.$s-800; height: 100%; padding-inline: deprecated.$s-100; - padding-block-start: deprecated.$s-40; - padding-block-end: deprecated.$s-40; + padding-block: deprecated.$s-40 deprecated.$s-40; border-radius: deprecated.$br-8; background-color: var(--modal-background-color); border: deprecated.$s-2 solid var(--modal-border-color); @@ -36,6 +35,7 @@ .paginator { @include deprecated.bodySmallTypography; + position: absolute; top: deprecated.$s-40; right: deprecated.$s-100; @@ -55,11 +55,13 @@ .modal-title { @include deprecated.bigTitleTipography; + color: var(--modal-title-foreground-color); } .modal-subtitle { @include deprecated.medTitleTipography; + color: var(--modal-title-foreground-color); } @@ -69,50 +71,58 @@ .modal-text { @include deprecated.bodyLargeTypography; + color: var(--modal-text-foreground-color); margin: 0; } .modal-desc { @include deprecated.smallTitleTipography; + margin: 0; color: var(--modal-title-foreground-color); } .team-features { @include deprecated.flexColumn; + gap: deprecated.$s-16; margin: 0; } .feature { @include deprecated.flexRow; + gap: deprecated.$s-16; } .icon { @include deprecated.flexCenter; + height: deprecated.$s-32; width: deprecated.$s-32; border-radius: deprecated.$br-circle; border: deprecated.$s-1 solid var(--color-accent-primary); + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--color-accent-primary); } } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; + justify-content: flex-end; } .accept-button { - @extend .modal-accept-btn; + @extend %modal-accept-btn; } .back-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } // SEPARATOR @@ -120,7 +130,7 @@ width: deprecated.$s-8; height: 100%; border-radius: deprecated.$br-8; - opacity: 42%; + opacity: 0.42; background-color: var(--modal-separator-background-color); } @@ -141,6 +151,7 @@ .first-block, .second-block { @include deprecated.flexColumn; + gap: deprecated.$s-16; } @@ -151,10 +162,12 @@ } .team-name-input { - @extend .input-element-label; + @extend %input-element-label; + label { @include deprecated.flexColumn; @include deprecated.bodySmallTypography; + align-items: flex-start; width: 100%; border: none; @@ -163,6 +176,7 @@ input { @include deprecated.bodySmallTypography; + margin-top: deprecated.$s-8; } } @@ -188,6 +202,7 @@ .role-title { @include deprecated.uppercaseTitleTipography; + margin-block-end: deprecated.$s-8; color: var(--modal-title-foreground-color); } @@ -199,6 +214,7 @@ .modal-hint { @include deprecated.bodySmallTypography; + color: var(--modal-text-foreground-color); text-align: right; } diff --git a/frontend/src/app/main/ui/releases.scss b/frontend/src/app/main/ui/releases.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/src/app/main/ui/releases/common.scss b/frontend/src/app/main/ui/releases/common.scss index 977411aec5..e3ab396ec1 100644 --- a/frontend/src/app/main/ui/releases/common.scss +++ b/frontend/src/app/main/ui/releases/common.scss @@ -15,8 +15,7 @@ width: fit-content; margin: 0; padding: 0; - align-self: center; - justify-self: flex-start; + place-self: center flex-start; } .dot { diff --git a/frontend/src/app/main/ui/releases/v2_0.scss b/frontend/src/app/main/ui/releases/v2_0.scss index 0d5bc38d2e..77d6a4ced1 100644 --- a/frontend/src/app/main/ui/releases/v2_0.scss +++ b/frontend/src/app/main/ui/releases/v2_0.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -40,6 +40,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -49,6 +50,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -67,17 +69,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -91,7 +96,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_1.scss b/frontend/src/app/main/ui/releases/v2_1.scss index 7b2559bc96..ccf5348282 100644 --- a/frontend/src/app/main/ui/releases/v2_1.scss +++ b/frontend/src/app/main/ui/releases/v2_1.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -40,6 +40,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -49,6 +50,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -61,6 +63,7 @@ .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } @@ -72,7 +75,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_10.scss b/frontend/src/app/main/ui/releases/v2_10.scss index e5d13841eb..68603d9658 100644 --- a/frontend/src/app/main/ui/releases/v2_10.scss +++ b/frontend/src/app/main/ui/releases/v2_10.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -44,6 +44,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -53,6 +54,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -71,17 +73,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -95,7 +100,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_11.scss b/frontend/src/app/main/ui/releases/v2_11.scss index e5d13841eb..68603d9658 100644 --- a/frontend/src/app/main/ui/releases/v2_11.scss +++ b/frontend/src/app/main/ui/releases/v2_11.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -44,6 +44,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -53,6 +54,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -71,17 +73,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -95,7 +100,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_12.scss b/frontend/src/app/main/ui/releases/v2_12.scss index e5d13841eb..68603d9658 100644 --- a/frontend/src/app/main/ui/releases/v2_12.scss +++ b/frontend/src/app/main/ui/releases/v2_12.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -44,6 +44,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -53,6 +54,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -71,17 +73,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -95,7 +100,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_13.scss b/frontend/src/app/main/ui/releases/v2_13.scss index e5d13841eb..68603d9658 100644 --- a/frontend/src/app/main/ui/releases/v2_13.scss +++ b/frontend/src/app/main/ui/releases/v2_13.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -44,6 +44,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -53,6 +54,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -71,17 +73,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -95,7 +100,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_14.scss b/frontend/src/app/main/ui/releases/v2_14.scss index e5d13841eb..68603d9658 100644 --- a/frontend/src/app/main/ui/releases/v2_14.scss +++ b/frontend/src/app/main/ui/releases/v2_14.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -44,6 +44,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -53,6 +54,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -71,17 +73,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -95,7 +100,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_2.scss b/frontend/src/app/main/ui/releases/v2_2.scss index 34d030466f..ede5b103bf 100644 --- a/frontend/src/app/main/ui/releases/v2_2.scss +++ b/frontend/src/app/main/ui/releases/v2_2.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -40,6 +40,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -49,6 +50,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -61,6 +63,7 @@ .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } @@ -72,7 +75,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_3.scss b/frontend/src/app/main/ui/releases/v2_3.scss index e5d13841eb..68603d9658 100644 --- a/frontend/src/app/main/ui/releases/v2_3.scss +++ b/frontend/src/app/main/ui/releases/v2_3.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -44,6 +44,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -53,6 +54,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -71,17 +73,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -95,7 +100,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_4.scss b/frontend/src/app/main/ui/releases/v2_4.scss index e5d13841eb..68603d9658 100644 --- a/frontend/src/app/main/ui/releases/v2_4.scss +++ b/frontend/src/app/main/ui/releases/v2_4.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -44,6 +44,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -53,6 +54,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -71,17 +73,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -95,7 +100,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_5.scss b/frontend/src/app/main/ui/releases/v2_5.scss index e5d13841eb..68603d9658 100644 --- a/frontend/src/app/main/ui/releases/v2_5.scss +++ b/frontend/src/app/main/ui/releases/v2_5.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -44,6 +44,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -53,6 +54,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -71,17 +73,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -95,7 +100,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_6.scss b/frontend/src/app/main/ui/releases/v2_6.scss index e5d13841eb..68603d9658 100644 --- a/frontend/src/app/main/ui/releases/v2_6.scss +++ b/frontend/src/app/main/ui/releases/v2_6.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -44,6 +44,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -53,6 +54,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -71,17 +73,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -95,7 +100,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_7.scss b/frontend/src/app/main/ui/releases/v2_7.scss index e5d13841eb..68603d9658 100644 --- a/frontend/src/app/main/ui/releases/v2_7.scss +++ b/frontend/src/app/main/ui/releases/v2_7.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -44,6 +44,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -53,6 +54,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -71,17 +73,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -95,7 +100,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_8.scss b/frontend/src/app/main/ui/releases/v2_8.scss index e5d13841eb..68603d9658 100644 --- a/frontend/src/app/main/ui/releases/v2_8.scss +++ b/frontend/src/app/main/ui/releases/v2_8.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -44,6 +44,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -53,6 +54,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -71,17 +73,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -95,7 +100,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/releases/v2_9.scss b/frontend/src/app/main/ui/releases/v2_9.scss index e5d13841eb..68603d9658 100644 --- a/frontend/src/app/main/ui/releases/v2_9.scss +++ b/frontend/src/app/main/ui/releases/v2_9.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -44,6 +44,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -53,6 +54,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -71,17 +73,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -95,7 +100,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; diff --git a/frontend/src/app/main/ui/settings.scss b/frontend/src/app/main/ui/settings.scss index 2963138812..4ee10be87e 100644 --- a/frontend/src/app/main/ui/settings.scss +++ b/frontend/src/app/main/ui/settings.scss @@ -33,6 +33,7 @@ &.dashboard-projects { user-select: none; } + &.dashboard-shared { width: calc(100vw - deprecated.$s-320); margin-right: deprecated.$s-52; @@ -48,13 +49,13 @@ width: 100%; justify-content: center; align-items: center; + a { color: var(--color-foreground-secondary); } } .form-container { - width: deprecated.$s-800; margin: deprecated.$s-48 auto deprecated.$s-32 deprecated.$s-120; display: flex; max-width: deprecated.$s-368; @@ -76,6 +77,7 @@ .custom-input, .custom-select { flex-direction: column-reverse; + label { position: relative; text-transform: uppercase; @@ -84,6 +86,7 @@ margin-bottom: deprecated.$s-12; margin-left: calc(-1 * deprecated.$s-4); } + input, select { background-color: var(--color-background-tertiary); @@ -91,20 +94,25 @@ border-color: transparent; color: var(--color-foreground-primary); padding: 0 deprecated.$s-16; + &:focus { outline: deprecated.$s-1 solid var(--color-accent-primary); } + ::placeholder { color: var(--color-foreground-secondary); } } + .help-icon { bottom: deprecated.$s-12; top: auto; + svg { fill: var(--color-foreground-secondary); } } + &.disabled { input { background-color: var(--input-background-color-disabled); @@ -112,30 +120,36 @@ color: var(--color-foreground-secondary); } } + .input-container { background-color: var(--color-background-tertiary); border-radius: deprecated.$s-8; border-color: transparent; margin-top: deprecated.$s-24; + .main-content { label { position: absolute; top: calc(-1 * deprecated.$s-24); } + span { color: var(--color-foreground-primary); } } + &:focus { border: deprecated.$s-1 solid var(--color-accent-primary); } } + textarea { border-radius: deprecated.$s-8; padding: deprecated.$s-12 deprecated.$s-16; background-color: var(--color-background-tertiary); color: var(--color-foreground-primary); border: none; + &:focus { outline: deprecated.$s-1 solid var(--color-accent-primary); } @@ -145,6 +159,7 @@ .field-title { color: var(--color-foreground-primary); } + .field-title:not(:first-child) { margin-top: deprecated.$s-64; } @@ -152,6 +167,7 @@ .field-text { color: var(--color-foreground-secondary); } + button, .btn-secondary { width: 100%; @@ -159,15 +175,18 @@ text-transform: uppercase; background-color: var(--color-background-tertiary); color: var(--color-foreground-primary); + &:hover { color: var(--color-accent-primary); background-color: var(--color-background-quaternary); } } + hr { display: none; } } + .links { margin-top: deprecated.$s-12; } @@ -186,13 +205,13 @@ margin-bottom: deprecated.$s-32; .newsletter-title { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; color: var(--color-foreground-secondary); font-size: deprecated.$fs-14; } label { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; color: var(--color-background-primary); font-size: deprecated.$fs-12; margin-right: calc(-1 * deprecated.$s-16); @@ -200,7 +219,7 @@ } .info { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; color: var(--color-foreground-secondary); font-size: deprecated.$fs-12; margin-bottom: deprecated.$s-8; diff --git a/frontend/src/app/main/ui/settings/change_email.scss b/frontend/src/app/main/ui/settings/change_email.scss index 71900cf9e4..dd4489d99b 100644 --- a/frontend/src/app/main/ui/settings/change_email.scss +++ b/frontend/src/app/main/ui/settings/change_email.scss @@ -7,11 +7,12 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; + min-width: deprecated.$s-408; } @@ -21,16 +22,18 @@ .modal-title { @include deprecated.uppercaseTitleTipography; + color: var(--modal-title-foreground-color); } .modal-close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content { @include deprecated.flexColumn; @include deprecated.bodySmallTypography; + gap: deprecated.$s-24; margin-bottom: deprecated.$s-24; } @@ -41,16 +44,18 @@ .select-title { @include deprecated.bodySmallTypography; + color: var(--modal-title-foreground-color); } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; + button { - @extend .modal-accept-btn; + @extend %modal-accept-btn; } } .cancel-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } diff --git a/frontend/src/app/main/ui/settings/delete_account.scss b/frontend/src/app/main/ui/settings/delete_account.scss index c69d17de53..3f07f30774 100644 --- a/frontend/src/app/main/ui/settings/delete_account.scss +++ b/frontend/src/app/main/ui/settings/delete_account.scss @@ -7,11 +7,12 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; + min-width: deprecated.$s-408; } @@ -21,16 +22,18 @@ .modal-title { @include deprecated.uppercaseTitleTipography; + color: var(--modal-title-foreground-color); } .modal-close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content { @include deprecated.flexColumn; @include deprecated.bodySmallTypography; + gap: deprecated.$s-24; margin-bottom: deprecated.$s-24; } @@ -41,20 +44,22 @@ .select-title { @include deprecated.bodySmallTypography; + color: var(--modal-title-foreground-color); } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; } .cancel-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } .accept-button { - @extend .modal-accept-btn; + @extend %modal-accept-btn; + &.danger { - @extend .modal-danger-btn; + @extend %modal-danger-btn; } } diff --git a/frontend/src/app/main/ui/settings/feedback.scss b/frontend/src/app/main/ui/settings/feedback.scss index ee91182b80..99e449267d 100644 --- a/frontend/src/app/main/ui/settings/feedback.scss +++ b/frontend/src/app/main/ui/settings/feedback.scss @@ -19,6 +19,7 @@ .feedback-description { @include t.use-typography("body-medium"); + border-radius: b.$br-8; padding: var(--sp-m); background-color: var(--color-background-tertiary); @@ -28,6 +29,7 @@ ::placeholder { color: var(--input-placeholder-color); } + &:focus { outline: b.$b-1 solid var(--color-accent-primary); } @@ -35,13 +37,15 @@ .field-label { @include t.use-typography("headline-small"); + block-size: $sz-32; color: var(--color-foreground-primary); margin-block-end: var(--sp-l); } .feedback-button-link { - @extend .button-primary; + @extend %button-primary; + margin-block-end: px2rem(72); } @@ -59,12 +63,14 @@ .link { @include t.use-typography("headline-small"); + color: var(--color-accent-tertiary); margin-block-end: var(--sp-s); } .download-button { @include t.use-typography("body-small"); + color: var(--color-foreground-primary); text-transform: lowercase; border: b.$b-1 solid var(--color-background-quaternary); diff --git a/frontend/src/app/main/ui/settings/integrations.scss b/frontend/src/app/main/ui/settings/integrations.scss index d7be475bb4..e1833c1c6e 100644 --- a/frontend/src/app/main/ui/settings/integrations.scss +++ b/frontend/src/app/main/ui/settings/integrations.scss @@ -5,7 +5,6 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; - @use "ds/_borders.scss" as *; @use "ds/_sizes.scss" as *; @use "ds/mixins.scss" as *; @@ -44,11 +43,12 @@ } .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; + inline-size: $sz-400; max-block-size: fit-content; position: relative; @@ -187,7 +187,8 @@ } .item-title { - @include textEllipsis; + @include text-ellipsis; + align-content: center; block-size: $sz-64; padding: 0 var(--sp-l); @@ -222,6 +223,7 @@ .textarea { @include t.use-typography("body-small"); + border-radius: $br-8; background-color: var(--color-background-tertiary); color: var(--color-foreground-secondary); diff --git a/frontend/src/app/main/ui/settings/notifications.scss b/frontend/src/app/main/ui/settings/notifications.scss index 27a2273536..4aaf1ac096 100644 --- a/frontend/src/app/main/ui/settings/notifications.scss +++ b/frontend/src/app/main/ui/settings/notifications.scss @@ -9,7 +9,9 @@ .update-btn { margin-top: deprecated.$s-16; - @extend .button-primary; + + @extend %button-primary; + height: deprecated.$s-36; } diff --git a/frontend/src/app/main/ui/settings/password.scss b/frontend/src/app/main/ui/settings/password.scss index 5a0551333e..504a6da2e5 100644 --- a/frontend/src/app/main/ui/settings/password.scss +++ b/frontend/src/app/main/ui/settings/password.scss @@ -9,6 +9,8 @@ .update-btn { margin-top: deprecated.$s-16; - @extend .button-primary; + + @extend %button-primary; + height: deprecated.$s-36; } diff --git a/frontend/src/app/main/ui/settings/profile.scss b/frontend/src/app/main/ui/settings/profile.scss index 4e8473b6e8..21249db182 100644 --- a/frontend/src/app/main/ui/settings/profile.scss +++ b/frontend/src/app/main/ui/settings/profile.scss @@ -11,17 +11,16 @@ width: 100%; justify-content: center; align-items: center; - a:not(.button-primary):not(.link) { + + a:not(.button-primary, .link) { color: var(--color-foreground-secondary); } } .form-container { display: flex; - justify-content: center; flex-direction: column; max-width: $s-500; - margin-bottom: $s-32; width: $s-580; margin: $s-80 auto $s-120 auto; justify-content: center; @@ -36,11 +35,13 @@ text-transform: uppercase; background-color: var(--color-background-tertiary); color: var(--color-foreground-primary); + &:hover { color: var(--color-accent-primary); background-color: var(--color-background-quaternary); } } + hr { display: none; } @@ -48,6 +49,7 @@ .fields-row { --input-height: #{$s-40}; + margin-bottom: $s-20; flex-direction: column; @@ -78,6 +80,7 @@ .custom-input, .custom-select { flex-direction: column-reverse; + label { position: relative; text-transform: uppercase; @@ -86,6 +89,7 @@ margin-bottom: $s-12; margin-left: calc(-1 * $s-4); } + input, select { background-color: var(--color-background-tertiary); @@ -93,20 +97,25 @@ border-color: transparent; color: var(--color-foreground-primary); padding: 0 $s-16; + &:focus { outline: $s-1 solid var(--color-accent-primary); } + ::placeholder { color: var(--color-foreground-secondary); } } + .help-icon { bottom: $s-12; top: auto; + svg { fill: var(--color-foreground-secondary); } } + &.disabled { input { background-color: var(--input-background-color-disabled); @@ -114,30 +123,36 @@ color: var(--color-foreground-secondary); } } + .input-container { background-color: var(--color-background-tertiary); border-radius: $br-8; border-color: transparent; margin-top: $s-24; + .main-content { label { position: absolute; top: calc(-1 * $s-24); } + span { color: var(--color-foreground-primary); } } + &:focus { border: $s-1 solid var(--color-accent-primary); } } + textarea { border-radius: $br-8; padding: $s-12 $s-16; background-color: var(--color-background-tertiary); color: var(--color-foreground-primary); border: none; + &:focus { outline: $s-1 solid var(--color-accent-primary); } @@ -296,13 +311,13 @@ form.avatar-form { margin-bottom: $s-32; .newsletter-title { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; color: var(--color-foreground-secondary); font-size: $fs-14; } label { - font-family: "worksans", "vazirmatn", sans-serif; + font-family: worksans, vazirmatn, sans-serif; color: var(--color-background-primary); font-size: $fs-12; margin-right: calc(-1 * $s-16); @@ -321,11 +336,13 @@ form.avatar-form { } .btn-secondary { - @extend .button-secondary; + @extend %button-secondary; + height: $s-32; } .btn-primary { - @extend .button-primary; + @extend %button-primary; + height: $s-32; } diff --git a/frontend/src/app/main/ui/settings/sidebar.scss b/frontend/src/app/main/ui/settings/sidebar.scss index a072c59b8f..501a9dd137 100644 --- a/frontend/src/app/main/ui/settings/sidebar.scss +++ b/frontend/src/app/main/ui/settings/sidebar.scss @@ -42,6 +42,7 @@ .settings-item { --settings-foreground-color: var(--menu-foreground-color-rest); --settings-background-color: transparent; + display: flex; align-items: center; padding: deprecated.$s-8 deprecated.$s-8 deprecated.$s-8 deprecated.$s-24; @@ -61,7 +62,8 @@ } .feedback-icon { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--settings-foreground-color); margin-right: deprecated.$s-8; } @@ -73,6 +75,7 @@ .back-to-dashboard { @include deprecated.buttonStyle; + display: flex; align-items: center; padding: deprecated.$s-12 deprecated.$s-16; @@ -84,7 +87,8 @@ } .arrow-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); transform: rotate(180deg); margin-right: deprecated.$s-12; diff --git a/frontend/src/app/main/ui/settings/subscription.scss b/frontend/src/app/main/ui/settings/subscription.scss index 7355b3e109..ca37947dd6 100644 --- a/frontend/src/app/main/ui/settings/subscription.scss +++ b/frontend/src/app/main/ui/settings/subscription.scss @@ -20,12 +20,11 @@ .dashboard-content { display: flex; - justify-content: center; flex-direction: column; max-inline-size: $sz-500; margin-block-end: var(--sp-xxxl); inline-size: px2rem(580); - margin: px2rem(92) auto px2rem(120) auto; + margin: px2rem(92) auto px2rem(120); justify-content: center; } @@ -45,13 +44,14 @@ .membership-date { @include t.use-typography("body-small"); + color: var(--color-foreground-secondary); margin-inline-start: var(--sp-s); } .subscription-member, .penpot-member { - @extend .button-icon; + @extend %button-icon; } .penpot-member { @@ -64,12 +64,14 @@ .title-section { @include t.use-typography("title-large"); + color: var(--color-foreground-primary); margin-block-end: var(--sp-xxl); } .plan-section-title { @include t.use-typography("headline-small"); + color: var(--color-foreground-primary); } @@ -98,7 +100,8 @@ } .plan-title-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--color-foreground-primary); block-size: var(--sp-xl); inline-size: var(--sp-xl); @@ -116,11 +119,13 @@ .plan-card-title, .plan-price-value { @include t.use-typography("title-medium"); + color: var(--color-foreground-primary); } .plan-editors { @include t.use-typography("body-medium"); + align-self: end; color: var(--color-foreground-primary); margin-block-end: 2px; @@ -128,6 +133,7 @@ .plan-price-period { @include t.use-typography("body-small"); + color: var(--color-foreground-primary); } @@ -138,6 +144,7 @@ .benefits-title, .benefit { @include t.use-typography("body-medium"); + color: var(--color-foreground-secondary); } @@ -148,6 +155,7 @@ .cta-button { @include t.use-typography("body-medium"); @include deprecated.buttonStyle; + align-items: center; color: var(--color-accent-primary); display: flex; @@ -156,7 +164,8 @@ } .cta-button svg { - @extend .button-icon; + @extend %button-icon; + block-size: var(--sp-l); inline-size: var(--sp-l); stroke: var(--color-accent-primary); @@ -176,11 +185,12 @@ } .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-dialog { - @extend .modal-container-base; + @extend %modal-container-base; + max-block-size: initial; min-inline-size: px2rem(548); } @@ -190,11 +200,12 @@ } .close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-title { @include t.use-typography("title-large"); + margin-block-end: var(--sp-xxxl); color: var(--modal-title-foreground-color); display: flex; @@ -240,7 +251,7 @@ } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; } .success-action-buttons { @@ -248,13 +259,15 @@ } .primary-button { - @extend .modal-accept-btn; + @extend %modal-accept-btn; + min-block-size: $sz-32; block-size: auto; } .cancel-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; + min-block-size: $sz-32; white-space: break-spaces; block-size: auto; @@ -270,13 +283,14 @@ block-size: auto; } - @media (max-width: 992px) { + @media (width <= 992px) { display: none; } } .editors-text { @include t.use-typography("body-medium"); + margin: 0; } @@ -287,6 +301,7 @@ .editors-list { @include t.use-typography("body-medium"); + list-style-position: inside; list-style-type: none; margin-inline-start: var(--sp-xl); @@ -296,11 +311,13 @@ .input-field { --input-icon-padding: var(--sp-s); + inline-size: px2rem(80); } .error-message { @include t.use-typography("body-small"); + color: var(--color-foreground-error); margin-block-start: var(--sp-s); } @@ -319,6 +336,7 @@ .unlimited-capped-warning { @include t.use-typography("body-small"); + background-color: var(--color-background-tertiary); border-radius: var(--sp-s); margin-block-start: $sz-40; @@ -333,6 +351,7 @@ .radio-btns { label { @include t.use-typography("body-large"); + padding: 0; display: flex; align-items: center; diff --git a/frontend/src/app/main/ui/static.scss b/frontend/src/app/main/ui/static.scss index cf8cb1ec9a..5f4c768792 100644 --- a/frontend/src/app/main/ui/static.scss +++ b/frontend/src/app/main/ui/static.scss @@ -106,7 +106,8 @@ } .login-header { - @extend .button-primary; + @extend %button-primary; + padding: deprecated.$s-8 deprecated.$s-16; font-size: deprecated.$fs-11; position: fixed; @@ -135,22 +136,26 @@ .main-message { @include t.use-typography("title-large"); + color: var(--color-foreground-primary); } .desc-message { @include t.use-typography("title-large"); + color: var(--color-foreground-secondary); } .desc-text { @include t.use-typography("title-large"); + color: var(--color-foreground-secondary); margin-block-end: 0; } .download-link { @include t.use-typography("code-font"); + color: var(--color-foreground-primary); text-transform: lowercase; } @@ -159,7 +164,8 @@ text-align: center; button { - @extend .button-primary; + @extend %button-primary; + text-transform: uppercase; padding: deprecated.$s-8 deprecated.$s-16; font-size: deprecated.$fs-11; @@ -197,11 +203,13 @@ .project-name { @include deprecated.uppercaseTitleTipography; + color: var(--title-foreground-color); } .file-name { @include deprecated.smallTitleTipography; + text-transform: none; color: var(--title-foreground-color-hover); } @@ -230,7 +238,7 @@ top: 0; left: 0; z-index: 100; - background-color: rgba(0, 0, 0, 0.65); + background-color: rgb(0 0 0 / 65%); display: flex; justify-content: center; align-items: center; @@ -274,14 +282,16 @@ margin-top: deprecated.$s-32; button { - @extend .button-primary; + @extend %button-primary; + text-transform: uppercase; padding: deprecated.$s-8 deprecated.$s-16; font-size: deprecated.$fs-11; } .cancel-button { - @extend .button-secondary; + @extend %button-secondary; + text-transform: uppercase; padding: deprecated.$s-8 deprecated.$s-16; font-size: deprecated.$fs-11; diff --git a/frontend/src/app/main/ui/viewer.scss b/frontend/src/app/main/ui/viewer.scss index 6b46b2d5f4..d4fab6c26f 100644 --- a/frontend/src/app/main/ui/viewer.scss +++ b/frontend/src/app/main/ui/viewer.scss @@ -25,6 +25,7 @@ .empty-state { @include deprecated.bodySmallTypography; + color: var(--empty-message-foreground-color); display: grid; place-items: center; @@ -47,6 +48,7 @@ .thumbnails-close { @include deprecated.buttonStyle; + grid-row: 1 / span 2; grid-column: 1 / span 1; z-index: deprecated.$z-index-10; @@ -58,14 +60,14 @@ } .viewer-section { - @extend .new-scrollbar; + @extend %new-scrollbar; + grid-row: 1 / span 2; grid-column: 1 / span 1; display: flex; align-items: center; - flex-wrap: nowrap; + flex-wrap: wrap; height: calc(100vh - deprecated.$s-48); - flex-flow: wrap; overflow: auto; } @@ -78,8 +80,9 @@ .viewer-go-prev, .viewer-go-next { - @extend .button-secondary; + @extend %button-secondary; @include deprecated.flexCenter; + position: absolute; right: deprecated.$s-8; height: deprecated.$s-64; @@ -88,8 +91,10 @@ z-index: deprecated.$z-index-2; background-color: var(--viewer-controls-background-color); transition: transform 400ms ease 300ms; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } } @@ -101,6 +106,7 @@ .viewer-go-prev { left: deprecated.$s-8; right: unset; + svg { transform: rotate(180deg); } @@ -121,15 +127,18 @@ } .reset-button { - @extend .button-secondary; + @extend %button-secondary; @include deprecated.flexCenter; + height: deprecated.$s-32; width: deprecated.$s-28; margin-left: deprecated.$s-8; background-color: var(--viewer-controls-background-color); pointer-events: all; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } } @@ -137,6 +146,7 @@ .counter { @include deprecated.flexCenter; @include deprecated.bodySmallTypography; + border-radius: deprecated.$br-8; width: deprecated.$s-64; height: deprecated.$s-32; @@ -153,8 +163,7 @@ display: grid; grid-template-rows: 1fr; grid-template-columns: 1fr; - justify-items: center; - align-items: center; + place-items: center center; overflow: hidden; } @@ -164,7 +173,7 @@ left: 0; &.visible { - background-color: rgb(0, 0, 0, 0.2); + background-color: rgb(0 0 0 / 20%); } } diff --git a/frontend/src/app/main/ui/viewer/comments.scss b/frontend/src/app/main/ui/viewer/comments.scss index fa9ef1daf8..f417ea86b9 100644 --- a/frontend/src/app/main/ui/viewer/comments.scss +++ b/frontend/src/app/main/ui/viewer/comments.scss @@ -9,6 +9,7 @@ // COMMENT DROPDOWN ON HEADER .view-options { @include deprecated.bodySmallTypography; + display: flex; align-items: center; position: relative; @@ -21,7 +22,8 @@ } .dropdown { - @extend .menu-dropdown; + @extend %menu-dropdown; + right: deprecated.$s-2; top: calc(deprecated.$s-2 + deprecated.$s-48); width: deprecated.$s-272; @@ -30,6 +32,7 @@ .dropdown-title { @include deprecated.bodySmallTypography; + flex-grow: 1; color: var(--input-foreground-color-active); } @@ -42,10 +45,13 @@ .icon, .icon-dropdown { @include deprecated.flexCenter; + height: 100%; width: deprecated.$s-16; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } @@ -55,16 +61,21 @@ } .dropdown-element { - @extend .dropdown-element-base; + @extend %dropdown-element-base; + .icon { @include deprecated.flexCenter; + height: 100%; width: deprecated.$s-16; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } + &:hover .label { color: var(--input-foreground-color-active); } @@ -74,6 +85,7 @@ .label { color: var(--input-foreground-color-active); } + .icon svg { stroke: var(--input-foreground-color); } @@ -86,8 +98,8 @@ // FLOATING COMMENT .viewer-comments-container { position: absolute; - top: 0px; - left: 0px; + top: 0; + left: 0; width: 100%; height: 100%; z-index: deprecated.$z-index-1; @@ -95,11 +107,11 @@ .threads { position: absolute; - top: 0px; - left: 0px; + top: 0; + left: 0; } -//COMMENT SIDEBAR +// COMMENT SIDEBAR .comments-sidebar { position: absolute; right: 0; diff --git a/frontend/src/app/main/ui/viewer/header.scss b/frontend/src/app/main/ui/viewer/header.scss index f9814d3a44..091df03fde 100644 --- a/frontend/src/app/main/ui/viewer/header.scss +++ b/frontend/src/app/main/ui/viewer/header.scss @@ -46,12 +46,14 @@ .sitemap-zone { @include deprecated.flexColumn; + position: relative; width: 100%; } .project-name { @include deprecated.uppercaseTitleTipography; + color: var(--title-foreground-color); } @@ -62,22 +64,27 @@ .breadcrumb { @include deprecated.bodySmallTypography; @include deprecated.flexRow; + color: var(--title-foreground-color); cursor: pointer; } .breadcrumb-text { @include deprecated.textEllipsis; + max-width: 12vw; // This is a fallback max-width: 12cqw; // This is a unit refered to container } .icon { @include deprecated.flexCenter; + height: deprecated.$s-16; width: deprecated.$s-16; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + transform: rotate(90deg); stroke: var(--icon-foreground); } @@ -88,7 +95,8 @@ } .dropdown-sitemap { - @extend .menu-dropdown; + @extend %menu-dropdown; + left: 0; top: calc(deprecated.$s-2 + deprecated.$s-48); width: deprecated.$s-272; @@ -96,16 +104,21 @@ } .dropdown-element { - @extend .dropdown-element-base; + @extend %dropdown-element-base; + .icon-check { @include deprecated.flexCenter; + height: 100%; width: deprecated.$s-16; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } + &:hover .label { color: var(--input-foreground-color-active); } @@ -114,9 +127,11 @@ .current-frame { @include deprecated.bodySmallTypography; @include deprecated.flexRow; + flex-grow: 1; color: var(--title-foreground-color-hover); cursor: pointer; + .icon svg { stroke: var(--title-foreground-color-hover); } @@ -124,6 +139,7 @@ .frame-name { @include deprecated.textEllipsis; + max-width: 17vw; // This is a fallback max-width: 17cqw; // This is a unit refered to container } @@ -131,27 +147,31 @@ // SECTION BUTTONS .mode-zone { @include deprecated.flexRow; + height: 100%; } .mode-zone-btn { - @extend .button-tertiary; + @extend %button-tertiary; @include deprecated.flexCenter; + height: deprecated.$s-32; width: deprecated.$s-28; padding: 0; + svg { - @extend .button-icon; + @extend %button-icon; } } .selected { - @extend .button-icon-selected; + @extend %button-icon-selected; } // OPTION AREA .options-zone { @include deprecated.flexRow; + position: relative; justify-content: flex-end; gap: deprecated.$s-8; @@ -166,37 +186,45 @@ } .fullscreen-btn { - @extend .button-tertiary; + @extend %button-tertiary; @include deprecated.flexCenter; + height: deprecated.$s-32; width: deprecated.$s-28; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } } .share-btn { - @extend .button-primary; + @extend %button-primary; + height: deprecated.$s-32; min-width: deprecated.$s-72; margin-left: deprecated.$s-4; } .edit-btn { - @extend .button-tertiary; + @extend %button-tertiary; @include deprecated.flexCenter; + height: deprecated.$s-32; width: deprecated.$s-28; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } } .go-log-btn { - @extend .button-tertiary; + @extend %button-tertiary; @include deprecated.bodySmallTypography; + height: deprecated.$s-32; padding: 0 deprecated.$s-8; border-radius: deprecated.$br-8; @@ -207,11 +235,14 @@ .zoom-widget { @include deprecated.buttonStyle; @include deprecated.flexCenter; + height: deprecated.$s-28; min-width: deprecated.$s-64; border-radius: deprecated.$br-8; + .label { @include deprecated.bodySmallTypography; + color: var(--button-tertiary-foreground-color-rest); } @@ -220,6 +251,7 @@ color: var(--button-tertiary-foreground-color-focus); } } + &.selected { .label { color: var(--button-tertiary-foreground-color-focus); @@ -228,7 +260,8 @@ } .dropdown { - @extend .menu-dropdown; + @extend %menu-dropdown; + right: deprecated.$s-2; top: calc(deprecated.$s-2 + deprecated.$s-48); width: deprecated.$s-272; @@ -246,19 +279,25 @@ } .zoom-btn { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-28; width: deprecated.$s-28; border-radius: deprecated.$br-8; + .zoom-icon { @include deprecated.flexCenter; + width: deprecated.$s-24; height: deprecated.$s-32; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } } + &:hover { .zoom-icon svg { stroke: var(--button-tertiary-foreground-color-hover); @@ -268,6 +307,7 @@ .zoom-text { @include deprecated.flexCenter; + height: 100%; min-width: deprecated.$s-64; padding: 0; @@ -276,22 +316,27 @@ } .reset-btn { - @extend .button-tertiary; + @extend %button-tertiary; + color: var(--button-tertiary-foreground-color-hover); height: deprecated.$s-28; border-radius: deprecated.$br-8; } .zoom-option { - @extend .menu-item-base; + @extend %menu-item-base; + .shortcuts { - @extend .shortcut-base; + @extend %shortcut-base; + .shortcut-key { - @extend .shortcut-key-base; + @extend %shortcut-key-base; } } + &:hover { color: var(--menu-foreground-color-hover); + .shortcuts { .shortcut-key { color: var(--menu-foreground-color-hover); diff --git a/frontend/src/app/main/ui/viewer/inspect.scss b/frontend/src/app/main/ui/viewer/inspect.scss index 0ed6152256..616c127985 100644 --- a/frontend/src/app/main/ui/viewer/inspect.scss +++ b/frontend/src/app/main/ui/viewer/inspect.scss @@ -8,6 +8,7 @@ .inspect-svg-wrapper { @include deprecated.flexCenter; + position: relative; flex-direction: column; flex: 1; @@ -30,7 +31,6 @@ position: relative; align-self: flex-start; width: var(--right-sidebar-width); - background-color: var(--panel-background-color); border-top: deprecated.$s-1 solid var(--search-bar-input-border-color); } diff --git a/frontend/src/app/main/ui/viewer/interactions.scss b/frontend/src/app/main/ui/viewer/interactions.scss index 8e7d03cab1..c7245a5910 100644 --- a/frontend/src/app/main/ui/viewer/interactions.scss +++ b/frontend/src/app/main/ui/viewer/interactions.scss @@ -8,6 +8,7 @@ .view-options { @include deprecated.bodySmallTypography; + display: flex; align-items: center; position: relative; @@ -18,8 +19,10 @@ padding: deprecated.$s-8; cursor: pointer; } + .dropdown-title { @include deprecated.bodySmallTypography; + flex-grow: 1; color: var(--input-foreground-color-active); } @@ -30,7 +33,8 @@ } .dropdown { - @extend .menu-dropdown; + @extend %menu-dropdown; + right: deprecated.$s-2; top: calc(deprecated.$s-2 + deprecated.$s-48); width: deprecated.$s-272; @@ -40,17 +44,23 @@ } .dropdown-element { - @extend .dropdown-element-base; + @extend %dropdown-element-base; + min-height: deprecated.$s-32; + .icon { @include deprecated.flexCenter; + height: 100%; width: deprecated.$s-16; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } + &:hover .label { color: var(--input-foreground-color-active); } @@ -60,6 +70,7 @@ .label { color: var(--input-foreground-color-active); } + .icon svg { stroke: var(--input-foreground-color); } @@ -68,10 +79,13 @@ .icon, .icon-dropdown { @include deprecated.flexCenter; + height: 100%; width: deprecated.$s-16; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } diff --git a/frontend/src/app/main/ui/viewer/login.scss b/frontend/src/app/main/ui/viewer/login.scss index f107742588..7fd5afb70a 100644 --- a/frontend/src/app/main/ui/viewer/login.scss +++ b/frontend/src/app/main/ui/viewer/login.scss @@ -7,11 +7,12 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; + width: deprecated.$s-368; } @@ -21,16 +22,18 @@ .modal-title { @include deprecated.uppercaseTitleTipography; + color: var(--modal-title-foreground-color); } .modal-close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content { @include deprecated.flexColumn; @include deprecated.bodySmallTypography; + gap: deprecated.$s-24; max-height: deprecated.$s-400; overflow: hidden auto; @@ -66,7 +69,8 @@ } a { - @extend .button-secondary; + @extend %button-secondary; + height: deprecated.$s-40; text-transform: uppercase; font-size: deprecated.$fs-11; diff --git a/frontend/src/app/main/ui/viewer/share_link.scss b/frontend/src/app/main/ui/viewer/share_link.scss index 2c8bcc60b1..1eea68ad9e 100644 --- a/frontend/src/app/main/ui/viewer/share_link.scss +++ b/frontend/src/app/main/ui/viewer/share_link.scss @@ -16,7 +16,8 @@ } .share-link-dialog { - @extend .modal-container-base; + @extend %modal-container-base; + min-height: unset; } @@ -26,21 +27,24 @@ .share-link-title { @include deprecated.uppercaseTitleTipography; + color: var(--modal-title-foreground-color); } .modal-close-button { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content { @include deprecated.bodySmallTypography; @include deprecated.flexColumn; + gap: deprecated.$s-24; } .share-link-section { @include deprecated.flexColumn; + gap: deprecated.$s-8; } @@ -55,6 +59,7 @@ .custon-input-wrapper { @include deprecated.flexRow; + border-radius: deprecated.$br-8; height: deprecated.$s-32; width: 100%; @@ -62,12 +67,14 @@ } .input-text { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + color: var(--input-foreground-color-active); padding-left: deprecated.$s-8; margin: 0; flex-grow: 1; + &:focus { outline: none; border: deprecated.$s-1 solid var(--input-border-color-active); @@ -75,48 +82,55 @@ } .copy-button { - @extend .button-secondary; + @extend %button-secondary; @include deprecated.flexRow; + gap: deprecated.$s-8; height: deprecated.$s-32; width: deprecated.$s-28; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground-hover); } } .description { @include deprecated.bodySmallTypography; + color: var(--modal-text-foreground-color); margin-bottom: deprecated.$s-24; } .actions { @include deprecated.flexRow; + justify-content: flex-end; } .button-active { - @extend .modal-accept-btn; + @extend %modal-accept-btn; } .button-cancel { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } .button-danger { - @extend .modal-danger-btn; + @extend %modal-danger-btn; } .permissions-section { @include deprecated.flexColumn; + gap: deprecated.$s-8; } .manage-permissions { @include deprecated.buttonStyle; @include deprecated.uppercaseTitleTipography; + color: var(--menu-foreground-color-rest); height: deprecated.$s-32; display: flex; @@ -126,11 +140,15 @@ .icon { @include deprecated.flexCenter; + margin-right: deprecated.$s-6; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } + &.rotated { transform: rotate(90deg); } @@ -162,39 +180,51 @@ flex-grow: 1; color: var(--input-foreground-color-active); } + .select-all-row { @include deprecated.flexRow; + justify-content: space-between; height: deprecated.$s-32; border-bottom: deprecated.$s-1 solid var(--input-border-color-disabled); } + .select-all-label { color: var(--input-foreground-color-active); } + .pages-selection { margin: 0; + li { border-bottom: deprecated.$s-1 solid var(--input-border-color-disabled); } + li:last-child { border-bottom: none; } } + .count-pages, .current-tag { @include deprecated.bodySmallTypography; + color: var(--input-foreground-color); } .checkbox-wrapper { - @extend .input-checkbox; + @extend %input-checkbox; + height: deprecated.$s-32; padding: 0; + span.checked { background-color: var(--input-checkbox-background-color-active); border: deprecated.$s-1 solid var(--input-checkbox-background-color-active); + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--input-checkbox-foreground-color-active); } } diff --git a/frontend/src/app/main/ui/viewer/thumbnails.scss b/frontend/src/app/main/ui/viewer/thumbnails.scss index c0735ddce1..15d12a6794 100644 --- a/frontend/src/app/main/ui/viewer/thumbnails.scss +++ b/frontend/src/app/main/ui/viewer/thumbnails.scss @@ -34,21 +34,25 @@ .counter { @include deprecated.bodySmallTypography; + color: var(--viewer-thumbnails-control-foreground-color); } .actions { @include deprecated.flexRow; + width: deprecated.$s-60; } .expand-btn, .close-btn { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-32; width: deprecated.$s-28; + svg { - @extend .button-icon; + @extend %button-icon; } } @@ -72,8 +76,9 @@ .right-scroll-handler, .left-scroll-handler { - @extend .button-tertiary; + @extend %button-tertiary; @include deprecated.flexCenter; + grid-column: 3 / span 1; grid-row: 1 / span 1; width: deprecated.$s-32; @@ -81,11 +86,14 @@ margin: auto 0; z-index: deprecated.$z-index-10; opacity: 0; + &:hover { opacity: 1; } + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } } @@ -93,6 +101,7 @@ .left-scroll-handler { grid-column: 1 / span 1; grid-row: 1 / span 1; + svg { transform: rotate(180deg); } @@ -113,6 +122,7 @@ .thumbnail-item { @include deprecated.buttonStyle; + display: flex; flex-direction: column; padding: deprecated.$s-16; @@ -120,6 +130,7 @@ .thumbnail-preview { @include deprecated.flexCenter; + width: deprecated.$s-132; min-height: deprecated.$s-132; height: deprecated.$s-132; @@ -144,6 +155,7 @@ .thumbnail-info { @include deprecated.bodySmallTypography; @include deprecated.textEllipsis; + text-align: center; color: var(--viewer-thumbnails-control-foreground-color); padding: deprecated.$s-8 0; diff --git a/frontend/src/app/main/ui/workspace.scss b/frontend/src/app/main/ui/workspace.scss index 5cd617bab4..558c7a4170 100644 --- a/frontend/src/app/main/ui/workspace.scss +++ b/frontend/src/app/main/ui/workspace.scss @@ -7,24 +7,20 @@ @use "refactor/common-refactor.scss" as deprecated; .workspace { - @extend .new-scrollbar; + @extend %new-scrollbar; + width: 100vw; height: 100vh; max-height: 100vh; user-select: none; display: grid; - grid-template-areas: "left-sidebar viewport right-sidebar"; - grid-template-rows: 1fr; - grid-template-columns: auto 1fr auto; + grid-template: "left-sidebar viewport right-sidebar" 1fr / auto 1fr auto; overflow: hidden; } .workspace-loader { position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; z-index: var(--z-index-loaders); background-color: var(--color-background-primary); } diff --git a/frontend/src/app/main/ui/workspace/color_palette.scss b/frontend/src/app/main/ui/workspace/color_palette.scss index 7a4cb7c09b..2e23c45844 100644 --- a/frontend/src/app/main/ui/workspace/color_palette.scss +++ b/frontend/src/app/main/ui/workspace/color_palette.scss @@ -15,15 +15,19 @@ .right-arrow { @include deprecated.buttonStyle; @include deprecated.flexCenter; + position: relative; height: 100%; width: deprecated.$s-24; padding: 0; z-index: deprecated.$z-index-5; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } + &::after { content: ""; position: absolute; @@ -39,20 +43,24 @@ ); pointer-events: none; } + &:hover { svg { stroke: var(--button-foreground-hover); } } + &:disabled { svg { stroke: var(--button-foreground-color-disabled); } + &::after { background-image: none; } } } + .left-arrow { &::after { left: deprecated.$s-24; @@ -99,11 +107,13 @@ &.no-text { @include deprecated.flexCenter; + width: deprecated.$s-32; } } .color-palette-empty { @include deprecated.bodySmallTypography; + color: var(--palette-text-color); } diff --git a/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss b/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss index a3703f8588..79e7cf868e 100644 --- a/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss +++ b/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss @@ -27,37 +27,50 @@ padding: deprecated.$s-8; border-radius: deprecated.$br-8; margin-bottom: deprecated.$s-4; + &:last-child { margin-bottom: 0; } + .option-wrapper { width: 100%; + .library-name { @include deprecated.bodySmallTypography; + color: var(--context-menu-foreground-color); display: grid; grid-template-columns: 1fr deprecated.$s-24; + .lib-name-wrapper { display: flex; max-width: deprecated.$s-400; + .lib-name { @include deprecated.textEllipsis; + max-width: deprecated.$s-380; } + .lib-num { margin-left: deprecated.$s-4; } } + .icon-wrapper { margin-left: deprecated.$s-4; + @include deprecated.flexCenter; + svg { - @extend .button-icon-small; + @extend %button-icon-small; @include deprecated.flexCenter; + stroke: var(--icon-foreground); } } } + .color-sample { display: flex; flex-direction: row; @@ -70,11 +83,14 @@ &:hover { .option-wrapper .library-name { color: var(--context-menu-foreground-color-selected); + .icon-wrapper { @include deprecated.flexCenter; + svg { @include deprecated.flexCenter; - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--context-menu-foreground-color-selected); } } diff --git a/frontend/src/app/main/ui/workspace/colorpicker.scss b/frontend/src/app/main/ui/workspace/colorpicker.scss index 1d7e303d41..a68182b24a 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker.scss @@ -5,16 +5,16 @@ // Copyright (c) KALEIDOS INC @use "ds/typography.scss" as t; -@use "ds/spacing.scss"; +@use "ds/spacing"; @use "ds/_borders.scss" as *; @use "ds/_sizes.scss" as *; @use "ds/_utils.scss" as *; @use "refactor/basic-rules.scss" as *; .colorpicker-tooltip { - @extend .modal-background; + @extend %modal-background; + left: calc(10 * px2rem(140)); - width: auto; padding: var(--sp-m); width: $sz-284; overflow: auto; @@ -41,8 +41,9 @@ } .opacity-input-wrapper { - @extend .input-element; + @extend %input-element; @include t.use-typography("body-small"); + width: px2rem(68); } @@ -51,10 +52,8 @@ display: flex; justify-content: center; align-items: center; - border: none; background: none; cursor: pointer; - border-radius: $br-8; background-color: transparent; border: $b-1 solid transparent; height: var(--sp-xl); @@ -62,29 +61,37 @@ border-radius: $br-4; padding: 0; margin-top: var(--sp-xs); + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--button-tertiary-foreground-color-rest); } + &:hover { svg { stroke: var(--button-tertiary-foreground-color-focus); } } + &:focus, &:focus-visible { outline: none; + svg { stroke: var(--button-secondary-foreground-color-hover); } } + &:active { outline: none; border: $b-1 solid transparent; + svg { stroke: var(--button-tertiary-foreground-color-active); } } + &.selected { svg { stroke: var(--button-tertiary-foreground-color-active); @@ -99,11 +106,13 @@ } .gradient-btn { - @extend .button-tertiary; + @extend %button-tertiary; + height: var(--sp-xl); width: var(--sp-xl); border-radius: $br-4; border: $b-2 solid transparent; + &:hover { border: $b-2 solid var(--colorpicker-details-color-selected); } @@ -111,16 +120,18 @@ .linear-gradient-btn { background: linear-gradient(180deg, var(--color-foreground-secondary), transparent); + &.selected { - background: linear-gradient(to bottom, rgba(126, 255, 245, 1) 0%, rgba(126, 255, 245, 0.2) 100%); + background: linear-gradient(to bottom, rgb(126 255 245 / 100%) 0%, rgb(126 255 245 / 20%) 100%); border: $b-2 solid var(--colorpicker-details-color-selected); } } .radial-gradient-btn { background: radial-gradient(transparent, var(--color-foreground-secondary)); + &.selected { - background: radial-gradient(rgba(126, 255, 245, 1) 0%, rgba(126, 255, 245, 0.2) 100%); + background: radial-gradient(rgb(126 255 245 / 100%) 0%, rgb(126 255 245 / 20%) 100%); border: $b-2 solid var(--colorpicker-details-color-selected); } } @@ -132,7 +143,8 @@ .accept-color { @include t.use-typography("headline-small"); - @extend .button-primary; + @extend %button-primary; + width: 100%; height: var(--sp-xxxl); margin-top: var(--sp-s); @@ -180,6 +192,7 @@ height: px2rem(140); margin-bottom: $sz-6; margin-right: $sz-1; + img { height: fit-content; width: fit-content; @@ -190,20 +203,23 @@ } .choose-image { - @extend .button-secondary; + @extend %button-secondary; @include t.use-typography("headline-small"); + width: 100%; margin-top: var(--sp-m); height: var(--sp-xxxl); } .checkbox-option { - @extend .input-checkbox; + @extend %input-checkbox; + margin: var(--sp-l) 0 0 0; } .token-color-title { @include t.use-typography("title-small"); + color: var(--color-foreground-secondary); display: flex; align-items: center; diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss index 6d653f34e3..e1e3a60acf 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss @@ -8,27 +8,35 @@ .color-values { @include deprecated.flexColumn; + margin-top: deprecated.$s-8; &.disable-opacity { grid-template-columns: 3.5rem repeat(3, 1fr); } + .colors-row { @include deprecated.flexRow; + .input-wrapper { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + width: deprecated.$s-84; display: flex; align-items: baseline; } } + .hex-alpha-wrapper { @include deprecated.flexRow; + .input-wrapper { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + width: deprecated.$s-84; + &.hex { width: deprecated.$s-172; display: flex; diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.scss b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.scss index 37ab3f3e40..ba963307bd 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.scss @@ -16,6 +16,7 @@ .color-token-item { --color-token-background: var(--color-background-primary); + background-color: var(--color-token-background); color: var(--color-foreground-primary); text-align: left; @@ -29,6 +30,7 @@ block-size: $sz-28; border: none; cursor: pointer; + &:hover { --color-token-background: var(--color-background-tertiary); } @@ -36,6 +38,7 @@ .color-token-empty-state { @include t.use-typography("body-small"); + padding: var(--sp-s) var(--sp-xxl); text-align: center; color: var(--color-foreground-secondary); @@ -57,6 +60,7 @@ .token-name { @include t.use-typography("body-small"); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -97,7 +101,9 @@ .set-title-bar { --title-color: var(--color-foreground-secondary); --arrow-color: var(--color-foreground-secondary); + @include t.use-typography("title-small"); + text-transform: none; display: flex; overflow: hidden; diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss b/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss index d9e5b75633..b2efc2b627 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss @@ -46,7 +46,7 @@ background-size: deprecated.$s-8; border-radius: deprecated.$br-6; border: deprecated.$s-2 solid var(--color-foreground-primary); - box-shadow: 0px 0px deprecated.$s-4 0px var(--menu-shadow-color); + box-shadow: 0 0 deprecated.$s-4 0 var(--menu-shadow-color); height: calc(deprecated.$s-24 - deprecated.$s-2); left: var(--position); overflow: hidden; @@ -59,11 +59,12 @@ outline: deprecated.$s-2 solid var(--color-accent-primary); } } + .gradient-preview-stop-decoration { background: var(--color-foreground-primary); border-radius: 100%; bottom: deprecated.$s-32; - box-shadow: 0px 0px deprecated.$s-4 0px var(--menu-shadow-color); + box-shadow: 0 0 deprecated.$s-4 0 var(--menu-shadow-color); height: deprecated.$s-4; left: calc(var(--position) + deprecated.$s-8); position: absolute; @@ -109,8 +110,7 @@ flex-direction: column; gap: deprecated.$s-4; max-height: deprecated.$s-180; - overflow-y: auto; - overflow-x: hidden; + overflow: hidden auto; padding: 0 0 var(--sp-s) var(--sp-m); } @@ -120,7 +120,6 @@ padding: deprecated.$s-2; border-radius: deprecated.$br-12; border: deprecated.$s-1 solid transparent; - position: relative; &.is-selected { @@ -141,8 +140,9 @@ } .offset-input-wrapper { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + width: deprecated.$s-92; } diff --git a/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss b/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss index e2438dc416..c3a3cced1e 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss @@ -16,6 +16,7 @@ .hue-wheel-wrapper { @include deprecated.flexCenter; + position: relative; } @@ -25,7 +26,8 @@ } .handler { - @extend .colorpicker-handler; + @extend %colorpicker-handler; + height: deprecated.$s-16; width: deprecated.$s-16; border: deprecated.$s-2 solid var(--colorpicker-handlers-color); @@ -38,6 +40,7 @@ .handlers-wrapper { @include deprecated.flexRow; + height: deprecated.$s-200; width: deprecated.$s-52; flex-grow: 1; diff --git a/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss b/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss index 08def7607f..e8222e902c 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss @@ -8,8 +8,9 @@ .hsva-selector { @include deprecated.flexColumn; + padding: deprecated.$s-4; - grid-row-gap: deprecated.$s-8; + row-gap: deprecated.$s-8; margin-bottom: deprecated.$s-8; } @@ -20,6 +21,7 @@ .hsva-selector-label { @include deprecated.uppercaseTitleTipography; + display: flex; align-items: center; justify-content: flex-start; diff --git a/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss b/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss index 0fc9028c86..1489da0fd0 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss @@ -23,13 +23,15 @@ .add-color-btn, .palette-btn { - @extend .button-secondary; + @extend %button-secondary; + height: deprecated.$s-24; width: deprecated.$s-24; border-radius: deprecated.$br-circle; padding: 0; + svg { - @extend .button-icon; + @extend %button-icon; } } diff --git a/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss b/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss index 00e5825af6..e5ba77ec55 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .value-saturation-selector { - background-color: rgba(var(--hue-rgb)); + background-color: rgb(var(--hue-rgb)); position: relative; height: deprecated.$s-140; width: 100%; @@ -20,7 +20,7 @@ position: absolute; width: 100%; height: 100%; - background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0)); + background: linear-gradient(to right, #fff, rgb(255 255 255 / 0%)); } &::after { @@ -28,12 +28,13 @@ position: absolute; width: 100%; height: 100%; - background: linear-gradient(to top, #000, rgba(0, 0, 0, 0)); + background: linear-gradient(to top, #000, rgb(0 0 0 / 0%)); } } .handler { - @extend .colorpicker-handler; + @extend %colorpicker-handler; + height: deprecated.$s-16; width: deprecated.$s-16; border: deprecated.$s-2 solid var(--colorpicker-handlers-color); diff --git a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss index 460939c0a8..4473eaface 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss @@ -14,17 +14,14 @@ --gradient-direction: 0deg; --background-repeat: top; } + position: relative; align-self: center; height: deprecated.$s-24; inline-size: 100%; border: deprecated.$s-2 solid var(--colorpicker-details-color); border-radius: deprecated.$br-6; - background: linear-gradient( - var(--gradient-direction), - rgba(var(--color), 0) 0%, - rgba(var(--color), 1) 100% - ); + background: linear-gradient(var(--gradient-direction), rgb(var(--color), 0) 0%, rgb(var(--color), 1) 100%); cursor: pointer; &.vertical { @@ -65,8 +62,8 @@ height: 100%; background: linear-gradient( var(--gradient-direction), - rgba(var(--color), 0) 0%, - rgba(var(--color), 1) 100% + rgb(var(--color), 0) 0%, + rgb(var(--color), 1) 100% ); } } @@ -109,6 +106,7 @@ .slider-selector.value { background: linear-gradient(var(--gradient-direction), var(--hue-from, #000) 0%, var(--hue-to, #fff) 100%); } + .slider-selector.saturation { background: linear-gradient( var(--gradient-direction), diff --git a/frontend/src/app/main/ui/workspace/comments.scss b/frontend/src/app/main/ui/workspace/comments.scss index 1cd9c04b64..0b49ea07c8 100644 --- a/frontend/src/app/main/ui/workspace/comments.scss +++ b/frontend/src/app/main/ui/workspace/comments.scss @@ -24,7 +24,8 @@ .mode-dropdown-wrapper { @include deprecated.buttonStyle; - @extend .asset-element; + @extend %asset-element; + background-color: var(--color-background-tertiary); display: flex; width: 100%; @@ -46,17 +47,21 @@ .arrow-icon { @include deprecated.flexCenter; + height: deprecated.$s-24; width: deprecated.$s-24; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + transform: rotate(90deg); stroke: var(--icon-foreground); } } .comment-mode-dropdown { - @extend .dropdown-wrapper; + @extend %dropdown-wrapper; + top: deprecated.$s-92; left: deprecated.$s-12; max-width: deprecated.$s-256; @@ -68,29 +73,38 @@ } .dropdown-item { - @extend .dropdown-element-base; + @extend %dropdown-element-base; + justify-content: space-between; + .icon { @include deprecated.flexCenter; + height: deprecated.$s-24; width: deprecated.$s-24; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: transparent; } } + .label { @include deprecated.bodySmallTypography; } + &:hover { .icon svg { stroke: transparent; } } + &.selected { .label { color: var(--menu-foreground-color); } + .icon svg { stroke: var(--icon-foreground-hover); } diff --git a/frontend/src/app/main/ui/workspace/context_menu.scss b/frontend/src/app/main/ui/workspace/context_menu.scss index 8d347c2190..bef5f39fd9 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.scss +++ b/frontend/src/app/main/ui/workspace/context_menu.scss @@ -16,6 +16,7 @@ .context-list, .workspace-context-submenu { @include deprecated.menuShadow; + display: grid; width: deprecated.$s-240; padding: deprecated.$s-4; @@ -46,15 +47,20 @@ .title { @include deprecated.bodySmallTypography; + color: var(--menu-foreground-color); } + .shortcut { @include deprecated.flexCenter; + gap: deprecated.$s-2; color: var(--menu-shortcut-foreground-color); + .shortcut-key { @include deprecated.bodySmallTypography; @include deprecated.flexCenter; + height: deprecated.$s-20; padding: deprecated.$s-2 deprecated.$s-6; border-radius: deprecated.$br-6; @@ -63,19 +69,23 @@ } .submenu-icon svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--menu-foreground-color); } &:hover { background-color: var(--menu-background-color-hover); + .title { color: var(--menu-foreground-color-hover); } + .shortcut { color: var(--menu-shortcut-foreground-color-hover); } } + &:focus { border: 1px solid var(--menu-border-color-focus); background-color: var(--menu-background-color-focus); @@ -89,6 +99,7 @@ height: deprecated.$s-28; padding: deprecated.$s-6; border-radius: deprecated.$br-8; + &:hover { background-color: var(--menu-background-color-hover); } @@ -99,15 +110,18 @@ .selected-icon { svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--menu-foreground-color); } } .shape-icon { margin-left: deprecated.$s-2; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--menu-foreground-color); } } diff --git a/frontend/src/app/main/ui/workspace/coordinates.scss b/frontend/src/app/main/ui/workspace/coordinates.scss index b338144d06..44c76da73e 100644 --- a/frontend/src/app/main/ui/workspace/coordinates.scss +++ b/frontend/src/app/main/ui/workspace/coordinates.scss @@ -11,7 +11,7 @@ $width-settings-bar: 256px; .container { background-color: var(--color-background-primary); border-radius: deprecated.$br-4; - bottom: 0px; + bottom: 0; padding: deprecated.$s-2 deprecated.$s-8; position: fixed; right: calc(#{$width-settings-bar} + #{deprecated.$s-24}); diff --git a/frontend/src/app/main/ui/workspace/left_header.scss b/frontend/src/app/main/ui/workspace/left_header.scss index a096c8144a..1007aeeb44 100644 --- a/frontend/src/app/main/ui/workspace/left_header.scss +++ b/frontend/src/app/main/ui/workspace/left_header.scss @@ -15,10 +15,12 @@ .main-icon { @include deprecated.flexCenter; + width: deprecated.$s-32; height: deprecated.$s-32; min-height: deprecated.$s-32; margin-right: deprecated.$s-4; + svg { min-height: deprecated.$s-32; width: deprecated.$s-32; @@ -38,6 +40,7 @@ .file-name { @include deprecated.uppercaseTitleTipography; @include deprecated.textEllipsis; + height: deprecated.$s-16; width: 100%; padding-bottom: deprecated.$s-2; @@ -47,6 +50,7 @@ .file-name { @include deprecated.smallTitleTipography; + text-transform: none; color: var(--title-foreground-color-hover); align-items: center; @@ -60,6 +64,7 @@ .file-name-input { @include deprecated.flexCenter; + width: 100%; margin: 0; border: 0; @@ -70,6 +75,7 @@ color: var(--input-foreground-color); z-index: deprecated.$z-index-20; white-space: break-spaces; + &:focus { outline: none; } @@ -77,9 +83,11 @@ .shared-badge { @include deprecated.flexCenter; + width: deprecated.$s-16; height: deprecated.$s-32; margin-right: deprecated.$s-4; + svg { stroke: var(--button-secondary-foreground-color-rest); fill: none; @@ -118,9 +126,11 @@ 0% { transform: translateY(0); } + 50% { transform: translateY(-4px); } + 100% { transform: translateY(0); } diff --git a/frontend/src/app/main/ui/workspace/libraries.scss b/frontend/src/app/main/ui/workspace/libraries.scss index 599cc8afde..e8a5d9a309 100644 --- a/frontend/src/app/main/ui/workspace/libraries.scss +++ b/frontend/src/app/main/ui/workspace/libraries.scss @@ -43,6 +43,7 @@ .modal-title { @include t.use-typography("headline-medium"); + margin-block-end: var(--sp-l); color: var(--color-foreground-primary); } @@ -83,7 +84,7 @@ overflow-y: auto; } -.section-list-item { +%section-list-item-placeholder { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-s); @@ -92,12 +93,17 @@ border-radius: $br-8; } -.section-list-item:first-child { - border: none; +.section-list-item { + @extend %section-list-item-placeholder; + + &:first-child { + border: none; + } } .section-list-item-double-icon { - @extend .section-list-item; + @extend %section-list-item-placeholder; + grid-template-columns: 1fr auto auto; } @@ -117,6 +123,7 @@ .section-title { @include t.use-typography("headline-small"); + margin-block-end: var(--sp-m); color: var(--title-foreground-color); } @@ -165,6 +172,7 @@ .libraries-updates-item { @include t.use-typography("body-large"); + display: grid; grid-template-columns: auto 1fr; align-items: start; @@ -191,31 +199,39 @@ padding-inline-start: calc(var(--sp-xxl) + var(--sp-s)); } -.item-name { +%item-name { @include t.use-typography("body-large"); - @include textEllipsis; + @include text-ellipsis; + margin: 0; max-width: px2rem(236); color: var(--library-name-foreground-color); } +.item-name { + @extend %item-name; +} + .item-name-short { max-width: px2rem(206); } .item-name-long { - @extend .item-name; + @extend %item-name; + max-width: px2rem(450); } .item-title { @include t.use-typography("body-large"); + margin: 0; color: var(--library-name-foreground-color); } .item-update { @include t.use-typography("headline-small"); + height: $sz-32; min-width: px2rem(92); padding: var(--sp-s) var(--sp-xxl); @@ -225,6 +241,7 @@ .item-contents { @include t.use-typography("body-small"); + color: var(--library-content-foreground-color); display: flex; flex-wrap: wrap; @@ -262,6 +279,7 @@ .modal-v2-title { @include t.use-typography("headline-medium"); + color: var(--modal-title-foreground-color); } @@ -274,11 +292,10 @@ .info-block { display: grid; - grid-template-columns: auto 1fr; column-gap: var(--sp-xl); grid-template: - "icon title" - "icon content"; + "icon title" auto + "icon content" auto / auto 1fr; } .info-icon { @@ -301,12 +318,14 @@ .info-block-title { @include t.use-typography("body-large"); + grid-area: title; color: var(--modal-title-foreground-color); } .info-block-content { @include t.use-typography("body-medium"); + grid-area: content; color: var(--library-content-foreground-color); } @@ -320,11 +339,13 @@ .primary-button { @include t.use-typography("headline-small"); + padding: 0 var(--sp-l); } .sample-libraries-info { @include t.use-typography("body-small"); + display: flex; flex-direction: column; margin: var(--sp-xxxl); @@ -333,6 +354,7 @@ .sample-libraries-link { @include t.use-typography("body-small"); + color: var(--color-accent-primary); &:hover { @@ -342,6 +364,7 @@ .sample-libraries-container { @include t.use-typography("body-small"); + display: flex; flex-direction: column; width: 100%; @@ -359,6 +382,7 @@ .sample-library-item-name { @include t.use-typography("body-medium"); + color: var(--color-foreground-primary); white-space: nowrap; overflow: hidden; @@ -368,6 +392,7 @@ .sample-library-button { @include t.use-typography("headline-small"); + height: $sz-32; width: px2rem(80); margin: 0; diff --git a/frontend/src/app/main/ui/workspace/main_menu.scss b/frontend/src/app/main/ui/workspace/main_menu.scss index 1b12e2cdbf..6f615a7263 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.scss +++ b/frontend/src/app/main/ui/workspace/main_menu.scss @@ -72,13 +72,13 @@ &.plugins { max-height: calc(100vh - $sz-200); - overflow-x: hidden; - overflow-y: auto; + overflow: hidden auto; } } .base-menu-item { @include t.use-typography("body-small"); + display: grid; align-items: center; grid-template-columns: auto $sz-16 $sz-16; @@ -99,6 +99,7 @@ &.disabled { --menu-foreground-color: var(--color-foreground-secondary); + pointer-events: none; } } @@ -122,6 +123,7 @@ .item-indicator { --menu-indicator-color: var(--color-foreground-secondary); + grid-area: indicator; display: flex; align-items: center; @@ -171,6 +173,7 @@ .shortcut-key { @include t.use-typography("body-small"); + display: flex; align-items: center; justify-content: center; diff --git a/frontend/src/app/main/ui/workspace/nudge.scss b/frontend/src/app/main/ui/workspace/nudge.scss index 9ed244bedd..82797d3b3e 100644 --- a/frontend/src/app/main/ui/workspace/nudge.scss +++ b/frontend/src/app/main/ui/workspace/nudge.scss @@ -7,11 +7,12 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; + min-width: deprecated.$s-408; } @@ -21,22 +22,28 @@ .modal-title { @include deprecated.headlineMediumTypography; + color: var(--modal-title-foreground-color); } + .modal-close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content { @include deprecated.flexColumn; + gap: deprecated.$s-24; + @include deprecated.bodyLargeTypography; + margin-bottom: deprecated.$s-24; } .input-wrapper { - @extend .input-with-label; + @extend %input-with-label; @include deprecated.bodySmallTypography; + label { text-transform: none; } @@ -44,6 +51,7 @@ .modal-msg { @include deprecated.bodyLargeTypography; + color: var(--modal-text-foreground-color); line-height: 1.5; } diff --git a/frontend/src/app/main/ui/workspace/palette.scss b/frontend/src/app/main/ui/workspace/palette.scss index 7dc42ffc37..f7d77f70b6 100644 --- a/frontend/src/app/main/ui/workspace/palette.scss +++ b/frontend/src/app/main/ui/workspace/palette.scss @@ -7,7 +7,6 @@ @use "ds/spacing.scss" as *; @use "ds/z-index.scss" as *; @use "ds/_sizes.scss" as *; - @use "refactor/common-refactor.scss" as deprecated; .palette-wrapper { @@ -30,11 +29,7 @@ right: 0; grid-area: color-palette; display: grid; - grid-template-areas: - "resize resize resize" - "buttons actions palette"; - grid-template-rows: deprecated.$s-8 1fr; - grid-template-columns: deprecated.$s-32 auto 1fr; + grid-template: "resize resize resize" deprecated.$s-8 "buttons actions palette" 1fr / deprecated.$s-32 auto 1fr; max-height: deprecated.$s-80; height: var(--height); width: fit-content; @@ -46,6 +41,7 @@ right 0.3s, opacity 0.2s, width 0.3s; + &.wide { width: 100%; } @@ -59,6 +55,7 @@ cursor: ns-resize; background-color: var(--palette-background-color); } + .palette-btn-list { grid-area: buttons; background-color: var(--palette-background-color); @@ -68,35 +65,44 @@ list-style: none; z-index: deprecated.$z-index-2; gap: deprecated.$s-2; + &.mid-palette, &.small-palette { display: flex; } + .palette-item { @include deprecated.flexCenter; + border-radius: deprecated.$br-8; opacity: deprecated.$op-10; transition: opacity 1s ease; + .palette-btn { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-32; width: deprecated.$s-32; border-radius: deprecated.$br-8; background-clip: padding-box; padding: 0; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } + &.selected { - @extend .button-icon-selected; + @extend %button-icon-selected; } } } } .palette-actions { - @extend .button-tertiary; + @extend %button-tertiary; + grid-area: actions; height: calc(var(--height) - deprecated.$s-16); width: deprecated.$s-32; @@ -105,11 +111,14 @@ border-radius: deprecated.$br-8; background-color: var(--palette-background-color); z-index: deprecated.$z-index-2; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } } + .palette { grid-area: palette; width: 100%; @@ -120,8 +129,10 @@ .handler { @include deprecated.buttonStyle; @include deprecated.flexCenter; + width: deprecated.$s-12; height: 100%; + .handler-btn { width: deprecated.$s-4; height: 100%; @@ -147,29 +158,35 @@ border-inline-start: 0; border-start-start-radius: 0; border-end-start-radius: 0; + .palette-btn-list { opacity: deprecated.$op-0; visibility: hidden; width: 0; + .palette-item { opacity: deprecated.$op-0; visibility: hidden; z-index: 0; } } + .resize-area { visibility: hidden; z-index: 0; width: 0; } + .palette-actions { visibility: hidden; z-index: 0; } + .palette { visibility: hidden; z-index: 0; } + .handler { padding-bottom: deprecated.$s-8; } @@ -179,21 +196,26 @@ .help-btn { z-index: var(--z-index-panels); flex-shrink: 0; - @extend .button-secondary; + + @extend %button-secondary; + inline-size: $sz-40; block-size: $sz-40; border-radius: deprecated.$br-circle; border: none; + &.selected { - @extend .button-icon-selected; + @extend %button-icon-selected; } + &:hover { border: none; } } .icon-help { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); inline-size: var(--sp-xxl); block-size: var(--sp-xxl); diff --git a/frontend/src/app/main/ui/workspace/plugins.scss b/frontend/src/app/main/ui/workspace/plugins.scss index 87af034d70..d51dc2ff68 100644 --- a/frontend/src/app/main/ui/workspace/plugins.scss +++ b/frontend/src/app/main/ui/workspace/plugins.scss @@ -7,11 +7,12 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-dialog { - @extend .modal-container-base; + @extend %modal-container-base; + display: grid; grid-template-rows: auto 1fr auto; max-height: initial; @@ -41,16 +42,18 @@ } .close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .close-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } .modal-title { @include deprecated.headlineMediumTypography; + margin-block-end: deprecated.$s-32; color: var(--modal-title-foreground-color); display: flex; @@ -82,8 +85,9 @@ } .primary-button { - @extend .button-primary; + @extend %button-primary; @include deprecated.headlineSmallTypography; + padding: deprecated.$s-0 deprecated.$s-16; } @@ -93,18 +97,21 @@ } .cancel-button { - @extend .button-secondary; + @extend %button-secondary; @include deprecated.headlineSmallTypography; + padding: deprecated.$s-0 deprecated.$s-16; } .search-icon { @include deprecated.flexCenter; + width: deprecated.$s-20; padding: 0 0 0 deprecated.$s-8; svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } @@ -126,8 +133,7 @@ .plugins-list { padding-top: deprecated.$s-20; - overflow-x: hidden; - overflow-y: auto; + overflow: hidden auto; flex: 1; display: flex; flex-direction: column; @@ -158,11 +164,13 @@ .plugin-title { @include deprecated.bodyMediumTypography; + color: var(--color-foreground-primary); } .plugin-summary { @include deprecated.bodySmallTypography; + color: var(--color-foreground-secondary); } @@ -195,6 +203,7 @@ .plugins-empty-text { @include deprecated.bodySmallTypography; + color: var(--color-foreground-primary); } @@ -204,6 +213,7 @@ div.input-error { .info { @include deprecated.bodySmallTypography; + margin-top: deprecated.$s-4; &.error { @@ -231,9 +241,6 @@ div.input-error { } } -.plugin-permissions { -} - .permissions-list { display: flex; flex-direction: column; @@ -256,12 +263,14 @@ div.input-error { .permissions-list-text { @include deprecated.bodySmallTypography; + margin: 0; color: var(--color-foreground-secondary); } .permissions-disclaimer { @include deprecated.bodySmallTypography; + padding: deprecated.$s-16; background: var(--color-background-quaternary); color: var(--color-foreground-primary); @@ -275,6 +284,7 @@ div.input-error { .discover { @include deprecated.bodySmallTypography; + color: var(--color-foreground-secondary); margin-top: deprecated.$s-24; diff --git a/frontend/src/app/main/ui/workspace/presence.scss b/frontend/src/app/main/ui/workspace/presence.scss index 03f6c8134e..76f488e1f2 100644 --- a/frontend/src/app/main/ui/workspace/presence.scss +++ b/frontend/src/app/main/ui/workspace/presence.scss @@ -12,7 +12,6 @@ .active-users-opened { background: none; cursor: pointer; - display: flex; flex-direction: row-reverse; justify-content: flex-end; @@ -33,6 +32,7 @@ %user-icon { @include t.use-typography("body-small"); + display: grid; place-content: center; height: $sz-24; @@ -48,6 +48,7 @@ .users-num { @extend %user-icon; + background-color: var(--user-count-background-color); color: var(--user-count-foreground-color); z-index: 3; // FIXME: this is hardcoded because of the way its component uses z-index from cljs @@ -57,6 +58,7 @@ .session-icon { @extend %user-icon; + margin-inline-start: var(--user-list-inline-margin, calc(-1 * var(--sp-xs))); } diff --git a/frontend/src/app/main/ui/workspace/right_header.scss b/frontend/src/app/main/ui/workspace/right_header.scss index e6d7ea2092..fd40e13768 100644 --- a/frontend/src/app/main/ui/workspace/right_header.scss +++ b/frontend/src/app/main/ui/workspace/right_header.scss @@ -29,6 +29,7 @@ .zoom-widget { @include deprecated.buttonStyle; + display: flex; align-items: center; justify-content: center; @@ -39,6 +40,7 @@ .label { @include deprecated.bodySmallTypography; + height: 100%; padding: deprecated.$s-8 0; color: var(--button-tertiary-foreground-color-rest); @@ -58,7 +60,8 @@ } .dropdown { - @extend .menu-dropdown; + @extend %menu-dropdown; + right: deprecated.$s-2; top: calc(deprecated.$s-2 + deprecated.$s-48); width: deprecated.$s-272; @@ -77,6 +80,7 @@ .zoom-text { @include deprecated.flexCenter; + height: 100%; min-width: deprecated.$s-48; padding: 0; @@ -85,20 +89,21 @@ } .reset-btn { - @extend .button-tertiary; + @extend %button-tertiary; + color: var(--button-tertiary-foreground-color-hover); height: deprecated.$s-28; border-radius: deprecated.$br-8; } .zoom-option { - @extend .menu-item-base; + @extend %menu-item-base; .shortcuts { - @extend .shortcut-base; + @extend %shortcut-base; .shortcut-key { - @extend .shortcut-key-base; + @extend %shortcut-key-base; } } @@ -114,7 +119,8 @@ } .comments-btn { - @extend .button-tertiary; + @extend %button-tertiary; + border-radius: deprecated.$br-8; margin: 0; height: deprecated.$s-28; @@ -122,7 +128,8 @@ border: none; svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); height: deprecated.$s-16; width: deprecated.$s-16; @@ -143,7 +150,8 @@ } .history-button { - @extend .button-tertiary; + @extend %button-tertiary; + border-radius: deprecated.$br-8; margin: 0; height: deprecated.$s-28; @@ -151,7 +159,8 @@ border: none; svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); height: deprecated.$s-16; width: deprecated.$s-16; @@ -173,19 +182,22 @@ .persistence-status-widget { @include deprecated.flexCenter; + width: deprecated.$s-28; height: deprecated.$s-28; } .status-icon { @include deprecated.flexCenter; + width: deprecated.$s-24; height: deprecated.$s-24; margin: 0; border-radius: deprecated.$br-circle; svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--status-widget-icon-foreground-color); } } @@ -213,7 +225,8 @@ .share-btn, .viewer-btn { - @extend .button-tertiary; + @extend %button-tertiary; + border-radius: deprecated.$br-8; margin: 0; width: deprecated.$s-28; @@ -221,7 +234,8 @@ border: none; svg { - @extend .button-icon; + @extend %button-icon; + height: deprecated.$s-16; width: deprecated.$s-16; stroke: var(--icon-foreground); diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss index 642a4cabf3..753a30ea46 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss +++ b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss @@ -8,7 +8,6 @@ .text-editor-container { height: 100%; position: relative; - cursor: text; } @@ -18,21 +17,17 @@ .text-editor-content { height: 100%; - font-family: sourcesanspro; - + font-family: sourcesanspro, sans-serif; outline: none; user-select: text; white-space: pre-wrap; overflow-wrap: break-word; - caret-color: var(--text-editor-caret-color); - color: transparent; // Match Skia's text layout precision: prevent browser text-size // adjustments and ensure consistent kerning across browsers. text-size-adjust: none; - -webkit-text-size-adjust: none; font-kerning: normal; &::selection, @@ -41,16 +36,16 @@ -webkit-text-fill-color: transparent; // WebKit/Safari } - &::-moz-selection, - *::-moz-selection { + &::selection, + *::selection { color: transparent; } [data-itype="paragraph"] { line-height: inherit; user-select: text; - margin: 0px; - font-size: 0px; + margin: 0; + font-size: 0; } [data-itype="inline"] { @@ -62,7 +57,6 @@ word-break: normal; overflow-wrap: break-word; tab-size: 2; - -o-tab-size: 2; } [data-itype="root"] { diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.scss b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.scss index 8539a7ca29..5659b0646e 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.scss +++ b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.scss @@ -6,7 +6,6 @@ width: 100%; height: 100%; position: absolute; - opacity: 0; overflow: hidden; white-space: pre; diff --git a/frontend/src/app/main/ui/workspace/sidebar.scss b/frontend/src/app/main/ui/workspace/sidebar.scss index 3c5360b4f4..42708ad9f6 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/sidebar.scss @@ -9,11 +9,7 @@ .left-settings-bar { display: grid; - grid-template-areas: - "header header" - "content resize"; - grid-template-rows: deprecated.$s-52 1fr; - grid-template-columns: 1fr 0; + grid-template: "header header" deprecated.$s-52 "content resize" 1fr / 1fr 0; position: relative; grid-area: left-sidebar; min-width: var(--left-sidebar-width); @@ -65,9 +61,11 @@ width: var(--right-sidebar-width); background-color: var(--panel-background-color); z-index: deprecated.$z-index-1; + &.not-expand { max-width: var(--right-sidebar-width); } + &.expanded { width: var(--right-sidebar-width, var(--right-sidebar-width)); } @@ -76,7 +74,6 @@ display: grid; grid-template-columns: 100%; grid-template-rows: 100%; - height: calc(100vh - deprecated.$s-52); overflow: hidden; } @@ -104,13 +101,16 @@ .collapse-sidebar-button { --collapse-icon-color: var(--color-foreground-secondary); + @include deprecated.flexCenter; @include deprecated.buttonStyle; + height: 100%; width: deprecated.$s-24; border-radius: deprecated.$br-5; color: var(--collapse-icon-color); transform: rotate(180deg); + &:hover { --collapse-icon-color: var(--color-foreground-primary); } @@ -118,6 +118,7 @@ .collapsed-sidebar { @include deprecated.flexCenter; + position: absolute; top: deprecated.$s-48; left: 0; @@ -126,27 +127,34 @@ background: var(--color-background-primary); margin-inline-start: var(--sp-m); } + .collapsed-title { @include deprecated.flexCenter; + height: deprecated.$s-36; width: deprecated.$s-24; border-radius: deprecated.$br-8; background: var(--color-background-secondary); } + .collapsed-button { @include deprecated.buttonStyle; + height: deprecated.$s-24; width: deprecated.$s-16; padding: 0; border-radius: deprecated.$br-5; + svg { @include deprecated.flexCenter; + height: deprecated.$s-16; width: deprecated.$s-16; color: transparent; fill: none; stroke: var(--icon-foreground); } + &:hover { svg { stroke: var(--icon-foreground-hover); diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.scss b/frontend/src/app/main/ui/workspace/sidebar/assets.scss index f89069cca5..ebf3dc0f89 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.scss @@ -8,8 +8,8 @@ .assets-bar { display: grid; - height: 100%; grid-auto-rows: max-content; + // TODO: ugly hack :( Fix this! we shouldn't be hardcoding this height height: calc(100vh - deprecated.$s-92); scrollbar-gutter: stable; @@ -18,8 +18,9 @@ } .libraries-button { - @extend .button-secondary; + @extend %button-secondary; @include deprecated.uppercaseTitleTipography; + gap: deprecated.$s-2; height: deprecated.$s-32; width: 100%; @@ -40,8 +41,9 @@ } .add-library-button { - @extend .button-primary; + @extend %button-primary; @include deprecated.uppercaseTitleTipography; + gap: deprecated.$s-2; height: deprecated.$s-32; width: 100%; @@ -52,6 +54,7 @@ .section-button { @include deprecated.flexCenter; @include deprecated.buttonStyle; + height: deprecated.$s-32; width: deprecated.$s-32; margin: 0; @@ -98,13 +101,14 @@ } &.opened { - @extend .button-icon-selected; + @extend %button-icon-selected; } } .sections-container { @include deprecated.menuShadow; @include deprecated.flexColumn; + position: absolute; top: deprecated.$s-84; left: deprecated.$s-12; @@ -117,6 +121,7 @@ .section-item { @include deprecated.bodySmallTypography; + display: flex; align-items: center; justify-content: space-between; diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss index e2c86936cb..2e44a4ba11 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss @@ -38,15 +38,18 @@ $assets-button-width: deprecated.$s-28; &.editing { border: deprecated.$s-1 solid var(--input-border-color-focus); + input.element-name { @include deprecated.textEllipsis; @include deprecated.bodySmallTypography; @include deprecated.removeInputStyle; + flex-grow: 1; margin: 0; color: var(--layer-row-foreground-color); } } + &:hover { background-color: var(--assets-item-background-color-hover); } @@ -54,6 +57,7 @@ $assets-button-width: deprecated.$s-28; .bullet-block { @include deprecated.flexCenter; + height: 100%; justify-content: flex-start; margin-inline-end: deprecated.$s-4; @@ -62,6 +66,7 @@ $assets-button-width: deprecated.$s-28; .name-block { @include deprecated.bodySmallTypography; @include deprecated.textEllipsis; + margin: 0; color: var(--assets-item-name-foreground-color); } @@ -77,6 +82,7 @@ $assets-button-width: deprecated.$s-28; .element-name { @include deprecated.textEllipsis; + color: var(--color-foreground-primary); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss index 257bd24b3c..4773f97583 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss @@ -8,6 +8,7 @@ .title-name { @include deprecated.uppercaseTitleTipography; + display: flex; align-items: center; flex-grow: 1; @@ -16,6 +17,7 @@ .title-tokens { @include deprecated.bodySmallTypography; + text-transform: capitalize; } @@ -25,13 +27,16 @@ .section-icon { @include deprecated.flexCenter; + padding-right: deprecated.$s-2; + svg { @include deprecated.flexCenter; + height: deprecated.$s-16; width: deprecated.$s-16; fill: none; - stroke: currentColor; + stroke: currentcolor; } } @@ -43,6 +48,7 @@ .num-assets { @include deprecated.flexCenter; + height: 100%; padding-left: deprecated.$s-8; } @@ -58,6 +64,7 @@ .drag-counter { @include deprecated.bodySmallTypography; @include deprecated.textEllipsis; + position: absolute; bottom: 0; left: 0; diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss index ec23a9d1f4..fa7242d60a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss @@ -20,6 +20,7 @@ border-radius: $br-8; background-color: var(--color-canvas); overflow: hidden; + &:hover { .component-item-grid-name { display: flex; @@ -32,10 +33,7 @@ &::before { content: " "; position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; border: calc($b-2 * 2) solid var(--color-background-primary); border-radius: $br-8; } @@ -50,7 +48,6 @@ left: var(--sp-xs); right: var(--sp-xs); bottom: var(--sp-xs); - padding: var(--sp-xxs) var(--sp-s); border-radius: $br-4; background-color: var(--color-background-primary); color: var(--color-foreground-primary); diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss index 18de784336..c4c2315d64 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss @@ -6,6 +6,7 @@ @use "ds/typography.scss" as t; @use "refactor/common-refactor.scss" as deprecated; + .tool-window { padding: 0 0 deprecated.$s-24 deprecated.$s-12; display: grid; @@ -16,6 +17,7 @@ .file-name { @include t.use-typography("body-small"); + display: flex; justify-content: flex-start; align-items: center; @@ -25,6 +27,7 @@ .loading { @include t.use-typography("body-small"); + display: flex; align-items: center; justify-content: flex-start; @@ -35,18 +38,22 @@ .special-title { @include deprecated.textEllipsis; + color: var(--title-foreground-color-hover); margin-left: deprecated.$s-2; text-align: left; } .file-link { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-32; width: deprecated.$s-28; border-radius: deprecated.$br-8; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); fill: var(--title-foreground-color-hover); } @@ -69,12 +76,15 @@ .no-found-icon { @include deprecated.flexCenter; + background-color: var(--not-found-background-color); border-radius: deprecated.$br-circle; height: deprecated.$s-48; width: deprecated.$s-48; + svg { - @extend .button-icon; + @extend %button-icon; + height: deprecated.$s-24; width: deprecated.$s-24; stroke: var(--not-found-foreground-color); @@ -83,5 +93,6 @@ .no-found-text { @include deprecated.bodySmallTypography; + color: var(--not-found-foreground-color); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss index 1237c53323..a2db8f408b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss @@ -40,17 +40,18 @@ .path { @include deprecated.textEllipsis; + margin-left: deprecated.$s-2; text-transform: initial; color: var(--title-foreground-color-hover); } .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { - @extend .modal-container-base; + @extend %modal-container-base; } .modal-header { @@ -59,36 +60,39 @@ .modal-title { @include deprecated.uppercaseTitleTipography; + color: var(--modal-title-foreground-color); } .modal-close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .modal-content { @include deprecated.bodySmallTypography; + margin-bottom: deprecated.$s-24; } .input-wrapper { - @extend .input-with-label; + @extend %input-with-label; @include deprecated.bodySmallTypography; + margin-bottom: deprecated.$s-8; } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; } .cancel-button { - @extend .modal-cancel-btn; + @extend %modal-cancel-btn; } .accept-btn { - @extend .modal-accept-btn; + @extend %modal-accept-btn; &.danger { - @extend .modal-danger-btn; + @extend %modal-danger-btn; } } diff --git a/frontend/src/app/main/ui/workspace/sidebar/common/sidebar.scss b/frontend/src/app/main/ui/workspace/sidebar/common/sidebar.scss index a20ad8d615..5627355c0c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/common/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/common/sidebar.scss @@ -52,6 +52,7 @@ $two-column-width: grid-width(2); $three-column-width: grid-width(3); $four-column-width: grid-width(4); $seven-column-width: grid-width(7); + // ------------------------------------------------------------ // Grid mixin — applies the standard structure to any container // ------------------------------------------------------------ @@ -86,6 +87,7 @@ $seven-column-width: grid-width(7); // - + 3 inter-column gaps // - − half a gap (because it’s visually shared with the next block) $grid-exception-input-width: calc(#{$sz-32} * 3.5 + 3 * var(--sp-xs) - (var(--sp-xs) / 2)); + // // |___|-|___|-|___|-|___|-|___|-|___|-|___|-|___| // @@ -111,10 +113,10 @@ $grid-exception-input-width-small: calc(#{$sz-32} * 2.5 + 2 * var(--sp-xs) - (va --left-sidebar-width-max: #{$left-sidebar-width-max}; --right-sidebar-width: #{$right-sidebar-width}; --right-sidebar-width-max: #{$right-sidebar-width-max}; - --2-columns-width: #{$two-column-width}; - --3-columns-width: #{$three-column-width}; - --4-columns-width: #{$four-column-width}; - --7-columns-width: #{$seven-column-width}; + --two-columns-width: #{$two-column-width}; + --three-columns-width: #{$three-column-width}; + --four-columns-width: #{$four-column-width}; + --seven-columns-width: #{$seven-column-width}; --options-width: #{$options-width}; --grid-exception-input-width: #{$grid-exception-input-width}; --grid-exception-input-width-small: #{$grid-exception-input-width-small}; diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug.scss b/frontend/src/app/main/ui/workspace/sidebar/debug.scss index 47f119009c..81a7921fe5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/debug.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/debug.scss @@ -21,12 +21,14 @@ } .checkbox-wrapper { - @extend .input-checkbox; + @extend %input-checkbox; + height: deprecated.$s-32; padding: 0; } .checkbox-icon { - @extend .checkbox-icon; + @extend %checkbox-icon; + cursor: pointer; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss index a72bf3a833..1f67e505bc 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss @@ -26,7 +26,6 @@ .shape-title { font-size: deprecated.$fs-14; - padding-bottom: deprecated.$s-4; background: var(--color-background-quaternary); color: var(--color-foreground-primary); padding: deprecated.$s-8; @@ -34,6 +33,7 @@ display: flex; gap: deprecated.$s-4; } + .shape-name { flex: 1; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/history.scss b/frontend/src/app/main/ui/workspace/sidebar/history.scss index 907e6732bf..069d1d5d73 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/history.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/history.scss @@ -24,8 +24,7 @@ .history-entries { height: calc(100vh - deprecated.$s-100); padding: deprecated.$s-12; - overflow-x: hidden; - overflow-y: auto; + overflow: hidden auto; font-size: deprecated.$fs-12; } @@ -45,26 +44,33 @@ .history-entry-summary { display: flex; align-items: center; + .history-entry-summary-icon { svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--entry-foreground-color); } } + .history-entry-summary-text { margin: 0 deprecated.$s-8; color: var(--color-foreground-primary); } + .history-entry-summary-button { opacity: deprecated.$op-0; margin-left: auto; + &.button-opened { svg { transform: rotate(90deg); } } + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--entry-foreground-color); } } @@ -74,6 +80,7 @@ display: block; padding-top: deprecated.$s-16; color: var(--modal-text-foreground-color); + .history-entry-details-list { margin: 0; } @@ -88,14 +95,17 @@ &:hover { background-color: var(--entry-background-color-hover); color: var(--entry-foreground-color-hover); + .history-entry-summary { .history-entry-summary-icon { svg { stroke: var(--entry-foreground-color-hover); } } + .history-entry-summary-button { opacity: deprecated.$op-10; + &.button-opened { svg { transform: rotate(90deg); diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss b/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss index 0fc369362d..f66d7b62fe 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss @@ -13,8 +13,8 @@ --layer-background-color: var(--color-background-primary); --layer-foreground-color: inherit; --shadow-color: transparent; - box-shadow: px2rem(16) px2rem(0) px2rem(0) px2rem(0) var(--shadow-color); + box-shadow: px2rem(16) px2rem(0) px2rem(0) px2rem(0) var(--shadow-color); display: flex; flex-direction: row; align-items: center; @@ -30,6 +30,7 @@ --context-hover-color: var(--layer-row-foreground-color-hover); --context-hover-opacity: 1; --layer-foreground-color: var(--layer-row-foreground-color-hover); + &.hidden { opacity: 1; } @@ -59,9 +60,11 @@ &.dnd-over-bot { border-block-end: $b-2 solid var(--color-accent-primary); } + &.dnd-over-top { border-block-start: $b-2 solid var(--color-accent-primary); } + &.dnd-over { border: $b-2 solid var(--color-accent-primary); } @@ -73,9 +76,11 @@ --layer-background-color: var(--color-background-quaternary); --shadow-color: var(--color-background-quaternary); } + .layer-row.type-comp & { --layer-foreground-color: var(--color-accent-secondary); } + .layer-row.selected & { --layer-background-color: transparent; --layer-foreground-color: var(--color-accent-primary); @@ -91,6 +96,7 @@ inline-size: calc(100% - (var(--depth) * var(--layer-indentation-size))); cursor: pointer; min-inline-size: px2rem(140); + &.filtered { inline-size: calc(100% - $sz-12); } @@ -106,6 +112,7 @@ &.selected { display: flex; } + .layer-row.highlight &, .layer-row:hover & { display: flex; @@ -130,21 +137,27 @@ inline-size: $sz-24; padding-inline-start: var(--sp-xs); color: var(--color-foreground-secondary); + .layer-row.selected & { color: var(--color-accent-primary); } + .layer-row.type-comp & { color: var(--color-accent-secondary); } + .inverse & { transform: rotate(-90deg); } + .layer-row.hidden & { opacity: 0.7; } + .layer-row.highlight &, .layer-row:hover & { opacity: 1; + svg { stroke: var(--color-accent-primary); } @@ -162,14 +175,17 @@ .layer-row.hidden & { opacity: 0.1; } + .layer-row.type-comp & { background-color: var(--color-accent-secondary); } + .layer-row.highlight &, .layer-row:hover & { opacity: 0.4; background-color: var(--color-accent-primary); } + .layer-row.selected & { background-color: var(--color-accent-primary); } @@ -200,12 +216,15 @@ .layer-row.hidden & { opacity: 0.7; } + .layer-row.selected & { stroke: var(--color-accent-primary); } + .layer-row.type-comp & { stroke: var(--color-accent-secondary); } + .layer-row.highlight &, .layer-row:hover & { opacity: 1; @@ -216,6 +235,7 @@ .layer-row.selected & { background-color: var(--color-background-quaternary); } + &.inverse svg { transform: rotate(90deg); } @@ -224,9 +244,9 @@ .toggle-element, .block-element { --layer-row-action-btn-background: none; + border: none; cursor: pointer; - display: flex; justify-content: center; align-items: center; block-size: 100%; @@ -235,6 +255,7 @@ display: none; background: var(--layer-row-action-btn-background); padding-inline-end: px2rem(6); + svg { display: flex; justify-content: center; @@ -249,6 +270,7 @@ .layer-row.hidden & { opacity: 0.7; } + .type-comp & { stroke: var(--color-accent-secondary); } @@ -257,6 +279,7 @@ .element-actions.selected & { display: flex; opacity: 0; + &.selected { opacity: 1; } @@ -269,15 +292,20 @@ .layer-row.highlight &, .layer-row:hover & { display: flex; + --layer-row-action-btn-background: var(--color-background-secondary); + svg { opacity: 1; stroke: var(--color-accent-primary); } } + .layer-row.selected & { display: flex; + --layer-row-action-btn-background: var(--color-background-quaternary); + svg { stroke: var(--color-accent-primary); } @@ -295,13 +323,16 @@ block-size: $sz-16; min-inline-size: calc(var(--depth) * var(--layer-indentation-size)); } + .filtered { min-inline-size: $sz-12; } + .lazy-load-sentinel { min-height: 1px; pointer-events: none; } + .lazy-load-sentinel { min-height: 1px; pointer-events: none; diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss b/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss index e2e4b1a723..290f4460e9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss @@ -15,7 +15,6 @@ @include deprecated.bodySmallTypography; color: var(--element-name-color); - flex-grow: 1; block-size: 100%; align-content: center; @@ -62,5 +61,6 @@ .element-name-touched { --element-name-touched-color: var(--layer-row-component-foreground-color); + color: var(--element-name-touched-color); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.scss b/frontend/src/app/main/ui/workspace/sidebar/layers.scss index e89f730323..234d1cee61 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.scss @@ -18,39 +18,47 @@ &.search { padding: 0 deprecated.$s-12 0 deprecated.$s-8; gap: deprecated.$s-4; + .filter-button { @include deprecated.flexCenter; @include deprecated.buttonStyle; + height: deprecated.$s-32; width: deprecated.$s-32; margin: 0; border: deprecated.$s-1 solid var(--color-background-tertiary); border-radius: deprecated.$br-8 deprecated.$br-2 deprecated.$br-2 deprecated.$br-8; background-color: var(--color-background-tertiary); + svg { height: deprecated.$s-16; width: deprecated.$s-16; stroke: var(--icon-foreground); } + &:focus { border: deprecated.$s-1 solid var(--input-border-color-focus); outline: 0; background-color: var(--input-background-color-active); color: var(--input-foreground-color-active); + svg { background-color: var(--input-background-color-active); } } + &:hover { border: deprecated.$s-1 solid var(--input-border-color-hover); background-color: var(--input-background-color-hover); + svg { background-color: var(--input-background-color-hover); stroke: var(--button-foreground-hover); } } + &.opened { - @extend .button-icon-selected; + @extend %button-icon-selected; } } } @@ -58,25 +66,30 @@ .page-name { @include deprecated.uppercaseTitleTipography; + padding: 0 deprecated.$s-12; color: var(--title-foreground-color); } .icon-search { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-32; width: deprecated.$s-28; border-radius: deprecated.$br-8; margin-right: deprecated.$s-8; padding: 0; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } } .focus-title { @include deprecated.buttonStyle; + display: grid; grid-template-columns: auto 1fr auto; align-items: center; @@ -86,11 +99,14 @@ .back-button { @include deprecated.flexCenter; + height: deprecated.$s-32; width: deprecated.$s-24; padding: 0 deprecated.$s-4 0 deprecated.$s-8; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); transform: rotate(180deg); } @@ -99,24 +115,28 @@ .focus-name { @include deprecated.textEllipsis; @include deprecated.bodySmallTypography; + padding-left: deprecated.$s-4; color: var(--title-foreground-color); } .focus-mode-tag-wrapper { @include deprecated.flexCenter; + height: 100%; margin-right: deprecated.$s-12; } .active-filters { @include deprecated.flexRow; + flex-wrap: wrap; margin: 0 deprecated.$s-12; } .layer-filter { - @extend .button-tag; + @extend %button-tag; + gap: deprecated.$s-6; height: deprecated.$s-24; margin: deprecated.$s-2 0; @@ -133,6 +153,7 @@ .layer-filter-name { @include deprecated.flexCenter; @include deprecated.bodySmallTypography; + color: var(--pill-foreground-color); } @@ -141,12 +162,15 @@ } .filters-container { - @extend .menu-dropdown; + @extend %menu-dropdown; + position: absolute; left: deprecated.$s-20; width: deprecated.$s-192; + .filter-menu-item { @include deprecated.bodySmallTypography; + display: flex; align-items: center; justify-content: space-between; @@ -158,28 +182,34 @@ display: flex; align-items: center; gap: deprecated.$s-8; + .filter-menu-item-icon { color: var(--menu-foreground-color); } + .filter-menu-item-name { padding-top: deprecated.$s-2; color: var(--menu-foreground-color); } } + .filter-menu-item-tick { color: var(--menu-foreground-color); } &.selected { background-color: var(--menu-background-color-selected); + .filter-menu-item-name-wrapper { .filter-menu-item-icon { color: var(--menu-foreground-color); } + .filter-menu-item-name { color: var(--menu-foreground-color); } } + .filter-menu-item-tick { color: var(--menu-foreground-color); } @@ -187,14 +217,17 @@ &:hover { background-color: var(--menu-background-color-hover); + .filter-menu-item-name-wrapper { .filter-menu-item-icon { color: var(--menu-foreground-color-hover); } + .filter-menu-item-name { color: var(--menu-foreground-color-hover); } } + .filter-menu-item-tick { color: var(--menu-foreground-color-hover); } @@ -204,12 +237,12 @@ .tool-window-content { --calculated-height: calc(#{deprecated.$s-136} + var(--height, #{deprecated.$s-200})); + display: flex; flex-direction: column; height: calc(100vh - var(--calculated-height)); width: calc(var(--left-sidebar-width) + var(--depth) * var(--layer-indentation-size)); - overflow-x: auto; - overflow-y: overlay; + overflow: auto; scrollbar-gutter: stable; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.scss b/frontend/src/app/main/ui/workspace/sidebar/options.scss index 8a819471e8..b7428196ab 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options.scss @@ -18,8 +18,7 @@ } .content-class { - overflow-y: auto; - overflow-x: hidden; + overflow: hidden auto; height: calc(100vh - #{$sz-96}); scrollbar-gutter: stable; } @@ -29,9 +28,11 @@ flex-direction: column; gap: var(--sp-s); width: 100%; + /* FIXME: This is hacky and prone to break, we should tackle the whole layout of the sidebar differently */ --sidebar-element-options-height: calc(100vh - #{$sz-88}); + height: var(--sidebar-element-options-height); padding-block-start: var(--sp-s); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss index d66e5de852..7a89a734af 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss @@ -10,11 +10,13 @@ .presets { @include sidebar.option-grid-structure; + grid-column: 1 / -1; } .presets-wrapper { - @extend .asset-element; + @extend %asset-element; + position: relative; grid-column: span 6; display: flex; @@ -24,9 +26,12 @@ .collapsed-icon { @include deprecated.flexCenter; + cursor: pointer; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); transform: rotate(90deg); } @@ -45,6 +50,7 @@ .select-name { @include deprecated.bodySmallTypography; + display: flex; justify-content: flex-start; align-items: center; @@ -53,19 +59,24 @@ } .custom-select-dropdown { - @extend .dropdown-wrapper; + @extend %dropdown-wrapper; + margin-top: deprecated.$s-2; max-height: 70vh; width: deprecated.$s-252; + .dropdown-element { - @extend .dropdown-element-base; + @extend %dropdown-element-base; + .name-wrapper { display: flex; gap: deprecated.$s-8; flex-grow: 1; + .preset-name { color: var(--menu-foreground-color-rest); } + .preset-size { color: var(--menu-foreground-color-rest); } @@ -73,8 +84,10 @@ .check-icon { @include deprecated.flexCenter; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } @@ -82,6 +95,7 @@ &.disabled { pointer-events: none; cursor: default; + .preset-name { color: var(--menu-foreground-color); } @@ -91,6 +105,7 @@ .name-wrapper .preset-name { color: var(--menu-foreground-color-hover); } + .check-icon svg { stroke: var(--menu-foreground-color-hover); } @@ -98,9 +113,11 @@ &:hover { background-color: var(--menu-background-color-hover); + .name-wrapper .preset-name { color: var(--menu-foreground-color-hover); } + .check-icon svg { stroke: var(--menu-foreground-color-hover); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.scss index 6535f728b4..698698ec9d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.scss @@ -9,14 +9,15 @@ .align-options { @include sidebar.option-grid-structure; + height: deprecated.$s-32; } + .align-group-horizontal, .align-group-vertical { display: grid; grid-template-columns: subgrid; - align-items: center; - justify-items: center; + place-items: center center; } .align-group-horizontal { @@ -28,22 +29,29 @@ } .align-button { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-32; width: deprecated.$s-32; padding: 0; border-radius: deprecated.$br-8; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } + &.disabled { cursor: default; + svg { stroke: var(--button-foreground-color-disabled); } + &:hover { background-color: var(--panel-background-color); + svg { stroke: var(--button-foreground-color-disabled); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss index c80e57e1ec..7445581652 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss @@ -22,6 +22,7 @@ .element-set-content { @include deprecated.flexColumn; + margin-bottom: deprecated.$s-8; } @@ -36,25 +37,32 @@ flex-grow: 1; border-radius: deprecated.$br-8; background-color: var(--input-details-color); + .show-more { - @extend .button-secondary; + @extend %button-secondary; + height: deprecated.$s-32; width: deprecated.$s-28; border-radius: deprecated.$br-8 0 0 deprecated.$br-8; box-sizing: border-box; border: deprecated.$s-1 solid var(--button-secondary-background-color-rest); + svg { - @extend .button-icon; + @extend %button-icon; } + &.selected { background-color: var(--button-radio-background-color-active); + svg { stroke: var(--button-radio-foreground-color-active); } } } + .label { @include deprecated.bodySmallTypography; + flex-grow: 1; display: flex; align-items: center; @@ -66,11 +74,13 @@ box-sizing: border-box; border: deprecated.$s-1 solid var(--input-border-color); } + .blur-type-select { flex-grow: 1; border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; } } + .actions { @include deprecated.flexRow; } @@ -78,12 +88,16 @@ &.hidden { .blur-info { @include deprecated.hiddenElement; + .show-more { @include deprecated.hiddenElement; + border: deprecated.$s-1 solid var(--input-border-color-disabled); } + .label { @include deprecated.hiddenElement; + border: deprecated.$s-1 solid var(--input-border-color-disabled); } } @@ -91,9 +105,11 @@ } .second-row { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + width: deprecated.$s-92; + .label { padding-left: deprecated.$s-8; width: deprecated.$s-60; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.scss index ef69c2dd4e..270b922093 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.scss @@ -9,6 +9,7 @@ .boolean-options { @include sidebar.option-grid-structure; + height: var(--sp-xxxl); } @@ -19,26 +20,31 @@ } .flatten-button { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-32; width: deprecated.$s-32; border-radius: deprecated.$br-8; grid-column: 5 / span 1; + --flatten-icon-foreground-color: var(--icon-foreground); &.disabled { cursor: default; + --flatten-icon-foreground-color: var(--button-foreground-color-disabled); &:hover { background-color: var(--panel-background-color); + --flatten-icon-foreground-color: var(--button-foreground-color-disabled); } } } .flatten-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--flatten-icon-foreground-color); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss index 222dc5bd03..e98b076c02 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss @@ -16,7 +16,7 @@ .radius-1, .small-input { - @extend .input-element; + @extend %input-element; @include t.use-typography("body-small"); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss index 7519bcb568..9d8c30d5c0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss @@ -21,17 +21,21 @@ } .add-fill { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-32; width: deprecated.$s-28; + svg { - @extend .button-icon; + @extend %button-icon; } } .element-content { grid-column: span 8; + @include deprecated.flexColumn; + margin-bottom: deprecated.$s-8; } @@ -40,7 +44,8 @@ } .more-colors-btn { - @extend .button-secondary; + @extend %button-secondary; @include deprecated.uppercaseTitleTipography; + height: deprecated.$s-32; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss index 88ca5dd5b6..521c409223 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss @@ -14,6 +14,7 @@ .annotation { @include t.use-typography("body-small"); + grid-column: span 8; color: var(--color-foreground-secondary); border-radius: $br-8; @@ -123,7 +124,7 @@ // easy way to plop the elements on top of each other and have them both sized based on the tallest one's height display: grid; - &:after { + &::after { // The space is needed to preventy jumpy behavior content: attr(data-replicated-value) " "; white-space: pre-wrap; @@ -132,7 +133,6 @@ /* Identical styling required!! */ font: inherit; overflow-wrap: anywhere; - padding: var(--sp-m); /* Place on top of each other */ @@ -143,20 +143,15 @@ .annotation-textarea { background-color: var(--color-background-primary); color: var(--color-foreground-primary); - padding: var(--sp-m); - border: none; overflow: hidden; outline: none; - box-shadow: none; - resize: none; /* Identical styling required!! */ font: inherit; overflow-wrap: anywhere; - padding: var(--sp-m); /* Place on top of each other */ @@ -165,6 +160,7 @@ .annotation-counter { @include t.use-typography("body-small"); + text-align: right; color: var(--color-foreground-secondary); margin: 0 var(--sp-s) var(--sp-s) 0; @@ -181,6 +177,7 @@ --swap-item-thumbnail-background-color: var(--color-canvas); @include t.use-typography("body-small"); + display: flex; align-items: center; padding: px2rem(1) var(--sp-m) px2rem(1) px2rem(1); @@ -237,8 +234,6 @@ --swap-item-thumbnail-background-color-disabled: var(--color-foreground-secondary); display: flex; - justify-content: center; - align-items: center; place-items: center; aspect-ratio: 1 / 1; flex-wrap: wrap; @@ -257,6 +252,7 @@ .swap-item-name { @include t.use-typography("body-small"); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -295,10 +291,8 @@ &::before { content: " "; position: absolute; - inset-inline-start: 0; - inset-inline-end: 0; - inset-block-start: 0; - inset-block-end: 0; + inset-inline: 0; + inset-block: 0; border: calc($b-2 * 2) solid var(--swap-item-border-inner-color-selected); border-radius: $br-8; } @@ -334,6 +328,7 @@ .swap-group { @include t.use-typography("body-small"); + cursor: pointer; display: grid; grid-template-columns: 1fr var(--sp-m); @@ -365,6 +360,7 @@ .swap-title { @include t.use-typography("headline-small"); + display: flex; align-items: center; block-size: $sz-32; @@ -397,6 +393,7 @@ .swap-library-name { @include t.use-typography("body-small"); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -419,6 +416,7 @@ .swap-library-back-name { @include t.use-typography("body-small"); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -429,6 +427,7 @@ .swap-library-empty { @include t.use-typography("body-small"); + margin: 0 var(--sp-xs) 0 var(--sp-s); color: var(--color-foreground-secondary); } @@ -457,6 +456,7 @@ .component-title-swap { @include t.use-typography("headline-small"); + cursor: pointer; display: flex; align-items: center; @@ -482,6 +482,7 @@ .component-title-bar-type { @include t.use-typography("body-small"); + block-size: 100%; display: flex; align-items: center; @@ -498,8 +499,7 @@ display: flex; flex-direction: column; row-gap: var(--sp-m); - padding-block-start: var(--sp-xs); - padding-block-end: var(--sp-s); + padding-block: var(--sp-xs) var(--sp-s); } .component-pill { @@ -562,6 +562,7 @@ .pill-btn-text { @include t.use-typography("body-small"); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -572,6 +573,7 @@ .pill-btn-subtext { @include t.use-typography("body-small"); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -587,19 +589,21 @@ } .pill-actions-btn { - @extend .button-secondary; + @extend %button-secondary; + cursor: unset; block-size: 100%; inline-size: 100%; border-radius: 0 $br-8 $br-8 0; &.selected { - @extend .button-icon-selected; + @extend %button-icon-selected; } } .pill-actions-dropdown { - @extend .dropdown-wrapper; + @extend %dropdown-wrapper; + inline-size: $sz-252; inset-inline-end: 0; inset-inline-start: unset; @@ -610,12 +614,11 @@ } .pill-actions-dropdown-item { - @extend .dropdown-element-base; + @extend %dropdown-element-base; } .variant-property-list { grid-column: span 8; - display: grid; flex-direction: column; gap: var(--sp-xs); @@ -672,6 +675,7 @@ .variant-property-name { @include t.use-typography("body-small"); + margin-inline-start: var(--sp-s); color: var(--color-foreground-secondary); display: block; @@ -682,8 +686,8 @@ .variant-warning { @include t.use-typography("body-small"); - grid-column: span 8; + grid-column: span 8; border: $b-1 solid var(--color-background-quaternary); border-radius: $br-8; padding: var(--sp-m); @@ -702,6 +706,7 @@ .variant-warning-button { @include t.use-typography("body-small"); + cursor: pointer; background-color: transparent; border: none; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss index 5f7578afe1..5aec6eef23 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss @@ -18,12 +18,7 @@ .constraints-widget { background-color: var(--constraint-widget-background-color); display: grid; - grid-template-columns: deprecated.$s-24 deprecated.$s-60 deprecated.$s-24; - grid-template-rows: deprecated.$s-24 deprecated.$s-60 deprecated.$s-24; - grid-template-areas: - "top top top" - "left center right" - "bottom bottom bottom"; + grid-template: "top top top" deprecated.$s-24 "left center right" deprecated.$s-60 "bottom bottom bottom" deprecated.$s-24 / deprecated.$s-24 deprecated.$s-60 deprecated.$s-24; height: deprecated.$s-108; width: deprecated.$s-108; border-radius: deprecated.$br-8; @@ -35,21 +30,27 @@ .constraints-right, .constraints-bottom { @include deprecated.flexCenter; + grid-area: top; } + .constraint-btn, .constraint-btn-special, .constraint-btn-rotated { @include deprecated.buttonStyle; @include deprecated.flexCenter; + width: 100%; height: 100%; + --resalted-area-background-color: var(--button-constraint-background-color-rest); --resalted-area-border-color: none; + &.active { --resalted-area-border-color: var(--button-constraint-border-color-hover); --resalted-area-background-color: var(--button-constraint-background-color-hover); } + &:hover, &:focus-visible { --resalted-area-border-color: var(--button-constraint-border-color-hover); @@ -69,9 +70,11 @@ .constraints-left { grid-area: left; + .constraint-btn-rotated { height: deprecated.$s-60; width: deprecated.$s-24; + .resalted-area { height: deprecated.$s-32; width: deprecated.$s-3; @@ -84,18 +87,22 @@ position: relative; background-color: var(--constraint-center-area-background-color); border-radius: deprecated.$br-8; + .constraint-btn { width: deprecated.$s-60; height: deprecated.$s-24; + .resalted-area { width: deprecated.$s-32; height: deprecated.$s-3; } } + .constraint-btn-special { position: absolute; height: deprecated.$s-60; width: deprecated.$s-24; + .resalted-area { height: deprecated.$s-32; width: deprecated.$s-3; @@ -105,9 +112,11 @@ .constraints-right { grid-area: right; + .constraint-btn-rotated { height: deprecated.$s-72; width: deprecated.$s-24; + .resalted-area { height: deprecated.$s-32; width: deprecated.$s-3; @@ -137,33 +146,42 @@ margin-bottom: deprecated.$s-8; margin-top: deprecated.$s-8; padding-left: 0; + input { margin: 0; } label { @include deprecated.bodySmallTypography; + display: flex; align-items: center; gap: deprecated.$s-2; cursor: pointer; color: var(--input-checkbox-text-foreground-color); + .check-mark { @include deprecated.flexCenter; + width: deprecated.$s-16; height: deprecated.$s-16; border-radius: deprecated.$br-6; background-color: var(--input-checkbox-inactive-background-color); + &.checked { background-color: var(--input-checkbox-background-color-active); + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--input-details-color); } } + &:hover { border-color: var(--input-checkbox-border-color-hover); } + &:focus { border-color: var(--input-checkbox-border-color-focus); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss index 487e4805bc..a1b4706f9a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss @@ -22,15 +22,19 @@ .element-set-content { @include sidebar.option-grid-structure; + gap: var(--sp-xs); } .multiple-exports { @include deprecated.flexRow; + grid-column: 1 / span 9; + .label { - @extend .mixed-bar; + @extend %mixed-bar; } + .actions { @include deprecated.flexRow; } @@ -62,6 +66,7 @@ .size-select { grid-column: span 2; padding: 0; + .dropdown-upwards { bottom: deprecated.$s-36; top: unset; @@ -71,13 +76,15 @@ .suffix-input { grid-column: span 3; - @extend .input-element; + + @extend %input-element; @include deprecated.bodySmallTypography; } .export-btn { - @extend .button-secondary; + @extend %button-secondary; @include deprecated.uppercaseTitleTipography; + grid-column: 1 / span 9; height: deprecated.$s-32; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.scss index 28a159b4de..a9d920386e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.scss @@ -24,7 +24,6 @@ .fill-content { grid-column: span 8; - display: flex; flex-direction: column; gap: var(--sp-m); @@ -39,6 +38,7 @@ .fill-multiple-label { @include t.use-typography("body-small"); + display: flex; align-items: center; flex-grow: 1; @@ -51,12 +51,16 @@ .fill-checkbox { // TODO create a checkbox component in the DS - @extend .input-checkbox; + @extend %input-checkbox; + padding-inline-start: var(--sp-s); + span.checked { background-color: var(--color-accent-primary); + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--color-background-primary); } } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss index 51b82c5434..52171dde2e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss @@ -22,6 +22,7 @@ .element-set-content { @include deprecated.flexColumn; + grid-column: span 8; margin: deprecated.$s-4 0 deprecated.$s-8 0; } @@ -37,70 +38,87 @@ gap: deprecated.$s-1; border-radius: deprecated.$br-8; background-color: var(--input-details-color); + .show-options { - @extend .button-secondary; + @extend %button-secondary; + height: deprecated.$s-32; width: deprecated.$s-28; border-radius: deprecated.$br-8 0 0 deprecated.$br-8; box-sizing: border-box; border: deprecated.$s-1 solid var(--input-border-color); + svg { - @extend .button-icon; + @extend %button-icon; } + &.selected { - @extend .button-icon-selected; + @extend %button-icon-selected; } } + .type-select-wrapper { flex-grow: 1; width: deprecated.$s-96; padding: 0; border-radius: 0; height: deprecated.$s-32; + .grid-type-select { border-radius: 0; height: 100%; box-sizing: border-box; border: deprecated.$s-1 solid var(--input-border-color); + &:hover { border: deprecated.$s-1 solid var(--input-border-color-hover); } } } + .grid-size { - @extend .asset-element; + @extend %asset-element; + width: deprecated.$s-60; margin: 0; padding: 0; padding-left: deprecated.$s-8; border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; + .numeric-input { - @extend .input-base; + @extend %input-base; @include deprecated.bodySmallTypography; } } + .editable-select-wrapper { - @extend .asset-element; + @extend %asset-element; + width: deprecated.$s-60; margin: 0; padding: 0; position: relative; border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; + .column-select { height: deprecated.$s-32; border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; box-sizing: border-box; border: deprecated.$s-1 solid var(--input-border-color); + .numeric-input { - @extend .input-base; + @extend %input-base; @include deprecated.bodySmallTypography; + margin: 0; padding: 0; } + span { @include deprecated.flexCenter; + svg { - @extend .button-icon; + @extend %button-icon; } } } @@ -109,38 +127,51 @@ &.hidden { .show-options { @include deprecated.hiddenElement; + border: deprecated.$s-1 solid var(--input-border-color-disabled); } + .type-select-wrapper, .editable-select-wrapper { @include deprecated.hiddenElement; + .column-select, .grid-type-select { @include deprecated.hiddenElement; + border: deprecated.$s-1 solid var(--input-border-color-disabled); } + .column-select { @include deprecated.hiddenElement; + border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; + .numeric-input { @include deprecated.hiddenElement; } } } + .grid-size { @include deprecated.hiddenElement; + border: deprecated.$s-1 solid var(--input-border-color-disabled); + .icon { stroke: var(--input-foreground-color-disabled); } + .numeric-input { color: var(--input-foreground-color-disabled); } } + .actions { .hidden-btn, .lock-btn { background-color: transparent; + svg { stroke: var(--input-foreground-color-disabled); } @@ -151,17 +182,20 @@ .actions { @include deprecated.flexRow; + grid-column: span 2; } .grid-advanced-options { @include deprecated.flexColumn; + margin-top: deprecated.$s-4; } .column-row, .square-row { @include deprecated.flexColumn; + position: relative; } @@ -169,35 +203,45 @@ position: relative; display: flex; gap: deprecated.$s-4; + .orientation-select-wrapper { width: deprecated.$s-92; padding: 0; } + .color-wrapper { width: deprecated.$s-156; } + .show-more-options { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-32; width: deprecated.$s-32; + svg { - @extend .button-icon; + @extend %button-icon; } + &.selected { - @extend .button-icon-selected; + @extend %button-icon-selected; } } + .height { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + .icon-text { padding-top: deprecated.$s-1; } } + .gutter, .margin { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + .icon { &.rotated svg { transform: rotate(90deg); @@ -208,6 +252,7 @@ .more-options { @include deprecated.menuShadow; @include deprecated.flexColumn; + position: absolute; top: calc(deprecated.$s-2 + deprecated.$s-28); right: 0; @@ -220,8 +265,10 @@ z-index: deprecated.$z-index-4; overflow-y: auto; background-color: var(--menu-background-color); + .option-btn { @include deprecated.buttonStyle; + display: flex; align-items: center; height: deprecated.$s-32; @@ -238,13 +285,16 @@ } .second-row { - @extend .dropdown-wrapper; + @extend %dropdown-wrapper; + left: unset; right: 0; width: deprecated.$s-108; + .btn-options { @include deprecated.buttonStyle; - @extend .dropdown-element-base; + @extend %dropdown-element-base; + width: 100%; } } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss index 5b61b4dabf..c54da28b43 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss @@ -9,6 +9,7 @@ .grid-cell-menu-container { @include deprecated.flexColumn; + margin-top: deprecated.$s-8; gap: deprecated.$s-16; } @@ -35,34 +36,39 @@ } .edit-grid-btn { - @extend .button-secondary; + @extend %button-secondary; @include deprecated.uppercaseTitleTipography; + width: 100%; padding: deprecated.$s-8; } .area-input { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + width: 100%; padding: deprecated.$s-8; } .grid-coord-group { @include deprecated.flexRow; + border-radius: deprecated.$br-8; padding-left: deprecated.$s-4; background-color: var(--input-background-color); } .icon svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } .coord-input { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; border-left: deprecated.$s-1 solid var(--panel-background-color); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.scss index aeeaa99191..3097f16e76 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.scss @@ -5,5 +5,5 @@ // Copyright (c) KALEIDOS INC .numeric-input-wrapper { - --dropdown-width: var(--7-columns-width); + --dropdown-width: var(--seven-columns-width); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss index 066145fa14..c4457a8aed 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss @@ -32,7 +32,6 @@ .content { grid-column: span 8; - display: flex; flex-direction: column; gap: var(--sp-xs); @@ -62,12 +61,12 @@ align-items: center; gap: px2rem(1); border-radius: $br-8; - padding: var(--sp-s) var(--sp-m); block-size: $sz-32; padding: 0; &.double { block-size: $sz-48; + .prototype-pill-button { block-size: $sz-48; } @@ -114,13 +113,12 @@ .prototype-pill-input { @include t.use-typography("body-small"); + border: none; background: none; outline: none; block-size: 100%; - inline-size: 100%; flex-grow: 1; - margin: var(--sp-xxs) 0; padding: 0 0 0 var(--sp-s); margin: 0; background-color: var(--color-background-tertiary); @@ -130,6 +128,7 @@ &:hover { background-color: var(--color-background-quaternary); + &:active { background-color: var(--color-background-quaternary); } @@ -142,13 +141,15 @@ .prototype-pill-name { @include t.use-typography("body-small"); - @include textEllipsis; + @include text-ellipsis; + color: var(--color-foreground-primary); } .prototype-pill-description { @include t.use-typography("body-small"); - @include textEllipsis; + @include text-ellipsis; + color: var(--color-foreground-secondary); } @@ -164,8 +165,9 @@ } .interaction-row-name { - @include twoLineTextEllipsis; + @include two-line-text-ellipsis; @include t.use-typography("body-small"); + color: var(--color-foreground-secondary); } @@ -191,12 +193,10 @@ .interaction-row-position { grid-column: 4 / span 5; display: grid; - grid-template-areas: - "topleft top topright" - "left center right" - "bottomleft bottom bottomright"; - grid-template-columns: repeat(3, 1fr); - grid-template-rows: repeat(3, 1fr); + grid-template: + "topleft top topright" 1fr + "left center right" 1fr + "bottomleft bottom bottomright" 1fr / repeat(3, 1fr); inline-size: calc($sz-32 * 3); block-size: calc($sz-32 * 3); border-radius: $br-8; @@ -205,21 +205,27 @@ .center { grid-area: center; } + .top-left { grid-area: topleft; } + .top-center { grid-area: top; } + .top-right { grid-area: topright; } + .bottom-left { grid-area: bottomleft; } + .bottom-center { grid-area: bottom; } + .bottom-right { grid-area: bottomright; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss index 21fe114676..333cb66bbf 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss @@ -15,17 +15,22 @@ // https://tree.taiga.io/project/penpot/task/13704 .element-set-content { @include sidebar.option-grid-structure; + block-size: $sz-32; margin-block-end: var(--sp-s); + .select { grid-column: span 4; padding: 0; } + .input { - @extend .input-element; + @extend %input-element; @include t.use-typography("body-small"); + grid-column: span 2; } + .actions { grid-column: span 2; display: grid; @@ -42,6 +47,7 @@ background-color: transparent; border: $b-1 solid var(--input-border-color-disabled); } + .input { cursor: default; pointer-events: none; @@ -50,9 +56,11 @@ stroke: var(--input-foreground-color-disabled); background-color: transparent; border: $b-1 solid var(--input-border-color-disabled); + .icon { stroke: var(--input-foreground-color-disabled); } + .numeric-input { color: var(--input-foreground-color-disabled); } @@ -67,6 +75,7 @@ // activated removing the token reference on the class .element-set-content-token { @include sidebar.option-grid-structure; + block-size: $sz-32; margin-block-end: var(--sp-s); grid-template-columns: var(--grid-exception-input-width) var(--grid-exception-input-width-small) auto auto; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss index a0311d38a6..c4ec2b3f77 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss @@ -42,6 +42,7 @@ .flex-layout-menu { @include sidebar.option-grid-structure; + margin-block-end: var(--sp-s); } @@ -49,8 +50,7 @@ grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; - margin-block-end: var(--sp-m); - margin-block-start: var(--sp-xs); + margin-block: var(--sp-xs) var(--sp-m); } .align-row { @@ -63,16 +63,20 @@ // TODO: Replace this buttons with DS buttons .wrap-button { - @extend .button-tertiary; + @extend %button-tertiary; + border-radius: $br-8; block-size: $sz-32; inline-size: $sz-32; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--color-foreground-secondary); } + &.selected { - @extend .button-icon-selected; + @extend %button-icon-selected; } } @@ -95,6 +99,7 @@ var(--grid-exception-input-width) /* first input block */ var(--grid-exception-input-width) /* second input block */ var(--sp-xxxl); /* action button */ + gap: var(--sp-xs); grid-column: 1 / -1; } @@ -115,10 +120,11 @@ // TODO: Remove when activating token numeric inputs .column-gap, .row-gap { - @extend .input-element; + @extend %input-element; @include t.use-typography("body-small"); + &.disabled { - @extend .disabled-input; + @extend %disabled-input; } } @@ -126,7 +132,7 @@ .padding-simple, .padding-multiple { @include t.use-typography("body-small"); - @extend .input-element; + @extend %input-element; } .padding-group { @@ -151,16 +157,20 @@ // TODO: Replace this buttons with DS buttons .padding-toggle { - @extend .button-tertiary; + @extend %button-tertiary; + block-size: $sz-32; inline-size: $sz-32; border-radius: $br-8; + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--color-foreground-secondary); } + &.selected { - @extend .button-icon-selected; + @extend %button-icon-selected; } } @@ -198,6 +208,7 @@ .grid-layout-menu-title { @include t.use-typography("headline-small"); + flex: 1; color: var(--color-foreground-primary); grid-column: span 5; @@ -205,8 +216,9 @@ // TODO: Replace this buttons with DS buttons .edit-mode-btn { - @extend .button-secondary; + @extend %button-secondary; @include t.use-typography("headline-small"); + inline-size: 100%; padding: var(--sp-s); grid-column: span 7; @@ -214,8 +226,9 @@ // TODO: Replace this buttons with DS buttons .exit-btn { - @extend .button-secondary; + @extend %button-secondary; @include t.use-typography("headline-small"); + padding: var(--sp-s) var(--sp-xl); grid-column: span 2; } @@ -268,19 +281,23 @@ border-radius: $br-8 0 0 $br-8; background-color: var(--color-background-tertiary); padding: 0 var(--sp-s); + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--color-foreground-secondary); block-size: 100%; } + &:hover svg { stroke: var(--color-foreground-primary); } } .track-info-value { - @extend .input-element; + @extend %input-element; @include t.use-typography("body-small"); + border-radius: 0; border-inline-end: $b-1 solid var(--color-background-primary); } @@ -299,6 +316,7 @@ .grid-track-header { @include t.use-typography("body-small"); + display: flex; align-items: center; gap: var(--sp-xs); @@ -331,16 +349,19 @@ // TODO: Replace this buttons with DS buttons .expand-icon { - @extend .button-secondary; - block-size: px2rem(52); + @extend %button-secondary; + block-size: px2rem(52); border-radius: $br-8 0 0 $br-8; border-inline-end: $b-1 solid var(--color-background-primary); + svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--color-foreground-secondary); fill: var(--color-foreground-secondary); } + &:hover, &:active { svg { @@ -352,7 +373,8 @@ // TODO: Replace this buttons with DS buttons .add-column { - @extend .button-tertiary; + @extend %button-tertiary; + block-size: px2rem(52); svg { @@ -360,7 +382,6 @@ justify-content: center; align-items: center; color: transparent; - fill: none; stroke-width: px2rem(1); block-size: $sz-12; inline-size: $sz-12; @@ -370,7 +391,7 @@ } .layout-options { - box-shadow: 0px 0px $sz-12 0px var(--color-shadow-dark); + box-shadow: 0 0 $sz-12 0 var(--color-shadow-dark); position: absolute; display: flex; flex-direction: column; @@ -383,8 +404,7 @@ margin-block-start: px2rem(1); border-radius: $br-8; z-index: var(--z-index-dropdown); - overflow-y: auto; - overflow-x: hidden; + overflow: hidden auto; background-color: var(--color-background-tertiary); color: var(--color-foreground-primary); border: $b-2 solid var(--color-background-quaternary); @@ -424,6 +444,7 @@ var(--grid-exception-input-width) /* first input block */ var(--grid-exception-input-width) /* second input block */ var(--sp-xxxl); /* action button */ + gap: var(--sp-xs); margin-block-end: var(--sp-xs); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.scss index d54b68e918..90a7f772f4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.scss @@ -25,6 +25,7 @@ .flex-element-menu { @include sidebar.option-grid-structure; + gap: var(--sp-xs); } @@ -42,7 +43,8 @@ .z-index-wrapper { @include use-typography("body-small"); - @extend .input-element; + @extend %input-element; + grid-column: 6 / span 3; } @@ -71,18 +73,22 @@ var(--grid-exception-input-width) /* first input block */ var(--grid-exception-input-width) /* second input block */ var(--sp-xxxl); /* action button */ + gap: var(--sp-xs); } .margin-mode { - @extend .button-tertiary; + @extend %button-tertiary; + grid-column: 3; height: deprecated.$s-32; + svg { - @extend .button-icon; + @extend %button-icon; } + &.selected { - @extend .button-icon-selected; + @extend %button-icon-selected; } } @@ -90,14 +96,17 @@ display: grid; gap: var(--sp-xs); grid-template-columns: subgrid; + .vertical-margin, .horizontal-margin { - @extend .input-element; + @extend %input-element; @include use-typography("body-small"); } + .vertical-margin { grid-column: 1; } + .horizontal-margin { grid-column: 2; } @@ -121,7 +130,7 @@ .bottom-margin, .left-margin, .right-margin { - @extend .input-element; + @extend %input-element; @include use-typography("body-small"); } @@ -155,6 +164,7 @@ var(--grid-exception-input-width) /* first input block */ var(--grid-exception-input-width) /* second input block */ var(--sp-xxxl); /* action button */ + gap: var(--sp-xs); } @@ -169,8 +179,9 @@ .layout-item-min-h, .layout-item-max-w, .layout-item-max-h { - @extend .input-element; + @extend %input-element; @include use-typography("body-small"); + .icon-text { justify-content: flex-start; inline-size: px2rem(80); diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss index c75c161942..e6266df974 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss @@ -14,17 +14,20 @@ var(--grid-exception-input-width) /* first input block */ var(--grid-exception-input-width) /* second input block */ var(--sp-xxxl); /* action button */ + gap: var(--sp-xs); margin-bottom: var(--sp-s); } .presets { @include sidebar.option-grid-structure; + grid-column: 1 / -1; } .presets-wrapper { - @extend .asset-element; + @extend %asset-element; + position: relative; grid-column: span 5; display: flex; @@ -34,9 +37,12 @@ .collapsed-icon { @include deprecated.flexCenter; + cursor: pointer; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); transform: rotate(90deg); } @@ -55,6 +61,7 @@ .select-name { @include deprecated.bodySmallTypography; + display: flex; justify-content: flex-start; align-items: center; @@ -63,19 +70,24 @@ } .custom-select-dropdown { - @extend .dropdown-wrapper; + @extend %dropdown-wrapper; + margin-top: deprecated.$s-2; max-height: 70vh; width: deprecated.$s-252; + .dropdown-element { - @extend .dropdown-element-base; + @extend %dropdown-element-base; + .name-wrapper { display: flex; gap: deprecated.$s-8; flex-grow: 1; + .preset-name { color: var(--menu-foreground-color-rest); } + .preset-size { color: var(--menu-foreground-color-rest); } @@ -83,8 +95,10 @@ .check-icon { @include deprecated.flexCenter; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } @@ -92,6 +106,7 @@ &.disabled { pointer-events: none; cursor: default; + .preset-name { color: var(--menu-foreground-color); } @@ -101,6 +116,7 @@ .name-wrapper .preset-name { color: var(--menu-foreground-color-hover); } + .check-icon svg { stroke: var(--menu-foreground-color-hover); } @@ -108,9 +124,11 @@ &:hover { background-color: var(--menu-background-color-hover); + .name-wrapper .preset-name { color: var(--menu-foreground-color-hover); } + .check-icon svg { stroke: var(--menu-foreground-color-hover); } @@ -131,13 +149,15 @@ .x-position, .y-position, .rotation { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + .icon-text { padding-top: deprecated.$s-1; } + &.disabled { - @extend .disabled-input; + @extend %disabled-input; } } @@ -146,17 +166,20 @@ } .lock-size-btn { - @extend .button-tertiary; + @extend %button-tertiary; + border-radius: deprecated.$br-8; height: deprecated.$s-32; width: deprecated.$s-28; + &.selected { - @extend .button-icon-selected; + @extend %button-icon-selected; } } .lock-ratio-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--icon-foreground); } @@ -175,21 +198,22 @@ } .clip-content-label { - @extend .button-tertiary; + @extend %button-tertiary; + height: var(--sp-xxxl); width: var(--sp-xxxl); border-radius: deprecated.$br-8; } .selected { - @extend .button-icon-selected; + @extend %button-icon-selected; } .checkbox-button { - @extend .button-icon; + @extend %button-icon; } // TODO: Add a proper variable to this sizing .numeric-input-measures { - --dropdown-width: var(--7-columns-width); + --dropdown-width: var(--seven-columns-width); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.scss index 4e2453ff6d..310ac1eb8c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.scss @@ -23,7 +23,6 @@ .shadow-content { grid-column: span 8; - display: flex; flex-direction: column; gap: var(--sp-xs); @@ -38,6 +37,7 @@ .shadow-multiple-label { @include t.use-typography("body-small"); + display: flex; align-items: center; flex-grow: 1; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.scss index 874beac840..ab0f622225 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.scss @@ -23,7 +23,6 @@ .stroke-content { grid-column: span 8; - display: flex; flex-direction: column; gap: var(--sp-m); @@ -42,6 +41,7 @@ .stroke-multiple-label { @include t.use-typography("body-small"); + display: flex; align-items: center; flex-grow: 1; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss index 50ba70a209..ccbb34de36 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss @@ -17,6 +17,7 @@ .element-set-content { @include deprecated.flexColumn; + margin: deprecated.$s-4 0 0 0; } @@ -28,6 +29,7 @@ .attr-name { @include deprecated.bodySmallTypography; @include deprecated.twoLineTextEllipsis; + width: deprecated.$s-88; margin: auto deprecated.$s-4; margin-right: 0; @@ -36,8 +38,9 @@ } .attr-input { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + width: deprecated.$s-124; } @@ -47,11 +50,13 @@ } .attr-action-btn { - @extend .button-tertiary; + @extend %button-tertiary; + width: deprecated.$s-28; height: deprecated.$s-32; + svg { - @extend .button-icon; + @extend %button-icon; } } @@ -62,6 +67,7 @@ .attr-title { @include deprecated.bodySmallTypography; + font-size: deprecated.$fs-10; text-transform: uppercase; margin-inline-start: deprecated.$s-4; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss index aa97d0ee32..44e721d5b8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss @@ -17,26 +17,31 @@ .element-content { grid-column: span 8; + @include deprecated.flexColumn; + margin-top: deprecated.$s-4; } .multiple-typography { - @extend .mixed-bar; + @extend %mixed-bar; } .multiple-text { @include deprecated.bodySmallTypography; + flex-grow: 1; color: var(--input-foreground-color-active); } .multiple-typography-button { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-32; width: deprecated.$s-28; + svg { - @extend .button-icon; + @extend %button-icon; } } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss index 506fb58b34..a9f91e206a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss @@ -15,6 +15,7 @@ border-radius: deprecated.$br-8; background-color: var(--assets-item-background-color); color: var(--assets-item-name-foreground-color-hover); + &:hover, &:focus-within { background-color: var(--assets-item-background-color-hover); @@ -28,14 +29,18 @@ .element-set-actions { display: flex; visibility: hidden; + .element-set-actions-button, .menu-btn { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-32; width: deprecated.$s-28; + svg { - @extend .button-icon; + @extend %button-icon; } + &:active { background-color: transparent; } @@ -44,6 +49,7 @@ &:hover { background-color: var(--assets-item-background-color-hover); + .element-set-actions { visibility: visible; } @@ -65,6 +71,7 @@ .typography-sample { @include deprecated.flexCenter; + min-width: deprecated.$s-24; height: deprecated.$s-32; color: var(--assets-item-name-foreground-color); @@ -74,6 +81,7 @@ .typography-font { @include deprecated.bodySmallTypography; @include deprecated.textEllipsis; + display: block; align-self: center; margin-left: deprecated.$s-6; @@ -89,6 +97,7 @@ .font-name-wrapper { @include deprecated.bodySmallTypography; + display: flex; align-items: center; height: deprecated.$s-32; @@ -102,38 +111,49 @@ .typography-sample-input { @include deprecated.flexCenter; + width: deprecated.$s-24; height: 100%; font-size: deprecated.$fs-16; color: var(--assets-item-name-foreground-color-hover); } + .adv-typography-name { @include deprecated.removeInputStyle; + font-size: deprecated.$fs-12; color: var(--input-foreground-color-active); flex-grow: 1; padding-left: deprecated.$s-6; margin: 0; } + .action-btn { - @extend .button-tertiary; + @extend %button-tertiary; @include deprecated.flexCenter; + width: deprecated.$s-28; height: deprecated.$s-28; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } + &:active { background-color: transparent; } } + &:focus-within { border: deprecated.$s-1 solid var(--input-border-color-active); + .adv-typography-name { color: var(--input-foreground-color-active); } } + &:hover { background-color: var(--assets-item-background-color-hover); } @@ -147,9 +167,12 @@ .typography-info-wrapper { @include deprecated.flexColumn; + margin-bottom: deprecated.$s-12; + .typography-name-wrapper { - @extend .asset-element; + @extend %asset-element; + display: grid; grid-template-columns: deprecated.$s-24 auto 1fr deprecated.$s-28; flex: 1; @@ -158,26 +181,32 @@ padding: 0 0 0 deprecated.$s-12; background-color: var(--assets-item-background-color-hover); margin-bottom: deprecated.$s-4; + .typography-sample { @include deprecated.flexCenter; + min-width: deprecated.$s-24; font-size: deprecated.$fs-16; height: deprecated.$s-32; padding: 0; color: var(--assets-item-name-foreground-color-hover); } + .typography-name { @include deprecated.bodySmallTypography; @include deprecated.textEllipsis; + display: flex; align-items: center; justify-content: flex-start; margin-left: deprecated.$s-6; color: var(--assets-item-name-foreground-color-hover); } + .typography-font { @include deprecated.bodySmallTypography; @include deprecated.textEllipsis; + margin-left: deprecated.$s-6; display: flex; align-items: center; @@ -185,13 +214,17 @@ min-width: 0; color: var(--assets-item-name-foreground-color); } + .action-btn { - @extend .button-tertiary; + @extend %button-tertiary; + width: deprecated.$s-28; height: deprecated.$s-32; + svg { - @extend .button-icon; + @extend %button-icon; } + &:active { background-color: transparent; } @@ -202,18 +235,24 @@ display: grid; grid-template-columns: 50% 50%; height: deprecated.$s-32; + --calculated-width: calc(var(--right-sidebar-width) - deprecated.$s-48); + padding-left: deprecated.$s-2; + .info-label { @include deprecated.bodySmallTypography; @include deprecated.textEllipsis; + width: calc(var(--calculated-width) / 2); padding-top: deprecated.$s-8; color: var(--assets-item-name-foreground-color); } + .info-content { @include deprecated.bodySmallTypography; @include deprecated.textEllipsis; + padding-top: deprecated.$s-8; width: calc(var(--calculated-width) / 2); color: var(--assets-item-name-foreground-color-hover); @@ -222,23 +261,28 @@ .link-btn { @include deprecated.uppercaseTitleTipography; - @extend .button-secondary; + @extend %button-secondary; + width: 100%; height: deprecated.$s-32; border-radius: deprecated.$br-8; + &:hover { background-color: var(--button-secondary-background-color-hover); color: var(--button-secondary-foreground-color-hover); border: deprecated.$s-1 solid var(--button-secondary-border-color-hover); text-decoration: none; + svg { stroke: var(--button-secondary-foreground-color-hover); } } + &:focus { background-color: var(--button-secondary-background-color-focus); color: var(--button-secondary-foreground-color-focus); border: deprecated.$s-1 solid var(--button-secondary-border-color-focus); + svg { stroke: var(--button-secondary-foreground-color-focus); } @@ -248,14 +292,17 @@ .text-options { @include deprecated.flexColumn; + max-width: var(--options-width); &:not(.text-options-full-size) { position: relative; } + .font-option { @include deprecated.bodySmallTypography; - @extend .asset-element; + @extend %asset-element; + padding: deprecated.$s-8 0 deprecated.$s-8 deprecated.$s-8; cursor: pointer; @@ -267,23 +314,30 @@ white-space: nowrap; width: 100%; } + .icon { @include deprecated.flexCenter; + height: deprecated.$s-28; width: deprecated.$s-28; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); transform: rotate(90deg); } } } + .font-modifiers { display: flex; gap: deprecated.$s-4; + .font-size-options { - @extend .asset-element; + @extend %asset-element; @include deprecated.bodySmallTypography; + flex-grow: 1; width: deprecated.$s-60; margin: 0; @@ -293,42 +347,55 @@ .icon { @include deprecated.flexCenter; + height: deprecated.$s-28; min-width: deprecated.$s-28; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); transform: rotate(90deg); } } } + .font-variant-options { padding: 0; flex-grow: 2; } } + .typography-variations { @include deprecated.flexRow; + .spacing-options { @include deprecated.flexRow; + .line-height, .letter-spacing { - @extend .input-element; + @extend %input-element; @include deprecated.bodySmallTypography; + .icon { @include deprecated.flexCenter; + width: deprecated.$s-28; + svg { - @extend .button-icon-small; + @extend %button-icon-small; } } } } + .text-transform { - @extend .asset-element; + @extend %asset-element; + width: fit-content; padding: 0; background-color: var(--radio-btns-background-color); + &:hover { background-color: var(--radio-btns-background-color); } @@ -339,20 +406,24 @@ .font-size-select { @include deprecated.removeInputStyle; @include deprecated.bodySmallTypography; + height: deprecated.$s-32; height: 100%; width: 100%; margin: 0; padding: deprecated.$s-8; + .numeric-input { - @extend .input-base; + @extend %input-base; @include deprecated.bodySmallTypography; + padding: 0; } } .font-selector { @include deprecated.flexCenter; + position: absolute; top: 0; left: 0; @@ -371,17 +442,21 @@ .font-selector-dropdown { width: 100%; + &:not(.font-selector-dropdown-full-size) { display: flex; flex-direction: column; flex-grow: 1; height: 100%; } + .header { display: grid; row-gap: deprecated.$s-2; + .title { @include deprecated.uppercaseTitleTipography; + color: var(--title-foreground-color); margin: 0; padding: deprecated.$s-12; @@ -395,21 +470,28 @@ } .font-item { - @extend .asset-element; + @extend %asset-element; + margin-bottom: deprecated.$s-4; border-radius: deprecated.$br-8; display: flex; + .icon { @include deprecated.flexCenter; + height: deprecated.$s-28; width: deprecated.$s-28; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } + &.selected { color: var(--assets-item-name-foreground-color-hover); + .icon { svg { stroke: var(--assets-item-name-foreground-color-hover); @@ -420,6 +502,7 @@ .label { @include deprecated.bodySmallTypography; @include deprecated.textEllipsis; + flex-grow: 1; min-width: 0; } @@ -444,6 +527,7 @@ border-radius: deprecated.$br-8; background-color: var(--dropdown-background-color); overflow: hidden; + &:not(.fonts-list-full-size) { margin-block-start: deprecated.$s-2; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.scss index 5d77b269d2..98b71bd436 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.scss @@ -7,11 +7,11 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-dialog { - @extend .modal-container-base; + @extend %modal-container-base; max-width: deprecated.$s-888; width: 100%; @@ -31,7 +31,7 @@ } .modal-close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .rule-list { diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss index 42f5dceefe..39f56950d8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss @@ -54,8 +54,10 @@ --color-name-wrapper-background-color: var(--color-background-tertiary); --color-name-wrapper-foreground-color: var(--color-foreground-primary); --color-name-wrapper-boder-color: var(--color-background-tertiary); + @include t.use-typography("body-small"); - @include textEllipsis; + @include text-ellipsis; + display: flex; align-items: center; flex-grow: 1; @@ -66,13 +68,13 @@ padding: 0; margin-inline-end: 0; border: $b-1 solid var(--color-name-wrapper-boder-color); - border-radius: $br-8; background-color: var(--color-name-wrapper-background-color); color: var(--color-name-wrapper-foreground-color); border-radius: $br-8 0 0 $br-8; &.no-opacity { border-radius: $br-8; + .color-input-wrapper { border-radius: $br-8; } @@ -86,6 +88,7 @@ .detach-btn { display: grid; } + &.editing { --color-name-wrapper-background-color: var(--color-background-primary); } @@ -101,6 +104,7 @@ &:focus-within { --color-name-wrapper-background-color: var(--color-background-tertiary); --color-name-wrapper-boder-color: var(--color-accent-primary); + &:hover { --color-name-wrapper-background-color: var(--color-background-quaternary); } @@ -108,6 +112,7 @@ &.editing { --color-name-wrapper-background-color: var(--color-background-primary); + &:hover { --color-name-wrapper-boder-color: var(--color-accent-primary); } @@ -121,6 +126,7 @@ .color-input-wrapper { @include t.use-typography("body-small"); + display: flex; align-items: center; flex-grow: 1; @@ -135,7 +141,8 @@ .color-name { @include t.use-typography("body-small"); - @include textEllipsis; + @include text-ellipsis; + flex-grow: 1; padding-inline: px2rem(6); border-radius: $br-8; @@ -150,13 +157,15 @@ padding: 0 var(--sp-xxs) 0 var(--sp-s); border-radius: $br-8 0 0 $br-8; background-color: transparent; + &:hover { background-color: transparent; } } .color-input { - @include textEllipsis; + @include text-ellipsis; + border: none; background: none; outline: none; @@ -167,6 +176,7 @@ padding: 0 0 0 px2rem(6); border-radius: $br-8; color: var(--input-foreground-color-active); + &[disabled] { opacity: 0.5; pointer-events: none; @@ -182,11 +192,14 @@ --opacity-input-boder-color: var(--color-background-tertiary); @include t.use-typography("body-small"); + display: flex; align-items: center; + &:not(:focus-within) { cursor: ew-resize; } + block-size: $sz-32; inline-size: px2rem(60); padding-inline-start: var(--sp-xs); @@ -201,6 +214,7 @@ .detach-btn { display: grid; } + &.editing { --opacity-input-background-color: var(--color-background-primary); } @@ -216,6 +230,7 @@ &:focus-within { --opacity-input-background-color: var(--color-background-tertiary); --opacity-input-boder-color: var(--color-accent-primary); + &:hover { --opacity-input-background-color: var(--color-background-quaternary); } @@ -223,6 +238,7 @@ &.editing { --opacity-input-background-color: var(--color-background-primary); + &:hover { --opacity-input-boder-color: var(--color-accent-primary); } @@ -230,12 +246,12 @@ } .opacity-input { - @include textEllipsis; + @include text-ellipsis; + block-size: $sz-28; min-inline-size: $sz-28; flex-grow: 1; inline-size: 100%; - padding: 0; border-radius: 0 $br-8 $br-8 0; border: none; background: none; @@ -244,9 +260,11 @@ padding: 0 0 0 px2rem(6); color: var(--color-foreground-primary); cursor: ew-resize; + &:focus { cursor: text; } + &[disabled] { opacity: 0.5; pointer-events: none; @@ -270,6 +288,7 @@ --token-color-wrapper-foreground-color: var(--color-token-foreground); --token-color-wrapper-border-color: var(--color-token-border); --token-actions-display: none; + display: grid; grid-template-columns: auto 1fr auto; gap: var(--sp-xs); @@ -281,6 +300,7 @@ background: var(--token-color-wrapper-background-color); border: $b-1 solid var(--token-color-wrapper-border-color); border-radius: $br-8; + &:hover { --token-color-wrapper-background-color: var(--color-token-background); --token-color-wrapper-foreground-color: var(--color-foreground-primary); @@ -294,6 +314,7 @@ --token-color-wrapper-background-color: var(--color-background-primary); --token-color-wrapper-foreground-color: var(--color-foreground-secondary); --token-color-wrapper-border-color: var(--color-token-border); + &:hover { --token-color-wrapper-background-color: var(--color-background-primary); --token-color-wrapper-foreground-color: var(--color-foreground-secondary); @@ -304,7 +325,8 @@ .token-name { @include t.use-typography("body-small"); - @include textEllipsis; + @include text-ellipsis; + color: var(--token-color-wrapper-foreground-color); block-size: $sz-32; line-height: $sz-32; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/shadow_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/rows/shadow_row.scss index f13404d03e..84109fe162 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/shadow_row.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/shadow_row.scss @@ -80,8 +80,9 @@ .shadow-advanced-spread, .shadow-advanced-offset-y { // TODO remove this input by changing the input to DS component - @extend .input-element; + @extend %input-element; @include t.use-typography("body-small"); + .shadow-advanced-label { padding-inline-start: var(--sp-s); inline-size: px2rem(60); diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss index 830acdab03..a93107afa9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss @@ -12,7 +12,6 @@ display: flex; flex-direction: column; gap: var(--sp-xs); - position: relative; --reorder-left-position: calc(-1 * var(--sp-l)); @@ -32,15 +31,16 @@ .stroke-options { @include sidebar.option-grid-structure; + align-items: center; } .stroke-width-input { grid-column: span 2; - @extend .input-element; - + @extend %input-element; @include t.use-typography("body-small"); + padding-inline-start: var(--sp-xs); } @@ -60,15 +60,16 @@ .stroke-options-tokens { @include sidebar.option-grid-structure; - grid-template-columns: var(--3-columns-width) var(--grid-exception-input-width-small) var( + + grid-template-columns: var(--three-columns-width) var(--grid-exception-input-width-small) var( --grid-exception-input-width-small ); } .stroke-align-icon-select { - --dropdown-width: var(--4-columns-width); + --dropdown-width: var(--four-columns-width); } .stroke-style-icon-select { - --dropdown-width: var(--4-columns-width); + --dropdown-width: var(--four-columns-width); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss index 56376d9f02..0e68e8ee26 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss @@ -9,6 +9,7 @@ .shortcuts { display: grid; grid-template-rows: auto auto 1fr; + // TODO: Fix this once we start implementing the DS. // We should not be doign these hardcoded calc's. height: calc(100vh - #{deprecated.$s-60}); @@ -28,6 +29,7 @@ .not-found { @include deprecated.bodySmallTypography; + color: var(--empty-message-foreground-color); margin: deprecated.$s-12; } @@ -44,6 +46,7 @@ .section-title, .subsection-title { @include deprecated.uppercaseTitleTipography; + display: flex; align-items: center; margin: 0; @@ -64,9 +67,11 @@ text-transform: none; padding-left: deprecated.$s-12; } + .subsection-menu { margin-bottom: deprecated.$s-4; } + .sub-menu { margin-bottom: deprecated.$s-4; @@ -83,23 +88,28 @@ .command-name { @include deprecated.bodySmallTypography; + margin-left: deprecated.$s-2; color: var(--pill-foreground-color); } + .keys { @include deprecated.flexCenter; + gap: deprecated.$s-2; color: var(--pill-foreground-color); .key { @include deprecated.bodySmallTypography; @include deprecated.flexCenter; + text-transform: capitalize; height: deprecated.$s-20; padding: deprecated.$s-2 deprecated.$s-6; border-radius: deprecated.$s-6; background-color: var(--menu-shortcut-background-color); } + .space { margin: 0 deprecated.$s-2; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss index 086af118e8..51b5f9fc5e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss @@ -29,6 +29,7 @@ border-top: deprecated.$s-2 solid var(--resize-area-border-color); background-color: var(--resize-area-background-color); cursor: ns-resize; + &:hover { border-color: var(--resize-area-border-color); } @@ -39,8 +40,7 @@ flex-direction: column; height: calc(-38px + var(--height, deprecated.$s-200)); width: var(--left-sidebar-width); - overflow-x: hidden; - overflow-y: overlay; + overflow: hidden auto; scrollbar-gutter: stable; .element-list { @@ -56,18 +56,23 @@ .page-element { @include deprecated.bodySmallTypography; + min-height: deprecated.$s-32; width: 100%; cursor: pointer; + &.dnd-over-top { border-top: deprecated.$s-1 solid var(--layer-row-foreground-color-drag); } + &.dnd-over-bot { border-bottom: deprecated.$s-1 solid var(--layer-row-foreground-color-drag); } + .dnd-over > .element-list-body { border: deprecated.$s-1 solid var(--layer-row-foreground-color-drag); } + .element-list-body { display: flex; align-items: center; @@ -76,18 +81,24 @@ padding: 0 deprecated.$s-12 0 0; transition: none; color: var(--layer-row-foreground-color); + .page-name { @include deprecated.textEllipsis; + flex-grow: 1; padding-left: deprecated.$s-2; } + .page-icon { @include deprecated.flexCenter; + height: deprecated.$s-32; width: deprecated.$s-24; padding: 0 deprecated.$s-4 0 deprecated.$s-8; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + height: deprecated.$s-12; width: deprecated.$s-12; color: transparent; @@ -95,16 +106,21 @@ stroke: var(--icon-foreground); } } + .page-actions { height: deprecated.$s-32; + button { @include deprecated.buttonStyle; @include deprecated.flexCenter; + width: deprecated.$s-24; height: 100%; opacity: deprecated.$op-0; + svg { - @extend .button-icon-small; + @extend %button-icon-small; + height: deprecated.$s-12; width: deprecated.$s-12; color: transparent; @@ -113,14 +129,18 @@ } } } + .element-name { @include deprecated.textEllipsis; + color: var(--layer-row-foreground-color-focus); } + input.element-name { @include deprecated.textEllipsis; @include deprecated.bodySmallTypography; @include deprecated.removeInputStyle; + flex-grow: 1; height: deprecated.$s-28; max-width: calc(var(--parent-size) - (var(--depth) * var(--layer-indentation-size))); @@ -131,21 +151,25 @@ color: var(--layer-row-foreground-color); } } + &:active, &.on-drag { .element-list-body { color: var(--layer-row-foreground-color-drag); background-color: var(--layer-row-background-color-drag); + .page-actions button { svg { stroke: var(--layer-row-foreground-color-drag); } } + .page-icon svg { stroke: var(--layer-row-foreground-color-drag); } } } + &.selected, &.selected:hover { .element-list-body { @@ -153,16 +177,19 @@ background-color: var(--layer-row-background-color-selected); box-shadow: deprecated.$s-16 deprecated.$s-0 deprecated.$s-0 deprecated.$s-0 var(--layer-row-background-color-selected); + .page-actions button { svg { stroke: var(--layer-row-foreground-color-selected); } } + .page-icon svg { stroke: var(--layer-row-foreground-color-selected); } } } + &:hover, &.hover { .element-list-body { @@ -170,30 +197,37 @@ background-color: var(--layer-row-background-color-hover); box-shadow: deprecated.$s-16 deprecated.$s-0 deprecated.$s-0 deprecated.$s-0 var(--layer-row-background-color-hover); + .page-actions button { opacity: deprecated.$op-10; + svg { stroke: var(--layer-row-foreground-color-hover); } } + .page-icon svg { stroke: var(--layer-row-foreground-color-hover); } } } + &:focus { .element-list-body { color: var(--layer-row-foreground-color-focus); border: deprecated.$s-1 solid var(--layer-row-border-color-focus); outline: none; + .page-actions button { opacity: deprecated.$op-10; } } } + &:focus-within { .element-list-body { outline: none; + .page-actions button { opacity: deprecated.$op-10; } @@ -205,11 +239,13 @@ color: var(--layer-row-foreground-color-hidden); background-color: var(--layer-row-background-color-hidden); opacity: deprecated.$op-7; + .page-actions button { svg { stroke: var(--layer-row-foreground-color-hidden); } } + .page-icon svg { stroke: var(--layer-row-foreground-color-hidden); } @@ -219,6 +255,5 @@ .title-spacing-sitemap { padding-inline-start: deprecated.$s-8; - margin-block-start: deprecated.$s-8; - margin-block-end: deprecated.$s-4; + margin-block: deprecated.$s-8 deprecated.$s-4; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/versions.scss b/frontend/src/app/main/ui/workspace/sidebar/versions.scss index eb4a736edb..41c04c3521 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/versions.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/versions.scss @@ -132,15 +132,17 @@ } .version-options-dropdown { - @extend .dropdown-wrapper; + @extend %dropdown-wrapper; + position: absolute; width: fit-content; max-width: deprecated.$s-200; right: 0; left: unset; top: var(--offset); + .menu-option { - @extend .dropdown-element-base; + @extend %dropdown-element-base; } } @@ -164,6 +166,7 @@ &:hover { color: var(--color-accent-primary); + .icon-arrow { stroke: var(--color-accent-primary); } @@ -214,6 +217,7 @@ &:active { color: var(--color-accent-primary); + :global(.icon-pin) { visibility: initial; fill: var(--color-accent-primary); @@ -227,6 +231,7 @@ .cta { @include t.use-typography("body-small"); + color: var(--color-foreground-secondary); a { diff --git a/frontend/src/app/main/ui/workspace/text_palette.scss b/frontend/src/app/main/ui/workspace/text_palette.scss index b8b438eb89..f5947604e8 100644 --- a/frontend/src/app/main/ui/workspace/text_palette.scss +++ b/frontend/src/app/main/ui/workspace/text_palette.scss @@ -10,18 +10,22 @@ height: 100%; display: flex; } + .left-arrow, .right-arrow { @include deprecated.buttonStyle; @include deprecated.flexCenter; + position: relative; height: 100%; width: deprecated.$s-24; padding: 0; z-index: deprecated.$z-index-2; + svg { - @extend .button-icon; + @extend %button-icon; } + &::after { content: ""; position: absolute; @@ -37,20 +41,24 @@ ); pointer-events: none; } + &:hover { svg { stroke: var(--button-foreground-hover); } } + &:disabled { svg { stroke: var(--button-foreground-color-disabled); } + &::after { background-image: none; } } } + .left-arrow { &::after { left: deprecated.$s-24; @@ -60,6 +68,7 @@ var(--palette-button-shadow-final) 100% ); } + &.disabled ::after { background-image: none; } @@ -81,6 +90,7 @@ .typography-item { @include deprecated.bodySmallTypography; + display: flex; flex-direction: column; justify-content: center; @@ -90,12 +100,14 @@ padding: deprecated.$s-8; border-radius: deprecated.$br-8; background-color: var(--palette-text-background-color); + &:first-child { margin-left: deprecated.$s-8; } .typography-name { @include deprecated.textEllipsis; + height: deprecated.$s-16; width: deprecated.$s-120; color: var(--palette-text-color-selected); @@ -103,6 +115,7 @@ .typography-font { @include deprecated.textEllipsis; + height: deprecated.$s-16; width: deprecated.$s-120; color: var(--palette-text-color); @@ -110,6 +123,7 @@ .typography-data { @include deprecated.textEllipsis; + height: deprecated.$s-16; width: deprecated.$s-120; color: var(--palette-text-color); @@ -119,16 +133,19 @@ .typography-name { height: deprecated.$s-16; } + .typography-data { display: none; } } + &.small-item { .typography-data, .typography-font { display: none; } } + &:hover { background-color: var(--palette-text-background-color-hover); } @@ -136,5 +153,6 @@ .text-palette-empty { @include deprecated.bodySmallTypography; + color: var(--palette-text-color); } diff --git a/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss b/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss index fe450d0b1a..83e01b672b 100644 --- a/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss +++ b/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss @@ -31,39 +31,52 @@ &:last-child { margin-bottom: 0; } + .library-name { @include deprecated.bodySmallTypography; + color: var(--context-menu-foreground-color); display: grid; grid-template-columns: 1fr deprecated.$s-24; max-width: deprecated.$s-400; + .lib-name { @include deprecated.textEllipsis; + max-width: deprecated.$s-380; } + .lib-num { margin-left: deprecated.$s-4; } } + .icon-wrapper { margin-left: deprecated.$s-4; + @include deprecated.flexCenter; + svg { @include deprecated.flexCenter; - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--icon-foreground); } } + &.selected, &:hover { .icon-wrapper { @include deprecated.flexCenter; + svg { @include deprecated.flexCenter; - @extend .button-icon-small; + @extend %button-icon-small; + stroke: var(--context-menu-foreground-color-selected); } } + .library-name { color: var(--context-menu-foreground-color-selected); } diff --git a/frontend/src/app/main/ui/workspace/tokens/export.scss b/frontend/src/app/main/ui/workspace/tokens/export.scss index 53f9be2209..d7ecd3cb12 100644 --- a/frontend/src/app/main/ui/workspace/tokens/export.scss +++ b/frontend/src/app/main/ui/workspace/tokens/export.scss @@ -10,14 +10,16 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-dialog { --modal-width: 32rem; --modal-padding: var(--sp-xxxl); --container-max-height: 16rem; - @extend .modal-container-base; + + @extend %modal-container-base; + user-select: none; width: var(--modal-width); max-width: 100%; diff --git a/frontend/src/app/main/ui/workspace/tokens/export/modal.scss b/frontend/src/app/main/ui/workspace/tokens/export/modal.scss index 81d4af36f8..a8d711103f 100644 --- a/frontend/src/app/main/ui/workspace/tokens/export/modal.scss +++ b/frontend/src/app/main/ui/workspace/tokens/export/modal.scss @@ -62,13 +62,10 @@ .file-name { display: block; max-width: 99%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + @include t.use-typography("body-medium"); + flex-grow: 1; - overflow: hidden; - text-overflow: ellipsis; padding: var(--sp-xs); overflow: hidden; text-overflow: ellipsis; @@ -90,14 +87,14 @@ border-radius: $br-8; margin: 0; max-height: var(--container-max-height); - overflow-y: auto; - overflow-x: auto; - word-wrap: normal; + overflow: auto; + overflow-wrap: normal; white-space: pre; } .disabled-message { @include t.use-typography("body-small"); + color: var(--color-foreground-secondary); display: flex; align-items: center; diff --git a/frontend/src/app/main/ui/workspace/tokens/import.scss b/frontend/src/app/main/ui/workspace/tokens/import.scss index 314edf94c8..26b77c7dc5 100644 --- a/frontend/src/app/main/ui/workspace/tokens/import.scss +++ b/frontend/src/app/main/ui/workspace/tokens/import.scss @@ -7,11 +7,12 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-dialog { - @extend .modal-container-base; + @extend %modal-container-base; + user-select: none; } diff --git a/frontend/src/app/main/ui/workspace/tokens/import/modal.scss b/frontend/src/app/main/ui/workspace/tokens/import/modal.scss index 9d8671d48d..971c0abc88 100644 --- a/frontend/src/app/main/ui/workspace/tokens/import/modal.scss +++ b/frontend/src/app/main/ui/workspace/tokens/import/modal.scss @@ -30,6 +30,7 @@ .import-actions { @include t.use-typography("body-small"); + display: flex; justify-content: flex-end; gap: var(--sp-s); @@ -40,8 +41,7 @@ border-end-start-radius: 0; border-inline-start: $b-1 solid var(--color-accent-tertiary); width: var(--sp-xxxl); - padding-inline-start: 0; - padding-inline-end: 0; + padding-inline: 0; justify-content: center; } diff --git a/frontend/src/app/main/ui/workspace/tokens/import_from_library.scss b/frontend/src/app/main/ui/workspace/tokens/import_from_library.scss index d1394861db..b63eaf6648 100644 --- a/frontend/src/app/main/ui/workspace/tokens/import_from_library.scss +++ b/frontend/src/app/main/ui/workspace/tokens/import_from_library.scss @@ -5,7 +5,6 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; - @use "ds/typography.scss" as t; @use "ds/_borders.scss" as *; @use "ds/_sizes.scss" as *; @@ -20,7 +19,8 @@ --modal-title-foreground-color: var(--color-foreground-primary); --modal-text-foreground-color: var(--color-foreground-secondary); - @extend .modal-overlay-base; + @extend %modal-overlay-base; + display: flex; justify-content: center; align-items: center; @@ -33,7 +33,8 @@ } .modal-dialog { - @extend .modal-container-base; + @extend %modal-container-base; + inline-size: 100%; max-inline-size: 32rem; max-block-size: unset; @@ -50,12 +51,14 @@ .modal-title { @include t.use-typography("headline-medium"); + color: var(--modal-title-foreground-color); - word-break: break-word; + overflow-wrap: break-word; } .modal-content { @include t.use-typography("body-large"); + color: var(--modal-text-foreground-color); } @@ -65,6 +68,7 @@ } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; + gap: var(--sp-s); } diff --git a/frontend/src/app/main/ui/workspace/tokens/management.scss b/frontend/src/app/main/ui/workspace/tokens/management.scss index 135d3bcc76..39ba2ef087 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management.scss @@ -8,9 +8,10 @@ .sets-header-container { @include use-typography("headline-small"); + padding: var(--sp-s); color: var(--title-foreground-color); - word-break: break-word; + overflow-wrap: break-word; display: flex; align-items: flex-start; justify-content: space-between; @@ -24,6 +25,7 @@ .sets-header-status { @include use-typography("body-small"); + text-transform: none; color: var(--color-foreground-secondary); display: flex; diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.scss b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.scss index 207166d747..fdbdcbe98d 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.scss @@ -23,6 +23,7 @@ .context-list, .token-context-submenu { @include deprecated.menuShadow; + display: grid; width: deprecated.$s-240; padding: deprecated.$s-4; @@ -56,7 +57,9 @@ --context-menu-item-bg-color: none; --context-menu-item-fg-color: var(--color-foreground-primary); --context-menu-item-border-color: none; + @include use-typography("body-small"); + display: flex; align-items: center; height: deprecated.$s-32; @@ -67,6 +70,7 @@ background-color: var(--context-menu-item-bg-color); border: deprecated.$s-1 solid var(--context-menu-item-border-color); cursor: pointer; + &:hover { --context-menu-item-bg-color: var(--color-background-quaternary); } @@ -124,6 +128,7 @@ .item-with-icon-space { padding-left: deprecated.$s-20; } + .icon-wrapper { margin-right: deprecated.$s-4; } diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.scss index b484cacfce..41cc87fd25 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.scss @@ -12,5 +12,6 @@ position: fixed; max-block-size: $sz-400; overflow-y: auto; - @include custom-scrollbar(); + + @include custom-scrollbar; } diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss index 00eb38a2f2..3ac3d7a753 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss @@ -33,6 +33,7 @@ .form-modal-title { @include t.use-typography("headline-medium"); + color: var(--color-foreground-primary); display: flex; align-items: center; diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.scss index c9dc66715a..2dd229e37a 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.scss @@ -13,18 +13,17 @@ border-radius: $br-4; background-color: var(--color-background-primary); border: $b-2 solid var(--color-background-quaternary); - min-width: $sz-364; min-height: $sz-192; max-width: $sz-512; max-height: $sz-512; - box-shadow: 0px 0px $sz-12 0px var(--color-shadow-dark); + box-shadow: 0 0 $sz-12 0 var(--color-shadow-dark); position: absolute; width: auto; min-width: auto; z-index: var(--z-index-set); - overflow-y: auto; - overflow-x: hidden; + overflow: hidden auto; padding: var(--sp-xxxl); + &.token-modal-large { max-block-size: 95vh; } diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss index 71e0cda690..e4f8e633e2 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss @@ -6,14 +6,14 @@ @use "ds/_sizes.scss" as *; @use "ds/typography.scss" as t; - @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { --modal-title-foreground-color: var(--color-foreground-primary); --modal-text-foreground-color: var(--color-foreground-secondary); - @extend .modal-overlay-base; + @extend %modal-overlay-base; + display: flex; justify-content: center; align-items: center; @@ -32,7 +32,8 @@ } .modal-dialog { - @extend .modal-container-base; + @extend %modal-container-base; + inline-size: 100%; max-inline-size: 32rem; max-block-size: unset; @@ -42,5 +43,6 @@ .form-modal-title { @include t.use-typography("headline-medium"); + color: var(--color-foreground-primary); } diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss index f0a973f0b5..fa23fa90a4 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss @@ -40,6 +40,7 @@ .title { @include t.use-typography("body-small"); + color: var(--color-foreground-primary); display: flex; align-items: center; @@ -51,6 +52,7 @@ .visible-label { @include t.use-typography("headline-small"); + color: var(--color-foreground-secondary); line-height: $sz-32; } diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss index 5bac11ad27..67b16bef4b 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss @@ -30,6 +30,7 @@ .title { @include t.use-typography("body-small"); + color: var(--color-foreground-primary); display: flex; align-items: center; diff --git a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.scss b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.scss index 7e84dfa6d8..362699c88c 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.scss @@ -34,7 +34,7 @@ background-color: var(--color-background-tertiary); max-block-size: 100vh; overflow-y: auto; - box-shadow: 0px 0px $sz-12 0px var(--menu-shadow-color); + box-shadow: 0 0 $sz-12 0 var(--menu-shadow-color); } .token-node-context-menu-action { @@ -43,6 +43,7 @@ --context-menu-item-border-color: none; @include t.use-typography("body-small"); + appearance: none; background: var(--context-menu-item-bg-color); border: $b-1 solid var(--context-menu-item-border-color); diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.scss b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.scss index e6df6d482e..ff9b9cc8c3 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.scss @@ -10,8 +10,7 @@ .token-pill { @include use-typography("code-font"); - border: none; - background: none; + cursor: pointer; display: grid; grid-template-columns: auto 1fr; @@ -33,6 +32,7 @@ .name-wrapper { @include use-typography("code-font"); + display: block; overflow: hidden; text-overflow: ellipsis; @@ -49,6 +49,7 @@ .first-name-wrapper { @include use-typography("code-font"); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -57,6 +58,7 @@ .last-name-wrapper { @include use-typography("code-font"); + flex-shrink: 0; } @@ -70,6 +72,7 @@ --token-pill-border: var(--color-background-tertiary); --token-pill-outline: none; --token-pill-accent: var(--color-background-quaternary); + &:hover { --token-pill-background: var(--color-token-background); --token-pill-foreground: var(--color-foreground-primary); @@ -81,6 +84,7 @@ &:focus-visible { --token-pill-outline: var(--color-background-primary); --token-pill-border: var(--color-accent-primary); + outline-offset: -3px; } } @@ -124,11 +128,13 @@ --token-pill-background: var(--color-background-tertiary); --token-pill-accent: var(--color-foreground-error); } + &:hover { --token-pill-foreground: var(--color-foreground-primary); --token-pill-outline: none; --token-pill-border: var(--color-foreground-error); } + &:focus-visible { --token-pill-foreground: var(--color-foreground-error); --token-pill-border: var(--color-accent-primary); @@ -138,6 +144,7 @@ .token-pill-invalid-applied { --token-pill-border: var(--color-foreground-error); + &:hover, &:focus-visible { --token-pill-border: var(--color-foreground-error); @@ -177,6 +184,7 @@ --token-pill-border: var(--color-accent-error); --token-pill-foreground: var(--color-foreground-error); --token-pill-accent: var(--color-foreground-error); + &:hover, &:focus-visible { --token-pill-border: var(--color-accent-error); diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss index 7b1ea0244e..0794172692 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss @@ -50,6 +50,7 @@ margin-block-end: var(--sp-s); } } + & .token-pill { flex: 0 0 auto; } diff --git a/frontend/src/app/main/ui/workspace/tokens/remapping_modal.scss b/frontend/src/app/main/ui/workspace/tokens/remapping_modal.scss index 704df4e5d3..3dd5b7db84 100644 --- a/frontend/src/app/main/ui/workspace/tokens/remapping_modal.scss +++ b/frontend/src/app/main/ui/workspace/tokens/remapping_modal.scss @@ -6,14 +6,14 @@ @use "ds/_sizes.scss" as *; @use "ds/typography.scss" as t; - @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { --modal-title-foreground-color: var(--color-foreground-primary); --modal-text-foreground-color: var(--color-foreground-secondary); - @extend .modal-overlay-base; + @extend %modal-overlay-base; + display: flex; justify-content: center; align-items: center; @@ -32,7 +32,8 @@ } .modal-dialog { - @extend .modal-container-base; + @extend %modal-container-base; + inline-size: 100%; max-inline-size: 32rem; max-block-size: unset; @@ -49,12 +50,14 @@ .modal-title { @include t.use-typography("headline-medium"); + color: var(--modal-title-foreground-color); - word-break: break-word; + overflow-wrap: break-word; } .modal-content { @include t.use-typography("body-large"); + color: var(--modal-text-foreground-color); } @@ -64,12 +67,14 @@ } .action-buttons { - @extend .modal-action-btns; + @extend %modal-action-btns; + gap: var(--sp-s); } .modal-scd-msg, .modal-msg { @include t.use-typography("body-large"); + color: var(--modal-text-foreground-color); } diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.scss b/frontend/src/app/main/ui/workspace/tokens/sets.scss index dee8bfe07b..5209ff4bed 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sets.scss @@ -21,6 +21,7 @@ .create-set-button { @include use-typography("body-small"); + background-color: transparent; border: none; appearance: none; @@ -30,6 +31,7 @@ .set-item-container { @include deprecated.bodySmallTypography; + display: flex; align-items: center; width: 100%; @@ -43,9 +45,11 @@ &.dnd-over-bot { border-bottom: deprecated.$s-2 solid var(--layer-row-foreground-color-hover); } + &.dnd-over-top { border-top: deprecated.$s-2 solid var(--layer-row-foreground-color-hover); } + &.dnd-over { border: deprecated.$s-2 solid var(--layer-row-foreground-color-hover); } @@ -65,6 +69,7 @@ .set-name { @include deprecated.textEllipsis; + flex-grow: 1; padding-left: deprecated.$s-2; } @@ -112,7 +117,7 @@ } .check-icon { - color: currentColor; + color: currentcolor; } .set-item-container:hover { @@ -129,6 +134,7 @@ padding: deprecated.$s-12; color: var(--color-foreground-secondary); } + .selected-set { background-color: var(--layer-row-background-color-selected); color: var(--layer-row-foreground-color-selected); @@ -138,8 +144,10 @@ .collapsabled-icon { @include deprecated.buttonStyle; @include deprecated.flexCenter; + height: deprecated.$s-24; border-radius: deprecated.$br-8; + &:hover { color: var(--title-foreground-color-hover); } diff --git a/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss b/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss index 1e36266233..a09d7699b1 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss @@ -14,6 +14,7 @@ .context-list { @include deprecated.menuShadow; + display: grid; width: deprecated.$s-240; padding: deprecated.$s-4; @@ -26,6 +27,7 @@ .context-menu-item { @include t.use-typography("body-small"); + color: var(--menu-foreground-color); display: flex; align-items: center; diff --git a/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss b/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss index dee8bfe07b..5209ff4bed 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss @@ -21,6 +21,7 @@ .create-set-button { @include use-typography("body-small"); + background-color: transparent; border: none; appearance: none; @@ -30,6 +31,7 @@ .set-item-container { @include deprecated.bodySmallTypography; + display: flex; align-items: center; width: 100%; @@ -43,9 +45,11 @@ &.dnd-over-bot { border-bottom: deprecated.$s-2 solid var(--layer-row-foreground-color-hover); } + &.dnd-over-top { border-top: deprecated.$s-2 solid var(--layer-row-foreground-color-hover); } + &.dnd-over { border: deprecated.$s-2 solid var(--layer-row-foreground-color-hover); } @@ -65,6 +69,7 @@ .set-name { @include deprecated.textEllipsis; + flex-grow: 1; padding-left: deprecated.$s-2; } @@ -112,7 +117,7 @@ } .check-icon { - color: currentColor; + color: currentcolor; } .set-item-container:hover { @@ -129,6 +134,7 @@ padding: deprecated.$s-12; color: var(--color-foreground-secondary); } + .selected-set { background-color: var(--layer-row-background-color-selected); color: var(--layer-row-foreground-color-selected); @@ -138,8 +144,10 @@ .collapsabled-icon { @include deprecated.buttonStyle; @include deprecated.flexCenter; + height: deprecated.$s-24; border-radius: deprecated.$br-8; + &:hover { color: var(--title-foreground-color-hover); } diff --git a/frontend/src/app/main/ui/workspace/tokens/settings/menu.scss b/frontend/src/app/main/ui/workspace/tokens/settings/menu.scss index ae5339a979..fc4164d92a 100644 --- a/frontend/src/app/main/ui/workspace/tokens/settings/menu.scss +++ b/frontend/src/app/main/ui/workspace/tokens/settings/menu.scss @@ -5,19 +5,18 @@ // Copyright (c) KALEIDOS INC @use "ds/spacing.scss" as *; - @use "refactor/common-refactor.scss" as deprecated; .setting-modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .setting-modal { - @extend .modal-container-base; + @extend %modal-container-base; } .close-btn { - @extend .modal-close-btn-base; + @extend %modal-close-btn-base; } .settings-modal-layout { diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss index a48adee402..3eefaa09b0 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss @@ -11,6 +11,7 @@ .sidebar-wrapper { display: grid; grid-template-rows: auto 1fr auto; + // Overflow on the bottom section can't be done without hardcoded values for the height // This has to be changed from the wrapping sidebar styles height: calc(100vh - #{deprecated.$s-92}); @@ -18,7 +19,6 @@ } .token-management-section-wrapper { - position: relative; display: flex; flex: 1; height: var(--resize-height); @@ -55,8 +55,9 @@ .section-icon { margin-right: var(--sp-xs); + // Align better with the label - translate: 0px -1px; + translate: 0 -1px; } .import-export-button-wrapper { @@ -72,7 +73,8 @@ } .import-export-button { - @extend .button-secondary; + @extend %button-secondary; + display: flex; align-items: center; justify-content: end; @@ -80,12 +82,12 @@ text-transform: uppercase; gap: var(--sp-s); background-color: var(--color-background-primary); - box-shadow: var(--el-shadow-dark); } .import-export-menu { - @extend .menu-dropdown; + @extend %menu-dropdown; + top: -#{deprecated.$s-6}; right: 0; translate: 0 -100%; @@ -94,8 +96,10 @@ } .import-export-menu-item { - @extend .menu-item-base; + @extend %menu-item-base; + cursor: pointer; + &:hover { color: var(--menu-foreground-color-hover); } diff --git a/frontend/src/app/main/ui/workspace/tokens/themes.scss b/frontend/src/app/main/ui/workspace/tokens/themes.scss index a96f9f341a..4dc34ad389 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes.scss +++ b/frontend/src/app/main/ui/workspace/tokens/themes.scss @@ -15,7 +15,7 @@ .themes-header { padding: var(--sp-s); color: var(--title-foreground-color); - word-break: break-word; + overflow-wrap: break-word; } .empty-theme-wrapper { @@ -29,6 +29,7 @@ .create-theme-button { @include use-typography("body-small"); + background-color: transparent; border: none; appearance: none; diff --git a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss index c1cc8b59a4..c252590b18 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss +++ b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss @@ -5,15 +5,14 @@ // Copyright (c) KALEIDOS INC @use "ds/_sizes.scss" as *; - @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-dialog { - @extend .modal-container-base; + @extend %modal-container-base; width: 100%; max-width: deprecated.$s-512; @@ -59,6 +58,7 @@ gap: deprecated.$s-4; align-items: center; padding: 0; + &:hover { color: var(--color-accent-primary); } @@ -125,6 +125,7 @@ .group-title-name { flex-grow: 1; + @include deprecated.textEllipsis; } @@ -152,6 +153,7 @@ .theme-name-row { @include deprecated.textEllipsis; + flex-grow: 1; } diff --git a/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.scss b/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.scss index 867d65eafe..7d7171536e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.scss +++ b/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.scss @@ -11,6 +11,7 @@ --custom-select-bg-color: var(--menu-background-color); --custom-select-icon-color: var(--color-foreground-secondary); --custom-select-text-color: var(--menu-foreground-color); + position: relative; display: grid; grid-template-columns: 1fr auto; @@ -24,6 +25,7 @@ border: deprecated.$s-1 solid var(--custom-select-border-color); color: var(--custom-select-text-color); cursor: pointer; + &:hover { --custom-select-bg-color: var(--menu-background-color-hover); --custom-select-border-color: var(--menu-background-color); @@ -42,6 +44,7 @@ .group { @include deprecated.textEllipsis; + display: block; padding: deprecated.$s-8; color: var(--color-foreground-secondary); @@ -52,23 +55,26 @@ --custom-select-border-color: var(--menu-border-color-disabled); --custom-select-icon-color: var(--menu-foreground-color-disabled); --custom-select-text-color: var(--menu-foreground-color-disabled); + pointer-events: none; cursor: default; } .dropdown-button { @include deprecated.flexCenter; + color: var(--color-foreground-secondary); } .current-icon { @include deprecated.flexCenter; + width: deprecated.$s-24; padding-right: deprecated.$s-4; } .custom-select-dropdown { - @extend .dropdown-wrapper; + @extend %dropdown-wrapper; } .separator { @@ -88,7 +94,8 @@ } .checked-element-button { - @extend .dropdown-element-base; + @extend %dropdown-element-base; + position: relative; display: flex; justify-content: space-between; @@ -96,10 +103,12 @@ } .checked-element { - @extend .dropdown-element-base; + @extend %dropdown-element-base; + &.is-selected { color: var(--menu-foreground-color); } + &.disabled { display: none; } @@ -107,6 +116,7 @@ .check-icon { @include deprecated.flexCenter; + color: var(--icon-foreground-primary); visibility: hidden; } @@ -126,7 +136,8 @@ .dropdown-portal { --menu-max-height: #{deprecated.$s-400}; - @extend .new-scrollbar; + + @extend %new-scrollbar; position: absolute; } diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.scss b/frontend/src/app/main/ui/workspace/top_toolbar.scss index d005682878..fe603de407 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.scss +++ b/frontend/src/app/main/ui/workspace/top_toolbar.scss @@ -27,6 +27,7 @@ --toolbar-position-y: #{deprecated.$s-28}; --toolbar-offset-y: 0px; + top: calc(var(--toolbar-position-y) + var(--toolbar-offset-y)); } @@ -37,6 +38,7 @@ .main-toolbar-hidden { --toolbar-offset-y: -#{deprecated.$s-4}; + height: deprecated.$s-16; z-index: deprecated.$z-index-1; border-radius: 0 0 deprecated.$s-8 deprecated.$s-8; @@ -62,7 +64,8 @@ } .main-toolbar-options-button { - @extend .button-tertiary; + @extend %button-tertiary; + height: deprecated.$s-36; width: deprecated.$s-36; flex-shrink: 0; @@ -70,18 +73,20 @@ margin: 0 deprecated.$s-2; svg { - @extend .button-icon; + @extend %button-icon; + stroke: var(--color-foreground-secondary); } &.selected { - @extend .button-icon-selected; + @extend %button-icon-selected; } } .toolbar-handler { @include deprecated.flexCenter; @include deprecated.buttonStyle; + position: absolute; left: 0; bottom: 0; diff --git a/frontend/src/app/main/ui/workspace/viewport.scss b/frontend/src/app/main/ui/workspace/viewport.scss index 89ebd06403..cfd2209f7b 100644 --- a/frontend/src/app/main/ui/workspace/viewport.scss +++ b/frontend/src/app/main/ui/workspace/viewport.scss @@ -31,9 +31,6 @@ overflow: hidden; pointer-events: none; position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; + inset: 0; z-index: deprecated.$z-index-1; } diff --git a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss index d0f1549cb1..5225f42f27 100644 --- a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss +++ b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss @@ -10,10 +10,11 @@ .marker-shape { fill: var(--grid-editor-marker-color); } + .marker-text { fill: var(--app-white); font-size: calc(deprecated.$s-12 / var(--zoom)); - font-family: worksans; + font-family: worksans, vazirmatn, sans-serif; } } @@ -36,7 +37,7 @@ background: none; border: 0; color: var(--grid-editor-marker-text); - font-family: worksans; + font-family: worksans, vazirmatn, sans-serif; font-size: calc(deprecated.$fs-12 / var(--zoom)); font-weight: 400; margin: 0; @@ -119,9 +120,10 @@ .grid-actions-container { @include deprecated.flexRow; + background: var(--panel-background-color); border-radius: deprecated.$br-12; - box-shadow: 0px 0px deprecated.$s-12 0px var(--menu-shadow-color); + box-shadow: 0 0 deprecated.$s-12 0 var(--menu-shadow-color); gap: deprecated.$s-8; height: deprecated.$s-48; margin-left: -50%; @@ -139,22 +141,25 @@ } .locate-btn { - @extend .button-secondary; + @extend %button-secondary; + text-transform: uppercase; padding: deprecated.$s-8 deprecated.$s-20; font-size: deprecated.$fs-11; } .done-btn { - @extend .button-primary; + @extend %button-primary; + text-transform: uppercase; padding: deprecated.$s-8 deprecated.$s-20; font-size: deprecated.$fs-11; } .close-btn { - @extend .button-tertiary; + @extend %button-tertiary; + svg { - @extend .button-icon; + @extend %button-icon; } } diff --git a/frontend/src/app/main/ui/workspace/viewport/path_actions.scss b/frontend/src/app/main/ui/workspace/viewport/path_actions.scss index 94c86e6a8d..e079d01f1a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/path_actions.scss +++ b/frontend/src/app/main/ui/workspace/viewport/path_actions.scss @@ -39,7 +39,9 @@ .topbar-btn { --pathbar-icon-color: var(--color-foreground-secondary); - @extend .button-tertiary; + + @extend %button-tertiary; + height: deprecated.$s-36; width: deprecated.$s-36; flex-shrink: 0; @@ -50,11 +52,13 @@ &.is-toggled { --pathbar-icon-color: var(--button-radio-foreground-color-active); + background-color: var(--button-radio-background-color-active); } .pathbar-icon { - @extend .button-icon; + @extend %button-icon; + stroke: var(--pathbar-icon-color); } } diff --git a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.scss b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.scss index fbbe82c72e..87d3abe4bc 100644 --- a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.scss +++ b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.scss @@ -5,11 +5,8 @@ // Copyright (c) KALEIDOS INC .pixel-overlay { - left: 0; + inset: 0; pointer-events: initial; position: absolute; - top: 0; - right: 0; - bottom: 0; z-index: 1; } diff --git a/frontend/src/app/main/ui/workspace/viewport/presence.scss b/frontend/src/app/main/ui/workspace/viewport/presence.scss index d71cd38e21..e429851ac2 100644 --- a/frontend/src/app/main/ui/workspace/viewport/presence.scss +++ b/frontend/src/app/main/ui/workspace/viewport/presence.scss @@ -8,7 +8,7 @@ .profile-name { width: fit-content; - font-family: worksans; + font-family: worksans, vazirmatn, sans-serif; padding: 2px 12px; border-radius: deprecated.$br-4; display: flex; diff --git a/frontend/src/app/main/ui/workspace/viewport/top_bar.scss b/frontend/src/app/main/ui/workspace/viewport/top_bar.scss index 802dc32ab7..484ae669d9 100644 --- a/frontend/src/app/main/ui/workspace/viewport/top_bar.scss +++ b/frontend/src/app/main/ui/workspace/viewport/top_bar.scss @@ -10,6 +10,7 @@ .viewport-actions-path { pointer-events: none; position: absolute; + --actions-toolbar-position-y: #{deprecated.$s-28}; --actions-toolbar-offset-y: #{deprecated.$s-6}; @@ -24,6 +25,7 @@ .viewport-actions-container { @include deprecated.flexRow; + background: var(--panel-background-color); border-radius: deprecated.$br-12; box-shadow: 0 0 deprecated.$s-12 0 var(--menu-shadow-color); @@ -45,7 +47,8 @@ } .done-btn { - @extend .button-primary; + @extend %button-primary; + text-transform: uppercase; padding: deprecated.$s-8 deprecated.$s-20; font-size: deprecated.$fs-11; diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.scss b/frontend/src/app/main/ui/workspace/viewport/widgets.scss index 319888776c..4a1b949374 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.scss +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.scss @@ -21,13 +21,13 @@ .frame-flow-badge-content { @include t.use-typography("body-small"); + display: flex; align-items: center; justify-content: center; gap: var(--sp-xs); border-radius: $br-6; - padding-inline-start: var(--sp-xs); - padding-inline-end: var(--sp-s); + padding-inline: var(--sp-xs) var(--sp-s); height: var(--sp-xxl); background-color: var(--frame-flow-badge-background-color); color: var(--frame-flow-badge-foreground-color); @@ -47,6 +47,7 @@ .frame-title-label { @include t.use-typography("body-small"); + text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -56,6 +57,7 @@ .frame-title-input { @include t.use-typography("body-small"); + flex-grow: 1; width: 100%; max-width: initial; @@ -73,6 +75,7 @@ cursor: pointer; fill: var(--button-add-background-color); + &:hover { --button-add-background-color: var(--button-add-background-color-hover); } diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.scss b/frontend/src/app/main/ui/workspace/viewport_wasm.scss index a83fde4650..44cb86ce00 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.scss +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.scss @@ -29,10 +29,7 @@ overflow: hidden; pointer-events: none; position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; + inset: 0; z-index: 10; } @@ -40,7 +37,7 @@ position: fixed; inset: 0; z-index: 100; - background-color: rgba(0, 0, 0, 0.5); + background-color: rgb(0 0 0 / 50%); display: grid; place-items: center; cursor: default; diff --git a/frontend/stylelint.config.mjs b/frontend/stylelint.config.mjs new file mode 100644 index 0000000000..18bd1e592b --- /dev/null +++ b/frontend/stylelint.config.mjs @@ -0,0 +1,67 @@ +import postcssScss from "postcss-scss"; + +/** @type {import("stylelint").Config} */ +export default { + extends: ["stylelint-config-standard-scss"], + plugins: ["stylelint-scss", "stylelint-use-logical-spec"], + overrides: [ + { + files: ["**/*.scss"], + customSyntax: postcssScss, + }, + ], + rules: { + "at-rule-no-unknown": null, + "declaration-property-value-no-unknown": null, + "selector-pseudo-class-no-unknown": [ + true, + { ignorePseudoClasses: ["global"] }, // TODO: Avoid global selector usage and remove this exception + ], + + // scss + "scss/comment-no-empty": null, + "scss/at-rule-no-unknown": true, + // TODO: this rule should be enabled to follow scss conventions + "scss/load-no-partial-leading-underscore": null, + // This allows using the characters - or _ as a prefix and is ISO compliant with the Sass specification. + "scss/dollar-variable-pattern": "^[-_]?([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + // This allows using the characters - or _ as a prefix and is ISO compliant with the Sass specification. + "scss/at-mixin-pattern": "^[-_]?([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + + // TODO: Enable rules secuentially + // // Using quotes + // "font-family-name-quotes": "always-unless-keyword", + // "function-url-quotes": "always", + // "selector-attribute-quotes": "always", + // // Disallow vendor prefixes + // "at-rule-no-vendor-prefix": true, + // "media-feature-name-no-vendor-prefix": true, + // "property-no-vendor-prefix": true, + // "selector-no-vendor-prefix": true, + // "value-no-vendor-prefix": true, + // // Specificity + // "no-descending-specificity": null, + // "max-nesting-depth": 3, + // "selector-max-compound-selectors": 3, + // "selector-max-specificity": "1,2,1", + // // Miscellanea + // "color-named": "never", + // "declaration-no-important": true, + // "declaration-property-unit-allowed-list": { + // "font-size": ["rem"], + // "/^animation/": ["s"], + // }, + // // 'order/properties-alphabetical-order': true, + // "selector-max-type": 1, + // "selector-type-no-unknown": true, + // // Notation + // "font-weight-notation": "numeric", + // // URLs + // "function-url-no-scheme-relative": true, + // "liberty/use-logical-spec": "always", + // "selector-class-pattern": null, + // "alpha-value-notation": null, + // "color-function-notation": null, + // "value-keyword-case": null, + }, +}; From 6fa0c5ceaa81a3d4e3b6a171500a2f0982f8787f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Wed, 25 Mar 2026 11:08:07 +0100 Subject: [PATCH 075/288] :sparkles: Add organization avatar --- backend/src/app/nitrate.clj | 4 +- .../app/main/ui/components/org_avatar.cljs | 47 +++++++++++++++ .../app/main/ui/components/org_avatar.scss | 58 +++++++++++++++++++ .../src/app/main/ui/dashboard/sidebar.cljs | 14 ++--- 4 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 frontend/src/app/main/ui/components/org_avatar.cljs create mode 100644 frontend/src/app/main/ui/components/org_avatar.scss diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 32e2b7ce5e..ba052d4477 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -98,7 +98,8 @@ [:name ::sm/text] [:slug ::sm/text] [:is-your-penpot :boolean] - [:owner-id ::sm/uuid]]) + [:owner-id ::sm/uuid] + [:avatar-bg-url [::sm/text]]]) (def ^:private schema:team [:map @@ -261,6 +262,7 @@ :organization-name (:name org) :organization-slug (:slug org) :organization-owner-id (:owner-id org) + :organization-avatar-bg-url (:avatar-bg-url org) :is-default (or (:is-default team) (true? (:is-your-penpot org)))) team)) (catch Throwable cause diff --git a/frontend/src/app/main/ui/components/org_avatar.cljs b/frontend/src/app/main/ui/components/org_avatar.cljs new file mode 100644 index 0000000000..45095ec770 --- /dev/null +++ b/frontend/src/app/main/ui/components/org_avatar.cljs @@ -0,0 +1,47 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.components.org-avatar + (:require-macros [app.main.style :as stl]) + (:require + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn- get-org-initials + [name] + (->> (str/split (str/trim (or name "")) #"\s+") + (keep #(first (re-seq #"[a-zA-Z]" %))) + (take 2) + (map str/upper) + (apply str))) + +(mf/defc org-avatar* + {::mf/props :obj} + [{:keys [org size]}] + (let [name (:name org) + custom-photo (:organization-custom-photo org) + avatar-bg (:organization-avatar-bg-url org) + initials (get-org-initials name)] + + (if custom-photo + [:img {:src custom-photo + :class (stl/css-case :org-avatar true + :org-avatar-custom true + :org-avatar-xxxl (= size "xxxl") + :org-avatar-xxl (= size "xxl")) + :alt name}] + [:div {:class (stl/css-case :org-avatar true + :org-avatar-xxxl (= size "xxxl") + :org-avatar-xxl (= size "xxl")) + :aria-hidden "true"} + [:img {:src avatar-bg + :class (stl/css :org-avatar-bg) + :alt ""}] + (when (seq initials) + [:span {:class (stl/css-case :org-avatar-initials true + :size-initials-xxxl (= size "xxxl") + :size-initials-xxl (= size "xxl"))} + initials])]))) diff --git a/frontend/src/app/main/ui/components/org_avatar.scss b/frontend/src/app/main/ui/components/org_avatar.scss new file mode 100644 index 0000000000..ab7ee242a1 --- /dev/null +++ b/frontend/src/app/main/ui/components/org_avatar.scss @@ -0,0 +1,58 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "ds/typography.scss" as t; +@use "ds/colors.scss" as *; + +.org-avatar { + position: relative; + border-radius: 50%; + overflow: hidden; + flex-shrink: 0; +} + +.org-avatar-custom { + object-fit: cover; +} + +.org-avatar-xxxl { + width: var(--sp-xxxl); + height: var(--sp-xxxl); +} + +.org-avatar-xxl { + width: var(--sp-xxl); + height: var(--sp-xxl); +} + +.org-avatar-bg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +.org-avatar-initials { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + inset: 0; + color: #{$gray-950}; +} + +.size-initials-xxl { + @include t.use-typography("headline-small"); + + font-weight: 600; +} + +.size-initials-xxxl { + @include t.use-typography("headline-medium"); + + font-weight: 600; +} diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 4b9a1aaaa4..f944e07e94 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -25,6 +25,7 @@ [app.main.ui.components.dropdown-menu :refer [dropdown-menu* dropdown-menu-item*]] [app.main.ui.components.link :refer [link]] + [app.main.ui.components.org-avatar :refer [org-avatar*]] [app.main.ui.dashboard.comments :refer [comments-icon* comments-section]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.project-menu :refer [project-menu*]] @@ -346,10 +347,7 @@ :data-value (:id org-item) :class (stl/css :org-dropdown-item) :key (str (:id org-item))} - ;; TODO org pictures - [:img {:src (cf/resolve-team-photo-url org-item) - :class (stl/css :team-picture) - :alt (:name org-item)}] + [:> org-avatar* {:org org-item :size "xxl"}] [:span {:class (stl/css :team-text) :title (:name org-item)} (:name org-item)] (when (= (:id org-item) (:id organization)) @@ -580,7 +578,7 @@ (defn- team->org [team] - (assoc (dm/select-keys team [:id :organization-id :organization-slug :organization-owner-id]) + (assoc (dm/select-keys team [:id :organization-id :organization-slug :organization-owner-id :organization-avatar-bg-url]) :name (:organization-name team))) (mf/defc sidebar-org-switch* @@ -659,11 +657,7 @@ [:span {:class (stl/css :team-text)} "Penpot"]] [:* - [:span {:class (stl/css :nitrate-penpot-icon)} - ;; TODO org pictures - [:img {:src (cf/resolve-team-photo-url current-org) - :class (stl/css :team-picture) - :alt (:name current-org)}]] + [:> org-avatar* {:org current-org :size "xxxl"}] [:span {:class (stl/css :team-text)} (:name current-org)]])] arrow-icon] From 1af2521f64421956d90588e564660dfa38063154 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Tue, 24 Mar 2026 14:53:29 +0100 Subject: [PATCH 076/288] :sparkles: Add create default team org for nitrate on adding an user to a team --- backend/src/app/nitrate.clj | 57 +++++++++++--- backend/src/app/rpc/commands/auth.clj | 8 +- backend/src/app/rpc/commands/demo.clj | 4 +- backend/src/app/rpc/commands/ldap.clj | 2 +- backend/src/app/rpc/commands/management.clj | 3 +- backend/src/app/rpc/commands/teams.clj | 74 +++++++++++++++++-- .../app/rpc/commands/teams_invitations.clj | 14 ++-- backend/src/app/rpc/commands/verify_token.clj | 3 +- backend/src/app/rpc/management/nitrate.clj | 19 +---- backend/src/app/srepl/cli.clj | 4 +- backend/src/app/srepl/main.clj | 3 +- backend/test/backend_tests/helpers.clj | 19 ++--- 12 files changed, 147 insertions(+), 63 deletions(-) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index ba052d4477..000a55953c 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -54,10 +54,11 @@ (if (>= status 400) ;; For error status codes (4xx, 5xx), fail immediately without validation (do - (l/error :hint "nitrate request failed with error status" - :uri uri - :status status - :body (:body response)) + (when (not= status 404) ;; Don't need to log 404 + (l/error :hint "nitrate request failed with error status" + :uri uri + :status status + :body (:body response))) nil) ;; For success status codes, validate the response (let [coercer-http (sm/coercer schema @@ -107,6 +108,11 @@ [:organization-id ::sm/uuid] [:is-your-penpot :boolean]]) +(def ^:private schema:profile-org + [:map + [:is-member :boolean] + [:organization-id ::sm/uuid]]) + ;; TODO Unify with schemas on backend/src/app/http/management.clj (def ^:private schema:timestamp (sm/type-schema @@ -179,7 +185,7 @@ [:map [:licenses ::sm/boolean]]) -(defn- get-team-org +(defn- get-team-org-api [cfg {:keys [team-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] (request-to-nitrate cfg :get @@ -188,7 +194,18 @@ team-id) schema:organization params))) -(defn- set-team-org +(defn- get-org-membership-by-team-api + [cfg {:keys [profile-id team-id] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri)] + (request-to-nitrate cfg :get + (str baseuri + "/api/teams/" + team-id + "/users/" + profile-id) + schema:profile-org params))) + +(defn- set-team-org-api [cfg {:keys [organization-id team-id is-default] :as params}] (let [baseuri (cf/get :nitrate-backend-uri) params (assoc params :request-params {:team-id team-id @@ -200,7 +217,18 @@ "/add-team") schema:team params))) -(defn- get-subscription +(defn- add-profile-to-org-api + [cfg {:keys [profile-id org-id team-id] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri) + params (assoc params :request-params {:user-id profile-id :team-id team-id})] + (request-to-nitrate cfg :post + (str baseuri + "/api/organizations/" + org-id + "/add-user") + schema:profile-org params))) + +(defn- get-subscription-api [cfg {:keys [profile-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] (request-to-nitrate cfg :get @@ -209,7 +237,7 @@ profile-id) schema:subscription params))) -(defn- get-connectivity +(defn- get-connectivity-api [cfg params] (let [baseuri (cf/get :nitrate-backend-uri)] (request-to-nitrate cfg :get @@ -224,10 +252,12 @@ (defmethod ig/init-key ::client [_ cfg] (when (contains? cf/flags :nitrate) - {:get-team-org (partial get-team-org cfg) - :set-team-org (partial set-team-org cfg) - :get-subscription (partial get-subscription cfg) - :connectivity (partial get-connectivity cfg)})) + {:get-team-org (partial get-team-org-api cfg) + :set-team-org (partial set-team-org-api cfg) + :get-org-membership-by-team (partial get-org-membership-by-team-api cfg) + :add-profile-to-org (partial add-profile-to-org-api cfg) + :get-subscription (partial get-subscription-api cfg) + :connectivity (partial get-connectivity-api cfg)})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; UTILS @@ -287,6 +317,9 @@ :organization-id (:organization-id params)}))) team)) + + (defn connectivity [cfg] (call cfg :connectivity {})) + diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index fb5db45ef7..10639970ef 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -371,9 +371,11 @@ (defn create-profile-rels - [conn {:keys [id] :as profile}] + [{:keys [::db/conn] :as cfg} {:keys [id] :as profile}] + (assert (db/connection-map? cfg) + "expected cfg with valid connection") (let [features (cfeat/get-enabled-features cf/flags) - team (teams/create-team conn + team (teams/create-team cfg {:profile-id id :name "Default" :features features @@ -426,7 +428,7 @@ (assoc :is-active is-active) (update :password auth/derive-password)) profile (->> (create-profile cfg params) - (create-profile-rels conn))] + (create-profile-rels cfg))] (vary-meta profile assoc :created true)))) created? (-> profile meta :created true?) diff --git a/backend/src/app/rpc/commands/demo.clj b/backend/src/app/rpc/commands/demo.clj index d4f46e750b..f3ff979113 100644 --- a/backend/src/app/rpc/commands/demo.clj +++ b/backend/src/app/rpc/commands/demo.clj @@ -49,9 +49,9 @@ :deleted-at (ct/in-future (cf/get-deletion-delay)) :password (derive-password password) :props {}} - profile (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + profile (db/tx-run! cfg (fn [cfg] (->> (auth/create-profile cfg params) - (auth/create-profile-rels conn))))] + (auth/create-profile-rels cfg))))] (with-meta {:email email :password password} {::audit/profile-id (:id profile)}))) diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj index 5aa2a21935..f4aea5bc10 100644 --- a/backend/src/app/rpc/commands/ldap.clj +++ b/backend/src/app/rpc/commands/ldap.clj @@ -84,5 +84,5 @@ (profile/get-profile-by-email conn)) (->> (assoc info :is-active true :is-demo false) (auth/create-profile cfg) - (auth/create-profile-rels conn) + (auth/create-profile-rels cfg) (profile/strip-private-attrs)))))) diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index c4d580c37d..d078983a27 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -207,8 +207,7 @@ (update :team-id bfc/lookup-index) (assoc :created-at timestamp) (assoc :modified-at timestamp))] - (db/insert! conn :team-profile-rel params - {::db/return-keys false}))) + (teams/add-profile-to-team! cfg params {::db/return-keys false}))) ;; Duplicate team fonts (doseq [font fonts] diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 55836cb5b6..854b36bb32 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -522,20 +522,78 @@ (with-meta team {::audit/props {:id (:id team)}}))) + +(defn create-default-org-team + [cfg profile-id organization-id] + (quotes/check! cfg {::quotes/id ::quotes/teams-per-profile + ::quotes/profile-id profile-id}) + + (let [features (-> (cfeat/get-enabled-features cf/flags) + (set/difference cfeat/frontend-only-features) + (set/difference cfeat/no-team-inheritable-features)) + params {:profile-id profile-id + :name "Default" + :features features + :organization-id organization-id + :is-default true} + team (create-team cfg params)] + (select-keys team [:id]))) + +(defn- initialize-user-in-nitrate-org + "If needed, create a default team for the user on the organization, + and notify Nitrate that an user has been added to an org." + [cfg profile-id team-id] + (assert (db/connection-map? cfg) + "expected cfg with valid connection") + (let [membership (nitrate/call cfg :get-org-membership-by-team {:profile-id profile-id :team-id team-id})] + ;; Only when the team belong to an organization and the user is not a member + (when (and + (some? (:organization-id membership)) ;; the team do belong to an organization + (not (:is-member membership))) ;; the user is not a member of the org yet + + (db/tx-run! + cfg + (fn [{:keys [::db/conn] :as tx-cfg}] + (let [org-id (:organization-id membership) + default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id org-id) + default-team-id (:id default-team) + result (nitrate/call tx-cfg :add-profile-to-org {:profile-id profile-id + :team-id default-team-id + :org-id org-id})] + (when (not (:is-member result)) + (ex/raise :type :internal + :code :failed-add-profile-org-nitrate + :context {:profile-id profile-id + :team-id team-id + :org-id org-id + :default-team-id default-team-id})) + nil)))))) + +(defn add-profile-to-team! + ([cfg params] + (add-profile-to-team! cfg params nil)) + ([{:keys [::db/conn] :as cfg} {:keys [:profile-id :team-id] :as params} options] + (assert (db/connection-map? cfg) + "expected cfg with valid connection") + (when (contains? cf/flags :nitrate) + (initialize-user-in-nitrate-org cfg profile-id team-id)) + (db/insert! conn :team-profile-rel params options))) + (defn create-team "This is a complete team creation process, it creates the team object and all related objects (default role and default project)." - [cfg-or-conn params] - (let [conn (db/get-connection cfg-or-conn) - team (create-team* conn params) + [{:keys [::db/conn] :as cfg} params] + (assert (db/connection-map? cfg) + "expected cfg with valid connection") + (let [team (create-team* conn params) params (assoc params :team-id (:id team) :role :owner) project (create-team-default-project conn params)] - (create-team-role conn params) + (create-team-role cfg params) ;; Set team organization in Nitrate if organization-id is provided (when (and (contains? cf/flags :nitrate) (:organization-id params)) - (nitrate/set-team-organization cfg-or-conn team params)) + (nitrate/set-team-organization cfg team params)) (assoc team :default-project-id (:id project)))) (defn- create-team* @@ -551,11 +609,13 @@ (decode-row team))) (defn- create-team-role - [conn {:keys [profile-id team-id role] :as params}] + [cfg {:keys [profile-id team-id role] :as params}] + (assert (db/connection-map? cfg) + "expected cfg with valid connection") (let [params {:team-id team-id :profile-id profile-id}] (->> (perms/assign-role-flags params role) - (db/insert! conn :team-profile-rel)))) + (add-profile-to-team! cfg)))) (defn- create-team-default-project [conn {:keys [profile-id team-id] :as params}] diff --git a/backend/src/app/rpc/commands/teams_invitations.clj b/backend/src/app/rpc/commands/teams_invitations.clj index 5cffdd0c69..deed84912c 100644 --- a/backend/src/app/rpc/commands/teams_invitations.clj +++ b/backend/src/app/rpc/commands/teams_invitations.clj @@ -85,7 +85,8 @@ (defn- create-invitation [{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}] - (assert (db/connection? conn) "expected valid connection on cfg parameter") + (assert (db/connection-map? cfg) + "expected cfg with valid connection") (assert (check-create-invitation-params params)) (let [email (profile/clean-email email) @@ -104,8 +105,7 @@ (get types.team/permissions-for-role role))] ;; Insert the invited member to the team - (db/insert! conn :team-profile-rel params - {::db/on-conflict-do-nothing? true}) + (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}) ;; If profile is not yet verified, mark it as verified because ;; accepting an invitation link serves as verification. @@ -166,7 +166,9 @@ itoken))))) (defn- add-member-to-team - [conn profile team role member] + [{:keys [::db/conn] :as cfg} profile team role member] + (assert (db/connection-map? cfg) + "expected cfg with valid connection") (let [team-id (:id team) params (merge @@ -186,7 +188,7 @@ ::quotes/team-id team-id}) ;; Insert the member to the team - (db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true}) + (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}) ;; Delete any request (db/delete! conn :team-access-request @@ -268,7 +270,7 @@ (filter #(contains? invitation-emails (key %))) (map (fn [[email member]] (let [role (:role (first (filter #(= (:email %) email) invitation-data)))] - (add-member-to-team conn profile team role member)))) + (add-member-to-team cfg profile team role member)))) (doall)) invitations)) diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index a3454f7135..9f78ef3e12 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -18,6 +18,7 @@ [app.main :as-alias main] [app.rpc :as-alias rpc] [app.rpc.commands.profile :as profile] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.rpc.quotes :as quotes] @@ -104,7 +105,7 @@ ::quotes/team-id team-id}) ;; Insert the invited member to the team - (db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true}) + (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}) ;; If profile is not yet verified, mark it as verified because ;; accepting an invitation link serves as verification. diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index b6ac38a4d4..fd15ff811b 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -8,7 +8,6 @@ "Internal Nitrate HTTP RPC API. Provides authenticated access to organization management and token validation endpoints." (:require - [app.common.features :as cfeat] [app.common.schema :as sm] [app.common.types.profile :refer [schema:profile, schema:basic-profile]] [app.common.types.team :refer [schema:team]] @@ -21,9 +20,7 @@ [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.doc :as doc] - [app.rpc.quotes :as quotes] [app.util.services :as sv] - [clojure.set :as set] [cuerdas.core :as str])) ;; ---- API: authenticate @@ -116,25 +113,15 @@ [:organization-id ::sm/uuid] [:role ::sm/text]]) + + (sv/defmethod ::notify-user-added-to-organization "Notify to Penpot that an user has joined an org from nitrate" {::doc/added "2.14" ::sm/params schema:notify-user-added-to-organization ::rpc/auth false} [cfg {:keys [profile-id organization-id]}] - (quotes/check! cfg {::quotes/id ::quotes/teams-per-profile - ::quotes/profile-id profile-id}) - - (let [features (-> (cfeat/get-enabled-features cf/flags) - (set/difference cfeat/frontend-only-features) - (set/difference cfeat/no-team-inheritable-features)) - params {:profile-id profile-id - :name "Default" - :features features - :organization-id organization-id - :is-default true} - team (db/tx-run! cfg teams/create-team params)] - (select-keys team [:id]))) + (db/tx-run! cfg teams/create-default-org-team profile-id organization-id)) ;; ---- API: get-managed-profiles diff --git a/backend/src/app/srepl/cli.clj b/backend/src/app/srepl/cli.clj index 519df65b6e..cec1ec6a97 100644 --- a/backend/src/app/srepl/cli.clj +++ b/backend/src/app/srepl/cli.clj @@ -53,7 +53,7 @@ :or {is-active true}}] (some-> (get-current-system) (db/tx-run! - (fn [{:keys [::db/conn] :as system}] + (fn [system] (let [password (derive-password password) params {:id (uuid/next) :email email @@ -62,7 +62,7 @@ :password password :props {}}] (->> (cmd.auth/create-profile system params) - (cmd.auth/create-profile-rels conn))))))) + (cmd.auth/create-profile-rels system))))))) (defmethod exec-command "update-profile" [{:keys [fullname email password is-active]}] diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index 30c8b403dc..f25bec50cb 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -905,5 +905,4 @@ (let [params (-> rel (assoc :id (uuid/next)) (assoc :team-id (:id team)))] - (db/insert! conn :team-profile-rel params - {::db/return-keys false})))))))) + (teams/add-profile-to-team! cfg params {::db/return-keys false})))))))) diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index 8ddb3448a2..081af944e3 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -186,10 +186,10 @@ :is-demo false} params)] (db/run! system - (fn [{:keys [::db/conn] :as cfg}] + (fn [cfg] (->> params (cmd.auth/create-profile cfg) - (cmd.auth/create-profile-rels conn))))))) + (cmd.auth/create-profile-rels cfg))))))) (defn create-project* ([i params] (create-project* *system* i params)) @@ -234,10 +234,10 @@ (dm/with-open [conn (db/open system)] (let [id (mk-uuid "team" i) features (cfeat/get-enabled-features cf/flags)] - (teams/create-team conn {:id id - :profile-id profile-id - :features features - :name (str "team" i)}))))) + (teams/create-team {::db/conn conn} {:id id + :profile-id profile-id + :features features + :name (str "team" i)}))))) (defn create-file-media-object* ([params] (create-file-media-object* *system* params)) @@ -283,9 +283,10 @@ ([params] (create-team-role* *system* params)) ([system {:keys [team-id profile-id role] :or {role :owner}}] (dm/with-open [conn (db/open system)] - (#'teams/create-team-role conn {:team-id team-id - :profile-id profile-id - :role role})))) + (#'teams/create-team-role {::db/conn conn} + {:team-id team-id + :profile-id profile-id + :role role})))) (defn create-project-role* ([params] (create-project-role* *system* params)) From 4b4b99a9492b196bcd1deca121a8209fa2bd0a3c Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 26 Mar 2026 17:25:07 +0100 Subject: [PATCH 077/288] :sparkles: Add response to nitrate request when nitrate is down (#8722) --- backend/src/app/nitrate.clj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 000a55953c..4d7aa5c230 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -51,6 +51,11 @@ (fn [] (let [response (handler) status (:status response)] + (when-not status + (l/error :hint "could't do the nitrate request, it is probably down" + :uri uri) + ;; TODO decide what to do when Nitrate is inaccesible + nil) (if (>= status 400) ;; For error status codes (4xx, 5xx), fail immediately without validation (do From 51b902364060456365f9664bab9423160802af0a Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 26 Mar 2026 17:25:30 +0100 Subject: [PATCH 078/288] :sparkles: Show nitrate org name on invitation to join team email (#8802) --- .../app/email/invite-to-team/en.html | 3 ++- .../app/rpc/commands/teams_invitations.clj | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/backend/resources/app/email/invite-to-team/en.html b/backend/resources/app/email/invite-to-team/en.html index 337593902d..31d1ddf4a3 100644 --- a/backend/resources/app/email/invite-to-team/en.html +++ b/backend/resources/app/email/invite-to-team/en.html @@ -186,7 +186,8 @@
- {{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.
+ {{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”{% if organization %} + part of the organization “{{ organization|abbreviate:25 }}”{% endif %}. diff --git a/backend/src/app/rpc/commands/teams_invitations.clj b/backend/src/app/rpc/commands/teams_invitations.clj index deed84912c..d4a2f96ded 100644 --- a/backend/src/app/rpc/commands/teams_invitations.clj +++ b/backend/src/app/rpc/commands/teams_invitations.clj @@ -21,6 +21,7 @@ [app.email :as eml] [app.loggers.audit :as audit] [app.main :as-alias main] + [app.nitrate :as nitrate] [app.rpc :as-alias rpc] [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] @@ -154,14 +155,18 @@ (audit/submit! cfg event)) (when (allow-invitation-emails? member) - (eml/send! {::eml/conn conn - ::eml/factory eml/invite-to-team - :public-uri (cf/get :public-uri) - :to email - :invited-by (:fullname profile) - :team (:name team) - :token itoken - :extra-data ptoken})) + (let [team (if (contains? cf/flags :nitrate) + (nitrate/add-org-info-to-team cfg team {}) + team)] + (eml/send! {::eml/conn conn + ::eml/factory eml/invite-to-team + :public-uri (cf/get :public-uri) + :to email + :invited-by (:fullname profile) + :team (:name team) + :organization (:organization-name team) + :token itoken + :extra-data ptoken}))) itoken))))) From 342b07779d157d5623ef9a24d84661554bbb613c Mon Sep 17 00:00:00 2001 From: Marius Date: Wed, 25 Mar 2026 19:33:12 +0100 Subject: [PATCH 079/288] :globe_with_meridians: Add translations for: German Currently translated at 95.1% (1974 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/ --- frontend/translations/de.po | 62 ++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 9345fb08e0..2e57efc6ed 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-17 10:09+0000\n" -"Last-Translator: nautilusx \n" +"PO-Revision-Date: 2026-03-26 19:09+0000\n" +"Last-Translator: Marius \n" "Language-Team: German \n" "Language: de\n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.16\n" +"X-Generator: Weblate 5.17-dev\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -1408,11 +1408,11 @@ msgstr "" #: src/app/main/errors.cljs:305 msgid "errors.deprecated.contact.after" -msgstr "damit wir Ihnen helfen können." +msgstr ", damit wir Ihnen helfen können." #: src/app/main/errors.cljs:304 msgid "errors.deprecated.contact.text" -msgstr "kontaktieren Sie uns" +msgstr "kontaktieren" #: src/app/main/data/workspace/tokens/library_edit.cljs:338 msgid "errors.drop-token-set-parent-to-child" @@ -6704,6 +6704,7 @@ msgid "workspace.options.shadow-options.remove-shadow" msgstr "Schatten entfernen" #: src/app/main/ui/inspect/attributes/shadow.cljs:48, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:191, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:193 +#, fuzzy msgid "workspace.options.shadow-options.spread" msgstr "Streuung" @@ -8398,3 +8399,54 @@ msgstr "Etwas Schlimmes ist passiert." #: src/app/main/ui/static.cljs:318 msgid "labels.reload-page" msgstr "Seite neu laden" + +#: src/app/main/ui/dashboard/sidebar.cljs:347 +msgid "dashboard.create-new-org" +msgstr "Neue Organisation erstellen" + +#: src/app/main/ui/dashboard/deleted.cljs:52 +msgid "dashboard.delete-project-forever-confirmation.description" +msgstr "" +"Wollen Sie wirklich das Projekt %s löschen? Diese Aktion kann nicht " +"rückgängig gemacht werden." + +#: src/app/main/ui/workspace/sidebar/debug.cljs:38 +msgid "workspace.debug.title" +msgstr "In Arbeitsbereich bearbeiten" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:422 +msgid "workspace.layout-grid.editor.margin.expand" +msgstr "Optionen für Abstände anzeigen" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:449 +msgid "workspace.layout-item.fit-content-horizontal" +msgstr "Inhalt horizontal anpassen" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:477 +msgid "workspace.layout-item.fit-content-vertical" +msgstr "Inhalt vertikal anpassen" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:471 +msgid "workspace.layout-item.height-100" +msgstr "Gesamte Höhe" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:444 +msgid "workspace.layout-item.width-100" +msgstr "Gesamte Breite" + +#: src/app/main/ui/workspace/libraries.cljs:338 +msgid "workspace.libraries.connected-to" +msgstr "Verbunden mit" + +#: src/app/main/ui/workspace/top_toolbar.cljs:231 +msgid "workspace.toolbar.debug" +msgstr "Werkzeuge zur Fehlersuche" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:287 +#, fuzzy, unused +msgid "workspace.tokens.shadow-token-spread-value-error" +msgstr "Der Wert für die Streuung darf nicht negativ sein" + +#: src/app/main/errors.cljs:303 +msgid "errors.deprecated.contact.before" +msgstr "Penpot unterstützt diese Assets nicht mehr. Sie können uns allerdings" From abe328973c68d0830bcbf0f36c87f6ee0db8a6ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Wed, 25 Mar 2026 11:37:16 +0100 Subject: [PATCH 080/288] :lipstick: Fix focus radio button --- frontend/src/app/main/ui/components/forms.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/components/forms.scss b/frontend/src/app/main/ui/components/forms.scss index 6db91af254..9ce358be39 100644 --- a/frontend/src/app/main/ui/components/forms.scss +++ b/frontend/src/app/main/ui/components/forms.scss @@ -429,9 +429,7 @@ padding: deprecated.$s-8; color: var(--input-foreground-color-rest); border: deprecated.$s-1 solid transparent; - - &:focus, - &:focus-within { + &:has(:focus-visible) { outline: none; border: deprecated.$s-1 solid var(--input-border-color-active); } From 1ecfbef6fbe581e44d9fc3e36dbb001d189377aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Wed, 25 Mar 2026 12:21:51 +0100 Subject: [PATCH 081/288] :recycle: Refactor forms file --- CHANGES.md | 1 + .../src/app/main/ui/components/forms.scss | 297 ++++++++++-------- 2 files changed, 160 insertions(+), 138 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d5a364afdb..6cccf6913b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -31,6 +31,7 @@ - Update copy on penpot update message [Taiga #12924](https://tree.taiga.io/project/penpot/issue/12924) - Fix scroll on library modal [Taiga #13639](https://tree.taiga.io/project/penpot/issue/13639) - Fix dates to avoid show them in english when browser is in auto [Taiga #13786](https://tree.taiga.io/project/penpot/issue/13786) +- Fix focus radio button [Taiga #13841](https://tree.taiga.io/project/penpot/issue/13841) ## 2.15.0 (Unreleased) diff --git a/frontend/src/app/main/ui/components/forms.scss b/frontend/src/app/main/ui/components/forms.scss index 9ce358be39..3bd61df854 100644 --- a/frontend/src/app/main/ui/components/forms.scss +++ b/frontend/src/app/main/ui/components/forms.scss @@ -5,6 +5,13 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; +@use "ds/typography.scss" as t; +@use "ds/_borders.scss" as *; +@use "ds/spacing.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/z-index.scss" as *; +@use "ds/mixins.scss" as *; // INPUT .input-wrapper { @@ -17,44 +24,44 @@ &.valid { input { - border: deprecated.$s-1 solid var(--input-border-color-success); - @extend %disabled-input; + border: $b-1 solid var(--input-border-color-success); + &:hover, &:focus { - border: deprecated.$s-1 solid var(--input-border-color-success); + border: $b-1 solid var(--input-border-color-success); } } } &.invalid { input { - border: deprecated.$s-1 solid var(--input-border-color-error); - @extend %disabled-input; + border: $b-1 solid var(--input-border-color-error); + &:hover, &:focus { - border: deprecated.$s-1 solid var(--input-border-color-error); + border: $b-1 solid var(--input-border-color-error); } } } &.valid .help-icon, &.invalid .help-icon { - right: deprecated.$s-40; + inset-inline-end: $sz-40; } } .input-with-label-form { - @include deprecated.flexColumn; - - gap: deprecated.$s-8; + display: flex; + flex-direction: column; + gap: var(--sp-s); justify-content: flex-start; align-items: flex-start; - height: 100%; - width: 100%; + block-size: 100%; + inline-size: 100%; padding: 0; cursor: pointer; color: var(--modal-title-foreground-color); @@ -64,16 +71,16 @@ @extend %input-element; color: var(--input-foreground-color-active); - margin-top: 0; - width: 100%; - max-width: 100%; - height: 100%; - padding: 0 deprecated.$s-8; + margin-block-start: 0; + inline-size: 100%; + max-inline-size: 100%; + block-size: 100%; + padding: 0 var(--sp-s); &:focus { outline: none; - border: deprecated.$s-1 solid var(--input-border-color-focus); - border-radius: deprecated.$br-8; + border: $b-1 solid var(--input-border-color-focus); + border-radius: var(--sp-s); } } @@ -84,7 +91,7 @@ input:-webkit-autofill:active { -webkit-text-fill-color: var(--input-foreground-color-active); box-shadow: inset 0 0 20px 20px var(--input-background-color); - border: deprecated.$s-1 solid var(--input-border-color); + border: $b-1 solid var(--input-border-color); background-clip: text; transition: background-color 5000s ease-in-out 0s; caret-color: var(--input-foreground-color-active); @@ -93,60 +100,60 @@ .input-and-icon { position: relative; - width: var(--input-width, calc(100% - deprecated.$s-1)); - min-width: var(--input-min-width); - height: var(--input-height, deprecated.$s-32); + inline-size: var(--input-width, calc(100% - deprecated.$s-1)); + min-inline-size: var(--input-min-width); + block-size: var(--input-height, $sz-32); } .help-icon { cursor: pointer; position: absolute; - right: deprecated.$s-16; - top: calc(50% - deprecated.$s-8); + inset-inline-end: var(--sp-l); + inset-block-start: calc(50% - var(--sp-s)); svg { @extend %button-icon-small; stroke: var(--color-foreground-secondary); - width: deprecated.$s-16; - height: deprecated.$s-16; + inline-size: $sz-16; + block-size: $sz-16; } } .invalid-icon { - width: deprecated.$s-16; - height: deprecated.$s-16; + inline-size: $sz-16; + block-size: $sz-16; background: var(--input-border-color-error); - border-radius: 50%; + border-radius: $br-circle; display: flex; align-items: center; justify-content: center; position: absolute; - right: var(--input-icon-padding); - top: calc(50% - deprecated.$s-8); + inset-inline-end: var(--input-icon-padding); + inset-block-start: calc(50% - var(--sp-s)); svg { - width: deprecated.$s-12; - height: deprecated.$s-12; + inline-size: $sz-12; + block-size: $sz-12; stroke: var(--input-background-color); } } .valid-icon { - width: deprecated.$s-16; - height: deprecated.$s-16; + inline-size: $sz-16; + block-size: $sz-16; background: var(--input-border-color-success); - border-radius: 50%; + border-radius: $br-circle; display: flex; align-items: center; justify-content: center; position: absolute; - right: deprecated.$s-16; - top: calc(50% - deprecated.$s-8); + inset-inline-end: var(--sp-l); + inset-block-start: calc(50% - var(--sp-s)); svg { - width: deprecated.$s-12; - height: deprecated.$s-12; + inline-size: $sz-12; + block-size: $sz-12; fill: var(--input-border-color-success); stroke: var(--input-background-color); } @@ -154,15 +161,15 @@ .error { color: var(--input-border-color-error); - width: 100%; + inline-size: 100%; font-size: deprecated.$fs-14; } .hint { - @include deprecated.bodySmallTypography; + @include t.use-typography("body-small"); - width: 99%; - margin-block-start: deprecated.$s-8; + inline-size: 99%; + margin-block-start: var(--sp-s); color: var(--modal-text-foreground-color); } @@ -170,13 +177,13 @@ @extend %input-checkbox; .checkbox-label { - @include deprecated.bodySmallTypography; + @include t.use-typography("body-small"); display: flex; align-items: center; flex-direction: row-reverse; - gap: deprecated.$s-6; - min-height: deprecated.$s-32; + gap: px2rem(6); + min-block-size: var(--sp-xxxl); cursor: pointer; span { @@ -204,34 +211,40 @@ .custom-select { @extend %select-wrapper; - height: deprecated.$s-32; + block-size: $sz-32; .input-container { - @include deprecated.flexRow; + display: flex; + align-items: center; + gap: var(--sp-xs); + block-size: $sz-32; + inline-size: 100%; + border-radius: var(--sp-s); + border: $b-1 solid var(--input-border-color); + + @extend %select-wrapper; - height: deprecated.$s-32; - width: 100%; - border-radius: deprecated.$br-8; - border: deprecated.$s-1 solid var(--input-border-color); color: var(--input-foreground-color-active); background-color: var(--input-background-color); .main-content { - @include deprecated.flexColumn; - @include deprecated.bodySmallTypography; + @include t.use-typography("body-small"); + display: flex; + flex-direction: column; + gap: var(--sp-xs); position: relative; justify-content: center; flex-grow: 1; - height: 100%; - padding: deprecated.$s-8; + block-size: 100%; + padding: var(--sp-s); .label { color: var(--input-foreground-color); } .value { - width: 100%; + inline-size: 100%; padding: 0; margin: 0; border: 0; @@ -240,10 +253,11 @@ } .icon { - @include deprecated.flexCenter; - - height: deprecated.$s-32; - width: deprecated.$s-24; + display: flex; + justify-content: center; + align-items: center; + block-size: $sz-32; + inline-size: $sz-24; pointer-events: none; svg { @@ -256,7 +270,7 @@ &.disabled { background-color: var(--input-background-color-disabled); - border: deprecated.$s-1 solid var(--input-border-color-disabled); + border: $b-1 solid var(--input-border-color-disabled); color: var(--input-foreground-color-disabled); } @@ -264,36 +278,36 @@ outline: none; color: var(--input-foreground-color-active); background-color: var(--input-background-color-active); - border: deprecated.$s-1 solid var(--input-border-color-active); + border: $b-1 solid var(--input-border-color-active); } } select { @extend %menu-dropdown; - @include deprecated.bodySmallTypography; + @include t.use-typography("body-small"); box-sizing: border-box; position: absolute; - top: 0; - left: 0; - min-height: deprecated.$s-32; - height: auto; - width: calc(100% - 1px); - padding: 0 deprecated.$s-12; + inset-block-start: 0; + inset-inline-start: 0; + min-block-size: $sz-32; + block-size: auto; + inline-size: calc(100% - 1px); + padding: 0 var(--sp-m); margin: 0; border: none; opacity: 0; - z-index: deprecated.$z-index-10; + z-index: var(--z-index-dropdown); background-color: transparent; cursor: pointer; option { - @include deprecated.bodySmallTypography; + @include t.use-typography("body-small"); color: var(--title-foreground-color-hover); background-color: var(--menu-background-color); appearance: none; - height: deprecated.$s-32; + block-size: $sz-32; } } } @@ -305,7 +319,7 @@ &:disabled { @extend %button-disabled; - min-height: deprecated.$s-32; + min-block-size: $sz-32; } } @@ -314,37 +328,38 @@ display: flex; flex-direction: column; position: relative; - min-height: deprecated.$s-40; - max-height: deprecated.$s-180; - width: 100%; + min-block-size: $sz-40; + max-block-size: px2rem(180); + inline-size: 100%; overflow-y: hidden; .inside-input { @include deprecated.removeInputStyle; - @include deprecated.bodySmallTypography; - @include deprecated.textEllipsis; + @include t.use-typography("body-small"); + @include text-ellipsis; - width: 100%; - max-width: calc(100% - deprecated.$s-1); - min-height: deprecated.$s-32; - height: deprecated.$s-32; - padding: deprecated.$s-8; + inline-size: 100%; + max-inline-size: calc(100% - deprecated.$s-1); + min-block-size: $sz-32; + padding-block-start: 0; + block-size: $sz-32; + padding: var(--sp-s); margin: 0; - border-radius: deprecated.$br-8; + border-radius: var(--sp-s); color: var(--input-foreground-color-active); background-color: var(--input-background-color); &:focus { outline: none; - border: deprecated.$s-1 solid var(--input-border-color-focus); + border: $b-1 solid var(--input-border-color-focus); } &.invalid { - border: deprecated.$s-1 solid var(--input-border-color-error); + border: $b-1 solid var(--input-border-color-error); &:hover, &:focus { - border: deprecated.$s-1 solid var(--input-border-color-error); + border: $b-1 solid var(--input-border-color-error); } } } @@ -356,36 +371,40 @@ .selected-items { display: flex; flex-wrap: wrap; - gap: deprecated.$s-4; - max-height: deprecated.$s-136; - padding: deprecated.$s-4 0; + gap: var(--sp-xs); + max-block-size: px2rem(136); + padding: var(--sp-xs) 0; overflow-y: auto; .selected-item { .around { - @include deprecated.flexRow; - - height: deprecated.$s-24; - width: fit-content; - padding-left: deprecated.$s-6; - border-radius: deprecated.$br-6; + display: flex; + align-items: center; + gap: var(--sp-xs); + block-size: $sz-24; + inline-size: fit-content; + padding-inline-start: px2rem(6); + border-radius: $br-6; background-color: var(--pill-background-color); - border: deprecated.$s-1 solid var(--pill-background-color); + border: $b-1 solid var(--pill-background-color); box-sizing: border-box; .text { - @include deprecated.bodySmallTypography; + @include t.use-typography("body-small"); - padding-right: deprecated.$s-8; + padding-inline-end: var(--sp-s); color: var(--pill-foreground-color); } .icon { - @include deprecated.flexCenter; - @include deprecated.buttonStyle; - - height: deprecated.$s-32; - width: deprecated.$s-24; + display: flex; + justify-content: center; + align-items: center; + border: none; + background: none; + cursor: pointer; + block-size: $sz-32; + inline-size: $sz-24; svg { @extend %button-icon-small; @@ -414,92 +433,95 @@ .custom-radio { display: grid; grid-template-columns: repeat(3, 1fr); - gap: deprecated.$s-16; + gap: var(--sp-l); } .radio-label { - @include deprecated.bodySmallTypography; - @include deprecated.flexRow; + @include t.use-typography("body-small"); + display: flex; + align-items: center; align-items: flex-start; - gap: deprecated.$s-8; - min-height: deprecated.$s-32; - height: fit-content; - border-radius: deprecated.$br-8; - padding: deprecated.$s-8; + gap: var(--sp-s); + min-block-size: $sz-32; + block-size: fit-content; + border-radius: var(--sp-s); + padding: var(--sp-s); color: var(--input-foreground-color-rest); - border: deprecated.$s-1 solid transparent; + border: $b-1 solid transparent; + &:has(:focus-visible) { outline: none; - border: deprecated.$s-1 solid var(--input-border-color-active); + border: $b-1 solid var(--input-border-color-active); } } .radio-dot { - height: deprecated.$s-8; - width: deprecated.$s-8; - border-radius: deprecated.$br-circle; + block-size: var(--sp-s); + inline-size: var(--sp-s); + border-radius: $br-circle; background-color: var(--color-background-tertiary); } .radio-input { - width: 0; + inline-size: 0; margin: 0; } .radio-icon { @extend %checkbox-icon; - border-radius: deprecated.$br-circle; + border-radius: $br-circle; } .radio-label-image { - @include deprecated.smallTitleTipography; + @include t.use-typography("body-medium"); display: grid; grid-template-rows: auto auto 0; justify-items: center; gap: 0; - border-radius: deprecated.$br-8; + border-radius: var(--sp-s); margin: 0; - border: 1px solid var(--color-background-tertiary); + border: $b-1 solid var(--color-background-tertiary); cursor: pointer; &:global(.checked) { - border: 1px solid var(--color-accent-primary); + border: $b-1 solid var(--color-accent-primary); } &:focus, &:focus-within { outline: none; - border: deprecated.$s-1 solid var(--input-border-color-active); + border: $b-1 solid var(--input-border-color-active); } .image-text { color: var(--input-foreground-color-rest); display: grid; align-self: center; - margin-bottom: deprecated.$s-16; - padding-inline: deprecated.$s-8; + margin-block-end: var(--sp-l); + padding-inline: var(--sp-s); text-align: center; } } .image-inside { - margin: deprecated.$s-16; + margin: var(--sp-l); background-size: 100%; background-repeat: no-repeat; background-position: center; } .icon-inside { - margin: deprecated.$s-16; - - @include deprecated.flexCenter; + margin: var(--sp-l); + display: flex; + justify-content: center; + align-items: center; svg { - width: 40px; - height: 60px; + inline-size: 40px; + block-size: 60px; stroke: var(--icon-foreground); fill: none; } @@ -508,11 +530,10 @@ // TEXTAREA .textarea-label { - @include deprecated.uppercaseTitleTipography; + @include t.use-typography("headline-small"); color: var(--modal-title-foreground-color); - text-transform: uppercase; - margin-bottom: deprecated.$s-8; + margin-block-end: var(--sp-s); } .textarea-wrapper { From 8cc6c40b87f514931091f7667f56e4193fec245f Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Fri, 27 Mar 2026 09:51:20 +0100 Subject: [PATCH 082/288] :sparkles: Update nitrate organizations dropdown visibility --- .../src/app/main/ui/dashboard/sidebar.cljs | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index f944e07e94..15f3aef434 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -593,9 +593,8 @@ (map team->org) (d/index-by :id))) - ;; There is always at least one default organization - ;; so no-orgs? is true when only that default one exists (count <= 1). - no-orgs? (<= (count orgs) 1) + show-dropdown? (or (dnt/is-valid-license? profile) + (> (count orgs) 1)) current-org (team->org team) @@ -632,16 +631,7 @@ (if (dnt/is-valid-license? profile) (dnt/go-to-nitrate-cc-create-org) (st/emit! (dnt/show-nitrate-popup :nitrate-form)))))] - (if no-orgs? - [:div {:class (stl/css :nitrate-selected-org)} - [:span {:class (stl/css :nitrate-penpot-icon)} - [:> raw-svg* {:id penpot-logo-icon}]] - "Penpot" - [:> button* {:variant "ghost" - :type "button" - :class (stl/css :nitrate-create-org) - :on-click on-create-org-click} (tr "dashboard.plus-create-new-org")]] - + (if show-dropdown? [:div {:class (stl/css :sidebar-org-switch)} [:button {:class (stl/css :current-org) @@ -669,7 +659,15 @@ :class (stl/css :dropdown :teams-dropdown) :organization current-org :profile profile - :organizations orgs}]]))) + :organizations orgs}]] + [:div {:class (stl/css :nitrate-selected-org)} + [:span {:class (stl/css :nitrate-penpot-icon)} + [:> raw-svg* {:id penpot-logo-icon}]] + "Penpot" + [:> button* {:variant "ghost" + :type "button" + :class (stl/css :nitrate-create-org) + :on-click on-create-org-click} (tr "dashboard.plus-create-new-org")]]))) (mf/defc sidebar-team-switch* [{:keys [team profile]}] From 7f228e58c6129a35e893d6d6041896102aa64ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Fri, 27 Mar 2026 11:13:37 +0100 Subject: [PATCH 083/288] :sparkles: Update delete team modal when in org --- .../src/app/main/ui/dashboard/sidebar.cljs | 21 ++++++++++++------- frontend/translations/en.po | 3 +++ frontend/translations/es.po | 3 +++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 15f3aef434..fb52064251 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -516,14 +516,19 @@ on-delete-clicked (mf/use-fn - (mf/deps delete-fn) - #(st/emit! - (modal/show - {:type :confirm - :title (tr "modals.delete-team-confirm.title") - :message (tr "modals.delete-team-confirm.message") - :accept-label (tr "modals.delete-team-confirm.accept") - :on-accept delete-fn})))] + (mf/deps team delete-fn) + (fn [] + (let [is-org-team? (some? (:organization-id team)) + message (if is-org-team? + (tr "modals.delete-org-team-confirm.message" (:organization-name team)) + (tr "modals.delete-team-confirm.message"))] + (st/emit! + (modal/show + {:type :confirm + :title (tr "modals.delete-team-confirm.title") + :message message + :accept-label (tr "modals.delete-team-confirm.accept") + :on-accept delete-fn})))))] [:> dropdown-menu* props [:> dropdown-menu-item* {:on-click go-members diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 1f1ca31b51..9108367da9 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -8950,3 +8950,6 @@ msgstr "Autosaved versions will be kept for %s days." #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Click to close the path" + +msgid "modals.delete-org-team-confirm.message" +msgstr "Are you sure you want to delete this team that is part of %s org?" \ No newline at end of file diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 23ab6c4cc8..c4bd7006d0 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -8783,3 +8783,6 @@ msgstr "Los autoguardados duran %s días." #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Pulsar para cerrar la ruta" + +msgid "modals.delete-org-team-confirm.message" +msgstr "¿Estás seguro de que deseas eliminar este equipo que forma parte de la organización %s?" From 1b68318c6b9879803e3deed5c308d4f753a6e700 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Mon, 30 Mar 2026 08:14:17 +0200 Subject: [PATCH 084/288] :bug: Token tree must be expanded by default (#8799) --- CHANGES.md | 1 + common/src/app/common/path_names.cljc | 33 ++++--- .../playwright/ui/specs/tokens/apply.spec.js | 58 ++++-------- .../playwright/ui/specs/tokens/crud.spec.js | 90 ++++++++++++------- .../playwright/ui/specs/tokens/helpers.js | 33 ++----- .../playwright/ui/specs/tokens/tree.spec.js | 6 +- .../data/workspace/tokens/library_edit.cljs | 66 +++++++++----- .../main/ui/workspace/tokens/management.cljs | 9 +- .../tokens/management/forms/generic_form.cljs | 4 +- .../ui/workspace/tokens/management/group.cljs | 22 +++-- .../tokens/management/token_tree.cljs | 27 +++--- 11 files changed, 176 insertions(+), 173 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6cccf6913b..dacdaeab9c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,6 +32,7 @@ - Fix scroll on library modal [Taiga #13639](https://tree.taiga.io/project/penpot/issue/13639) - Fix dates to avoid show them in english when browser is in auto [Taiga #13786](https://tree.taiga.io/project/penpot/issue/13786) - Fix focus radio button [Taiga #13841](https://tree.taiga.io/project/penpot/issue/13841) +- Token tree should be expanded by default [Taiga #13631](https://tree.taiga.io/project/penpot/issue/13631) ## 2.15.0 (Unreleased) diff --git a/common/src/app/common/path_names.cljc b/common/src/app/common/path_names.cljc index 00038cdf6c..658ffe0349 100644 --- a/common/src/app/common/path_names.cljc +++ b/common/src/app/common/path_names.cljc @@ -148,16 +148,16 @@ Some naming conventions: :path 'one' :depth 0 :leaf nil - :children-fn (fn [] [{:name 'two' - :path 'one.two' - :depth 1 - :leaf nil - :children-fn (fn [] [{... :name 'three'} {... :name 'four'}])} - {:name 'five' - :path 'one.five' - :depth 1 - :leaf {... :name 'five'} - ...}])}]" + :children [{:name 'two' + :path 'one.two' + :depth 1 + :leaf nil + :children [{... :name 'three'} {... :name 'four'}]} + {:name 'five' + :path 'one.five' + :depth 1 + :leaf {... :name 'five'} + :children nil}]}]" (defn- sort-by-children "Sorts segments so that those with children come first." @@ -191,7 +191,7 @@ Some naming conventions: (into (sorted-map) grouped))) (defn- build-tree-node - "Builds a single tree node with lazy children." + "Builds a single tree node with computed children." [segment-name remaining-segments separator parent-path depth] (let [current-path (if parent-path (str parent-path "." segment-name) @@ -208,12 +208,11 @@ Some naming conventions: :path current-path :depth depth :leaf leaf-segment - :children-fn (when-not is-leaf? - (fn [] - (let [grouped-elements (sort-and-group-segments remaining-segments separator)] - (mapv (fn [[child-segment-name remaining-child-segments]] - (build-tree-node child-segment-name remaining-child-segments separator current-path (inc depth))) - grouped-elements))))}] + :children (when-not is-leaf? + (let [grouped-elements (sort-and-group-segments remaining-segments separator)] + (mapv (fn [[child-segment-name remaining-child-segments]] + (build-tree-node child-segment-name remaining-child-segments separator current-path (inc depth))) + grouped-elements)))}] node)) (defn build-tree-root diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js index 69ed14f051..bda71e08ba 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -4,7 +4,7 @@ import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage"; import { setupTokensFileRender, setupTypographyTokensFileRender, - unfoldTokenTree, + unfoldTokenType, } from "./helpers"; test.beforeEach(async ({ page }) => { @@ -24,10 +24,9 @@ test.describe("Tokens: Apply token", () => { .filter({ hasText: "Button" }) .click(); - const tokensTabButton = page.getByRole("tab", { name: "Tokens" }); - await tokensTabButton.click(); + await page.getByRole("tab", { name: "Tokens" }).click(); - unfoldTokenTree(tokensSidebar, "color", "colors.black"); + await unfoldTokenType(tokensSidebar, "color"); await tokensSidebar .getByRole("button", { name: "black" }) @@ -52,17 +51,15 @@ test.describe("Tokens: Apply token", () => { await workspacePage.layers.getByTestId("layer-row").nth(1).click(); // Open tokens sections on left sidebar - const tokensTabButton = page.getByRole("tab", { name: "Tokens" }); - await tokensTabButton.click(); - // Unfold border radius tokens - await page.getByRole("button", { name: "Border Radius 3" }).click(); + await page.getByRole("tab", { name: "Tokens" }).click(); + + await unfoldTokenType(tokensSidebar, "border radius"); await expect( - tokensSidebar.getByRole("button", { name: "borderRadius" }), - ).toBeVisible(); - await tokensSidebar.getByRole("button", { name: "borderRadius" }).click(); - await expect( - tokensSidebar.getByRole("button", { name: "borderRadius.sm" }), + tokensSidebar.getByRole("button", { + name: "borderRadius.sm", + exact: true, + }), ).toBeVisible(); // Apply border radius token from token panels @@ -119,13 +116,7 @@ test.describe("Tokens: Apply token", () => { await tokensTabButton.click(); // Unfold opacity tokens - await page.getByRole("button", { name: "Opacity 3" }).click(); - await expect( - tokensSidebar.getByRole("button", { name: "opacity", exact: true }), - ).toBeVisible(); - await tokensSidebar - .getByRole("button", { name: "opacity", exact: true }) - .click(); + await unfoldTokenType(tokensSidebar, "opacity"); await expect( tokensSidebar.getByRole("button", { name: "opacity.high" }), ).toBeVisible(); @@ -203,12 +194,8 @@ test.describe("Tokens: Apply token", () => { test("User adds shadow token with multiple shadows and applies it to shape", async ({ page, }) => { - const { - tokensUpdateCreateModal, - tokensSidebar, - workspacePage, - tokenContextMenuForToken, - } = await setupTokensFileRender(page, { flags: ["enable-token-shadow"] }); + const { tokensUpdateCreateModal, tokensSidebar, workspacePage } = + await setupTokensFileRender(page, { flags: ["enable-token-shadow"] }); const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -476,8 +463,6 @@ test.describe("Tokens: Apply token", () => { await submitButton.click(); await expect(tokensUpdateCreateModal).not.toBeVisible(); - unfoldTokenTree(tokensSidebar, "shadow", "primary"); - // Verify token appears in sidebar const shadowToken = tokensSidebar.getByRole("button", { name: "primary", @@ -512,7 +497,7 @@ test.describe("Tokens: Apply token", () => { const tokensTabButton = page.getByRole("tab", { name: "Tokens" }); await tokensTabButton.click(); - unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.sm"); + await unfoldTokenType(tokensSidebar, "dimensions"); // Apply token to width and height token from token panel await tokensSidebar.getByRole("button", { name: "dimension.sm" }).click(); @@ -565,7 +550,7 @@ test.describe("Tokens: Apply token", () => { const tokensTabButton = page.getByRole("tab", { name: "Tokens" }); await tokensTabButton.click(); - unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.sm"); + await unfoldTokenType(tokensSidebar, "dimensions"); // Apply token to width and height token from token panel await tokensSidebar @@ -621,7 +606,7 @@ test.describe("Tokens: Apply token", () => { const tokensTabButton = page.getByRole("tab", { name: "Tokens" }); await tokensTabButton.click(); - unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.sm"); + await unfoldTokenType(tokensSidebar, "dimensions"); // Apply token to width and height token from token panel await tokensSidebar @@ -677,7 +662,7 @@ test.describe("Tokens: Apply token", () => { const tokensTabButton = page.getByRole("tab", { name: "Tokens" }); await tokensTabButton.click(); - unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.xs"); + await unfoldTokenType(tokensSidebar, "dimensions"); // Apply token to width and height token from token panel await tokensSidebar @@ -809,8 +794,7 @@ test.describe("Tokens: Apply token", () => { const tokensTab = page.getByRole("tab", { name: "Tokens" }); await expect(tokensTab).toBeVisible(); await tokensTab.click(); - await page.getByRole("button", { name: "Dimensions 4" }).click(); - await page.getByRole("button", { name: "dim", exact: true }).click(); + await unfoldTokenType(workspace.tokensSidebar, "dimensions"); const tokensSidebar = workspace.tokensSidebar; await expect( tokensSidebar.getByRole("button", { name: "dim.md" }), @@ -881,11 +865,7 @@ test.describe("Tokens: Detach token", () => { await tokensTabButton.click(); // Unfold border radius tokens - await page.getByRole("button", { name: "Border Radius 3" }).click(); - await expect( - tokensSidebar.getByRole("button", { name: "borderRadius" }), - ).toBeVisible(); - await tokensSidebar.getByRole("button", { name: "borderRadius" }).click(); + await unfoldTokenType(tokensSidebar, "Border Radius"); await expect( tokensSidebar.getByRole("button", { name: "borderRadius.sm" }), ).toBeVisible(); diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index 153f1dc25d..58e67be6e5 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -6,7 +6,7 @@ import { setupTokensFileRender, setupTypographyTokensFileRender, testTokenCreationFlow, - unfoldTokenTree, + unfoldTokenType, } from "./helpers"; test.beforeEach(async ({ page }) => { @@ -31,15 +31,9 @@ test.describe("Tokens - creation", () => { }); test("User creates border radius token with combobox", async ({ page }) => { - const invalidValueError = "Invalid token value"; - const emptyNameError = "Name should be at least 1 character"; - const selfReferenceError = "Token has self reference"; - const missingReferenceError = "Missing token references"; - - const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = - await setupEmptyTokensFileRender(page, { - flags: ["enable-token-combobox", "enable-feature-token-input"], - }); + const { tokensUpdateCreateModal } = await setupEmptyTokensFileRender(page, { + flags: ["enable-token-combobox", "enable-feature-token-input"], + }); // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -83,8 +77,10 @@ test.describe("Tokens - creation", () => { await submitButton.click(); + await unfoldTokenType(tokensTabPanel, "border radius"); + await expect( - tokensTabPanel.getByRole('button', { name: 'my-token' }), + tokensTabPanel.getByRole("button", { name: "my-token" }), ).toBeEnabled(); // Create second token referencing the first one using the combobox options @@ -310,7 +306,7 @@ test.describe("Tokens - creation", () => { await expect(submitButton).toBeEnabled(); await submitButton.click(); - await unfoldTokenTree(tokensSidebar, "color", "color.primary"); + await unfoldTokenType(tokensSidebar, "color"); // Create token referencing the previous one with keyboard @@ -477,6 +473,8 @@ test.describe("Tokens - creation", () => { await submitButton.click(); + await unfoldTokenType(tokensTabPanel, "font family"); + await expect( tokensTabPanel.getByRole("button", { name: "my-token" }), ).toBeEnabled(); @@ -631,6 +629,8 @@ test.describe("Tokens - creation", () => { await submitButton.click(); + await unfoldTokenType(tokensTabPanel, "font weight"); + await expect( tokensTabPanel.getByRole("button", { name: "my-token" }), ).toBeEnabled(); @@ -767,6 +767,8 @@ test.describe("Tokens - creation", () => { await submitButton.click(); + await unfoldTokenType(tokensTabPanel, "text case"); + await expect( tokensTabPanel.getByRole("button", { name: "my-token" }), ).toBeEnabled(); @@ -885,6 +887,8 @@ test.describe("Tokens - creation", () => { await submitButton.click(); + await unfoldTokenType(tokensTabPanel, "text decoration"); + await expect( tokensTabPanel.getByRole("button", { name: "my-token" }), ).toBeEnabled(); @@ -1051,6 +1055,8 @@ test.describe("Tokens - creation", () => { await expect(submitButton).toBeEnabled(); await submitButton.click(); + await unfoldTokenType(tokensTabPanel, "shadow"); + await expect( tokensTabPanel.getByRole("button", { name: "my-token" }), ).toBeEnabled(); @@ -1088,6 +1094,8 @@ test.describe("Tokens - creation", () => { await expect(submitButton).toBeEnabled(); await submitButton.click(); + + await unfoldTokenType(tokensTabPanel, "shadow"); await expect( tokensTabPanel.getByRole("button", { name: "my-token-2" }), ).toBeEnabled(); @@ -1109,7 +1117,9 @@ test.describe("Tokens - creation", () => { const nameField = tokensUpdateCreateModal.getByLabel("Name"); await nameField.fill("typography.empty"); - const valueField = tokensUpdateCreateModal.getByRole("textbox", { name: "Font Size" }); + const valueField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Font Size", + }); // Insert a value and then delete it await valueField.fill("1"); @@ -1274,6 +1284,8 @@ test.describe("Tokens - creation", () => { await expect(submitButton).toBeEnabled(); await submitButton.click(); + await unfoldTokenType(tokensTabPanel, "shadow"); + await expect( tokensTabPanel.getByRole("button", { name: "my-token" }), ).toBeEnabled(); @@ -1642,7 +1654,7 @@ test.describe("Tokens - creation", () => { await expect(submitButton).toBeEnabled(); await submitButton.click(); - await unfoldTokenTree(tokensSidebar, "color", "dark.primary"); + await unfoldTokenType(tokensSidebar, "color"); await expect(tokensSidebar.getByLabel("primary")).toBeEnabled(); }); @@ -1681,10 +1693,10 @@ test.describe("Tokens - creation", () => { await expect(tokensSidebar).toBeVisible(); - unfoldTokenTree(tokensSidebar, "color", "colors.blue.100"); + await unfoldTokenType(tokensSidebar, "color"); const colorToken = tokensSidebar.getByRole("button", { - name: "100", + name: "colors.blue.100", }); await colorToken.click({ button: "right" }); @@ -1724,7 +1736,7 @@ test("User creates grouped color token", async ({ page }) => { await expect(submitButton).toBeEnabled(); await submitButton.click(); - await unfoldTokenTree(tokensSidebar, "color", "dark.primary"); + await unfoldTokenType(tokensSidebar, "color"); await expect(tokensSidebar.getByLabel("primary")).toBeEnabled(); }); @@ -1761,10 +1773,10 @@ test("User duplicate color token", async ({ page }) => { await expect(tokensSidebar).toBeVisible(); - unfoldTokenTree(tokensSidebar, "color", "colors.blue.100"); + await unfoldTokenType(tokensSidebar, "color"); const colorToken = tokensSidebar.getByRole("button", { - name: "100", + name: "colors.blue.100", }); await colorToken.click({ button: "right" }); @@ -1809,7 +1821,9 @@ test.describe("Tokens tab - edition", () => { await fontFamilyField.fill("OneWord"); // Invalidate incorrect values for font size - const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", { name: "Font Size" }); + const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Font Size", + }); await fontSizeField.fill("invalid"); await expect( tokensUpdateCreateModal.getByText(/Invalid token value:/), @@ -1824,13 +1838,21 @@ test.describe("Tokens tab - edition", () => { await fontSizeField.fill("16"); await expect(saveButton).toBeEnabled(); - const fontWeightField = tokensUpdateCreateModal.getByRole("textbox", { name: "Font Weight" }); - const letterSpacingField = - tokensUpdateCreateModal.getByRole("textbox", { name: "Letter Spacing" }); - const lineHeightField = tokensUpdateCreateModal.getByRole("textbox", { name: "Line Height" }); - const textCaseField = tokensUpdateCreateModal.getByRole("textbox", { name: "Text Case" }); - const textDecorationField = - tokensUpdateCreateModal.getByRole("textbox", { name: "Text Decoration" }); + const fontWeightField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Font Weight", + }); + const letterSpacingField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Letter Spacing", + }); + const lineHeightField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Line Height", + }); + const textCaseField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Text Case", + }); + const textDecorationField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Text Decoration", + }); // Capture all values before switching tabs const originalValues = { @@ -1883,10 +1905,10 @@ test.describe("Tokens tab - edition", () => { await expect(tokensSidebar).toBeVisible(); - await unfoldTokenTree(tokensSidebar, "color", "colors.blue.100"); + await unfoldTokenType(tokensSidebar, "color"); const colorToken = tokensSidebar.getByRole("button", { - name: "100", + name: "colors.blue.100", }); await expect(colorToken).toBeVisible(); @@ -1904,7 +1926,7 @@ test.describe("Tokens tab - edition", () => { await expect(tokensUpdateCreateModal).not.toBeVisible(); - await unfoldTokenTree(tokensSidebar, "color", "colors.blue.100.changed"); + await unfoldTokenType(tokensSidebar, "color"); const colorTokenChanged = tokensSidebar.getByRole("button", { name: "changed", @@ -1975,10 +1997,10 @@ test.describe("Tokens tab - delete", () => { await expect(tokensSidebar).toBeVisible(); - unfoldTokenTree(tokensSidebar, "color", "colors.blue.100"); + await unfoldTokenType(tokensSidebar, "color"); const colorToken = tokensSidebar.getByRole("button", { - name: "100", + name: "colors.blue.100", }); await expect(colorToken).toBeVisible(); await colorToken.click({ button: "right" }); @@ -1996,7 +2018,7 @@ test.describe("Tokens tab - delete", () => { await expect(tokensSidebar).toBeVisible(); // Expand color tokens - unfoldTokenTree(tokensSidebar, "color", "colors.blue.100"); + await unfoldTokenType(tokensSidebar, "color"); // Verify that the node and child token are visible before deletion const colorNode = tokensSidebar.getByRole("button", { @@ -2004,7 +2026,7 @@ test.describe("Tokens tab - delete", () => { exact: true, }); const colorNodeToken = tokensSidebar.getByRole("button", { - name: "100", + name: "colors.blue.100", }); // Select a node and right click on it to open context menu diff --git a/frontend/playwright/ui/specs/tokens/helpers.js b/frontend/playwright/ui/specs/tokens/helpers.js index 8f8974e40f..657bce8b54 100644 --- a/frontend/playwright/ui/specs/tokens/helpers.js +++ b/frontend/playwright/ui/specs/tokens/helpers.js @@ -207,7 +207,7 @@ const testTokenCreationFlow = async ( const selfReferenceError = "Token has self reference"; const missingReferenceError = "Missing token references"; - const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = + const { tokensUpdateCreateModal, tokensSidebar } = await setupEmptyTokensFileRender(page); // Open modal @@ -313,12 +313,11 @@ const testTokenCreationFlow = async ( ).toBeEnabled(); }; -const unfoldTokenTree = async (tokensTabPanel, type, tokenName) => { - const tokenSegments = tokenName.split("."); - const tokenFolderTree = tokenSegments.slice(0, -1); - const tokenLeafName = tokenSegments.pop(); - - const typeParentWrapper = tokensTabPanel.getByTestId(`section-${type}`); +const unfoldTokenType = async (tokensTabPanel, type) => { + const kebabClaseType = type.toLocaleLowerCase().replace(/\s/g, "-"); + const typeParentWrapper = tokensTabPanel.getByTestId( + `section-${kebabClaseType}`, + ); const typeSectionButton = typeParentWrapper .getByRole("button", { name: type, @@ -331,24 +330,6 @@ const unfoldTokenTree = async (tokensTabPanel, type, tokenName) => { if (isSectionExpanded === "false") { await typeSectionButton.click(); } - - for (const segment of tokenFolderTree) { - const segmentButton = typeParentWrapper - .getByRole("listitem") - .getByRole("button", { name: segment }) - .first(); - - const isExpanded = await segmentButton.getAttribute("aria-expanded"); - if (isExpanded === "false") { - await segmentButton.click(); - } - } - - await expect( - typeParentWrapper.getByRole("button", { - name: tokenLeafName, - }), - ).toBeEnabled(); }; export { @@ -359,5 +340,5 @@ export { setupTypographyTokensFile, setupTypographyTokensFileRender, testTokenCreationFlow, - unfoldTokenTree, + unfoldTokenType, }; diff --git a/frontend/playwright/ui/specs/tokens/tree.spec.js b/frontend/playwright/ui/specs/tokens/tree.spec.js index ae43197acc..6228b52a2a 100644 --- a/frontend/playwright/ui/specs/tokens/tree.spec.js +++ b/frontend/playwright/ui/specs/tokens/tree.spec.js @@ -1,7 +1,7 @@ import { test, expect } from "@playwright/test"; import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage"; import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage"; -import { setupTokensFileRender, unfoldTokenTree } from "./helpers"; +import { setupTokensFileRender } from "./helpers"; test.beforeEach(async ({ page }) => { await WasmWorkspacePage.init(page); @@ -20,10 +20,8 @@ test.describe("Tokens - node tree", () => { await expect(tokensColorGroup).toBeVisible(); await tokensColorGroup.click(); - await unfoldTokenTree(tokensSidebar, "color", "colors.blue.100"); - const colorToken = tokensSidebar.getByRole("button", { - name: "100", + name: "colors.blue.100", }); await expect(colorToken).toBeVisible(); await tokensColorGroup.click(); diff --git a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs index 22bd7789fb..2fe57d491f 100644 --- a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs +++ b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs @@ -11,7 +11,6 @@ [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.logic.tokens :as clt] - [app.common.path-names :as cpn] [app.common.types.shape :as cts] [app.common.types.tokens-lib :as ctob] [app.common.uuid :as uuid] @@ -62,52 +61,77 @@ (watch [_ _ _] (rx/of (dwsh/update-shapes [id] #(merge % attrs))))))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Toggle tree nodes +;; TOKENS TREE - Type folders ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn- remove-paths-recursively +(defn open-token-type + ([types type] + (conj (or types #{}) type)) + ([type] + (ptk/reify ::open-token-type + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-tokens :unfolded-token-types] + #(open-token-type % type)))))) + +(defn close-token-type + ([types type] + (disj (or types #{}) type)) + ([type] + (ptk/reify ::close-token-type + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-tokens :unfolded-token-types] + #(close-token-type % type)))))) + +(defn toggle-token-type + [type] + (ptk/reify ::toggle-token-type + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-tokens :unfolded-token-types] + (fn [types] + (if (contains? (or types #{}) type) + (close-token-type types type) + (open-token-type types type))))))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TOKENS TREE - Toggle tree nodes +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- remove-path [path paths] (->> paths - (remove #(str/starts-with? % (str path))) + (remove #(= % path)) vec)) (defn add-path [path paths] - (let [split-path (cpn/split-path path :separator ".") - partial-paths (->> split-path - (reduce - (fn [acc segment] - (let [new-acc (if (empty? acc) - segment - (str (last acc) "." segment))] - (conj acc new-acc))) - []))] - (->> paths - (into partial-paths) - distinct - vec))) + (vec (conj paths path))) (defn clear-tokens-paths [] (ptk/reify ::clear-tokens-paths ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-tokens :unfolded-token-paths] [])))) + (assoc-in state [:workspace-tokens :folded-token-paths] [])))) (defn toggle-token-path [path] (ptk/reify ::toggle-token-path ptk/UpdateEvent (update [_ state] - (update-in state [:workspace-tokens :unfolded-token-paths] + (update-in state [:workspace-tokens :folded-token-paths] (fn [paths] (let [paths (or paths [])] (if (some #(= % path) paths) - (remove-paths-recursively path paths) + (remove-path path paths) (add-path path paths)))))))) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; TOKENS Actions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/ui/workspace/tokens/management.cljs b/frontend/src/app/main/ui/workspace/tokens/management.cljs index 7e077cc1f4..f462b48e5e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management.cljs @@ -171,14 +171,11 @@ path (:name token) tokens-by-type (ctob/group-by-type selected-token-set-tokens) tokens-filtered-by-type (get tokens-by-type type) - tokens-in-path-ids (filter-tokens-by-path-ids type path) - remaining-tokens? (remaining-tokens-of-type-in-set? tokens-filtered-by-type tokens-in-path-ids)] - ;; Delete the token + remaining-tokens? (remaining-tokens-of-type-in-set? tokens-filtered-by-type [id])] (st/emit! (dwtl/delete-token selected-token-set-id id)) - ;; Remove from unfolded tree path (if remaining-tokens? (st/emit! (dwtl/toggle-token-path (str (name type) "." path))) - (st/emit! (dwtl/toggle-token-path (name type))))))) + (st/emit! (dwtl/close-token-type type)))))) delete-node (mf/with-memo [selected-token-set-tokens selected-token-set-id] @@ -193,7 +190,7 @@ ;; Remove from unfolded tree path (if remaining-tokens? (st/emit! (dwtl/toggle-token-path (str (name type) "." path))) - (st/emit! (dwtl/toggle-token-path (name type))))))) + (st/emit! (dwtl/close-token-type type)))))) bulk-rename-tokens-in-path ;; Rename tokens in bulk affected by a node rename. diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs index b968395e7c..a4cc813bbc 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.workspace.tokens.management.forms.generic-form (:require-macros [app.main.style :as stl]) (:require - [app.common.data :as d] [app.common.files.tokens :as cfo] [app.common.schema :as sm] [app.common.types.tokens-lib :as ctob] @@ -186,7 +185,6 @@ (mf/deps validate-token token tokens token-type value-subfield value-type active-tab on-remap-token on-rename-token is-create) (fn [form _event] (let [name (get-in @form [:clean-data :name]) - path (str (d/name token-type) "." name) description (get-in @form [:clean-data :description]) value (get-in @form [:clean-data :value]) value-for-validation (get-value-for-validator active-tab value value-subfield value-type)] @@ -221,7 +219,7 @@ {:name name :value (:value valid-token) :description description})) - (dwtl/toggle-token-path path) + (dwtl/open-token-type (:type token)) (dwtp/propagate-workspace-tokens) (modal/hide!))))) ;; WORKAROUND: display validation errors in the form instead of crashing diff --git a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs index 19c3636e28..83228ecaac 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs @@ -27,8 +27,11 @@ [okulary.core :as l] [rumext.v2 :as mf])) -(def ref:unfolded-token-paths - (l/derived (l/key :unfolded-token-paths) refs/workspace-tokens)) +(def ref:folded-token-paths + (l/derived (l/key :folded-token-paths) refs/workspace-tokens)) + +(def ref:unfolded-token-types + (l/derived (l/key :unfolded-token-types) refs/workspace-tokens)) (defn token-section-icon [type] @@ -72,8 +75,10 @@ (let [{:keys [modal title]} (get dwta/token-properties type) - unfolded-token-paths (mf/deref ref:unfolded-token-paths) - is-type-unfolded (contains? (set unfolded-token-paths) (name type)) + folded-token-paths (mf/deref ref:folded-token-paths) + unfolded-token-types (mf/deref ref:unfolded-token-types) + + is-type-unfolded (contains? (set unfolded-token-types) type) editing-ref (mf/deref refs/workspace-editor-state) edition (mf/deref refs/selected-edition) @@ -117,7 +122,7 @@ (mf/deps type expandable?) (fn [] (when expandable? - (st/emit! (dwtl/toggle-token-path (name type)))))) + (st/emit! (dwtl/toggle-token-type type))))) on-popover-open-click (mf/use-fn @@ -172,13 +177,12 @@ (when is-type-unfolded [:> token-tree* {:tokens tokens :type type - :id (dm/str "token-tree-" (name type)) - :tokens-lib tokens-lib - :unfolded-token-paths unfolded-token-paths + :folded-token-paths folded-token-paths :selected-shapes selected-shapes + :is-selected-inside-layout is-selected-inside-layout :active-theme-tokens active-theme-tokens :selected-token-set-id selected-token-set-id - :is-selected-inside-layout is-selected-inside-layout + :tokens-lib tokens-lib :on-token-pill-click on-token-pill-click :on-pill-context-menu on-pill-context-menu :on-node-context-menu on-node-context-menu}])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs index 3fdc067bd0..b27db0bc85 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs @@ -21,7 +21,7 @@ [:map [:node :any] [:type :keyword] - [:unfolded-token-paths {:optional true} [:vector :string]] + [:folded-token-paths {:optional true} [:maybe [:vector :string]]] [:selected-shapes :any] [:is-selected-inside-layout {:optional true} :boolean] [:active-theme-tokens {:optional true} :any] @@ -35,7 +35,7 @@ {::mf/schema schema:folder-node} [{:keys [node type - unfolded-token-paths + folded-token-paths selected-shapes is-selected-inside-layout active-theme-tokens @@ -45,12 +45,11 @@ on-pill-context-menu on-node-context-menu]}] (let [full-path (str (name type) "." (:path node)) - is-folder-expanded (contains? (set (or unfolded-token-paths [])) full-path) + is-folder-expanded (not (contains? (set (or folded-token-paths [])) full-path)) swap-folder-expanded (mf/use-fn - (mf/deps (:path node) type) + (mf/deps full-path) (fn [] - (let [path (str (name type) "." (:path node))] - (st/emit! (dwtl/toggle-token-path path))))) + (st/emit! (dwtl/toggle-token-path full-path)))) node-context-menu-prep (mf/use-fn (mf/deps on-node-context-menu node) @@ -66,18 +65,18 @@ :on-toggle-expand swap-folder-expanded :on-context-menu node-context-menu-prep}] (when is-folder-expanded - (let [children-fn (:children-fn node)] + (let [children (:children node)] [:div {:class (stl/css :folder-children-wrapper) :id (str "folder-children-" (:path node))} - (when children-fn - (let [sorted-children (d/natural-sort-by :name (children-fn))] + (when (seq children) + (let [sorted-children (d/natural-sort-by :name children)] (for [child sorted-children] (if (not (:leaf child)) [:ul {:class (stl/css :node-parent) :key (:path child)} [:> folder-node* {:type type :node child - :unfolded-token-paths unfolded-token-paths + :folded-token-paths folded-token-paths :selected-shapes selected-shapes :is-selected-inside-layout is-selected-inside-layout :active-theme-tokens active-theme-tokens @@ -101,12 +100,12 @@ [:map [:tokens :any] [:type :keyword] - [:unfolded-token-paths {:optional true} [:vector :string]] + [:folded-token-paths {:optional true} [:maybe [:vector :string]]] [:selected-shapes :any] [:is-selected-inside-layout {:optional true} :boolean] [:active-theme-tokens {:optional true} :any] - [:selected-token-set-id {:optional true} :any] [:tokens-lib {:optional true} :any] + [:selected-token-set-id {:optional true} :any] [:on-token-pill-click {:optional true} fn?] [:on-pill-context-menu {:optional true} fn?] [:on-node-context-menu {:optional true} fn?]]) @@ -115,7 +114,7 @@ {::mf/schema schema:token-tree} [{:keys [tokens type - unfolded-token-paths + folded-token-paths selected-shapes is-selected-inside-layout active-theme-tokens @@ -153,7 +152,7 @@ :key (:path node)} [:> folder-node* {:node node :type type - :unfolded-token-paths unfolded-token-paths + :folded-token-paths folded-token-paths :selected-shapes selected-shapes :is-selected-inside-layout is-selected-inside-layout :active-theme-tokens active-theme-tokens From 06aec4b3a3b1de65c393eeda6151b809d0cf986d Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 30 Mar 2026 09:39:35 +0200 Subject: [PATCH 085/288] :sparkles: Add is-default to nitrate summary (#8814) --- backend/src/app/rpc/management/nitrate.clj | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index fd15ff811b..beba802848 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -158,7 +158,7 @@ ;; ---- API: get-teams-summary (def ^:private sql:get-teams-summary - "SELECT t.id, t.name + "SELECT t.id, t.name, t.is_default FROM team AS t WHERE t.id = ANY(?) AND t.deleted_at IS NULL;") @@ -181,7 +181,8 @@ [:map [:teams [:vector [:map [:id ::sm/uuid] - [:name ::sm/text]]]] + [:name ::sm/text] + [:is-default ::sm/boolean]]]] [:num-files ::sm/int]]) (sv/defmethod ::get-teams-summary @@ -204,7 +205,7 @@ (let [ids-array (db/create-array conn "uuid" ids) teams (db/exec! conn [sql:get-teams-summary ids-array]) files-count (-> (db/exec-one! conn [sql:get-files-count ids-array]) :count)] - {:teams (mapv #(select-keys % [:id :name]) teams) + {:teams teams :num-files files-count}))))) From 04f6307c696059b93be3672b8bf8467517e91399 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Mon, 30 Mar 2026 13:35:24 +0200 Subject: [PATCH 086/288] :bug: Fix radio-buttons component in the DS (#8820) --- .../main/ui/ds/controls/radio_buttons.cljs | 42 ++++++---- .../app/main/ui/ds/controls/radio_buttons.mdx | 77 +++++++++++++------ .../main/ui/ds/controls/radio_buttons.scss | 5 ++ .../ui/ds/controls/radio_buttons.stories.jsx | 29 ++++++- 4 files changed, 113 insertions(+), 40 deletions(-) diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs b/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs index ea9dd6fff3..044fe87bc4 100644 --- a/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs +++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs @@ -36,45 +36,55 @@ [:selected {:optional true} [:maybe [:or :keyword :string]]] [:allow-empty {:optional true} :boolean] + [:disabled {:optional true} :boolean] [:options [:vector {:min 1} schema:radio-button]] [:on-change {:optional true} fn?]]) (mf/defc radio-buttons* {::mf/schema schema:radio-buttons} - [{:keys [class variant extended name selected allow-empty options on-change] :rest props}] + [{:keys [class variant extended name selected allow-empty options on-change disabled] :rest props}] (let [options (if (array? options) (mfu/bean options) options) - type (if allow-empty "checkbox" "radio") - variant (d/nilv variant "secondary") + type (if allow-empty "checkbox" "radio") + variant (d/nilv variant "secondary") + wrapper-disabled (d/nilv disabled false) handle-click (mf/use-fn (fn [event] (let [target (dom/get-target event) - label (dom/get-parent-with-data target "label")] - (dom/prevent-default event) - (dom/stop-propagation event) - (dom/click label)))) + label (dom/get-parent-with-data target "label") + input (dom/query label "input") + disabled? (dom/get-attribute target "disabled")] + (when-not disabled? + (dom/click input))))) handle-change (mf/use-fn - (mf/deps selected on-change) + (mf/deps selected on-change allow-empty) (fn [event] - (let [input (dom/get-target event) - value (dom/get-target-val event)] + (let [input (dom/get-target event) + value (dom/get-target-val event) + selected-str (when selected (d/name selected)) + new-value (if (and allow-empty (= value selected-str)) + nil + value)] (when (fn? on-change) - (on-change value event)) + (on-change new-value event)) (dom/blur! input)))) props (mf/spread-props props {:key (dm/str name "-" selected) :class [class (stl/css-case :wrapper true + :disabled disabled :extended extended)]})] [:> :div props (for [[idx {:keys [id class value label icon disabled]}] (d/enumerate options)] - (let [checked? (= selected value)] + (let [value-str (d/name value) + selected-str (when selected (d/name selected)) + checked? (= selected-str value-str)] [:label {:key idx :html-for id :data-label true @@ -88,13 +98,13 @@ :aria-pressed checked? :aria-label label :icon icon - :disabled disabled}] + :disabled (or disabled wrapper-disabled)}] [:> button* {:variant variant :on-click handle-click :aria-pressed checked? :class (stl/css-case :button true :extended extended) - :disabled disabled} + :disabled (or disabled wrapper-disabled)} label]) [:input {:id id @@ -102,6 +112,6 @@ :on-change handle-change :type type :name name - :disabled disabled + :disabled (or disabled wrapper-disabled) :value value - :default-checked checked?}]]))])) + :checked checked?}]]))])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.mdx b/frontend/src/app/main/ui/ds/controls/radio_buttons.mdx index 226319286a..5346d8751c 100644 --- a/frontend/src/app/main/ui/ds/controls/radio_buttons.mdx +++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.mdx @@ -11,11 +11,17 @@ import * as RadioButtons from "./radio_buttons.stories"; # Radio Buttons -The `radio-buttons*` component allows users to switch between two or more options that are mutually exclusive. +The `radio-buttons*` component lets users select a single option from a set of mutually exclusive choices. + +It is designed for immediate selection changes, without requiring a confirmation step. + +--- ## Variants -Radio buttons with text only. The label will be the text of the button. +### Text only + +Radio buttons using text labels. The label is displayed directly on each option. @@ -34,12 +40,14 @@ Radio buttons with text only. The label will be the text of the button. {:id "align-right" :label "Right" :value "right"}]}] + + Icon only ``` -Radio buttons with icons only. In this case, the label will act as the tooltip of each button. +### Icons only +Radio buttons using icons instead of text labels. The label is used as tooltip and accessibility text. - ```clj (ns app.main.ui.foo (:require @@ -63,35 +71,58 @@ Radio buttons with icons only. In this case, the label will act as the tooltip o :label "Right align" :value "right"}]}] ``` +### Anatomy -## Anatomy +Each option is composed of: -Under the hood, each option is represented by -- a button, which is the visible and clickable element. It may be either an icon button or a text button. -- a radio input, which is not visible but retains the current state of the option. +A visible control (button or icon button) +A hidden native input (radio or checkbox) that stores the state -A radio group is defined by giving each of radio buttons in the group the same name. Once a radio group is established, -selecting any radio button in that group automatically deselects any currently-selected radio button in the same group. +All options share the same name, forming a radio group. Selecting one option automatically deselects the previously selected one. -The `selected` parameter should be set to the value of the option that is to be active. Otherwise, no option will be selected. +## Behavior -If the parameter `allow-empty` is enabled, then the component will work with checkboxes instead of radio buttons, -and therefore the selected option can be deselected. However, it will still only be possible to select one option. +### Selection +The selected prop controls the active option +It must match the value of one of the provided options +If selected is nil, no option is selected -The `extended` parameter allows the component to use all the available space from the parent and distribute it equally -among all elements. +### Allow empty -Any option can be individually disabled using the `disabled` parameter. +When allow-empty is enabled: + +The selected option can be deselected +Only one option can still be active at a time +This introduces toggle-like behavior over a single selection group + +### Extended + +When extended is enabled: + +The component expands to fill the width of its container +Options are evenly distributed across available space + +### Disabled state +The entire group can be disabled using the `:disabled` prop +Individual options can also be disabled using `:disabled` inside each option +Disabled options cannot be interacted with. ## Usage Guidelines -### When to Use +### When to use +For settings where users must choose exactly one option +For preference or configuration panels +When changes should take effect immediately -- For multiple choice settings that take effect immediately. -- In preference panels and configuration screens. +### When not to use -### When Not to Use +For boolean toggles → use a switch or checkbox +For multiple selection → use checkboxes +For actions requiring confirmation → use buttons or dialogs +For workflows that require an explicit “Apply” step -- For boolean settings (use switch or checkbox instead). -- For actions that require confirmation (use buttons instead). -- For temporary states that need explicit "Apply" action. +### Notes + +This component is controlled: state must be managed externally via selected +It does not manage internal state +The on-change handler is called with the new value whenever selection changes \ No newline at end of file diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.scss b/frontend/src/app/main/ui/ds/controls/radio_buttons.scss index 05957025dc..56e53fae7e 100644 --- a/frontend/src/app/main/ui/ds/controls/radio_buttons.scss +++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.scss @@ -20,6 +20,11 @@ width: 100%; display: flex; } + + &.disabled { + outline: $b-1 solid var(--color-background-quaternary); + background-color: transparent; + } } .label { diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx b/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx index 7133a1b961..157f83e465 100644 --- a/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx +++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx @@ -15,6 +15,12 @@ const options = [ { id: "right", label: "Right", value: "right" }, ]; +const optionsDisabled = [ + { id: "left", label: "Left", value: "left" }, + { id: "center", label: "Center", value: "center", disabled: true }, + { id: "right", label: "Right", value: "right" }, +]; + const optionsIcon = [ { id: "left", label: "Left align", value: "left", icon: "text-align-left" }, { @@ -68,9 +74,24 @@ export default { parameters: { controls: { exclude: ["options", "on-change"], + disabled: { + control: { type: "boolean" }, + }, }, }, - render: ({ ...args }) => , + render: (args) => { + const [selected, setSelected] = React.useState(args.selected); + + return ( + { + setSelected(value); + }} + /> + ); + }, }; export const Default = {}; @@ -80,3 +101,9 @@ export const WithIcons = { options: optionsIcon, }, }; + +export const WithOptionDisabled = { + args: { + options: optionsDisabled, + }, +}; From 7ecfe773383eedccb44ac8d5b2c983a2aceb3f93 Mon Sep 17 00:00:00 2001 From: Elenzakaleidos Date: Mon, 30 Mar 2026 16:17:39 +0200 Subject: [PATCH 087/288] Update README.md (#8833) * :lipstick: Update README.md I updated the two images and removed the fest announcement Signed-off-by: Elenzakaleidos * :recycle: Improve Markdown --------- Signed-off-by: Elenzakaleidos Co-authored-by: Luis de Dios --- README.md | 93 +++++++++++++++++++++++-------------------------------- 1 file changed, 39 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 07190bcb29..988407f1b3 100644 --- a/README.md +++ b/README.md @@ -9,45 +9,39 @@

-License: MPL-2.0 -Penpot Community -Managed with Taiga.io -Gitpod ready-to-code + License: MPL-2.0 + Penpot Community + Managed with Taiga.io + Gitpod ready-to-code

- Website • - User Guide • - Learning Center • - Community + Website • + User Guide • + Learning Center • + Community

- Youtube • - Peertube • - Linkedin • - Instagram • - Mastodon • - Bluesky • - X - + Youtube • + Peertube • + Linkedin • + Instagram • + Mastodon • + Bluesky • + X

-
- -[Penpot video](https://github.com/user-attachments/assets/7c67fd7c-04d3-4c9b-88ec-b6f5e23f8332 -) - -
+[Penpot video](https://github.com/user-attachments/assets/7c67fd7c-04d3-4c9b-88ec-b6f5e23f8332) Penpot is the first **open-source** design tool for design and code collaboration. Designers can create stunning designs, interactive prototypes, design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast. And all of this with no handoff drama. Available on browser or self-hosted, Penpot works with open standards like SVG, CSS, HTML and JSON, and it’s free! The latest updates take Penpot even further. It’s the first design tool to integrate native [design tokens](https://penpot.dev/collaboration/design-tokens)—a single source of truth to improve efficiency and collaboration between product design and development. -With the [huge 2.0 release](https://penpot.app/dev-diaries), Penpot took the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more. -For organizations that need extra service for its teams, [get in touch](https://cal.com/team/penpot/talk-to-us) -🎇 Design, code, and Open Source meet at [Penpot Fest](https://penpot.app/penpotfest)! Be part of the 2025 edition in Madrid, Spain, on October 9-10. +With the [huge 2.0 release](https://penpot.app/dev-diaries), Penpot took the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more. + +For organizations that need extra service for its teams, [get in touch](https://cal.com/team/penpot/talk-to-us). ## Table of contents ## @@ -63,43 +57,42 @@ For organizations that need extra service for its teams, [get in touch](https:// Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration. ### Plugin system ### + [Penpot plugins](https://penpot.app/penpothub/plugins) let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions. ### Designed for developers ### + Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo". ### Inspect mode ### + Work with ready-to-use code and make your workflow easy and fast. The inspect tab gives instant access to SVG, CSS and HTML code. ### Self host your own instance ### + Provide your team or organization with a completely owned collaborative design tool. Use Penpot's cloud service or deploy your own Penpot server. ### Integrations ### + Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens. -### Building Design Systems: design tokens, components and variants ### +### Building Design Systems: design tokens, components and variants ### + Penpot brings design systems to code-minded teams: a single source of truth with native Design Tokens, Components, and Variants for scalable, reusable, and consistent UI across projects and platforms. - -
-

- +

-
- ## Getting started ## Penpot is the only design & prototype platform that is deployment agnostic. You can use it in our [SAAS](https://design.penpot.app) or deploy it anywhere. Learn how to install it with Docker, Kubernetes, Elestio or other options on [our website](https://penpot.app/self-host). -

- Open Source +

-
## Community ## @@ -108,6 +101,7 @@ We love the Open Source software community. Contributing is our passion and if i If you need help or have any questions; if you’d like to share your experience using Penpot or get inspired; if you’d rather meet our community of developers and designers, [join our Community](https://community.penpot.app/)! You will find the following categories: + - [Ask the Community](https://community.penpot.app/c/ask-for-help-using-penpot/6) - [Troubleshooting](https://community.penpot.app/c/technical/8) - [Help us Improve Penpot](https://community.penpot.app/c/help-us-improve-penpot/7) @@ -117,45 +111,36 @@ You will find the following categories: - [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12) - [Design and Code Essentials](https://community.penpot.app/c/design-and-code-essentials/22) - -
-

- Community + Community

-
### Code of Conduct ### Anyone who contributes to Penpot, whether through code, in the community, or at an event, must adhere to the [code of conduct](https://help.penpot.app/contributing-guide/coc/) and foster a positive and safe environment. - ## Contributing ## Any contribution will make a difference to improve Penpot. How can you get involved? Choose your way: -- Create and [share Libraries & Templates](https://penpot.app/libraries-templates.html) that will be helpful for the community -- Invite your [team to join](https://design.penpot.app/#/auth/register) -- Give this repo a star and follow us on Social Media: [Mastodon](https://fosstodon.org/@penpot/), [Youtube](https://www.youtube.com/c/Penpot), [Instagram](https://instagram.com/penpot.app), [Linkedin](https://www.linkedin.com/company/penpotdesign), [Peertube](https://peertube.kaleidos.net/a/penpot_app), [X](https://twitter.com/penpotapp) and [BlueSky](https://bsky.app/profile/penpot.app) +- Create and [share Libraries & Templates](https://penpot.app/libraries-templates.html) that will be helpful for the community. +- Invite your [team to join](https://design.penpot.app/#/auth/register). +- Give this repo a star and follow us on Social Media: [Mastodon](https://fosstodon.org/@penpot/), [Youtube](https://www.youtube.com/c/Penpot), [Instagram](https://instagram.com/penpot.app), [Linkedin](https://www.linkedin.com/company/penpotdesign), [Peertube](https://peertube.kaleidos.net/a/penpot_app), [X](https://twitter.com/penpotapp) and [BlueSky](https://bsky.app/profile/penpot.app). - Participate in the [Community](https://community.penpot.app/) space by asking and answering questions; reacting to others’ articles; opening your own conversations and following along on decisions affecting the project. -- Report bugs with our easy [guide for bugs hunting](https://help.penpot.app/contributing-guide/reporting-bugs/) or [GitHub issues](https://github.com/penpot/penpot/issues) -- Become a [translator](https://help.penpot.app/contributing-guide/translations) -- Give feedback: [Email us](mailto:support@penpot.app) -- **Contribute to Penpot's code:** [Watch this video](https://www.youtube.com/watch?v=TpN0osiY-8k) by Alejandro Alonso, CIO and developer at Penpot, where he gives us a hands-on demo of how to use Penpot’s repository and make changes in both front and back end +- Report bugs with our easy [guide for bugs hunting](https://help.penpot.app/contributing-guide/reporting-bugs/) or [GitHub issues](https://github.com/penpot/penpot/issues). +- Become a [translator](https://help.penpot.app/contributing-guide/translations). +- Give feedback: [Email us](mailto:support@penpot.app). +- **Contribute to Penpot's code:** [Watch this video](https://www.youtube.com/watch?v=TpN0osiY-8k) by Alejandro Alonso, CIO and developer at Penpot, where he gives us a hands-on demo of how to use Penpot’s repository and make changes in both front and back end. To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing guide](https://help.penpot.app/contributing-guide/). -
-

Libraries and templates

-
- ## Resources ## You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project. @@ -170,14 +155,14 @@ You can ask and answer questions, have open-ended conversations, and follow alon 📚 [Dev Diaries](https://penpot.app/dev-diaries.html) - ## License ## -``` +```text This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. Copyright (c) KALEIDOS INC ``` + Penpot is a Kaleidos’ [open source project](https://kaleidos.net/) From 27313e6add0d168874ffca4a652c35d49edaedbd Mon Sep 17 00:00:00 2001 From: Xaviju Date: Tue, 31 Mar 2026 13:04:47 +0200 Subject: [PATCH 088/288] :bug: Fix error on path and review UI (#8844) --- .../management/forms/rename_node_modal.cljs | 51 ++++++++++--------- .../management/forms/rename_node_modal.scss | 12 +++++ 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs index c58d244b6a..cb630d5b55 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs @@ -42,36 +42,37 @@ (not (get-in @form [:touched :name])) (= (get-in @form [:clean-data :name]) (:name node))) - new-path (mf/with-memo [@form node] - (let [new-name (get-in @form [:clean-data :name]) - path (str (:path node)) - new-path (str/replace path (:name node) new-name)] - new-path))] + hint-path (mf/with-memo [@form node] + (let [new-name (get-in @form [:clean-data :name]) + path (str (:path node)) + new-path (str/replace path (:name node) new-name)] + (if (get-in @form [:touched :name]) + new-path + path)))] - [:* + [:> fc/form* {:class (stl/css :form-wrapper) + :form form + :on-submit on-submit} [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)} (tr "workspace.tokens.rename-group")] - [:> fc/form* {:class (stl/css :form-wrapper) - :form form - :on-submit on-submit} - [:> fc/form-input* {:id "rename-node" - :name :name - :label (tr "workspace.tokens.token-name") - :placeholder (tr "workspace.tokens.token-name") - :max-length 255 - :variant "comfortable" - :hint-type "hint" - :hint-message (tr "workspace.tokens.rename-group-name-hint" new-path) - :auto-focus true}] - [:div {:class (stl/css :form-actions)} - [:> button* {:variant "secondary" - :name "cancel" - :on-click on-close} (tr "labels.cancel")] - [:> fc/form-submit* {:variant "primary" - :disabled is-disabled? - :name "rename"} (tr "labels.rename")]]]])) + [:> fc/form-input* {:id "rename-node" + :name :name + :label (tr "workspace.tokens.token-name") + :placeholder (tr "workspace.tokens.token-name") + :max-length 255 + :variant "comfortable" + :hint-type "hint" + :hint-message (tr "workspace.tokens.rename-group-name-hint" hint-path) + :auto-focus true}] + [:div {:class (stl/css :form-actions)} + [:> button* {:variant "secondary" + :name "cancel" + :on-click on-close} (tr "labels.cancel")] + [:> fc/form-submit* {:variant "primary" + :disabled is-disabled? + :name "rename"} (tr "labels.rename")]]])) (mf/defc rename-node-modal {::mf/register modal/components diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss index e4f8e633e2..16206e3ea2 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss @@ -41,8 +41,20 @@ position: relative; } +.form-wrapper { + display: flex; + flex-direction: column; + gap: var(--sp-l); +} + .form-modal-title { @include t.use-typography("headline-medium"); color: var(--color-foreground-primary); } + +.form-actions { + display: flex; + justify-content: flex-end; + gap: var(--sp-m); +} From 5f474f9536fb388cdfca1814b2d85c1e0e18e436 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 31 Mar 2026 13:48:49 +0200 Subject: [PATCH 089/288] :tada: Add typography token row (#8749) * :wrench: Create flag * :sparkles: Add typography type on tokens by input * :tada: Add typography token row * :recycle: Update sub-components to use new style * :tada: Add disabled option on radio-buttons* component * :tada: Add combobox search in a new component * :tada: Divide components * :bug: Fix placeholder --- common/src/app/common/flags.cljc | 2 + common/src/app/common/types/token.cljc | 3 +- .../main/ui/ds/controls/radio_buttons.cljs | 3 +- .../controls/shared/dropdown_navigation.cljs | 89 +++ .../main/ui/ds/controls/shared/option.scss | 5 + .../ds/controls/shared/options_dropdown.cljs | 77 +-- .../ds/controls/shared/options_dropdown.scss | 22 +- .../ui/ds/controls/shared/render_option.cljs | 67 ++ .../ui/ds/controls/shared/render_option.scss | 40 ++ .../shared/searchable_options_dropdown.cljs | 149 +++++ .../shared/searchable_options_dropdown.scss | 47 ++ .../ui/ds/controls/shared/token_option.cljs | 7 +- .../ui/ds/controls/shared/token_option.scss | 5 + .../ui/ds/controls/utilities/token_field.cljs | 2 +- .../workspace/sidebar/options/menus/text.cljs | 590 +++++++++++------- .../workspace/sidebar/options/menus/text.scss | 5 +- .../options/menus/token_typography_row.cljs | 87 +++ .../options/menus/token_typography_row.scss | 101 +++ .../sidebar/options/rows/color_row.cljs | 10 +- .../sidebar/options/shapes/group.cljs | 7 +- .../sidebar/options/shapes/multiple.cljs | 8 +- .../sidebar/options/shapes/text.cljs | 5 +- frontend/translations/de.po | 4 +- frontend/translations/en.po | 14 +- frontend/translations/es.po | 14 +- frontend/translations/fa.po | 2 +- frontend/translations/fr.po | 4 +- frontend/translations/fr_CA.po | 4 +- frontend/translations/he.po | 4 +- frontend/translations/hi.po | 4 +- frontend/translations/it.po | 4 +- frontend/translations/ko.po | 2 +- frontend/translations/lv.po | 4 +- frontend/translations/nl.po | 4 +- frontend/translations/pt_BR.po | 4 +- frontend/translations/ro.po | 4 +- frontend/translations/ru.po | 4 +- frontend/translations/sv.po | 4 +- frontend/translations/tr.po | 4 +- frontend/translations/zh_CN.po | 2 +- 40 files changed, 1059 insertions(+), 358 deletions(-) create mode 100644 frontend/src/app/main/ui/ds/controls/shared/dropdown_navigation.cljs create mode 100644 frontend/src/app/main/ui/ds/controls/shared/render_option.cljs create mode 100644 frontend/src/app/main/ui/ds/controls/shared/render_option.scss create mode 100644 frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.cljs create mode 100644 frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.scss create mode 100644 frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.cljs create mode 100644 frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.scss diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index 51832b5e27..aad100c0d9 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -128,6 +128,8 @@ :token-shadow :token-tokenscript :token-import-from-library + :token-typography-row + ;; Only for developtment. :transit-readable-response :user-feedback diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 2d4b5b0395..c168bfc5a0 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -557,7 +557,8 @@ :font-size [:font-size] :letter-spacing [:letter-spacing] :fill [:color] - :stroke-color [:color]}) + :stroke-color [:color] + :typography [:typography]}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HELPERS for tokens application diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs b/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs index 044fe87bc4..24e4b49453 100644 --- a/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs +++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs @@ -52,6 +52,7 @@ handle-click (mf/use-fn + (mf/deps selected on-change allow-empty) (fn [event] (let [target (dom/get-target event) label (dom/get-parent-with-data target "label") @@ -114,4 +115,4 @@ :name name :disabled (or disabled wrapper-disabled) :value value - :checked checked?}]]))])) \ No newline at end of file + :checked checked?}]]))])) diff --git a/frontend/src/app/main/ui/ds/controls/shared/dropdown_navigation.cljs b/frontend/src/app/main/ui/ds/controls/shared/dropdown_navigation.cljs new file mode 100644 index 0000000000..3f9fc2fa8b --- /dev/null +++ b/frontend/src/app/main/ui/ds/controls/shared/dropdown_navigation.cljs @@ -0,0 +1,89 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC +(ns app.main.ui.ds.controls.shared.dropdown-navigation + (:require + [app.util.dom :as dom] + [app.util.keyboard :as kbd] + [app.util.object :as obj] + [rumext.v2 :as mf])) + +(defn use-dropdown-navigation + "Hook for keyboard navigation in dropdowns. + + Options: + - focusable-ids: vector of focusable ids (already filtered) + - nodes-ref: ref to a JS object mapping id -> DOM node + - on-enter: fn called with focused-id when Enter is pressed + - searchable: when true, nil focused-id means search input is focused + - search-input-ref: ref to the search input DOM node + - on-close: optional fn called when Esc/Tab is pressed" + + [{:keys [focusable-ids nodes-ref on-enter searchable search-input-ref on-close]}] + (let [focused-id* (mf/use-state nil) + focused-id (deref focused-id*) + + focus-input! + (mf/use-fn + (mf/deps search-input-ref) + (fn [] + (reset! focused-id* nil) + (when-let [input (mf/ref-val search-input-ref)] + (dom/focus! input)))) + + on-key-down + (mf/use-fn + (mf/deps focused-id focusable-ids searchable) + (fn [event] + (cond + (kbd/down-arrow? event) + (do + (dom/prevent-default event) + (dom/stop-propagation event) + (if (nil? focused-id) + (reset! focused-id* (first focusable-ids)) + (let [idx (or (first (keep-indexed #(when (= %2 focused-id) %1) focusable-ids)) -1) + next-idx (mod (inc idx) (count focusable-ids)) + wrap-to-input? (and ^boolean searchable + (= next-idx 0) + (= idx (dec (count focusable-ids))))] + (if wrap-to-input? + (focus-input!) + (reset! focused-id* (nth focusable-ids next-idx nil)))))) + + (kbd/up-arrow? event) + (do + (dom/prevent-default event) + (dom/stop-propagation event) + (if (nil? focused-id) + (reset! focused-id* (last focusable-ids)) + (let [idx (or (first (keep-indexed #(when (= %2 focused-id) %1) focusable-ids)) 0) + prev-idx (dec idx) + wrap-to-input? (and ^boolean searchable (= prev-idx -1))] + (if wrap-to-input? + (focus-input!) + (reset! focused-id* (nth focusable-ids (mod prev-idx (count focusable-ids)) nil)))))) + + (kbd/enter? event) + (when focused-id + (dom/prevent-default event) + (dom/stop-propagation event) + (on-enter focused-id)) + + (or (kbd/esc? event) (kbd/tab? event)) + (do + (dom/prevent-default event) + (dom/stop-propagation event) + (reset! focused-id* nil) + (when on-close (on-close))))))] + + (mf/with-effect [focused-id] + (when (some? focused-id) + (when-let [node (obj/get (mf/ref-val nodes-ref) focused-id)] + (dom/scroll-into-view-if-needed! node {:block "nearest" :inline "nearest"})))) + + {:focused-id focused-id + :focused-id* focused-id* + :on-key-down on-key-down})) \ No newline at end of file diff --git a/frontend/src/app/main/ui/ds/controls/shared/option.scss b/frontend/src/app/main/ui/ds/controls/shared/option.scss index ae38b73b01..bc693a14d4 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/option.scss +++ b/frontend/src/app/main/ui/ds/controls/shared/option.scss @@ -63,3 +63,8 @@ --options-fg-color: var(--color-accent-primary); --options-icon-fg-color: var(--color-accent-primary); } + +.option-check { + color: var(--token-options-icon-fg-color); + min-width: var(--sp-l); +} diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs index 60de5830a0..e3f7b778d7 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs @@ -9,10 +9,8 @@ [app.main.style :as stl]) (:require [app.common.data :as d] - [app.common.weak :refer [weak-key]] - [app.main.ui.ds.controls.shared.option :refer [option*]] - [app.main.ui.ds.controls.shared.token-option :refer [token-option*]] - [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] + [app.main.ui.ds.controls.shared.render-option :refer [render-option]] + [app.main.ui.ds.foundations.assets.icon :as i] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -20,6 +18,14 @@ [:and :string [:fn {:error/message "invalid data: invalid icon"} #(contains? i/icon-list %)]]) +(def ^:private + xf:filter-blank-id + (filter #(str/blank? (get % :id)))) + +(def ^:private + xf:filter-non-blank-id + (remove #(str/blank? (get % :id)))) + (def schema:option "A schema for the option data structure expected to receive on props for the `options-dropdown*` component." @@ -33,67 +39,6 @@ [:label {:optional true} :string] [:aria-label {:optional true} :string]]) - - -(def ^:private - xf:filter-blank-id - (filter #(str/blank? (get % :id)))) - -(def ^:private - xf:filter-non-blank-id - (remove #(str/blank? (get % :id)))) - -(defn- render-option - [option ref on-click selected focused] - (let [id (get option :id) - name (get option :name) - type (get option :type)] - - (mf/html - (case type - :group - [:li {:class (stl/css :group-option) - :role "presentation" - :key (weak-key option)} - [:> icon* - {:icon-id i/arrow-down - :size "m" - :class (stl/css :option-check) - :aria-hidden (when name true)}] - (d/name name)] - - :separator - [:hr {:key (weak-key option) :class (stl/css :option-separator)}] - - :empty - [:li {:key (weak-key option) :class (stl/css :option-empty) :role "presentation"} - (get option :label)] - - ;; Token option - :token - [:> token-option* {:selected (= id selected) - :key (weak-key option) - :id id - :name name - :resolved (get option :resolved-value) - :ref ref - :role "option" - :focused (= id focused) - :on-click on-click}] - - ;; Normal option - [:> option* {:selected (= id selected) - :key (weak-key option) - :id id - :label (get option :label) - :aria-label (get option :aria-label) - :icon (get option :icon) - :ref ref - :role "option" - :focused (= id focused) - :dimmed (true? (:dimmed option)) - :on-click on-click}])))) - (def ^:private schema:options-dropdown [:map [:ref {:optional true} fn?] @@ -142,4 +87,4 @@ [:hr {:class (stl/css :option-separator)}]) (for [option options-blank] - (render-option option ref on-click selected focused))])])) + (render-option option ref on-click selected focused))])])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss index 4af9bb4793..0041dc1a9c 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss +++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss @@ -28,6 +28,10 @@ overflow: hidden auto; z-index: var(--z-index-dropdown); box-shadow: 0 0 $sz-12 0 var(--color-shadow-dark); + + &:focus { + outline: none; + } } .left-align { @@ -42,21 +46,3 @@ border: $b-1 solid var(--options-dropdown-border-color); margin-block: var(--sp-xs) var(--sp-xs); } - -.group-option, -.option-empty { - @include use-typography("body-small"); - - display: flex; - align-items: center; - gap: var(--sp-xs); - color: var(--color-foreground-secondary); - padding-inline: var(--sp-s); - block-size: var(--sp-xxxl); -} - -.option-empty { - justify-content: center; - text-align: center; - padding: 0 px2rem(40); -} diff --git a/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs b/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs new file mode 100644 index 0000000000..b3dec0b710 --- /dev/null +++ b/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs @@ -0,0 +1,67 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.ds.controls.shared.render-option + (:require-macros + [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.weak :refer [weak-key]] + [app.main.ui.ds.controls.shared.option :refer [option*]] + [app.main.ui.ds.controls.shared.token-option :refer [token-option*]] + [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] + [rumext.v2 :as mf])) + +(defn render-option + [option ref on-click selected focused] + (let [id (get option :id) + name (get option :name) + type (get option :type)] + + (mf/html + (case type + :group + [:li {:class (stl/css :group-option) + :role "presentation" + :key (weak-key option)} + [:> icon* + {:icon-id i/arrow-down + :size "m" + :class (stl/css :option-check) + :aria-hidden (when name true)}] + (d/name name)] + + :separator + [:hr {:key (weak-key option) :class (stl/css :option-separator)}] + + :empty + [:li {:key (weak-key option) :class (stl/css :option-empty) :role "presentation"} + (get option :label)] + + ;; Token option + :token + [:> token-option* {:selected (= id selected) + :key (weak-key option) + :id id + :name name + :resolved (get option :resolved-value) + :ref ref + :role "option" + :focused (= id focused) + :on-click on-click}] + + ;; Normal option + [:> option* {:selected (= id selected) + :key (weak-key option) + :id id + :label (get option :label) + :aria-label (get option :aria-label) + :icon (get option :icon) + :ref ref + :role "option" + :focused (= id focused) + :dimmed (true? (:dimmed option)) + :on-click on-click}])))) \ No newline at end of file diff --git a/frontend/src/app/main/ui/ds/controls/shared/render_option.scss b/frontend/src/app/main/ui/ds/controls/shared/render_option.scss new file mode 100644 index 0000000000..232efb42a5 --- /dev/null +++ b/frontend/src/app/main/ui/ds/controls/shared/render_option.scss @@ -0,0 +1,40 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/typography.scss" as *; +@use "ds/_utils.scss" as *; + +.left-align { + inset-inline-start: var(--dropdown-offset, 0); +} + +.right-align { + inset-inline-end: var(--dropdown-offset, 0); +} + +.option-separator { + border: $b-1 solid var(--options-dropdown-border-color); + margin-block: var(--sp-xs) var(--sp-xs); +} + +.group-option, +.option-empty { + @include use-typography("body-small"); + + display: flex; + align-items: center; + gap: var(--sp-xs); + color: var(--color-foreground-secondary); + padding-inline: var(--sp-s); + block-size: var(--sp-xxxl); +} + +.option-check { + color: var(--token-options-icon-fg-color); + min-width: var(--sp-l); +} diff --git a/frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.cljs b/frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.cljs new file mode 100644 index 0000000000..6e5d8e7d51 --- /dev/null +++ b/frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.cljs @@ -0,0 +1,149 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.ds.controls.shared.searchable-options-dropdown + (:require-macros + [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.main.ui.ds.controls.input :as ds] + [app.main.ui.ds.controls.shared.dropdown-navigation :refer [use-dropdown-navigation]] + [app.main.ui.ds.controls.shared.render-option :refer [render-option]] + [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.object :as obj] + [app.util.timers :as ts] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(def ^:private schema:icon-list + [:and :string + [:fn {:error/message "invalid data: invalid icon"} #(contains? i/icon-list %)]]) + +(def schema:option + "A schema for the option data structure expected to receive on props + for the `options-dropdown*` component." + [:map + [:id {:optional true} :string] + [:resolved-value {:optional true} + [:or :int :string :float :map]] + [:name {:optional true} :string] + [:value {:optional true} :keyword] + [:icon {:optional true} schema:icon-list] + [:label {:optional true} :string] + [:aria-label {:optional true} :string]]) + +(def ^:private schema:options-dropdown + [:map + [:ref {:optional true} fn?] + [:class {:optional true} :string] + [:wrapper-ref {:optional true} :any] + [:placeholder {:optional true} :string] + [:on-click fn?] + [:options [:vector schema:option]] + [:selected {:optional true} :any] + [:align {:optional true} [:maybe [:enum :left :right]]]]) + +(mf/defc searchable-options-dropdown* + {::mf/schema schema:options-dropdown} + [{:keys [on-click options selected align class placeholder] :rest props}] + (let [align (d/nilv align :left) + + search* (mf/use-state "") + search (deref search*) + search-input-ref (mf/use-ref nil) + + list-ref (mf/use-ref nil) + nodes-ref (mf/use-ref nil) + + filtered-options + (mf/with-memo [options search] + (if (seq search) + (filterv (fn [opt] + (or (not= :token (:type opt)) + (str/includes? (str/lower (:name opt "")) + (str/lower search)))) + options) + options)) + + focusable-ids + (mf/with-memo [filtered-options] + (mapv :id (csu/focusable-options filtered-options))) + + on-search-change + (mf/use-fn + (fn [event] + (reset! search* (dom/get-target-val event)))) + + set-option-ref + (mf/use-fn + (fn [node] + (when node + (let [state (d/nilv (mf/ref-val nodes-ref) #js {}) + id (dom/get-data node "id")] + (mf/set-ref-val! nodes-ref (obj/set! state id node)) + (fn [] + (let [state (d/nilv (mf/ref-val nodes-ref) #js {})] + (mf/set-ref-val! nodes-ref (obj/unset! state id)))))))) + + {:keys [focused-id focused-id* on-key-down]} + (use-dropdown-navigation + {:focusable-ids focusable-ids + :nodes-ref nodes-ref + :on-enter (fn [id] + (when-let [node (obj/get (mf/ref-val nodes-ref) id)] + (.click node))) + :searchable true + :search-input-ref search-input-ref + :on-close nil}) + + on-click-inner + (mf/use-fn + (mf/deps on-click) + (fn [event] + (dom/stop-propagation event) + (on-click event))) + + list-props + (mf/spread-props props + {:class [class (stl/css-case :option-list true + :left-align (= align :left) + :right-align (= align :right))] + :ref list-ref + :tab-index "-1" + :role "listbox" + :on-key-down on-key-down})] + + (mf/with-effect [] + (ts/schedule 0 + #(if (mf/ref-val search-input-ref) + (dom/focus! (mf/ref-val search-input-ref)) + (when-let [list (mf/ref-val list-ref)] + (dom/focus! list))))) + + (mf/with-effect [focused-id] + (when (some? focused-id) + (when-let [list (mf/ref-val list-ref)] + (when-not (dom/active? list) + (dom/focus! list))) + (when-let [node (obj/get (mf/ref-val nodes-ref) focused-id)] + (dom/scroll-into-view-if-needed! node {:block "nearest" :inline "nearest"})))) + + [:> :ul list-props + [:li {:class (stl/css :option-search) + :role "presentation"} + [:> ds/input* {:placeholder (or placeholder (tr "dashboard.search-placeholder")) + :value search + :ref search-input-ref + :variant "comfortable" + :on-change on-search-change + :on-click #(reset! focused-id* nil) + :on-key-down on-key-down}]] + + (for [option filtered-options] + (render-option option set-option-ref on-click-inner selected focused-id))])) diff --git a/frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.scss b/frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.scss new file mode 100644 index 0000000000..cbeae912d8 --- /dev/null +++ b/frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.scss @@ -0,0 +1,47 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/typography.scss" as *; +@use "ds/_utils.scss" as *; + +.option-list { + --options-dropdown-icon-fg-color: var(--color-foreground-secondary); + --options-dropdown-bg-color: var(--color-background-tertiary); + --options-dropdown-outline-color: none; + --options-dropdown-border-color: var(--color-background-quaternary); + + position: absolute; + inset-block-start: $sz-36; + inline-size: var(--dropdown-width, 100%); + transform: translateX(var(--dropdown-translate-distance, 0)); + background-color: var(--options-dropdown-bg-color); + border-radius: $br-8; + border: $b-1 solid var(--options-dropdown-border-color); + padding-block: var(--sp-xs); + margin-block-end: 0; + max-block-size: $sz-400; + overflow: hidden auto; + z-index: var(--z-index-dropdown); + box-shadow: 0 0 $sz-12 0 var(--color-shadow-dark); + + &:focus { + outline: none; + } +} + +.left-align { + inset-inline-start: var(--dropdown-offset, 0); +} + +.right-align { + inset-inline-end: var(--dropdown-offset, 0); +} + +.option-search { + padding: var(--sp-xs); +} diff --git a/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs b/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs index 11667ba8f8..45189a3156 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs @@ -18,7 +18,7 @@ [:map [:id {:optiona true} :string] [:ref some?] - [:resolved {:optional true} [:or :int :string :float]] + [:resolved {:optional true} [:or :int :string :float :map]] [:name {:optional true} :string] [:on-click {:optional true} fn?] [:selected {:optional true} :boolean] @@ -55,10 +55,11 @@ :trigger-ref element-ref :id (dm/str id "-name") :class (stl/css :option-text)} - ;; Add ellipsis + [:span {:aria-labelledby (dm/str id "-name") + :class (stl/css :option-name) :ref element-ref} name]] - (when resolved + (when (and resolved (not (map? resolved))) [:> :span {:class (stl/css :option-pill)} resolved])])) diff --git a/frontend/src/app/main/ui/ds/controls/shared/token_option.scss b/frontend/src/app/main/ui/ds/controls/shared/token_option.scss index b05b6e8e66..14f4c6b3a8 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/token_option.scss +++ b/frontend/src/app/main/ui/ds/controls/shared/token_option.scss @@ -7,6 +7,7 @@ @use "ds/_borders.scss" as *; @use "ds/_sizes.scss" as *; @use "ds/typography.scss" as *; +@use "ds/mixins.scss" as *; .token-option { --token-options-fg-color: var(--color-foreground-primary); @@ -78,3 +79,7 @@ color: var(--token-options-icon-fg-color); min-width: var(--sp-l); } + +.option-name { + @include text-ellipsis; +} diff --git a/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs b/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs index 8b67fa82f8..04c5900961 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs +++ b/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs @@ -97,5 +97,5 @@ :tooltip-placement tooltip-placement :icon i/broken-link :ref token-detach-btn-ref - :aria-label (tr "ds.inputs.token-field.detach-token") + :aria-label (tr "token-actions.detach-token") :on-click detach-token}])]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 75c0344662..ca77bdd320 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -10,26 +10,30 @@ [app.common.data :as d] [app.common.types.text :as txt] [app.common.uuid :as uuid] + [app.config :as cf] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.shortcuts :as sc] [app.main.data.workspace.texts :as dwt] + [app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.wasm-text :as dwwt] [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] [app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.context :as ctx] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]] + [app.main.ui.ds.controls.shared.searchable-options-dropdown :refer [searchable-options-dropdown*]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.hooks :as hooks] - [app.main.ui.icons :as deprecated-icon] - [app.main.ui.workspace.sidebar.options.menus.typography :refer [text-options - typography-entry]] + [app.main.ui.workspace.sidebar.options.menus.token-typography-row :refer [token-typography-row*]] + [app.main.ui.workspace.sidebar.options.menus.typography :refer [text-options typography-entry]] + [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [app.util.object :as obj] [app.util.text.content :as content] [app.util.text.ui :as txu] [app.util.timers :as ts] @@ -37,70 +41,68 @@ [potok.v2.core :as ptk] [rumext.v2 :as mf])) -(mf/defc text-align-options - [{:keys [values on-change on-blur] :as props}] - (let [{:keys [text-align]} values - handle-change +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Sub-components +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(mf/defc text-align-options* + [{:keys [values on-change on-blur]}] + (let [handle-change (mf/use-fn (mf/deps on-change on-blur) (fn [value] (on-change {:text-align value}) (when (some? on-blur) (on-blur))))] - ;; --- Align [:div {:class (stl/css :align-options)} - [:& radio-buttons {:selected text-align - :on-change handle-change - :name "align-text-options"} - [:& radio-button {:value "left" - :id "text-align-left" - :title (tr "workspace.options.text-options.text-align-left") - :icon i/text-align-left}] - [:& radio-button {:value "center" - :id "text-align-center" - :title (tr "workspace.options.text-options.text-align-center") - :icon i/text-align-center}] - [:& radio-button {:value "right" - :id "text-align-right" - :title (tr "workspace.options.text-options.text-align-right") - :icon i/text-align-right}] - [:& radio-button {:value "justify" - :id "text-align-justify" - :title (tr "workspace.options.text-options.text-align-justify") - :icon i/text-justify}]]])) + [:> radio-buttons* {:selected (:text-align values) + :on-change handle-change + :name "align-text-options" + :options [{:value "left" + :id "text-align-left" + :label (tr "workspace.options.text-options.text-align-left") + :icon i/text-align-left} + {:value "center" + :id "text-align-center" + :label (tr "workspace.options.text-options.text-align-center") + :icon i/text-align-center} + {:value "right" + :id "text-align-right" + :label (tr "workspace.options.text-options.text-align-right") + :icon i/text-align-right} + {:value "justify" + :id "text-align-justify" + :label (tr "workspace.options.text-options.text-align-justify") + :icon i/text-justify}]}]])) + +(mf/defc text-direction-options* + [{:keys [values on-change on-blur]}] + (let [direction (:text-direction values) -(mf/defc text-direction-options - [{:keys [values on-change on-blur] :as props}] - (let [direction (:text-direction values) handle-change (mf/use-fn (mf/deps on-change on-blur direction) (fn [value] - (let [dir (if (= value direction) - "none" - value)] - (on-change {:text-direction dir}) - (when (some? on-blur) (on-blur)))))] + (on-change {:text-direction (if (= value direction) "none" value)}) + (when (some? on-blur) (on-blur))))] [:div {:class (stl/css :text-direction-options)} - [:& radio-buttons {:selected direction - :on-change handle-change - :name "text-direction-options"} - [:& radio-button {:value "ltr" - :type "checkbox" - :id "ltr-text-direction" - :title (tr "workspace.options.text-options.direction-ltr") - :icon i/text-ltr}] - [:& radio-button {:value "rtl" - :type "checkbox" - :id "rtl-text-direction" - :title (tr "workspace.options.text-options.direction-rtl") - :icon i/text-rtl}]]])) + [:> radio-buttons* {:selected direction + :on-change handle-change + :name "text-direction-options" + :options [{:value "ltr" + :id "ltr-text-direction" + :label (tr "workspace.options.text-options.direction-ltr") + :icon i/text-ltr} + {:value "rtl" + :id "rtl-text-direction" + :label (tr "workspace.options.text-options.direction-rtl") + :icon i/text-rtl}]}]])) + +(mf/defc vertical-align* + [{:keys [values on-change on-blur]}] + (let [vertical-align (or (:vertical-align values) "top") -(mf/defc vertical-align - [{:keys [values on-change on-blur] :as props}] - (let [{:keys [vertical-align]} values - vertical-align (or vertical-align "top") handle-change (mf/use-fn (mf/deps on-change on-blur) @@ -109,123 +111,223 @@ (when (some? on-blur) (on-blur))))] [:div {:class (stl/css :vertical-align-options)} - [:& radio-buttons {:selected vertical-align - :on-change handle-change - :name "vertical-align-text-options"} - [:& radio-button {:value "top" - :id "vertical-text-align-top" - :title (tr "workspace.options.text-options.align-top") - :icon i/text-top}] - [:& radio-button {:value "center" - :id "vertical-text-align-center" - :title (tr "workspace.options.text-options.align-middle") - :icon i/text-middle}] - [:& radio-button {:value "bottom" - :id "vertical-text-align-bottom" - :title (tr "workspace.options.text-options.align-bottom") - :icon i/text-bottom}]]])) - -(mf/defc grow-options - [{:keys [ids values on-blur] :as props}] - (let [grow-type (:grow-type values) + [:> radio-buttons* {:selected vertical-align + :on-change handle-change + :name "vertical-align-text-options" + :options [{:value "top" + :id "vertical-text-align-top" + :label (tr "workspace.options.text-options.align-top") + :icon i/text-top} + {:value "center" + :id "vertical-text-align-center" + :label (tr "workspace.options.text-options.align-middle") + :icon i/text-middle} + {:value "bottom" + :id "vertical-text-align-bottom" + :label (tr "workspace.options.text-options.align-bottom") + :icon i/text-bottom}]}]])) +(mf/defc grow-options* + [{:keys [ids values on-blur on-change]}] + (let [grow-type (:grow-type values) editor-instance (mf/deref refs/workspace-editor) - handle-change-grow - (mf/use-fn - (mf/deps ids on-blur editor-instance) - (fn [value] - (on-blur) - (let [uid (js/Symbol) - grow-type (keyword value)] - (st/emit! (dwu/start-undo-transaction uid)) - (when (features/active-feature? @st/state "text-editor/v2") - (let [content (when editor-instance - (content/dom->cljs (dwt/get-editor-root editor-instance)))] - (when (some? content) - (st/emit! (dwt/v2-update-text-shape-content (first ids) content :finalize? true))))) - (st/emit! (dwsh/update-shapes ids #(assoc % :grow-type grow-type))) - - (when (features/active-feature? @st/state "render-wasm/v1") - (st/emit! (dwwt/resize-wasm-text-all ids))) - ;; We asynchronously commit so every sychronous event is resolved first and inside the transaction - (ts/schedule #(st/emit! (dwu/commit-undo-transaction uid)))) - (when (some? on-blur) (on-blur))))] - - [:div {:class (stl/css :grow-options)} - [:& radio-buttons {:selected (d/name grow-type) - :on-change handle-change-grow - :name "grow-text-options"} - [:& radio-button {:value "fixed" - :id "text-fixed-grow" - :title (tr "workspace.options.text-options.grow-fixed") - :icon i/text-fixed}] - [:& radio-button {:value "auto-width" - :id "text-auto-width-grow" - :title (tr "workspace.options.text-options.grow-auto-width") - :icon i/text-auto-width}] - [:& radio-button {:value "auto-height" - :id "text-auto-height-grow" - :title (tr "workspace.options.text-options.grow-auto-height") - :icon i/text-auto-height}]]])) - -(mf/defc text-decoration-options - [{:keys [values on-change on-blur] :as props}] - (let [text-decoration (or (:text-decoration values) "none") handle-change (mf/use-fn - (mf/deps on-change on-blur text-decoration) + (mf/deps ids on-blur on-change editor-instance) (fn [value] - (let [decoration (if (= value text-decoration) - "none" - value)] - (on-change {:text-decoration decoration}) - (when (some? on-blur) (on-blur)))))] + (on-change {:grow-type (keyword value)}) + (when (some? on-blur) + (on-blur))))] + + [:div {:class (stl/css :grow-options)} + [:> radio-buttons* {:selected (d/name grow-type) + :on-change handle-change + :name "grow-text-options" + :options [{:value "fixed" + :id "text-fixed-grow" + :label (tr "workspace.options.text-options.grow-fixed") + :icon i/text-fixed} + {:value "auto-width" + :id "text-auto-width-grow" + :label (tr "workspace.options.text-options.grow-auto-width") + :icon i/text-auto-width} + {:value "auto-height" + :id "text-auto-height-grow" + :label (tr "workspace.options.text-options.grow-auto-height") + :icon i/text-auto-height}]}]])) + +(mf/defc text-decoration-options* + [{:keys [values on-change on-blur token-applied]}] + (let [token-row (contains? cf/flags :token-typography-row) + text-decoration (some-> (:text-decoration values) d/name) + handle-change + (mf/use-fn + (mf/deps on-change on-blur) + (fn [value] + (on-change {:text-decoration value}) + (when (some? on-blur) + (on-blur))))] + [:div {:class (stl/css :text-decoration-options)} - [:& radio-buttons {:selected text-decoration - :on-change handle-change - :name "text-decoration-options"} - [:& radio-button {:value "underline" - :type "checkbox" - :id "underline-text-decoration" - :title (tr "workspace.options.text-options.underline" (sc/get-tooltip :underline)) - :icon i/text-underlined}] - [:& radio-button {:value "line-through" - :type "checkbox" - :id "line-through-text-decoration" - :title (tr "workspace.options.text-options.strikethrough" (sc/get-tooltip :line-through)) - :icon i/text-stroked}]]])) + [:> radio-buttons* {:selected (if (= text-decoration "none") + nil + text-decoration) + :on-change handle-change + :name "text-decoration-options" + :disabled (and token-row (some? token-applied)) + :allow-empty true + :options [{:value "underline" + :id "underline-text-decoration" + :disabled (and token-row (some? token-applied)) + :label (tr "workspace.options.text-options.underline" (sc/get-tooltip :underline)) + :icon i/text-underlined} + {:value "line-through" + :id "line-through-text-decoration" + :disabled (and token-row (some? token-applied)) + :label (tr "workspace.options.text-options.strikethrough" (sc/get-tooltip :line-through)) + :icon i/text-stroked}]}]])) -(mf/defc text-menu - {::mf/wrap [mf/memo]} - [{:keys [ids type values] :as props}] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - (let [file-id (mf/use-ctx ctx/current-file-id) - typographies (mf/deref refs/workspace-file-typography) - libraries (mf/deref refs/files) +(defn- get-option-by-name [options name] + (let [options (if (delay? options) (deref options) options)] + (d/seek #(= name (get % :name)) options))) + +(defn- resolve-delay [tokens] + (if (delay? tokens) @tokens tokens)) + +(defn- find-token-by-id [tokens id] + (->> (:typography tokens) + (d/seek #(= (:id %) (uuid/uuid id))))) + +(defn- check-props [n-props o-props] + (and (identical? (unchecked-get n-props "ids") + (unchecked-get o-props "ids")) + (identical? (unchecked-get n-props "appliedTokens") + (unchecked-get o-props "appliedTokens")) + (identical? (unchecked-get n-props "values") + (unchecked-get o-props "values")))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Main component +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(mf/defc text-menu* + {::mf/wrap [#(mf/memo' % check-props)]} + [{:keys [ids type values applied-tokens]}] + + (let [file-id (mf/use-ctx ctx/current-file-id) + typographies (mf/deref refs/workspace-file-typography) + editor-instance (mf/deref refs/workspace-editor) + libraries (mf/deref refs/files) + token-row (contains? cf/flags :token-typography-row) + + ;; --- UI state + menu-state* (mf/use-state {:main-menu true + :more-options false}) + menu-state (deref menu-state*) + main-menu-open? (:main-menu menu-state) + more-options-open? (:more-options menu-state) + + token-dropdown-open* (mf/use-state false) + token-dropdown-open? (deref token-dropdown-open*) + + ;; --- Applied token + applied-token-name (:typography applied-tokens) + current-token-name* (mf/use-state applied-token-name) + current-token-name (deref current-token-name*) + + ;; --- Available tokens + active-tokens (mf/use-ctx ctx/active-tokens-by-type) + typography-tokens (mf/with-memo [active-tokens] (csu/filter-tokens-for-input active-tokens :typography)) + + ;; --- Dropdown + listbox-id (mf/use-id) + nodes-ref (mf/use-ref nil) + dropdown-ref (mf/use-ref nil) + + dropdown-options + (mf/with-memo [typography-tokens] + (csu/get-token-dropdown-options typography-tokens nil)) + + selected-token-id* + (mf/use-state #(when current-token-name + (:id (get-option-by-name dropdown-options current-token-name)))) + selected-token-id (deref selected-token-id*) + + ;; --- Typography + typography-id (:typography-ref-id values) + typography-file-id (:typography-ref-file values) + + typography + (mf/with-memo [typography-id typography-file-id file-id libraries] + (cond + (and typography-id + (not= typography-id :multiple) + (not= typography-file-id file-id)) + (-> (get-in libraries [typography-file-id :data :typographies typography-id]) + (assoc :file-id typography-file-id)) + + (and typography-id + (not= typography-id :multiple) + (= typography-file-id file-id)) + (get typographies typography-id))) + + ;; --- Helpers + multiple? (->> values vals (d/seek #(= % :multiple))) + + apply-token! + (mf/use-fn + (mf/deps ids typography-tokens) + (fn [id] + (let [token (find-token-by-id (resolve-delay typography-tokens) id)] + (reset! selected-token-id* id) + (reset! token-dropdown-open* false) + (st/emit! + (dwta/apply-token {:shape-ids ids + :attributes #{:typography} + :token token + :on-update-shape dwta/update-typography}))))) label (case type :multiple (tr "workspace.options.text-options.title-selection") :group (tr "workspace.options.text-options.title-group") (tr "workspace.options.text-options.title")) + set-option-ref + (mf/use-fn + (fn [node] + (let [state (d/nilv (mf/ref-val nodes-ref) #js {}) + id (dom/get-data node "id")] + (mf/set-ref-val! nodes-ref (obj/set! state id node)) + (fn [] + (let [state (d/nilv (mf/ref-val nodes-ref) #js {})] + (mf/set-ref-val! nodes-ref (obj/unset! state id))))))) - state* (mf/use-state {:main-menu true - :more-options false}) - state (deref state*) - main-menu-open? (:main-menu state) - more-options-open? (:more-options state) - + ;; --- Toggles toggle-main-menu (mf/use-fn (mf/deps main-menu-open?) - #(swap! state* assoc-in [:main-menu] (not main-menu-open?))) + #(swap! menu-state* update :main-menu not)) toggle-more-options (mf/use-fn (mf/deps more-options-open?) - #(swap! state* assoc-in [:more-options] (not more-options-open?))) + #(swap! menu-state* update :more-options not)) - typography-id (:typography-ref-id values) - typography-file-id (:typography-ref-file values) + toggle-token-dropdown + (mf/use-fn + #(swap! token-dropdown-open* not)) + + ;; --- Event handlers + on-option-click + (mf/use-fn + (mf/deps apply-token!) + (fn [event] + (dom/stop-propagation event) + (let [id (dom/get-data (dom/get-current-target event) "id")] + (apply-token! id)))) emit-update! (mf/use-fn @@ -241,42 +343,40 @@ (fn [attrs] (emit-update! ids attrs))) - typography - (mf/with-memo [values file-id libraries] - (cond - (and typography-id - (not= typography-id :multiple) - (not= typography-file-id file-id)) - (-> libraries - (get-in [typography-file-id :data :typographies typography-id]) - (assoc :file-id typography-file-id)) - - (and typography-id - (not= typography-id :multiple) - (= typography-file-id file-id)) - (get typographies typography-id))) - on-convert-to-typography - (fn [_] - (let [set-values (-> (d/without-nils values) - (select-keys - (d/concat-vec txt/text-font-attrs - txt/text-spacing-attrs - txt/text-transform-attrs))) - typography (merge txt/default-typography set-values) - typography (dwt/generate-typography-name typography) - id (uuid/next)] - (st/emit! (dwl/add-typography (assoc typography :id id) false)) - (emit-update! ids - {:typography-ref-id id - :typography-ref-file file-id}))) + (mf/use-fn + (mf/deps values ids file-id emit-update!) + (fn [_] + (let [set-values (-> (d/without-nils values) + (select-keys (d/concat-vec txt/text-font-attrs + txt/text-spacing-attrs + txt/text-transform-attrs))) + typography (-> (merge txt/default-typography set-values) + (dwt/generate-typography-name)) + id (uuid/next)] + (st/emit! (dwl/add-typography (assoc typography :id id) false)) + (emit-update! ids {:typography-ref-id id :typography-ref-file file-id})))) + + on-grow-type-change + (mf/use-fn + (mf/deps ids editor-instance) + (fn [{:keys [grow-type]}] + (let [uid (js/Symbol)] + (st/emit! (dwu/start-undo-transaction uid)) + (when (features/active-feature? @st/state "text-editor/v2") + (let [content (when editor-instance + (content/dom->cljs (dwt/get-editor-root editor-instance)))] + (when (some? content) + (st/emit! (dwt/v2-update-text-shape-content (first ids) content :finalize? true))))) + (st/emit! (dwsh/update-shapes ids #(assoc % :grow-type grow-type))) + (when (features/active-feature? @st/state "render-wasm/v1") + (st/emit! (dwwt/resize-wasm-text-all ids))) + (ts/schedule #(st/emit! (dwu/commit-undo-transaction uid)))))) handle-detach-typography (mf/use-fn (mf/deps on-change) - (fn [] - (on-change {:typography-ref-file nil - :typography-ref-id nil}))) + #(on-change {:typography-ref-file nil :typography-ref-id nil})) handle-change-typography (mf/use-fn @@ -284,74 +384,124 @@ (fn [changes] (st/emit! (dwl/update-typography (merge typography changes) file-id)))) + detach-token + (mf/use-fn + (fn [token-name] + (st/emit! (dwta/unapply-token {:token-name token-name + :attributes #{:typography} + :shape-ids ids})))) + expand-stream (mf/with-memo [] - (->> st/stream - (rx/filter (ptk/type? :expand-text-more-options)))) + (->> st/stream (rx/filter (ptk/type? :expand-text-more-options)))) - multiple? (->> values vals (d/seek #(= % :multiple))) + on-text-blur + (mf/use-fn + (fn [] + (ts/schedule + 100 + (fn [] + (when (not= "INPUT" (-> (dom/get-active) dom/get-tag-name)) + (dom/focus! (txu/get-text-editor-content))))))) + + common-props + (mf/spread-props {} {:values values + :on-change on-change + :on-blur on-text-blur})] - opts #js {:ids ids - :values values - :on-change on-change - :show-recent true - :on-blur - (fn [] - (ts/schedule - 100 - (fn [] - (when (not= "INPUT" (-> (dom/get-active) (dom/get-tag-name))) - (let [node (txu/get-text-editor-content)] - (dom/focus! node))))))}] (hooks/use-stream expand-stream - #(swap! state* assoc-in [:more-options] true)) + #(swap! menu-state* assoc :more-options true)) - [:div {:class (stl/css :element-set)} + (mf/with-effect [applied-token-name] + (reset! current-token-name* applied-token-name)) + + (mf/with-effect [applied-token-name dropdown-options] + (reset! selected-token-id* + (when applied-token-name + (:id (get-option-by-name dropdown-options applied-token-name))))) + + (mf/with-effect [token-dropdown-open?] + (when token-dropdown-open? + (ts/schedule 0 #(some-> (mf/ref-val dropdown-ref) dom/focus!)))) + + [:section {:class (stl/css :element-set) + :aria-label (tr "workspace.options.text-options.text-section")} [:div {:class (stl/css :element-title)} [:> title-bar* {:collapsable true :collapsed (not main-menu-open?) :on-collapsed toggle-main-menu :title label :class (stl/css :title-spacing-text)} - (when (and (not typography) (not multiple?)) - [:> icon-button* {:variant "ghost" - :aria-label (tr "labels.options") - :on-click on-convert-to-typography - :icon i/add}])]] + [:* + (when (and token-row (some? typography-tokens) (not typography)) + [:> icon-button* {:variant "ghost" + :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown") + :on-click toggle-token-dropdown + :tooltip-placement "top-left" + :icon i/tokens}]) + (when (and (not typography) (not multiple?) (not applied-token-name)) + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.options.convert-to-typography") + :on-click on-convert-to-typography + :tooltip-placement "top-left" + :icon i/add}])]]] (when main-menu-open? [:div {:class (stl/css :element-content)} (cond + (and token-row current-token-name) + [:> token-typography-row* {:token-name current-token-name + :detach-token detach-token + :active-tokens (resolve-delay typography-tokens)}] + typography - [:& typography-entry {:file-id typography-file-id + [:& typography-entry {:file-id typography-file-id :typography typography - :local? (= typography-file-id file-id) - :on-detach handle-detach-typography - :on-change handle-change-typography}] + :local? (= typography-file-id file-id) + :on-detach handle-detach-typography + :on-change handle-change-typography}] (= typography-id :multiple) [:div {:class (stl/css :multiple-typography)} [:span {:class (stl/css :multiple-text)} (tr "workspace.libraries.text.multiple-typography")] - [:div {:class (stl/css :multiple-typography-button) - :on-click handle-detach-typography - :title (tr "workspace.libraries.text.multiple-typography-tooltip")} - deprecated-icon/detach]] + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.libraries.text.multiple-typography-tooltip") + :on-click handle-detach-typography + :icon i/detach}]] :else - [:> text-options opts]) + [:> text-options #js {:ids ids + :values values + :on-change on-change + :show-recent true + :on-blur + (fn [] + (ts/schedule + 100 + (fn [] + (when (not= "INPUT" (-> (dom/get-active) dom/get-tag-name)) + (dom/focus! (txu/get-text-editor-content))))))}]) [:div {:class (stl/css :text-align-options)} - [:> text-align-options opts] - [:> grow-options opts] - [:> icon-button* {:variant "ghost" - :aria-label (tr "labels.options") + [:> text-align-options* common-props] + [:> grow-options* (mf/spread-props common-props {:on-change on-grow-type-change})] + [:> icon-button* {:variant "ghost" + :aria-label (tr "labels.options") :data-testid "text-align-options-button" - :on-click toggle-more-options - :icon i/menu}]] + :on-click toggle-more-options + :icon i/menu}]] (when more-options-open? - [:div {:class (stl/css :text-decoration-options)} - [:> vertical-align opts] - [:> text-decoration-options opts] - [:> text-direction-options opts]])])])) + [:div {:class (stl/css :text-decoration-options)} + [:> vertical-align* common-props] + [:> text-decoration-options* (mf/spread-props common-props {:token-applied current-token-name})] + [:> text-direction-options* common-props]])]) + + (when (and token-row token-dropdown-open?) + [:> searchable-options-dropdown* {:on-click on-option-click + :id listbox-id + :options (resolve-delay dropdown-options) + :selected selected-token-id + :align "right" + :ref set-option-ref}])])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss index 44e721d5b8..8805b83543 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss @@ -9,6 +9,8 @@ .element-set { @include sidebar.option-grid-structure; + + position: relative; } .element-title { @@ -16,10 +18,9 @@ } .element-content { - grid-column: span 8; - @include deprecated.flexColumn; + grid-column: span 8; margin-top: deprecated.$s-4; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.cljs new file mode 100644 index 0000000000..9d9944a1d2 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.cljs @@ -0,0 +1,87 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.sidebar.options.menus.token-typography-row + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] + [app.main.ui.ds.tooltip :refer [tooltip*]] + [app.util.i18n :as i18n :refer [tr]] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(mf/defc resolved-value-tooltip* + {::mf/private true} + [{:keys [token-name resolved-value]}] + [:* + [:span (dm/str (tr "workspace.tokens.token-name") ": ")] + [:span {:class (stl/css :token-name-tooltip)} token-name] + [:div + [:span (tr "inspect.tabs.styles.token-resolved-value")] + [:ul + (for [[k v] resolved-value] + [:li {:key (d/name k)} + [:span {:class (stl/css :resolved-key)} (str "- " (d/name k) ": ")] + [:span {:class (stl/css :resolved-value)} + (if (sequential? v) + (str/join ", " (map #(dm/str "\"" % "\"") v)) + (dm/str v))]])]]]) + +(mf/defc token-typography-row* + [{:keys [token-name active-tokens detach-token] :rest props}] + (let [element-ref (mf/use-ref nil) + id (mf/use-id) + + token (->> (:typography active-tokens) + (d/seek #(= (:name %) token-name))) + + has-errors (some? (:errors token)) + display-name (or (:name token) token-name) + + resolved-value (:resolved-value token) + not-active (or (nil? token) + (empty? (:typography active-tokens))) + on-detach + (mf/use-fn + (mf/deps display-name) + (fn [] + (detach-token display-name))) + + tooltip-content (cond + not-active + (tr "not-active-token.no-name") + has-errors + (tr "options.deleted-token") + :else + (mf/html [:> resolved-value-tooltip* {:token-name token-name + :resolved-value resolved-value}]))] + + [:div {:class (stl/css-case :token-typography-row true + :token-typography-row-with-errors has-errors + :token-typography-row-not-active not-active)} + (when (or has-errors not-active) + [:div {:class (stl/css :error-dot)}]) + [:> icon* {:icon-id i/text-typography + :class (stl/css :icon)}] + [:> tooltip* {:content tooltip-content + :trigger-ref element-ref + :class (stl/css :token-tooltip) + :id id} + + [:span {:aria-labelledby (dm/str id) + :class (stl/css :token-name) + :ref element-ref} + display-name]] + + [:> icon-button* {:variant "action" + :aria-label (tr "token-actions.detach-token") + :tooltip-class (stl/css :detach-button) + :tooltip-placement "top-left" + :on-click on-detach + :icon i/detach}]])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.scss new file mode 100644 index 0000000000..ac89984a55 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.scss @@ -0,0 +1,101 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "ds/typography.scss" as t; +@use "ds/_sizes.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/mixins.scss" as *; +@use "ds/_utils.scss" as *; + +.token-typography-row { + --token-typography-row-background-color: var(--color-background-tertiary); + --token-typography-row-foreground-color: var(--color-token-foreground); + --token-typography-row-border-color: var(--color-token-border); + + display: flex; + align-items: center; + position: relative; + gap: var(--sp-xs); + block-size: $sz-32; + min-inline-size: 0; + inline-size: 100%; + padding: var(--sp-s); + margin-inline-end: 0; + background: var(--token-typography-row-background-color); + border: $b-1 solid var(--token-typography-row-border-color); + border-radius: $br-8; + + &:hover { + --token-typography-row-background-color: var(--color-token-background); + --token-typography-row-foreground-color: var(--color-foreground-primary); + --token-typography-row-border-color: var(--color-token-accent); + } +} + +.token-typography-row-with-errors, +.token-typography-row-not-active { + --token-typography-row-background-color: var(--color-background-primary); + --token-typography-row-foreground-color: var(--color-foreground-secondary); + --token-typography-row-border-color: var(--color-token-border); + + &:hover { + --token-typography-row-background-color: var(--color-background-primary); + --token-typography-row-foreground-color: var(--color-foreground-secondary); + --token-typography-row-border-color: var(--color-token-background); + } +} + +.icon { + display: block; + min-inline-size: $sz-16; + color: var(--token-typography-row-foreground-color); +} + +.token-name { + @include t.use-typography("body-small"); + @include text-ellipsis; + + color: var(--token-typography-row-foreground-color); + block-size: $sz-32; + flex: 1; + line-height: $sz-32; +} + +.token-tooltip { + min-inline-size: 0; + inline-size: inherit; +} + +.token-name-tooltip { + color: var(--color-foreground-primary); +} + +.detach-button { + flex-shrink: 0; + inline-size: 0; + max-inline-size: 0; + overflow: hidden; + opacity: 0; + pointer-events: none; +} + +.token-typography-row:hover .detach-button { + inline-size: auto; + opacity: 1; + pointer-events: auto; + max-inline-size: $sz-32; +} + +.error-dot { + inline-size: px2rem(4); + block-size: px2rem(4); + border-radius: 50%; + background-color: var(--color-foreground-error); + margin-inline-start: var(--sp-xs); + position: absolute; + inset-inline-end: px2rem(1); + inset-block-start: px2rem(5); +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 281ed7e888..31e98bf832 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -97,16 +97,16 @@ token-name-ref (mf/use-ref nil) swatch-tooltip-content (cond not-active - (tr "ds.inputs.token-field.no-active-color.token-option") + (tr "not-active-token.no-name") has-errors - (tr "color-row.token-color-row.deleted-token") + (tr "options.deleted-token") :else (tr "workspace.tokens.resolved-value" resolved)) name-tooltip-content (cond not-active - (tr "ds.inputs.token-field.no-active-color.token-option") + (tr "not-active-token.no-name") has-errors - (tr "color-row.token-color-row.deleted-token") + (tr "options.deleted-token") :else #(mf/html [:div @@ -137,7 +137,7 @@ [:div {:class (stl/css :token-actions)} [:> icon-button* {:variant "action" - :aria-label (tr "ds.inputs.token-field.detach-token") + :aria-label (tr "token-actions.detach-token") :on-click on-detach-token :icon i/detach}] [:> icon-button* diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs index eaab775a8e..f99364cbc9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs @@ -102,7 +102,7 @@ [stroke-ids stroke-values stroke-tokens] (get-attrs shapes objects :stroke) - [text-ids text-values] + [text-ids text-values text-tokens] (get-attrs shapes objects :text) [layout-item-ids layout-item-values] @@ -171,7 +171,10 @@ [:& blur-menu {:type type :ids blur-ids :values blur-values}]) (when-not (empty? text-ids) - [:& ot/text-menu {:type type :ids text-ids :values text-values}]) + [:> ot/text-menu* {:type type + :ids text-ids + :values text-values + :applied-tokens text-tokens}]) (when-not (empty? svg-values) [:& svg-attrs-menu {:ids ids :values svg-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index f3c6ee2b1e..caff7d7454 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -385,7 +385,7 @@ [layer-ids layer-values layer-tokens] (get-attrs shapes objects :layer) - [text-ids text-values] + [text-ids text-values text-tokens] (get-attrs shapes objects :text) [constraint-ids constraint-values] @@ -478,7 +478,11 @@ [:& constraints-menu {:ids constraint-ids :values constraint-values}]) (when-not (empty? text-ids) - [:& ot/text-menu {:type type :ids text-ids :values text-values}]) + [:> ot/text-menu* + {:type type + :ids text-ids + :values text-values + :applied-tokens text-tokens}]) (when-not (empty? fill-ids) [:> fill/fill-menu* {:type type diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs index 21cd88b881..acf7c6c61f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs @@ -25,7 +25,7 @@ [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu*]] [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu*]] [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] - [app.main.ui.workspace.sidebar.options.menus.text :refer [text-menu]] + [app.main.ui.workspace.sidebar.options.menus.text :refer [text-menu*]] [rumext.v2 :as mf])) (mf/defc options* @@ -162,9 +162,10 @@ {:ids ids :values (select-keys shape constraint-attrs)}]) - [:& text-menu + [:> text-menu* {:ids ids :type type + :applied-tokens applied-tokens :values text-values}] [:> fill/fill-menu* diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 1a35b21e8e..b1bead45b4 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -208,7 +208,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "...Branding, Illustrationen, Marketingmaterialien, usw." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "Dieses Token existiert nicht oder wurde gelöscht." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1357,7 +1357,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "Token-Liste öffnen" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "Token trennen" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:43, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:99, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:106 diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 9108367da9..10851f8f6b 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -197,7 +197,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "...branding, illustrations, marketing pieces, etc." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "This token does not exists or has been deleted." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1288,7 +1288,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "Open token list" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "Detach token" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:43, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:99, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:106 @@ -1296,7 +1296,7 @@ msgid "ds.inputs.token-field.no-active-token-option" msgstr "{%s} is not in any active set or has an invalid value." #: src/app/main/ui/ds/controls/utilities/token_field.cljs -msgid "ds.inputs.token-field.no-active-color.token-option" +msgid "not-active-token.no-name" msgstr "This token is not in any active set or has an invalid value." #: src/app/main/data/auth.cljs:339 @@ -7366,6 +7366,14 @@ msgstr "Title case" msgid "workspace.options.text-options.underline" msgstr "Underline (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.convert-to-typography" +msgstr "Create typography style" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-section" +msgstr "Text section" + #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs #, unused msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index c4bd7006d0..74d8ef2b66 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -204,7 +204,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "diseño de marca, ilustraciones, piezas de marketing..." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "Este token no existe o ha sido borrado." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1278,7 +1278,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "Abrir lista de tokens" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "Desvincular token" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:43, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:99, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:106 @@ -1286,7 +1286,7 @@ msgid "ds.inputs.token-field.no-active-token-option" msgstr "{%s} no está disponible en ningún set o tiene un valor inválido." #: src/app/main/ui/ds/controls/utilities/token_field.cljs -msgid "ds.inputs.token-field.no-active-color.token-option" +msgid "not-active-token.no-name" msgstr "Este token no está disponible en ningún set o tiene un valor inválido." #: src/app/main/data/auth.cljs:339 @@ -7281,6 +7281,14 @@ msgstr "Título" msgid "workspace.options.text-options.underline" msgstr "Subrayado (%s)" +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.convert-to-typography" +msgstr "Crear estilo de tipografía" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.text-section" +msgstr "Sección de textos" + #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs #, unused msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/fa.po b/frontend/translations/fa.po index 55e50951db..14e02fc6eb 100644 --- a/frontend/translations/fa.po +++ b/frontend/translations/fa.po @@ -199,7 +199,7 @@ msgid "auth.work-email" msgstr "ایمیلِ کار" #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "این توکن وجود ندارد یا حذف شده است." #: src/app/main/ui/workspace/libraries.cljs:323 diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 29010e9d1c..ef26ef224c 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -206,7 +206,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "...image de marque, illustrations, supports marketing, etc." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "Ce token n'existe pas ou a été supprimé." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1364,7 +1364,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "Ouvrir la liste des tokens" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "Détacher le token" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:43, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:99, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:106 diff --git a/frontend/translations/fr_CA.po b/frontend/translations/fr_CA.po index c592b77f2b..c71fb8dff0 100644 --- a/frontend/translations/fr_CA.po +++ b/frontend/translations/fr_CA.po @@ -208,7 +208,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "images de marque, illustrations, matériel de marketing..." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "Ce token n'existe pas ou a été supprimé." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1355,7 +1355,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "Ouvrir la liste de tokens" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "Détacher du token" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:43, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:99, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:106 diff --git a/frontend/translations/he.po b/frontend/translations/he.po index a288628e94..395bce7724 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -194,7 +194,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "…מיתוג, איורים, חומרים שיווקיים ועוד." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "האסימון הזה לא קיים או שנמחק." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1182,7 +1182,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "פתיחת רשימת אסימונים" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "ניתוק אסימון" #: src/app/main/data/auth.cljs:339 diff --git a/frontend/translations/hi.po b/frontend/translations/hi.po index 77507cfd03..54c563fd5d 100644 --- a/frontend/translations/hi.po +++ b/frontend/translations/hi.po @@ -200,7 +200,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "...ब्रांडिंग, चित्रण, मार्केटिंग सामग्री आदि।" #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "यह token मौजूद नहीं है या हटा दिया गया है।" #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1253,7 +1253,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "token सूची खोलें" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "token अलग करें" #: src/app/main/data/auth.cljs:339 diff --git a/frontend/translations/it.po b/frontend/translations/it.po index cac516f2bb..a6d88a564c 100644 --- a/frontend/translations/it.po +++ b/frontend/translations/it.po @@ -203,7 +203,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "…branding, illustrazione, materiali di marketing, etc." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "Questo token non esiste o è stato eliminato." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1350,7 +1350,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "Apri elenco token" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "Scollega token" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:43, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:99, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:106 diff --git a/frontend/translations/ko.po b/frontend/translations/ko.po index f590f97179..f27c397ff8 100644 --- a/frontend/translations/ko.po +++ b/frontend/translations/ko.po @@ -191,7 +191,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "...브랜딩, 일러스트레이션, 마케팅 자료 등." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "이 토큰은 존재하지 않거나 삭제되었습니다." #: src/app/main/ui/comments.cljs:530 diff --git a/frontend/translations/lv.po b/frontend/translations/lv.po index 48ae267a5a..72fef0e36f 100644 --- a/frontend/translations/lv.po +++ b/frontend/translations/lv.po @@ -205,7 +205,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "... zīmolrades, ilustrācijām, mārketinga materiāliem utt." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "Šī tekstvienība nepastāv vai ir izdzēsta." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1211,7 +1211,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "Atvērt tekstvienību sarakstu" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "Atdalīt tekstvienību" #: src/app/main/data/auth.cljs:339 diff --git a/frontend/translations/nl.po b/frontend/translations/nl.po index ea4c4ba523..819e3da13d 100644 --- a/frontend/translations/nl.po +++ b/frontend/translations/nl.po @@ -201,7 +201,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "…branding, illustraties, marketingstukken, etc." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "Dit token bestaat niet of is verwijderd." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1353,7 +1353,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "Lijst met tokens openen" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "Token loskoppelen" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:43, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:99, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:106 diff --git a/frontend/translations/pt_BR.po b/frontend/translations/pt_BR.po index 82a5480389..eb14ed44e8 100644 --- a/frontend/translations/pt_BR.po +++ b/frontend/translations/pt_BR.po @@ -202,7 +202,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "... marca, ilustrações, materiais de marketing, etc." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "Este token não existe ou foi excluído." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1182,7 +1182,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "Abrir lista de tokens" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "Desvincular token" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:43, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:99, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:106 diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index 9ec2ddf587..11d1473623 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -208,7 +208,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "... mărci, ilustrații, piese de marketing, etc." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "Acest token nu există sau a fost șters." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1199,7 +1199,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "Deschide lista de token-uri" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "Detașează tokenul" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:43, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:99, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:106 diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index e4d471bde5..95874fe4dc 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -204,7 +204,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "...брендинг, иллюстрации, маркетинговые материалы и т.д." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "Этот токен не существует или был удален." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1181,7 +1181,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "Открыть список токенов" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "Отсоединить токен" #: src/app/main/data/auth.cljs:339 diff --git a/frontend/translations/sv.po b/frontend/translations/sv.po index bdf1d0ea0e..810055f737 100644 --- a/frontend/translations/sv.po +++ b/frontend/translations/sv.po @@ -201,7 +201,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "...varumärkesbyggande, illustrationer, marknadsföringsmaterial, etc." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "Denna token existerar inte eller har raderats." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1183,7 +1183,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "Öppna token-lista" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "Lösgör token" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:43, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:99, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:106 diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index f8d364d3bf..c430de809f 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -202,7 +202,7 @@ msgid "branding-illustrations-marketing-pieces" msgstr "...marka çalışması, çizimler, pazarlama materyalleri, vb." #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:101, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:108 -msgid "color-row.token-color-row.deleted-token" +msgid "options.deleted-token" msgstr "Bu token yok veya silindi." #: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35 @@ -1352,7 +1352,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "Token listesini aç" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "Tokeni ayır" #: src/app/main/data/auth.cljs:339 diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po index 19707c26a6..2eb79194f5 100644 --- a/frontend/translations/zh_CN.po +++ b/frontend/translations/zh_CN.po @@ -1113,7 +1113,7 @@ msgid "ds.inputs.numeric-input.open-token-list-dropdown" msgstr "打开token列表" #: src/app/main/ui/ds/controls/utilities/token_field.cljs:91, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:136 -msgid "ds.inputs.token-field.detach-token" +msgid "token-actions.detach-token" msgstr "分离token" #: src/app/main/data/auth.cljs:339 From 28cefa9cbadf1ac15c1186ba309aadb509238bfe Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 31 Mar 2026 14:06:50 +0200 Subject: [PATCH 090/288] :bug: Fix delay tokens on typography row (#8851) --- .../src/app/main/ui/workspace/sidebar/options/menus/text.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index ca77bdd320..9ed0a9edfb 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -434,7 +434,7 @@ :title label :class (stl/css :title-spacing-text)} [:* - (when (and token-row (some? typography-tokens) (not typography)) + (when (and token-row (some? (resolve-delay typography-tokens)) (not typography)) [:> icon-button* {:variant "ghost" :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown") :on-click toggle-token-dropdown From d5855f355f32d60af6cd9c36b0368810d72b6614 Mon Sep 17 00:00:00 2001 From: Cheonji Kim <76100119+CheonjiKim@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:45:20 +0900 Subject: [PATCH 091/288] :paperclip: Fix typo in README.md for MCP server description (#8884) Edited line 7(perfom -> perform) Signed-off-by: Cheonji Kim <76100119+CheonjiKim@users.noreply.github.com> --- mcp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp/README.md b/mcp/README.md index bd9d9b6447..4ae97c8c69 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -4,7 +4,7 @@ Penpot integrates a LLM layer built on the Model Context Protocol (MCP) via Penpot's Plugin API to interact with a Penpot design -file. Penpot's MCP server enables LLMs to perfom data queries, +file. Penpot's MCP server enables LLMs to perform data queries, transformation and creation operations. Penpot's MCP Server is unlike any other you've seen. You get From a5055af538f7b175a8fd11ca8867ccc95828f925 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 7 Apr 2026 10:58:34 +0200 Subject: [PATCH 092/288] :bug: Fix hidden on multiple selection (#8854) --- CHANGES.md | 1 + frontend/src/app/main/ui/ds/controls/radio_buttons.cljs | 9 +++++---- .../ui/workspace/sidebar/options/shapes/multiple.cljs | 3 +++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8e26ef176e..e14685e6ac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -33,6 +33,7 @@ - Fix dates to avoid show them in english when browser is in auto [Taiga #13786](https://tree.taiga.io/project/penpot/issue/13786) - Fix focus radio button [Taiga #13841](https://tree.taiga.io/project/penpot/issue/13841) - Token tree should be expanded by default [Taiga #13631](https://tree.taiga.io/project/penpot/issue/13631) +- Fix opacity incorrectly disabled for visible shapes [Taiga #13906](https://tree.taiga.io/project/penpot/issue/13906) ## 2.15.0 (Unreleased) diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs b/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs index 24e4b49453..93837196d0 100644 --- a/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs +++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs @@ -24,7 +24,7 @@ [:and :string [:fn #(contains? icon-list %)]]] [:label :string] [:value [:or :keyword :string]] - [:disabled {:optional true} :boolean]]) + [:disabled {:optional true} [:maybe :boolean]]]) (def ^:private schema:radio-buttons [:map @@ -35,8 +35,8 @@ [:name {:optional true} :string] [:selected {:optional true} [:maybe [:or :keyword :string]]] - [:allow-empty {:optional true} :boolean] - [:disabled {:optional true} :boolean] + [:allow-empty {:optional true} [:maybe :boolean]] + [:disabled {:optional true} [:maybe :boolean]] [:options [:vector {:min 1} schema:radio-button]] [:on-change {:optional true} fn?]]) @@ -85,7 +85,8 @@ (for [[idx {:keys [id class value label icon disabled]}] (d/enumerate options)] (let [value-str (d/name value) selected-str (when selected (d/name selected)) - checked? (= selected-str value-str)] + checked? (= selected-str value-str) + disabled (d/nilv disabled false)] [:label {:key idx :html-for id :data-label true diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index caff7d7454..45f8b994e4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -255,6 +255,9 @@ (cond (= attr-group :measure) (select-measure-keys shape) :else (select-keys shape editable-attrs))) + shape-values (cond-> shape-values + (= attr-group :layer) + (update :hidden #(if (nil? %) false %))) new-token-acc (merge-token-values token-acc editable-attrs applied-tokens)] [(conj ids id) (merge-attrs values shape-values) From 3c639f41c41da1a43c7f1cedc761228df5ebc99c Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Tue, 7 Apr 2026 11:26:57 +0200 Subject: [PATCH 093/288] :sparkles: Add option to leave a nitrate organization --- backend/src/app/nitrate.clj | 57 ++- backend/src/app/rpc/commands/nitrate.clj | 130 ++++- backend/src/app/rpc/commands/teams.clj | 63 +-- backend/src/app/rpc/management/nitrate.clj | 25 +- .../test/backend_tests/rpc_nitrate_test.clj | 444 ++++++++++++++++++ common/src/app/common/data.cljc | 10 + frontend/src/app/main/data/nitrate.cljs | 25 + frontend/src/app/main/data/team.cljs | 10 +- frontend/src/app/main/ui/confirm.cljs | 4 + frontend/src/app/main/ui/confirm.scss | 4 + .../app/main/ui/dashboard/change_owner.cljs | 110 +++++ .../app/main/ui/dashboard/change_owner.scss | 45 ++ .../src/app/main/ui/dashboard/sidebar.cljs | 217 +++++++-- .../src/app/main/ui/dashboard/sidebar.scss | 59 ++- frontend/translations/en.po | 42 ++ frontend/translations/es.po | 46 ++ 16 files changed, 1194 insertions(+), 97 deletions(-) create mode 100644 backend/test/backend_tests/rpc_nitrate_test.clj diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 4d7aa5c230..92c0745670 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -56,7 +56,8 @@ :uri uri) ;; TODO decide what to do when Nitrate is inaccesible nil) - (if (>= status 400) + (cond + (>= status 400) ;; For error status codes (4xx, 5xx), fail immediately without validation (do (when (not= status 404) ;; Don't need to log 404 @@ -65,7 +66,9 @@ :status status :body (:body response))) nil) - ;; For success status codes, validate the response + (= status 204) ;; 204 doesn't return any body + nil + :else ;; For success status codes, validate the response (let [coercer-http (sm/coercer schema :type :validation :hint (str "invalid data received calling " uri)) @@ -107,6 +110,17 @@ [:owner-id ::sm/uuid] [:avatar-bg-url [::sm/text]]]) +(def ^:private schema:org-summary + [:map + [:id ::sm/uuid] + [:name ::sm/text] + [:owner-id ::sm/uuid] + [:teams + [:vector + [:map + [:id ::sm/uuid] + [:is-your-penpot :boolean]]]]]) + (def ^:private schema:team [:map [:id ::sm/uuid] @@ -118,6 +132,7 @@ [:is-member :boolean] [:organization-id ::sm/uuid]]) + ;; TODO Unify with schemas on backend/src/app/http/management.clj (def ^:private schema:timestamp (sm/type-schema @@ -210,6 +225,18 @@ profile-id) schema:profile-org params))) + +(defn- get-org-summary-api + [cfg {:keys [org-id] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri)] + (request-to-nitrate cfg :get + (str baseuri + "/api/organizations/" + org-id + "/summary") + schema:org-summary params))) + + (defn- set-team-org-api [cfg {:keys [organization-id team-id is-default] :as params}] (let [baseuri (cf/get :nitrate-backend-uri) @@ -233,6 +260,26 @@ "/add-user") schema:profile-org params))) +(defn- remove-profile-from-org-api + [cfg {:keys [profile-id org-id] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri) + params (assoc params :request-params {:user-id profile-id})] + (request-to-nitrate cfg :post + (str baseuri + "/api/organizations/" + org-id + "/remove-user") + nil params))) + +(defn- delete-team-api + [cfg {:keys [team-id] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri)] + (request-to-nitrate cfg :delete + (str baseuri + "/api/teams/" + team-id) + nil params))) + (defn- get-subscription-api [cfg {:keys [profile-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] @@ -260,7 +307,10 @@ {:get-team-org (partial get-team-org-api cfg) :set-team-org (partial set-team-org-api cfg) :get-org-membership-by-team (partial get-org-membership-by-team-api cfg) + :get-org-summary (partial get-org-summary-api cfg) :add-profile-to-org (partial add-profile-to-org-api cfg) + :remove-profile-from-org (partial remove-profile-from-org-api cfg) + :delete-team (partial delete-team-api cfg) :get-subscription (partial get-subscription-api cfg) :connectivity (partial get-connectivity-api cfg)})) @@ -324,7 +374,4 @@ -(defn connectivity - [cfg] - (call cfg :connectivity {})) diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index 5313817fd3..d16bd8cd22 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -1,8 +1,12 @@ (ns app.rpc.commands.nitrate (:require + [app.common.data :as d] + [app.common.exceptions :as ex] [app.common.schema :as sm] + [app.db :as db] [app.nitrate :as nitrate] [app.rpc :as-alias rpc] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.util.services :as sv])) @@ -12,9 +16,129 @@ [:licenses ::sm/boolean]]) (sv/defmethod ::get-nitrate-connectivity - {::rpc/auth false - ::doc/added "1.18" + {::rpc/auth true + ::doc/added "2.14" ::sm/params [:map] ::sm/result schema:connectivity} [cfg _params] - (nitrate/connectivity cfg)) + (nitrate/call cfg :connectivity {})) + +(def ^:private sql:prefix-team-name-and-unset-default + "UPDATE team + SET name = ? || name, + is_default = FALSE + WHERE id = ?;") + +(def ^:private sql:get-member-teams-info + "SELECT t.id, + tpr.is_owner, + (SELECT count(*) FROM team_profile_rel WHERE team_id = t.id) AS num_members, + (SELECT array_agg(profile_id) FROM team_profile_rel WHERE team_id = t.id) AS member_ids + FROM team AS t + JOIN team_profile_rel AS tpr ON (tpr.team_id = t.id) + WHERE tpr.profile_id = ? + AND t.id = ANY(?) + AND t.deleted_at IS NULL") + +(def ^:private schema:leave-org + [:map + [:org-id ::sm/uuid] + [:default-team-id ::sm/uuid] + [:teams-to-delete + [:vector ::sm/uuid]] + [:teams-to-leave + [:vector + [:map + [:id ::sm/uuid] + [:reassign-to {:optional true} ::sm/uuid]]]]]) + +(sv/defmethod ::leave-org + {::rpc/auth true + ::doc/added "2.15" + ::sm/params schema:leave-org + ::db/transaction true} + [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id org-id default-team-id teams-to-delete teams-to-leave] :as params}] + (let [org-summary (nitrate/call cfg :get-org-summary {:org-id org-id}) + + org-name (:name org-summary) + org-prefix (str "[" (d/sanitize-string org-name) "] ") + + your-penpot-ids (->> (:teams org-summary) + (filter :is-your-penpot) + (map :id) + (into #{})) + + valid-default-team-id? (contains? your-penpot-ids default-team-id) + + org-team-ids (->> (:teams org-summary) + (remove :is-your-penpot) + (map :id)) + ids-array (db/create-array conn "uuid" org-team-ids) + teams (db/exec! conn [sql:get-member-teams-info profile-id ids-array]) + teams-by-id (d/index-by :id teams) + + ;; valid teams to delete are those that the user is owner, and only have one member + valid-teams-to-delete-ids (->> teams + (filter #(and (:is-owner %) + (= (:num-members %) 1))) + (map :id) + (into #{})) + + valid-teams-to-delete? (= valid-teams-to-delete-ids (into #{} teams-to-delete)) + + ;; valid teams to transfer are those that the user is owner, and have more than one member + valid-teams-to-transfer (->> teams + (filter #(and (:is-owner %) + (> (:num-members %) 1)))) + valid-teams-to-transfer-ids (->> valid-teams-to-transfer (map :id) (into #{})) + + ;; valid teams to exit are those that the user isn't owner, and have more than one member + valid-teams-to-exit (->> teams + (filter #(and (not (:is-owner %)) + (> (:num-members %) 1)))) + valid-teams-to-exit-ids (->> valid-teams-to-exit (map :id) (into #{})) + + valid-teams-to-leave-ids (into valid-teams-to-transfer-ids valid-teams-to-exit-ids) + + ;; for every team in teams-to-leave, check that: + ;; - if it has a reassign-to, it belongs to valid-teams-to-transfer and + ;; the reassign-to is a member of the team and not the current user; + ;; - if it hasn't a reassign-to, check that it belongs to valid-teams-to-exit + valid-teams-to-leave? (and + (= valid-teams-to-leave-ids (->> teams-to-leave (map :id) (into #{}))) + (every? (fn [{:keys [id reassign-to]}] + (if reassign-to + (let [members (db/pgarray->set (:member-ids (get teams-by-id id)))] + (and (contains? valid-teams-to-transfer-ids id) + (not= reassign-to profile-id) + (contains? members reassign-to))) + (contains? valid-teams-to-exit-ids id))) + teams-to-leave))] + + + (when (= (:owner-id org-summary) profile-id) + (ex/raise :type :validation + :code :org-owner-cannot-leave)) + + (when (or + (not valid-teams-to-delete?) + (not valid-teams-to-leave?) + (not valid-default-team-id?)) + (ex/raise :type :validation + :code :not-valid-teams)) + + ;; delete the teams-to-delete + (doseq [id teams-to-delete] + (teams/delete-team cfg {:profile-id profile-id :team-id id})) + + ;; leave the teams-to-leave + (doseq [{:keys [id reassign-to]} teams-to-leave] + (teams/leave-team cfg {:profile-id profile-id :id id :reassign-to reassign-to})) + + ;; Rename default-team-id + (db/exec! conn [sql:prefix-team-name-and-unset-default org-prefix default-team-id]) + + ;; Api call to nitrate + (nitrate/call cfg :remove-profile-from-org {:profile-id profile-id :org-id org-id}) + + nil)) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 854b36bb32..b849ba0aa7 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -532,7 +532,7 @@ (set/difference cfeat/frontend-only-features) (set/difference cfeat/no-team-inheritable-features)) params {:profile-id profile-id - :name "Default" + :name "Your Penpot" :features features :organization-id organization-id :is-default true} @@ -674,7 +674,7 @@ ;; --- Mutation: Leave Team (defn leave-team - [conn {:keys [profile-id id reassign-to]}] + [{:keys [::db/conn]} {:keys [profile-id id reassign-to]}] (let [perms (get-permissions conn profile-id id) members (get-team-members conn id)] @@ -689,7 +689,9 @@ ;; if the `reassign-to` is filled and has a different value ;; than the current profile-id, we proceed to reassing the ;; owner role to profile identified by the `reassign-to`. - (and reassign-to (not= reassign-to profile-id)) + ;; Ignore the reasignation if the current profile is not + ;; the owner + (and reassign-to (not= reassign-to profile-id) (:is-owner perms)) (let [member (d/seek #(= reassign-to (:id %)) members)] (when-not member (ex/raise :type :not-found :code :member-does-not-exist)) @@ -728,32 +730,44 @@ {::doc/added "1.17" ::sm/params schema:leave-team ::db/transaction true} - [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id] :as params}] - (leave-team conn (assoc params :profile-id profile-id))) + [cfg {:keys [::rpc/profile-id] :as params}] + (leave-team cfg (assoc params :profile-id profile-id))) + ;; --- Mutation: Delete Team -(defn- delete-team +(defn delete-team "Mark a team for deletion" - [conn {:keys [id] :as team}] + [{:keys [::db/conn] :as cfg} {:keys [profile-id team-id]}] - (let [delay (ldel/get-deletion-delay team) - team (db/update! conn :team - {:deleted-at (ct/in-future delay)} - {:id id} - {::db/return-keys true})] + (let [team (get-team conn :profile-id profile-id :team-id team-id) + perms (get team :permissions)] + + (when-not (:is-owner perms) + (ex/raise :type :validation + :code :only-owner-can-delete-team)) (when (:is-default team) (ex/raise :type :validation :code :non-deletable-team :hint "impossible to delete default team")) - (wrk/submit! {::db/conn conn - ::wrk/task :delete-object - ::wrk/params {:object :team - :deleted-at (:deleted-at team) - :id id}}) - team)) + (let [delay (ldel/get-deletion-delay team) + team (db/update! conn :team + {:deleted-at (ct/in-future delay)} + {:id team-id} + {::db/return-keys true})] + + ;; Api call to nitrate + (when (contains? cf/flags :nitrate) + (nitrate/call cfg :delete-team {:profile-id profile-id :team-id team-id})) + + (wrk/submit! {::db/conn conn + ::wrk/task :delete-object + ::wrk/params {:object :team + :deleted-at (:deleted-at team) + :id team-id}}) + team))) (def ^:private schema:delete-team [:map {:title "delete-team"} @@ -763,16 +777,9 @@ {::doc/added "1.17" ::sm/params schema:delete-team ::db/transaction true} - [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id] :as params}] - (let [team (get-team conn :profile-id profile-id :team-id id) - perms (get team :permissions)] - - (when-not (:is-owner perms) - (ex/raise :type :validation - :code :only-owner-can-delete-team)) - - (delete-team conn team) - nil)) + [cfg {:keys [::rpc/profile-id id] :as params}] + (delete-team cfg {:team-id id :profile-id profile-id}) + nil) ;; --- Mutation: Team Update Role diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index beba802848..62c0070364 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -8,6 +8,7 @@ "Internal Nitrate HTTP RPC API. Provides authenticated access to organization management and token validation endpoints." (:require + [app.common.data :as d] [app.common.schema :as sm] [app.common.types.profile :refer [schema:profile, schema:basic-profile]] [app.common.types.team :refer [schema:team]] @@ -20,8 +21,7 @@ [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.doc :as doc] - [app.util.services :as sv] - [cuerdas.core :as str])) + [app.util.services :as sv])) ;; ---- API: authenticate @@ -211,9 +211,10 @@ ;; ---- API: delete-teams-keeping-your-penpot-projects -(def ^:private sql:add-prefix-to-teams +(def ^:private sql:prefix-teams-name-and-unset-default "UPDATE team - SET name = ? || name + SET name = ? || name, + is_default = FALSE WHERE id = ANY(?) RETURNING id, name;") @@ -230,23 +231,15 @@ RETURNING id, name;") ::sm/params schema:notify-org-deletion} [cfg {:keys [teams org-name]}] (when (seq teams) - (let [cleaned-org-name (if org-name - (-> org-name - str - str/trim - (str/replace #"[^\w\s\-_()]+" "") - (str/replace #"\s+" " ") - str/trim) - "") - org-prefix (str "[" cleaned-org-name "] ")] + (let [org-prefix (str "[" (d/sanitize-string org-name) "] ")] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (let [ids-array (db/create-array conn "uuid" teams) - ;; ---- Rename projects ---- - updated-teams (db/exec! conn [sql:add-prefix-to-teams org-prefix ids-array])] + ;; Rename projects + updated-teams (db/exec! conn [sql:prefix-teams-name-and-unset-default org-prefix ids-array])] - ;; ---- Notify users ---- + ;; Notify users (doseq [team updated-teams] (notify-team-change cfg (:id team) (:name team) nil org-name "dashboard.org-deleted")))))))) diff --git a/backend/test/backend_tests/rpc_nitrate_test.clj b/backend/test/backend_tests/rpc_nitrate_test.clj new file mode 100644 index 0000000000..0a0c35296f --- /dev/null +++ b/backend/test/backend_tests/rpc_nitrate_test.clj @@ -0,0 +1,444 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns backend-tests.rpc-nitrate-test + (:require + [app.common.uuid :as uuid] + [app.db :as-alias db] + [app.nitrate :as nitrate] + [app.rpc :as-alias rpc] + [backend-tests.helpers :as th] + [clojure.test :as t] + [cuerdas.core :as str])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- make-org-summary + [& {:keys [org-id org-name owner-id your-penpot-teams org-teams] + :or {your-penpot-teams [] org-teams []}}] + {:id org-id + :name org-name + :owner-id owner-id + :teams (into + (mapv (fn [id] {:id id :is-your-penpot true}) your-penpot-teams) + (mapv (fn [id] {:id id :is-your-penpot false}) org-teams))}) + +(defn- nitrate-call-mock + "Creates a mock for nitrate/call that returns the given org-summary for + :get-org-summary and nil for any other method." + [org-summary] + (fn [_cfg method _params] + (case method + :get-org-summary org-summary + nil))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Tests +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest leave-org-happy-path-no-extra-teams + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + + org-id (uuid/random) + ;; The user's personal penpot team in the org context + your-penpot-id (:default-team-id profile-user) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :default-team-id your-penpot-id + :teams-to-delete [] + :teams-to-leave []} + out (th/command! data)] + + ;; (th/print-result! out) + (t/is (th/success? out)) + (t/is (nil? (:result out))) + + ;; The personal team must be renamed with the org prefix and + ;; unset as a default team. + (let [team (th/db-get :team {:id your-penpot-id})] + (t/is (str/starts-with? (:name team) "[Test Org] ")) + (t/is (false? (:is-default team)))))))) + +(t/deftest leave-org-with-teams-to-delete + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + ;; profile-user is the sole owner/member of team1 + team1 (th/create-team* 1 {:profile-id (:id profile-user)}) + + org-id (uuid/random) + your-penpot-id (:default-team-id profile-user) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [(:id team1)])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :default-team-id your-penpot-id + :teams-to-delete [(:id team1)] + :teams-to-leave []} + out (th/command! data)] + + ;; (th/print-result! out) + (t/is (th/success? out)) + + ;; team1 should be scheduled for deletion (deleted-at set) + (let [team (th/db-get :team {:id (:id team1)} {::db/remove-deleted false})] + (t/is (some? (:deleted-at team)))))))) + +(t/deftest leave-org-with-ownership-transfer + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + ;; profile-user owns team1; profile-owner is also a member + team1 (th/create-team* 1 {:profile-id (:id profile-user)}) + _ (th/create-team-role* {:team-id (:id team1) + :profile-id (:id profile-owner) + :role :editor}) + + org-id (uuid/random) + your-penpot-id (:default-team-id profile-user) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [(:id team1)])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :default-team-id your-penpot-id + :teams-to-delete [] + :teams-to-leave [{:id (:id team1) :reassign-to (:id profile-owner)}]} + out (th/command! data)] + + ;; (th/print-result! out) + (t/is (th/success? out)) + + ;; profile-user should no longer be a member of team1 + (let [rel (th/db-get :team-profile-rel + {:team-id (:id team1) + :profile-id (:id profile-user)})] + (t/is (nil? rel))) + + ;; profile-owner should have been promoted to owner + (let [rel (th/db-get :team-profile-rel + {:team-id (:id team1) + :profile-id (:id profile-owner)})] + (t/is (true? (:is-owner rel)))))))) + +(t/deftest leave-org-exit-as-non-owner + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + ;; profile-owner owns team1; profile-user is a non-owner member + team1 (th/create-team* 1 {:profile-id (:id profile-owner)}) + _ (th/create-team-role* {:team-id (:id team1) + :profile-id (:id profile-user) + :role :editor}) + + org-id (uuid/random) + your-penpot-id (:default-team-id profile-user) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [(:id team1)])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :default-team-id your-penpot-id + :teams-to-delete [] + :teams-to-leave [{:id (:id team1)}]} + out (th/command! data)] + + ;; (th/print-result! out) + (t/is (th/success? out)) + + ;; profile-user should no longer be a member of team1 + (let [rel (th/db-get :team-profile-rel + {:team-id (:id team1) + :profile-id (:id profile-user)})] + (t/is (nil? rel))) + + ;; The team itself should still exist + (let [team (th/db-get :team {:id (:id team1)})] + (t/is (nil? (:deleted-at team)))))))) + +(t/deftest leave-org-error-org-owner-cannot-leave + (let [profile-owner (th/create-profile* 1 {:is-active true}) + org-id (uuid/random) + your-penpot-id (:default-team-id profile-owner) + + ;; profile-owner IS the org owner in the org-summary + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-owner) + :org-id org-id + :default-team-id your-penpot-id + :teams-to-delete [] + :teams-to-leave []} + out (th/command! data)] + + (t/is (not (th/success? out))) + (t/is (= :validation (th/ex-type (:error out)))) + (t/is (= :org-owner-cannot-leave (th/ex-code (:error out)))))))) + +(t/deftest leave-org-error-invalid-default-team-id + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + org-id (uuid/random) + your-penpot-id (:default-team-id profile-user) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + ;; Pass a random UUID that is not in the your-penpot-teams list + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :default-team-id (uuid/random) + :teams-to-delete [] + :teams-to-leave []} + out (th/command! data)] + + (t/is (not (th/success? out))) + (t/is (= :validation (th/ex-type (:error out)))) + (t/is (= :not-valid-teams (th/ex-code (:error out)))))))) + +(t/deftest leave-org-error-teams-to-delete-incomplete + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + ;; profile-user is the sole owner/member of both team1 and team2 + team1 (th/create-team* 1 {:profile-id (:id profile-user)}) + team2 (th/create-team* 2 {:profile-id (:id profile-user)}) + + org-id (uuid/random) + your-penpot-id (:default-team-id profile-user) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [(:id team1) (:id team2)])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + ;; Only team1 is listed; team2 is also a sole-owner team and must be included + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :default-team-id your-penpot-id + :teams-to-delete [(:id team1)] + :teams-to-leave []} + out (th/command! data)] + + (t/is (not (th/success? out))) + (t/is (= :validation (th/ex-type (:error out)))) + (t/is (= :not-valid-teams (th/ex-code (:error out)))))))) + +(t/deftest leave-org-error-cannot-delete-multi-member-team + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + ;; team1 has two members: profile-user (owner) and profile-owner (editor) + team1 (th/create-team* 1 {:profile-id (:id profile-user)}) + _ (th/create-team-role* {:team-id (:id team1) + :profile-id (:id profile-owner) + :role :editor}) + + org-id (uuid/random) + your-penpot-id (:default-team-id profile-user) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [(:id team1)])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + ;; team1 has 2 members so it is not a valid deletion candidate + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :default-team-id your-penpot-id + :teams-to-delete [(:id team1)] + :teams-to-leave []} + out (th/command! data)] + + (t/is (not (th/success? out))) + (t/is (= :validation (th/ex-type (:error out)))) + (t/is (= :not-valid-teams (th/ex-code (:error out)))))))) + +(t/deftest leave-org-error-teams-to-leave-incomplete + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + ;; profile-user owns team1, which also has profile-owner as editor + team1 (th/create-team* 1 {:profile-id (:id profile-user)}) + _ (th/create-team-role* {:team-id (:id team1) + :profile-id (:id profile-owner) + :role :editor}) + + org-id (uuid/random) + your-penpot-id (:default-team-id profile-user) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [(:id team1)])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + ;; team1 must be transferred (owner + multiple members) but is absent + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :default-team-id your-penpot-id + :teams-to-delete [] + :teams-to-leave []} + out (th/command! data)] + + (t/is (not (th/success? out))) + (t/is (= :validation (th/ex-type (:error out)))) + (t/is (= :not-valid-teams (th/ex-code (:error out)))))))) + +(t/deftest leave-org-error-reassign-to-self + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + team1 (th/create-team* 1 {:profile-id (:id profile-user)}) + _ (th/create-team-role* {:team-id (:id team1) + :profile-id (:id profile-owner) + :role :editor}) + + org-id (uuid/random) + your-penpot-id (:default-team-id profile-user) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [(:id team1)])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + ;; reassign-to points to the profile that is leaving — not allowed + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :default-team-id your-penpot-id + :teams-to-delete [] + :teams-to-leave [{:id (:id team1) :reassign-to (:id profile-user)}]} + out (th/command! data)] + + (t/is (not (th/success? out))) + (t/is (= :validation (th/ex-type (:error out)))) + (t/is (= :not-valid-teams (th/ex-code (:error out)))))))) + +(t/deftest leave-org-error-reassign-to-non-member + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + profile-other (th/create-profile* 3 {:is-active true}) + ;; team1 has profile-user (owner) and profile-owner (editor) — NOT profile-other + team1 (th/create-team* 1 {:profile-id (:id profile-user)}) + _ (th/create-team-role* {:team-id (:id team1) + :profile-id (:id profile-owner) + :role :editor}) + + org-id (uuid/random) + your-penpot-id (:default-team-id profile-user) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [(:id team1)])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + ;; profile-other is not a member of team1 + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :default-team-id your-penpot-id + :teams-to-delete [] + :teams-to-leave [{:id (:id team1) :reassign-to (:id profile-other)}]} + out (th/command! data)] + + (t/is (not (th/success? out))) + (t/is (= :validation (th/ex-type (:error out)))) + (t/is (= :not-valid-teams (th/ex-code (:error out)))))))) + +(t/deftest leave-org-error-reassign-on-non-owned-team + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + ;; profile-owner owns team1; profile-user is just a non-owner member + team1 (th/create-team* 1 {:profile-id (:id profile-owner)}) + _ (th/create-team-role* {:team-id (:id team1) + :profile-id (:id profile-user) + :role :editor}) + + org-id (uuid/random) + your-penpot-id (:default-team-id profile-user) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [(:id team1)])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + ;; profile-user is not the owner so providing reassign-to is invalid + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :default-team-id your-penpot-id + :teams-to-delete [] + :teams-to-leave [{:id (:id team1) :reassign-to (:id profile-owner)}]} + out (th/command! data)] + + (t/is (not (th/success? out))) + (t/is (= :validation (th/ex-type (:error out)))) + (t/is (= :not-valid-teams (th/ex-code (:error out)))))))) diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 2b9748183d..aa06e14e9d 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -1171,6 +1171,16 @@ [key coll] (sort-by key natural-compare coll)) +(defn sanitize-string [s] + (if s + (-> s + str + str/trim + (str/replace #"[^\w\s\-_()]+" "") + (str/replace #"\s+" " ") + str/trim) + "")) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Util protocols ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index 8670833bdf..b65d0ad264 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -3,10 +3,14 @@ [app.common.data.macros :as dm] [app.common.uri :as u] [app.config :as cf] + [app.main.data.common :as dcm] [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] + [app.main.data.team :as dt] [app.main.repo :as rp] [app.main.router :as rt] [app.main.store :as st] + [app.util.i18n :as i18n :refer [tr]] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -55,4 +59,25 @@ (contains? #{"active" "past_due" "trialing"} (dm/get-in profile [:subscription :status])))) +(defn leave-org + [{:keys [org-id org-name default-team-id teams-to-delete teams-to-leave on-error] :as params}] + (ptk/reify ::leave-org + ptk/WatchEvent + (watch [_ state _] + (let [profile-team-id (dm/get-in state [:profile :default-team-id])] + (->> (rp/cmd! ::leave-org {:org-id org-id + :org-name org-name + :default-team-id default-team-id + :teams-to-delete teams-to-delete + :teams-to-leave teams-to-leave}) + (rx/mapcat + (fn [_] + (rx/of + (dt/fetch-teams) + (dcm/go-to-dashboard-recent :team-id profile-team-id) + (modal/hide) + (ntf/show {:content (tr "dasboard.leave-org.toast" org-name) + :type :toast + :level :success})))) + (rx/catch on-error)))))) diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs index 2f6c03f68e..0756e71823 100644 --- a/frontend/src/app/main/data/team.cljs +++ b/frontend/src/app/main/data/team.cljs @@ -41,8 +41,14 @@ ptk/UpdateEvent (update [_ state] - (reduce (fn [state {:keys [id] :as team}] - (update-in state [:teams id] merge team)) + (reduce (fn [state {:keys [id organization-id] :as team}] + (let [team-updated (cond-> (merge (dm/get-in state [:teams id]) team) + (not organization-id) (dissoc :organization-id + :organization-name + :organization-slug + :organization-owner-id + :organization-avatar-bg-url))] + (update state :teams assoc id team-updated))) state teams)))) diff --git a/frontend/src/app/main/ui/confirm.cljs b/frontend/src/app/main/ui/confirm.cljs index d2c068ebf2..f9ba97b7e2 100644 --- a/frontend/src/app/main/ui/confirm.cljs +++ b/frontend/src/app/main/ui/confirm.cljs @@ -30,6 +30,7 @@ on-accept on-cancel hint + error-msg items cancel-label accept-label @@ -86,6 +87,9 @@ [:> context-notification* {:level :info :appearance :ghost} hint]) + (when (string? error-msg) + [:> context-notification* {:level :error :class (stl/css :modal-error-msg)} + error-msg]) (when (> (count items) 0) [:* [:p {:class (stl/css :modal-subtitle)} diff --git a/frontend/src/app/main/ui/confirm.scss b/frontend/src/app/main/ui/confirm.scss index f5261af38d..e3106e528a 100644 --- a/frontend/src/app/main/ui/confirm.scss +++ b/frontend/src/app/main/ui/confirm.scss @@ -65,3 +65,7 @@ color: var(--modal-text-foreground-color); } + +.modal-error-msg { + margin: var(--sp-xxl) 0; +} diff --git a/frontend/src/app/main/ui/dashboard/change_owner.cljs b/frontend/src/app/main/ui/dashboard/change_owner.cljs index fc4ae33cc9..31bb3b3b0a 100644 --- a/frontend/src/app/main/ui/dashboard/change_owner.cljs +++ b/frontend/src/app/main/ui/dashboard/change_owner.cljs @@ -7,11 +7,14 @@ (ns app.main.ui.dashboard.change-owner (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] [app.common.schema :as sm] + [app.common.uuid :as uuid] [app.main.data.modal :as modal] [app.main.ui.components.forms :as fm] [app.main.ui.icons :as deprecated-icon] [app.util.i18n :as i18n :refer [tr]] + [cuerdas.core :as str] [rumext.v2 :as mf])) (def ^:private schema:leave-modal-form @@ -72,3 +75,110 @@ :disabled (not (:valid @form)) :value (tr "modals.leave-and-reassign.promote-and-leave") :on-click on-accept}]]]]])) + + + +(mf/defc ^:private team-member-select* + [{:keys [team profile form field-name default-member-id]}] + (let [members (get team :members) + filtered-members (->> members + (filter #(not= (:email %) (:email profile)))) + options (->> filtered-members + (map #(hash-map :label (:name %) :value (str (:id %)))))] + [:div {:class (stl/css :team-select-container)} + [:div {:class (stl/css :team-name)} (:name team)] + (if (empty? filtered-members) + [:p {:class (stl/css :modal-msg)} + (tr "modals.leave-and-reassign.forbidden")] + [:& fm/select {:name field-name + :select-class (stl/css :team-member) + :dropdown-class (stl/css :team-member) + :options options + :form form + :default default-member-id}])])) + +(defn- make-leave-org-modal-form-schema [teams] + (into + [:map {:title "LeaveOrgModalForm"}] + (for [team teams] + [(keyword (str "member-id-" (:id team))) ::sm/text]))) + + +(mf/defc leave-and-reassign-org-modal + {::mf/register modal/components + ::mf/register-as :leave-and-reassign-org + ::mf/wrap [mf/memo]} + [{:keys [profile teams-to-transfer num-teams-to-delete accept] :as props}] + (let [schema (mf/with-memo [teams-to-transfer] + (make-leave-org-modal-form-schema teams-to-transfer)) + ;; Compute initial values for each team select + team-fields (mf/with-memo [teams-to-transfer] + (for [team teams-to-transfer] + (let [members (get team :members) + filtered-members (filter #(not= (:email %) (:email profile)) members) + first-admin (first (filter :is-admin filtered-members)) + first-member (first filtered-members) + default-member-id (cond + first-admin (str (:id first-admin)) + first-member (str (:id first-member)) + :else "") + field-name (keyword (str "member-id-" (:id team)))] + {:team team + :field-name field-name + :default-member-id default-member-id}))) + + initial-values (mf/with-memo [team-fields] + (d/index-by :field-name :default-member-id team-fields)) + + form (fm/use-form :schema schema :initial initial-values) + + all-valid? (every? + (fn [{:keys [field-name]}] + (let [val (get-in @form [:clean-data field-name])] + (not (str/blank? val)))) + team-fields) + + on-accept (fn [_] + (let [teams-to-transfer (mapv (fn [{:keys [team field-name]}] + (let [val (get-in @form [:clean-data field-name])] + {:id (:id team) + :reassign-to (uuid/parse val)})) + team-fields)] + (accept {:teams-to-transfer teams-to-transfer})))] + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-org-container)} + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-org-title)} (tr "modals.before-leave-org.title")] + [:button {:class (stl/css :modal-close-btn) + :on-click modal/hide!} deprecated-icon/close]] + + [:div {:class (stl/css :modal-content)} + (if (zero? num-teams-to-delete) + [:p {:class (stl/css :modal-org-msg)} + (tr "modals.leave-org-and-reassign.hint")] + [:* + [:p {:class (stl/css :modal-org-msg)} + (tr "modals.leave-org-and-reassign.hint-delete")] + [:p {:class (stl/css :modal-org-msg)} + (tr "modals.leave-org-and-reassign.hint-promote")]]) + [:& fm/form {:form form} + [:div {:class (stl/css :teams-container)} + (for [{:keys [team field-name default-member-id]} team-fields] + ^{:key (:id team)} + [:> team-member-select* {:team team :profile profile :form form :field-name field-name :default-member-id default-member-id}])]]] + + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:input {:class (stl/css :cancel-button) + :type "button" + :value (tr "labels.cancel") + :on-click modal/hide!}] + + [:input.accept-button + {:type "button" + :class (stl/css-case :accept-btn true + :danger all-valid? + :global/disabled (not all-valid?)) + :disabled (not all-valid?) + :value (tr "modals.leave-and-reassign.promote-and-leave") + :on-click on-accept}]]]]])) diff --git a/frontend/src/app/main/ui/dashboard/change_owner.scss b/frontend/src/app/main/ui/dashboard/change_owner.scss index fadf30e3f6..6fa30819ca 100644 --- a/frontend/src/app/main/ui/dashboard/change_owner.scss +++ b/frontend/src/app/main/ui/dashboard/change_owner.scss @@ -5,6 +5,8 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; +@use "ds/typography.scss" as t; +@use "ds/_sizes.scss" as *; .modal-overlay { @extend %modal-overlay-base; @@ -58,3 +60,46 @@ .modal-msg { color: var(--modal-text-foreground-color); } + +.teams-container { + display: flex; + flex-direction: column; + gap: var(--sp-s); + margin: var(--sp-xxxl) 0; +} + +.team-select-container { + display: grid; + grid-template-columns: 1fr 2fr; + align-items: center; + width: 100%; +} + +.modal-org-container { + @extend %modal-container-base; + + overflow-y: auto; + max-height: $sz-512; +} + +.modal-org-title { + @include t.use-typography("headline-large"); + + color: var(--modal-title-foreground-color); +} + +.modal-org-msg { + @include t.use-typography("body-large"); + + color: var(--modal-text-foreground-color); +} + +.team-name { + @include t.use-typography("body-medium"); + + color: var(--modal-text-foreground-color); +} + +.team-member { + @include t.use-typography("body-medium"); +} diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index fb52064251..2d3ddeb915 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -73,6 +73,12 @@ (def ^:private menu-icon (deprecated-icon/icon-xref :menu (stl/css :menu-icon))) +(def ^:private org-menu-icon + (deprecated-icon/icon-xref :menu (stl/css :org-menu-icon))) + +(def ^:private org-menu-icon-open + (deprecated-icon/icon-xref :menu (stl/css :org-menu-icon-open))) + (def ^:private pin-icon (deprecated-icon/icon-xref :pin (stl/css :pin-icon))) @@ -336,10 +342,10 @@ [:> dropdown-menu-item* {:on-click on-org-click :data-value default-team-id :class (stl/css :org-dropdown-item)} - [:span {:class (stl/css :nitrate-org-icon)} + [:span {:class (stl/css :org-icon)} [:> raw-svg* {:id penpot-logo-icon}]] "Penpot" - (when (= default-team-id (:id organization)) + (when (= default-team-id (:default-team-id organization)) tick-icon)] (for [org-item (remove :is-default (vals organizations))] @@ -350,7 +356,7 @@ [:> org-avatar* {:org org-item :size "xxl"}] [:span {:class (stl/css :team-text) :title (:name org-item)} (:name org-item)] - (when (= (:id org-item) (:id organization)) + (when (= (:id org-item) (:default-team-id organization)) tick-icon)]) [:hr {:role "separator" :class (stl/css :team-separator)}] @@ -449,18 +455,22 @@ (modal/hide)))) on-error - (fn [{:keys [code] :as error}] - (condp = code - :no-enough-members-for-leave - (rx/of (ntf/error (tr "errors.team-leave.insufficient-members"))) + (fn [error] + (let [code (-> error ex-data :code)] + (condp = code + :only-owner-can-delete-team + (rx/of (ntf/error (tr "errors.team-leave.only-owner-can-delete"))) - :member-does-not-exist - (rx/of (ntf/error (tr "errors.team-leave.member-does-not-exists"))) + :no-enough-members-for-leave + (rx/of (ntf/error (tr "errors.team-leave.insufficient-members"))) - :owner-cant-leave-team - (rx/of (ntf/error (tr "errors.team-leave.owner-cant-leave"))) + :member-does-not-exist + (rx/of (ntf/error (tr "errors.team-leave.member-does-not-exists"))) - (rx/throw error))) + :owner-cant-leave-team + (rx/of (ntf/error (tr "errors.team-leave.owner-cant-leave"))) + + (rx/throw error)))) leave-fn (mf/use-fn @@ -581,10 +591,110 @@ :data-testid "delete-team"} (tr "dashboard.delete-team")])])) +(mf/defc org-options-dropdown* + {::mf/private true} + [{:keys [organization profile teams] :rest props}] + (let [default-team-id (mf/with-memo [teams] + (->> teams + (filter :is-default) + first + :id)) + non-default-teams (mf/with-memo [teams] + (remove :is-default teams)) + owned-teams (mf/with-memo [non-default-teams] + (filter #(dm/get-in % [:permissions :is-owner]) non-default-teams)) + not-owned-teams (mf/with-memo [non-default-teams] + (remove #(dm/get-in % [:permissions :is-owner]) non-default-teams)) + teams-to-delete (mf/with-memo [owned-teams] + (filter #(= (count (:members %)) 1) owned-teams)) + teams-to-transfer (mf/with-memo [owned-teams] + (filter #(> (count (:members %)) 1) owned-teams)) + num-teams-to-leave (+ (count teams-to-transfer) (count not-owned-teams)) + num-teams-to-delete (count teams-to-delete) + num-teams-to-transfer (count teams-to-transfer) + + on-error + (mf/use-fn + (fn [error] + (let [code (-> error ex-data :code) + ;; Map error codes to their translation keys + error-map {:not-valid-teams "errors.org-leave.no-valid-teams" + :org-owner-cannot-leave "errors.org-leave.org-owner-cannot-leave" + :only-owner-can-delete-team "errors.team-leave.only-owner-can-delete" + :no-enough-members-for-leave "errors.team-leave.insufficient-members" + :member-does-not-exist "errors.team-leave.member-does-not-exists" + :owner-cant-leave-team "errors.team-leave.owner-cant-leave"}] + + (if-let [tr-key (get error-map code)] + (rx/of (dtm/fetch-teams) + (modal/hide) + (ntf/error (tr tr-key))) + (rx/throw error))))) + + leave-fn + (mf/use-fn + (mf/deps on-error organization default-team-id not-owned-teams teams-to-delete) + (fn [{:keys [teams-to-transfer]}] + (let [teams-to-leave (cond->> not-owned-teams + :always + (map #(select-keys % [:id])) + (seq teams-to-transfer) + (concat teams-to-transfer)) + teams-to-delete (map :id teams-to-delete)] + + (st/emit! (dnt/leave-org {:org-id (:organization-id organization) + :default-team-id default-team-id + :teams-to-delete teams-to-delete + :teams-to-leave teams-to-leave + :on-error on-error}))))) + + on-leave-clicked + (mf/use-fn + (mf/deps leave-fn profile organization teams-to-transfer num-teams-to-leave num-teams-to-delete num-teams-to-transfer) + (cond + (and (pos? num-teams-to-delete) + (zero? num-teams-to-transfer)) + #(st/emit! (modal/show + {:type :confirm + :title (tr "modals.before-leave-org.title" (:name organization)) + :message (tr "modals.before-leave-org.message") + :accept-label (tr "modals.leave-org-confirm.accept") + :on-accept leave-fn + :error-msg (tr "modals.before-leave-org.warning")})) + (pos? num-teams-to-transfer) + #(st/emit! + (modal/show + {:type :leave-and-reassign-org + :profile profile + :teams-to-transfer teams-to-transfer + :num-teams-to-delete num-teams-to-delete + :accept leave-fn})) + + :else + #(st/emit! (modal/show + {:type :confirm + :title (tr "modals.leave-org-confirm.title" (:name organization)) + :message (tr "modals.leave-org-confirm.message") + :accept-label (tr "modals.leave-org-confirm.accept") + :on-accept leave-fn}))))] + (mf/use-effect + (fn [] + ;; We need all the team members of the owned teams + ;; TODO this will re-render once for each owned team, not very performance-wise + (do + (doseq [team owned-teams] + (st/emit! (dtm/fetch-members (:id team))))))) + [:> dropdown-menu* props + + [:> dropdown-menu-item* {:on-click on-leave-clicked + :class (stl/css :team-options-item)} + (tr "dashboard.leave-org")]])) + (defn- team->org [team] (assoc (dm/select-keys team [:id :organization-id :organization-slug :organization-owner-id :organization-avatar-bg-url]) - :name (:organization-name team))) + :name (:organization-name team) + :default-team-id (:id team))) (mf/defc sidebar-org-switch* [{:keys [team profile]}] @@ -603,6 +713,11 @@ current-org (team->org team) + org-teams (mf/with-memo [teams current-org] + (->> teams + vals + (filter #(= (:organization-id %) (:organization-id current-org))))) + default-org? (nil? (:organization-id current-org)) show-orgs-menu* @@ -611,6 +726,21 @@ show-orgs-menu? (deref show-orgs-menu*) + show-org-options-menu* + (mf/use-state false) + + show-org-options-menu? + (deref show-org-options-menu*) + + on-show-options-click + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (swap! show-org-options-menu* not))) + + close-org-options-menu + (mf/use-fn #(reset! show-org-options-menu* false)) + on-show-orgs-click (mf/use-fn (fn [event] @@ -638,24 +768,33 @@ (st/emit! (dnt/show-nitrate-popup :nitrate-form)))))] (if show-dropdown? [:div {:class (stl/css :sidebar-org-switch)} + [:div {:class (stl/css :org-switch-content)} + [:button {:class (stl/css :current-org) + :on-click on-show-orgs-click + :on-key-down on-show-orgs-keydown + :aria-expanded show-orgs-menu? + :aria-haspopup "menu"} + [:div {:class (stl/css :team-name)} + (if default-org? + [:* + [:span {:class (stl/css :org-penpot-icon)} + [:> raw-svg* {:id penpot-logo-icon}]] + [:span {:class (stl/css :team-text)} + "Penpot"]] + [:* + [:> org-avatar* {:org current-org :size "xxxl"}] + [:span {:class (stl/css :team-text)} + (:name current-org)]])] + arrow-icon] + (if (or default-org? + (= (:id profile) (:organization-owner-id current-org))) + [:div {:class (stl/css :org-options)}] + [:> button* {:variant "ghost" + :type "button" + :class (stl/css :org-options-btn) + :on-click on-show-options-click} + (if show-org-options-menu? org-menu-icon-open org-menu-icon)])] - [:button {:class (stl/css :current-org) - :on-click on-show-orgs-click - :on-key-down on-show-orgs-keydown - :aria-expanded show-orgs-menu? - :aria-haspopup "menu"} - [:div {:class (stl/css :team-name)} - (if default-org? - [:* - [:span {:class (stl/css :nitrate-penpot-icon)} - [:> raw-svg* {:id penpot-logo-icon}]] - [:span {:class (stl/css :team-text)} - "Penpot"]] - [:* - [:> org-avatar* {:org current-org :size "xxxl"}] - [:span {:class (stl/css :team-text)} - (:name current-org)]])] - arrow-icon] ;; Orgs Dropdown [:> organizations-selector-dropdown* {:show show-orgs-menu? @@ -664,14 +803,22 @@ :class (stl/css :dropdown :teams-dropdown) :organization current-org :profile profile - :organizations orgs}]] - [:div {:class (stl/css :nitrate-selected-org)} - [:span {:class (stl/css :nitrate-penpot-icon)} + :organizations orgs}] + ;; Orgs options + [:> org-options-dropdown* {:show show-org-options-menu? + :on-close close-org-options-menu + :id "team-options" + :class (stl/css :dropdown :options-dropdown) + :organization current-org + :profile profile + :teams org-teams}]] + [:div {:class (stl/css :selected-org)} + [:span {:class (stl/css :org-penpot-icon)} [:> raw-svg* {:id penpot-logo-icon}]] "Penpot" [:> button* {:variant "ghost" :type "button" - :class (stl/css :nitrate-create-org) + :class (stl/css :create-org) :on-click on-create-org-click} (tr "dashboard.plus-create-new-org")]]))) (mf/defc sidebar-team-switch* @@ -914,7 +1061,7 @@ [:* [:div {:ref container} (when nitrate? - [:div {:class (stl/css :nitrate-orgs-container)} + [:div {:class (stl/css :orgs-container)} [:> sidebar-org-switch* {:team team :profile profile}]]) [:div {:class (stl/css-case :sidebar-content true :sidebar-content-nitrate nitrate?)} [:> sidebar-team-switch* {:team team :profile profile}] diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index 52a17762e3..f2191747b4 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -576,16 +576,16 @@ color: var(--color-accent-tertiary); } -.nitrate-orgs-container { +.orgs-container { align-items: center; display: flex; height: calc(2 * var(--sp-xxxl)); max-height: calc(2 * var(--sp-xxxl)); justify-content: space-between; - padding: 0 var(--sp-l); + padding: 0 var(--sp-xl); } -.nitrate-selected-org { +.selected-org { @include t.use-typography("body-medium"); color: var(--color-foreground-primary); @@ -596,12 +596,12 @@ gap: var(--sp-s); } -.nitrate-create-org { +.create-org { margin-inline-start: auto; text-transform: uppercase; } -.nitrate-penpot-icon { +.org-penpot-icon { display: flex; justify-content: center; align-items: center; @@ -617,7 +617,7 @@ } } -.nitrate-org-icon { +.org-icon { display: flex; justify-content: center; align-items: center; @@ -641,15 +641,58 @@ .current-org { @include deprecated.buttonStyle; + text-transform: none; display: grid; align-items: center; - grid-template-columns: 1fr auto; + grid-template-columns: 1fr auto auto; gap: var(--sp-s); height: 100%; width: 100%; - padding: 0 var(--sp-s); } .current-org .arrow-icon { margin-inline-end: var(--sp-xs); } + +.org-options { + display: flex; + justify-content: center; + align-items: center; + max-width: var(--sp-xxl); + min-width: $sz-28; + height: 100%; +} + +.org-switch-content { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + height: $sz-48; + width: 100%; +} + +.org-options-btn { + --icon-stroke: var(--icon-foreground); + + display: flex; + justify-content: center; + align-items: center; + width: $sz-32; + height: $sz-32; + + &:hover { + --icon-stroke: var(--color-accent-primary); + } +} + +.org-menu-icon { + @extend %button-icon; + + stroke: var(--icon-stroke); +} + +.org-menu-icon-open { + @extend %button-icon; + + stroke: var(--color-accent-primary); +} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 10851f8f6b..b73799b790 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -767,6 +767,9 @@ msgstr "Invite people" msgid "dashboard.leave-team" msgstr "Leave team" +msgid "dashboard.leave-org" +msgstr "Leave org" + #: src/app/main/ui/dashboard/templates.cljs:84, src/app/main/ui/dashboard/templates.cljs:169 msgid "dashboard.libraries-and-templates" msgstr "Libraries & Templates" @@ -1551,6 +1554,15 @@ msgstr "SVG is invalid or malformed" msgid "errors.team-feature-mismatch" msgstr "Detected incompatible feature '%s'" +msgid "errors.org-leave.org-owner-cannot-leave" +msgstr "The organization owner can't leave the organization." + +msgid "errors.org-leave.no-valid-teams" +msgstr "There was a problem leaving the organization. Please try again." + +msgid "errors.team-leave.only-owner-can-delete" +msgstr "Only the owner of a team can delete it." + #: src/app/main/ui/dashboard/sidebar.cljs:373, src/app/main/ui/dashboard/team.cljs:393 msgid "errors.team-leave.insufficient-members" msgstr "Insufficient members to leave team, you probably want to delete it." @@ -3593,6 +3605,15 @@ msgstr "" "You are the owner of this team. Please select another member to promote to " "owner before you leave." +msgid "modals.leave-org-and-reassign.hint" +msgstr "You are the owner of some organization's teams. Please promote another member to become an owner." + +msgid "modals.leave-org-and-reassign.hint-delete" +msgstr "You are the only member of some of the teams. Those teams will be deleted along with its projects and files." + +msgid "modals.leave-org-and-reassign.hint-promote" +msgstr "Also, you are the owner of some organization's teams. Please promote another member to become an owner." + #: src/app/main/ui/dashboard/change_owner.cljs:73 msgid "modals.leave-and-reassign.promote-and-leave" msgstr "Promote and leave" @@ -3609,14 +3630,35 @@ msgstr "Before you leave" msgid "modals.leave-confirm.accept" msgstr "Leave team" +msgid "modals.leave-org-confirm.accept" +msgstr "Leave organization" + #: src/app/main/ui/dashboard/sidebar.cljs:409, src/app/main/ui/dashboard/team.cljs:449 msgid "modals.leave-confirm.message" msgstr "Are you sure you want to leave this team?" +msgid "modals.leave-org-confirm.message" +msgstr "You will permanently lose access to all teams, projects, and files within it." + #: src/app/main/ui/dashboard/sidebar.cljs:408, src/app/main/ui/dashboard/sidebar.cljs:429, src/app/main/ui/dashboard/team.cljs:425, src/app/main/ui/dashboard/team.cljs:448 msgid "modals.leave-confirm.title" msgstr "Leaving team" +msgid "modals.leave-org-confirm.title" +msgstr "Leaving %s organization?" + +msgid "modals.before-leave-org.title" +msgstr "BEFORE YOU LEAVE THE ORGANIZATION" + +msgid "modals.before-leave-org.message" +msgstr "You are the only member of some of the teams. Those teams will be deleted along with its projects and files." + +msgid "modals.before-leave-org.warning" +msgstr "Any team where you’re the only member will be deleted." + +msgid "dasboard.leave-org.toast" +msgstr "You're no longer part of the %s organization." + #: src/app/main/ui/delete_shared.cljs:56 msgid "modals.move-shared-confirm.accept" msgid_plural "modals.move-shared-confirm.accept" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 74d8ef2b66..e56c446312 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -771,6 +771,9 @@ msgstr "Invitar a la gente" msgid "dashboard.leave-team" msgstr "Abandonar equipo" +msgid "dashboard.leave-org" +msgstr "Abandonar organización" + #: src/app/main/ui/dashboard/templates.cljs:84, src/app/main/ui/dashboard/templates.cljs:169 msgid "dashboard.libraries-and-templates" msgstr "Bibliotecas y plantillas" @@ -1522,6 +1525,15 @@ msgstr "El SVG no es válido o está mal formado" msgid "errors.team-feature-mismatch" msgstr "Detectada funcionalidad incompatible '%s'" +msgid "errors.org-leave.org-owner-cannot-leave" +msgstr "El dueño de la organización no puede abandonarla." + +msgid "errors.org-leave.no-valid-teams" +msgstr "Ha habido un problema abandonando la organización. Intentalo de nuevo, por favor." + +msgid "errors.team-leave.only-owner-can-delete" +msgstr "Sólo puede borrar un equipo su propietario." + #: src/app/main/ui/dashboard/sidebar.cljs:373, src/app/main/ui/dashboard/team.cljs:393 msgid "errors.team-leave.insufficient-members" msgstr "" @@ -3544,6 +3556,19 @@ msgstr "" "Tienes la propiedad de este equipo. Por favor selecciona otra persona " "integrante para promover al rol Propiedad." +msgid "modals.leave-org-and-reassign.hint" +msgstr "" +"Tienes la propiedad de algunos equipos de esta organización. " +"Por favor selecciona otra persona integrante para promover al rol Propiedad." + +msgid "modals.leave-org-and-reassign.hint-delete" +msgstr "Eres el único miembro de algunos equipos. Esos equipos se van a borrar, junto con sus proyectos y ficheros." + +msgid "modals.leave-org-and-reassign.hint-promote" +msgstr "" +"Además, tienes la propiedad de algunos equipos de esta organización. " +"Por favor selecciona otra persona integrante para promover al rol Propiedad." + #: src/app/main/ui/dashboard/change_owner.cljs:73 msgid "modals.leave-and-reassign.promote-and-leave" msgstr "Promocionar y abandonar" @@ -3560,14 +3585,35 @@ msgstr "Antes de que abandones" msgid "modals.leave-confirm.accept" msgstr "Abandonar el equipo" +msgid "modals.leave-org-confirm.accept" +msgstr "Abandonar organización" + #: src/app/main/ui/dashboard/sidebar.cljs:409, src/app/main/ui/dashboard/team.cljs:449 msgid "modals.leave-confirm.message" msgstr "¿Seguro que quieres abandonar este equipo?" +msgid "modals.leave-org-confirm.message" +msgstr "Perderás permanentemente el acceso a todos los equipos, proyectos y archivos en ella." + #: src/app/main/ui/dashboard/sidebar.cljs:408, src/app/main/ui/dashboard/sidebar.cljs:429, src/app/main/ui/dashboard/team.cljs:425, src/app/main/ui/dashboard/team.cljs:448 msgid "modals.leave-confirm.title" msgstr "Abandonando el equipo" +msgid "modals.leave-org-confirm.title" +msgstr "¿Abandonando la organización %s?" + +msgid "modals.before-leave-org.title" +msgstr "ANTES DE ABANDONAR LA ORGANIZACIÓN" + +msgid "modals.before-leave-org.message" +msgstr "Eres el único miembro de algunos equipos. Esos equipos se van a borrar, junto con sus proyectos y ficheros." + +msgid "modals.before-leave-org.warning" +msgstr "Se van a borrar todos los equipos en los que eres el único miembro." + +msgid "dasboard.leave-org.toast" +msgstr "Ya no eres miembro de la organización %s." + #: src/app/main/ui/delete_shared.cljs:56 msgid "modals.move-shared-confirm.accept" msgid_plural "modals.move-shared-confirm.accept" From 48e8c0bc65bbebf14a643ace078287f875d3de95 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 7 Apr 2026 14:27:26 +0200 Subject: [PATCH 094/288] :bug: Fix show resolved value instead of value (#8883) --- .../ui/workspace/tokens/management/forms/controls/utils.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs index 8127dc2f1e..904f8c811e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs @@ -9,7 +9,7 @@ [token] {:id (str (get token :id)) :type :token - :resolved-value (get token :value) + :resolved-value (or (get token :resolved-value) (get token :value)) :name (get token :name)}) (defn- generate-dropdown-options From 0c08dfb13d46ec3dc277003b61137c0f3ef0ddb2 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:30:47 -0400 Subject: [PATCH 095/288] :sparkles: Add the ability for save and restore selection state in undo/redo (#8652) * :sparkles: Capture selection state before changes are applied Save current selection IDs in commit-changes so undo entries can track what was selected before each action. * :sparkles: Save and restore selection state in undo/redo Extend undo entry with selected-before and selected-after fields. On undo, restore selection to what it was before the action. On redo, restore selection to what it was after the action. Handles single entries, stacked entries, accumulated transactions, and undo groups. Fixes #6007 * :recycle: Wire selected-before through workspace undo stream Pass the captured selection state from commit data into the undo entry so it is stored alongside changes. * :bug: Fix unmatched delimiter in changes.cljs * :bug: Pass selected-before through commit event to undo entry selected-before was captured in commit-changes but dropped by the commit function since it was missing from the destructuring and the commit map. This caused restore-selection to receive nil on undo. --------- Signed-off-by: eureka928 Co-authored-by: Mihai --- frontend/src/app/main/data/changes.cljs | 32 +++++----- frontend/src/app/main/data/workspace.cljs | 5 +- .../src/app/main/data/workspace/undo.cljs | 58 +++++++++++++------ 3 files changed, 62 insertions(+), 33 deletions(-) diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs index 7cae1add0f..042f943e2c 100644 --- a/frontend/src/app/main/data/changes.cljs +++ b/frontend/src/app/main/data/changes.cljs @@ -122,7 +122,8 @@ (defn commit "Create a commit event instance" [{:keys [commit-id redo-changes undo-changes origin save-undo? features - file-id file-revn file-vern undo-group tags stack-undo? source ignore-wasm?]}] + file-id file-revn file-vern undo-group tags stack-undo? source ignore-wasm? + selected-before]}] (assert (cpc/check-changes redo-changes) "expect valid vector of changes for redo-changes") @@ -148,7 +149,8 @@ :undo-group undo-group :tags tags :stack-undo? stack-undo? - :ignore-wasm? ignore-wasm?}] + :ignore-wasm? ignore-wasm? + :selected-before selected-before}] (ptk/reify ::commit cljs.core/IDeref @@ -205,15 +207,17 @@ ;; Prevent commit changes by a viewer team member (it really should never happen) (when (:can-edit permissions) - (rx/of (-> params - (assoc :undo-group undo-group) - (assoc :features features) - (assoc :tags tags) - (assoc :stack-undo? stack-undo?) - (assoc :save-undo? save-undo?) - (assoc :file-id file-id) - (assoc :file-revn (resolve-file-revn state file-id)) - (assoc :file-vern (resolve-file-vern state file-id)) - (assoc :undo-changes uchg) - (assoc :redo-changes rchg) - (commit)))))))) + (let [selected (dm/get-in state [:workspace-local :selected])] + (rx/of (-> params + (assoc :undo-group undo-group) + (assoc :features features) + (assoc :tags tags) + (assoc :stack-undo? stack-undo?) + (assoc :save-undo? save-undo?) + (assoc :file-id file-id) + (assoc :file-revn (resolve-file-revn state file-id)) + (assoc :file-vern (resolve-file-vern state file-id)) + (assoc :undo-changes uchg) + (assoc :redo-changes rchg) + (assoc :selected-before selected) + (commit))))))))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index dd03d7601e..35677f3785 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -484,12 +484,13 @@ (rx/filter dch/commit?) (rx/map deref) (rx/mapcat - (fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}] + (fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo? selected-before]}] (if (and save-undo? (seq undo-changes)) (let [entry {:undo-changes undo-changes :redo-changes redo-changes :undo-group undo-group - :tags tags}] + :tags tags + :selected-before selected-before}] (rx/of (dwu/append-undo entry stack-undo?))) (rx/empty)))))) (rx/take-until stoper-s)))) diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs index 2b2c6f048b..2296aed447 100644 --- a/frontend/src/app/main/data/workspace/undo.cljs +++ b/frontend/src/app/main/data/workspace/undo.cljs @@ -60,7 +60,9 @@ [:undo-changes [:vector cpc/schema:change]] [:redo-changes [:vector cpc/schema:change]] [:undo-group ::sm/uuid] - [:tags [:set :keyword]]]) + [:tags [:set :keyword]] + [:selected-before {:optional true} [:maybe [:set ::sm/uuid]]] + [:selected-after {:optional true} [:maybe [:set ::sm/uuid]]]]) (def check-undo-entry (sm/check-fn schema:undo-entry)) @@ -103,24 +105,28 @@ (defn- stack-undo-entry "Extends the current undo entry in the workspace with new changes if it exists, or creates a new entry if it doesn't." - [state {:keys [undo-changes redo-changes] :as entry}] + [state {:keys [undo-changes redo-changes selected-after] :as entry}] (let [index (get-in state [:workspace-undo :index] -1)] (if (>= index 0) (update-in state [:workspace-undo :items index] (fn [item] (-> item (update :undo-changes #(into undo-changes %)) - (update :redo-changes #(into % redo-changes))))) + (update :redo-changes #(into % redo-changes)) + (assoc :selected-after selected-after)))) (add-undo-entry state entry)))) (defn- accumulate-undo-entry "Extends the current undo transaction with new changes." - [state {:keys [undo-changes redo-changes undo-group tags]}] + [state {:keys [undo-changes redo-changes undo-group tags selected-before selected-after]}] (-> state (update-in [:workspace-undo :transaction :undo-changes] #(into undo-changes %)) (update-in [:workspace-undo :transaction :redo-changes] #(into % redo-changes)) (cond-> (nil? (get-in state [:workspace-undo :transaction :undo-group])) (assoc-in [:workspace-undo :transaction :undo-group] undo-group)) + (cond-> (nil? (get-in state [:workspace-undo :transaction :selected-before])) + (assoc-in [:workspace-undo :transaction :selected-before] selected-before)) + (assoc-in [:workspace-undo :transaction :selected-after] selected-after) (assoc-in [:workspace-undo :transaction :tags] tags))) (defn append-undo @@ -137,18 +143,20 @@ (ptk/reify ::append-undo ptk/UpdateEvent (update [_ state] - (cond - (and (get-in state [:workspace-undo :transaction]) - (or (not stack?) - (d/not-empty? (get-in state [:workspace-undo :transaction :undo-changes])) - (d/not-empty? (get-in state [:workspace-undo :transaction :redo-changes])))) - (accumulate-undo-entry state entry) + (let [selected-after (dm/get-in state [:workspace-local :selected]) + entry (assoc entry :selected-after selected-after)] + (cond + (and (get-in state [:workspace-undo :transaction]) + (or (not stack?) + (d/not-empty? (get-in state [:workspace-undo :transaction :undo-changes])) + (d/not-empty? (get-in state [:workspace-undo :transaction :redo-changes])))) + (accumulate-undo-entry state entry) - stack? - (stack-undo-entry state entry) + stack? + (stack-undo-entry state entry) - :else - (add-undo-entry state entry))))) + :else + (add-undo-entry state entry)))))) (def empty-tx {:undo-changes [] :redo-changes []}) @@ -234,6 +242,16 @@ (rx/map first) (rx/map commit-undo-transaction)))))) +(defn- restore-selection + "Restores the selection state from an undo entry." + [selected-ids] + (ptk/reify ::restore-selection + ptk/UpdateEvent + (update [_ state] + (if (some? selected-ids) + (assoc-in state [:workspace-local :selected] selected-ids) + state)))) + (defn undo-to-index "Repeat undoing or redoing until dest-index is reached." [dest-index] @@ -302,12 +320,15 @@ (find-first-group-idx index))] (if undo-group - (rx/of (undo-to-index (dec undo-group-index))) + (let [first-item (get items undo-group-index)] + (rx/of (undo-to-index (dec undo-group-index)) + (restore-selection (:selected-before first-item)))) (rx/of (materialize-undo changes (dec index)) (dch/commit-changes {:redo-changes changes :undo-changes [] :save-undo? false :origin it}) + (restore-selection (:selected-before item)) (assure-valid-current-page))))))))))) (def redo @@ -337,12 +358,15 @@ redo-group-index (when undo-group (find-last-group-idx (inc index)))] (if undo-group - (rx/of (undo-to-index redo-group-index)) + (let [last-item (get items redo-group-index)] + (rx/of (undo-to-index redo-group-index) + (restore-selection (:selected-after last-item)))) (rx/of (materialize-undo changes (inc index)) (dch/commit-changes {:redo-changes changes :undo-changes [] :origin it - :save-undo? false}))))))))))) + :save-undo? false}) + (restore-selection (:selected-after item)))))))))))) (defn- assure-valid-current-page [] From 0b0e193b70741656f517634428cf2f8352faac58 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 8 Apr 2026 10:21:32 +0200 Subject: [PATCH 096/288] :bug: Fix problem with text auto grow in layouts --- frontend/src/app/main/data/workspace/texts.cljs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 43ff2a71af..46ae233a2b 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -1049,16 +1049,15 @@ content-has-text? has-prev-content?) (dissoc :prev-content)) + (cond-> (and (not new-shape?) prev-content-has-text? (not content-has-text?) (not finalize?)) (assoc :prev-content prev-content)) + (cond-> (and update-name? (some? name)) - (assoc :name name)) - (cond-> (some? new-size) - (gsh/transform-shape - (ctm/change-size shape (:width new-size) (:height new-size)))))) + (assoc :name name)))) {:save-undo? finalize-save-undo-first? :stack-undo? effective-stack-undo? :undo-group (when new-shape? id)}) From b8be89f23108cc91a5677fd37e0888012d24be48 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 8 Apr 2026 11:00:59 +0200 Subject: [PATCH 097/288] :bug: Update onboarding image (#8902) --- CHANGES.md | 1 + frontend/resources/images/newsletter-notification.svg | 1 + frontend/src/app/main/ui/onboarding/newsletter.cljs | 3 +-- frontend/src/app/main/ui/onboarding/newsletter.scss | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 frontend/resources/images/newsletter-notification.svg diff --git a/CHANGES.md b/CHANGES.md index 7e52212519..ec15ccc3c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -34,6 +34,7 @@ - Fix focus radio button [Taiga #13841](https://tree.taiga.io/project/penpot/issue/13841) - Token tree should be expanded by default [Taiga #13631](https://tree.taiga.io/project/penpot/issue/13631) - Fix opacity incorrectly disabled for visible shapes [Taiga #13906](https://tree.taiga.io/project/penpot/issue/13906) +- Update onboarding image [Taiga #13864](https://tree.taiga.io/project/penpot/issue/13864) ## 2.15.0 (Unreleased) diff --git a/frontend/resources/images/newsletter-notification.svg b/frontend/resources/images/newsletter-notification.svg new file mode 100644 index 0000000000..395e291284 --- /dev/null +++ b/frontend/resources/images/newsletter-notification.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/main/ui/onboarding/newsletter.cljs b/frontend/src/app/main/ui/onboarding/newsletter.cljs index 3f5ee18557..8b32e224a4 100644 --- a/frontend/src/app/main/ui/onboarding/newsletter.cljs +++ b/frontend/src/app/main/ui/onboarding/newsletter.cljs @@ -56,8 +56,7 @@ [:div.animated.fadeInDown {:class (stl/css :modal-container)} [:div {:class (stl/css :modal-left)} - [:img {:src "images/deco-newsletter.png" - :border "0"}]] + [:img {:src "images/newsletter-notification.svg"}]] [:div {:class (stl/css :modal-right)} [:h2 {:class (stl/css :modal-title) diff --git a/frontend/src/app/main/ui/onboarding/newsletter.scss b/frontend/src/app/main/ui/onboarding/newsletter.scss index cb128fda7a..a496bec95b 100644 --- a/frontend/src/app/main/ui/onboarding/newsletter.scss +++ b/frontend/src/app/main/ui/onboarding/newsletter.scss @@ -33,6 +33,7 @@ img { width: deprecated.$s-172; border-radius: deprecated.$br-8 0 0 deprecated.$br-8; + height: auto; } } From 10cfd995257a9d80e3fe724767d35e660087b1f2 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Wed, 8 Apr 2026 15:44:09 +0200 Subject: [PATCH 098/288] :bug: Fix lint invalid CSS props (#8907) * :bug: Fix lint invalid CSS props * :bug: Fix named colors in favor of modern notation or custom properties * :bug: Removed multiple combined selectors * :bug: Convert alpha value to numeric --- frontend/src/app/main/ui/comments.scss | 2 +- .../app/main/ui/components/button_link.scss | 2 +- .../src/app/main/ui/dashboard/comments.scss | 2 +- .../src/app/main/ui/dashboard/sidebar.scss | 1 - .../src/app/main/ui/dashboard/templates.scss | 12 ++--- .../src/app/main/ui/ds/buttons/_buttons.scss | 4 +- frontend/src/app/main/ui/ds/typography.scss | 20 ++++---- .../src/app/main/ui/ds/utilities/swatch.scss | 4 +- frontend/src/app/main/ui/exports/assets.scss | 2 +- frontend/src/app/main/ui/exports/files.scss | 2 +- frontend/src/app/main/ui/settings.scss | 6 +-- .../src/app/main/ui/settings/profile.scss | 4 +- frontend/src/app/main/ui/static.scss | 8 ++-- frontend/src/app/main/ui/viewer.scss | 2 +- .../app/main/ui/workspace/colorpicker.scss | 4 +- .../main/ui/workspace/colorpicker/ramp.scss | 4 +- .../app/main/ui/workspace/right_header.scss | 2 +- .../ui/workspace/shapes/text/v2_editor.scss | 3 +- .../viewport/grid_layout_editor.scss | 4 +- .../main/ui/workspace/viewport/presence.scss | 2 +- .../app/main/ui/workspace/viewport_wasm.scss | 2 +- frontend/stylelint.config.mjs | 48 +++++++++---------- 22 files changed, 70 insertions(+), 70 deletions(-) diff --git a/frontend/src/app/main/ui/comments.scss b/frontend/src/app/main/ui/comments.scss index 546cfa0af4..79abfc420f 100644 --- a/frontend/src/app/main/ui/comments.scss +++ b/frontend/src/app/main/ui/comments.scss @@ -116,7 +116,7 @@ } .avatar-darken { - background: rgb(0 0 0 / 50%); + background: rgb(0 0 0 / 0.5); } .cover { diff --git a/frontend/src/app/main/ui/components/button_link.scss b/frontend/src/app/main/ui/components/button_link.scss index b8b598f305..b3693cbdb0 100644 --- a/frontend/src/app/main/ui/components/button_link.scss +++ b/frontend/src/app/main/ui/components/button_link.scss @@ -12,7 +12,7 @@ border: none; cursor: pointer; display: flex; - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; justify-content: center; min-width: 25px; padding: 0 1rem; diff --git a/frontend/src/app/main/ui/dashboard/comments.scss b/frontend/src/app/main/ui/dashboard/comments.scss index 9c1fc9b244..71f6fe7ebd 100644 --- a/frontend/src/app/main/ui/dashboard/comments.scss +++ b/frontend/src/app/main/ui/dashboard/comments.scss @@ -53,7 +53,7 @@ height: deprecated.$s-8; border: deprecated.$s-2 solid var(--color-background-tertiary); border-radius: 50%; - background: red; + background: var(--color-foreground-error); top: deprecated.$s-6; right: deprecated.$s-6; } diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index f2191747b4..922f6e99e7 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -255,7 +255,6 @@ .sidebar-nav { margin: 0; user-select: none; - overflow: none; } .pinned-projects { diff --git a/frontend/src/app/main/ui/dashboard/templates.scss b/frontend/src/app/main/ui/dashboard/templates.scss index 583e87d6ad..4ca5d92e47 100644 --- a/frontend/src/app/main/ui/dashboard/templates.scss +++ b/frontend/src/app/main/ui/dashboard/templates.scss @@ -19,17 +19,17 @@ flex-direction: column; height: px2rem(244); justify-content: flex-end; - margin-inline: px2rem(6); - margin-block-end: px2rem(6); + margin-inline: var(--sp-s); + margin-block-end: var(--sp-xs); position: absolute; - transition: bottom 300ms; + transition: inset-block-end 300ms; width: calc(100% - $sz-12); pointer-events: none; &.collapsed { inset-block-end: calc(-1 * px2rem(228)); background-color: transparent; - transition: bottom 300ms; + transition: inset-block-end 300ms; .title-btn { border-end-end-radius: $br-8; @@ -207,7 +207,7 @@ width: 100%; height: px2rem(136); margin-block-end: var(--sp-s); - border-radius: px2rem(5); + border-radius: $br-6; display: flex; justify-content: center; flex-direction: column; @@ -218,7 +218,7 @@ } .card-name { - padding: 0 px2rem(6); + padding: 0 var(--sp-s); display: flex; justify-content: space-between; height: $sz-24; diff --git a/frontend/src/app/main/ui/ds/buttons/_buttons.scss b/frontend/src/app/main/ui/ds/buttons/_buttons.scss index a3001d8311..41c9474839 100644 --- a/frontend/src/app/main/ui/ds/buttons/_buttons.scss +++ b/frontend/src/app/main/ui/ds/buttons/_buttons.scss @@ -72,7 +72,7 @@ &:active, &[aria-pressed="true"] { - box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgb(0 0 0 / 20%); + box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgb(0 0 0 / 0.2); } } @@ -122,6 +122,6 @@ &:active, &[aria-pressed="true"] { - box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgb(0 0 0 / 20%); + box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgb(0 0 0 / 0.2); } } diff --git a/frontend/src/app/main/ui/ds/typography.scss b/frontend/src/app/main/ui/ds/typography.scss index 7f27419cd0..36b4086fba 100644 --- a/frontend/src/app/main/ui/ds/typography.scss +++ b/frontend/src/app/main/ui/ds/typography.scss @@ -20,7 +20,7 @@ $_fs-24: px2rem(24); $_fs-36: px2rem(36); @mixin _font-style-display { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -28,7 +28,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-title-large { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -36,7 +36,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-title-medium { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -44,7 +44,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-title-small { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-dense; @@ -52,7 +52,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-headline-large { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -61,7 +61,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-headline-medium { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -70,7 +70,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-headline-small { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-medium; line-height: $_font-lineheight-dense; @@ -79,7 +79,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-body-large { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -87,7 +87,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-body-medium { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-normal; @@ -95,7 +95,7 @@ $_fs-36: px2rem(36); } @mixin _font-style-body-small { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-optical-sizing: auto; font-weight: $_font-weight-regular; line-height: $_font-lineheight-compact; diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.scss b/frontend/src/app/main/ui/ds/utilities/swatch.scss index 9daa4d9141..3052f5f6c3 100644 --- a/frontend/src/app/main/ui/ds/utilities/swatch.scss +++ b/frontend/src/app/main/ui/ds/utilities/swatch.scss @@ -11,7 +11,7 @@ @property --solid-color-overlay { syntax: ""; inherits: false; - initial-value: rgb(0 0 0 / 0%); + initial-value: rgb(0 0 0 / 0); } .swatch { @@ -19,7 +19,7 @@ --border-radius: #{$br-4}; --border-color-active: var(--color-foreground-primary); --border-color-active-inset: var(--color-background-primary); - --checkerboard-background: repeating-conic-gradient(lightgray 0% 25%, white 0% 50%); + --checkerboard-background: repeating-conic-gradient(rgb(212 212 212) 0% 25%, rgb(255 255 255) 0% 50%); --checkerboard-size: 0.5rem 0.5rem; border: $b-1 solid var(--border-color); diff --git a/frontend/src/app/main/ui/exports/assets.scss b/frontend/src/app/main/ui/exports/assets.scss index 4a8b358930..338daf19e7 100644 --- a/frontend/src/app/main/ui/exports/assets.scss +++ b/frontend/src/app/main/ui/exports/assets.scss @@ -196,7 +196,7 @@ left: 0; width: 100%; height: 50px; - background: linear-gradient(to top, rgb(24 24 26 / 100%) 0%, rgb(24 24 26 / 0%) 100%); + background: linear-gradient(to top, rgb(24 24 26 / 1) 0%, rgb(24 24 26 / 0) 100%); content: ""; pointer-events: none; } diff --git a/frontend/src/app/main/ui/exports/files.scss b/frontend/src/app/main/ui/exports/files.scss index 9a149abc4c..ed33acb665 100644 --- a/frontend/src/app/main/ui/exports/files.scss +++ b/frontend/src/app/main/ui/exports/files.scss @@ -92,7 +92,7 @@ left: 0; width: 100%; height: 50px; - background: linear-gradient(to top, rgb(24 24 26 / 100%) 0%, rgb(24 24 26 / 0%) 100%); + background: linear-gradient(to top, rgb(24 24 26 / 1) 0%, rgb(24 24 26 / 0) 100%); content: ""; pointer-events: none; } diff --git a/frontend/src/app/main/ui/settings.scss b/frontend/src/app/main/ui/settings.scss index 4ee10be87e..91cbc781a5 100644 --- a/frontend/src/app/main/ui/settings.scss +++ b/frontend/src/app/main/ui/settings.scss @@ -205,13 +205,13 @@ margin-bottom: deprecated.$s-32; .newsletter-title { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; color: var(--color-foreground-secondary); font-size: deprecated.$fs-14; } label { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; color: var(--color-background-primary); font-size: deprecated.$fs-12; margin-right: calc(-1 * deprecated.$s-16); @@ -219,7 +219,7 @@ } .info { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; color: var(--color-foreground-secondary); font-size: deprecated.$fs-12; margin-bottom: deprecated.$s-8; diff --git a/frontend/src/app/main/ui/settings/profile.scss b/frontend/src/app/main/ui/settings/profile.scss index 21249db182..115c33864a 100644 --- a/frontend/src/app/main/ui/settings/profile.scss +++ b/frontend/src/app/main/ui/settings/profile.scss @@ -311,13 +311,13 @@ form.avatar-form { margin-bottom: $s-32; .newsletter-title { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; color: var(--color-foreground-secondary); font-size: $fs-14; } label { - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; color: var(--color-background-primary); font-size: $fs-12; margin-right: calc(-1 * $s-16); diff --git a/frontend/src/app/main/ui/static.scss b/frontend/src/app/main/ui/static.scss index 5f4c768792..7a5a694402 100644 --- a/frontend/src/app/main/ui/static.scss +++ b/frontend/src/app/main/ui/static.scss @@ -238,7 +238,7 @@ top: 0; left: 0; z-index: 100; - background-color: rgb(0 0 0 / 65%); + background-color: rgb(0 0 0 / 0.65); display: flex; justify-content: center; align-items: center; @@ -348,8 +348,10 @@ margin: deprecated.$s-20 0; } - form div { - margin-bottom: deprecated.$s-8; + form { + div { + margin-bottom: deprecated.$s-8; + } } } } diff --git a/frontend/src/app/main/ui/viewer.scss b/frontend/src/app/main/ui/viewer.scss index d4fab6c26f..b64ebe2b79 100644 --- a/frontend/src/app/main/ui/viewer.scss +++ b/frontend/src/app/main/ui/viewer.scss @@ -173,7 +173,7 @@ left: 0; &.visible { - background-color: rgb(0 0 0 / 20%); + background-color: rgb(0 0 0 / 0.2); } } diff --git a/frontend/src/app/main/ui/workspace/colorpicker.scss b/frontend/src/app/main/ui/workspace/colorpicker.scss index a68182b24a..5de25d32a1 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker.scss @@ -122,7 +122,7 @@ background: linear-gradient(180deg, var(--color-foreground-secondary), transparent); &.selected { - background: linear-gradient(to bottom, rgb(126 255 245 / 100%) 0%, rgb(126 255 245 / 20%) 100%); + background: linear-gradient(to bottom, rgb(126 255 245 / 1) 0%, rgb(126 255 245 / 0.2) 100%); border: $b-2 solid var(--colorpicker-details-color-selected); } } @@ -131,7 +131,7 @@ background: radial-gradient(transparent, var(--color-foreground-secondary)); &.selected { - background: radial-gradient(rgb(126 255 245 / 100%) 0%, rgb(126 255 245 / 20%) 100%); + background: radial-gradient(rgb(126 255 245 / 1) 0%, rgb(126 255 245 / 0.2) 100%); border: $b-2 solid var(--colorpicker-details-color-selected); } } diff --git a/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss b/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss index e5ba77ec55..952f6f344b 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss @@ -20,7 +20,7 @@ position: absolute; width: 100%; height: 100%; - background: linear-gradient(to right, #fff, rgb(255 255 255 / 0%)); + background: linear-gradient(to right, #fff, rgb(255 255 255 / 0)); } &::after { @@ -28,7 +28,7 @@ position: absolute; width: 100%; height: 100%; - background: linear-gradient(to top, #000, rgb(0 0 0 / 0%)); + background: linear-gradient(to top, #000, rgb(0 0 0 / 0)); } } diff --git a/frontend/src/app/main/ui/workspace/right_header.scss b/frontend/src/app/main/ui/workspace/right_header.scss index fd40e13768..2f6bbbeb52 100644 --- a/frontend/src/app/main/ui/workspace/right_header.scss +++ b/frontend/src/app/main/ui/workspace/right_header.scss @@ -253,7 +253,7 @@ height: 8px; border: 2px solid var(--color-background-tertiary); border-radius: 50%; - background: red; + background: var(--color-foreground-error); top: 6px; right: 6px; } diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss index 753a30ea46..b06ed24005 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss +++ b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss @@ -17,7 +17,7 @@ .text-editor-content { height: 100%; - font-family: sourcesanspro, sans-serif; + font-family: "sourcesanspro", sans-serif; outline: none; user-select: text; white-space: pre-wrap; @@ -53,7 +53,6 @@ display: inline; line-height: inherit; caret-color: var(--text-editor-caret-color); - white-space-collapse: pre; word-break: normal; overflow-wrap: break-word; tab-size: 2; diff --git a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss index 5225f42f27..a2f8f0b345 100644 --- a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss +++ b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss @@ -14,7 +14,7 @@ .marker-text { fill: var(--app-white); font-size: calc(deprecated.$s-12 / var(--zoom)); - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; } } @@ -37,7 +37,7 @@ background: none; border: 0; color: var(--grid-editor-marker-text); - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-size: calc(deprecated.$fs-12 / var(--zoom)); font-weight: 400; margin: 0; diff --git a/frontend/src/app/main/ui/workspace/viewport/presence.scss b/frontend/src/app/main/ui/workspace/viewport/presence.scss index e429851ac2..32486a0244 100644 --- a/frontend/src/app/main/ui/workspace/viewport/presence.scss +++ b/frontend/src/app/main/ui/workspace/viewport/presence.scss @@ -8,7 +8,7 @@ .profile-name { width: fit-content; - font-family: worksans, vazirmatn, sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; padding: 2px 12px; border-radius: deprecated.$br-4; display: flex; diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.scss b/frontend/src/app/main/ui/workspace/viewport_wasm.scss index 44cb86ce00..99af2bee48 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.scss +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.scss @@ -37,7 +37,7 @@ position: fixed; inset: 0; z-index: 100; - background-color: rgb(0 0 0 / 50%); + background-color: rgb(0 0 0 / 0.5); display: grid; place-items: center; cursor: default; diff --git a/frontend/stylelint.config.mjs b/frontend/stylelint.config.mjs index 18bd1e592b..d0b144242f 100644 --- a/frontend/stylelint.config.mjs +++ b/frontend/stylelint.config.mjs @@ -30,38 +30,38 @@ export default { // TODO: Enable rules secuentially // // Using quotes - // "font-family-name-quotes": "always-unless-keyword", - // "function-url-quotes": "always", - // "selector-attribute-quotes": "always", + "font-family-name-quotes": "always-unless-keyword", + "function-url-quotes": "always", + "selector-attribute-quotes": "always", // // Disallow vendor prefixes - // "at-rule-no-vendor-prefix": true, - // "media-feature-name-no-vendor-prefix": true, - // "property-no-vendor-prefix": true, - // "selector-no-vendor-prefix": true, - // "value-no-vendor-prefix": true, + "at-rule-no-vendor-prefix": true, + "media-feature-name-no-vendor-prefix": true, + "property-no-vendor-prefix": true, + "selector-no-vendor-prefix": true, + "value-no-vendor-prefix": true, // // Specificity - // "no-descending-specificity": null, + "no-descending-specificity": null, // "max-nesting-depth": 3, - // "selector-max-compound-selectors": 3, - // "selector-max-specificity": "1,2,1", + "selector-max-compound-selectors": 3, + "selector-max-specificity": "1,2,1", // // Miscellanea - // "color-named": "never", + "color-named": "never", // "declaration-no-important": true, - // "declaration-property-unit-allowed-list": { - // "font-size": ["rem"], - // "/^animation/": ["s"], - // }, + "declaration-property-unit-allowed-list": { + "font-size": ["rem"], + "/^animation/": ["s"], + }, // // 'order/properties-alphabetical-order': true, - // "selector-max-type": 1, - // "selector-type-no-unknown": true, + "selector-max-type": 1, + "selector-type-no-unknown": true, // // Notation - // "font-weight-notation": "numeric", + "font-weight-notation": "numeric", // // URLs - // "function-url-no-scheme-relative": true, + "function-url-no-scheme-relative": true, // "liberty/use-logical-spec": "always", - // "selector-class-pattern": null, - // "alpha-value-notation": null, - // "color-function-notation": null, - // "value-keyword-case": null, + "selector-class-pattern": null, + "alpha-value-notation": "number", + "color-function-notation": "modern", + "value-keyword-case": "lower", }, }; From 5502fe8df3955ee70b84c56a224fde1f11a93712 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:05:53 -0400 Subject: [PATCH 099/288] :books: Add changelog entry for undo/redo selection state (#8896) --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index ec15ccc3c0..969db06cc3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,7 @@ - Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713) - Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466) - Add CSS linter [Taiga #13790](https://tree.taiga.io/project/penpot/us/13790) +- Save and restore selection state in undo/redo (by @eureka928) [Github #6007](https://github.com/penpot/penpot/issues/6007) ### :bug: Bugs fixed From 62b59991a9bd4bbd06f531895aa43402379c6dc8 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 9 Apr 2026 09:16:28 +0200 Subject: [PATCH 100/288] :wrench: Add guard to apply-token (#8879) --- common/src/app/common/files/tokens.cljc | 14 +++++--------- .../main/data/workspace/tokens/application.cljs | 6 ++++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/common/src/app/common/files/tokens.cljc b/common/src/app/common/files/tokens.cljc index 071b28a4e7..cd6dc44cb2 100644 --- a/common/src/app/common/files/tokens.cljc +++ b/common/src/app/common/files/tokens.cljc @@ -314,16 +314,12 @@ {:value parsed-value :unit unit})))) -;; FIXME: looks very redundant function -(defn token-identifier - [{:keys [name] :as _token}] - name) - (defn attributes-map - "Creats an attributes map using collection of `attributes` for `id`." + "Creates an attributes map using collection of `attributes` for `id`." [attributes token] - (->> (map (fn [attr] [attr (token-identifier token)]) attributes) - (into {}))) + (into {} + (map (fn [attr] [attr (:name token)])) + attributes)) (defn remove-attributes-for-token "Removes applied tokens with `token-name` for the given `attributes` set from `applied-tokens`." @@ -339,7 +335,7 @@ "Test if `token` is applied to a `shape` on single `token-attribute`." [token shape token-attribute] (when-let [id (dm/get-in shape [:applied-tokens token-attribute])] - (= (token-identifier token) id))) + (= (:name token) id))) (defn token-applied? "Test if `token` is applied to a `shape` with at least one of the given `token-attributes`." diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index 9c79d40260..89cccdd869 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -656,6 +656,7 @@ this is useful for applying a single attribute from an attributes set while removing other applied tokens from this set." [{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}] + (assert (ctob/token? token) "apply-token event requires a valid token") (ptk/reify ::apply-token ptk/WatchEvent (watch [_ state _] @@ -667,9 +668,10 @@ text-editing? (and (some? edition) (= :text (:type (get objects edition))))] (if (and (empty? (get state :workspace-editor-state)) + (some? token) (not text-editing?)) (let [attributes-to-remove - ;; Remove atomic typography tokens when applying composite and vice-verca + ;; Remove atomic typography tokens when applying composite and vice-versa (cond (ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys) (ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys) @@ -696,7 +698,7 @@ shape-ids (d/nilv (keys shapes) []) any-variant? (->> shapes vals (some ctk/is-variant?) boolean) - resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value]) + resolved-value (get-in resolved-tokens [(:name token) :resolved-value]) resolved-value (if (contains? cf/flags :tokenscript) (ts/tokenscript-symbols->penpot-unit resolved-value) resolved-value) From e51e0c7933a7aeaf43b662ab3fd572b04be15ca4 Mon Sep 17 00:00:00 2001 From: Juanfran Date: Wed, 8 Apr 2026 12:26:50 +0200 Subject: [PATCH 101/288] :sparkles: Add theme field to nitrate authenticate response --- backend/src/app/rpc/management/nitrate.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 62c0070364..1258feb6ba 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -35,7 +35,8 @@ {:id (get profile :id) :name (get profile :fullname) :email (get profile :email) - :photo-url (files/resolve-public-uri (get profile :photo-id))})) + :photo-url (files/resolve-public-uri (get profile :photo-id)) + :theme (get profile :theme)})) ;; ---- API: get-teams From 21217c5622447edfbe6d8b0821da11eec6be33d4 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Thu, 9 Apr 2026 03:32:56 -0400 Subject: [PATCH 102/288] :sparkles: Add per-group add button for typographies (#8895) * :sparkles: Add per-group add button for typographies Add a "+" button to each typography group header, allowing users to create new typographies directly inside a group instead of only at the top level. The button only appears for local, editable files. Closes #5275 * :books: Add changelog entry for typography group add button * :bug: Fix typography group title button layout wrapping * :recycle: Address review feedback for typography group add button Signed-off-by: eureka928 --- CHANGES.md | 1 + .../src/app/main/data/workspace/texts.cljs | 79 ++++++++++--------- .../ui/workspace/sidebar/assets/groups.cljs | 7 +- .../ui/workspace/sidebar/assets/groups.scss | 1 + .../sidebar/assets/typographies.cljs | 21 +++-- 5 files changed, 66 insertions(+), 43 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ac952bd18c..09afc6beb2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ - Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466) - Add CSS linter [Taiga #13790](https://tree.taiga.io/project/penpot/us/13790) - Save and restore selection state in undo/redo (by @eureka928) [Github #6007](https://github.com/penpot/penpot/issues/6007) +- Add per-group add button for typographies (by @eureka928) [Github #5275](https://github.com/penpot/penpot/issues/5275) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 46ae233a2b..68c389d467 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -859,50 +859,55 @@ "A higher level version of dwl/add-typography, and has mainly two responsabilities: add the typography to the library and apply it to the currently selected text shapes (being aware of the open text - editors." - [file-id] - (ptk/reify ::add-typography - ptk/WatchEvent - (watch [_ state _] - (let [selected (dsh/lookup-selected state) - objects (dsh/lookup-page-objects state) + editors. + Optionally accepts a group-path to place the new typography inside + a specific group." + ([file-id] (add-typography file-id nil)) + ([file-id group-path] + (ptk/reify ::add-typography + ptk/WatchEvent + (watch [_ state _] + (let [selected (dsh/lookup-selected state) + objects (dsh/lookup-page-objects state) - xform (comp (keep (d/getf objects)) - (filter cfh/text-shape?)) - shapes (into [] xform selected) - shape (first shapes) + xform (comp (keep (d/getf objects)) + (filter cfh/text-shape?)) + shapes (into [] xform selected) + shape (first shapes) - values (current-text-values - {:editor-state (dm/get-in state [:workspace-editor-state (:id shape)]) - :shape shape - :attrs txt/text-node-attrs}) + values (current-text-values + {:editor-state (dm/get-in state [:workspace-editor-state (:id shape)]) + :shape shape + :attrs txt/text-node-attrs}) - multiple? (or (> 1 (count shapes)) - (d/seek (partial = :multiple) - (vals values))) + multiple? (or (> 1 (count shapes)) + (d/seek (partial = :multiple) + (vals values))) - values (-> (d/without-nils values) - (select-keys - (d/concat-vec txt/text-font-attrs - txt/text-spacing-attrs - txt/text-transform-attrs))) + values (-> (d/without-nils values) + (select-keys + (d/concat-vec txt/text-font-attrs + txt/text-spacing-attrs + txt/text-transform-attrs))) - typ-id (uuid/next) - typ (-> (if multiple? - txt/default-typography - (merge txt/default-typography values)) - (generate-typography-name) - (assoc :id typ-id))] + typ-id (uuid/next) + typ (-> (if multiple? + txt/default-typography + (merge txt/default-typography values)) + (generate-typography-name) + (assoc :id typ-id) + (cond-> (string? group-path) + (update :name #(str group-path " / " %))))] - (rx/concat - (rx/of (dwl/add-typography typ) - (ptk/event ::ev/event {::ev/name "add-asset-to-library" - :asset-type "typography"})) + (rx/concat + (rx/of (dwl/add-typography typ) + (ptk/event ::ev/event {::ev/name "add-asset-to-library" + :asset-type "typography"})) - (when (not multiple?) - (rx/of (update-attrs (:id shape) - {:typography-ref-id typ-id - :typography-ref-file file-id})))))))) + (when (not multiple?) + (rx/of (update-attrs (:id shape) + {:typography-ref-id typ-id + :typography-ref-file file-id}))))))))) ;; -- New Editor diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs index fa81b21157..3bc5fe58f1 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs @@ -23,7 +23,7 @@ [rumext.v2 :as mf])) (mf/defc asset-group-title* - [{:keys [file-id section path is-group-open on-rename on-ungroup on-group-combine-variants is-can-combine]}] + [{:keys [file-id section path is-group-open on-rename on-ungroup on-group-combine-variants is-can-combine on-add]}] (when-not (empty? path) (let [[other-path last-path truncated] (cpn/compact-path path 35 true) menu-state (mf/use-state cmm/initial-context-menu-state) @@ -76,6 +76,11 @@ :handler #(on-group-combine-variants path)}))}]] [:div {:class (stl/css :title-menu)} + (when on-add + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.assets.typography.add-typography") + :on-click on-add + :icon i/add}]) [:> icon-button* {:variant "ghost" :aria-label (tr "workspace.assets.component-group-options") :on-click on-context-menu diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss index a2db8f408b..12a50ea556 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss @@ -25,6 +25,7 @@ } .title-menu { + display: flex; visibility: hidden; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs index d222ffafd0..d9665d4bba 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs @@ -133,7 +133,7 @@ {::mf/wrap-props false} [{:keys [file-id prefix groups open-groups force-open? file local? selected local-data editing-id renaming-id on-asset-click handle-change on-rename-group - on-ungroup on-context-menu selected-full]}] + on-ungroup on-context-menu selected-full is-read-only]}] (let [group-open? (if (false? (get open-groups prefix)) ;; if the user has closed it specifically, respect that false (get open-groups prefix true)) @@ -164,7 +164,14 @@ (mf/use-fn (mf/deps dragging* prefix selected-paths selected-full move-typography) (fn [event] - (cmm/on-drop-asset-group event dragging* prefix selected-paths selected-full move-typography)))] + (cmm/on-drop-asset-group event dragging* prefix selected-paths selected-full move-typography))) + + add-typography-to-group + (mf/use-fn + (mf/deps file-id prefix) + (fn [_] + (st/emit! (dw/set-assets-section-open file-id :typographies true) + (dwt/add-typography file-id prefix))))] [:div {:class (stl/css :typographies-group) :on-drag-enter on-drag-enter @@ -176,7 +183,9 @@ :path prefix :is-group-open group-open? :on-rename on-rename-group - :on-ungroup on-ungroup}] + :on-ungroup on-ungroup + :on-add (when (and local? (not is-read-only)) + add-typography-to-group)}] (when group-open? [:* @@ -229,7 +238,8 @@ :on-rename-group on-rename-group :on-ungroup on-ungroup :on-context-menu on-context-menu - :selected-full selected-full}]))])])) + :selected-full selected-full + :is-read-only is-read-only}]))])])) (mf/defc typographies-section* [{:keys [file file-id typographies open-status-ref selected @@ -431,7 +441,8 @@ :on-rename-group on-rename-group :on-ungroup on-ungroup :on-context-menu on-context-menu - :selected-full selected-full}] + :selected-full selected-full + :is-read-only read-only?}] (if is-local [:> cmm/assets-context-menu* From c1d815f97cfb62c10b3b58d1ccf424177684a605 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 9 Apr 2026 10:05:56 +0200 Subject: [PATCH 103/288] :bug: Fix go to viewer with frame selected (#8878) --- CHANGES.md | 1 + frontend/src/app/main/data/common.cljs | 11 +++++++++++ frontend/src/app/main/ui/workspace/right_header.cljs | 6 ++---- frontend/src/app/main/ui/workspace/sidebar.cljs | 4 ++-- .../main/ui/workspace/webgl_unavailable_modal.scss | 5 ++--- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 09afc6beb2..032c8019c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -37,6 +37,7 @@ - Token tree should be expanded by default [Taiga #13631](https://tree.taiga.io/project/penpot/issue/13631) - Fix opacity incorrectly disabled for visible shapes [Taiga #13906](https://tree.taiga.io/project/penpot/issue/13906) - Update onboarding image [Taiga #13864](https://tree.taiga.io/project/penpot/issue/13864) +- Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923) ## 2.15.0 (Unreleased) diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index fb55df73de..cecb34d2ad 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -459,6 +459,17 @@ (let [page-id (or page-id (:current-page-id state)) file-id (or file-id (:current-file-id state)) section (or section :interactions) + selected (get-in state [:workspace-local :selected]) + objects (dsh/lookup-page-objects state file-id page-id) + frame-id (or frame-id + (reduce + (fn [_ id] + (let [obj (get objects id)] + (when (and obj + (= :frame (:type obj))) + (reduced (:id obj))))) + nil + selected)) params {:file-id file-id :page-id page-id :section section diff --git a/frontend/src/app/main/ui/workspace/right_header.cljs b/frontend/src/app/main/ui/workspace/right_header.cljs index ce10de99cf..addbfc251e 100644 --- a/frontend/src/app/main/ui/workspace/right_header.cljs +++ b/frontend/src/app/main/ui/workspace/right_header.cljs @@ -111,10 +111,8 @@ ;; --- Header Component (mf/defc right-header* - [{:keys [file layout page-id]}] - (let [file-id (:id file) - - threads-map (mf/deref refs/comment-threads) + [{:keys [file-id layout page-id]}] + (let [threads-map (mf/deref refs/comment-threads) zoom (mf/deref refs/selected-zoom) read-only? (mf/use-ctx ctx/workspace-read-only?) diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index ac219faa2f..7db1fa2077 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -280,7 +280,7 @@ [:> history-toolbox*]])])) (mf/defc right-sidebar* - [{:keys [layout section file page-id drawing-tool active-tokens] :as props}] + [{:keys [layout section file-id page-id drawing-tool active-tokens] :as props}] (let [is-comments? (= drawing-tool :comments) is-history? (contains? layout :document-history) is-inspect? (= section :inspect) @@ -340,7 +340,7 @@ :on-pointer-move on-pointer-move}]) [:> right-header* - {:file file + {:file-id file-id :layout layout :page-id page-id}] diff --git a/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.scss b/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.scss index 0ad86b0c29..d57571ef0c 100644 --- a/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.scss +++ b/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.scss @@ -6,15 +6,14 @@ @use "ds/_utils.scss" as *; @use "ds/_borders.scss" as *; - @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-dialog { - @extend .modal-container-base; + @extend %modal-container-base; color: var(--color-foreground-secondary); display: grid; From da6bd7509b996f52e6673275dbfd4ac922bc3c90 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 9 Apr 2026 10:18:21 +0200 Subject: [PATCH 104/288] :bug: Fix hot reload on color-row text (#8880) --- .../app/main/ui/workspace/sidebar/options/menus/fill.cljs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index b3509487e2..107150a16e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -52,16 +52,15 @@ [n-props o-props] (and (identical? (unchecked-get n-props "ids") (unchecked-get o-props "ids")) + (identical? (unchecked-get n-props "appliedTokens") + (unchecked-get o-props "appliedTokens")) (let [o-vals (unchecked-get o-props "values") n-vals (unchecked-get n-props "values") o-fills (get o-vals :fills) n-fills (get n-vals :fills) - o-applied-tokens (get o-vals :applied-tokens) - n-applied-tokens (get n-vals :applied-tokens) o-hide (get o-vals :hide-fill-on-export) n-hide (get n-vals :hide-fill-on-export)] (and (identical? o-hide n-hide) - (identical? o-applied-tokens n-applied-tokens) (identical? o-fills n-fills))))) (mf/defc fill-menu* From 1c68810521d24625384bef43d755e4bdcac73b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Wed, 1 Apr 2026 13:49:52 +0200 Subject: [PATCH 105/288] :sparkles: Add can use trial prop in nitrate profile --- backend/src/app/rpc/management/nitrate.clj | 28 +++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 1258feb6ba..80b92ae218 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -10,7 +10,7 @@ (:require [app.common.data :as d] [app.common.schema :as sm] - [app.common.types.profile :refer [schema:profile, schema:basic-profile]] + [app.common.types.profile :refer [schema:basic-profile]] [app.common.types.team :refer [schema:team]] [app.common.uuid :as uuid] [app.config :as cf] @@ -25,18 +25,30 @@ ;; ---- API: authenticate +(def ^:private schema:nitrate-profile + [:map {:title "NitrateProfile"} + [:id ::sm/uuid] + [:name {:optional true} :string] + [:email {:optional true} :string] + [:photo-url {:optional true} :string] + [:theme {:optional true} :string] + [:can-use-trial ::sm/boolean]]) + (sv/defmethod ::authenticate "Authenticate the current user" {::doc/added "2.14" ::sm/params [:map] - ::sm/result schema:profile} + ::sm/result schema:nitrate-profile} [cfg {:keys [::rpc/profile-id] :as params}] - (let [profile (profile/get-profile cfg profile-id)] - {:id (get profile :id) - :name (get profile :fullname) - :email (get profile :email) - :photo-url (files/resolve-public-uri (get profile :photo-id)) - :theme (get profile :theme)})) + (let [profile (profile/get-profile cfg profile-id) + nitrate-subscription (:subscription profile) + can-use-nitrate-trial (nil? nitrate-subscription)] + {:id (get profile :id) + :name (get profile :fullname) + :email (get profile :email) + :photo-url (files/resolve-public-uri (get profile :photo-id)) + :theme (get profile :theme) + :can-use-trial can-use-nitrate-trial})) ;; ---- API: get-teams From fe2023dde51ce65f012e11ce4cff472a3c3a7900 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Tue, 7 Apr 2026 15:48:28 +0200 Subject: [PATCH 106/288] :sparkles: Add nitrate api endpoints to get an user profile --- backend/src/app/rpc/management/nitrate.clj | 1 - backend/test/backend_tests/helpers.clj | 25 ++ .../rpc_management_nitrate_test.clj | 219 ++++++++++++++++++ 3 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 backend/test/backend_tests/rpc_management_nitrate_test.clj diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 80b92ae218..6a553f5e70 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -255,4 +255,3 @@ RETURNING id, name;") ;; Notify users (doseq [team updated-teams] (notify-team-change cfg (:id team) (:name team) nil org-name "dashboard.org-deleted")))))))) - diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index 081af944e3..93197ddd6a 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -385,6 +385,31 @@ (dissoc ::type) (assoc :app.rpc/request-at (ct/now))))))) +(defn management-command! + ([data] + (management-command! data nil)) + ([{:keys [::type] :as data} flags-to-add] + (let [flags (reduce conj cf/flags (or flags-to-add [])) + + resolve-management-methods + (requiring-resolve 'app.rpc/resolve-management-methods) + + methods + (with-redefs [cf/flags flags] + (resolve-management-methods *system*)) + + [_ method-fn] + (get methods type)] + + (when-not method-fn + (ex/raise :type :assertion + :code :rpc-method-not-found + :hint (str/ffmt "management rpc method '%' not found" (name type)))) + + (try-on! (method-fn (-> data + (dissoc ::type) + (assoc :app.rpc/request-at (ct/now)))))))) + (defn run-task! ([name] (run-task! name {})) diff --git a/backend/test/backend_tests/rpc_management_nitrate_test.clj b/backend/test/backend_tests/rpc_management_nitrate_test.clj new file mode 100644 index 0000000000..f6d65a675d --- /dev/null +++ b/backend/test/backend_tests/rpc_management_nitrate_test.clj @@ -0,0 +1,219 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns backend-tests.rpc-management-nitrate-test + (:require + [app.common.data :as d] + [app.common.time :as ct] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as-alias db] + [app.msgbus :as mbus] + [app.rpc :as-alias rpc] + [backend-tests.helpers :as th] + [clojure.set :as set] + [clojure.test :as t] + [cuerdas.core :as str])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(defn- management-command-with-nitrate! + [data] + (th/management-command! data [:nitrate])) + +(t/deftest authenticate-success + (let [profile (th/create-profile* 1 {:is-active true + :fullname "Nitrate User"}) + out (management-command-with-nitrate! {::th/type :authenticate + ::rpc/profile-id (:id profile)})] + (t/is (th/success? out)) + (t/is (= (:id profile) (-> out :result :id))) + (t/is (= "Nitrate User" (-> out :result :name))) + (t/is (= (:email profile) (-> out :result :email))) + (t/is (nil? (-> out :result :photo-url))))) + +(t/deftest authenticate-requires-authentication + (let [out (management-command-with-nitrate! {::th/type :authenticate})] + (t/is (not (th/success? out))) + (t/is (= :authentication (th/ex-type (:error out)))) + (t/is (= :authentication-required (th/ex-code (:error out)))))) + +(t/deftest get-penpot-version + (let [profile (th/create-profile* 1 {:is-active true}) + out (management-command-with-nitrate! {::th/type :get-penpot-version + ::rpc/profile-id (:id profile)})] + (t/is (th/success? out)) + (t/is (= cf/version (-> out :result :version))))) + +(t/deftest get-teams-returns-only-owned-non-default-non-deleted + (let [profile (th/create-profile* 1 {:is-active true}) + other (th/create-profile* 2 {:is-active true}) + owned-team (th/create-team* 1 {:profile-id (:id profile)}) + deleted-team (th/create-team* 2 {:profile-id (:id profile)}) + _ (th/db-update! :team + {:deleted-at (ct/now)} + {:id (:id deleted-team)}) + other-team (th/create-team* 3 {:profile-id (:id other)}) + _ (th/create-team-role* {:team-id (:id other-team) + :profile-id (:id profile) + :role :editor}) + out (management-command-with-nitrate! {::th/type :get-teams + ::rpc/profile-id (:id profile)})] + (t/is (th/success? out)) + (t/is (= #{(:id owned-team)} + (->> out :result (map :id) set))) + (t/is (= #{(:name owned-team)} + (->> out :result (map :name) set))))) + +(t/deftest notify-team-change-publishes-event + (let [team-id (uuid/random) + organization-id (uuid/random) + calls (atom []) + out (with-redefs [mbus/pub! (fn [_cfg & {:keys [topic message]}] + (swap! calls conj {:topic topic + :message message}))] + (management-command-with-nitrate! {::th/type :notify-team-change + :id team-id + :organization-id organization-id + :organization-name "Acme Inc"}))] + (t/is (th/success? out)) + (t/is (= 1 (count @calls))) + (t/is (= uuid/zero (-> @calls first :topic))) + (t/is (= {:type :team-org-change + :team-id team-id + :team-name nil + :organization-id organization-id + :organization-name "Acme Inc" + :notification nil} + (-> @calls first :message))))) + +(t/deftest notify-user-added-to-organization-creates-default-org-team + (let [profile (th/create-profile* 1 {:is-active true}) + before-teams (->> (th/db-query :team-profile-rel {:profile-id (:id profile) + :is-owner true}) + (map :team-id) + set) + out (management-command-with-nitrate! {::th/type :notify-user-added-to-organization + :profile-id (:id profile) + :organization-id (uuid/random) + :role "owner"}) + after-teams (->> (th/db-query :team-profile-rel {:profile-id (:id profile) + :is-owner true}) + (map :team-id) + set) + new-team-id (first (set/difference after-teams before-teams)) + new-team (th/db-get :team {:id new-team-id})] + (t/is (th/success? out)) + (t/is (= 1 (count (set/difference after-teams before-teams)))) + (t/is (= "Your Penpot" (:name new-team))) + (t/is (true? (:is-default new-team))))) + +(t/deftest get-managed-profiles-returns-unique-members-for-owned-teams + (let [owner (th/create-profile* 1 {:is-active true}) + member1 (th/create-profile* 2 {:is-active true}) + member2 (th/create-profile* 3 {:is-active true}) + team1 (th/create-team* 1 {:profile-id (:id owner)}) + team2 (th/create-team* 2 {:profile-id (:id owner)}) + _ (th/create-team-role* {:team-id (:id team1) + :profile-id (:id member1) + :role :editor}) + _ (th/create-team-role* {:team-id (:id team1) + :profile-id (:id member2) + :role :editor}) + _ (th/create-team-role* {:team-id (:id team2) + :profile-id (:id member1) + :role :editor}) + out (management-command-with-nitrate! {::th/type :get-managed-profiles + ::rpc/profile-id (:id owner)})] + (t/is (th/success? out)) + (t/is (= #{(:id member1) (:id member2)} + (->> out :result (map :id) set))) + (t/is (= #{(:email member1) (:email member2)} + (->> out :result (map :email) set))))) + +(t/deftest get-teams-summary-returns-teams-and-files-count + (let [profile (th/create-profile* 1 {:is-active true}) + team1 (th/create-team* 1 {:profile-id (:id profile)}) + team2 (th/create-team* 2 {:profile-id (:id profile)}) + proj1 (th/create-project* 1 {:profile-id (:id profile) + :team-id (:id team1)}) + proj2 (th/create-project* 2 {:profile-id (:id profile) + :team-id (:id team2)}) + _ (th/create-file* 1 {:profile-id (:id profile) + :project-id (:id proj1)}) + _ (th/create-file* 2 {:profile-id (:id profile) + :project-id (:id proj2)}) + out (management-command-with-nitrate! {::th/type :get-teams-summary + ::rpc/profile-id (:id profile) + :ids [(:id team1) (:id team2)]})] + (t/is (th/success? out)) + (t/is (= 2 (-> out :result :num-files))) + (t/is (= #{(:id team1) (:id team2)} + (->> out :result :teams (map :id) set))))) + +(t/deftest notify-org-deletion-prefixes-teams-and-notifies + (let [profile (th/create-profile* 1 {:is-active true}) + extra-team (th/create-team* 1 {:profile-id (:id profile)}) + default-team (th/db-get :team {:id (:default-team-id profile)}) + teams [(:id default-team) (:id extra-team)] + org-name "Acme / Design" + expected-start (str "[" (d/sanitize-string org-name) "] ") + calls (atom []) + out (with-redefs [mbus/pub! (fn [_cfg & {:keys [topic message]}] + (swap! calls conj {:topic topic + :message message}))] + (management-command-with-nitrate! {::th/type :notify-org-deletion + ::rpc/profile-id (:id profile) + :teams teams + :org-name org-name})) + updated (map #(th/db-get :team {:id %} {::db/remove-deleted false}) teams)] + (t/is (th/success? out)) + (t/is (= 2 (count @calls))) + (doseq [team updated] + (t/is (false? (:is-default team))) + (t/is (str/starts-with? (:name team) expected-start))) + (doseq [call @calls] + (t/is (= uuid/zero (:topic call))) + (t/is (= :team-org-change (-> call :message :type))) + (t/is (= org-name (-> call :message :organization-name))) + (t/is (= "dashboard.org-deleted" (-> call :message :notification)))))) + +(t/deftest get-profile-by-email-success-and-not-found + (let [profile (th/create-profile* 1 {:is-active true + :fullname "Lookup by Email"}) + ok-out (management-command-with-nitrate! {::th/type :get-profile-by-email + ::rpc/profile-id (:id profile) + :email (:email profile)}) + ko-out (management-command-with-nitrate! {::th/type :get-profile-by-email + ::rpc/profile-id (:id profile) + :email "not-found@example.com"})] + (t/is (th/success? ok-out)) + (t/is (= (:id profile) (-> ok-out :result :id))) + (t/is (= "Lookup by Email" (-> ok-out :result :name))) + (t/is (nil? (-> ok-out :result :photo-url))) + + (t/is (not (th/success? ko-out))) + (t/is (= :not-found (th/ex-type (:error ko-out)))) + (t/is (= :profile-not-found (th/ex-code (:error ko-out)))))) + +(t/deftest get-profile-by-id-success-and-not-found + (let [profile (th/create-profile* 1 {:is-active true + :fullname "Lookup by Id"}) + ok-out (management-command-with-nitrate! {::th/type :get-profile-by-id + ::rpc/profile-id (:id profile) + :id (:id profile)}) + ko-out (management-command-with-nitrate! {::th/type :get-profile-by-id + ::rpc/profile-id (:id profile) + :id (uuid/random)})] + (t/is (th/success? ok-out)) + (t/is (= (:id profile) (-> ok-out :result :id))) + (t/is (= "Lookup by Id" (-> ok-out :result :name))) + (t/is (nil? (-> ok-out :result :photo-url))) + + (t/is (not (th/success? ko-out))) + (t/is (= :not-found (th/ex-type (:error ko-out)))) + (t/is (= :profile-not-found (th/ex-code (:error ko-out)))))) From d65f3b5396ff69a64956a1268996c0d06fccd1db Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 9 Apr 2026 12:07:52 +0200 Subject: [PATCH 107/288] :sparkles: Add nitrate api endpoints to get an user profile --- backend/src/app/rpc/management/nitrate.clj | 65 +++++++++++++++++++--- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 6a553f5e70..9cd5200341 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -9,8 +9,9 @@ organization management and token validation endpoints." (:require [app.common.data :as d] + [app.common.exceptions :as ex] [app.common.schema :as sm] - [app.common.types.profile :refer [schema:basic-profile]] + [app.common.types.profile :refer [schema:profile, schema:basic-profile]] [app.common.types.team :refer [schema:team]] [app.common.uuid :as uuid] [app.config :as cf] @@ -23,6 +24,13 @@ [app.rpc.doc :as doc] [app.util.services :as sv])) + +(defn- profile-to-map [profile] + {:id (:id profile) + :name (:fullname profile) + :email (:email profile) + :photo-url (files/resolve-public-uri (get profile :photo-id))}) + ;; ---- API: authenticate (def ^:private schema:nitrate-profile @@ -43,12 +51,9 @@ (let [profile (profile/get-profile cfg profile-id) nitrate-subscription (:subscription profile) can-use-nitrate-trial (nil? nitrate-subscription)] - {:id (get profile :id) - :name (get profile :fullname) - :email (get profile :email) - :photo-url (files/resolve-public-uri (get profile :photo-id)) - :theme (get profile :theme) - :can-use-trial can-use-nitrate-trial})) + (-> (profile-to-map profile) + (assoc :can-use-nitrate-trial can-use-nitrate-trial + :theme (:theme profile))))) ;; ---- API: get-teams @@ -255,3 +260,49 @@ RETURNING id, name;") ;; Notify users (doseq [team updated-teams] (notify-team-change cfg (:id team) (:name team) nil org-name "dashboard.org-deleted")))))))) + +;; ---- API: get-profile-by-email + +(def ^:private sql:get-profile-by-email + "SELECT DISTINCT id, fullname, email, photo_id + FROM profile + WHERE email = ? + AND deleted_at IS NULL;") + +(sv/defmethod ::get-profile-by-email + "Get profile by email" + {::doc/added "2.15" + ::sm/params [:map [:email ::sm/email]] + ::sm/result schema:profile} + [cfg {:keys [email]}] + (let [profile (db/exec-one! cfg [sql:get-profile-by-email email])] + (when-not profile + (ex/raise :type :not-found + :code :profile-not-found + :hint "profile does not exist" + :email email)) + (profile-to-map profile))) + + +;; ---- API: get-profile-by-id + +(def ^:private sql:get-profile-by-id + "SELECT DISTINCT id, fullname, email, photo_id + FROM profile + WHERE id = ? + AND deleted_at IS NULL;") + +(sv/defmethod ::get-profile-by-id + "Get profile by email" + {::doc/added "2.15" + ::sm/params [:map [:id ::sm/uuid]] + ::sm/result schema:profile} + [cfg {:keys [id]}] + (let [profile (db/exec-one! cfg [sql:get-profile-by-id id])] + (when-not profile + (ex/raise :type :not-found + :code :profile-not-found + :hint "profile does not exist" + :id id)) + (profile-to-map profile))) + From 666313c2c3031b34b0bad2fd95b4c6bd8ad4c7d6 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Thu, 9 Apr 2026 12:37:29 +0200 Subject: [PATCH 108/288] :bug: Close expanded tree when switching or creating sets (#8920) --- .../src/app/main/data/workspace/tokens/library_edit.cljs | 7 +++++++ frontend/src/app/main/ui/workspace/tokens/sets.cljs | 5 +++-- .../src/app/main/ui/workspace/tokens/sets/helpers.cljs | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs index 2fe57d491f..595ec4a86d 100644 --- a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs +++ b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs @@ -96,6 +96,13 @@ (close-token-type types type) (open-token-type types type))))))) +(defn clear-tokens-types + [] + (ptk/reify ::clear-tokens-types + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-tokens :unfolded-token-types] [])))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; TOKENS TREE - Toggle tree nodes diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs index fd35710a74..49938e462b 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs @@ -16,8 +16,9 @@ [rumext.v2 :as mf])) (defn- on-select-token-set-click [id] - (st/emit! (dwtl/clear-tokens-paths)) - (st/emit! (dwtl/set-selected-token-set-id id))) + (st/emit! (dwtl/clear-tokens-paths) + (dwtl/clear-tokens-types) + (dwtl/set-selected-token-set-id id))) (defn- on-toggle-token-set-click [name] (st/emit! (dwtl/toggle-token-set name))) diff --git a/frontend/src/app/main/ui/workspace/tokens/sets/helpers.cljs b/frontend/src/app/main/ui/workspace/tokens/sets/helpers.cljs index e7b9bf98c6..825e31571c 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets/helpers.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets/helpers.cljs @@ -41,7 +41,9 @@ (dwtl/clear-token-set-creation)) (if (empty? errors) (let [token-set (ctob/make-token-set :name name)] - (st/emit! (dwtl/create-token-set token-set))) + (st/emit! (dwtl/create-token-set token-set) + (dwtl/clear-tokens-paths) + (dwtl/clear-tokens-types))) (st/emit! (ntf/show {:content (tr "errors.token-set-already-exists") :type :toast :level :error From 5b78de3594c69025c1d0df9aec17f0c101d47c84 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 9 Apr 2026 14:10:23 +0200 Subject: [PATCH 109/288] :bug: Fix selected colors with tokens (#8889) --- CHANGES.md | 1 + .../workspace/tokens/management/forms/controls/utils.cljs | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 032c8019c0..b91412bf54 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -38,6 +38,7 @@ - Fix opacity incorrectly disabled for visible shapes [Taiga #13906](https://tree.taiga.io/project/penpot/issue/13906) - Update onboarding image [Taiga #13864](https://tree.taiga.io/project/penpot/issue/13864) - Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923) +- Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930) ## 2.15.0 (Unreleased) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs index 904f8c811e..99c08fcf1d 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs @@ -95,7 +95,13 @@ [raw-tokens input-type] (delay (let [raw-tokens (deref raw-tokens) - key-order (get cto/tokens-by-input input-type)] + key-order (case input-type + :color-selection + (concat + (get cto/tokens-by-input :fill) + (get cto/tokens-by-input :stroke-color)) + + (get cto/tokens-by-input input-type))] (-> (reduce (fn [acc k] (if (contains? raw-tokens k) (assoc acc k (get raw-tokens k)) From d2050d53319211c6262f34c2dd0c8bcc2d1532de Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Thu, 9 Apr 2026 15:38:34 +0200 Subject: [PATCH 110/288] :wrench: Update tests-mcp.yml Add a more explicit name for a workflow Signed-off-by: Yamila Moreno --- .github/workflows/tests-mcp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-mcp.yml b/.github/workflows/tests-mcp.yml index 9f2a4ed589..489899863f 100644 --- a/.github/workflows/tests-mcp.yml +++ b/.github/workflows/tests-mcp.yml @@ -25,7 +25,7 @@ on: jobs: test: - name: "Test" + name: "Test MCP" runs-on: penpot-runner-02 container: penpotapp/devenv:latest From e49b7ce14cad02d99a72cf3b0579d33aa0af5808 Mon Sep 17 00:00:00 2001 From: Dexterity <173429049+Dexterity104@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:09:19 -0400 Subject: [PATCH 111/288] :bug: Fix warnings for unsupported token $type (#8873) * :bug: Fix warnings for unsupported token $type Signed-off-by: Dexterity104 * :bug: Add changelog entry for Github #8790 --------- Signed-off-by: Dexterity104 Signed-off-by: Dexterity <173429049+Dexterity104@users.noreply.github.com> --- CHANGES.md | 1 + .../main/data/workspace/tokens/import_export.cljs | 14 +++++++++++--- frontend/translations/de.po | 8 ++++---- frontend/translations/en.po | 8 ++++---- frontend/translations/es.po | 10 ++++------ frontend/translations/fr.po | 8 ++++---- frontend/translations/he.po | 8 ++++---- frontend/translations/hi.po | 8 ++++---- frontend/translations/it.po | 10 ++++------ frontend/translations/lv.po | 4 ++-- frontend/translations/nl.po | 8 ++++---- frontend/translations/ro.po | 8 ++++---- frontend/translations/sv.po | 8 ++++---- frontend/translations/tr.po | 8 ++++---- frontend/translations/ukr_UA.po | 4 ++-- 15 files changed, 60 insertions(+), 55 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b91412bf54..98e3a212b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ - Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466) - Add CSS linter [Taiga #13790](https://tree.taiga.io/project/penpot/us/13790) - Save and restore selection state in undo/redo (by @eureka928) [Github #6007](https://github.com/penpot/penpot/issues/6007) +- Fix warnings for unsupported token $type (by @Dexterity104) [Github #8790](https://github.com/penpot/penpot/issues/8790) - Add per-group add button for typographies (by @eureka928) [Github #5275](https://github.com/penpot/penpot/issues/5275) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/data/workspace/tokens/import_export.cljs b/frontend/src/app/main/data/workspace/tokens/import_export.cljs index 32ba61fd70..545d893d27 100644 --- a/frontend/src/app/main/data/workspace/tokens/import_export.cljs +++ b/frontend/src/app/main/data/workspace/tokens/import_export.cljs @@ -7,6 +7,7 @@ (ns app.main.data.workspace.tokens.import-export (:require [app.common.json :as json] + [app.common.logging :as l] [app.common.path-names :as cpn] [app.common.types.tokens-lib :as ctob] [app.config :as cf] @@ -44,10 +45,17 @@ (defn- show-unknown-types-warning [unknown-tokens] (let [type->tokens (group-by-value unknown-tokens)] + (l/wrn :hint "unsupported token types found during import" + :tokens (str/join ", " (map (fn [[path type]] (str path " (" type ")")) unknown-tokens))) (ntf/show {:content (tr "workspace.tokens.unknown-token-type-message") - :detail (->> (for [[token-type tokens] type->tokens] - (tr "workspace.tokens.unknown-token-type-section" token-type (count tokens))) - (str/join "
")) + :detail (->> (for [[token-type token-paths] type->tokens] + (str (tr "workspace.tokens.unknown-token-type-section" token-type (count token-paths)) + "
" + (->> token-paths + (sort) + (map #(str "  • " %)) + (str/join "
")))) + (str/join "

")) :type :toast :level :info}))) diff --git a/frontend/translations/de.po b/frontend/translations/de.po index b1bead45b4..510ad2f093 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -8023,13 +8023,13 @@ msgstr "TOKENS - %s" msgid "workspace.tokens.tools" msgstr "Werkzeuge" -#: src/app/main/data/workspace/tokens/import_export.cljs:46 +#: src/app/main/data/workspace/tokens/import_export.cljs:50 msgid "workspace.tokens.unknown-token-type-message" -msgstr "Der Import war erfolgreich. Einige Token wurden nicht übernommen." +msgstr "Der Import war erfolgreich, aber einige Token wurden übersprungen, da sie nicht unterstützte $type-Werte verwenden. Details aufklappen, um zu sehen, welche Token betroffen sind." -#: src/app/main/data/workspace/tokens/import_export.cljs:48 +#: src/app/main/data/workspace/tokens/import_export.cljs:52 msgid "workspace.tokens.unknown-token-type-section" -msgstr "„%s“ wird nicht als Datentyp unterstützt (%s)\n" +msgstr "„%s“ wird nicht als Datentyp unterstützt (%s):" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 msgid "workspace.tokens.use-reference" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 9b98da0cd0..68521498ed 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -8626,13 +8626,13 @@ msgstr "TOKENS - %s" msgid "workspace.tokens.tools" msgstr "Tools" -#: src/app/main/data/workspace/tokens/import_export.cljs:46 +#: src/app/main/data/workspace/tokens/import_export.cljs:50 msgid "workspace.tokens.unknown-token-type-message" -msgstr "Import was successful. Some tokens were not included." +msgstr "Import was successful, but some tokens were skipped because they use unsupported $type values. Expand details to see which tokens were affected." -#: src/app/main/data/workspace/tokens/import_export.cljs:48 +#: src/app/main/data/workspace/tokens/import_export.cljs:52 msgid "workspace.tokens.unknown-token-type-section" -msgstr "Type '%s' is not supported (%s)\n" +msgstr "Type '%s' is not supported (%s):" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 msgid "workspace.tokens.use-reference" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 3381ae09bf..3d74830dce 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -8477,15 +8477,13 @@ msgstr "Introduce un valor o un alias usando {alias}" msgid "workspace.tokens.tools" msgstr "Herramientas" -#: src/app/main/data/workspace/tokens/import_export.cljs:46 +#: src/app/main/data/workspace/tokens/import_export.cljs:50 msgid "workspace.tokens.unknown-token-type-message" -msgstr "" -"La importación se ha realizado correctamente. Algunos tokens no se " -"incluyeron." +msgstr "La importación se ha realizado correctamente, pero algunos tokens fueron omitidos porque usan valores de $type no soportados. Expande los detalles para ver qué tokens se vieron afectados." -#: src/app/main/data/workspace/tokens/import_export.cljs:48 +#: src/app/main/data/workspace/tokens/import_export.cljs:52 msgid "workspace.tokens.unknown-token-type-section" -msgstr "El tipo '%s' no está soportado (%s)\n" +msgstr "El tipo '%s' no está soportado (%s):" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 msgid "workspace.tokens.use-reference" diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index ef26ef224c..d061e50e7e 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -8150,13 +8150,13 @@ msgstr "TOKENS - %s" msgid "workspace.tokens.tools" msgstr "Outils" -#: src/app/main/data/workspace/tokens/import_export.cljs:46 +#: src/app/main/data/workspace/tokens/import_export.cljs:50 msgid "workspace.tokens.unknown-token-type-message" -msgstr "L'importation a réussi. Certains tokens n'ont pas été inclus." +msgstr "L'importation a réussi, mais certains tokens ont été ignorés car ils utilisent des valeurs $type non prises en charge. Développez les détails pour voir quels tokens ont été affectés." -#: src/app/main/data/workspace/tokens/import_export.cljs:48 +#: src/app/main/data/workspace/tokens/import_export.cljs:52 msgid "workspace.tokens.unknown-token-type-section" -msgstr "Le type « %s » n'est pas pris en charge (%s)\n" +msgstr "Le type « %s » n'est pas pris en charge (%s) :" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 msgid "workspace.tokens.use-reference" diff --git a/frontend/translations/he.po b/frontend/translations/he.po index 395bce7724..a66e623ff0 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -8033,13 +8033,13 @@ msgstr "אסימונים - %s" msgid "workspace.tokens.tools" msgstr "כלים" -#: src/app/main/data/workspace/tokens/import_export.cljs:46 +#: src/app/main/data/workspace/tokens/import_export.cljs:50 msgid "workspace.tokens.unknown-token-type-message" -msgstr "הייבוא הצליח. חלק מהאסימונים לא נכללו." +msgstr "הייבוא הצליח, אך חלק מהאסימונים דולגו כי הם משתמשים בערכי $type שאינם נתמכים. הרחב את הפרטים כדי לראות אילו אסימונים הושפעו." -#: src/app/main/data/workspace/tokens/import_export.cljs:48 +#: src/app/main/data/workspace/tokens/import_export.cljs:52 msgid "workspace.tokens.unknown-token-type-section" -msgstr "הסוג ‚%s’ לא נתמך (%s)\n" +msgstr "הסוג ‚%s’ לא נתמך (%s):" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 msgid "workspace.tokens.use-reference" diff --git a/frontend/translations/hi.po b/frontend/translations/hi.po index 54c563fd5d..a169abe7a7 100644 --- a/frontend/translations/hi.po +++ b/frontend/translations/hi.po @@ -8187,13 +8187,13 @@ msgstr "टोकन - %s" msgid "workspace.tokens.tools" msgstr "औजार" -#: src/app/main/data/workspace/tokens/import_export.cljs:46 +#: src/app/main/data/workspace/tokens/import_export.cljs:50 msgid "workspace.tokens.unknown-token-type-message" -msgstr "आयात सफल रहा। कुछ टोकन शामिल नहीं किए गए।" +msgstr "आयात सफल रहा, लेकिन कुछ टोकन छोड़ दिए गए क्योंकि वे असमर्थित $type मानों का उपयोग करते हैं। प्रभावित टोकन देखने के लिए विवरण विस्तृत करें।" -#: src/app/main/data/workspace/tokens/import_export.cljs:48 +#: src/app/main/data/workspace/tokens/import_export.cljs:52 msgid "workspace.tokens.unknown-token-type-section" -msgstr "टाइप '%s' समर्थित नहीं है (%s)\n" +msgstr "टाइप '%s' समर्थित नहीं है (%s):" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 msgid "workspace.tokens.use-reference" diff --git a/frontend/translations/it.po b/frontend/translations/it.po index a6d88a564c..75f3d4ed1f 100644 --- a/frontend/translations/it.po +++ b/frontend/translations/it.po @@ -8419,15 +8419,13 @@ msgstr "TOKEN - %s" msgid "workspace.tokens.tools" msgstr "Strumenti" -#: src/app/main/data/workspace/tokens/import_export.cljs:46 +#: src/app/main/data/workspace/tokens/import_export.cljs:50 msgid "workspace.tokens.unknown-token-type-message" -msgstr "" -"L’importazione è stata completata con successo. Alcuni token non sono stati " -"inclusi." +msgstr "L’importazione è stata completata con successo, ma alcuni token sono stati ignorati perché utilizzano valori $type non supportati. Espandi i dettagli per vedere quali token sono stati interessati." -#: src/app/main/data/workspace/tokens/import_export.cljs:48 +#: src/app/main/data/workspace/tokens/import_export.cljs:52 msgid "workspace.tokens.unknown-token-type-section" -msgstr "Il tipo '%s' non è supportato (%s)\n" +msgstr "Il tipo '%s' non è supportato (%s):" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 msgid "workspace.tokens.use-reference" diff --git a/frontend/translations/lv.po b/frontend/translations/lv.po index 72fef0e36f..4268d7a133 100644 --- a/frontend/translations/lv.po +++ b/frontend/translations/lv.po @@ -7720,9 +7720,9 @@ msgstr "TEKSTVIENĪBAS - %s" msgid "workspace.tokens.tools" msgstr "Rīki" -#: src/app/main/data/workspace/tokens/import_export.cljs:48 +#: src/app/main/data/workspace/tokens/import_export.cljs:52 msgid "workspace.tokens.unknown-token-type-section" -msgstr "Veids \"%s\" nav atbalstīts (%s)\n" +msgstr "Veids \"%s\" nav atbalstīts (%s):" #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:132 msgid "workspace.tokens.value-not-valid" diff --git a/frontend/translations/nl.po b/frontend/translations/nl.po index 819e3da13d..d78724baee 100644 --- a/frontend/translations/nl.po +++ b/frontend/translations/nl.po @@ -8435,13 +8435,13 @@ msgstr "TOKENS - %s" msgid "workspace.tokens.tools" msgstr "Hulpmiddelen" -#: src/app/main/data/workspace/tokens/import_export.cljs:46 +#: src/app/main/data/workspace/tokens/import_export.cljs:50 msgid "workspace.tokens.unknown-token-type-message" -msgstr "Importeren was succesvol. Sommige tokens zijn niet inbegrepen." +msgstr "Importeren was succesvol, maar sommige tokens zijn overgeslagen omdat ze niet-ondersteunde $type-waarden gebruiken. Klap de details uit om te zien welke tokens getroffen zijn." -#: src/app/main/data/workspace/tokens/import_export.cljs:48 +#: src/app/main/data/workspace/tokens/import_export.cljs:52 msgid "workspace.tokens.unknown-token-type-section" -msgstr "Typ '%s' wordt niet ondersteund (%s)\n" +msgstr "Typ '%s' wordt niet ondersteund (%s):" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 msgid "workspace.tokens.use-reference" diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index 11d1473623..d0807b28a9 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -7956,13 +7956,13 @@ msgstr "TOKEN-URI- %s" msgid "workspace.tokens.tools" msgstr "Unelte" -#: src/app/main/data/workspace/tokens/import_export.cljs:46 +#: src/app/main/data/workspace/tokens/import_export.cljs:50 msgid "workspace.tokens.unknown-token-type-message" -msgstr "Import cu succes. Unele token-uri nu au fost incluse." +msgstr "Importul a fost realizat cu succes, dar unele token-uri au fost omise deoarece folosesc valori $type neacceptate. Extindeți detaliile pentru a vedea care token-uri au fost afectate." -#: src/app/main/data/workspace/tokens/import_export.cljs:48 +#: src/app/main/data/workspace/tokens/import_export.cljs:52 msgid "workspace.tokens.unknown-token-type-section" -msgstr "Tipul '%s' nu este suportat (%s)\n" +msgstr "Tipul '%s' nu este suportat (%s):" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 msgid "workspace.tokens.use-reference" diff --git a/frontend/translations/sv.po b/frontend/translations/sv.po index 810055f737..6d57efaf41 100644 --- a/frontend/translations/sv.po +++ b/frontend/translations/sv.po @@ -8107,13 +8107,13 @@ msgstr "TOKEN - %s" msgid "workspace.tokens.tools" msgstr "Verktyg" -#: src/app/main/data/workspace/tokens/import_export.cljs:46 +#: src/app/main/data/workspace/tokens/import_export.cljs:50 msgid "workspace.tokens.unknown-token-type-message" -msgstr "Importen lyckades. Vissa tokens inkluderades ej." +msgstr "Importen lyckades, men vissa tokens hoppades över eftersom de använder $type-värden som inte stöds. Expandera detaljerna för att se vilka tokens som påverkades." -#: src/app/main/data/workspace/tokens/import_export.cljs:48 +#: src/app/main/data/workspace/tokens/import_export.cljs:52 msgid "workspace.tokens.unknown-token-type-section" -msgstr "Typen '%s' stödjs ej (%s)\n" +msgstr "Typen '%s' stöds ej (%s):" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 msgid "workspace.tokens.use-reference" diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index c430de809f..72c1bfbedf 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -8393,13 +8393,13 @@ msgstr "TOKENLER - %s" msgid "workspace.tokens.tools" msgstr "Araçlar" -#: src/app/main/data/workspace/tokens/import_export.cljs:46 +#: src/app/main/data/workspace/tokens/import_export.cljs:50 msgid "workspace.tokens.unknown-token-type-message" -msgstr "İçe aktarma başarılı oldu. Bazı tokenler dahil edilmedi." +msgstr "İçe aktarma başarılı oldu, ancak bazı tokenler desteklenmeyen $type değerleri kullandıkları için atlandı. Hangi tokenlerin etkilendiğini görmek için ayrıntıları genişletin." -#: src/app/main/data/workspace/tokens/import_export.cljs:48 +#: src/app/main/data/workspace/tokens/import_export.cljs:52 msgid "workspace.tokens.unknown-token-type-section" -msgstr "'%s' türü desteklenmiyor (%s)\n" +msgstr "'%s' türü desteklenmiyor (%s):" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:244, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:199 msgid "workspace.tokens.use-reference" diff --git a/frontend/translations/ukr_UA.po b/frontend/translations/ukr_UA.po index 4769f8e70a..e587f083dc 100644 --- a/frontend/translations/ukr_UA.po +++ b/frontend/translations/ukr_UA.po @@ -7421,9 +7421,9 @@ msgstr "ТОКЕНИ - %s" msgid "workspace.tokens.tools" msgstr "Інструменти" -#: src/app/main/data/workspace/tokens/import_export.cljs:48 +#: src/app/main/data/workspace/tokens/import_export.cljs:52 msgid "workspace.tokens.unknown-token-type-section" -msgstr "Тип \"%s\" непідтримуваний (%s)\n" +msgstr "Тип \"%s\" непідтримуваний (%s):" #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:132 msgid "workspace.tokens.value-not-valid" From a803bde2ff1962d20ffc6cb87a945d174ceb1c72 Mon Sep 17 00:00:00 2001 From: Marek Hrabe Date: Thu, 9 Apr 2026 21:13:10 +0200 Subject: [PATCH 112/288] :bug: Fix plugin modal dragging bugs (#8871) * :bug: Fix plugin modal drag and close interactions Switch plugin modal dragging to pointer-capture semantics from the header so drag state remains stable when crossing iframe boundaries. Prevent drag start from close-button pointerdown and add regression tests for both non-draggable close-button interaction and close-event dispatch. Signed-off-by: Marek Hrabe * :books: Update changelog for plugin modal drag fix Document plugin modal drag and close-button interaction fixes in the unreleased changelog. Signed-off-by: Marek Hrabe * :bug: Simplify plugin modal drag CSS selection rules Keep user-select disabled at the modal wrapper level and keep touch-action scoped to the header drag handle to remove redundant declarations while preserving drag behavior. Signed-off-by: Marek Hrabe --------- Signed-off-by: Marek Hrabe --- CHANGES.md | 1 + .../src/lib/drag-handler.spec.ts | 116 +++++++++++++--- .../plugins-runtime/src/lib/drag-handler.ts | 91 +++++++++++-- .../src/lib/modal/plugin-modal.spec.ts | 125 ++++++++++++++++++ .../src/lib/modal/plugin-modal.ts | 22 ++- .../src/lib/modal/plugin.modal.css | 10 +- 6 files changed, 330 insertions(+), 35 deletions(-) create mode 100644 plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts diff --git a/CHANGES.md b/CHANGES.md index 98e3a212b9..db692c6c4b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -38,6 +38,7 @@ - Token tree should be expanded by default [Taiga #13631](https://tree.taiga.io/project/penpot/issue/13631) - Fix opacity incorrectly disabled for visible shapes [Taiga #13906](https://tree.taiga.io/project/penpot/issue/13906) - Update onboarding image [Taiga #13864](https://tree.taiga.io/project/penpot/issue/13864) +- Fix plugin modal drag interactions over iframe and close-button behavior (by @marekhrabe) [Github #8871](https://github.com/penpot/penpot/pull/8871) - Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923) - Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930) diff --git a/plugins/libs/plugins-runtime/src/lib/drag-handler.spec.ts b/plugins/libs/plugins-runtime/src/lib/drag-handler.spec.ts index 18ec9ef75c..8989b55eab 100644 --- a/plugins/libs/plugins-runtime/src/lib/drag-handler.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/drag-handler.spec.ts @@ -1,11 +1,45 @@ import { expect, describe, vi } from 'vitest'; import { dragHandler } from './drag-handler.js'; +type PointerLikeEvent = MouseEvent & { pointerId: number }; + +function createPointerEvent( + type: string, + init: Partial = {}, +): PointerLikeEvent { + const event = new MouseEvent(type, { + bubbles: true, + cancelable: true, + clientX: init.clientX ?? 0, + clientY: init.clientY ?? 0, + button: init.button ?? 0, + }) as PointerLikeEvent; + + Object.defineProperty(event, 'pointerId', { + configurable: true, + value: init.pointerId ?? 1, + }); + + return event; +} + describe('dragHandler', () => { let element: HTMLElement; beforeEach(() => { element = document.createElement('div'); + Object.defineProperty(element, 'setPointerCapture', { + configurable: true, + value: vi.fn(), + }); + Object.defineProperty(element, 'releasePointerCapture', { + configurable: true, + value: vi.fn(), + }); + Object.defineProperty(element, 'hasPointerCapture', { + configurable: true, + value: vi.fn().mockReturnValue(true), + }); document.body.appendChild(element); }); @@ -14,65 +48,109 @@ describe('dragHandler', () => { vi.clearAllMocks(); }); - it('should attach mousedown event listener to the element', () => { + it('should attach pointerdown event listener to the element', () => { const addEventListenerMock = vi.spyOn(element, 'addEventListener'); dragHandler(element); expect(addEventListenerMock).toHaveBeenCalledWith( - 'mousedown', + 'pointerdown', expect.any(Function), ); }); - it('should update element transform on mousemove', () => { - const mouseDownEvent = new MouseEvent('mousedown', { + it('should update element transform on pointermove', () => { + const pointerDownEvent = createPointerEvent('pointerdown', { clientX: 100, clientY: 100, }); dragHandler(element); - element.dispatchEvent(mouseDownEvent); + element.dispatchEvent(pointerDownEvent); - const mouseMoveEvent = new MouseEvent('mousemove', { + const pointerMoveEvent = createPointerEvent('pointermove', { clientX: 150, clientY: 150, }); - document.dispatchEvent(mouseMoveEvent); + element.dispatchEvent(pointerMoveEvent); expect(element.style.transform).toBe('translate(50px, 50px)'); - const mouseMoveEvent2 = new MouseEvent('mousemove', { + const pointerMoveEvent2 = createPointerEvent('pointermove', { clientX: 200, clientY: 200, }); - document.dispatchEvent(mouseMoveEvent2); + element.dispatchEvent(pointerMoveEvent2); expect(element.style.transform).toBe('translate(100px, 100px)'); }); - it('should remove event listeners on mouseup', () => { - const removeEventListenerMock = vi.spyOn(document, 'removeEventListener'); - - const mouseDownEvent = new MouseEvent('mousedown', { + it('should run lifecycle callbacks on drag start/end', () => { + const start = vi.fn(); + const end = vi.fn(); + const pointerDownEvent = createPointerEvent('pointerdown', { clientX: 100, clientY: 100, + pointerId: 2, + }); + const pointerUpEvent = createPointerEvent('pointerup', { + pointerId: 2, }); - dragHandler(element); + dragHandler(element, element, undefined, { start, end }); + element.dispatchEvent(pointerDownEvent); + element.dispatchEvent(pointerUpEvent); - element.dispatchEvent(mouseDownEvent); + expect(start).toHaveBeenCalledTimes(1); + expect(end).toHaveBeenCalledTimes(1); + expect(element.releasePointerCapture).toHaveBeenCalledWith(2); + }); - const mouseUpEvent = new MouseEvent('mouseup'); - document.dispatchEvent(mouseUpEvent); + it('should ignore pointerdown events from button targets', () => { + const start = vi.fn(); + const button = document.createElement('button'); + const icon = document.createElement('span'); + button.appendChild(icon); + element.appendChild(button); + + dragHandler(element, element, undefined, { start }); + + icon.dispatchEvent( + createPointerEvent('pointerdown', { + pointerId: 5, + button: 0, + }), + ); + + expect(start).not.toHaveBeenCalled(); + expect(element.setPointerCapture).not.toHaveBeenCalled(); + }); + + it('should remove pointer listeners on teardown', () => { + const removeEventListenerMock = vi.spyOn(element, 'removeEventListener'); + + const cleanup = dragHandler(element); + cleanup(); expect(removeEventListenerMock).toHaveBeenCalledWith( - 'mousemove', + 'pointerdown', expect.any(Function), ); expect(removeEventListenerMock).toHaveBeenCalledWith( - 'mouseup', + 'pointermove', + expect.any(Function), + ); + expect(removeEventListenerMock).toHaveBeenCalledWith( + 'pointerup', + expect.any(Function), + ); + expect(removeEventListenerMock).toHaveBeenCalledWith( + 'pointercancel', + expect.any(Function), + ); + expect(removeEventListenerMock).toHaveBeenCalledWith( + 'lostpointercapture', expect.any(Function), ); }); diff --git a/plugins/libs/plugins-runtime/src/lib/drag-handler.ts b/plugins/libs/plugins-runtime/src/lib/drag-handler.ts index 9ee5c83c93..fd79f07d3e 100644 --- a/plugins/libs/plugins-runtime/src/lib/drag-handler.ts +++ b/plugins/libs/plugins-runtime/src/lib/drag-handler.ts @@ -1,14 +1,36 @@ import { parseTranslate } from './parse-translate'; +type DragHandlerLifecycle = { + start?: () => void; + end?: () => void; +}; + export const dragHandler = ( el: HTMLElement, target: HTMLElement = el, move?: () => void, + lifecycle?: DragHandlerLifecycle, ) => { let initialTranslate = { x: 0, y: 0 }; let initialClientPosition = { x: 0, y: 0 }; + let pointerId: number | null = null; + let dragging = false; + + const endDrag = () => { + if (!dragging) { + return; + } + + dragging = false; + pointerId = null; + lifecycle?.end?.(); + }; + + const handlePointerMove = (moveEvent: PointerEvent) => { + if (!dragging || moveEvent.pointerId !== pointerId) { + return; + } - const handleMouseMove = (moveEvent: MouseEvent) => { const { clientX: moveX, clientY: moveY } = moveEvent; const deltaX = moveX - initialClientPosition.x + initialTranslate.x; const deltaY = moveY - initialClientPosition.y + initialTranslate.y; @@ -18,19 +40,70 @@ export const dragHandler = ( move?.(); }; - const handleMouseUp = () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); + const handlePointerUp = (upEvent: PointerEvent) => { + if (upEvent.pointerId !== pointerId) { + return; + } + + if (el.hasPointerCapture(upEvent.pointerId)) { + el.releasePointerCapture(upEvent.pointerId); + } + + endDrag(); }; - const handleMouseDown = (e: MouseEvent) => { + const handlePointerCancel = (cancelEvent: PointerEvent) => { + if (cancelEvent.pointerId !== pointerId) { + return; + } + + endDrag(); + }; + + const handleLostPointerCapture = (lostCaptureEvent: PointerEvent) => { + if (lostCaptureEvent.pointerId !== pointerId) { + return; + } + + endDrag(); + }; + + const handlePointerDown = (e: PointerEvent) => { + if (e.button !== 0) { + return; + } + + const fromButton = + e.target instanceof Element && !!e.target.closest('button'); + + if (fromButton) { + return; + } + + e.preventDefault(); + initialClientPosition = { x: e.clientX, y: e.clientY }; initialTranslate = parseTranslate(target); - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); + + dragging = true; + pointerId = e.pointerId; + lifecycle?.start?.(); + + el.setPointerCapture(e.pointerId); }; - el.addEventListener('mousedown', handleMouseDown); + el.addEventListener('pointerdown', handlePointerDown); + el.addEventListener('pointermove', handlePointerMove); + el.addEventListener('pointerup', handlePointerUp); + el.addEventListener('pointercancel', handlePointerCancel); + el.addEventListener('lostpointercapture', handleLostPointerCapture); - return handleMouseUp; + return () => { + el.removeEventListener('pointerdown', handlePointerDown); + el.removeEventListener('pointermove', handlePointerMove); + el.removeEventListener('pointerup', handlePointerUp); + el.removeEventListener('pointercancel', handlePointerCancel); + el.removeEventListener('lostpointercapture', handleLostPointerCapture); + endDrag(); + }; }; diff --git a/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts b/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts new file mode 100644 index 0000000000..d7b774c2c3 --- /dev/null +++ b/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts @@ -0,0 +1,125 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import './plugin-modal.js'; + +type PointerLikeEvent = MouseEvent & { pointerId: number }; + +function createPointerEvent( + type: string, + init: Partial = {}, +): PointerLikeEvent { + const event = new MouseEvent(type, { + bubbles: true, + cancelable: true, + clientX: init.clientX ?? 0, + clientY: init.clientY ?? 0, + button: init.button ?? 0, + }) as PointerLikeEvent; + + Object.defineProperty(event, 'pointerId', { + configurable: true, + value: init.pointerId ?? 1, + }); + + return event; +} + +describe('PluginModalElement', () => { + let setPointerCaptureSpy: ReturnType; + let releasePointerCaptureSpy: ReturnType; + let hasPointerCaptureSpy: ReturnType; + let originalSetPointerCapture: typeof HTMLElement.prototype.setPointerCapture; + let originalReleasePointerCapture: typeof HTMLElement.prototype.releasePointerCapture; + let originalHasPointerCapture: typeof HTMLElement.prototype.hasPointerCapture; + + beforeEach(() => { + originalSetPointerCapture = HTMLElement.prototype.setPointerCapture; + originalReleasePointerCapture = HTMLElement.prototype.releasePointerCapture; + originalHasPointerCapture = HTMLElement.prototype.hasPointerCapture; + + setPointerCaptureSpy = vi.fn(); + releasePointerCaptureSpy = vi.fn(); + hasPointerCaptureSpy = vi.fn().mockReturnValue(true); + + Object.defineProperty(HTMLElement.prototype, 'setPointerCapture', { + configurable: true, + value: setPointerCaptureSpy, + }); + Object.defineProperty(HTMLElement.prototype, 'releasePointerCapture', { + configurable: true, + value: releasePointerCaptureSpy, + }); + Object.defineProperty(HTMLElement.prototype, 'hasPointerCapture', { + configurable: true, + value: hasPointerCaptureSpy, + }); + }); + + afterEach(() => { + document.body.innerHTML = ''; + Object.defineProperty(HTMLElement.prototype, 'setPointerCapture', { + configurable: true, + value: originalSetPointerCapture, + }); + Object.defineProperty(HTMLElement.prototype, 'releasePointerCapture', { + configurable: true, + value: originalReleasePointerCapture, + }); + Object.defineProperty(HTMLElement.prototype, 'hasPointerCapture', { + configurable: true, + value: originalHasPointerCapture, + }); + vi.restoreAllMocks(); + }); + + it('should not start dragging on close button pointerdown', () => { + const modal = document.createElement('plugin-modal'); + modal.setAttribute('title', 'Test modal'); + modal.setAttribute('iframe-src', 'about:blank'); + document.body.appendChild(modal); + + const shadow = modal.shadowRoot; + expect(shadow).toBeTruthy(); + + const wrapper = shadow?.querySelector('.wrapper'); + const closeButton = shadow?.querySelector('button'); + + expect(wrapper).toBeTruthy(); + expect(closeButton).toBeTruthy(); + + closeButton?.dispatchEvent( + createPointerEvent('pointerdown', { + pointerId: 11, + button: 0, + }), + ); + + expect(wrapper?.classList.contains('is-dragging')).toBe(false); + expect(setPointerCaptureSpy).not.toHaveBeenCalled(); + + modal.remove(); + }); + + it('should dispatch close event when close button is clicked', () => { + const modal = document.createElement('plugin-modal'); + modal.setAttribute('title', 'Test modal'); + modal.setAttribute('iframe-src', 'about:blank'); + + const onClose = vi.fn(); + modal.addEventListener('close', onClose); + document.body.appendChild(modal); + + const closeButton = modal.shadowRoot?.querySelector('button'); + expect(closeButton).toBeTruthy(); + + closeButton?.dispatchEvent( + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }), + ); + + expect(onClose).toHaveBeenCalledTimes(1); + + modal.remove(); + }); +}); diff --git a/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.ts b/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.ts index 3346175212..c61ad7fce5 100644 --- a/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.ts +++ b/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.ts @@ -67,11 +67,6 @@ export class PluginModalElement extends HTMLElement { this.wrapper.style.maxInlineSize = '90vw'; this.wrapper.style.maxBlockSize = '90vh'; - // move modal to the top - this.#dragEvents = dragHandler(this.#inner, this.wrapper, () => { - this.calculateZIndex(); - }); - const header = document.createElement('div'); header.classList.add('header'); @@ -124,6 +119,23 @@ export class PluginModalElement extends HTMLElement { ); }); + // move modal to the top + this.#dragEvents = dragHandler( + header, + this.wrapper, + () => { + this.calculateZIndex(); + }, + { + start: () => { + this.wrapper.classList.add('is-dragging'); + }, + end: () => { + this.wrapper.classList.remove('is-dragging'); + }, + }, + ); + this.addEventListener('message', (e: Event) => { if (!iframe.contentWindow) { return; diff --git a/plugins/libs/plugins-runtime/src/lib/modal/plugin.modal.css b/plugins/libs/plugins-runtime/src/lib/modal/plugin.modal.css index 4ac13c45d9..bd987442d2 100644 --- a/plugins/libs/plugins-runtime/src/lib/modal/plugin.modal.css +++ b/plugins/libs/plugins-runtime/src/lib/modal/plugin.modal.css @@ -42,6 +42,8 @@ min-inline-size: 25px; min-block-size: 200px; resize: both; + user-select: none; + -webkit-user-select: none; &:after { content: ''; cursor: se-resize; @@ -58,7 +60,6 @@ .inner { padding: 10px; - cursor: grab; box-sizing: border-box; display: flex; flex-direction: column; @@ -78,6 +79,12 @@ justify-content: space-between; border-block-end: 2px solid var(--color-background-quaternary); padding-block-end: var(--spacing-4); + cursor: grab; + touch-action: none; +} + +.wrapper.is-dragging .header { + cursor: grabbing; } button { @@ -92,7 +99,6 @@ h1 { font-weight: var(--font-weight-bold); margin: 0; margin-inline-end: var(--spacing-4); - user-select: none; } iframe { From 9e4c8981be3634d4515fe605319c0c0fecd0e2cb Mon Sep 17 00:00:00 2001 From: Xaviju Date: Fri, 10 Apr 2026 10:42:35 +0200 Subject: [PATCH 113/288] :tada: Duplicate token group (#8886) --- CHANGES.md | 3 +- .../playwright/ui/specs/tokens/crud.spec.js | 44 ----- .../playwright/ui/specs/tokens/helpers.js | 29 ++++ .../ui/specs/tokens/remapping.spec.js | 85 +--------- .../playwright/ui/specs/tokens/tree.spec.js | 159 +++++++++++++++++- .../data/workspace/tokens/library_edit.cljs | 33 ++++ .../main/ui/workspace/tokens/management.cljs | 28 ++- .../management/forms/rename_node_modal.cljs | 23 +-- .../tokens/management/node_context_menu.cljs | 14 +- 9 files changed, 271 insertions(+), 147 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index db692c6c4b..c703c050b3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,7 @@ - Add Tab/Shift+Tab navigation to rename layers sequentially (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8474) - Copy and paste entire rows in existing table (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8498) - Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137) +- Duplicate token group [Taiga #10653](https://tree.taiga.io/project/penpot/us/10653) - Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568) - Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713) - Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466) @@ -84,7 +85,6 @@ - Guard delete undo against missing sibling order [Github #8858](https://github.com/penpot/penpot/pull/8858) - Fix ICounted error on numeric-input token dropdown keyboard nav [Github #8803](https://github.com/penpot/penpot/pull/8803) - ## 2.14.1 ### :sparkles: New features & Enhancements @@ -108,7 +108,6 @@ - Ensure path content is always PathData when saving - Fix error when get-parent-with-data encounters non-Element nodes - ## 2.14.0 ### :boom: Breaking changes & Deprecations diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index 58e67be6e5..4b9266fa60 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -2011,48 +2011,4 @@ test.describe("Tokens tab - delete", () => { await expect(tokenContextMenuForToken).not.toBeVisible(); await expect(colorToken).not.toBeVisible(); }); - - test("User removes node and all child tokens", async ({ page }) => { - const { tokensSidebar } = await setupTokensFileRender(page); - - await expect(tokensSidebar).toBeVisible(); - - // Expand color tokens - await unfoldTokenType(tokensSidebar, "color"); - - // Verify that the node and child token are visible before deletion - const colorNode = tokensSidebar.getByRole("button", { - name: "colors", - exact: true, - }); - const colorNodeToken = tokensSidebar.getByRole("button", { - name: "colors.blue.100", - }); - - // Select a node and right click on it to open context menu - await expect(colorNode).toBeVisible(); - await expect(colorNodeToken).toBeVisible(); - await colorNode.click({ button: "right" }); - - // select "Delete" from the context menu - const deleteNodeButton = page.getByRole("button", { - name: "Delete", - exact: true, - }); - await expect(deleteNodeButton).toBeVisible(); - await deleteNodeButton.click(); - - // Verify that the node is removed - await expect(colorNode).not.toBeVisible(); - // Verify that child token is also removed - await expect(colorNodeToken).not.toBeVisible(); - - // Save the type button to verify that expands/folds - const tokenTypeButton = await tokensSidebar.getByRole("button", { - name: "Color", - exact: true, - }); - - await expect(tokenTypeButton).toHaveAttribute("aria-expanded", "false"); - }); }); diff --git a/frontend/playwright/ui/specs/tokens/helpers.js b/frontend/playwright/ui/specs/tokens/helpers.js index 657bce8b54..937a268242 100644 --- a/frontend/playwright/ui/specs/tokens/helpers.js +++ b/frontend/playwright/ui/specs/tokens/helpers.js @@ -332,6 +332,34 @@ const unfoldTokenType = async (tokensTabPanel, type) => { } }; +const createToken = async (page, type, name, textFieldName, value) => { + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + const { tokensUpdateCreateModal } = await setupTokensFileRender(page, { + flags: ["enable-token-shadow"], + }); + + // Create base token + await tokensTabPanel + .getByRole("button", { name: `Add Token: ${type}` }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + await nameField.fill(name); + + const colorField = tokensUpdateCreateModal.getByRole("textbox", { + name: textFieldName, + }); + await colorField.fill(value); + + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + await submitButton.click(); + await expect(tokensUpdateCreateModal).not.toBeVisible(); +}; + export { setupEmptyTokensFile, setupEmptyTokensFileRender, @@ -341,4 +369,5 @@ export { setupTypographyTokensFileRender, testTokenCreationFlow, unfoldTokenType, + createToken, }; diff --git a/frontend/playwright/ui/specs/tokens/remapping.spec.js b/frontend/playwright/ui/specs/tokens/remapping.spec.js index 90eb658a77..44163bfdfa 100644 --- a/frontend/playwright/ui/specs/tokens/remapping.spec.js +++ b/frontend/playwright/ui/specs/tokens/remapping.spec.js @@ -2,6 +2,7 @@ import { test, expect } from "@playwright/test"; import { WorkspacePage } from "../../pages/WorkspacePage"; import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage"; import { + createToken, setupTokensFileRender, setupTypographyTokensFileRender, } from "./helpers"; @@ -14,34 +15,6 @@ test.beforeEach(async ({ page }) => { await WasmWorkspacePage.mockRPC(page, "get-teams", "get-teams-tokens.json"); }); -const createToken = async (page, type, name, textFieldName, value) => { - const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); - - const { tokensUpdateCreateModal } = await setupTokensFileRender(page, { - flags: ["enable-token-shadow"], - }); - - // Create base token - await tokensTabPanel - .getByRole("button", { name: `Add Token: ${type}` }) - .click(); - await expect(tokensUpdateCreateModal).toBeVisible(); - - const nameField = tokensUpdateCreateModal.getByLabel("Name"); - await nameField.fill(name); - - const colorField = tokensUpdateCreateModal.getByRole("textbox", { - name: textFieldName, - }); - await colorField.fill(value); - - const submitButton = tokensUpdateCreateModal.getByRole("button", { - name: "Save", - }); - await submitButton.click(); - await expect(tokensUpdateCreateModal).not.toBeVisible(); -}; - const createTokenCombobox = async (page, type, name, textFieldName, value) => { const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); @@ -636,62 +609,6 @@ test.describe("Remapping a single token", () => { }); test.describe("Remapping group of tokens", () => { - test("User renames a group - no remap", async ({ page }) => { - const { tokensSidebar } = await setupTokensFileRender(page); - - // Create multiple tokens in a group - await createToken(page, "Color", "dark.primary", "Value", "#000000"); - await createToken(page, "Color", "dark.secondary", "Value", "#111111"); - - // Verify that the node and child token are visible before deletion - const darkNode = tokensSidebar.getByRole("button", { - name: "dark", - exact: true, - }); - const darkNodeToken = tokensSidebar.getByRole("button", { - name: "primary", - }); - - // Select a node and right click on it to open context menu - await expect(darkNode).toBeVisible(); - await expect(darkNodeToken).toBeVisible(); - await darkNode.click({ button: "right" }); - - // select "Rename" from the context menu - const renameNodeButton = page.getByRole("button", { - name: "Rename", - exact: true, - }); - await expect(renameNodeButton).toBeVisible(); - await renameNodeButton.click(); - - // Expect the rename modal to be visible, fill in the new name and submit - const tokenRenameNodeModal = page.getByTestId("token-rename-node-modal"); - await expect(tokenRenameNodeModal).toBeVisible(); - - const nameField = tokenRenameNodeModal.getByRole("textbox", { - name: "Name", - }); - await nameField.fill("darker"); - - const submitButton = tokenRenameNodeModal.getByRole("button", { - name: "Rename", - }); - await submitButton.click(); - - // Ensure that the remapping modal does not appear - const remappingModal = page.getByTestId("token-remapping-modal"); - await expect(remappingModal).not.toBeVisible(); - - // Verify that the node has been renamed and tokens are still visible - const darkerNode = tokensSidebar.getByRole("button", { - name: "darker", - exact: true, - }); - - await expect(darkerNode).toBeVisible(); - }); - test("User renames a group - and remaps", async ({ page }) => { const { tokensSidebar } = await setupTokensFileRender(page); const workspacePage = new WasmWorkspacePage(page); diff --git a/frontend/playwright/ui/specs/tokens/tree.spec.js b/frontend/playwright/ui/specs/tokens/tree.spec.js index 6228b52a2a..243a539432 100644 --- a/frontend/playwright/ui/specs/tokens/tree.spec.js +++ b/frontend/playwright/ui/specs/tokens/tree.spec.js @@ -1,7 +1,7 @@ import { test, expect } from "@playwright/test"; import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage"; import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage"; -import { setupTokensFileRender } from "./helpers"; +import { createToken, setupTokensFileRender, unfoldTokenType } from "./helpers"; test.beforeEach(async ({ page }) => { await WasmWorkspacePage.init(page); @@ -27,4 +27,161 @@ test.describe("Tokens - node tree", () => { await tokensColorGroup.click(); await expect(colorToken).not.toBeVisible(); }); + + test("User renames a group", async ({ page }) => { + const { tokensSidebar } = await setupTokensFileRender(page); + + // Create multiple tokens in a group + await createToken(page, "Color", "dark.primary", "Value", "#000000"); + await createToken(page, "Color", "dark.secondary", "Value", "#111111"); + + // Verify that the node and child token are visible before deletion + const darkNode = tokensSidebar.getByRole("button", { + name: "dark", + exact: true, + }); + const darkNodeToken = tokensSidebar.getByRole("button", { + name: "primary", + }); + + // Select a node and right click on it to open context menu + await expect(darkNode).toBeVisible(); + await expect(darkNodeToken).toBeVisible(); + await darkNode.click({ button: "right" }); + + // select "Rename" from the context menu + const renameNodeButton = page.getByRole("button", { + name: "Rename", + exact: true, + }); + await expect(renameNodeButton).toBeVisible(); + await renameNodeButton.click(); + + // Expect the rename modal to be visible, fill in the new name and submit + const tokenRenameNodeModal = page.getByTestId("token-rename-node-modal"); + await expect(tokenRenameNodeModal).toBeVisible(); + + const nameField = tokenRenameNodeModal.getByRole("textbox", { + name: "Name", + }); + await nameField.fill("darker"); + + const submitButton = tokenRenameNodeModal.getByRole("button", { + name: "Rename", + }); + await submitButton.click(); + + // Ensure that the remapping modal does not appear + const remappingModal = page.getByTestId("token-remapping-modal"); + await expect(remappingModal).not.toBeVisible(); + + // Verify that the node has been renamed and tokens are still visible + const darkerNode = tokensSidebar.getByRole("button", { + name: "darker", + exact: true, + }); + + await expect(darkerNode).toBeVisible(); + }); + + test("User duplicates a group", async ({ page }) => { + const { tokensSidebar } = await setupTokensFileRender(page); + + // Create multiple tokens in a group + await createToken(page, "Color", "dark.primary", "Value", "#000000"); + await createToken(page, "Color", "dark.secondary", "Value", "#111111"); + + // Verify that the node and child token are visible before deletion + const darkNode = tokensSidebar.getByRole("button", { + name: "dark", + exact: true, + }); + const darkNodeToken = tokensSidebar.getByRole("button", { + name: "primary", + }); + + // Select a node and right click on it to open context menu + await expect(darkNode).toBeVisible(); + await expect(darkNodeToken).toBeVisible(); + await darkNode.click({ button: "right" }); + + // select "Duplicate" from the context menu + const duplicateNodeButton = page.getByRole("button", { + name: "Duplicate", + exact: true, + }); + await expect(duplicateNodeButton).toBeVisible(); + await duplicateNodeButton.click(); + + // Expect the duplicate modal to be visible, fill in the new name and submit + const tokenDuplicateNodeModal = page.getByTestId("token-rename-node-modal"); + await expect(tokenDuplicateNodeModal).toBeVisible(); + + const nameField = tokenDuplicateNodeModal.getByRole("textbox", { + name: "Name", + }); + await nameField.fill("darker"); + + const submitButton = tokenDuplicateNodeModal.getByRole("button", { + name: "Duplicate", + }); + await submitButton.click(); + + // Verify that the node has been duplicated and tokens are visible + const darkerNode = tokensSidebar.getByRole("button", { + name: "darker", + exact: true, + }); + + const darkerNodeToken = tokensSidebar.getByRole("button", { + name: "darker.primary", + }); + + await expect(darkerNode).toBeVisible(); + await expect(darkerNodeToken).toBeVisible(); + }); + + test("User removes node and all child tokens", async ({ page }) => { + const { tokensSidebar } = await setupTokensFileRender(page); + + await expect(tokensSidebar).toBeVisible(); + + // Expand color tokens + await unfoldTokenType(tokensSidebar, "color"); + + // Verify that the node and child token are visible before deletion + const colorNode = tokensSidebar.getByRole("button", { + name: "colors", + exact: true, + }); + const colorNodeToken = tokensSidebar.getByRole("button", { + name: "colors.blue.100", + }); + + // Select a node and right click on it to open context menu + await expect(colorNode).toBeVisible(); + await expect(colorNodeToken).toBeVisible(); + await colorNode.click({ button: "right" }); + + // select "Delete" from the context menu + const deleteNodeButton = page.getByRole("button", { + name: "Delete", + exact: true, + }); + await expect(deleteNodeButton).toBeVisible(); + await deleteNodeButton.click(); + + // Verify that the node is removed + await expect(colorNode).not.toBeVisible(); + // Verify that child token is also removed + await expect(colorNodeToken).not.toBeVisible(); + + // Save the type button to verify that expands/folds + const tokenTypeButton = await tokensSidebar.getByRole("button", { + name: "Color", + exact: true, + }); + + await expect(tokenTypeButton).toHaveAttribute("aria-expanded", "false"); + }); }); diff --git a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs index 595ec4a86d..947e8ad456 100644 --- a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs +++ b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs @@ -11,6 +11,8 @@ [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.logic.tokens :as clt] + [app.common.path-names :as cpn] + [app.common.test-helpers.ids-map :as cthi] [app.common.types.shape :as cts] [app.common.types.tokens-lib :as ctob] [app.common.uuid :as uuid] @@ -461,6 +463,37 @@ (rx/of (create-token-with-set token))))))) +(defn bulk-create-tokens + [set-id token-ids type node new-node-name] + (assert (uuid? set-id) "expected uuid for `set-id`") + (assert (every? uuid? token-ids) "expected a collection of uuids for `token-ids`") + (assert (keyword? type) "expected keyword for `type`") + (assert (string? new-node-name) "expected string for `new-node-name`") + + (ptk/reify ::bulk-create-tokens + ptk/WatchEvent + (watch [it state _] + (let [token-set (lookup-token-set state set-id) + data (dsh/lookup-file-data state) + changes (reduce (fn [changes token-id] + (let [token (-> (get-tokens-lib state) + (ctob/get-token (ctob/get-id token-set) token-id)) + new-name (-> + (cpn/split-path (:name token) :separator ".") + (assoc (:depth node) new-node-name) + (cpn/join-path :separator "." :with-spaces? false)) + token' (->> (merge token {:name new-name + :id (cthi/new-id! (:name new-name))}) + (into {}) + (ctob/make-token))] + (pcb/set-token changes (ctob/get-id token-set) (:id token') token'))) + (-> (pcb/empty-changes it) + (pcb/with-library-data data)) + token-ids)] + (rx/of + (dch/commit-changes changes) + (ptk/data-event ::ev/event {::ev/name "bulk-create-tokens" :type type})))))) + (defn update-token ([id params] (update-token nil id params)) ([set-id id params] diff --git a/frontend/src/app/main/ui/workspace/tokens/management.cljs b/frontend/src/app/main/ui/workspace/tokens/management.cljs index f462b48e5e..e3a0dafed1 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management.cljs @@ -22,7 +22,6 @@ [app.main.ui.workspace.tokens.management.node-context-menu :refer [token-node-context-menu*]] [app.util.array :as array] [app.util.i18n :refer [tr]] - [cljs.pprint :as pp] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -136,8 +135,7 @@ (fn [tokens-filtered-by-type node] (->> tokens-filtered-by-type (filter (fn [token] - (let [token-path (cpn/split-path (:name token) :separator ".") - _ (pp/pprint {:token-path token-path :count (count token-path)})] + (let [token-path (cpn/split-path (:name token) :separator ".")] (and (> (count token-path) 0) (str/starts-with? (:name token) (str (:path node) "."))))))))) @@ -192,6 +190,8 @@ (st/emit! (dwtl/toggle-token-path (str (name type) "." path))) (st/emit! (dwtl/close-token-type type)))))) + + bulk-rename-tokens-in-path ;; Rename tokens in bulk affected by a node rename. (mf/use-fn @@ -254,7 +254,6 @@ tokens-by-type (ctob/group-by-type selected-token-set-tokens) tokens-filtered-by-type (get tokens-by-type type) tokens-in-current-path (filter-tokens-by-path tokens-filtered-by-type node) - _ (pp/pprint {:tokens-in-current-path tokens-in-current-path}) token-references-count (reduce (fn [count token] (+ count (remap/count-token-references file-data (:name token)))) 0 @@ -263,6 +262,13 @@ (on-remap-node-warning node type new-node-name) (bulk-rename-tokens-in-path node type new-node-name))))) + on-duplicate-node + (fn [node type new-node-name] + (let [tokens-in-path-ids (filter-tokens-by-path-ids type (:path node))] + (st/emit! + (modal/hide) + (dwtl/bulk-create-tokens selected-token-set-id tokens-in-path-ids type node new-node-name)))) + open-rename-node-modal ;; When user renames a node, we display a form modal (mf/use-fn @@ -271,7 +277,18 @@ (let [on-rename-node-handler #(on-rename-node node type %)] (st/emit! (modal/show :tokens/rename-node {:node node :tokens-in-active-set selected-token-set-tokens - :on-rename on-rename-node-handler})))))] + :on-rename on-rename-node-handler}))))) + + open-duplicate-node-modal + (mf/use-fn + (mf/deps selected-token-set-tokens on-duplicate-node) + (fn [node type] + (let [on-duplicate-node-handler #(on-duplicate-node node type %)] + (st/emit! (modal/show :tokens/rename-node {:new-node-name (str (:name node) "-copy") + :node node + :variant "duplicate" + :tokens-in-active-set selected-token-set-tokens + :on-rename on-duplicate-node-handler})))))] (mf/with-effect [tokens-lib selected-token-set-id] (when (and tokens-lib @@ -286,6 +303,7 @@ [:* [:& token-context-menu {:on-delete-token delete-token}] [:> token-node-context-menu* {:on-rename-node open-rename-node-modal + :on-duplicate-node open-duplicate-node-modal :on-delete-node delete-node}] [:> selected-set-info* {:tokens-lib tokens-lib diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs index cb630d5b55..c786852441 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs @@ -18,15 +18,15 @@ [rumext.v2 :as mf])) (mf/defc rename-node-form* - [{:keys [node active-tokens tokens-tree on-close on-submit]}] + [{:keys [new-node-name node active-tokens tokens-tree variant on-close on-submit]}] (let [make-schema #(cfo/make-node-token-schema active-tokens tokens-tree node) schema (mf/with-memo [active-tokens] (make-schema)) - initial (mf/with-memo [node] - {:name (:name node)}) + initial (mf/with-memo [node new-node-name] + {:name (d/nilv new-node-name (:name node))}) form (fm/use-form :schema schema :initial initial) @@ -35,11 +35,10 @@ (mf/deps form on-submit) (fn [] (let [name (get-in @form [:clean-data :name])] - (when (and (get-in @form [:touched :name]) (not= name (:name node))) + (when (not= name (:name node)) (on-submit name))))) is-disabled? (or (not (:valid @form)) - (not (get-in @form [:touched :name])) (= (get-in @form [:clean-data :name]) (:name node))) hint-path (mf/with-memo [@form node] @@ -64,7 +63,7 @@ :max-length 255 :variant "comfortable" :hint-type "hint" - :hint-message (tr "workspace.tokens.rename-group-name-hint" hint-path) + :hint-message (when (= variant "rename") (tr "workspace.tokens.rename-group-name-hint" hint-path)) :auto-focus true}] [:div {:class (stl/css :form-actions)} [:> button* {:variant "secondary" @@ -72,14 +71,16 @@ :on-click on-close} (tr "labels.cancel")] [:> fc/form-submit* {:variant "primary" :disabled is-disabled? - :name "rename"} (tr "labels.rename")]]])) + :name "rename"} (if (= variant "rename") (tr "labels.rename") (tr "labels.duplicate"))]]])) (mf/defc rename-node-modal {::mf/register modal/components ::mf/register-as :tokens/rename-node} - [{:keys [node tokens-in-active-set on-rename]}] + [{:keys [new-node-name node tokens-in-active-set on-rename variant]}] - (let [tokens-tree-in-selected-set + (let [variant (d/nilv variant "rename") ;; "rename" or "duplicate" + + tokens-tree-in-selected-set (mf/with-memo [tokens-in-active-set node] (-> (ctob/tokens-tree tokens-in-active-set) (d/dissoc-in (:name node)))) @@ -111,7 +112,9 @@ :aria-label (tr "labels.close") :variant "ghost" :icon i/close}] - [:> rename-node-form* {:node node + [:> rename-node-form* {:new-node-name new-node-name + :node node + :variant variant :active-tokens tokens-in-active-set :tokens-tree tokens-tree-in-selected-set :on-close close-modal diff --git a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs index a75966f9e4..6b49e7df0a 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs @@ -27,7 +27,7 @@ (mf/defc token-node-context-menu* {::mf/schema schema:token-node-context-menu} - [{:keys [on-rename-node on-delete-node]}] + [{:keys [on-rename-node on-duplicate-node on-delete-node]}] (let [mdata (mf/deref tokens-node-menu-ref) is-open? (boolean mdata) dropdown-ref (mf/use-ref) @@ -44,6 +44,13 @@ type (get mdata :type)] (when node (on-rename-node node type))))) + duplicate-node (mf/use-fn + (mf/deps mdata on-duplicate-node) + (fn [] + (let [node (get mdata :node) + type (get mdata :type)] + (when node + (on-duplicate-node node type))))) container (hooks/use-portal-container) delete-node (mf/use-fn @@ -90,6 +97,11 @@ :type "button" :on-click rename-node} (tr "labels.rename")]] + [:li {:class (stl/css :token-node-context-menu-listitem)} + [:button {:class (stl/css :token-node-context-menu-action) + :type "button" + :on-click duplicate-node} + (tr "labels.duplicate")]] [:li {:class (stl/css :token-node-context-menu-listitem)} [:button {:class (stl/css :token-node-context-menu-action) :type "button" From 240e8ce50c99c031c9cf23bb5c3e2b3472406d22 Mon Sep 17 00:00:00 2001 From: Dexterity <173429049+Dexterity104@users.noreply.github.com> Date: Fri, 10 Apr 2026 05:21:14 -0400 Subject: [PATCH 114/288] :bug: Use page name for multi-export downloads (#8874) * :bug: Use page name for multi-export downloads * :recycle: Refactor parameter formatting in asset export function * :sparkles: Use page name for multi-export ZIP/PDF downloads [Github #8773] --------- Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + .../src/app/main/data/exports/assets.cljs | 59 +++++++++++-------- frontend/src/app/main/ui/exports/assets.cljs | 14 +++-- frontend/src/app/main/ui/inspect/exports.cljs | 2 +- 4 files changed, 46 insertions(+), 30 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c703c050b3..2f4847e0b7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,7 @@ - Save and restore selection state in undo/redo (by @eureka928) [Github #6007](https://github.com/penpot/penpot/issues/6007) - Fix warnings for unsupported token $type (by @Dexterity104) [Github #8790](https://github.com/penpot/penpot/issues/8790) - Add per-group add button for typographies (by @eureka928) [Github #5275](https://github.com/penpot/penpot/issues/5275) +- Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [Github #8773](https://github.com/penpot/penpot/issues/8773) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/data/exports/assets.cljs b/frontend/src/app/main/data/exports/assets.cljs index 8ab85b5228..143dec67d4 100644 --- a/frontend/src/app/main/data/exports/assets.cljs +++ b/frontend/src/app/main/data/exports/assets.cljs @@ -65,6 +65,9 @@ (dsh/lookup-shapes state selected) (reverse (dsh/filter-shapes state #(pos? (count (:exports %)))))) + page (dsh/lookup-page state) + page-name (:name page) + exports (for [shape shapes export (:exports shape)] (-> export @@ -76,10 +79,12 @@ (assoc :name (:name shape))))] (rx/of (modal/show :export-shapes - {:exports (vec exports) :origin origin})))))) + {:exports (vec exports) + :origin origin + :name page-name})))))) (defn show-viewer-export-dialog - [{:keys [shapes page-id file-id share-id exports]}] + [{:keys [shapes page-id file-id share-id exports name]}] (ptk/reify ::show-viewer-export-dialog ptk/WatchEvent (watch [_ _ _] @@ -93,27 +98,32 @@ (assoc :shape (dissoc shape :exports)) (assoc :name (:name shape)) (cond-> share-id (assoc :share-id share-id))))] - (rx/of (modal/show :export-shapes {:exports (vec exports) :origin "viewer"})))))) #_TODO + (rx/of (modal/show :export-shapes {:exports (vec exports) + :origin "viewer" + :name name})))))) #_TODO (defn show-workspace-export-frames-dialog [frames] (ptk/reify ::show-workspace-export-frames-dialog ptk/WatchEvent (watch [_ state _] - (let [file-id (:current-file-id state) - page-id (:current-page-id state) - exports (mapv (fn [frame] - {:enabled true - :page-id page-id - :file-id file-id - :object-id (:id frame) - :shape frame - :name (:name frame)}) - frames)] + (let [file-id (:current-file-id state) + page-id (:current-page-id state) + page (dsh/lookup-page state) + page-name (:name page) + exports (mapv (fn [frame] + {:enabled true + :page-id page-id + :file-id file-id + :object-id (:id frame) + :shape frame + :name (:name frame)}) + frames)] (rx/of (modal/show :export-frames {:exports exports - :origin "workspace:menu"})))))) + :origin "workspace:menu" + :name page-name})))))) (defn- initialize-export-status [exports cmd resource] @@ -197,7 +207,7 @@ (rx/throw cause))))))))))) (defn request-multiple-export - [{:keys [exports cmd] + [{:keys [exports cmd name] :or {cmd :export-shapes} :as params}] (ptk/reify ::request-multiple-export @@ -206,14 +216,17 @@ (let [resource-id (volatile! nil) profile-id (:profile-id state) ws-conn (:ws-conn state) - params {:exports exports - :cmd cmd - :profile-id profile-id - :force-multiple true - :is-wasm - (and - (features/active-feature? state "render-wasm/v1") - (contains? cf/flags :wasm-export))} + params (cond-> + {:exports exports + :cmd cmd + :profile-id profile-id + :force-multiple true + :is-wasm + (and + (features/active-feature? state "render-wasm/v1") + (contains? cf/flags :wasm-export))} + (some? name) + (assoc :name name)) progress-stream (->> (ws/get-rcv-stream ws-conn) diff --git a/frontend/src/app/main/ui/exports/assets.cljs b/frontend/src/app/main/ui/exports/assets.cljs index feb7f52906..a32a2ca5f6 100644 --- a/frontend/src/app/main/ui/exports/assets.cljs +++ b/frontend/src/app/main/ui/exports/assets.cljs @@ -36,7 +36,7 @@ (mf/defc export-multiple-dialog* {::mf/private true} - [{:keys [exports title cmd no-selection origin]}] + [{:keys [exports title cmd no-selection origin name]}] (let [lstate (mf/deref refs/export) in-progress? (:in-progress lstate) exports (mf/use-state exports) @@ -59,7 +59,7 @@ (fn [event] (dom/prevent-default event) (st/emit! (modal/hide) - (de/request-multiple-export {:exports enabled-exports :cmd cmd}) + (de/request-multiple-export {:exports enabled-exports :cmd cmd :name name}) (de/export-shapes-event enabled-exports origin))) on-toggle-enabled @@ -185,25 +185,27 @@ (mf/defc export-shapes-dialog {::mf/register modal/components ::mf/register-as :export-shapes} - [{:keys [exports origin]}] + [{:keys [exports origin name]}] (let [title (tr "dashboard.export-shapes.title")] [:> export-multiple-dialog* {:exports exports :title title :cmd :export-shapes :no-selection shapes-no-selection - :origin origin}])) + :origin origin + :name name}])) (mf/defc export-frames {::mf/register modal/components ::mf/register-as :export-frames} - [{:keys [exports origin]}] + [{:keys [exports origin name]}] (let [title (tr "dashboard.export-frames.title")] [:> export-multiple-dialog* {:exports exports :title title :cmd :export-frames - :origin origin}])) + :origin origin + :name name}])) ;; FIXME: deprecated, should be refactored in two components and use ;; the generic progress reporter diff --git a/frontend/src/app/main/ui/inspect/exports.cljs b/frontend/src/app/main/ui/inspect/exports.cljs index 7240ad59ea..04cd8260ac 100644 --- a/frontend/src/app/main/ui/inspect/exports.cljs +++ b/frontend/src/app/main/ui/inspect/exports.cljs @@ -47,7 +47,7 @@ (if (= :multiple type) (st/emit! (de/show-viewer-export-dialog {:shapes shapes :exports @exports - :filename filename + :name filename :page-id page-id :file-id file-id :share-id share-id})) From 3312bfe62c2da86b53e82b7151f05ab24019190c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Wed, 11 Mar 2026 14:15:24 +0100 Subject: [PATCH 115/288] :sparkles: Force current set as active when resolving tokens in sidebar --- CHANGES.md | 1 + common/src/app/common/types/tokens_lib.cljc | 16 ++++++++++ .../playwright/ui/specs/tokens/crud.spec.js | 22 ++++++++++++++ .../src/app/main/ui/workspace/sidebar.cljs | 30 +++++++++++++------ 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2f4847e0b7..026fdc1b8e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -43,6 +43,7 @@ - Fix plugin modal drag interactions over iframe and close-button behavior (by @marekhrabe) [Github #8871](https://github.com/penpot/penpot/pull/8871) - Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923) - Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930) +- Display resolved values of inactive tokens [Taiga #13628](https://tree.taiga.io/project/penpot/issue/13628) ## 2.15.0 (Unreleased) diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index b391421af2..65e49ac443 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -933,6 +933,7 @@ Will return a value that matches this schema: `:all` All of the nested sets are active `:partial` Mixed active state of nested sets") (get-tokens-in-active-sets [_] "set of set names that are active in the the active themes") + (get-tokens-in-active-sets-force [_ force-set-id] "same as above but forcing a set to be active, even if it's not in the active themes") (get-all-tokens [_] "all tokens in the lib, as a sequence") (get-all-tokens-map [_] "all tokens in the lib, as a map name -> token") (get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name")) @@ -1330,6 +1331,21 @@ Will return a value that matches this schema: active-set-names)] tokens)) + (get-tokens-in-active-sets-force [this force-set-id] + (let [theme-set-names (get-active-themes-set-names this) + all-set-names (get-set-names this) + force-set (get-set this force-set-id) + active-set-names (cond-> (filter theme-set-names all-set-names) + (some? force-set) + (conj (get-name force-set))) + + tokens (reduce (fn [tokens set-name] + (let [set (get-set-by-name this set-name)] + (merge tokens (get-tokens- set)))) + (d/ordered-map) + active-set-names)] + tokens)) + (get-all-tokens [this] (mapcat #(vals (get-tokens- %)) (get-sets this))) diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index 4b9266fa60..4bfd1c6a4b 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -7,6 +7,7 @@ import { setupTypographyTokensFileRender, testTokenCreationFlow, unfoldTokenType, + createToken, } from "./helpers"; test.beforeEach(async ({ page }) => { @@ -1790,6 +1791,27 @@ test("User duplicate color token", async ({ page }) => { ).toBeVisible(); }); +test("User disables the current set but token still have resolved values shown in the sidebar", async ({ + page, +}) => { + const { tokenThemesSetsSidebar, tokensSidebar } = await setupEmptyTokensFileRender(page); + + // Create color token + await createToken(page, "Color", "color.primary", "Value", "#ff0000"); + await unfoldTokenType(tokensSidebar, "color"); + + // Deactivate current set + await tokenThemesSetsSidebar + .getByRole("checkbox") + .click(); + + // Tokens tab panel should have a token with the color #ff0000 and correct resolved value in the tooltip + const colorTokenPill = tokensSidebar.getByRole("button", { name: "#ff0000 color.primary" }); + await expect(colorTokenPill).toHaveCount(1); + await colorTokenPill.hover(); // Force title attribute to be attached to the button + await expect(colorTokenPill).toHaveAttribute("title", /Resolved value: #ff0000/); +}); + test.describe("Tokens tab - edition", () => { test("User edits typography token and all fields are valid", async ({ page, diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index 7db1fa2077..ae43b60052 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -372,14 +372,26 @@ (ctob/get-tokens-in-active-sets tokens-lib) {})) + selected-token-set-id + (mf/deref refs/selected-token-set-id) + + active-tokens-force-set + (mf/with-memo [tokens-lib selected-token-set-id] + (if (and tokens-lib selected-token-set-id) + (ctob/get-tokens-in-active-sets-force tokens-lib selected-token-set-id) + {})) + tokenscript? (contains? cf/flags :tokenscript) - tokenscript-resolved-active-tokens - (mf/with-memo [tokens-lib tokenscript?] - (when tokenscript? (ts/resolve-tokens active-tokens))) - resolved-active-tokens - (sd/use-resolved-tokens* active-tokens)] + (sd/use-resolved-tokens* active-tokens) + + tokenscript-resolved-active-tokens-force-set + (mf/with-memo [active-tokens-force-set tokenscript?] + (when tokenscript? (ts/resolve-tokens active-tokens-force-set))) + + resolved-active-tokens-force-set + (sd/use-resolved-tokens* active-tokens-force-set)] [:* (if (:collapse-left-sidebar layout) @@ -388,10 +400,10 @@ :file file :page-id page-id :tokens-lib tokens-lib - :active-tokens active-tokens - :resolved-active-tokens (if (contains? cf/flags :tokenscript) - tokenscript-resolved-active-tokens - resolved-active-tokens)}]) + :active-tokens active-tokens-force-set + :resolved-active-tokens (if tokenscript? + tokenscript-resolved-active-tokens-force-set + resolved-active-tokens-force-set)}]) [:> right-sidebar* {:section section :selected selected :drawing-tool drawing-tool From e7e5a19db7868d4f4e35e33433e875939e1c2655 Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Fri, 10 Apr 2026 14:43:29 +0200 Subject: [PATCH 116/288] :wrench: Prevent draft pr from executing the CI (#8934) --- .github/workflows/commit-checker.yml | 3 +++ .github/workflows/tests-mcp.yml | 4 +++- .github/workflows/tests.yml | 14 +++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/commit-checker.yml b/.github/workflows/commit-checker.yml index f7126a40cb..a80e6e4cc0 100644 --- a/.github/workflows/commit-checker.yml +++ b/.github/workflows/commit-checker.yml @@ -6,12 +6,14 @@ on: - edited - reopened - synchronize + - ready_for_review pull_request_target: types: - opened - edited - reopened - synchronize + - ready_for_review push: branches: - main @@ -20,6 +22,7 @@ on: jobs: check-commit-message: + if: ${{ !github.event.pull_request.draft }} name: Check Commit Message runs-on: ubuntu-latest steps: diff --git a/.github/workflows/tests-mcp.yml b/.github/workflows/tests-mcp.yml index 489899863f..0ab2909b72 100644 --- a/.github/workflows/tests-mcp.yml +++ b/.github/workflows/tests-mcp.yml @@ -10,6 +10,7 @@ on: types: - opened - synchronize + - ready_for_review paths: - 'mcp/**' @@ -24,7 +25,8 @@ on: - 'mcp/**' jobs: - test: + test-mcp: + if: ${{ !github.event.pull_request.draft }} name: "Test MCP" runs-on: penpot-runner-02 container: penpotapp/devenv:latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 77505663ed..b44b95a941 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,6 +9,7 @@ on: types: - opened - synchronize + - ready_for_review push: branches: - develop @@ -20,6 +21,7 @@ concurrency: jobs: lint: + if: ${{ !github.event.pull_request.draft }} name: "Linter" runs-on: penpot-runner-02 container: penpotapp/devenv:latest @@ -79,6 +81,7 @@ jobs: pnpm run lint test-common: + if: ${{ !github.event.pull_request.draft }} name: "Common Tests" runs-on: penpot-runner-02 container: penpotapp/devenv:latest @@ -93,6 +96,7 @@ jobs: ./scripts/test test-plugins: + if: ${{ !github.event.pull_request.draft }} name: Plugins Runtime Linter & Tests runs-on: penpot-runner-02 container: penpotapp/devenv:latest @@ -143,6 +147,7 @@ jobs: run: pnpm run build:styles-example test-frontend: + if: ${{ !github.event.pull_request.draft }} name: "Frontend Tests" runs-on: penpot-runner-02 container: penpotapp/devenv:latest @@ -171,6 +176,7 @@ jobs: ./scripts/test-components test-render-wasm: + if: ${{ !github.event.pull_request.draft }} name: "Render WASM Tests" runs-on: penpot-runner-02 container: penpotapp/devenv:latest @@ -208,6 +214,7 @@ jobs: path: frontend/src/app/render_wasm/api/shared.js test-backend: + if: ${{ !github.event.pull_request.draft }} name: "Backend Tests" runs-on: penpot-runner-02 container: penpotapp/devenv:latest @@ -247,6 +254,7 @@ jobs: clojure -M:dev:test --reporter kaocha.report/documentation test-library: + if: ${{ !github.event.pull_request.draft }} name: "Library Tests" runs-on: penpot-runner-02 container: penpotapp/devenv:latest @@ -261,6 +269,7 @@ jobs: ./scripts/test build-integration: + if: ${{ !github.event.pull_request.draft }} name: "Build Integration Bundle" runs-on: penpot-runner-02 container: penpotapp/devenv:latest @@ -280,8 +289,8 @@ jobs: key: "integration-bundle-${{ github.sha }}" path: frontend/resources/public - test-integration-1: + if: ${{ !github.event.pull_request.draft }} name: "Integration Tests 1/4" runs-on: penpot-runner-02 container: penpotapp/devenv:latest @@ -312,6 +321,7 @@ jobs: retention-days: 3 test-integration-2: + if: ${{ !github.event.pull_request.draft }} name: "Integration Tests 2/4" runs-on: penpot-runner-02 container: penpotapp/devenv:latest @@ -342,6 +352,7 @@ jobs: retention-days: 3 test-integration-3: + if: ${{ !github.event.pull_request.draft }} name: "Integration Tests 3/4" runs-on: penpot-runner-02 container: penpotapp/devenv:latest @@ -372,6 +383,7 @@ jobs: retention-days: 3 test-integration-4: + if: ${{ !github.event.pull_request.draft }} name: "Integration Tests 4/4" runs-on: penpot-runner-02 container: penpotapp/devenv:latest From 8dccb2a427306a2283cd9b66caa9ed0dc438f742 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:55:53 -0400 Subject: [PATCH 117/288] :sparkles: Make links in comments clickable (#8894) * :sparkles: Make links in comments clickable Detect URLs in comment text and render them as clickable links that open in a new tab. Extends the existing mention parsing to also split text elements by URL patterns, handling trailing punctuation and mixed mention+URL content. Closes #1602 * :books: Add changelog entry for clickable links in comments * :bug: Fix URL elements dropped in comment input initialization * :bug: Keep empty text elements in parse-urls to preserve cursor anchors The remove filter in parse-urls was stripping empty text elements produced by str/split at URL boundaries. These elements are needed as cursor anchor spans in the contenteditable input, without them ESC keydown and visual layout broke. Signed-off-by: eureka928 --- CHANGES.md | 1 + frontend/src/app/main/ui/comments.cljs | 45 +++++++++++++++++++------- frontend/src/app/main/ui/comments.scss | 6 ++++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 026fdc1b8e..b81ad93aa3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,7 @@ - Fix warnings for unsupported token $type (by @Dexterity104) [Github #8790](https://github.com/penpot/penpot/issues/8790) - Add per-group add button for typographies (by @eureka928) [Github #5275](https://github.com/penpot/penpot/issues/5275) - Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [Github #8773](https://github.com/penpot/penpot/issues/8773) +- Make links in comments clickable (by @eureka928) [Github #1602](https://github.com/penpot/penpot/issues/1602) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index 8f34c41277..45660b3bb8 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -45,20 +45,34 @@ (def mentions-context (mf/create-context nil)) (def r-mentions-split #"@\[[^\]]*\]\([^\)]*\)") (def r-mentions #"@\[([^\]]*)\]\(([^\)]*)\)") +(def r-url-split #"https?://[^\s\)\]]+[^\s\)\]\.,;:!?]") (def zero-width-space \u200B) -(defn- parse-comment - "Parse a comment into its elements (texts and mentions)" - [comment] - (d/interleave-all - (->> (str/split comment r-mentions-split) - (map #(hash-map :type :text :content %))) +(defn- parse-urls + "Split a text element into text and url sub-elements" + [element] + (if (= (:type element) :text) + (let [text (:content element) + parts (str/split text r-url-split) + urls (re-seq r-url-split text)] + (d/interleave-all + (map #(hash-map :type :text :content %) parts) + (map #(hash-map :type :url :content %) urls))) + [element])) - (->> (re-seq r-mentions comment) - (map (fn [[_ user id]] - {:type :mention - :content user - :data {:id id}}))))) +(defn- parse-comment + "Parse a comment into its elements (texts, mentions and urls)" + [comment] + (->> (d/interleave-all + (->> (str/split comment r-mentions-split) + (map #(hash-map :type :text :content %))) + + (->> (re-seq r-mentions comment) + (map (fn [[_ user id]] + {:type :mention + :content user + :data {:id id}})))) + (mapcat parse-urls))) (defn- parse-nodes "Parse the nodes to format a comment" @@ -146,7 +160,13 @@ [{:keys [content]}] (let [comment-elements (mf/use-memo (mf/deps content) #(parse-comment content))] (for [[idx {:keys [type content]}] (d/enumerate comment-elements)] - (case type + (if (= type :url) + [:a {:key idx + :href content + :target "_blank" + :rel "noopener noreferrer" + :class (stl/css :comment-link)} + content] [:span {:key idx :class (stl/css-case @@ -177,6 +197,7 @@ (doseq [{:keys [type content data]} (parse-comment value)] (case type :text (dom/append-child! node (create-text-node content)) + :url (dom/append-child! node (create-text-node content)) :mention (dom/append-child! node (create-mention-node (:id data) content)) nil))))) diff --git a/frontend/src/app/main/ui/comments.scss b/frontend/src/app/main/ui/comments.scss index 79abfc420f..051a6cd613 100644 --- a/frontend/src/app/main/ui/comments.scss +++ b/frontend/src/app/main/ui/comments.scss @@ -418,6 +418,12 @@ color: var(--color-accent-primary); } +.comment-link { + color: var(--color-accent-primary); + text-decoration: underline; + cursor: pointer; +} + .comments-mentions-empty { font-size: deprecated.$fs-12; color: var(--color-foreground-secondary); From 78a16d99a9e90b9ee6e5e5b20f1f89fc52488286 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:58:23 -0400 Subject: [PATCH 118/288] :sparkles: Add clear artboard guides option to context menu (#8936) * :sparkles: Add clear artboard guides option to context menu Adds a "Clear artboard guides" option to the right-click context menu when one or more frames with guides are selected. Closes #6987 * :recycle: Address review feedback from niwinz - Replace deprecated dm/assert! with assert - Replace (map :id) with d/xf:map-id Signed-off-by: eureka928 --- CHANGES.md | 1 + .../src/app/main/data/workspace/guides.cljs | 31 +++++++++++++++++++ frontend/src/app/main/refs.cljs | 3 ++ .../app/main/ui/workspace/context_menu.cljs | 21 +++++++++++++ frontend/translations/en.po | 3 ++ 5 files changed, 59 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index b81ad93aa3..031efd2165 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ ### :sparkles: New features & Enhancements +- Add "Clear artboard guides" option to right-click context menu for frames (by @eureka928) [Github #6987](https://github.com/penpot/penpot/issues/6987) - Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912) - Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248) - Import Tokens from linked library (by @dfelinto) [Github #8391](https://github.com/penpot/penpot/pull/8391) diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs index 4ef75ee613..16762ad3ed 100644 --- a/frontend/src/app/main/data/workspace/guides.cljs +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -6,6 +6,7 @@ (ns app.main.data.workspace.guides (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.changes-builder :as pcb] [app.common.geom.point :as gpt] @@ -77,6 +78,36 @@ guides (-> (select-keys guides ids) (vals))] (rx/from (mapv remove-guide guides)))))) +(defn remove-frame-guides + [frame-ids] + + (assert (every? uuid? frame-ids) "expected a coll of uuids") + + (ptk/reify ::remove-frame-guides + ptk/UpdateEvent + (update [_ state] + (let [{:keys [guides]} (dsh/lookup-page state) + frame-ids-set (set frame-ids) + guide-ids (into #{} + (comp (filter #(contains? frame-ids-set (:frame-id %))) + d/xf:map-id) + (vals guides))] + (update-in state [:workspace-guides :hover] + (fn [hover] (reduce disj (or hover #{}) guide-ids))))) + + ptk/WatchEvent + (watch [it state _] + (let [{:keys [guides] :as page} (dsh/lookup-page state) + frame-ids-set (set frame-ids) + to-remove (filter #(contains? frame-ids-set (:frame-id %)) (vals guides)) + changes (reduce + (fn [acc {:keys [id]}] + (pcb/set-guide acc id nil)) + (-> (pcb/empty-changes it) + (pcb/with-page page)) + to-remove)] + (rx/of (dwc/commit-changes changes)))))) + (defmethod ptk/resolve ::move-frame-guides [_ args] (dm/assert! diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 20074c584b..1db210344a 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -306,6 +306,9 @@ (def workspace-page-flows (l/derived #(-> % :flows not-empty) workspace-page)) +(def workspace-page-guides + (l/derived :guides workspace-page)) + (defn workspace-page-object-by-id [page-id shape-id] (l/derived #(dsh/lookup-shape % page-id shape-id) st/state =)) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index a441963ea7..23552907fe 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -21,6 +21,7 @@ [app.main.data.modal :as modal] [app.main.data.shortcuts :as scd] [app.main.data.workspace :as dw] + [app.main.data.workspace.guides :as dwg] [app.main.data.workspace.interactions :as dwi] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.selection :as dws] @@ -646,6 +647,25 @@ [:> menu-entry* {:title (tr "workspace.shape.menu.combine-as-variants") :on-click do-combine-as-variants}]])])) +(mf/defc context-menu-guides* + {::mf/props :obj + ::mf/private true} + [{:keys [shapes]}] + (let [frame-ids (into #{} (comp (filter cfh/frame-shape?) d/xf:map-id) shapes) + guides (mf/deref refs/workspace-page-guides) + has-guides? (some #(contains? frame-ids (:frame-id %)) (vals guides)) + + do-remove-guides + (mf/use-fn + (mf/deps frame-ids) + #(st/emit! (dwg/remove-frame-guides frame-ids)))] + + (when (and (seq frame-ids) has-guides?) + [:* + [:> menu-separator* {}] + [:> menu-entry* {:title (tr "workspace.shape.menu.clear-guides") + :on-click do-remove-guides}]]))) + (mf/defc context-menu-delete* {::mf/props :obj ::mf/private true} @@ -687,6 +707,7 @@ (when is-not-variant-container? [:> context-menu-layout* props]) [:> context-menu-component* props] + [:> context-menu-guides* props] [:> context-menu-delete* props]]))) (mf/defc page-item-context-menu* diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 68521498ed..6e7bb1b24d 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7867,6 +7867,9 @@ msgid "workspace.shape.menu.show-main" msgstr "Show main component" #: src/app/main/ui/workspace/context_menu.cljs:314 +msgid "workspace.shape.menu.clear-guides" +msgstr "Clear artboard guides" + msgid "workspace.shape.menu.thumbnail-remove" msgstr "Remove thumbnail" From 707cc53ca4f3c8f1409116baa06659e2788ebdb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Mon, 13 Apr 2026 11:41:32 +0200 Subject: [PATCH 119/288] Revert :sparkles: Add can use trial prop in nitrate profile (#8954) --- backend/src/app/rpc/management/nitrate.clj | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 9cd5200341..440b3022b4 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -33,27 +33,15 @@ ;; ---- API: authenticate -(def ^:private schema:nitrate-profile - [:map {:title "NitrateProfile"} - [:id ::sm/uuid] - [:name {:optional true} :string] - [:email {:optional true} :string] - [:photo-url {:optional true} :string] - [:theme {:optional true} :string] - [:can-use-trial ::sm/boolean]]) - (sv/defmethod ::authenticate "Authenticate the current user" {::doc/added "2.14" ::sm/params [:map] - ::sm/result schema:nitrate-profile} + ::sm/result schema:profile} [cfg {:keys [::rpc/profile-id] :as params}] - (let [profile (profile/get-profile cfg profile-id) - nitrate-subscription (:subscription profile) - can-use-nitrate-trial (nil? nitrate-subscription)] + (let [profile (profile/get-profile cfg profile-id)] (-> (profile-to-map profile) - (assoc :can-use-nitrate-trial can-use-nitrate-trial - :theme (:theme profile))))) + (assoc :theme (:theme profile))))) ;; ---- API: get-teams From 5c761125f3b088cac76bb234d404df217679efdf Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 9 Apr 2026 10:38:13 +0200 Subject: [PATCH 120/288] :sparkles: Add invite-to-org to Nitrate API --- .../resources/app/email/invite-to-org/en.html | 259 ++++++++++++++++++ .../resources/app/email/invite-to-org/en.subj | 1 + .../resources/app/email/invite-to-org/en.txt | 10 + .../resources/app/email/invite-to-team/en.txt | 2 +- backend/src/app/email.clj | 15 + backend/src/app/migrations.clj | 6 +- .../sql/0147-mod-team-invitation-table.sql | 13 + backend/src/app/rpc/commands/teams.clj | 51 ++-- .../app/rpc/commands/teams_invitations.clj | 125 +++++++-- backend/src/app/rpc/commands/verify_token.clj | 115 +++++--- backend/src/app/rpc/management/nitrate.clj | 16 ++ .../rpc_management_nitrate_test.clj | 1 + common/src/app/common/data.cljc | 10 + common/test/common_tests/data_test.cljc | 8 + .../src/app/main/ui/auth/verify_token.cljs | 19 +- .../app/main/ui/components/org_avatar.cljs | 12 +- frontend/translations/en.po | 3 + frontend/translations/es.po | 3 + 18 files changed, 552 insertions(+), 117 deletions(-) create mode 100644 backend/resources/app/email/invite-to-org/en.html create mode 100644 backend/resources/app/email/invite-to-org/en.subj create mode 100644 backend/resources/app/email/invite-to-org/en.txt create mode 100644 backend/src/app/migrations/sql/0147-mod-team-invitation-table.sql diff --git a/backend/resources/app/email/invite-to-org/en.html b/backend/resources/app/email/invite-to-org/en.html new file mode 100644 index 0000000000..912b67746b --- /dev/null +++ b/backend/resources/app/email/invite-to-org/en.html @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + +
+
+ Hi{{ user-name|abbreviate:25 }}, +
+
+
+ {{invited-by|abbreviate:25}} sent you an invitation to join the organization {{ org-name|abbreviate:25 }}: +
+
+
+ + {{ org-initials }} + + + + “{{ org-name|abbreviate:25 }}” + +
+
+ + + + +
+ ACCEPT INVITE +
+
+
+ Enjoy!
+
+
+ The Penpot team.
+
+
+ +
+
+ + {% include "app/email/includes/footer.html" %} + +
+ + + diff --git a/backend/resources/app/email/invite-to-org/en.subj b/backend/resources/app/email/invite-to-org/en.subj new file mode 100644 index 0000000000..d61232bfd3 --- /dev/null +++ b/backend/resources/app/email/invite-to-org/en.subj @@ -0,0 +1 @@ +{{invited-by|abbreviate:25}} has invited you to join the organization “{{ org-name|abbreviate:25 }}” \ No newline at end of file diff --git a/backend/resources/app/email/invite-to-org/en.txt b/backend/resources/app/email/invite-to-org/en.txt new file mode 100644 index 0000000000..65317deb6e --- /dev/null +++ b/backend/resources/app/email/invite-to-org/en.txt @@ -0,0 +1,10 @@ +Hello! + +{{invited-by|abbreviate:25}} has invited you to join the organization “{{ org-name|abbreviate:25 }}”. + +Accept invitation using this link: + +{{ public-uri }}/#/auth/verify-token?token={{token}} + +Enjoy! +The Penpot team. diff --git a/backend/resources/app/email/invite-to-team/en.txt b/backend/resources/app/email/invite-to-team/en.txt index 55e61d8e23..3482fab0a5 100644 --- a/backend/resources/app/email/invite-to-team/en.txt +++ b/backend/resources/app/email/invite-to-team/en.txt @@ -1,6 +1,6 @@ Hello! -{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”. +{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:25 }}"{% if organization %}, part of the organization "{{ organization|abbreviate:25 }}"{% endif %}. Accept invitation using this link: diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index 44d5cd7e67..bd68ec92ee 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -412,6 +412,21 @@ :id ::invite-to-team :schema schema:invite-to-team)) +(def ^:private schema:invite-to-org + [:map + [:invited-by ::sm/text] + [:org-name ::sm/text] + [:org-initials ::sm/text] + [:org-logo ::sm/uri] + [:user-name [:maybe ::sm/text]] + [:token ::sm/text]]) + +(def invite-to-org + "Org member invitation email." + (template-factory + :id ::invite-to-org + :schema schema:invite-to-org)) + (def ^:private schema:join-team [:map [:invited-by ::sm/text] diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 4c9199a6f5..2551f29fff 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -466,7 +466,11 @@ :fn mg0145/migrate} {:name "0146-mod-access-token-table" - :fn (mg/resource "app/migrations/sql/0146-mod-access-token-table.sql")}]) + :fn (mg/resource "app/migrations/sql/0146-mod-access-token-table.sql")} + + {:name "0147-mod-team-invitation-table" + :fn (mg/resource "app/migrations/sql/0147-mod-team-invitation-table.sql")}]) + (defn apply-migrations! [pool name migrations] diff --git a/backend/src/app/migrations/sql/0147-mod-team-invitation-table.sql b/backend/src/app/migrations/sql/0147-mod-team-invitation-table.sql new file mode 100644 index 0000000000..6c60428f06 --- /dev/null +++ b/backend/src/app/migrations/sql/0147-mod-team-invitation-table.sql @@ -0,0 +1,13 @@ +ALTER TABLE team_invitation + ADD COLUMN org_id uuid NULL; + +ALTER TABLE team_invitation + ALTER COLUMN team_id DROP NOT NULL; + +ALTER TABLE team_invitation + ADD CONSTRAINT team_invitation_team_or_org_not_null + CHECK (team_id IS NOT NULL OR org_id IS NOT NULL); + +CREATE UNIQUE INDEX team_invitation_org_unique + ON team_invitation (org_id, email_to) + WHERE team_id IS NULL; diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index b849ba0aa7..25cbe48640 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -539,35 +539,29 @@ team (create-team cfg params)] (select-keys team [:id]))) -(defn- initialize-user-in-nitrate-org +(defn initialize-user-in-nitrate-org "If needed, create a default team for the user on the organization, and notify Nitrate that an user has been added to an org." - [cfg profile-id team-id] + [cfg profile-id org-id] (assert (db/connection-map? cfg) "expected cfg with valid connection") - (let [membership (nitrate/call cfg :get-org-membership-by-team {:profile-id profile-id :team-id team-id})] - ;; Only when the team belong to an organization and the user is not a member - (when (and - (some? (:organization-id membership)) ;; the team do belong to an organization - (not (:is-member membership))) ;; the user is not a member of the org yet - - (db/tx-run! - cfg - (fn [{:keys [::db/conn] :as tx-cfg}] - (let [org-id (:organization-id membership) - default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id org-id) - default-team-id (:id default-team) - result (nitrate/call tx-cfg :add-profile-to-org {:profile-id profile-id - :team-id default-team-id - :org-id org-id})] - (when (not (:is-member result)) - (ex/raise :type :internal - :code :failed-add-profile-org-nitrate - :context {:profile-id profile-id - :team-id team-id - :org-id org-id - :default-team-id default-team-id})) - nil)))))) + (when (contains? cf/flags :nitrate) + (db/tx-run! + cfg + (fn [{:keys [::db/conn] :as tx-cfg}] + (let [org-id org-id + default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id org-id) + default-team-id (:id default-team) + result (nitrate/call tx-cfg :add-profile-to-org {:profile-id profile-id + :team-id default-team-id + :org-id org-id})] + (when (not (:is-member result)) + (ex/raise :type :internal + :code :failed-add-profile-org-nitrate + :context {:profile-id profile-id + :org-id org-id + :default-team-id default-team-id})) + default-team-id))))) (defn add-profile-to-team! ([cfg params] @@ -576,7 +570,12 @@ (assert (db/connection-map? cfg) "expected cfg with valid connection") (when (contains? cf/flags :nitrate) - (initialize-user-in-nitrate-org cfg profile-id team-id)) + (let [membership (nitrate/call cfg :get-org-membership-by-team {:profile-id profile-id :team-id team-id})] + ;; Only when the team belong to an organization and the user is not a member + (when (and + (some? (:organization-id membership)) ;; the team do belong to an organization + (not (:is-member membership))) ;; the user is not a member of the org yet + (initialize-user-in-nitrate-org cfg profile-id (:organization-id membership))))) (db/insert! conn :team-profile-rel params options))) (defn create-team diff --git a/backend/src/app/rpc/commands/teams_invitations.clj b/backend/src/app/rpc/commands/teams_invitations.clj index d4a2f96ded..730a3c8887 100644 --- a/backend/src/app/rpc/commands/teams_invitations.clj +++ b/backend/src/app/rpc/commands/teams_invitations.clj @@ -36,20 +36,29 @@ ;; --- Mutation: Create Team Invitation (def sql:upsert-team-invitation - "insert into team_invitation(id, team_id, email_to, created_by, role, valid_until) - values (?, ?, ?, ?, ?, ?) + "insert into team_invitation(id, team_id, org_id, email_to, created_by, role, valid_until) + values (?, ?, null, ?, ?, ?, ?) on conflict(team_id, email_to) do update set role = ?, valid_until = ?, updated_at = now() returning *") +(def sql:upsert-org-invitation + "insert into team_invitation(id, team_id, org_id, email_to, created_by, role, valid_until) + values (?, null, ?, ?, ?, ?, ?) + on conflict(org_id, email_to) where team_id is null do + update set role = ?, valid_until = ?, updated_at = now() + returning *") + (defn- create-invitation-token - [cfg {:keys [profile-id valid-until team-id member-id member-email role]}] + [cfg {:keys [profile-id valid-until org-id org-name team-id member-id member-email role]}] (tokens/generate cfg {:iss :team-invitation :exp valid-until :profile-id profile-id :role role :team-id team-id + :org-id org-id + :org-name org-name :member-email member-email :member-id member-id})) @@ -75,20 +84,40 @@ [:role types.team/schema:role] [:email ::sm/email]]) +(def ^:private schema:create-org-invitation + [:map {:title "params:create-org-invitation"} + [::rpc/profile-id ::sm/uuid] + [:organization + [:map + [:id ::sm/uuid] + [:name :string] + [:logo ::sm/uri]]] + [:profile + [:map + [:id ::sm/uuid] + [:fullname :string]]] + [:role types.team/schema:role] + [:email ::sm/email]]) + (def ^:private check-create-invitation-params (sm/check-fn schema:create-invitation)) +(def ^:private check-create-org-invitation-params + (sm/check-fn schema:create-org-invitation)) + (defn- allow-invitation-emails? [member] (let [notifications (dm/get-in member [:props :notifications])] (not= :none (:email-invites notifications)))) (defn- create-invitation - [{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}] + [{:keys [::db/conn] :as cfg} {:keys [team organization profile role email] :as params}] (assert (db/connection-map? cfg) "expected cfg with valid connection") - (assert (check-create-invitation-params params)) + (if organization + (assert (check-create-org-invitation-params params)) + (assert (check-create-invitation-params params))) (let [email (profile/clean-email email) member (profile/get-profile-by-email conn email)] @@ -105,8 +134,12 @@ :profile-id (:id member)} (get types.team/permissions-for-role role))] - ;; Insert the invited member to the team - (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}) + (if organization + ;; Insert the invited member to the org + (when (contains? cf/flags :nitrate) + (teams/initialize-user-in-nitrate-org cfg (:id member) (:id organization))) + ;; Insert the invited member to the team + (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true})) ;; If profile is not yet verified, mark it as verified because ;; accepting an invitation link serves as verification. @@ -123,18 +156,30 @@ (teams/check-email-spam conn email true) (let [id (uuid/next) - expire (ct/in-future "168h") ;; 7 days - invitation (db/exec-one! conn [sql:upsert-team-invitation id - (:id team) (str/lower email) - (:id profile) - (name role) expire - (name role) expire]) + expire (if organization + (ct/in-future "876000h") ;; Organization invitations doesn't expire + (ct/in-future "168h")) ;; 7 days + invitation (db/exec-one! conn (if organization + [sql:upsert-org-invitation id + (:id organization) + (str/lower email) + (:id profile) + (name role) expire + (name role) expire] + [sql:upsert-team-invitation id + (:id team) + (str/lower email) + (:id profile) + (name role) expire + (name role) expire])) updated? (not= id (:id invitation)) profile-id (:id profile) tprops {:profile-id profile-id :invitation-id (:id invitation) :valid-until expire :team-id (:id team) + :org-id (:id organization) + :org-name (:name organization) :member-email (:email-to invitation) :member-id (:id member) :role role} @@ -146,30 +191,54 @@ (let [props (-> (dissoc tprops :profile-id) (audit/clean-props)) - evname (if updated? - "update-team-invitation" - "create-team-invitation") + evname (cond + (and updated? organization) "update-org-invitation" + updated? "update-team-invitation" + organization "create-org-invitation" + :else "create-team-invitation") event (-> (audit/event-from-rpc-params params) (assoc ::audit/name evname) (assoc ::audit/props props))] (audit/submit! cfg event)) (when (allow-invitation-emails? member) - (let [team (if (contains? cf/flags :nitrate) - (nitrate/add-org-info-to-team cfg team {}) - team)] - (eml/send! {::eml/conn conn - ::eml/factory eml/invite-to-team - :public-uri (cf/get :public-uri) - :to email - :invited-by (:fullname profile) - :team (:name team) - :organization (:organization-name team) - :token itoken - :extra-data ptoken}))) + (if organization + (when (contains? cf/flags :nitrate) + (eml/send! {::eml/conn conn + ::eml/factory eml/invite-to-org + :public-uri (cf/get :public-uri) + :to email + :invited-by (:fullname profile) + :user-name (:fullname member) + :org-name (:name organization) + :org-logo (:logo organization) + :org-initials (d/get-initials (:name organization)) + :token itoken + :extra-data ptoken})) + (let [team (if (contains? cf/flags :nitrate) + (nitrate/add-org-info-to-team cfg team {}) + team)] + (eml/send! {::eml/conn conn + ::eml/factory eml/invite-to-team + :public-uri (cf/get :public-uri) + :to email + :invited-by (:fullname profile) + :team (:name team) + :organization (:organization-name team) + :token itoken + :extra-data ptoken})))) itoken))))) +(defn create-org-invitation + [cfg {:keys [::rpc/profile-id id name logo] :as params}] + (let [profile (db/get-by-id cfg :profile profile-id)] + (create-invitation cfg + (assoc params + :organization {:id id :name name :logo logo} + :profile profile + :role :editor)))) + (defn- add-member-to-team [{:keys [::db/conn] :as cfg} profile team role member] (assert (db/connection-map? cfg) diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index 9f78ef3e12..9a6fa8de9b 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -87,52 +87,74 @@ ;; --- Team Invitation (defn- accept-invitation - [{:keys [::db/conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member] + [{:keys [::db/conn] :as cfg} + {:keys [team-id org-id role member-email] :as claims} invitation member] (let [;; Update the role if there is an invitation role (or (some-> invitation :role keyword) role) - params (merge - {:team-id team-id - :profile-id (:id member)} - (get types.team/permissions-for-role role))] + id-member (:id member)] ;; Do not allow blocked users accept invitations. (when (:is-blocked member) (ex/raise :type :restriction :code :profile-blocked)) - (quotes/check! cfg {::quotes/id ::quotes/profiles-per-team - ::quotes/profile-id (:id member) - ::quotes/team-id team-id}) + (when team-id + (quotes/check! cfg {::quotes/id ::quotes/profiles-per-team + ::quotes/profile-id id-member + ::quotes/team-id team-id})) - ;; Insert the invited member to the team - (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}) + (let [params (merge + {:team-id team-id + :profile-id id-member} + (get types.team/permissions-for-role role)) - ;; If profile is not yet verified, mark it as verified because - ;; accepting an invitation link serves as verification. - (when-not (:is-active member) - (db/update! conn :profile - {:is-active true} - {:id (:id member)})) + accepted-team-id (if org-id + ;; Insert the invited member to the org + (when (contains? cf/flags :nitrate) + (teams/initialize-user-in-nitrate-org cfg id-member org-id)) + ;; Insert the invited member to the team + (do (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}) + team-id))] - ;; Delete the invitation - (db/delete! conn :team-invitation - {:team-id team-id :email-to member-email}) + (when-not accepted-team-id + (ex/raise :type :internal + :code :accept-invitation-failed + :hint "the accept invitation has failed")) - ;; Delete any request - (db/delete! conn :team-access-request - {:team-id team-id :requester-id (:id member)}) - (assoc member :is-active true))) + ;; If profile is not yet verified, mark it as verified because + ;; accepting an invitation link serves as verification. + (when-not (:is-active member) + (db/update! conn :profile + {:is-active true} + {:id id-member})) + + ;; Delete the invitation + (db/delete! conn :team-invitation + (cond-> {:email-to member-email} + team-id (assoc :team-id team-id) + org-id (assoc :org-id org-id))) + + ;; Delete any request (only applicable for team invitations) + (when team-id + (db/delete! conn :team-access-request + {:team-id team-id :requester-id id-member})) + + accepted-team-id))) (def schema:team-invitation-claims - [:map {:title "TeamInvitationClaims"} - [:iss :keyword] - [:exp ::ct/inst] - [:profile-id ::sm/uuid] - [:role types.team/schema:role] - [:team-id ::sm/uuid] - [:member-email ::sm/email] - [:member-id {:optional true} ::sm/uuid]]) + [:and + [:map {:title "TeamInvitationClaims"} + [:iss :keyword] + [:exp ::ct/inst] + [:profile-id ::sm/uuid] + [:role types.team/schema:role] + [:team-id {:optional true} ::sm/uuid] + [:org-id {:optional true} ::sm/uuid] + [:member-email ::sm/email] + [:member-id {:optional true} ::sm/uuid]] + [:fn {:error/message "team-id or org-id must be present"} + (fn [m] (or (:team-id m) (:org-id m)))]]) (def valid-team-invitation-claims? (sm/lazy-validator schema:team-invitation-claims)) @@ -140,7 +162,7 @@ (defmethod process-token :team-invitation [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id token] :as params} - {:keys [member-id team-id member-email] :as claims}] + {:keys [member-id team-id org-id member-email] :as claims}] (when-not (valid-team-invitation-claims? claims) (ex/raise :type :validation @@ -148,19 +170,27 @@ :hint "invitation token contains unexpected data")) (let [invitation (db/get* conn :team-invitation - {:team-id team-id :email-to member-email}) + (cond-> {:email-to member-email} + team-id (assoc :team-id team-id) + org-id (assoc :org-id org-id))) profile (db/get* conn :profile {:id profile-id} {:columns [:id :email]}) registration-disabled? (not (contains? cf/flags :registration))] + (when (nil? invitation) (ex/raise :type :validation :code :invalid-token :hint "no invitation associated with the token")) - (if (some? profile) - (if (or (= member-id profile-id) - (= member-email (:email profile))) + (if profile + (do + (when-not (or (= member-id profile-id) + (= member-email (:email profile))) + (ex/raise :type :validation + :code :invalid-token + :hint "logged-in user does not matches the invitation")) + ;; if we have logged-in user and it matches the invitation we proceed ;; with accepting the invitation and joining the current profile to the @@ -188,17 +218,16 @@ :profile-id (:id profile) :email (:email profile)))))) - (accept-invitation cfg claims invitation profile) - (assoc claims :state :created)) - - (ex/raise :type :validation - :code :invalid-token - :hint "logged-in user does not matches the invitation")) + (let [accepted-team-id (accept-invitation cfg claims invitation profile)] + (cond-> (assoc claims :state :created) + ;; when the invitation is to an org, instead of a team, add the + ;; accepted-team-id as :org-team-id + (:org-id claims) + (assoc :org-team-id accepted-team-id))))) ;; If we have not logged-in user, and invitation comes with member-id we ;; redirect user to login, if no memeber-id is present and in the invitation ;; token and registration is enabled, we redirect user the the register page. - {:invitation-token token :iss :team-invitation :redirect-to (if (or member-id registration-disabled?) :auth-login :auth-register) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 440b3022b4..338a59f28b 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -21,6 +21,7 @@ [app.rpc.commands.files :as files] [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] + [app.rpc.commands.teams-invitations :as ti] [app.rpc.doc :as doc] [app.util.services :as sv])) @@ -294,3 +295,18 @@ RETURNING id, name;") :id id)) (profile-to-map profile))) + +;; API: invite-to-org + +(sv/defmethod ::invite-to-org + "Invite to organization" + {::doc/added "2.15" + ::sm/params [:map + [:email ::sm/email] + [:id ::sm/uuid] + [:name ::sm/text] + [:logo ::sm/uri]]} + [cfg params] + (db/tx-run! cfg ti/create-org-invitation params) + nil) + diff --git a/backend/test/backend_tests/rpc_management_nitrate_test.clj b/backend/test/backend_tests/rpc_management_nitrate_test.clj index f6d65a675d..bceafbc72e 100644 --- a/backend/test/backend_tests/rpc_management_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_management_nitrate_test.clj @@ -11,6 +11,7 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.db :as-alias db] + [app.email :as email] [app.msgbus :as mbus] [app.rpc :as-alias rpc] [backend-tests.helpers :as th] diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index aa06e14e9d..6537a281e2 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -1181,6 +1181,16 @@ str/trim) "")) +(defn get-initials + "Returns up to two uppercase initials extracted from a string. + Non-letter prefixes in each token are ignored." + [name] + (->> (str/split (str/trim (or name "")) #"\s+") + (keep #(first (re-seq #"[a-zA-Z]" %))) + (take 2) + (map str/upper) + (apply str))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Util protocols ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/common/test/common_tests/data_test.cljc b/common/test/common_tests/data_test.cljc index 1854158b74..1a582c0c52 100644 --- a/common/test/common_tests/data_test.cljc +++ b/common/test/common_tests/data_test.cljc @@ -28,6 +28,14 @@ (t/is (not (d/in-range? 5 -1))) (t/is (not (d/in-range? 0 0)))) +(t/deftest get-initials-test + (t/is (= "JD" (d/get-initials "John Doe"))) + (t/is (= "A" (d/get-initials "acme"))) + (t/is (= "AB" (d/get-initials "123 Alpha ## beta"))) + (t/is (= "PD" (d/get-initials " penpot design tool "))) + (t/is (= "" (d/get-initials nil))) + (t/is (= "" (d/get-initials "!!! ???")))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Ordered Data Structures ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs index 334303ade4..9a3a33c47b 100644 --- a/frontend/src/app/main/ui/auth/verify_token.cljs +++ b/frontend/src/app/main/ui/auth/verify_token.cljs @@ -41,19 +41,22 @@ (st/emit! (da/login-from-token tdata))) (defmethod handle-token :team-invitation - [tdata] - (case (:state tdata) + [{:keys [state team-id org-team-id org-name invitation-token] :as tdata}] + (case state :created - (let [team-id (:team-id tdata)] + (if org-team-id (st/emit! - (ntf/success (tr "auth.notifications.team-invitation-accepted")) (du/refresh-profile) - (dcm/go-to-dashboard-recent :team-id team-id))) + (dcm/go-to-dashboard-recent :team-id org-team-id) + (ntf/success (tr "auth.notifications.org-invitation-accepted" org-name))) + (st/emit! + (du/refresh-profile) + (dcm/go-to-dashboard-recent :team-id team-id) + (ntf/success (tr "auth.notifications.team-invitation-accepted")))) :pending - (let [token (:invitation-token tdata) - route-id (:redirect-to tdata :auth-register)] - (st/emit! (rt/nav route-id {:invitation-token token}))))) + (let [route-id (:redirect-to tdata :auth-register)] + (st/emit! (rt/nav route-id {:invitation-token invitation-token}))))) (defmethod handle-token :default [_tdata] diff --git a/frontend/src/app/main/ui/components/org_avatar.cljs b/frontend/src/app/main/ui/components/org_avatar.cljs index 45095ec770..c4430af12d 100644 --- a/frontend/src/app/main/ui/components/org_avatar.cljs +++ b/frontend/src/app/main/ui/components/org_avatar.cljs @@ -7,24 +7,16 @@ (ns app.main.ui.components.org-avatar (:require-macros [app.main.style :as stl]) (:require - [cuerdas.core :as str] + [app.common.data :as d] [rumext.v2 :as mf])) -(defn- get-org-initials - [name] - (->> (str/split (str/trim (or name "")) #"\s+") - (keep #(first (re-seq #"[a-zA-Z]" %))) - (take 2) - (map str/upper) - (apply str))) - (mf/defc org-avatar* {::mf/props :obj} [{:keys [org size]}] (let [name (:name org) custom-photo (:organization-custom-photo org) avatar-bg (:organization-avatar-bg-url org) - initials (get-org-initials name)] + initials (d/get-initials name)] (if custom-photo [:img {:src custom-photo diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 6e7bb1b24d..5fc069d7fe 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -108,6 +108,9 @@ msgstr "Password recovery link sent to your inbox." msgid "auth.notifications.team-invitation-accepted" msgstr "Joined the team successfully" +msgid "auth.notifications.org-invitation-accepted" +msgstr "You're now part of %s" + #: src/app/main/ui/auth/login.cljs:188, src/app/main/ui/auth/register.cljs:174 msgid "auth.password" msgstr "Password" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 3d74830dce..59b7eb0bd5 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -114,6 +114,9 @@ msgstr "Hemos enviado a tu buzón un enlace para recuperar tu contraseña." msgid "auth.notifications.team-invitation-accepted" msgstr "Te uniste al equipo" +msgid "auth.notifications.org-invitation-accepted" +msgstr "Te uniste a la organización %s" + #: src/app/main/ui/auth/login.cljs:188, src/app/main/ui/auth/register.cljs:174 msgid "auth.password" msgstr "Contraseña" From d91ce0f9d16991d746f4d579b8285efd402eb97a Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 13 Apr 2026 12:14:40 +0200 Subject: [PATCH 121/288] :bug: Fix nitrate go to control center --- frontend/src/app/main/data/nitrate.cljs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index b65d0ad264..c62dca99f6 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -27,11 +27,13 @@ ([] (st/emit! (rt/nav-raw :href "/control-center/"))) ([{:keys [organization-id organization-slug]}] - (let [href (dm/str "/control-center/org/" - (u/percent-encode organization-slug) - "/" - (u/percent-encode (str organization-id)))] - (st/emit! (rt/nav-raw :href href))))) + (if (and organization-id organization-slug) + (let [href (dm/str "/control-center/org/" + (u/percent-encode organization-slug) + "/" + (u/percent-encode (str organization-id)))] + (st/emit! (rt/nav-raw :href href))) + (st/emit! (rt/nav-raw :href "/control-center/"))))) (defn go-to-nitrate-cc-create-org [] From 87179e806f0d6ffaa1bc4f80c27d97f05642ed38 Mon Sep 17 00:00:00 2001 From: Juanfran Date: Mon, 13 Apr 2026 15:49:22 +0200 Subject: [PATCH 122/288] :sparkles: Add subscribe-nitrate route with post-registration nitrate modal (#8941) --- frontend/src/app/main/data/nitrate.cljs | 28 ++++++++++++++++++- frontend/src/app/main/ui.cljs | 9 ++++++ frontend/src/app/main/ui/auth/register.cljs | 1 + frontend/src/app/main/ui/dashboard.cljs | 9 ++++++ frontend/src/app/main/ui/nitrate/entry.cljs | 31 +++++++++++++++++++++ frontend/src/app/main/ui/routes.cljs | 3 ++ 6 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/main/ui/nitrate/entry.cljs diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index c62dca99f6..d4f4ed533c 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -10,10 +10,36 @@ [app.main.repo :as rp] [app.main.router :as rt] [app.main.store :as st] - [app.util.i18n :as i18n :refer [tr]] + [app.util.i18n :refer [tr]] + [app.util.storage :as storage] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) +(def ^:private nitrate-entry-active-key ::nitrate-entry-active) +(def ^:private nitrate-entry-pending-popup-key ::nitrate-entry-pending-popup) + +(defn activate-nitrate-entry-popup! + [] + (binding [storage/*sync* true] + (swap! storage/storage assoc + nitrate-entry-active-key true + nitrate-entry-pending-popup-key true))) + +(defn nitrate-entry-active? + [] + (true? (get storage/storage nitrate-entry-active-key))) + +(defn nitrate-entry-popup-pending? + [] + (true? (get storage/storage nitrate-entry-pending-popup-key))) + +(defn consume-nitrate-entry-popup! + [] + (binding [storage/*sync* true] + (swap! storage/storage dissoc + nitrate-entry-active-key + nitrate-entry-pending-popup-key))) + (defn show-nitrate-popup [popup-type] (ptk/reify ::show-nitrate-popup diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 7a2b6ef86a..787c17edfa 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -10,6 +10,7 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.common :as dcm] + [app.main.data.nitrate :as dnt] [app.main.data.team :as dtm] [app.main.errors :as errors] [app.main.refs :as refs] @@ -23,6 +24,7 @@ [app.main.ui.error-boundary :refer [error-boundary*]] [app.main.ui.exports.files] [app.main.ui.frame-preview :as frame-preview] + [app.main.ui.nitrate.entry :as nitrate-entry] [app.main.ui.notifications :as notifications] [app.main.ui.onboarding.questions :refer [questions-modal]] [app.main.ui.onboarding.team-choice :refer [onboarding-team-modal]] @@ -152,21 +154,25 @@ props (get profile :props) section (get data :name) team (mf/deref refs/team) + nitrate-entry-active? (dnt/nitrate-entry-active?) show-question-modal? (and (contains? cf/flags :onboarding) + (not nitrate-entry-active?) (not (:onboarding-viewed props)) (not (contains? props :onboarding-questions))) show-team-modal? (and (contains? cf/flags :onboarding) + (not nitrate-entry-active?) (not (:onboarding-viewed props)) (not (contains? props :onboarding-team-id)) (:is-default team)) show-release-modal? (and (contains? cf/flags :onboarding) + (not nitrate-entry-active?) (not (contains? cf/flags :hide-release-modal)) (:onboarding-viewed props) (not= (:release-notes-viewed props) (:main cf/version)) @@ -185,6 +191,9 @@ :auth-verify-token [:? [:& verify-token-page* {:route route}]] + :nitrate-entry + [:> nitrate-entry/nitrate-entry-page* {:profile profile}] + (:settings-profile :settings-password :settings-options diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 3bd3fdf564..97ed5eddb3 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -221,6 +221,7 @@ :class (stl/css :demo-account-link)} (tr "auth.create-demo-account")]]])]]) + ;; --- PAGE: register success page (mf/defc register-success-page* diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 04c376def4..f28068a27b 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -13,6 +13,7 @@ [app.main.data.dashboard.shortcuts :as sc] [app.main.data.event :as ev] [app.main.data.modal :as modal] + [app.main.data.nitrate :as dnt] [app.main.data.notifications :as notif] [app.main.data.plugins :as dp] [app.main.data.project :as dpj] @@ -262,6 +263,13 @@ (binding [storage/*sync* true] (swap! storage/session dissoc :template)))))) +(defn- use-nitrate-entry-popup + [] + (mf/with-effect [] + (when (dnt/nitrate-entry-popup-pending?) + (dnt/consume-nitrate-entry-popup!) + (st/emit! (dnt/show-nitrate-popup :nitrate-form))))) + (mf/defc dashboard* [{:keys [profile project-id team-id search-term plugin-url template section]}] (let [team (mf/deref refs/team) @@ -300,6 +308,7 @@ (use-plugin-register plugin-url team-id (:id default-project)) (use-templates-import can-edit? template default-project) + (use-nitrate-entry-popup) [:& (mf/provider ctx/current-project-id) {:value project-id} [:> modal-container*] diff --git a/frontend/src/app/main/ui/nitrate/entry.cljs b/frontend/src/app/main/ui/nitrate/entry.cljs new file mode 100644 index 0000000000..4bcadf3216 --- /dev/null +++ b/frontend/src/app/main/ui/nitrate/entry.cljs @@ -0,0 +1,31 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.nitrate.entry + (:require + [app.main.data.auth :as da] + [app.main.data.nitrate :as dnt] + [app.main.router :as rt] + [app.main.store :as st] + [app.main.ui.ds.product.loader :refer [loader*]] + [app.util.i18n :refer [tr]] + [rumext.v2 :as mf])) + +(mf/defc nitrate-entry* + {::mf/private true} + [{:keys [profile]}] + (mf/with-effect [profile] + (dnt/activate-nitrate-entry-popup!) + (if (da/is-authenticated? profile) + (st/emit! (rt/nav :dashboard-recent {:team-id (:default-team-id profile)})) + (st/emit! (rt/nav :auth-register)))) + + [:> loader* {:title (tr "labels.loading") + :overlay true}]) + +(mf/defc nitrate-entry-page* + [props] + [:> nitrate-entry* props]) diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index ca45bc5133..920a79605f 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -30,6 +30,9 @@ ["/recovery" :auth-recovery] ["/verify-token" :auth-verify-token]] + (when (contains? cf/flags :nitrate) + ["/subscribe-nitrate" :nitrate-entry]) + ["/settings" ["/profile" :settings-profile] ["/password" :settings-password] From bbd200f8693c9ec2067614d3a292bdc6c93b7451 Mon Sep 17 00:00:00 2001 From: rockchris099 Date: Mon, 13 Apr 2026 09:55:13 -0400 Subject: [PATCH 123/288] :bug: Fix dashboard Recent/Deleted titles overlapped by scrolling content (#8945) Add z-index to the sticky .nav element in the dashboard so that section titles (Recent, Deleted) stay above scrolling content instead of being obscured by project cards and file thumbnails. Fixes #8577 Signed-off-by: rockchris99 --- CHANGES.md | 1 + frontend/src/app/main/ui/dashboard/deleted.scss | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 031efd2165..9300839618 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -45,6 +45,7 @@ - Fix plugin modal drag interactions over iframe and close-button behavior (by @marekhrabe) [Github #8871](https://github.com/penpot/penpot/pull/8871) - Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923) - Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930) +- Fix dashboard Recent/Deleted titles overlapped by scrolling content (by @rockchris99) [Github #8577](https://github.com/penpot/penpot/issues/8577) - Display resolved values of inactive tokens [Taiga #13628](https://tree.taiga.io/project/penpot/issue/13628) ## 2.15.0 (Unreleased) diff --git a/frontend/src/app/main/ui/dashboard/deleted.scss b/frontend/src/app/main/ui/dashboard/deleted.scss index 9a1ee20ac7..0ebba0c81b 100644 --- a/frontend/src/app/main/ui/dashboard/deleted.scss +++ b/frontend/src/app/main/ui/dashboard/deleted.scss @@ -52,6 +52,7 @@ padding: var(--sp-xxl) var(--sp-xxl) var(--sp-s) var(--sp-xxl); position: sticky; top: 0; + z-index: $z-index-100; } .nav-inside { From a52831aa8c57d6c18f5e1d15869e1182349ca914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Fri, 10 Apr 2026 12:58:19 +0200 Subject: [PATCH 124/288] :sparkles: Show professional card when has nitrate subscription --- frontend/src/app/main/ui/settings/subscription.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index f8fc390a3c..ee884c056e 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -416,7 +416,7 @@ (-> profile :props :subscription) subscription-type - (get-subscription-type subscription) + (if (and (contains? cf/flags :nitrate) nitrate?) (:type nitrate-license) (get-subscription-type subscription)) subscription-is-trial? (= (:status subscription) "trialing") From a3f7a1def63754f7b1c8ca30556a0c92bcee3c33 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Mon, 13 Apr 2026 18:29:15 +0200 Subject: [PATCH 125/288] :bug: Fix bugs with multiselection (#8932) * :bug: Fix app crash when selecting shapes with one hidden * :bug: Fix opacity input when mixed values --- CHANGES.md | 2 + .../workspace/get-file-fragment-tokens.json | 15 ++++-- .../playwright/ui/specs/tokens/apply.spec.js | 54 +++++++++++++++++++ .../sidebar/options/menus/layer.cljs | 9 ++-- 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8117220348..5f92376246 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -47,6 +47,8 @@ - Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930) - Fix dashboard Recent/Deleted titles overlapped by scrolling content (by @rockchris99) [Github #8577](https://github.com/penpot/penpot/issues/8577) - Display resolved values of inactive tokens [Taiga #13628](https://tree.taiga.io/project/penpot/issue/13628) +- Fix app crash when selecting shapes with one hidden [Taiga #13959](https://tree.taiga.io/project/penpot/issue/13959) +- Fix opacity mixed value [Taiga #13960](https://tree.taiga.io/project/penpot/issue/13960) ## 2.15.0 (Unreleased) diff --git a/frontend/playwright/data/workspace/get-file-fragment-tokens.json b/frontend/playwright/data/workspace/get-file-fragment-tokens.json index 128f45d28d..69061c79eb 100644 --- a/frontend/playwright/data/workspace/get-file-fragment-tokens.json +++ b/frontend/playwright/data/workspace/get-file-fragment-tokens.json @@ -186,7 +186,8 @@ }, "~:fills": [ { - "~:fill-color": "#7f9cf5" + "~:fill-color": "#7f9cf5", + "~:fill-opacity": 1 } ], "~:flip-x": null, @@ -235,7 +236,8 @@ "~:letter-spacing": "0", "~:fills": [ { - "~:fill-color": "#ffffff" + "~:fill-color": "#ffffff", + "~:fill-opacity": 1 } ], "~:font-family": "sourcesanspro", @@ -257,7 +259,8 @@ "~:letter-spacing": "0", "~:fills": [ { - "~:fill-color": "#ffffff" + "~:fill-color": "#ffffff", + "~:fill-opacity": 1 } ], "~:font-family": "sourcesanspro" @@ -328,7 +331,8 @@ "~:y2": 37.33333456516266, "~:fills": [ { - "~:fill-color": "#ffffff" + "~:fill-color": "#ffffff", + "~:fill-opacity": 1 } ], "~:x2": 86.60417175292969, @@ -445,7 +449,8 @@ }, "~:fills": [ { - "~:fill-color": "#ffffff" + "~:fill-color": "#ffffff", + "~:fill-opacity": 1 } ], "~:flip-x": null, diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js index bda71e08ba..a866fefe6a 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -927,3 +927,57 @@ test.describe("Tokens: Detach token", () => { await expect(brokenPill).not.toBeVisible(); }); }); + +test("Bug: 13959, User select shapes with different hidden state.", async ({ + page, +}) => { + const { workspacePage } = + await setupTokensFileRender(page); + + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers.getByTestId("layer-row").nth(1).click(); + const layerMenuSection = page.getByRole("region", { + name: "Layer menu section", + }); + await expect(layerMenuSection).toBeVisible(); + await layerMenuSection + .getByRole("button", { name: "Toggle layer visibility" }) + .click(); + await expect(layerMenuSection).toBeVisible(); + await workspacePage.layers + .getByTestId("layer-row") + .nth(0) + .click({ modifiers: ["Shift"] }); + await expect(layerMenuSection).toBeVisible(); +}); + +test("Bug: 13960, User select shapes with different opacity and input show mixed state.", async ({ + page, +}) => { + const { workspacePage } = + await setupTokensFileRender(page); + + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers.getByTestId("layer-row").nth(1).click(); + const layerMenuSection = page.getByRole("region", { + name: "Layer menu section", + }); + await expect(layerMenuSection).toBeVisible(); + await layerMenuSection + .getByRole('textbox', { name: 'Opacity' }) + .fill('50'); + await expect(layerMenuSection).toBeVisible(); + await workspacePage.layers + .getByTestId("layer-row") + .nth(0) + .click({ modifiers: ["Shift"] }); + await expect(layerMenuSection + .getByRole('textbox', { name: 'Opacity' })).toBeVisible(); + await expect(layerMenuSection + .getByRole('textbox', { name: 'Opacity' })).toBeVisible(); + + await expect(layerMenuSection + .getByRole('textbox', { name: 'Opacity' })).toHaveAttribute("placeholder", "Mixed"); +}); diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs index 3c08d9d6d8..41ec031902 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs @@ -69,6 +69,8 @@ hidden? (get values :hidden) blocked? (get values :blocked) + opacity (get values :opacity) + on-detach-token (mf/use-fn (mf/deps ids) @@ -237,10 +239,11 @@ (tr "settings.multiple") "--") :align :right - :disabled hidden? + :disabled (if (or (= :multiple hidden?) hidden?) true false) :class (stl/css :numeric-input-wrapper) - :value (* 100 - (or (get values :opacity) 1))}] + :value (if (= :multiple opacity) + opacity + (* 100 (d/nilv opacity 1)))}] (cond (or (= :multiple hidden?) (not hidden?)) From 4703fe6e3bf9010979c3e52ef41a7304b38d3c59 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Tue, 14 Apr 2026 04:08:13 -0400 Subject: [PATCH 126/288] :sparkles: Add visibility toggle for strokes (#8913) * :sparkles: Add visibility toggle for strokes * :recycle: Use single emit! call for stroke visibility toggle * :lipstick: Disable stroke controls when hidden, matching shadow/blur pattern When a stroke is hidden, the alignment/style selects, cap selects, and cap switch button are now disabled. A .hidden CSS class dims the options area with reduced opacity. This matches the existing behavior in shadow_row and blur menu where controls are disabled when the effect is hidden. * :lipstick: Move stroke hide button before remove button --------- Signed-off-by: eureka928 --- CHANGES.md | 1 + common/src/app/common/types/shape.cljc | 3 +- .../src/app/main/ui/shapes/custom_stroke.cljs | 3 +- .../sidebar/options/menus/stroke.cljs | 9 +++ .../sidebar/options/rows/stroke_row.cljs | 65 ++++++++++++----- .../sidebar/options/rows/stroke_row.scss | 19 +++++ frontend/src/app/render_wasm/api.cljs | 73 ++++++++++--------- frontend/translations/en.po | 4 + 8 files changed, 121 insertions(+), 56 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index aea60b608c..910495858b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -29,6 +29,7 @@ - Add per-group add button for typographies (by @eureka928) [Github #5275](https://github.com/penpot/penpot/issues/5275) - Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [Github #8773](https://github.com/penpot/penpot/issues/8773) - Make links in comments clickable (by @eureka928) [Github #1602](https://github.com/penpot/penpot/issues/1602) +- Add visibility toggle for strokes (by @eureka928) [Github #7438](https://github.com/penpot/penpot/issues/7438) ### :bug: Bugs fixed diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 20935a1723..5f6ac22ad3 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -145,7 +145,8 @@ [::sm/one-of stroke-caps]] [:stroke-color {:optional true} clr/schema:hex-color] [:stroke-color-gradient {:optional true} clr/schema:gradient] - [:stroke-image {:optional true} clr/schema:image]]) + [:stroke-image {:optional true} clr/schema:image] + [:hidden {:optional true} :boolean]]) (def stroke-attrs "A set of attrs that corresponds to stroke data type" diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 02c3b5d07e..01d5c64c5b 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -509,7 +509,8 @@ (when (some? shape-strokes) [:> :g props - (for [[index value] (reverse (d/enumerate shape-strokes))] + (for [[index value] (reverse (d/enumerate shape-strokes)) + :when (not (:hidden value))] [:& shape-custom-stroke {:shape shape :stroke value :index index diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index 96cf70f8f5..491aef1343 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -12,6 +12,7 @@ [app.common.types.stroke :as cts] [app.main.data.workspace :as udw] [app.main.data.workspace.colors :as dc] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.tokens.application :as dwta] [app.main.store :as st] [app.main.ui.components.title-bar :refer [title-bar*]] @@ -155,6 +156,13 @@ (st/emit! (udw/trigger-bounding-box-cloaking ids)) (st/emit! (dc/change-stroke-attrs ids {:stroke-cap-start stroke-cap-end :stroke-cap-end stroke-cap-start} index))))) + on-toggle-visibility + (mf/use-fn + (mf/deps ids) + (fn [index] + (st/emit! (udw/trigger-bounding-box-cloaking ids) + (dwsh/update-shapes ids #(update-in % [:strokes index :hidden] not))))) + on-add-stroke (fn [_] (st/emit! (udw/trigger-bounding-box-cloaking ids)) @@ -226,6 +234,7 @@ :applied-tokens (when (= 0 index) applied-tokens) :on-detach-token on-detach-token :on-remove on-remove + :on-toggle-visibility on-toggle-visibility :on-reorder handle-reorder :disable-drag disable-drag :on-focus on-focus diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index 6eb0cc349d..d1c0a80497 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -40,6 +40,7 @@ on-stroke-cap-start-change on-stroke-cap-end-change on-stroke-cap-switch + on-toggle-visibility disable-drag on-focus on-blur @@ -49,7 +50,9 @@ select-on-focus ids]}] - (let [token-numeric-inputs + (let [hidden? (:hidden stroke) + + token-numeric-inputs (features/use-feature "tokens/numeric-input") on-drop @@ -182,10 +185,18 @@ on-cap-switch (mf/use-fn (mf/deps index on-stroke-cap-switch) - #(on-stroke-cap-switch index))] + #(on-stroke-cap-switch index)) + + on-toggle-visibility + (mf/use-fn + (mf/deps index on-toggle-visibility) + (fn [] + (when on-toggle-visibility + (on-toggle-visibility index))))] [:div {:class (stl/css-case :stroke-data true + :hidden hidden? :dnd-over-top (= (:over dprops) :top) :dnd-over-bot (= (:over dprops) :bot)) :aria-label (str "stroke-row-" index)} @@ -195,22 +206,33 @@ ;; Stroke Color ;; FIXME: memorize stroke color - [:> color-row* {:color (ctc/stroke->color stroke) - :index index - :title title - :on-change on-color-change-refactor - :on-detach on-color-detach - :on-remove on-remove - :disable-drag disable-drag - :applied-token (if (= index 0) - stroke-color-token - nil) - :on-detach-token on-detach-token-color - :on-token-change on-token-change - :on-focus on-focus - :origin :stroke-color - :select-on-focus select-on-focus - :on-blur on-blur}] + [:div {:class (stl/css :stroke-color-actions)} + [:> color-row* {:color (ctc/stroke->color stroke) + :index index + :title title + :on-change on-color-change-refactor + :on-detach on-color-detach + :disable-drag disable-drag + :applied-token (if (= index 0) + stroke-color-token + nil) + :on-detach-token on-detach-token-color + :on-token-change on-token-change + :on-focus on-focus + :origin :stroke-color + :select-on-focus select-on-focus + :on-blur on-blur}] + + (when (some? on-toggle-visibility) + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.options.stroke.toggle-stroke") + :on-click on-toggle-visibility + :icon (if hidden? "hide" "shown")}]) + + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.options.stroke.remove-stroke") + :on-click on-remove + :icon i/remove}]] ;; Stroke Width, Alignment & Style (if token-numeric-inputs @@ -230,6 +252,7 @@ :options stroke-alignment-options :variant "icon-only" :data-testid "stroke.alignment" + :disabled hidden? :wrapper-class (stl/css :stroke-align-icon-select) :on-change on-alignment-change}] @@ -239,6 +262,7 @@ :wrapper-class (stl/css :stroke-style-icon-select) :data-testid "stroke.style" :variant "icon-only" + :disabled hidden? :dropdown-alignment :right :on-change on-style-change}])] @@ -258,6 +282,7 @@ :data-testid "stroke.alignment"} [:& select {:default-value stroke-alignment :options stroke-alignment-options + :disabled hidden? :on-change on-alignment-change}]] (when-not disable-stroke-style @@ -265,6 +290,7 @@ :data-testid "stroke.style"} [:& select {:default-value stroke-style :options stroke-style-options + :disabled hidden? :on-change on-style-change}]])]) ;; Stroke Caps @@ -272,11 +298,14 @@ [:div {:class (stl/css :stroke-caps-options)} [:& select {:default-value (:stroke-cap-start stroke) :options stroke-caps-options + :disabled hidden? :on-change on-caps-start-change}] [:> icon-button* {:variant "secondary" :aria-label (tr "labels.switch") + :disabled hidden? :on-click on-cap-switch :icon i/switch}] [:& select {:default-value (:stroke-cap-end stroke) :options stroke-caps-options + :disabled hidden? :on-change on-caps-end-change}]])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss index a93107afa9..c764e60f3f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss @@ -27,6 +27,25 @@ &.dnd-over-bot { --reorder-bottom-display: block; } + + &.hidden { + .stroke-options, + .stroke-options-tokens, + .stroke-caps-options { + opacity: 0.5; + pointer-events: none; + } + } +} + +.stroke-color-actions { + display: flex; + align-items: center; + + > :first-child { + flex: 1; + min-width: 0; + } } .stroke-options { diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 3d8fe0d0df..f12cbbc332 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -558,45 +558,46 @@ [shape-id strokes thumbnail?] (h/call wasm/internal-module "_clear_shape_strokes") (keep (fn [stroke] - (let [opacity (or (:stroke-opacity stroke) 1.0) - color (:stroke-color stroke) - gradient (:stroke-color-gradient stroke) - image (:stroke-image stroke) - width (:stroke-width stroke) - align (:stroke-alignment stroke) - style (-> stroke :stroke-style sr/translate-stroke-style) - cap-start (-> stroke :stroke-cap-start sr/translate-stroke-cap) - cap-end (-> stroke :stroke-cap-end sr/translate-stroke-cap) - offset (mem/alloc types.fills.impl/FILL-U8-SIZE) - heap (mem/get-heap-u8) - dview (js/DataView. (.-buffer heap))] - (case align - :inner (h/call wasm/internal-module "_add_shape_inner_stroke" width style cap-start cap-end) - :outer (h/call wasm/internal-module "_add_shape_outer_stroke" width style cap-start cap-end) - (h/call wasm/internal-module "_add_shape_center_stroke" width style cap-start cap-end)) + (when-not (:hidden stroke) + (let [opacity (or (:stroke-opacity stroke) 1.0) + color (:stroke-color stroke) + gradient (:stroke-color-gradient stroke) + image (:stroke-image stroke) + width (:stroke-width stroke) + align (:stroke-alignment stroke) + style (-> stroke :stroke-style sr/translate-stroke-style) + cap-start (-> stroke :stroke-cap-start sr/translate-stroke-cap) + cap-end (-> stroke :stroke-cap-end sr/translate-stroke-cap) + offset (mem/alloc types.fills.impl/FILL-U8-SIZE) + heap (mem/get-heap-u8) + dview (js/DataView. (.-buffer heap))] + (case align + :inner (h/call wasm/internal-module "_add_shape_inner_stroke" width style cap-start cap-end) + :outer (h/call wasm/internal-module "_add_shape_outer_stroke" width style cap-start cap-end) + (h/call wasm/internal-module "_add_shape_center_stroke" width style cap-start cap-end)) - (cond - (some? gradient) - (do - (types.fills.impl/write-gradient-fill offset dview opacity gradient) - (h/call wasm/internal-module "_add_shape_stroke_fill")) + (cond + (some? gradient) + (do + (types.fills.impl/write-gradient-fill offset dview opacity gradient) + (h/call wasm/internal-module "_add_shape_stroke_fill")) - (some? image) - (let [image-id (get image :id) - buffer (uuid/get-u32 image-id) - cached-image? (h/call wasm/internal-module "_is_image_cached" - (aget buffer 0) (aget buffer 1) - (aget buffer 2) (aget buffer 3) - thumbnail?)] - (types.fills.impl/write-image-fill offset dview opacity image) - (h/call wasm/internal-module "_add_shape_stroke_fill") - (when (== cached-image? 0) - (fetch-image shape-id image-id thumbnail?))) + (some? image) + (let [image-id (get image :id) + buffer (uuid/get-u32 image-id) + cached-image? (h/call wasm/internal-module "_is_image_cached" + (aget buffer 0) (aget buffer 1) + (aget buffer 2) (aget buffer 3) + thumbnail?)] + (types.fills.impl/write-image-fill offset dview opacity image) + (h/call wasm/internal-module "_add_shape_stroke_fill") + (when (== cached-image? 0) + (fetch-image shape-id image-id thumbnail?))) - (some? color) - (do - (types.fills.impl/write-solid-fill offset dview opacity color) - (h/call wasm/internal-module "_add_shape_stroke_fill"))))) + (some? color) + (do + (types.fills.impl/write-solid-fill offset dview opacity color) + (h/call wasm/internal-module "_add_shape_stroke_fill")))))) strokes)) (defn set-shape-svg-attrs diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 5fc069d7fe..2a481b0d0d 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7316,6 +7316,10 @@ msgstr "Outside" msgid "workspace.options.stroke.remove-stroke" msgstr "Remove stroke" +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +msgid "workspace.options.stroke.toggle-stroke" +msgstr "Toggle stroke" + #: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:137 msgid "workspace.options.stroke.solid" msgstr "Solid" From 19b9c696fcee1beb4a5e02ce340aa0518cd7a603 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Tue, 14 Apr 2026 04:33:32 -0400 Subject: [PATCH 127/288] :bug: Reset account submenu state when profile menu closes (#8953) Closes #8947 Signed-off-by: eureka928 --- CHANGES.md | 1 + frontend/src/app/main/ui/dashboard/sidebar.cljs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 910495858b..7863ad2ec1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -33,6 +33,7 @@ ### :bug: Bugs fixed +- Reset profile submenu state when the account menu closes (by @eureka928) [Github #8947](https://github.com/penpot/penpot/issues/8947) - Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582) - Fix styles between grid layout inputs [Taiga #13526](https://tree.taiga.io/project/penpot/issue/13526) - Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 2d3ddeb915..9e8de37d3e 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -1323,6 +1323,10 @@ (st/emit! (ptk/event ::ev/event {::ev/name "explore-pricing-click" ::ev/origin "dashboard" :section "sidebar"})) (dom/open-new-window "https://penpot.app/pricing")))] + (mf/with-effect [show-profile-menu?] + (when-not show-profile-menu? + (reset! sub-menu* nil))) + [:* (if (contains? cf/flags :nitrate) [:> nitrate-sidebar* {:profile profile :teams teams}] From d90e7f8164e2f070e303ab182dcab42d1873bfc9 Mon Sep 17 00:00:00 2001 From: Statxc Date: Tue, 14 Apr 2026 03:41:31 -0500 Subject: [PATCH 128/288] :sparkles: Add Find & Replace for text content and layer names (#8899) * :sparkles: Add Find & Replace for text content and layer names * :lipstick: Fix cross-browser styling for Find & Replace radio buttons and action buttons * :lipstick: Fix stylelint empty line before declaration in layers.scss * :zap: Improve match-filters and match-ids efficiency --------- Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + common/src/app/common/types/text.cljc | 26 ++ frontend/src/app/main/data/workspace.cljs | 13 + .../app/main/data/workspace/shortcuts.cljs | 5 + .../src/app/main/data/workspace/texts.cljs | 29 +++ .../src/app/main/ui/workspace/main_menu.cljs | 20 ++ .../app/main/ui/workspace/sidebar/layers.cljs | 244 +++++++++++++----- .../app/main/ui/workspace/sidebar/layers.scss | 154 +++++++++++ frontend/translations/en.po | 36 +++ 9 files changed, 469 insertions(+), 59 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7863ad2ec1..af5d0da55d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,7 @@ - Save and restore selection state in undo/redo (by @eureka928) [Github #6007](https://github.com/penpot/penpot/issues/6007) - Fix warnings for unsupported token $type (by @Dexterity104) [Github #8790](https://github.com/penpot/penpot/issues/8790) - Add per-group add button for typographies (by @eureka928) [Github #5275](https://github.com/penpot/penpot/issues/5275) +- Add Find & Replace for text content (by @statxc) [Github #7108](https://github.com/penpot/penpot/issues/7108) - Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [Github #8773](https://github.com/penpot/penpot/issues/8773) - Make links in comments clickable (by @eureka928) [Github #1602](https://github.com/penpot/penpot/issues/1602) - Add visibility toggle for strokes (by @eureka928) [Github #7438](https://github.com/penpot/penpot/issues/7438) diff --git a/common/src/app/common/types/text.cljc b/common/src/app/common/types/text.cljc index 053a963f84..d9cd5488dc 100644 --- a/common/src/app/common/types/text.cljc +++ b/common/src/app/common/types/text.cljc @@ -354,6 +354,32 @@ [k (get attrs k v)])))) +(defn content-has-text? + [content search] + (let [search-lower (str/lower search)] + (->> (node-seq is-text-node? content) + (some #(str/includes? (str/lower (:text %)) search-lower)) + (boolean)))) + +(defn replace-all-case-insensitive + [text search replacement] + (let [text-lower (str/lower text) + search-lower (str/lower search) + search-len (count search)] + (loop [result "" idx 0] + (let [found (str/index-of text-lower search-lower idx)] + (if (nil? found) + (str result (subs text idx)) + (recur (str result (subs text idx found) replacement) + (+ found search-len))))))) + +(defn replace-text-in-content + [content search replacement] + (transform-nodes + is-text-node? + (fn [node] (update node :text replace-all-case-insensitive search replacement)) + content)) + (defn content->text "Given a root node of a text content extracts the texts with its associated styles" [content] diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index e8a8a84029..75939e4858 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1420,6 +1420,19 @@ (update [_ state] (assoc-in state [:workspace-global :clipboard-style] style)))) +(defn open-layers-search + [mode] + (ptk/reify ::open-layers-search + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :layers-panel-search] mode)))) + +(def clear-layers-search + (ptk/reify ::clear-layers-search + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local dissoc :layers-panel-search)))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Exports ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 4f4d9296cc..3c147d7a7f 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -146,6 +146,11 @@ :subsections [:edit] :fn #(st/emit! esc-pressed)} + :find {:tooltip (ds/meta "F") :command (ds/c-mod "f") :subsections [:edit] + :fn #(st/emit! (dw/open-layers-search :find))} + :find-and-replace {:tooltip (ds/meta "H") :command (ds/c-mod "h") :subsections [:edit] + :fn #(st/emit! (dw/open-layers-search :find-and-replace))} + ;; MODIFY LAYERS :rename {:tooltip (ds/alt "N") diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index cc0b46dde5..19cb56be7c 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -1155,6 +1155,35 @@ (gsh/transform-shape (ctm/change-size shape width height)))))) {:undo-group (when new-shape? id)}))))))) +(defn replace-layer-names-in-shapes + [ids search replacement] + (ptk/reify ::replace-layer-names-in-shapes + ptk/WatchEvent + (watch [_ _ _] + (let [undo-group (uuid/next)] + (rx/of + (dwsh/update-shapes + ids + (fn [shape] (update shape :name txt/replace-all-case-insensitive search replacement)) + {:attrs #{:name} :undo-group undo-group})))))) + +(defn replace-text-in-shapes + [ids search replacement] + (ptk/reify ::replace-text-in-shapes + ptk/WatchEvent + (watch [_ _ _] + (let [undo-group (uuid/next)] + (rx/of + (dwsh/update-shapes + ids + (fn [shape] + (if (and (= :text (:type shape)) (some? (:content shape))) + (let [new-content (txt/replace-text-in-content (:content shape) search replacement) + new-name (txt/generate-shape-name (txt/content->text new-content))] + (-> shape (assoc :content new-content) (assoc :name new-name))) + shape)) + {:attrs #{:content :name} :undo-group undo-group})))))) + ;; -- Text Editor v3 ;; @see texts_v3.cljs diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 4d16c79646..60d6233eb6 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -454,6 +454,12 @@ (mf/use-fn #(st/emit! (dw/select-all))) + find + (mf/use-fn (fn [] (on-close) (st/emit! (dw/open-layers-search :find)))) + + find-and-replace + (mf/use-fn (fn [] (on-close) (st/emit! (dw/open-layers-search :find-and-replace)))) + undo (mf/use-fn #(st/emit! dwu/undo)) @@ -476,6 +482,20 @@ (tr "workspace.header.menu.select-all")] [:> shortcuts* {:id :select-all}]] + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) + :on-click find + :on-key-down (fn [event] (when (kbd/enter? event) (find event))) + :id "file-menu-find"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.find")] + [:> shortcuts* {:id :find}]] + + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) + :on-click find-and-replace + :on-key-down (fn [event] (when (kbd/enter? event) (find-and-replace event))) + :id "file-menu-find-and-replace"} + [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.find-and-replace")] + [:> shortcuts* {:id :find-and-replace}]] + (when can-edit [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click undo diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 49433489c6..7d41d982c1 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -11,8 +11,10 @@ [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] [app.common.types.shape :as cts] + [app.common.types.text :as txt] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] + [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.search-bar :refer [search-bar*]] @@ -205,61 +207,70 @@ ;; --- Layers Toolbox +(def ^:private ref:layers-panel-search + (l/derived (l/key :layers-panel-search) refs/workspace-local)) + ;; FIXME: optimize (defn- match-filters? [state [id shape]] (let [search (:search-text state) + scope (:search-scope state) filters (:filters state) filters (cond-> filters (contains? filters :shape) - (conj :rect :circle :path :bool))] + (conj :rect :circle :path :bool)) + text-match? (case scope + :canvas (and (= :text (:type shape)) + (some? (:content shape)) + (txt/content-has-text? (:content shape) search)) + (or (str/includes? (str/lower (:name shape)) (str/lower search)) + (str/includes? (str/lower (:variant-name shape)) (str/lower search)) + ;; Dev-only: allow search by id + (and *assert* (str/includes? (dm/str (:id shape)) (str/lower search)))))] (or (= uuid/zero id) - (and (or (str/includes? (str/lower (:name shape)) (str/lower search)) - (str/includes? (str/lower (:variant-name shape)) (str/lower search)) - ;; Only for local development we allow search for ids. Otherwise will be hard - ;; search for numbers or single letter shape names (ie: "A") - (and *assert* - (str/includes? (dm/str (:id shape)) (str/lower search)))) + (and text-match? (or (empty? filters) - (and (contains? filters :component) - (contains? shape :component-id)) - (and (contains? filters :image) - (some? (cts/has-images? shape))) - + (and (contains? filters :component) (contains? shape :component-id)) + (and (contains? filters :image) (some? (cts/has-images? shape))) (let [direct-filters (into #{} (filter #{:frame :rect :circle :path :bool :text}) filters)] (contains? direct-filters (:type shape))) (and (contains? filters :group) - (and (cfh/group-shape? shape) - (not (contains? shape :component-id)) - (or (not (contains? shape :masked-group)) - (false? (:masked-group shape))))) - (and (contains? filters :mask) - (true? (:masked-group shape)))))))) + (cfh/group-shape? shape) + (not (contains? shape :component-id)) + (or (not (contains? shape :masked-group)) + (false? (:masked-group shape)))) + (and (contains? filters :mask) (true? (:masked-group shape)))))))) (defn use-search [page objects] - (let [state* (mf/use-state - #(do {:show-search false - :show-menu false - :search-text "" - :filters #{} - :num-items 100})) - - state (deref state*) - current-filters (:filters state) - current-items (:num-items state) - current-search (:search-text state) - show-menu? (:show-menu state) - show-search? (:show-search state) + (let [state* (mf/use-state + #(do {:show-search false + :find-replace-mode? false + :search-scope :layers + :show-menu false + :search-text "" + :replace-text "" + :filters #{} + :num-items 100 + :current-match-idx 0})) + layers-search-request (mf/deref ref:layers-panel-search) + state (deref state*) + current-filters (:filters state) + current-items (:num-items state) + current-search (:search-text state) + replace-text (:replace-text state) + show-menu? (:show-menu state) + show-search? (:show-search state) + find-replace-mode? (:find-replace-mode? state) + search-scope (:search-scope state) + current-match-idx (:current-match-idx state) clear-search-text (mf/use-fn - #(swap! state* assoc :search-text "" :num-items 100)) - + #(swap! state* assoc :search-text "" :num-items 100 :current-match-idx 0)) toggle-filters - (mf/use-fn - #(swap! state* update :show-menu not)) + (mf/use-fn #(swap! state* update :show-menu not)) on-toggle-filters-click (mf/use-fn @@ -268,18 +279,26 @@ (toggle-filters))) hide-menu - (mf/use-fn - #(swap! state* assoc :show-menu false)) + (mf/use-fn #(swap! state* assoc :show-menu false)) on-key-down - (mf/use-fn - (fn [event] - (when (kbd/esc? event) (hide-menu)))) + (mf/use-fn (fn [event] (when (kbd/esc? event) (hide-menu)))) update-search-text (mf/use-fn (fn [value _event] - (swap! state* assoc :search-text value :num-items 100))) + (swap! state* assoc :search-text value :num-items 100 :current-match-idx 0))) + + update-replace-text + (mf/use-fn (fn [value _event] (swap! state* assoc :replace-text value))) + + clear-replace-text + (mf/use-fn #(swap! state* assoc :replace-text "")) + + set-search-scope + (mf/use-fn + (fn [scope] + (swap! state* assoc :search-scope scope :num-items 100 :current-match-idx 0))) toggle-search (mf/use-fn @@ -288,30 +307,23 @@ (dom/blur! node) (swap! state* (fn [state] (-> state - (assoc :search-text "") - (assoc :filters #{}) - (assoc :show-menu false) - (assoc :num-items 100) + (assoc :search-text "" :replace-text "" :filters #{}) + (assoc :show-menu false :find-replace-mode? false) + (assoc :search-scope :layers :num-items 100 :current-match-idx 0) (update :show-search not))))))) remove-filter (mf/use-fn (fn [event] - (let [fkey (-> (dom/get-current-target event) - (dom/get-data "filter") - (keyword))] + (let [fkey (-> (dom/get-current-target event) (dom/get-data "filter") (keyword))] (swap! state* (fn [state] - (-> state - (update :filters disj fkey) - (assoc :num-items 100))))))) + (-> state (update :filters disj fkey) (assoc :num-items 100))))))) add-filter (mf/use-fn (fn [event] (dom/stop-propagation event) - (let [key (-> (dom/get-current-target event) - (dom/get-data "filter") - (keyword))] + (let [key (-> (dom/get-current-target event) (dom/get-data "filter") (keyword))] (swap! state* (fn [state] (-> state (update :filters conj key) @@ -331,6 +343,65 @@ filtered-objects-total (count filtered-objects-all) + canvas-match-ids + (mf/with-memo [objects current-search search-scope] + (when (and (= :canvas search-scope) (d/not-empty? current-search)) + (reduce-kv (fn [acc id shape] + (cond-> acc + (and (= :text (:type shape)) + (some? (:content shape)) + (txt/content-has-text? (:content shape) current-search)) + (conj id))) + [] objects))) + + layer-match-ids + (mf/with-memo [objects current-search search-scope] + (when (and (= :layers search-scope) (d/not-empty? current-search)) + (reduce-kv (fn [acc id shape] + (cond-> acc + (str/includes? (str/lower (:name shape)) (str/lower current-search)) + (conj id))) + [] objects))) + + text-match-ids (if (= :canvas search-scope) canvas-match-ids layer-match-ids) + text-match-count (count text-match-ids) + safe-match-idx (if (pos? text-match-count) (mod current-match-idx text-match-count) 0) + + navigate-next + (mf/use-fn + (mf/deps text-match-count) + (fn [_] + (when (pos? text-match-count) + (swap! state* update :current-match-idx + (fn [idx] (mod (inc idx) text-match-count)))))) + + navigate-prev + (mf/use-fn + (mf/deps text-match-count) + (fn [_] + (when (pos? text-match-count) + (swap! state* update :current-match-idx + (fn [idx] (mod (+ (dec idx) text-match-count) text-match-count)))))) + + handle-replace + (mf/use-fn + (mf/deps text-match-ids safe-match-idx replace-text current-search search-scope) + (fn [_] + (when (and (pos? text-match-count) (d/not-empty? current-search)) + (let [id (nth text-match-ids safe-match-idx)] + (if (= :canvas search-scope) + (st/emit! (dwt/replace-text-in-shapes [id] current-search replace-text)) + (st/emit! (dwt/replace-layer-names-in-shapes [id] current-search replace-text))))))) + + handle-replace-all + (mf/use-fn + (mf/deps text-match-ids replace-text current-search search-scope) + (fn [_] + (when (and (pos? text-match-count) (d/not-empty? current-search)) + (if (= :canvas search-scope) + (st/emit! (dwt/replace-text-in-shapes text-match-ids current-search replace-text)) + (st/emit! (dwt/replace-layer-names-in-shapes text-match-ids current-search replace-text)))))) + filtered-objects (mf/with-memo [active? filtered-objects-all current-items] (when active? @@ -352,6 +423,16 @@ (events/unlistenByKey key1) (events/unlistenByKey key2)))) + (mf/with-effect [layers-search-request] + (when (some? layers-search-request) + (let [replace-mode? (= layers-search-request :find-and-replace)] + (swap! state* (fn [s] + (-> s + (assoc :show-search true :find-replace-mode? replace-mode?) + (assoc :search-scope (if replace-mode? :canvas :layers)) + (assoc :search-text "" :replace-text "" :current-match-idx 0))))) + (st/emit! dw/clear-layers-search))) + [filtered-objects handle-show-more #(mf/html @@ -363,17 +444,62 @@ :on-clear clear-search-text :placeholder (tr "workspace.sidebar.layers.search")} [:button {:on-click on-toggle-filters-click - :class (stl/css-case - :filter-button true - :opened show-menu? - :active active?)} + :class (stl/css-case :filter-button true :opened show-menu? :active active?)} [:> icon* {:icon-id i/filter}]]] - [:> icon-button* {:variant "ghost" :aria-label (tr "labels.close") :on-click toggle-search :icon i/close}]] + [:div {:class (stl/css :search-scope-row)} + [:label {:class (stl/css-case :scope-option true :scope-selected (= :canvas search-scope))} + [:span {:class (stl/css-case :scope-radio true :scope-radio-checked (= :canvas search-scope))}] + [:input {:type "radio" :name "search-scope" :class (stl/css :scope-radio-input) + :checked (= :canvas search-scope) + :on-change (fn [_] (set-search-scope :canvas))}] + [:span {:class (stl/css :scope-label)} + (tr "workspace.sidebar.layers.search-scope-canvas")]] + [:label {:class (stl/css-case :scope-option true :scope-selected (= :layers search-scope))} + [:span {:class (stl/css-case :scope-radio true :scope-radio-checked (= :layers search-scope))}] + [:input {:type "radio" :name "search-scope" :class (stl/css :scope-radio-input) + :checked (= :layers search-scope) + :on-change (fn [_] (set-search-scope :layers))}] + [:span {:class (stl/css :scope-label)} + (tr "workspace.sidebar.layers.search-scope-layers")]]] + + (when ^boolean find-replace-mode? + [:* + [:div {:class (stl/css :tool-window-bar :replace-row)} + [:div {:class (stl/css :replace-input-wrapper)} + [:input {:class (stl/css :replace-input) + :value replace-text + :placeholder (tr "workspace.sidebar.layers.replace-placeholder") + :on-change (fn [event] + (update-replace-text (dom/get-target-val event) event))}] + (when (not= "" replace-text) + [:button {:class (stl/css :clear-icon) :on-click clear-replace-text} + [:> icon* {:icon-id i/delete-text :size "s"}]])] + (when (d/not-empty? current-search) + (if (pos? text-match-count) + [:div {:class (stl/css :match-navigation)} + [:span {:class (stl/css :match-count)} + (dm/str (inc safe-match-idx) " / " text-match-count)] + [:> icon-button* {:variant "ghost" :aria-label (tr "labels.previous") + :on-click navigate-prev :icon i/arrow-up}] + [:> icon-button* {:variant "ghost" :aria-label (tr "labels.next") + :on-click navigate-next :icon i/arrow-down}]] + [:span {:class (stl/css :no-matches)} + (tr "workspace.sidebar.layers.no-matches")]))] + [:div {:class (stl/css :replace-actions-row)} + [:button {:class (stl/css :replace-button) + :on-click handle-replace + :disabled (or (zero? text-match-count) (str/empty? current-search))} + (tr "workspace.sidebar.layers.replace")] + [:button {:class (stl/css :replace-button) + :on-click handle-replace-all + :disabled (or (zero? text-match-count) (str/empty? current-search))} + (tr "workspace.sidebar.layers.replace-all")]]]) + [:div {:class (stl/css :active-filters)} (for [fkey current-filters] (let [fname (d/name fkey) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.scss b/frontend/src/app/main/ui/workspace/sidebar/layers.scss index 234d1cee61..7fb33a72da 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.scss @@ -246,6 +246,160 @@ scrollbar-gutter: stable; } +.replace-row { + padding: 0 deprecated.$s-12; + gap: deprecated.$s-4; +} + +.search-scope-row { + display: flex; + gap: deprecated.$s-16; + padding: deprecated.$s-4 deprecated.$s-12 deprecated.$s-8; + align-items: center; +} + +.scope-option { + display: flex; + align-items: center; + gap: deprecated.$s-6; + cursor: pointer; +} + +.scope-radio { + width: deprecated.$s-12; + height: deprecated.$s-12; + border: deprecated.$s-1 solid var(--color-foreground-secondary); + border-radius: 50%; + background-color: transparent; + flex-shrink: 0; +} + +.scope-radio-checked { + border-color: var(--color-accent-primary); + background-color: var(--color-accent-primary); + box-shadow: inset 0 0 0 deprecated.$s-2 var(--color-background-primary); +} + +.scope-radio-input { + display: none; +} + +.scope-label { + @include deprecated.bodySmallTypography; + + color: var(--color-foreground-secondary); + cursor: pointer; +} + +.scope-selected .scope-label { + color: var(--color-foreground-primary); +} + +.replace-actions-row { + display: flex; + gap: deprecated.$s-4; + padding: 0 deprecated.$s-12 deprecated.$s-8; +} + +.replace-input-wrapper { + @include deprecated.flexCenter; + + flex: 1; + height: deprecated.$s-32; + border: deprecated.$s-1 solid var(--search-bar-input-border-color); + border-radius: deprecated.$br-8; + background-color: var(--search-bar-input-background-color); + + &:hover { + border: deprecated.$s-1 solid var(--input-border-color-hover); + background-color: var(--input-background-color-hover); + + .replace-input { + background-color: var(--input-background-color-hover); + } + } + + &:focus-within { + background-color: var(--input-background-color-active); + color: var(--input-foreground-color-active); + border: deprecated.$s-1 solid var(--input-border-color-focus); + + .replace-input { + background-color: var(--input-background-color-active); + } + } +} + +.replace-input { + width: 100%; + height: 100%; + margin: 0 deprecated.$s-8; + border: 0; + background-color: var(--input-background-color); + font-size: deprecated.$fs-12; + color: var(--input-foreground-color); + border-radius: deprecated.$br-8; + + &:focus { + outline: none; + } +} + +.replace-button { + @include deprecated.bodySmallTypography; + @include deprecated.buttonStyle; + + flex: 1; + height: deprecated.$s-28; + padding: 0 deprecated.$s-8; + border: deprecated.$s-1 solid var(--color-background-tertiary); + border-radius: deprecated.$br-8; + background-color: var(--color-background-tertiary); + color: var(--color-foreground-primary); + white-space: nowrap; + text-transform: uppercase; + + &:hover:not(:disabled) { + border: deprecated.$s-1 solid var(--input-border-color-hover); + background-color: var(--input-background-color-hover); + } + + &:disabled { + opacity: 0.4; + cursor: default; + } +} + +.match-navigation { + display: flex; + align-items: center; + gap: deprecated.$s-2; + flex-shrink: 0; +} + +.match-count { + @include deprecated.bodySmallTypography; + + color: var(--color-foreground-secondary); + white-space: nowrap; +} + +.no-matches { + @include deprecated.bodySmallTypography; + + color: var(--color-foreground-secondary); + white-space: nowrap; + flex-shrink: 0; +} + +.clear-icon { + @extend %button-tag; + + flex: 0 0 deprecated.$s-32; + height: 100%; + color: var(--color-icon-default); +} + .element-list { display: grid; position: relative; diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 2a481b0d0d..9ce50c445c 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5863,6 +5863,14 @@ msgstr "Redo" msgid "workspace.header.menu.select-all" msgstr "Select all" +#: src/app/main/ui/workspace/main_menu.cljs +msgid "workspace.header.menu.find" +msgstr "Find" + +#: src/app/main/ui/workspace/main_menu.cljs +msgid "workspace.header.menu.find-and-replace" +msgstr "Find and Replace" + #: src/app/main/ui/workspace/main_menu.cljs:423 msgid "workspace.header.menu.show-artboard-names" msgstr "Show boards names" @@ -7966,6 +7974,34 @@ msgstr "Shapes" msgid "workspace.sidebar.layers.texts" msgstr "Texts" +#: src/app/main/ui/workspace/sidebar/layers.cljs +msgid "workspace.sidebar.layers.replace" +msgstr "Replace" + +#: src/app/main/ui/workspace/sidebar/layers.cljs +msgid "workspace.sidebar.layers.replace-all" +msgstr "Replace all" + +#: src/app/main/ui/workspace/sidebar/layers.cljs +msgid "workspace.sidebar.layers.replace-placeholder" +msgstr "Replace with..." + +#: src/app/main/ui/workspace/sidebar/layers.cljs +msgid "workspace.sidebar.layers.match-count" +msgstr "%s of %s" + +#: src/app/main/ui/workspace/sidebar/layers.cljs +msgid "workspace.sidebar.layers.no-matches" +msgstr "No matches" + +#: src/app/main/ui/workspace/sidebar/layers.cljs +msgid "workspace.sidebar.layers.search-scope-layers" +msgstr "Search layers" + +#: src/app/main/ui/workspace/sidebar/layers.cljs +msgid "workspace.sidebar.layers.search-scope-canvas" +msgstr "Search on canvas" + #: src/app/main/ui/inspect/attributes/svg.cljs:56, src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs:101 msgid "workspace.sidebar.options.svg-attrs.title" msgstr "Imported SVG Attributes" From 8b14de2610ef7cbbb42827231178b8915d73b2b5 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Tue, 14 Apr 2026 04:50:46 -0400 Subject: [PATCH 129/288] :sparkles: Sort asset subfolders alphabetically (#8952) Subgroups in asset libraries were rendered in hash-map order because update-in descends into plain maps instead of sorted ones. Add a recursive post-process that rebuilds every level as a sorted-map so subfolders are alphabetical at every nesting depth. Closes #2572 Signed-off-by: eureka928 --- CHANGES.md | 1 + .../ui/workspace/sidebar/assets/groups.cljs | 35 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index af5d0da55d..709b24a7cf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -31,6 +31,7 @@ - Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [Github #8773](https://github.com/penpot/penpot/issues/8773) - Make links in comments clickable (by @eureka928) [Github #1602](https://github.com/penpot/penpot/issues/1602) - Add visibility toggle for strokes (by @eureka928) [Github #7438](https://github.com/penpot/penpot/issues/7438) +- Sort asset library subfolders alphabetically at every nesting level (by @eureka928) [Github #2572](https://github.com/penpot/penpot/issues/2572) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs index 3bc5fe58f1..4a781fa48a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs @@ -86,6 +86,17 @@ :on-click on-context-menu :icon i/menu}]]]))) +(defn- sort-groups + "Recursively sort subgroup keys alphabetically at every nesting level." + [groups reverse-sort?] + (let [cmp (if reverse-sort? #(compare %2 %1) compare) + sort-tree (fn sort-tree [m] + (into (sorted-map-by cmp) + (map (fn [[k v]] + [k (if (map? v) (sort-tree v) v)])) + m))] + (sort-tree groups))) + (defn group-assets "Convert a list of assets in a nested structure like this: @@ -97,19 +108,17 @@ " [assets reverse-sort?] (when-not (empty? assets) - (reduce (fn [groups {:keys [path] :as asset}] - (let [path (cpn/split-path (or path ""))] - (update-in groups - (conj path "") - (fn [group] - (if group - (conj group asset) - [asset]))))) - (sorted-map-by (fn [key1 key2] - (if reverse-sort? - (compare key2 key1) - (compare key1 key2)))) - assets))) + (-> (reduce (fn [groups {:keys [path] :as asset}] + (let [path (cpn/split-path (or path ""))] + (update-in groups + (conj path "") + (fn [group] + (if group + (conj group asset) + [asset]))))) + {} + assets) + (sort-groups reverse-sort?)))) (def ^:private schema:group-form [:map {:title "GroupForm"} From 6788df02ca7519562b8054add926756586be926f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 14 Apr 2026 11:49:16 +0200 Subject: [PATCH 130/288] :wrench: Add minor adjustments on ci workflow related to e2e tests --- .github/workflows/tests.yml | 63 ++++--------------------------------- frontend/scripts/test-e2e | 2 +- 2 files changed, 7 insertions(+), 58 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b44b95a941..afcffb0ae7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -151,18 +151,11 @@ jobs: name: "Frontend Tests" runs-on: penpot-runner-02 container: penpotapp/devenv:latest - needs: test-render-wasm steps: - name: Checkout repository uses: actions/checkout@v6 - - name: Restore shared.js - uses: actions/cache/restore@v5 - with: - key: "render-wasm-shared-js-${{ github.sha }}" - path: frontend/src/app/render_wasm/api/shared.js - - name: Unit Tests working-directory: ./frontend run: | @@ -200,19 +193,6 @@ jobs: run: | ./test - - name: Copy shared.js artifact - working-directory: ./render-wasm - run: | - SHARED_FILE=$(find target -name render_wasm_shared.js | head -n 1); - mkdir -p ../frontend/src/app/render_wasm/api; - cp $SHARED_FILE ../frontend/src/app/render_wasm/api/shared.js; - - - name: Cache shared.js - uses: actions/cache@v5 - with: - key: "render-wasm-shared-js-${{ github.sha }}" - path: frontend/src/app/render_wasm/api/shared.js - test-backend: if: ${{ !github.event.pull_request.draft }} name: "Backend Tests" @@ -291,7 +271,7 @@ jobs: test-integration-1: if: ${{ !github.event.pull_request.draft }} - name: "Integration Tests 1/4" + name: "Integration Tests 1/3" runs-on: penpot-runner-02 container: penpotapp/devenv:latest needs: build-integration @@ -309,7 +289,7 @@ jobs: - name: Run Tests working-directory: ./frontend run: | - ./scripts/test-e2e --shard="1/4"; + ./scripts/test-e2e --shard="1/3"; - name: Upload test result uses: actions/upload-artifact@v7 @@ -322,7 +302,7 @@ jobs: test-integration-2: if: ${{ !github.event.pull_request.draft }} - name: "Integration Tests 2/4" + name: "Integration Tests 2/3" runs-on: penpot-runner-02 container: penpotapp/devenv:latest needs: build-integration @@ -340,7 +320,7 @@ jobs: - name: Run Tests working-directory: ./frontend run: | - ./scripts/test-e2e --shard="2/4"; + ./scripts/test-e2e --shard="2/3"; - name: Upload test result uses: actions/upload-artifact@v7 @@ -353,7 +333,7 @@ jobs: test-integration-3: if: ${{ !github.event.pull_request.draft }} - name: "Integration Tests 3/4" + name: "Integration Tests 3/3" runs-on: penpot-runner-02 container: penpotapp/devenv:latest needs: build-integration @@ -371,7 +351,7 @@ jobs: - name: Run Tests working-directory: ./frontend run: | - ./scripts/test-e2e --shard="3/4"; + ./scripts/test-e2e --shard="3/3"; - name: Upload test result uses: actions/upload-artifact@v7 @@ -381,34 +361,3 @@ jobs: path: frontend/test-results/ overwrite: true retention-days: 3 - - test-integration-4: - if: ${{ !github.event.pull_request.draft }} - name: "Integration Tests 4/4" - runs-on: penpot-runner-02 - container: penpotapp/devenv:latest - needs: build-integration - - steps: - - name: Checkout Repository - uses: actions/checkout@v6 - - - name: Restore Cache - uses: actions/cache/restore@v5 - with: - key: "integration-bundle-${{ github.sha }}" - path: frontend/resources/public - - - name: Run Tests - working-directory: ./frontend - run: | - ./scripts/test-e2e --shard="4/4"; - - - name: Upload test result - uses: actions/upload-artifact@v7 - if: always() - with: - name: integration-tests-result-4 - path: frontend/test-results/ - overwrite: true - retention-days: 3 diff --git a/frontend/scripts/test-e2e b/frontend/scripts/test-e2e index dd25bed989..fca7cf941e 100755 --- a/frontend/scripts/test-e2e +++ b/frontend/scripts/test-e2e @@ -5,4 +5,4 @@ SCRIPT_DIR=$(dirname $0); set -ex $SCRIPT_DIR/setup; -pnpm run test:e2e -x --workers=2 --reporter=list "$@"; +pnpm run test:e2e -x --workers=1 --reporter=list "$@"; From 68595e90ebd51a5148a360d3e67a7a718443688e Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Tue, 14 Apr 2026 06:31:25 -0400 Subject: [PATCH 131/288] :sparkles: Persist asset search query when switching sidebar tabs (#8985) When users switch between the Layers and Assets sidebar tabs, the `assets-toolbox*` component unmounts and its local `use-state` is discarded, so the search query and section filter are lost. Lift the search term and section filter into a per-file, in-memory session atom that survives tab switches but doesn't leak across files or persist across reloads. Ordering and list-style continue to use localStorage as before. Closes #2913 Signed-off-by: eureka0928 Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + .../app/main/ui/workspace/sidebar/assets.cljs | 21 ++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 709b24a7cf..aece6a2a85 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -64,6 +64,7 @@ - Access Tokens look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114) - Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714) +- Persist asset search query and section filter when switching sidebar tabs (by @eureka0928) [Github #2913](https://github.com/penpot/penpot/issues/2913) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index dc7fd0a50d..206a16f8e0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -70,16 +70,24 @@ [v a b] (if (= v a) b a)) +;; Per-file, session-scoped (in-memory only) so the search term and section +;; filter survive switching between the Layers and Assets sidebar tabs without +;; leaking across files or persisting across reloads. +(defonce ^:private session-filters* + (atom {})) + (mf/defc assets-toolbox* {::mf/wrap [mf/memo]} [{:keys [size file-id]}] (let [read-only? (mf/use-ctx ctx/workspace-read-only?) filters* (mf/use-state - {:term "" - :section "all" - :ordering (dwa/get-current-assets-ordering) - :list-style (dwa/get-current-assets-list-style) - :open-menu false}) + (fn [] + (-> (or (get @session-filters* file-id) + {:term "" + :section "all"}) + (assoc :ordering (dwa/get-current-assets-ordering) + :list-style (dwa/get-current-assets-list-style) + :open-menu false)))) filters (deref filters*) term (:term filters) list-style (:list-style filters) @@ -162,6 +170,9 @@ :id "typographies" :handler on-section-filter-change}])] + (mf/with-effect [file-id term section] + (swap! session-filters* assoc file-id {:term term :section section})) + [:article {:class (stl/css :assets-bar)} [:div {:class (stl/css :assets-header)} (when-not ^boolean read-only? From b211594ce8c7560a7d10eabb749a531325890763 Mon Sep 17 00:00:00 2001 From: James <63717587+jamesrayammons@users.noreply.github.com> Date: Tue, 14 Apr 2026 04:36:26 -0600 Subject: [PATCH 132/288] :bug: Fix hyphens stripped from export filenames (#8944) Replace str/slug with a targeted regex that only removes filesystem-unsafe characters when generating export filenames. The slug function strips all non-word characters including hyphens, causing names like "my-board" to become "myboard" on export. Fixes #8901 Signed-off-by: jamesrayammons Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + exporter/src/app/handlers/resources.cljs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index aece6a2a85..21d977a0db 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -51,6 +51,7 @@ - Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930) - Fix dashboard Recent/Deleted titles overlapped by scrolling content (by @rockchris99) [Github #8577](https://github.com/penpot/penpot/issues/8577) - Display resolved values of inactive tokens [Taiga #13628](https://tree.taiga.io/project/penpot/issue/13628) +- Fix hyphens stripped from export filenames (by @jamesrayammons) [Github #8901](https://github.com/penpot/penpot/issues/8901) - Fix app crash when selecting shapes with one hidden [Taiga #13959](https://tree.taiga.io/project/penpot/issue/13959) - Fix opacity mixed value [Taiga #13960](https://tree.taiga.io/project/penpot/issue/13960) diff --git a/exporter/src/app/handlers/resources.cljs b/exporter/src/app/handlers/resources.cljs index f0f655c498..e981856da4 100644 --- a/exporter/src/app/handlers/resources.cljs +++ b/exporter/src/app/handlers/resources.cljs @@ -36,7 +36,7 @@ {:path path :mtype (mime/get type) :name name - :filename (str/concat (str/slug name) (mime/get-extension type)) + :filename (str/concat (str/replace name #"[\\/:*?\"<>|]" "_") (mime/get-extension type)) :id task-id})) (defn create-zip From 3469e867ff4852943e047f5d1f5f7779d25ffd77 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 14 Apr 2026 13:02:03 +0200 Subject: [PATCH 133/288] :bug: Fix gap input throwing an error (#8984) --- .../ui/workspace/sidebar/options/menus/layout_container.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs index 7ad7c0186a..0d16dd4664 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs @@ -713,7 +713,7 @@ (mf/use-fn (mf/deps on-change wrap-type ids) (fn [value event attr] - (let [on-change-fn #((on-change (= "nowrap" wrap-type) attr % event))] + (let [on-change-fn #(on-change (= "nowrap" wrap-type) attr % event)] (soc/emit-value-or-token value on-change-fn ids #{attr})))) on-detach-token From 7c3a1a905e92ba407bb2b7d843ef58ab2c8f5cb2 Mon Sep 17 00:00:00 2001 From: Dexterity <173429049+Dexterity104@users.noreply.github.com> Date: Tue, 14 Apr 2026 07:21:51 -0400 Subject: [PATCH 134/288] :sparkles: Fix locked elements not selectable in viewer + add guide locking (#8949) * :bug: Allow viewers to select locked elements in canvas * :sparkles: Add ability to lock guides to prevent accidental movement --------- Signed-off-by: Dexterity104 Co-authored-by: Andrey Antukh --- frontend/src/app/main/data/workspace/layout.cljs | 1 + frontend/src/app/main/ui/workspace/main_menu.cljs | 11 +++++++++++ frontend/src/app/main/ui/workspace/viewport.cljs | 5 +++-- .../src/app/main/ui/workspace/viewport/hooks.cljs | 6 +++--- frontend/src/app/main/ui/workspace/viewport_wasm.cljs | 5 +++-- frontend/src/app/worker/selection.cljs | 2 -- frontend/translations/en.po | 8 ++++++++ 7 files changed, 29 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/main/data/workspace/layout.cljs b/frontend/src/app/main/data/workspace/layout.cljs index 44cd36e5ce..fad7a91802 100644 --- a/frontend/src/app/main/data/workspace/layout.cljs +++ b/frontend/src/app/main/data/workspace/layout.cljs @@ -25,6 +25,7 @@ :element-options :rulers :display-guides + :lock-guides :snap-guides :scale-text :dynamic-alignment diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 60d6233eb6..ab4c7896ab 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -380,6 +380,17 @@ (tr "workspace.header.menu.show-guides"))] [:> shortcuts* {:id :toggle-guides}]] + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) + :on-click toggle-flag + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-flag event))) + :data-testid "lock-guides" + :id "file-menu-lock-guides"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :lock-guides) + (tr "workspace.header.menu.unlock-guides") + (tr "workspace.header.menu.lock-guides"))]] (when-not ^boolean read-only? [:* diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index b0e540ac71..5bf6037c1a 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -259,7 +259,8 @@ show-rulers? (and (contains? layout :rulers) (not hide-ui?)) - disabled-guides? (or drawing-tool transform path-drawing? path-editing? @space? @mod?) + disabled-guides? (or drawing-tool transform path-drawing? path-editing? @space? @mod? + (contains? layout :lock-guides)) single-select? (= (count selected-shapes) 1) @@ -307,7 +308,7 @@ (hooks/setup-cursor cursor alt? mod? space? panning drawing-tool path-drawing? path-editing? z? read-only?) (hooks/setup-keyboard alt? mod? space? z? shift?) (hooks/setup-hover-shapes page-id move-stream base-objects transform selected mod? hover measure-hover - hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures?) + hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures? read-only?) (hooks/setup-viewport-modifiers modifiers base-objects) (hooks/setup-shortcuts path-editing? path-drawing? text-editing? grid-editing?) (hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 225dde4fd9..18e4045c18 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -177,7 +177,7 @@ (dw/increase-zoom))))))) (defn setup-hover-shapes - [page-id move-stream objects transform selected mod? hover measure-hover hover-ids hover-top-frame-id hover-disabled? focus zoom show-measures?] + [page-id move-stream objects transform selected mod? hover measure-hover hover-ids hover-top-frame-id hover-disabled? focus zoom show-measures? read-only?] (let [;; We use ref so we don't recreate the stream on a change zoom-ref (mf/use-ref zoom) mod-ref (mf/use-ref @mod?) @@ -267,7 +267,7 @@ (let [sorted-ids-cache (mf/use-ref {})] (hooks/use-stream over-shapes-stream - (mf/deps page-id objects show-measures?) + (mf/deps page-id objects show-measures? read-only?) (fn [ids] (let [selected (mf/ref-val selected-ref) focus (mf/ref-val focus-ref) @@ -279,7 +279,7 @@ (let [sorted-ids (into (d/ordered-set) (comp (remove (partial cfh/hidden-parent? objects)) - (remove #(dm/get-in objects [% :blocked])) + (remove #(and (not read-only?) (dm/get-in objects [% :blocked]))) (remove (partial cfh/svg-raw-shape? objects))) (ctt/sort-z-index objects ids {:bottom-frames? mod?}))] (mf/set-ref-val! sorted-ids-cache (assoc cached-ids [mod? ids] sorted-ids)) diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 091a00a3e7..edfd3ce582 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -269,7 +269,8 @@ show-rulers? (and (contains? layout :rulers) (not hide-ui?)) - disabled-guides? (or drawing-tool transform path-drawing? path-editing?) + disabled-guides? (or drawing-tool transform path-drawing? path-editing? + (contains? layout :lock-guides)) single-select? (= (count selected-shapes) 1) @@ -432,7 +433,7 @@ (hooks/setup-cursor cursor alt? mod? space? panning drawing-tool path-drawing? path-editing? z? read-only?) (hooks/setup-keyboard alt? mod? space? z? shift?) (hooks/setup-hover-shapes page-id move-stream base-objects transform selected mod? hover measure-hover - hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures?) + hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures? read-only?) (hooks/setup-shortcuts path-editing? path-drawing? text-editing? grid-editing?) (hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index c3bbdde95c..333f0af73b 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -157,8 +157,6 @@ match-criteria? (fn [shape] (and (not (:hidden shape)) - (or (cfh/frame-shape? shape) ;; We return frames even if blocked - (not (:blocked shape))) (or (not frame-id) (= frame-id (:frame-id shape))) (case (:type shape) :frame include-frames? diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 9ce50c445c..5f19fc3290 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5799,6 +5799,14 @@ msgstr "Hide board names" msgid "workspace.header.menu.hide-guides" msgstr "Hide guides" +#: src/app/main/ui/workspace/main_menu.cljs:387 +msgid "workspace.header.menu.lock-guides" +msgstr "Lock guides" + +#: src/app/main/ui/workspace/main_menu.cljs:387 +msgid "workspace.header.menu.unlock-guides" +msgstr "Unlock guides" + #: src/app/main/ui/workspace/main_menu.cljs:393 msgid "workspace.header.menu.hide-palette" msgstr "Hide color palette" From 39b0e011fc7ebfa267c7f799f60418772b9854c2 Mon Sep 17 00:00:00 2001 From: Clayton <118192227+claytonlin1110@users.noreply.github.com> Date: Tue, 14 Apr 2026 06:30:22 -0500 Subject: [PATCH 135/288] :sparkles: Differentiate incoming and outgoing interaction link colors (#8923) * :sparkles: Color incoming and outgoing interaction links differently * :bug: Fix lint --------- Signed-off-by: Clayton Co-authored-by: Andrey Antukh --- .../ui/workspace/viewport/interactions.cljs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index daac5372e8..af6b1e58aa 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -33,6 +33,10 @@ :move-overlay-index]) refs/workspace-local =)) +(def ^:private outgoing-link-color "var(--color-accent-tertiary)") +(def ^:private incoming-link-color "var(--color-accent-quaternary)") +(def ^:private neutral-link-color "var(--df-secondary)") + (defn- on-pointer-down [event index {:keys [id] :as shape}] (dom/stop-propagation event) @@ -139,7 +143,7 @@ (mf/defc interaction-path - [{:keys [index level orig-shape dest-shape dest-point selected? action-type zoom] :as props}] + [{:keys [index level orig-shape dest-shape dest-point selected selected? action-type zoom] :as props}] (let [[orig-pos orig-x orig-y dest-pos dest-x dest-y] (cond dest-shape @@ -160,11 +164,17 @@ path ["M" orig-x orig-y "C" (+ orig-x orig-dx) orig-y (+ dest-x dest-dx) dest-y dest-x dest-y] pdata (str/join " " path) - arrow-dir (if (= dest-pos :left) :right :left)] + arrow-dir (if (= dest-pos :left) :right :left) + incoming? (and (some? dest-shape) + (contains? selected (:id dest-shape))) + stroke-color (cond + selected? outgoing-link-color + incoming? incoming-link-color + :else neutral-link-color)] (if-not selected? [:g {:on-pointer-down #(on-pointer-down % index orig-shape)} - [:path {:stroke "var(--df-secondary)" + [:path {:stroke stroke-color :fill "none" :pointer-events "visible" :stroke-width (/ 2 zoom) @@ -173,13 +183,13 @@ [:& interaction-marker {:index index :x dest-x :y dest-y - :stroke "var(--df-secondary)" + :stroke stroke-color :action-type action-type :arrow-dir arrow-dir :zoom zoom}])] [:g {:on-pointer-down #(on-pointer-down % index orig-shape)} - [:path {:stroke "var(--color-accent-tertiary)" + [:path {:stroke stroke-color :fill "none" :pointer-events "visible" :stroke-width (/ 2 zoom) @@ -188,17 +198,17 @@ (when dest-shape [:& outline {:zoom zoom :shape dest-shape - :color "var(--color-accent-tertiary)"}]) + :color stroke-color}]) [:& interaction-marker {:index index :x orig-x :y orig-y - :stroke "var(--color-accent-tertiary)" + :stroke stroke-color :zoom zoom}] [:& interaction-marker {:index index :x dest-x :y dest-y - :stroke "var(--color-accent-tertiary)" + :stroke stroke-color :action-type action-type :arrow-dir arrow-dir :zoom zoom}]]))) From 650f725f110f9a5f19a74f7b8bbf331ee9bce691 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 14 Apr 2026 13:43:41 +0200 Subject: [PATCH 136/288] :paperclip: Update changelog --- CHANGES.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 21d977a0db..ca646b5b8a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,32 +10,35 @@ ### :sparkles: New features & Enhancements -- Add "Clear artboard guides" option to right-click context menu for frames (by @eureka928) [Github #6987](https://github.com/penpot/penpot/issues/6987) +- Add "Clear artboard guides" option to right-click context menu for frames (by @eureka0928) [Github #6987](https://github.com/penpot/penpot/issues/6987) - Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912) - Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248) - Import Tokens from linked library (by @dfelinto) [Github #8391](https://github.com/penpot/penpot/pull/8391) - Option to download custom fonts (by @dfelinto) [Github #8320](https://github.com/penpot/penpot/issues/8320) - Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313) - Add Tab/Shift+Tab navigation to rename layers sequentially (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8474) -- Copy and paste entire rows in existing table (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8498) +- Copy and paste entire rows in existing table (by @bittoby) [Github #8498](https://github.com/penpot/penpot/pull/8498) - Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137) - Duplicate token group [Taiga #10653](https://tree.taiga.io/project/penpot/us/10653) - Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568) - Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713) - Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466) - Add CSS linter [Taiga #13790](https://tree.taiga.io/project/penpot/us/13790) -- Save and restore selection state in undo/redo (by @eureka928) [Github #6007](https://github.com/penpot/penpot/issues/6007) +- Save and restore selection state in undo/redo (by @eureka0928) [Github #6007](https://github.com/penpot/penpot/issues/6007) - Fix warnings for unsupported token $type (by @Dexterity104) [Github #8790](https://github.com/penpot/penpot/issues/8790) -- Add per-group add button for typographies (by @eureka928) [Github #5275](https://github.com/penpot/penpot/issues/5275) -- Add Find & Replace for text content (by @statxc) [Github #7108](https://github.com/penpot/penpot/issues/7108) +- Add per-group add button for typographies (by @eureka0928) [Github #5275](https://github.com/penpot/penpot/issues/5275) +- Add Find & Replace for text content and layer names (by @statxc) [Github #7108](https://github.com/penpot/penpot/issues/7108) - Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [Github #8773](https://github.com/penpot/penpot/issues/8773) -- Make links in comments clickable (by @eureka928) [Github #1602](https://github.com/penpot/penpot/issues/1602) -- Add visibility toggle for strokes (by @eureka928) [Github #7438](https://github.com/penpot/penpot/issues/7438) -- Sort asset library subfolders alphabetically at every nesting level (by @eureka928) [Github #2572](https://github.com/penpot/penpot/issues/2572) +- Make links in comments clickable (by @eureka0928) [Github #1602](https://github.com/penpot/penpot/issues/1602) +- Add visibility toggle for strokes (by @eureka0928) [Github #7438](https://github.com/penpot/penpot/issues/7438) +- Sort asset library subfolders alphabetically at every nesting level (by @eureka0928) [Github #2572](https://github.com/penpot/penpot/issues/2572) +- Differentiate incoming and outgoing interaction link colors (by @claytonlin1110) [Github #7794](https://github.com/penpot/penpot/issues/7794) +- Add guide locking and fix locked elements not selectable in viewer (by @Dexterity104) [Github #8358](https://github.com/penpot/penpot/issues/8358) +- Apply styles to selection (by @AzazelN28) [Taiga #13647](https://tree.taiga.io/project/penpot/task/13647) ### :bug: Bugs fixed -- Reset profile submenu state when the account menu closes (by @eureka928) [Github #8947](https://github.com/penpot/penpot/issues/8947) +- Reset profile submenu state when the account menu closes (by @eureka0928) [Github #8947](https://github.com/penpot/penpot/issues/8947) - Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582) - Fix styles between grid layout inputs [Taiga #13526](https://tree.taiga.io/project/penpot/issue/13526) - Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534) @@ -49,11 +52,12 @@ - Fix plugin modal drag interactions over iframe and close-button behavior (by @marekhrabe) [Github #8871](https://github.com/penpot/penpot/pull/8871) - Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923) - Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930) -- Fix dashboard Recent/Deleted titles overlapped by scrolling content (by @rockchris99) [Github #8577](https://github.com/penpot/penpot/issues/8577) +- Fix dashboard Recent/Deleted titles overlapped by scrolling content (by @rockchris099) [Github #8577](https://github.com/penpot/penpot/issues/8577) - Display resolved values of inactive tokens [Taiga #13628](https://tree.taiga.io/project/penpot/issue/13628) - Fix hyphens stripped from export filenames (by @jamesrayammons) [Github #8901](https://github.com/penpot/penpot/issues/8901) - Fix app crash when selecting shapes with one hidden [Taiga #13959](https://tree.taiga.io/project/penpot/issue/13959) - Fix opacity mixed value [Taiga #13960](https://tree.taiga.io/project/penpot/issue/13960) +- Fix gap input throwing an error [Github #8984](https://github.com/penpot/penpot/pull/8984) ## 2.15.0 (Unreleased) From 207cb87d5edfc56f95efe5b56144272043cf336d Mon Sep 17 00:00:00 2001 From: rockchris099 Date: Tue, 14 Apr 2026 07:56:26 -0400 Subject: [PATCH 137/288] :sparkles: Reorder prototype overlay options (position before relative to) (#8972) Closes #2910 Signed-off-by: rockchris99 Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + .../sidebar/options/menus/interactions.cljs | 20 +++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ca646b5b8a..8469e45ce1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,6 +35,7 @@ - Differentiate incoming and outgoing interaction link colors (by @claytonlin1110) [Github #7794](https://github.com/penpot/penpot/issues/7794) - Add guide locking and fix locked elements not selectable in viewer (by @Dexterity104) [Github #8358](https://github.com/penpot/penpot/issues/8358) - Apply styles to selection (by @AzazelN28) [Taiga #13647](https://tree.taiga.io/project/penpot/task/13647) +- Reorder prototyping overlay options to show Position before Relative to (by @rockchris099) [Github #2910](https://github.com/penpot/penpot/issues/2910) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index ac8fdc23cf..c1a54d9764 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -475,16 +475,6 @@ (when (ctsi/has-overlay-opts interaction) [:* - ;; Overlay position relative-to (select) - [:div {:class (stl/css :interaction-row)} - [:div {:class (stl/css :interaction-row-label)} - [:div {:class (stl/css :interaction-row-name)} - (tr "workspace.options.interaction-relative-to")]] - [:div {:class (stl/css :interaction-row-select)} - [:& select {:default-value (str (:position-relative-to interaction)) - :options relative-to-opts - :on-change change-position-relative-to}]]] - ;; Overlay position (select) [:div {:class (stl/css :interaction-row)} [:div {:class (stl/css :interaction-row-label)} @@ -495,6 +485,16 @@ :options overlay-position-opts :on-change change-overlay-pos-type}]]] + ;; Overlay position relative-to (select) + [:div {:class (stl/css :interaction-row)} + [:div {:class (stl/css :interaction-row-label)} + [:div {:class (stl/css :interaction-row-name)} + (tr "workspace.options.interaction-relative-to")]] + [:div {:class (stl/css :interaction-row-select)} + [:& select {:default-value (str (:position-relative-to interaction)) + :options relative-to-opts + :on-change change-position-relative-to}]]] + ;; Overlay position (buttons) [:div {:class (stl/css :interaction-row)} [:div {:class (stl/css :interaction-row-position)} From 8cc05d957952c26a93a0a1108ec56c2a3a849b69 Mon Sep 17 00:00:00 2001 From: rockchris099 Date: Tue, 14 Apr 2026 15:02:42 -0400 Subject: [PATCH 138/288] :sparkles: Show alpha percentage in asset library color names (#8975) When several library colors share the same RGB value but differ only in opacity, append the alpha percentage (e.g. "#ff0000 50%") next to the displayed default name and in the color bullet tooltip so users can tell them apart at a glance. Closes #6328 Signed-off-by: rockchris99 Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + .../src/app/main/ui/components/color_bullet.cljs | 15 ++++++++++++--- .../main/ui/workspace/sidebar/assets/colors.cljs | 15 +++++++++++---- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8469e45ce1..3cc7a1ae0f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ ### :sparkles: New features & Enhancements +- Show alpha percentage next to library color values to distinguish colors that differ only in opacity (by @rockchris099) [Github #6328](https://github.com/penpot/penpot/issues/6328) - Add "Clear artboard guides" option to right-click context menu for frames (by @eureka0928) [Github #6987](https://github.com/penpot/penpot/issues/6987) - Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912) - Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248) diff --git a/frontend/src/app/main/ui/components/color_bullet.cljs b/frontend/src/app/main/ui/components/color_bullet.cljs index d94938d147..55d9e4c57c 100644 --- a/frontend/src/app/main/ui/components/color_bullet.cljs +++ b/frontend/src/app/main/ui/components/color_bullet.cljs @@ -7,24 +7,33 @@ (ns app.main.ui.components.color-bullet (:require-macros [app.main.style :as stl]) (:require + [app.common.math :as mth] [app.config :as cfg] [app.util.color :as uc] [app.util.i18n :refer [tr]] [cuerdas.core :as str] [rumext.v2 :as mf])) +(defn- format-color-with-alpha + [color opacity] + (if (and (number? opacity) (< opacity 1)) + (str color " " (mth/round (* opacity 100)) "%") + color)) + (defn- color-title [color-item] (let [{:keys [name path]} (meta color-item) path-and-name (if path (str path " / " name) name) gradient (:gradient color-item) image (:image color-item) - color (:color color-item)] + opacity (:opacity color-item) + color (:color color-item) + color-str (when color (format-color-with-alpha color opacity))] (if (some? name) (cond (some? color) - (str/ffmt "% (%)" path-and-name color) + (str/ffmt "% (%)" path-and-name color-str) (some? gradient) (str/ffmt "% (%)" path-and-name (uc/gradient-type->string (:type gradient))) @@ -37,7 +46,7 @@ (cond (some? color) - color + color-str (some? gradient) (uc/gradient-type->string (:type gradient)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs index b2dc3e8e5b..c0395cf4f4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.math :as mth] [app.common.path-names :as cpn] [app.config :as cf] [app.main.constants :refer [max-input-length]] @@ -61,10 +62,16 @@ menu-state (mf/use-state cmm/initial-context-menu-state) read-only? (mf/use-ctx ctx/workspace-read-only?) + opacity (:opacity color) + alpha-suffix (when (and (number? opacity) (< opacity 1)) + (dm/str " " (mth/round (* opacity 100)) "%")) default-name (cond (:gradient color) (uc/gradient-type->string (dm/get-in color [:gradient :type])) (:color color) (:color color) :else (:value color)) + display-name (if (and alpha-suffix (not (:gradient color))) + (dm/str default-name alpha-suffix) + default-name) rename-color (mf/use-fn @@ -231,16 +238,16 @@ :default-value (cpn/merge-path-item (:path color) (:name color))}] [:div {:title (if (= (:name color) default-name) - default-name - (dm/str (:name color) " (" default-name ")")) + display-name + (dm/str (:name color) " (" display-name ")")) :class (stl/css :name-block) :on-double-click rename-color-clicked} (if (= (:name color) default-name) - [:span {:class (stl/css :default-name)} default-name] + [:span {:class (stl/css :default-name)} display-name] [:* (:name color) - [:span {:class (stl/css :default-name :default-name-with-color)} default-name]])]) + [:span {:class (stl/css :default-name :default-name-with-color)} display-name]])]) (when local? [:> cmm/assets-context-menu* From dfec9004bf3132b93b9a53dcefa07e1962c57495 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:32:56 -0400 Subject: [PATCH 139/288] :sparkles: Add delete and duplicate buttons to typography dialog (#8983) * :sparkles: Add delete and duplicate buttons to typography dialog Add delete and duplicate action buttons to the expanded typography editing panel, allowing users to quickly manage typographies without needing to close the panel and use the context menu. Fixes #5270 * :recycle: Use DS icon-button for typography dialog actions Address review feedback: replace raw `:button`/`:div` elements and deprecated-icon usage with the design system `icon-button*` and non-deprecated icons (`i/add`, `i/delete`, `i/tick`). * :recycle: Only show typography delete/duplicate buttons in assets sidebar `typography-entry` is reused from the right sidebar text options panel, where the delete and duplicate actions don't make sense. Add an `is-asset?` opt-in prop and gate the `on-delete`/`on-duplicate` handlers behind it, so the buttons only appear when the entry is rendered from the assets sidebar. * :recycle: Move typography delete/duplicate handlers next to their use site Refine the previous opt-in: instead of plumbing on-delete/on-duplicate function props through typography-entry, build them directly inside typography-advanced-options where they're actually rendered. The component now takes :file-id and :is-asset? and gates the action buttons on a single `show-actions?` flag. --------- Signed-off-by: eureka0928 --- CHANGES.md | 1 + .../sidebar/assets/typographies.cljs | 3 +- .../sidebar/options/menus/typography.cljs | 51 ++++++++++++++++--- .../sidebar/options/menus/typography.scss | 6 +++ 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3cc7a1ae0f..f7ba9d16c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -72,6 +72,7 @@ - Access Tokens look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114) - Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714) - Persist asset search query and section filter when switching sidebar tabs (by @eureka0928) [Github #2913](https://github.com/penpot/penpot/issues/2913) +- Add delete and duplicate buttons to typography dialog (by @eureka0928) [Github #5270](https://github.com/penpot/penpot/issues/5270) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs index d9665d4bba..082fecb996 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs @@ -125,7 +125,8 @@ :editing? editing? :renaming? renaming? :focus-name? rename? - :external-open* open*}] + :external-open* open* + :is-asset? true}] (when ^boolean dragging? [:div {:class (stl/css :dragging)}])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 940682be89..90124b22aa 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -16,6 +16,8 @@ [app.main.data.common :as dcm] [app.main.data.fonts :as fts] [app.main.data.shortcuts :as dsc] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.undo :as dwu] [app.main.features :as features] [app.main.fonts :as fonts] [app.main.refs :as refs] @@ -26,6 +28,7 @@ [app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.components.select :refer [select]] [app.main.ui.context :as ctx] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.icons :as deprecated-icon] [app.util.dom :as dom] @@ -464,9 +467,29 @@ (mf/defc typography-advanced-options {::mf/wrap [mf/memo]} - [{:keys [visible? typography editable? name-input-ref on-close on-change on-name-blur local? navigate-to-library on-key-down]}] - (let [ref (mf/use-ref nil) - font-data (fonts/get-font-data (:font-id typography))] + [{:keys [visible? typography editable? name-input-ref on-close on-change on-name-blur + local? navigate-to-library on-key-down file-id is-asset?]}] + (let [ref (mf/use-ref nil) + font-data (fonts/get-font-data (:font-id typography)) + typography-id (:id typography) + show-actions? (and is-asset? editable?) + + on-delete + (mf/use-fn + (mf/deps typography-id file-id on-close) + (fn [] + (on-close) + (let [undo-id (js/Symbol)] + (st/emit! (dwu/start-undo-transaction undo-id) + (dwl/delete-typography typography-id) + (dwl/sync-file file-id file-id :typographies typography-id) + (dwu/commit-undo-transaction undo-id))))) + + on-duplicate + (mf/use-fn + (mf/deps file-id typography-id) + (fn [] + (st/emit! (dwl/duplicate-typography file-id typography-id))))] (fonts/ensure-loaded! (:font-id typography)) (mf/use-effect @@ -498,9 +521,21 @@ :on-key-down on-key-down :on-blur on-name-blur}] - [:div {:class (stl/css :action-btn) - :on-click on-close} - deprecated-icon/tick]] + [:div {:class (stl/css :action-btns)} + (when show-actions? + [:* + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.assets.duplicate") + :on-click on-duplicate + :icon i/add}] + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.assets.delete") + :on-click on-delete + :icon i/delete}]]) + [:> icon-button* {:variant "ghost" + :aria-label (tr "labels.close") + :on-click on-close + :icon i/tick}]]] [:& text-options {:values typography :on-change on-change @@ -551,7 +586,7 @@ (mf/defc typography-entry {::mf/wrap-props false} - [{:keys [file-id typography local? selected? on-click on-change on-detach on-context-menu editing? renaming? focus-name? external-open*]}] + [{:keys [file-id typography local? selected? on-click on-change on-detach on-context-menu editing? renaming? focus-name? external-open* is-asset?]}] (let [name-input-ref (mf/use-ref) read-only? (mf/use-ctx ctx/workspace-read-only?) editable? (and local? (not read-only?)) @@ -667,5 +702,7 @@ :on-change on-change :on-name-blur on-name-blur :on-key-down on-key-down + :file-id file-id + :is-asset? is-asset? :local? local? :navigate-to-library navigate-to-library}]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss index a9f91e206a..29213fd2ac 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss @@ -146,6 +146,12 @@ } } + .action-btns { + display: flex; + align-items: center; + gap: deprecated.$s-2; + } + &:focus-within { border: deprecated.$s-1 solid var(--input-border-color-active); From 909427d4427c971e8b48bb9f5fcb114d4ba12dd6 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Tue, 14 Apr 2026 22:41:47 +0200 Subject: [PATCH 140/288] :recycle: Improve import/export warning semantics (#8991) --- .../data/workspace/tokens/import_export.cljs | 17 +++++---- .../shared/notification_pill.cljs | 24 ++++--------- .../shared/notification_pill.scss | 36 ++++++++++++++----- .../app/main/ui/ds/notifications/toast.cljs | 10 ++---- frontend/src/app/main/ui/notifications.cljs | 14 ++------ frontend/stylelint.config.mjs | 2 +- frontend/translations/en.po | 6 ++++ 7 files changed, 56 insertions(+), 53 deletions(-) diff --git a/frontend/src/app/main/data/workspace/tokens/import_export.cljs b/frontend/src/app/main/data/workspace/tokens/import_export.cljs index 545d893d27..f9793d38c8 100644 --- a/frontend/src/app/main/data/workspace/tokens/import_export.cljs +++ b/frontend/src/app/main/data/workspace/tokens/import_export.cljs @@ -16,7 +16,7 @@ [app.main.data.tokenscript :as ts] [app.main.data.workspace.tokens.errors :as wte] [app.main.store :as st] - [app.util.i18n :refer [tr]] + [app.util.i18n :as i18n] [beicon.v2.core :as rx] [cuerdas.core :as str])) @@ -47,15 +47,18 @@ (let [type->tokens (group-by-value unknown-tokens)] (l/wrn :hint "unsupported token types found during import" :tokens (str/join ", " (map (fn [[path type]] (str path " (" type ")")) unknown-tokens))) - (ntf/show {:content (tr "workspace.tokens.unknown-token-type-message") + (ntf/show {:content (i18n/tr "workspace.tokens.unknown-token-type-message") :detail (->> (for [[token-type token-paths] type->tokens] - (str (tr "workspace.tokens.unknown-token-type-section" token-type (count token-paths)) - "
" + (str (i18n/tr "workspace.tokens.unknown-token-type-section" + token-type + (i18n/tr "labels.warning-count" (i18n/c (count token-paths)))) + "
    " (->> token-paths (sort) - (map #(str "  • " %)) - (str/join "
    ")))) - (str/join "

    ")) + (map #(str "
  • " % "
  • ")) + (str/join "")) + "
")) + (str/join "")) :type :toast :level :info}))) diff --git a/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.cljs b/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.cljs index efa97a9247..a8fbe76c0e 100644 --- a/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.cljs +++ b/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.cljs @@ -9,7 +9,6 @@ [app.main.style :as stl]) (:require [app.common.data.macros :as dm] - [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) @@ -29,13 +28,11 @@ [:level [:enum :default :info :warning :error :success]] [:type [:enum :toast :context]] [:appearance {:optional true} [:enum :neutral :ghost]] - [:is-html {:optional true} :boolean] - [:show-detail {:optional true} [:maybe :boolean]] - [:on-toggle-detail {:optional true} [:maybe fn?]]]) + [:is-html {:optional true} :boolean]]) (mf/defc notification-pill* {::mf/schema schema:notification-pill} - [{:keys [level type is-html appearance detail children show-detail on-toggle-detail]}] + [{:keys [level type is-html appearance detail children]}] (let [class (stl/css-case :appearance-neutral (= appearance :neutral) :appearance-ghost (= appearance :ghost) :with-detail detail @@ -60,16 +57,7 @@ children)] (when detail - [:div {:class (stl/css :error-detail)} - [:div {:class (stl/css :error-detail-title)} - [:> icon-button* - {:icon (if show-detail "arrow-down" "arrow") - :aria-label (tr "workspace.notification-pill.detail") - :icon-class (stl/css :expand-icon) - :variant "action" - :on-click on-toggle-detail}] - [:div {:on-click on-toggle-detail} - (tr "workspace.notification-pill.detail")]] - (when show-detail - [:div {:class (stl/css :error-detail-content) - :dangerouslySetInnerHTML #js {:__html detail}}])])])) + [:details {:class (stl/css :error-detail)} + [:summary {:class (stl/css :error-detail-summary)} (tr "workspace.notification-pill.detail")] + [:div {:class (stl/css :error-detail-content) + :dangerouslySetInnerHTML #js {:__html detail}}]])])) diff --git a/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.scss b/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.scss index 3974567f7c..0124798e98 100644 --- a/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.scss +++ b/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.scss @@ -98,20 +98,38 @@ } .error-detail { - overflow: auto; + list-style: none; + padding-inline-start: var(--sp-xxl); } -.error-detail-title { - display: flex; - align-items: center; +.error-detail-summary { + list-style: none; cursor: pointer; -} + position: relative; -.expand-icon { - --icon-fill-color: var(--color-foreground-primary); - --icon-stroke-color: var(--color-foreground-primary); + &::marker { + display: none; + } + + &::before { + content: "‣"; + position: absolute; + inset-block-start: 0; + inset-inline-start: -1.5rem; + inline-size: $sz-16; + text-box: trim-start cap alphabetic; + text-align: end; + font-size: 1lh; + line-height: 1; + font-weight: 700; + color: currentcolor; + } } .error-detail-content { - padding-left: var(--sp-xxxl); + padding-block-start: var(--sp-s); + + & ul { + list-style: disc inside; + } } diff --git a/frontend/src/app/main/ui/ds/notifications/toast.cljs b/frontend/src/app/main/ui/ds/notifications/toast.cljs index f83dbd5fd6..c00827eb89 100644 --- a/frontend/src/app/main/ui/ds/notifications/toast.cljs +++ b/frontend/src/app/main/ui/ds/notifications/toast.cljs @@ -21,13 +21,11 @@ [:level {:optional true} [:maybe [:enum :default :info :warning :error :success]]] [:appearance {:optional true} [:enum :neutral :ghost]] [:is-html {:optional true} :boolean] - [:show-detail {:optional true} [:maybe :boolean]] - [:on-close {:optional true} fn?] - [:on-toggle-detail {:optional true} [:maybe fn?]]]) + [:on-close {:optional true} fn?]]) (mf/defc toast* {::mf/schema schema:toast} - [{:keys [class level appearance type is-html children detail show-detail on-close on-toggle-detail] :rest props}] + [{:keys [class level appearance type is-html children detail on-close] :rest props}] (let [class (dm/str class " " (stl/css :toast)) level (if (string? level) (keyword level) @@ -47,9 +45,7 @@ :type type :is-html is-html :appearance appearance - :detail detail - :show-detail show-detail - :on-toggle-detail on-toggle-detail} children] + :detail detail} children] ;; TODO: this should be a buttom from the DS, but this variant is not designed yet. diff --git a/frontend/src/app/main/ui/notifications.cljs b/frontend/src/app/main/ui/notifications.cljs index de7161db99..e318946b6a 100644 --- a/frontend/src/app/main/ui/notifications.cljs +++ b/frontend/src/app/main/ui/notifications.cljs @@ -27,14 +27,7 @@ (= :floating (:position notification))) toast? (or (= :toast (:type notification)) (some? (:timeout notification))) - content (or (:content notification) "") - - show-detail* (mf/use-state false) - - handle-toggle-detail - (mf/use-fn - (fn [] - (swap! show-detail* not)))] + content (or (:content notification) "")] (when notification (cond @@ -43,9 +36,8 @@ {:level (or (:level notification) :info) :type (:type notification) :detail (:detail notification) - :on-close on-close - :show-detail @show-detail* - :on-toggle-detail handle-toggle-detail} content] + :on-close on-close} + content] inline? [:& inline-notification diff --git a/frontend/stylelint.config.mjs b/frontend/stylelint.config.mjs index d0b144242f..99a9474a2b 100644 --- a/frontend/stylelint.config.mjs +++ b/frontend/stylelint.config.mjs @@ -48,7 +48,7 @@ export default { "color-named": "never", // "declaration-no-important": true, "declaration-property-unit-allowed-list": { - "font-size": ["rem"], + "font-size": ["rem", "lh"], "/^animation/": ["s"], }, // // 'order/properties-alphabetical-order': true, diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 5f19fc3290..65c38e5ccd 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -2594,6 +2594,12 @@ msgstr "Empty" msgid "labels.error" msgstr "Error" +#: src/app/main/ui/dashboard/import.cljs:297 +msgid "labels.warning-count" +msgid_plural "labels.warning-count" +msgstr[0] "%s warning" +msgstr[1] "%s warnings" + #: src/app/main/ui/onboarding/questions.cljs:404 #, unused msgid "labels.event" From a3ea9fbecb1c76d88d5afc9cf2f93c34c26772f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Wed, 15 Apr 2026 08:42:42 +0200 Subject: [PATCH 141/288] :wrench: Add more validations for components, to avoid some crashes (#7820) * :wrench: Validate only after propagation in tests * :lipstick: Enhance some component sync traces * :wrench: Add fake uuid generator for debugging * :bug: Remove old feature of advancing references when reset changes Since long time ago, we only allow to reset changes in the top copy shape. In this case the near and the remote shapes are the same, so the advance-ref has no effect. * :bug: Fix some bugs and add validations, repair and migrations Also added several utilities to debug and to create scripts that processes files * :bug: Fix misplaced parenthesis passing propagate-fn to wrong function The :propagate-fn keyword argument was incorrectly placed inside the ths/get-shape call instead of being passed to tho/reset-overrides. This caused reset-overrides to never propagate component changes, making the test not validate what it intended. * :bug: Accept and forward :include-deleted? in find-near-match Callers were passing :include-deleted? true but the parameter was not in the destructuring, so it was silently ignored and the function always hardcoded true. This made the API misleading and would cause incorrect behavior if called with :include-deleted? false. * :lipstick: Use set/union alias instead of fully-qualified clojure.set/union The namespace already requires [clojure.set :as set], so use the alias for consistency. * :bug: Add tests for reset-overrides with and without propagate-fn Add two focused tests to comp_reset_test to cover the propagate-fn path in reset-overrides: - test-reset-with-propagation-updates-copies: verifies that resetting an override on a nested copy inside a main and supplying propagate-fn causes the canonical color to appear in all downstream copies. - test-reset-without-propagation-does-not-update-copies: regression guard for the misplaced-parenthesis bug; confirms that omitting propagate-fn leaves copies with the overridden value because the component sync never runs. --------- Co-authored-by: Andrey Antukh --- .../src/app/common/files/comp_processors.cljc | 115 +++ common/src/app/common/files/migrations.cljc | 24 +- common/src/app/common/files/repair.cljc | 47 +- common/src/app/common/files/validate.cljc | 85 +- common/src/app/common/logic/libraries.cljc | 74 +- .../app/common/test_helpers/compositions.cljc | 182 +++- common/src/app/common/test_helpers/files.cljc | 8 +- .../src/app/common/test_helpers/shapes.cljc | 12 + common/src/app/common/types/component.cljc | 8 +- .../src/app/common/types/components_list.cljc | 3 + common/src/app/common/types/container.cljc | 4 + common/src/app/common/types/file.cljc | 103 ++- common/src/app/common/types/shape_tree.cljc | 2 - common/src/app/common/uuid.cljc | 21 +- .../files/comp_processors_test.cljc | 787 ++++++++++++++++++ .../logic/comp_detach_with_nested_test.cljc | 5 +- .../common_tests/logic/comp_reset_test.cljc | 71 +- .../logic/duplicated_pages_test.cljc | 5 +- .../logic/multiple_nesting_levels_test.cljc | 16 +- .../logic/swap_as_override_test.cljc | 43 +- .../logic/swap_keeps_id_test.cljc | 2 +- .../logic/variants_switch_test.cljc | 165 ++-- .../common_tests/types/components_test.cljc | 358 ++++++++ frontend/src/app/main/data/changes.cljs | 4 +- 24 files changed, 1877 insertions(+), 267 deletions(-) create mode 100644 common/src/app/common/files/comp_processors.cljc create mode 100644 common/test/common_tests/files/comp_processors_test.cljc diff --git a/common/src/app/common/files/comp_processors.cljc b/common/src/app/common/files/comp_processors.cljc new file mode 100644 index 0000000000..80e782e7cc --- /dev/null +++ b/common/src/app/common/files/comp_processors.cljc @@ -0,0 +1,115 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.files.comp-processors + "Repair, migration or transformation utilities for components." + (:require + [app.common.logging :as log] + [app.common.types.component :as ctk] + [app.common.types.file :as ctf])) + +(log/set-level! :warn) + +(defn remove-unneeded-objects-in-components + "Some components have an :objects attribute, despite not being deleted. This removes it. + It also adds an empty :objects if it's deleted and does not have it." + [file-data] + (ctf/update-components + file-data + (fn [component] + (if (:deleted component) + (if (nil? (:objects component)) + (do + (log/warn :msg "Adding empty :objects to deleted component" + :component-id (:id component) + :component-name (:name component) + :file-id (:id file-data)) + (assoc component :objects {})) + component) + (if (contains? component :objects) + (do + (log/warn :msg "Removing :objects from non-deleted component" + :component-id (:id component) + :component-name (:name component) + :file-id (:id file-data)) + (dissoc component :objects)) + component))))) + +(defn fix-missing-swap-slots + "Locate shapes that have been swapped (i.e. their shape-ref does not point to the near match) but + they don't have a swap slot. In this case, add one pointing to the near match." + [file-data libraries] + (ctf/update-all-shapes + file-data + (fn [shape] + (if (ctk/subcopy-head? shape) + (let [container (:container (meta shape)) + file {:id (:id file-data) :data file-data} + near-match (ctf/find-near-match file container libraries shape :include-deleted? true :with-context? false)] + (if (and (some? near-match) + (not= (:shape-ref shape) (:id near-match)) + (nil? (ctk/get-swap-slot shape))) + (let [updated-shape (ctk/set-swap-slot shape (:id near-match))] + (log/warn :msg "Adding missing swap slot to shape" + :shape-id (:id shape) + :shape-name (:name shape) + :swap-slot (:id near-match) + :file-id (:id file) + :container-id (:id container) + :container-type (:type container)) + {:result :update :updated-shape updated-shape}) + {:result :keep})) + {:result :keep})))) + +(defn sync-component-id-with-ref-shape + "Ensure that all copies heads have the same component id and file as the referenced shape. + There may be bugs that cause them to get out of sync." + [file-data libraries] + (letfn [(sync-one-iteration + [file-data libraries] + (ctf/update-all-shapes + file-data + (fn [shape] + (if (and (ctk/subcopy-head? shape) (nil? (ctk/get-swap-slot shape))) + (let [container (:container (meta shape)) + file {:id (:id file-data) :data file-data} + ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true :with-context? true})] + (if (and (some? ref-shape) + (or (not= (:component-id shape) (:component-id ref-shape)) + (not= (:component-file shape) (:component-file ref-shape)))) + (let [shape' (cond-> shape + (some? (:component-id ref-shape)) + (assoc :component-id (:component-id ref-shape)) + + (nil? (:component-id ref-shape)) + (dissoc :component-id) + + (some? (:component-file ref-shape)) + (assoc :component-file (:component-file ref-shape)) + + (nil? (:component-file ref-shape)) + (dissoc :component-file))] + (log/warn :msg "Syncing component id and file with ref shape" + :shape-id (:id shape) + :shape-name (:name shape) + :component-id (:component-id shape') + :component-file (:component-file shape') + :ref-shape-id (:id ref-shape) + :file-id (:id file) + :container-id (:id container) + :container-type (:type container)) + {:result :update :updated-shape shape'}) + {:result :keep})) + {:result :keep}))))] + ;; If a copy inside a main is updated, we need to repeat the process for the change to be + ;; propagated to all copies. + (loop [current-data file-data + iteration 0] + (let [next-data (sync-one-iteration current-data libraries)] + (if (or (= current-data next-data) + (> iteration 20)) ;; safety bound + next-data + (recur next-data (inc iteration))))))) diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index 3655f3ece5..eeb11e9067 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -10,6 +10,7 @@ [app.common.data.macros :as dm] [app.common.features :as cfeat] [app.common.files.changes :as cpc] + [app.common.files.comp-processors :as cfcp] [app.common.files.defaults :as cfd] [app.common.files.helpers :as cfh] [app.common.geom.matrix :as gmt] @@ -1786,6 +1787,24 @@ (update :pages-index d/update-vals update-container) (d/update-when :components d/update-vals update-container)))) +(defmethod migrate-data "0018-remove-unneeded-objects-from-components" + [data _] + (cfcp/remove-unneeded-objects-in-components data)) + +(defmethod migrate-data "0019-fix-missing-swap-slots" + [data _] + (let [libraries (if (:libs data) + (deref (:libs data)) + {})] + (cfcp/fix-missing-swap-slots data libraries))) + +(defmethod migrate-data "0020-sync-component-id-with-near-main" + [data _] + (let [libraries (if (:libs data) + (deref (:libs data)) + {})] + (cfcp/sync-component-id-with-ref-shape data libraries))) + (def available-migrations (into (d/ordered-set) ["legacy-2" @@ -1860,4 +1879,7 @@ "0015-fix-text-attrs-blank-strings" "0015-clean-shadow-color" "0016-copy-fills-from-position-data-to-text-node" - "0017-fix-layout-flex-dir"])) + "0017-fix-layout-flex-dir" + "0018-remove-unneeded-objects-from-components" + "0019-fix-missing-swap-slots" + "0020-sync-component-id-with-near-main"])) diff --git a/common/src/app/common/files/repair.cljc b/common/src/app/common/files/repair.cljc index 454cc78e0a..29e6d4fdf5 100644 --- a/common/src/app/common/files/repair.cljc +++ b/common/src/app/common/files/repair.cljc @@ -334,6 +334,31 @@ (pcb/with-file-data file-data) (pcb/update-shapes [(:id shape)] repair-shape)))) +(defmethod repair-error :component-id-mismatch + [_ {:keys [shape page-id args] :as error} file-data _] + (let [repair-shape + (fn [shape] + ; Set the component-id and component-file to the ones of the near main + (log/debug :hint (str " -> set component-id to " (:component-id args))) + (log/debug :hint (str " -> set component-file to " (:component-file args))) + (cond-> shape + (some? (:component-id args)) + (assoc :component-id (:component-id args)) + + (nil? (:component-id args)) + (dissoc :component-id) + + (some? (:component-file args)) + (assoc :component-file (:component-file args)) + + (nil? (:component-file args)) + (dissoc :component-file)))] + + (log/dbg :hint "repairing shape :component-id-mismatch" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + (defmethod repair-error :ref-shape-is-head [_ {:keys [shape page-id args] :as error} file-data _] (let [repair-shape @@ -501,7 +526,7 @@ (pcb/update-shapes [(:id shape)] repair-shape)))) (defmethod repair-error :component-nil-objects-not-allowed - [_ {:keys [shape] :as error} file-data _] + [_ {component :shape} file-data _] ; in this error the :shape argument is the component (let [repair-component (fn [component] ;; Remove the objects key, or set it to {} if the component is deleted @@ -513,10 +538,26 @@ (log/debug :hint " -> remove :objects") (dissoc component :objects))))] - (log/dbg :hint "repairing component :component-nil-objects-not-allowed" :id (:id shape) :name (:name shape)) + (log/dbg :hint "repairing component :component-nil-objects-not-allowed" :id (:id component) :name (:name component)) (-> (pcb/empty-changes nil) (pcb/with-library-data file-data) - (pcb/update-component (:id shape) repair-component)))) + (pcb/update-component (:id component) repair-component)))) + +(defmethod repair-error :non-deleted-component-cannot-have-objects + [_ {component :shape} file-data _] ; in this error the :shape argument is the component + (let [repair-component + (fn [component] + ; Remove the :objects field + (if-not (:deleted component) + (do + (log/debug :hint " -> remove :objects") + (dissoc component :objects)) + component))] + + (log/dbg :hint "repairing component :non-deleted-component-cannot-have-objects" :id (:id component) :name (:name component)) + (-> (pcb/empty-changes nil) + (pcb/with-library-data file-data) + (pcb/update-component (:id component) repair-component)))) (defmethod repair-error :invalid-text-touched [_ {:keys [shape page-id] :as error} file-data _] diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc index 5b0e1d74d4..1c16c4dcbc 100644 --- a/common/src/app/common/files/validate.cljc +++ b/common/src/app/common/files/validate.cljc @@ -51,6 +51,7 @@ :ref-shape-is-head :ref-shape-is-not-head :shape-ref-in-main + :component-id-mismatch :root-main-not-allowed :nested-main-not-allowed :root-copy-not-allowed @@ -59,6 +60,7 @@ :not-head-copy-not-allowed :not-component-not-allowed :component-nil-objects-not-allowed + :non-deleted-component-cannot-have-objects :instance-head-not-frame :invalid-text-touched :misplaced-slot @@ -326,6 +328,20 @@ :component-file (:component-file ref-shape) :component-id (:component-id ref-shape))))) +(defn- check-ref-component-id + "Validate that if the copy has not been swapped, the component-id and component-file are + the same as in the referenced shape in the near main." + [shape file page libraries] + (when (nil? (ctk/get-swap-slot shape)) + (when-let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true)] + (when (or (not= (:component-id shape) (:component-id ref-shape)) + (not= (:component-file shape) (:component-file ref-shape))) + (report-error :component-id-mismatch + "Nested copy component-id and component-file must be the same as the near main" + shape file page + :component-id (:component-id ref-shape) + :component-file (:component-file ref-shape)))))) + (defn- check-empty-swap-slot "Validate that this shape does not have any swap slot." [shape file page] @@ -350,6 +366,19 @@ "This shape has children with the same swap slot" shape file page))) +(defn- check-required-swap-slot + "Validate that the shape has swap-slot if it's a subinstance head and the ref shape is not the + matching shape by position in the near main." + [shape file page libraries] + (let [near-match (ctf/find-near-match file page libraries shape :include-deleted? true :with-context? false)] + (when (and (some? near-match) + (not= (:shape-ref shape) (:id near-match)) + (nil? (ctk/get-swap-slot shape))) + (report-error :missing-slot + "Shape has been swapped, should have swap slot" + shape file page + :swap-slot (or (ctk/get-swap-slot near-match) (:id near-match)))))) + (defn- check-valid-touched "Validate that the text touched flags are coherent." [shape file page] @@ -418,6 +447,8 @@ (check-component-not-main-head shape file page libraries) (check-component-not-root shape file page) (check-valid-touched shape file page) + (check-ref-component-id shape file page libraries) + (check-required-swap-slot shape file page libraries) ;; We can have situations where the nested copy and the ancestor copy come from different libraries and some of them have been dettached ;; so we only validate the shape-ref if the ancestor is from a valid library (when library-exists @@ -458,8 +489,7 @@ (defn- check-variant-container "Shape is a variant container, so: -all its children should be variants with variant-id equals to the shape-id - -all the components should have the same properties - " + -all the components should have the same properties" [shape file page] (let [shape-id (:id shape) shapes (:shapes shape) @@ -648,6 +678,13 @@ "Component main not allowed inside other component" main-instance file component-page)))) +(defn- check-not-objects + [component file] + (when (d/not-empty? (:objects component)) + (report-error :non-deleted-component-cannot-have-objects + "A non-deleted component cannot have shapes inside" + component file nil))) + (defn- check-component "Validate semantic coherence of a component. Report all errors found." [component file] @@ -656,7 +693,8 @@ "Objects list cannot be nil" component file nil)) (when-not (:deleted component) - (check-main-inside-main component file)) + (check-main-inside-main component file) + (check-not-objects component file)) (when (:deleted component) (check-component-duplicate-swap-slot component file) (check-ref-cycles component file)) @@ -674,8 +712,6 @@ ;; PUBLIC API: VALIDATION FUNCTIONS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare check-swap-slots) - (defn validate-file "Validate full referential integrity and semantic coherence on file data. @@ -686,8 +722,6 @@ (doseq [page (filter :id (ctpl/pages-seq data))] (check-shape uuid/zero file page libraries) - (when (str/includes? (:name file) "check-swap-slot") - (check-swap-slots uuid/zero file page libraries)) (->> (get-orphan-shapes page) (run! #(check-shape % file page libraries)))) @@ -728,40 +762,3 @@ :hint "error on validating file referential integrity" :file-id (:id file) :details errors))) - -(declare compare-slots) - -;; Optional check to look for missing swap slots. -;; Search for copies that do not point the shape-ref to the near component but don't have swap slot -;; (looking for position relative to the parent, in the copy and the main). -;; -;; This check cannot be generally enabled, because files that have been migrated from components v1 -;; may have copies with shapes that do not match by position, but have not been swapped. So we enable -;; it for specific files only. To activate the check, you need to add the string "check-swap-slot" to -;; the name of the file. -(defn- check-swap-slots - [shape-id file page libraries] - (let [shape (ctst/get-shape page shape-id)] - (if (and (ctk/instance-root? shape) (ctk/in-component-copy? shape)) - (let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true :with-context? true) - container (:container (meta ref-shape))] - (when (some? ref-shape) - (compare-slots shape ref-shape file page container))) - (doall (for [child-id (:shapes shape)] - (check-swap-slots child-id file page libraries)))))) - -(defn- compare-slots - [shape-copy shape-main file container-copy container-main] - (if (and (not= (:shape-ref shape-copy) (:id shape-main)) - (nil? (ctk/get-swap-slot shape-copy))) - (report-error :missing-slot - "Shape has been swapped, should have swap slot" - shape-copy file container-copy - :swap-slot (or (ctk/get-swap-slot shape-main) (:id shape-main))) - (when (nil? (ctk/get-swap-slot shape-copy)) - (let [children-id-pairs (d/zip-all (:shapes shape-copy) (:shapes shape-main))] - (doall (for [[child-copy-id child-main-id] children-id-pairs] - (let [child-copy (ctst/get-shape container-copy child-copy-id) - child-main (ctst/get-shape container-main child-main-id)] - (when (and (some? child-copy) (some? child-main)) - (compare-slots child-copy child-main file container-copy container-main))))))))) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 7e703385e7..35df16aa86 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -333,7 +333,7 @@ (pcb/update-shapes [shape-id] #(do (log/trace :msg " -> promote to root") (assoc % :component-root true))) - :always + (some? (ctk/get-swap-slot shape)) ; First level subinstances of a detached component can't have swap-slot (pcb/update-shapes [shape-id] #(do (log/trace :msg " -> remove swap-slot") (ctk/remove-swap-slot %))) @@ -364,7 +364,7 @@ (let [ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true})] (cond-> changes (some? (:shape-ref ref-shape)) - (pcb/update-shapes [(:id shape)] #(do (log/trace :msg " (advanced)") + (pcb/update-shapes [(:id shape)] #(do (log/trace :msg (str " (advanced to " (:shape-ref ref-shape) ")")) (assoc % :shape-ref (:shape-ref ref-shape)))) ;; When advancing level, the normal touched groups (not swap slots) of the @@ -374,16 +374,18 @@ (pcb/update-shapes [(:id shape)] #(do (log/trace :msg " (merge touched)") + (log/trace :msg (str " (ref-shape: " (:id ref-shape) ")")) + (log/trace :msg (str " (ref touched: " (:touched ref-shape) ")")) (assoc % :touched - (clojure.set/union (:touched shape) - (ctk/normal-touched-groups ref-shape))))) + (set/union (:touched shape) + (ctk/normal-touched-groups ref-shape))))) ;; Swap slot must also be copied if the current shape has not any, ;; except if this is the first level subcopy. (and (some? (ctk/get-swap-slot ref-shape)) (nil? (ctk/get-swap-slot shape)) (not= (:id shape) shape-id)) - (pcb/update-shapes [(:id shape)] #(do (log/trace :msg " (got swap-slot)") + (pcb/update-shapes [(:id shape)] #(do (log/trace :msg (str " (got swap-slot " (ctk/get-swap-slot ref-shape) ")")) (ctk/set-swap-slot % (ctk/get-swap-slot ref-shape)))) ;; If we can't get the ref-shape (e.g. it's in an external library not linked), @@ -771,14 +773,6 @@ ;; is different than the one in the near component (Shape-2-2-1) ;; but it's not touched. -(defn- redirect-shaperef ;;Set the :shape-ref of a shape pointing to the :id of its remote-shape - ([container libraries shape] - (redirect-shaperef nil nil shape (ctf/find-remote-shape container libraries shape))) - ([_ _ shape remote-shape] - (if (some? (:shape-ref shape)) - (assoc shape :shape-ref (:id remote-shape)) - shape))) - (defn generate-sync-shape-direct "Generate changes to synchronize one shape that is the root of a component instance, and all its children, from the given component." @@ -790,18 +784,12 @@ component (ctkl/get-component library (:component-id shape-inst) true)] (if (and (ctk/in-component-copy? shape-inst) (or (ctf/direct-copy? shape-inst component container nil libraries) reset?)) ; In a normal sync, we don't want to sync remote mains, only direct/near - (let [redirect-shaperef (partial redirect-shaperef container libraries) - - shape-main (when component + (let [shape-main (when component (if reset? ;; the reset is against the ref-shape, not against the original shape of the component (ctf/find-ref-shape file container libraries shape-inst) (ctf/get-ref-shape library component shape-inst))) - shape-inst (if reset? - (redirect-shaperef shape-inst shape-main) - shape-inst) - initial-root? (:component-root shape-inst) root-inst shape-inst @@ -819,8 +807,8 @@ root-inst root-main reset? - initial-root? - redirect-shaperef) + initial-root?) + ;; If the component is not found, because the master component has been ;; deleted or the library unlinked, do nothing. changes)) @@ -844,7 +832,7 @@ nil)))))) (defn- generate-sync-shape-direct-recursive - [changes container shape-inst component library file libraries shape-main root-inst root-main reset? initial-root? redirect-shaperef] + [changes container shape-inst component library file libraries shape-main root-inst root-main reset? initial-root?] (shape-log :debug (:id shape-inst) container :msg "Sync shape direct recursive" :shape-inst (str (:name shape-inst) " " (pretty-uuid (:id shape-inst))) @@ -891,9 +879,6 @@ children-inst (vec (ctn/get-direct-children container shape-inst)) children-main (vec (ctn/get-direct-children component-container shape-main)) - children-inst (if reset? - (map #(redirect-shaperef %) children-inst) children-inst) - only-inst (fn [changes child-inst] (shape-log :trace (:id child-inst) container :msg "Only inst" @@ -942,8 +927,7 @@ root-inst root-main reset? - initial-root? - redirect-shaperef)) + initial-root?)) swapped (fn [changes child-inst child-main] (shape-log :trace (:id child-inst) container @@ -1008,16 +992,13 @@ the values in the shape and all its children." [changes file libraries container shape-id] (shape-log :debug shape-id container :msg "Sync shape inverse" :shape (str shape-id)) - (let [redirect-shaperef (partial redirect-shaperef container libraries) - shape-inst (ctn/get-shape container shape-id) + (let [shape-inst (ctn/get-shape container shape-id) library (dm/get-in libraries [(:component-file shape-inst) :data]) component (ctkl/get-component library (:component-id shape-inst)) shape-main (when component (ctf/find-remote-shape container libraries shape-inst)) - shape-inst (redirect-shaperef shape-inst shape-main) - initial-root? (:component-root shape-inst) root-inst shape-inst @@ -1038,12 +1019,11 @@ shape-main root-inst root-main - initial-root? - redirect-shaperef) + initial-root?) changes))) (defn- generate-sync-shape-inverse-recursive - [changes container shape-inst component library file libraries shape-main root-inst root-main initial-root? redirect-shaperef] + [changes container shape-inst component library file libraries shape-main root-inst root-main initial-root?] (shape-log :trace (:id shape-inst) container :msg "Sync shape inverse recursive" :shape (str (:name shape-inst)) @@ -1100,8 +1080,6 @@ children-main (mapv #(ctn/get-shape component-container %) (:shapes shape-main)) - children-inst (map #(redirect-shaperef %) children-inst) - only-inst (fn [changes child-inst] (add-shape-to-main changes child-inst @@ -1130,8 +1108,7 @@ child-main root-inst root-main - initial-root? - redirect-shaperef)) + initial-root?)) swapped (fn [changes child-inst child-main] (shape-log :trace (:id child-inst) container @@ -1773,6 +1750,23 @@ (pcb/update-shapes changes [(:id dest-shape)] ctk/unhead-shape {:ignore-touched true}) changes)) +(defn- check-swapped-main + [changes dest-shape origin-shape] + ;; Only for direct updates (from main to copy). Check if the main shape + ;; has been swapped. If so, the new component-id and component-file must + ;; be put into the copy. + (if (and (= (:shape-ref dest-shape) (:id origin-shape)) + (ctk/instance-head? dest-shape) + (ctk/instance-head? origin-shape) + (or (not= (:component-id dest-shape) (:component-id origin-shape)) + (not= (:component-file dest-shape) (:component-file origin-shape)))) + (pcb/update-shapes changes [(:id dest-shape)] + #(assoc % + :component-id (:component-id origin-shape) + :component-file (:component-file origin-shape)) + {:ignore-touched true}) + changes)) + (defn- update-attrs "The main function that implements the attribute sync algorithm. Copy attributes that have changed in the origin shape to the dest shape. @@ -1816,6 +1810,8 @@ :always (check-detached-main dest-shape origin-shape) :always + (check-swapped-main dest-shape origin-shape) + :always (generate-update-tokens container dest-shape origin-shape touched omit-touched? nil)) (let [sync-group diff --git a/common/src/app/common/test_helpers/compositions.cljc b/common/src/app/common/test_helpers/compositions.cljc index f5c9b5a1ca..83f12fa084 100644 --- a/common/src/app/common/test_helpers/compositions.cljc +++ b/common/src/app/common/test_helpers/compositions.cljc @@ -177,8 +177,11 @@ (thc/instantiate-component component-label copy-root-label copy-root-params))) (defn add-nested-component - [file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label - & {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params]}] + [file + component1-label main1-root-label main1-child-label + component2-label main2-root-label nested-head-label + & {:keys [component1-params root1-params main1-child-params + component2-params main2-root-params nested-head-params]}] ;; Generated shape tree: ;; {:main1-root-label} [:name Frame1] # [Component :component1-label] ;; :main1-child-label [:name Rect1] @@ -204,8 +207,13 @@ component2-params))) (defn add-nested-component-with-copy - [file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label copy2-root-label - & {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params copy2-root-params]}] + [file + component1-label main1-root-label main1-child-label + component2-label main2-root-label nested-head-label + copy2-root-label + & {:keys [component1-params root1-params main1-child-params + component2-params main2-root-params nested-head-params + copy2-root-params]}] ;; Generated shape tree: ;; {:main1-root-label} [:name Frame1] # [Component :component1-label] ;; :main1-child-label [:name Rect1] @@ -232,6 +240,102 @@ :nested-head-params nested-head-params) (thc/instantiate-component component2-label copy2-root-label copy2-root-params))) +(defn add-two-levels-nested-component + [file + component1-label main1-root-label main1-child-label + component2-label main2-root-label nested-head1-label + component3-label main3-root-label nested-head2-label nested-subhead2-label + & {:keys [component1-params root1-params main1-child-params + component2-params main2-root-params nested-head1-params + component3-params main3-root-params nested-head2-params]}] + ;; Generated shape tree: + ;; {:main1-root-label} [:name Frame1] # [Component :component1-label] + ;; :main1-child-label [:name Rect1] + ;; + ;; {:main2-root-label} [:name Frame2] # [Component :component2-label] + ;; :nested-head1-label [:name Frame1] @--> [Component :component1-label] :main1-root-label + ;; [:name Rect1] ---> :main1-child-label + ;; + ;; {:main3-root-label} [:name Frame3] # [Component :component3-label] + ;; :nested-head2-label [:name Frame2] @--> [Component :component2-label] :main2-root-label + ;; :nested-subhead2-label [:name Frame1] @--> [Component :component1-label] :main1-root-label + ;; [:name Rect1] ---> :main1-child-label + (-> file + (add-simple-component component1-label + main1-root-label + main1-child-label + :component-params component1-params + :root-params root1-params + :child-params main1-child-params) + (add-frame main2-root-label (merge {:name "Frame2"} + main2-root-params)) + (thc/instantiate-component component1-label + nested-head1-label + (assoc nested-head1-params + :parent-label main2-root-label)) + (thc/make-component component2-label + main2-root-label + component2-params) + (add-frame main3-root-label (merge {:name "Frame3"} + main3-root-params)) + (thc/instantiate-component component2-label + nested-head2-label + (assoc nested-head2-params + :parent-label main3-root-label + :children-labels [nested-subhead2-label])) + (thc/make-component component3-label + main3-root-label + component3-params))) + +(defn add-two-levels-nested-component-with-copy + [file + component1-label main1-root-label main1-child-label + component2-label main2-root-label nested-head1-label + component3-label main3-root-label nested-head2-label nested-subhead2-label + copy2-root-label + & {:keys [component1-params root1-params main1-child-params + component2-params main2-root-params nested-head1-params + component3-params main3-root-params nested-head2-params + copy2-root-params]}] + ;; Generated shape tree: + ;; {:main1-root-label} [:name Frame1] # [Component :component1-label] + ;; :main1-child-label [:name Rect1] + ;; + ;; {:main2-root-label} [:name Frame2] # [Component :component2-label] + ;; :nested-head1-label [:name Frame1] @--> [Component :component1-label] :main1-root-label + ;; [:name Rect1] ---> :main1-child-label + ;; + ;; {:main3-root-label} [:name Frame3] # [Component :component3-label] + ;; :nested-head2-label [:name Frame2] @--> [Component :component2-label] :main2-root-label + ;; :nested-subhead2-label [:name Frame1] @--> [Component :component1-label] :main1-root-label + ;; [:name Rect1] ---> :main1-child-label + ;; + ;; :copy2-label [:name Frame3] #--> [Component :component3-label] :main3-root-label + ;; [:name Frame2] @--> [Component :component2-label] :nested-head2-label + ;; [:name Frame1] @--> [Component :component1-label] :nested-subhead2-label + ;; [:name Rect1] ---> + (-> file + (add-two-levels-nested-component component1-label + main1-root-label + main1-child-label + component2-label + main2-root-label + nested-head1-label + component3-label + main3-root-label + nested-head2-label + nested-subhead2-label + :component1-params component1-params + :root1-params root1-params + :main1-child-params main1-child-params + :component2-params component2-params + :main2-root-params main2-root-params + :nested-head1-params nested-head1-params + :component3-params component3-params + :main3-root-params main3-root-params + :nested-head2-params nested-head2-params) + (thc/instantiate-component component3-label copy2-root-label copy2-root-params))) + ;; ----- Getters (defn bottom-shape-by-id @@ -274,15 +378,18 @@ file-id {file-id file} file-id))] - (thf/apply-changes file changes))) + (thf/apply-changes file changes :validate? false))) -(defn swap-component +(defn swap-component- "Swap the specified shape by the component specified by component-tag" - [file shape component-tag & {:keys [page-label propagate-fn keep-touched? new-shape-label]}] + [file shape component-tag & {:keys [page-label propagate-fn keep-touched? new-shape-label library]}] (let [page (if page-label (thf/get-page file page-label) (thf/current-page file)) - libraries {(:id file) file} + libraries (cond-> {(:id file) file} + (some? library) + (assoc (:id library) library)) + library (or library file) orig-shapes (when keep-touched? (cfh/get-children-with-self (:objects page) (:id shape))) @@ -290,10 +397,10 @@ (cll/generate-component-swap (pcb/empty-changes) (:objects page) shape - (:data file) + (:data library) page libraries - (-> (thc/get-component file component-tag) + (-> (thc/get-component library component-tag) :id) 0 nil @@ -305,26 +412,36 @@ [changes nil]) - file' (thf/apply-changes file changes)] + file' (thf/apply-changes file changes :validate? (not propagate-fn))] (when new-shape-label (thi/rm-id! (:id new-shape)) (thi/set-id! new-shape-label (:id new-shape))) (if propagate-fn - (propagate-fn file') + (-> (propagate-fn file') + (thf/validate-file!)) file'))) -(defn swap-component-in-shape [file shape-tag component-tag & {:keys [page-label propagate-fn]}] - (swap-component file (ths/get-shape file shape-tag :page-label page-label) component-tag :page-label page-label :propagate-fn propagate-fn)) +(defn swap-component-in-shape + [file shape-tag component-tag & {:keys [page-label propagate-fn keep-touched? new-shape-label library]}] + (swap-component- file (ths/get-shape file shape-tag :page-label page-label) + component-tag + :page-label page-label + :propagate-fn propagate-fn + :keep-touched? keep-touched? + :new-shape-label new-shape-label + :library library)) -(defn swap-component-in-first-child [file shape-tag component-tag & {:keys [page-label propagate-fn]}] +(defn swap-component-in-first-child + [file shape-tag component-tag & {:keys [page-label propagate-fn library]}] (let [first-child-id (->> (ths/get-shape file shape-tag :page-label page-label) :shapes first)] - (swap-component file - (ths/get-shape-by-id file first-child-id :page-label page-label) - component-tag - :page-label page-label - :propagate-fn propagate-fn))) + (swap-component- file + (ths/get-shape-by-id file first-child-id :page-label page-label) + component-tag + :page-label page-label + :propagate-fn propagate-fn + :library library))) (defn update-color "Update the first fill color for the shape identified by shape-tag" @@ -339,9 +456,10 @@ (assoc shape :fills (ths/sample-fills-color :fill-color color))) (:objects page) {}) - file' (thf/apply-changes file changes)] + file' (thf/apply-changes file changes :validate? (not propagate-fn))] (if propagate-fn - (propagate-fn file') + (-> (propagate-fn file') + (thf/validate-file!)) file'))) (defn update-bottom-color @@ -357,9 +475,10 @@ (assoc shape :fills (ths/sample-fills-color :fill-color color))) (:objects page) {}) - file' (thf/apply-changes file changes)] + file' (thf/apply-changes file changes :validate? (not propagate-fn))] (if propagate-fn - (propagate-fn file') + (-> (propagate-fn file') + (thf/validate-file!)) file'))) (defn reset-overrides [file shape & {:keys [page-label propagate-fn]}] @@ -374,9 +493,10 @@ {file-id file} (ctn/make-container container :page) (:id shape))) - file' (thf/apply-changes file changes)] + file' (thf/apply-changes file changes :validate? (not propagate-fn))] (if propagate-fn - (propagate-fn file') + (-> (propagate-fn file') + (thf/validate-file!)) file'))) (defn reset-overrides-in-first-child [file shape-tag & {:keys [page-label propagate-fn]}] @@ -398,9 +518,10 @@ #{(-> (ths/get-shape file shape-tag :page-label page-label) :id)} {}) - file' (thf/apply-changes file changes)] + file' (thf/apply-changes file changes :validate? (not propagate-fn))] (if propagate-fn - (propagate-fn file') + (-> (propagate-fn file') + (thf/validate-file!)) file'))) (defn duplicate-shape [file shape-tag & {:keys [page-label propagate-fn]}] @@ -419,8 +540,9 @@ (:id file)) ;; file-id (cll/generate-duplicate-changes-update-indices (:objects page) ;; objects #{(:id shape)})) - file' (thf/apply-changes file changes)] + file' (thf/apply-changes file changes :validate? (not propagate-fn))] (if propagate-fn - (propagate-fn file') + (-> (propagate-fn file') + (thf/validate-file!)) file'))) diff --git a/common/src/app/common/test_helpers/files.cljc b/common/src/app/common/test_helpers/files.cljc index a80675b65a..6357ab555b 100644 --- a/common/src/app/common/test_helpers/files.cljc +++ b/common/src/app/common/test_helpers/files.cljc @@ -54,12 +54,14 @@ ([file] (validate-file! file {})) ([file libraries] (cfv/validate-file-schema! file) - (cfv/validate-file! file libraries))) + (cfv/validate-file! file libraries) + file)) (defn apply-changes - [file changes] + [file changes & {:keys [validate?] :or {validate? true}}] (let [file' (ctf/update-file-data file #(cfc/process-changes % (:redo-changes changes) true))] - (validate-file! file') + (when validate? + (validate-file! file')) file')) (defn apply-undo-changes diff --git a/common/src/app/common/test_helpers/shapes.cljc b/common/src/app/common/test_helpers/shapes.cljc index b212984e06..d557c0501f 100644 --- a/common/src/app/common/test_helpers/shapes.cljc +++ b/common/src/app/common/test_helpers/shapes.cljc @@ -82,6 +82,18 @@ (:id page) #(ctst/set-shape % (ctn/set-shape-attr shape attr val))))))) +(defn update-shape-by-id + [file shape-id attr val & {:keys [page-label]}] + (let [page (if page-label + (thf/get-page file page-label) + (thf/current-page file)) + shape (ctst/get-shape page shape-id)] + (update file :data + (fn [file-data] + (ctpl/update-page file-data + (:id page) + #(ctst/set-shape % (ctn/set-shape-attr shape attr val))))))) + (defn update-shape-text [file shape-label attr val & {:keys [page-label]}] (let [page (if page-label diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index d07ffaeb50..ecc6e30c65 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -163,11 +163,15 @@ Note that design tokens also are involved, although they go by an alternate route and thus they are not part of :sync-attrs. Also when detaching a nested copy it also needs to trigger a synchronization, - even though :shape-ref is not a synced attribute per se" + even though :shape-ref, :component-id or :component-file are not synced + attributes per se." [attr] (or (contains? sync-attrs attr) (= :shape-ref attr) - (= :applied-tokens attr))) + (= :applied-tokens attr) + (= :component-id attr) + (= :component-file attr) + (= :component-root attr))) (defn instance-root? "Check if this shape is the head of a top instance." diff --git a/common/src/app/common/types/components_list.cljc b/common/src/app/common/types/components_list.cljc index c4f3a66063..be92b16999 100644 --- a/common/src/app/common/types/components_list.cljc +++ b/common/src/app/common/types/components_list.cljc @@ -60,6 +60,9 @@ (some? objects) (assoc :objects objects) + (nil? objects) + (dissoc :objects) + (some? modified-at) (assoc :modified-at modified-at) diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index df9ce86be2..4e6021dbb6 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -55,6 +55,10 @@ [page-or-component type] (assoc page-or-component :type type)) +(defn unmake-container + [container] + (dissoc container :type)) + (defn page? [container] (= (:type container) :page)) diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 3733359a6c..974db477b3 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -204,7 +204,8 @@ (defn update-file-data [file f] - (update file :data f)) + (when file + (update file :data f))) (defn containers-seq "Generate a sequence of all pages and all components, wrapped as containers" @@ -225,6 +226,85 @@ (ctpl/update-page file-data (:id container) f) (ctkl/update-component file-data (:id container) f))) +(defn update-pages + "Update all pages inside the file" + [file-data f] + (update file-data :pages-index d/update-vals + (fn [page] + (-> page + (ctn/make-container :page) + (f) + (ctn/unmake-container))))) + +(defn update-components + "Update all components inside the file" + [file-data f] + (d/update-when file-data :components d/update-vals + (fn [component] + (-> component + (ctn/make-container :component) + (f) + (ctn/unmake-container))))) + +(defn update-containers + "Update all pages and components inside the file" + [file-data f] + (-> file-data + (update-pages f) + (update-components f))) + +(defn update-objects-tree + "Do a depth-first traversal of the shapes in a container, doing different kinds of updates. + The function f receives a shape with a context metadata with the container. + It must return a map with the following keys: + - :result -> :keep, :update or :remove + - :updated-shape -> the updated shape if result is :update" + [container f] + (letfn [(update-shape-recursive + [container shape-id] + (let [shape (ctst/get-shape container shape-id)] + (when (not shape) + (throw (ex-info "Shape not found" {:shape-id shape-id}))) + (let [shape (with-meta shape {:container container}) + + {:keys [result updated-shape]} (f shape) + + container' + (case result + :keep + container + + :update + (ctst/set-shape container updated-shape) + + :remove + (ctst/delete-shape container shape-id true) + + (throw (ex-info "Invalid result from update function" {:result result})))] + + (if (= result :remove) + container' + (reduce update-shape-recursive + container' + (:shapes shape))))))] + + (let [root-id (if (ctn/page? container) + uuid/zero + (:main-instance-id container))] + + (if-not (empty? (:objects container)) + (update-shape-recursive container root-id) + container)))) + +(defn update-all-shapes + "Update all shapes in the file data, using the update-objects-tree function for each container" + [file-data f] + (when file-data + (update-containers + file-data + (fn [container] + (update-objects-tree container f))))) + ;; Asset helpers (defn find-component-file [file libraries component-file] @@ -328,6 +408,27 @@ (get-ref-shape (:data component-file) component shape :with-context? with-context?))))] (some find-ref-shape-in-head (ctn/get-parent-heads (:objects container) shape)))) +(defn find-near-match + "Locate the shape that occupies the same position in the near main component. + This will be the ref-shape except if the shape is a copy subhead that has been + swapped. In this case, the near match will be the ref-shape that was before + the swap." + [file container libraries shape & {:keys [include-deleted? with-context?] :or {include-deleted? false with-context? false}}] + (let [parent-shape (ctst/get-shape container (:parent-id shape)) + parent-ref-shape (when parent-shape + (find-ref-shape file container libraries parent-shape :include-deleted? include-deleted? :with-context? true)) + ref-container (when parent-ref-shape + (:container (meta parent-ref-shape))) + shape-index (when parent-shape + (d/index-of (:shapes parent-shape) (:id shape))) + near-match-id (when (and parent-ref-shape shape-index) + (get (:shapes parent-ref-shape) shape-index)) + near-match (when near-match-id + (cond-> (ctst/get-shape ref-container near-match-id) + with-context? + (with-meta (meta parent-ref-shape))))] + near-match)) + (defn advance-shape-ref "Get the shape-ref of the near main of the shape, recursively repeated as many times as the given levels." diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index 92732e18a1..3944d96afb 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -16,8 +16,6 @@ [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid])) - -;; FIXME: the order of arguments seems arbitrary, container should be a first artgument (defn add-shape "Insert a shape in the tree, at the given index below the given parent or frame. Update the parent as needed." diff --git a/common/src/app/common/uuid.cljc b/common/src/app/common/uuid.cljc index 9b21f8f796..ed542d868b 100644 --- a/common/src/app/common/uuid.cljc +++ b/common/src/app/common/uuid.cljc @@ -60,8 +60,9 @@ :cljs (uuid (impl/v4)))) (defn custom - ([a] #?(:clj (UUID. 0 a) :cljs (uuid (impl/custom 0 a)))) - ([b a] #?(:clj (UUID. b a) :cljs (uuid (impl/custom b a))))) + "Generate a uuid using directly the given number (specified as one or two long integers)" + ([low] #?(:clj (UUID. 0 low) :cljs (uuid (impl/custom 0 low)))) + ([high low] #?(:clj (UUID. high low) :cljs (uuid (impl/custom high low))))) (def zero (uuid "00000000-0000-0000-0000-000000000000")) @@ -137,6 +138,22 @@ (+ (clojure.lang.Murmur3/hashLong a) (clojure.lang.Murmur3/hashLong b))))) +;; Fake uuids generator +(def ^:private fake-ids (atom 0)) + +(defn reset-fake! + "Reset the fake uuid counter to 0, for reproducible results across tests." + [] + (reset! fake-ids 0)) + +(defn next-fake + "When you need predictable uuids, for example when debugging a failing test, wrap the code with + (with-redefs [uuid/next uuid/next-fake] + ...tested code...)" + [] + (-> (swap! fake-ids inc) + (custom))) + ;; Commented code used for debug ;; #?(:cljs ;; (defn ^:export test-uuid diff --git a/common/test/common_tests/files/comp_processors_test.cljc b/common/test/common_tests/files/comp_processors_test.cljc new file mode 100644 index 0000000000..c1cabbbb72 --- /dev/null +++ b/common/test/common_tests/files/comp_processors_test.cljc @@ -0,0 +1,787 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns common-tests.files.comp-processors-test + (:require + [app.common.data :as d] + [app.common.files.comp-processors :as cfcp] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [app.common.types.components-list :as ctkl] + [app.common.types.file :as ctf] + [clojure.test :as t])) + +(t/deftest test-remove-unneeded-objects-in-components + + (t/testing "nil file should return nil" + (let [file nil + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)] + (t/is (nil? file')))) + + (t/testing "empty file should not need any action" + (let [file (thf/sample-file :file1) + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)] + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file without components should not need any action" + (let [file + (-> (thf/sample-file :file1) + (tho/add-frame-with-child :frame1 :shape1)) + + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with non deleted components should not need any action" + (let [file + (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 :frame1 :shape1)) + + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with deleted components should not need any action" + (let [file + (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 :frame1 :shape1) + (tho/delete-shape :frame1)) + + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with non deleted components with :objects nil should remove it" + (let [file + (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 :frame1 :shape1) + (thc/update-component :component1 {:objects nil})) + + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components) + + diff (d/map-diff file file') + + expected-diff {:data + {:components + {(thi/id :component1) + {}}}}] + + (t/is (= expected-diff diff)))) + + (t/testing "file with non deleted components with :objects should remove it" + (let [file + (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 :frame1 :shape1) + (thc/update-component :component1 {:objects {:sample 777}})) + + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components) + + diff (d/map-diff file file') + + expected-diff {:data + {:components + {(thi/id :component1) + {:objects + [{:sample 777} nil]}}}}] + + (t/is (= expected-diff diff)))) + + (t/testing "file with deleted components without :objects should add an empty one" + (let [file + (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 :frame1 :shape1) + (tho/delete-shape :frame1) + (ctf/update-file-data + (fn [file-data] + (ctkl/update-component file-data (thi/id :component1) #(dissoc % :objects))))) + + file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components) + + diff (d/map-diff file file') + + expected-diff {:data + {:components + {(thi/id :component1) + {:objects + [nil {}]}}}}] + + (t/is (= expected-diff diff))))) + +(t/deftest test-fix-missing-swap-slots + + (t/testing "nil file should return nil" + (let [file nil + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + (t/is (nil? file')))) + + (t/testing "empty file should not need any action" + (let [file (thf/sample-file :file1) + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file without components should not need any action" + (let [file + ;; :frame1 [:name Frame1] + ;; :child1 [:name Rect1] + (-> (thf/sample-file :file1) + (tho/add-frame-with-child :frame1 :shape1)) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with nested not swapped components should not need any action" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; [:name Frame1] @--> [Component :component1] :nested-head + ;; [:name Rect1] ---> + (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 :main1-root :main1-child + :component2 :main2-root :nested-head + :copy2-root)) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with a normally swapped copy should not need any action" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; {swap-slot :nested-head} + ;; [:name Rect3] ---> :main3-child + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (tho/swap-component-in-first-child :copy2-root :component3)) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with a swapped nested copy in a main should not need any action" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; {swap-slot :nested-head} + ;; [:name Rect3] ---> :main3-child + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :nested-head + ;; [:name Rect3] ---> + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (tho/swap-component-in-shape :nested-head :component3 + :propagate-fn #(tho/propagate-component-changes % :component2))) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with a swapped copy with broken slot should have it repaired" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; NO SWAP SLOT + ;; [:name Rect3] ---> :main3-child + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (tho/swap-component-in-first-child :copy2-root :component3) + (ths/update-shape :copy2-nested-head :touched nil)) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {})) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :copy2-nested-head) + {:touched + [nil + #{(ctk/build-swap-slot-group (str (thi/id :nested-head)))}]}}}}}}] + + (t/is (= expected-diff diff)))) + + (t/testing "file with a swapped copy inside a main with broken slot has no effect since it cannot be distinguished" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; NO SWAP SLOT + ;; [:name Rect3] ---> :main3-child + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :nested-head + ;; [:name Rect3] ---> + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (tho/swap-component-in-shape :nested-head :component3 + :propagate-fn #(tho/propagate-component-changes % :component2)) + (ths/update-shape :nested-head :touched nil)) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with a two levels nested copy in a main swapped with broken slot should have it repaired" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head1 [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main4-root} [:name Frame4] # [Component :component4] + ;; :main4-child [:name Rect4] + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :nested-head2 [:name Frame2] @--> [Component :component2] :main2-root + ;; :nested-subhead2 [:name Frame4] @--> [Component :component4] :main4-root + ;; NO SWAP SLOT + ;; [:name Rect4] ---> :main4-child + ;; + ;; :copy2-root [:name Frame3] #--> [Component :component3] :main3-root + ;; [:name Frame2] @--> [Component :component2] :nested-head2 + ;; [:name Frame4] @--> [Component :component4] :nested-subhead2 + ;; [:name Rect4] ---> + (-> (thf/sample-file :file1) + (tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child + :component2 :main2-root :nested-head1 + :component3 :main3-root :nested-head2 :nested-subhead2 + :copy2-root) + (tho/add-simple-component :component4 :main4-root :main4-child + :root-params {:name "Frame4"} + :child-params {:name "Rect4"}) + (tho/swap-component-in-shape :nested-subhead2 :component4 + :propagate-fn #(tho/propagate-component-changes % :component3)) + (ths/update-shape :nested-subhead2 :touched nil)) + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {})) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :nested-subhead2) + {:touched + [nil + #{(ctk/build-swap-slot-group (str (thi/id :nested-head1)))}]}}}}}}] + + (t/is (= expected-diff diff)))) + + (t/testing "when components are in external libraries, the fix still works well" + (let [library1 + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested2-head [:name Frame1] @--> [Component :component1] :main1-root + ;; :nested2-child [:name Rect1] ---> :main1-child + (-> (thf/sample-file :library1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested2-head + :nested-head-params {:children-labels [:nested2-child]})) + library2 + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main4-root} [:name Frame4] # [Component :component4] + ;; :nested4-head [:name Frame3] @--> [Component :component1] :main3-root + ;; :nested4-child [:name Rect3] ---> :main3-child + (-> (thf/sample-file :library2) + (tho/add-nested-component :component3 :main3-root :main3-child + :component4 :main4-root :nested4-head + :root1-params {:name "Frame3"} + :main1-child-params {:name "Rect3"} + :main2-root-params {:name "Frame4"} + :nested-head-params {:children-labels [:nested4-child]})) + + file + ;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame4] @--> [Component :component4] :main4-root + ;; NO SWAP SLOT + ;; [:name Frame3] @--> :nested4-head + ;; [:name Rect3] ---> :nested4-child + (-> (thf/sample-file :file1) + (thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head] + :library library1) + (tho/swap-component-in-first-child :copy2 :component4 :library library2) + (ths/update-shape :copy2-nested-head :touched nil)) + + libraries {(:id library1) library1 + (:id library2) library2} + + file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % libraries)) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :copy2-nested-head) + {:touched + [nil + #{(ctk/build-swap-slot-group (str (thi/id :nested2-head)))}]}}}}}}] + + (t/is (= expected-diff diff))))) + +(t/deftest test-sync-component-id-with-ref-shape + + (t/testing "nil file should return nil" + (let [file nil + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))] + (t/is (nil? file')))) + + (t/testing "empty file should not need any action" + (let [file (thf/sample-file :file1) + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))] + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file without components should not need any action" + (let [file + ;; :frame1 [:name Frame1] + ;; :child1 [:name Rect1] + (-> (thf/sample-file :file1) + (tho/add-frame-with-child :frame1 :shape1)) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))] + + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with valid normal components should not need any action" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head1 [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :nested-head2 [:name Frame2] @--> [Component :component2] :main2-root + ;; :nested-subhead2 [:name Frame1] @--> [Component :component1] :nested-head1 + ;; [:name Rect1] ---> + ;; + ;; :copy2-root [:name Frame3] #--> [Component :component3] :main3-root + ;; [:name Frame2] @--> [Component :component2] :nested-head2 + ;; [:name Frame1] @--> [Component :component1] :nested-subhead2 + ;; [:name Rect1] ---> + (-> (thf/sample-file :file1) + (tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child + :component2 :main2-root :nested-head1 + :component3 :main3-root :nested-head2 :nested-subhead2 + :copy2-root)) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))] + + #_(thf/dump-file file') ;; Uncomment to debug + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with valid swapped components should not need any action" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; [:name Frame1] @--> [Component :component1] :nested-head + ;; [:name Rect1] ---> + ;; + ;; :copy3-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy3-nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; {swap-slot :nested-head} + ;; [:name Rect3] ---> :main3-child + (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 :main1-root :main1-child + :component2 :main2-root :nested-head + :copy2-root) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (thc/instantiate-component :component2 :copy3-root :children-labels [:copy3-nested-head]) + (tho/swap-component-in-first-child :copy3-root :component3)) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))] + + #_(thf/dump-file file') ;; Uncomment to debug + (t/is (empty? (d/map-diff file file'))))) + + (t/testing "file with a non swapped copy with broken component id/file should have it repaired" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame1] @--> [Component ] :nested-head ## <- BAD component-id + ;; [:name Rect1] ---> + ;; + ;; :copy3-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy3-nested-head [:name Frame1] @--> [Component ] :nested-head ## <- BAD component-file + ;; [:name Rect1] ---> + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (thc/instantiate-component :component2 :copy3-root :children-labels [:copy3-nested-head]) + (ths/update-shape :copy2-nested-head :component-id (thi/new-id! :some-other-id)) + (ths/update-shape :copy3-nested-head :component-file (thi/new-id! :some-other-file))) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {})) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :copy2-nested-head) + {:component-id + [(thi/id :some-other-id) (thi/id :component1)]} + (thi/id :copy3-nested-head) + {:component-file + [(thi/id :some-other-file) (thi/id :file1)]}}}}}}] + + #_(ctf/dump-tree file' (thf/current-page-id file') {(:id file') file'} {:show-ids true}) ;; Uncomment to debug + (t/is (= expected-diff diff)))) + + (t/testing "file with a copy of a swapped main with broken component id/file should have it repaired" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; {swap-slot :nested-head} + ;; [:name Rect3] ---> :main3-child + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame3] @--> [Component: ] :nested-head ## <- BAD component-id/file + ;; [:name Rect3] ---> + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (tho/swap-component-in-shape :nested-head :component3 + :propagate-fn #(tho/propagate-component-changes % :component2)) + (ths/update-shape :copy2-nested-head :component-id (thi/new-id! :some-other-id)) + (ths/update-shape :copy2-nested-head :component-file (thi/new-id! :some-other-file))) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {})) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :copy2-nested-head) + {:component-id + [(thi/id :some-other-id) (thi/id :component3)] + :component-file + [(thi/id :some-other-file) (thi/id :file1)]}}}}}}] + + #_(ctf/dump-tree file' (thf/current-page-id file') {(:id file') file'} {:show-ids true}) ;; Uncomment to debug + (t/is (= expected-diff diff)))) + + (t/testing "file with multiple copies of same component should sync all" + (let [file + (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 :frame1 :shape1) + (thc/instantiate-component :component1 :copy1-root :children-labels [:copy1-child]) + (thc/instantiate-component :component1 :copy2-root :children-labels [:copy2-child]) + (ths/update-shape :copy1-child :component-id (thi/new-id! :wrong-id1)) + (ths/update-shape :copy2-child :component-id (thi/new-id! :wrong-id2))) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {})) + + diff (d/map-diff file file')] + + ;; Both copies should be corrected + (t/is (contains? diff :data)) + (t/is (contains? (get-in diff [:data :pages-index]) (thf/current-page-id file))))) + + (t/testing "file with a copy root with broken component id/file cannot be repaired. But it's propagated to copies." + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component ] :main1-root ## <- BAD component-id/file + ;; [:name Rect1] ---> :main1-child + ;; + ;; :copy2-root [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame1] @--> [Component :component1] :nested-head + ;; [:name Rect1] ---> + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head) + (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head]) + (ths/update-shape :nested-head :component-id (thi/new-id! :some-other-id)) + (ths/update-shape :nested-head :component-file (thi/new-id! :some-other-file))) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {})) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :copy2-nested-head) + {:component-id + [(thi/id :component1) (thi/id :some-other-id)] + :component-file + [(thi/id :file1) (thi/id :some-other-file)]}}}}}}] + + (t/is (= expected-diff diff)))) + + (t/testing "file with a 2nd nested copy inside a main with broken component/id should have it repaired, and propagated to copies" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head1 [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :nested-head2 [:name Frame2] @--> [Component :component2] :main2-root + ;; :nested-subhead2 [:name Frame1] @--> [Component ] :nested-head1 ## <- BAD component-id/file + ;; [:name Rect1] ---> + ;; + ;; :copy2-root [:name Frame3] #--> [Component :component3] :main3-root + ;; [:name Frame2] @--> [Component :component2] :nested-head2 + ;; [:name Frame1] @--> [Component :component1] :nested-subhead2 + ;; [:name Rect1] ---> + (-> (thf/sample-file :file1) + (tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child + :component2 :main2-root :nested-head1 + :component3 :main3-root :nested-head2 :nested-subhead2 + :copy2-root) + (ths/update-shape :nested-subhead2 :component-id (thi/new-id! :some-other-id)) + (ths/update-shape :nested-subhead2 :component-file (thi/new-id! :some-other-file))) + + copy2-root (ths/get-shape file :copy2-root) + copy2-root-child1 (ths/get-shape-by-id file (first (:shapes copy2-root))) + copy2-root-child2 (ths/get-shape-by-id file (first (:shapes copy2-root-child1))) + file (-> file + (ths/update-shape-by-id (:id copy2-root-child2) :component-id (thi/id :some-other-id)) + (ths/update-shape-by-id (:id copy2-root-child2) :component-file (thi/id :some-other-file))) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {})) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :nested-subhead2) + {:component-id + [(thi/id :some-other-id) (thi/id :component1)] + :component-file + [(thi/id :some-other-file) (thi/id :file1)]} + (:id copy2-root-child2) + {:component-id + [(thi/id :some-other-id) (thi/id :component1)] + :component-file + [(thi/id :some-other-file) (thi/id :file1)]}}}}}}] + + #_(ctf/dump-tree file' (thf/current-page-id file') {(:id file') file'} {:show-ids true}) ;; Uncomment to debug + (t/is (= expected-diff diff)))) + + (t/testing "when components are in external libraries, the fix still works well" + (let [library1 + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested2-head [:name Frame4] @--> [Component :component4] :main4-root + ;; {swap-slot :nested2-head} + ;; :nested4-head [:name Frame3] @--> [Component: component3] :main3-root + ;; :nested4-child [:name Rect3] ---> :nested4-child + (-> (thf/sample-file :library1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested2-head + :nested-head-params {:children-labels [:nested2-child]})) + library2 + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main4-root} [:name Frame4] # [Component :component4] + ;; :nested4-head [:name Frame3] @--> [Component :component1] :main3-root + ;; :nested4-child [:name Rect3] ---> :main3-child + (-> (thf/sample-file :library2) + (tho/add-nested-component :component3 :main3-root :main3-child + :component4 :main4-root :nested4-head + :root1-params {:name "Frame3"} + :main1-child-params {:name "Rect3"} + :main2-root-params {:name "Frame4"} + :nested-head-params {:children-labels [:nested4-child]})) + + library1 + (tho/swap-component-in-shape library1 :nested2-head :component4 :library library2) + + file + ;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame4] @--> [Component ] :main4-root ## <- BAD component-id/file + ;; [:name Frame3] @--> :nested4-head + ;; [:name Rect3] ---> :nested4-child + (-> (thf/sample-file :file1) + (thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head] + :library library1) + (ths/update-shape :copy2-nested-head :component-id (thi/new-id! :some-other-id)) + (ths/update-shape :copy2-nested-head :component-file (thi/new-id! :some-other-file))) + + libraries {(:id library1) library1 + (:id library2) library2} + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % libraries)) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(thi/id :copy2-nested-head) + {:component-id + [(thi/id :some-other-id) (thi/id :component4)] + :component-file + [(thi/id :some-other-file) (thi/id :library2)]}}}}}}] + + #_(thf/dump-file library2) ;; Uncomment to debug + (t/is (= expected-diff diff)))) + + (t/testing "file with several broken ids should propagate to all copies" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head1 [:name Frame1] @--> [Component :component1] :main1-root + ;; [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :nested-head2 [:name Frame2] @--> [Component ] :main2-root ## <- BAD component-id + ;; :nested-subhead2 [:name Frame1] @--> [Component ] :nested-head1 ## <- BAD component-id + ;; [:name Rect1] ---> + ;; + ;; :copy2-root [:name Frame3] #--> [Component :component3] :main3-root + ;; [:name Frame2] @--> [Component :component2] :nested-head2 + ;; [:name Frame1] @--> [Component :component1] :nested-subhead2 + ;; [:name Rect1] ---> + (-> (thf/sample-file :file1) + (tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child + :component2 :main2-root :nested-head1 + :component3 :main3-root :nested-head2 :nested-subhead2 + :copy2-root) + ;; Corrupt both levels + (ths/update-shape :nested-head2 :component-id (thi/new-id! :wrong-comp2)) + (ths/update-shape :nested-subhead2 :component-id (thi/new-id! :wrong-comp3))) + + file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {})) + copy2-root (ths/get-shape file' :copy2-root) + copy2-root-child1 (ths/get-shape-by-id file' (first (:shapes copy2-root))) + copy2-root-child2 (ths/get-shape-by-id file' (first (:shapes copy2-root-child1))) + + diff (d/map-diff file file') + + expected-diff {:data + {:pages-index + {(thf/current-page-id file) + {:objects + {(:id copy2-root-child1) + {:component-id [(thi/id :component2) (thi/id :wrong-comp2)]} + (:id copy2-root-child2) + {:component-id [(thi/id :component1) (thi/id :wrong-comp3)]}}}}}}] + + (thf/dump-file file') ;; Uncomment to debug + (t/is (= expected-diff diff))))) + diff --git a/common/test/common_tests/logic/comp_detach_with_nested_test.cljc b/common/test/common_tests/logic/comp_detach_with_nested_test.cljc index 9460a3b91c..143221a4d3 100644 --- a/common/test/common_tests/logic/comp_detach_with_nested_test.cljc +++ b/common/test/common_tests/logic/comp_detach_with_nested_test.cljc @@ -465,9 +465,10 @@ page {(:id file) file} (thi/id :nested-h-ellipse)) - file' (-> (thf/apply-changes file changes) + file' (-> (thf/apply-changes file changes :validate? false) (tho/propagate-component-changes :c-board-with-ellipse) - (tho/propagate-component-changes :c-big-board)) + (tho/propagate-component-changes :c-big-board) + (thf/validate-file!)) ;; ==== Get nested2-h-ellipse (ths/get-shape file' :nested-h-ellipse) diff --git a/common/test/common_tests/logic/comp_reset_test.cljc b/common/test/common_tests/logic/comp_reset_test.cljc index 23894cc398..649b25e757 100644 --- a/common/test/common_tests/logic/comp_reset_test.cljc +++ b/common/test/common_tests/logic/comp_reset_test.cljc @@ -349,4 +349,73 @@ (t/is (= (:fill-color fill') "#FFFFFF")) (t/is (= (:fill-opacity fill') 1)) (t/is (= (:touched copy2-root') nil)) - (t/is (= (:touched copy2-child') nil)))) \ No newline at end of file + (t/is (= (:touched copy2-child') nil)))) + +(t/deftest test-reset-with-propagation-updates-copies + ;; When a nested copy inside a main component has an override and we + ;; reset it passing a propagate-fn, the reset must be propagated to + ;; all copies of that component so they reflect the canonical color. + (let [;; ==== Setup + file + (-> (thf/sample-file :file1) + ;; component1: main1-root / main1-child (fill "#aabbcc") + ;; component2: main2-root contains nested-head (instance of component1) + ;; copy2-root: copy of component2 + (tho/add-nested-component-with-copy + :component1 :main1-root :main1-child + :component2 :main2-root :nested-head + :copy2-root + :main1-child-params {:fills (ths/sample-fills-color :fill-color "#aabbcc")} + :copy2-root-params {:children-labels [:copy2-nested-head]})) + + propagate-fn (fn [f] + (-> f + (tho/propagate-component-changes :component1) + (tho/propagate-component-changes :component2))) + + ;; ==== Action – override the nested-head color, then reset it with propagation + file' + (-> file + (tho/update-bottom-color :nested-head "#fabada" :propagate-fn propagate-fn) + (tho/reset-overrides (ths/get-shape file :nested-head) :propagate-fn propagate-fn)) + + ;; ==== Get + copy2-bottom-color (tho/bottom-fill-color file' :copy2-root)] + + ;; ==== Check + ;; After reset + propagation the copy should mirror the canonical color + (t/is (= copy2-bottom-color "#aabbcc")))) + +(t/deftest test-reset-without-propagation-does-not-update-copies + ;; This is the regression test for the misplaced-parenthesis bug: when + ;; propagate-fn is NOT passed to reset-overrides the copies of the component + ;; must still hold the overridden value because the component sync never ran. + (let [;; ==== Setup + file + (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy + :component1 :main1-root :main1-child + :component2 :main2-root :nested-head + :copy2-root + :main1-child-params {:fills (ths/sample-fills-color :fill-color "#aabbcc")} + :copy2-root-params {:children-labels [:copy2-nested-head]})) + + propagate-fn (fn [f] + (-> f + (tho/propagate-component-changes :component1) + (tho/propagate-component-changes :component2))) + + ;; ==== Action – override the nested-head color, then reset WITHOUT propagation + file' + (-> file + (tho/update-bottom-color :nested-head "#fabada" :propagate-fn propagate-fn) + ;; Reset without propagate-fn: the component definition is updated but + ;; the change is never pushed to the copy. + (tho/reset-overrides (ths/get-shape file :nested-head))) + + ;; ==== Get + copy2-bottom-color (tho/bottom-fill-color file' :copy2-root)] + + ;; ==== Check + ;; Without propagation the copy still reflects the overridden color + (t/is (= copy2-bottom-color "#fabada")))) \ No newline at end of file diff --git a/common/test/common_tests/logic/duplicated_pages_test.cljc b/common/test/common_tests/logic/duplicated_pages_test.cljc index d1bafb88d7..57dd490143 100644 --- a/common/test/common_tests/logic/duplicated_pages_test.cljc +++ b/common/test/common_tests/logic/duplicated_pages_test.cljc @@ -64,9 +64,8 @@ (reset-all-overrides [file] (-> file - (tho/reset-overrides-in-first-child :frame-board-1 :page-label :page-1) - (tho/reset-overrides-in-first-child :copy-board-1 :page-label :page-2) - (propagate-all-component-changes))) + (tho/reset-overrides-in-first-child :frame-board-1 :page-label :page-1 :propagate-fn propagate-all-component-changes) + (tho/reset-overrides-in-first-child :copy-board-1 :page-label :page-2 :propagate-fn propagate-all-component-changes))) (fill-colors [file] [(tho/bottom-fill-color file :frame-ellipse-1 :page-label :page-1) diff --git a/common/test/common_tests/logic/multiple_nesting_levels_test.cljc b/common/test/common_tests/logic/multiple_nesting_levels_test.cljc index 43b7c7ef0e..11276ceb85 100644 --- a/common/test/common_tests/logic/multiple_nesting_levels_test.cljc +++ b/common/test/common_tests/logic/multiple_nesting_levels_test.cljc @@ -6,20 +6,11 @@ (ns common-tests.logic.multiple-nesting-levels-test (:require - [app.common.files.changes :as ch] - [app.common.files.changes-builder :as pcb] - [app.common.logic.libraries :as cll] - [app.common.logic.shapes :as cls] - [app.common.pprint :as pp] [app.common.test-helpers.components :as thc] [app.common.test-helpers.compositions :as tho] [app.common.test-helpers.files :as thf] [app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.shapes :as ths] - [app.common.types.component :as ctk] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [app.common.uuid :as uuid] [clojure.test :as t])) (t/use-fixtures :each thi/test-fixture) @@ -56,10 +47,9 @@ (reset-all-overrides [file] (-> file - (tho/reset-overrides (ths/get-shape file :copy-simple-1)) - (tho/reset-overrides (ths/get-shape file :copy-frame-composed-1)) - (tho/reset-overrides (ths/get-shape file :composed-1-composed-2-copy)) - (propagate-all-component-changes))) + (tho/reset-overrides (ths/get-shape file :copy-simple-1) :propagate-fn propagate-all-component-changes) + (tho/reset-overrides (ths/get-shape file :copy-frame-composed-1) :propagate-fn propagate-all-component-changes) + (tho/reset-overrides (ths/get-shape file :composed-1-composed-2-copy) :propagate-fn propagate-all-component-changes))) (fill-colors [file] [(tho/bottom-fill-color file :frame-simple-1) diff --git a/common/test/common_tests/logic/swap_as_override_test.cljc b/common/test/common_tests/logic/swap_as_override_test.cljc index a4a1b5a632..519b24e48f 100644 --- a/common/test/common_tests/logic/swap_as_override_test.cljc +++ b/common/test/common_tests/logic/swap_as_override_test.cljc @@ -6,20 +6,12 @@ (ns common-tests.logic.swap-as-override-test (:require - [app.common.files.changes :as ch] - [app.common.files.changes-builder :as pcb] - [app.common.logic.libraries :as cll] - [app.common.logic.shapes :as cls] - [app.common.pprint :as pp] + [app.common.data :as d] [app.common.test-helpers.components :as thc] [app.common.test-helpers.compositions :as tho] [app.common.test-helpers.files :as thf] [app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.shapes :as ths] - [app.common.types.component :as ctk] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [app.common.uuid :as uuid] [clojure.test :as t])) (t/use-fixtures :each thi/test-fixture) @@ -27,23 +19,40 @@ (defn- setup [] (-> (thf/sample-file :file1) - (tho/add-simple-component :component-1 :frame-component-1 :child-component-1 :child-params {:name "child-component-1" :type :rect :fills (ths/sample-fills-color :fill-color "#111111")}) - (tho/add-simple-component :component-2 :frame-component-2 :child-component-2 :child-params {:name "child-component-2" :type :rect :fills (ths/sample-fills-color :fill-color "#222222")}) - (tho/add-simple-component :component-3 :frame-component-3 :child-component-3 :child-params {:name "child-component-3" :type :rect :fills (ths/sample-fills-color :fill-color "#333333")}) + (tho/add-simple-component :component-1 :frame-component-1 :child-component-1 + :root-params {:name "component-1"} + :child-params {:name "child-component-1" + :type :rect + :fills (ths/sample-fills-color :fill-color "#111111")}) + (tho/add-simple-component :component-2 :frame-component-2 :child-component-2 + :root-params {:name "component-2"} + :child-params {:name "child-component-2" + :type :rect + :fills (ths/sample-fills-color :fill-color "#222222")}) + (tho/add-simple-component :component-3 :frame-component-3 :child-component-3 + :root-params {:name "component-3"} + :child-params {:name "child-component-3" + :type :rect + :fills (ths/sample-fills-color :fill-color "#333333")}) - (tho/add-frame :frame-icon-and-text) - (thc/instantiate-component :component-1 :copy-component-1 :parent-label :frame-icon-and-text :children-labels [:component-1-icon-and-text]) + (tho/add-frame :frame-icon-and-text :name "copy-component-1") + (thc/instantiate-component :component-1 :copy-component-1 + :parent-label :frame-icon-and-text + :children-labels [:component-1-icon-and-text]) (ths/add-sample-shape :text {:type :text :name "icon+text" :parent-label :frame-icon-and-text}) (thc/make-component :icon-and-text :frame-icon-and-text) - (tho/add-frame :frame-panel) - (thc/instantiate-component :icon-and-text :copy-icon-and-text :parent-label :frame-panel :children-labels [:icon-and-text-panel]) + (tho/add-frame :frame-panel :name "icon-and-text") + (thc/instantiate-component :icon-and-text :copy-icon-and-text + :parent-label :frame-panel + :children-labels [:icon-and-text-panel]) (thc/make-component :panel :frame-panel) - (thc/instantiate-component :panel :copy-panel :children-labels [:copy-icon-and-text-panel]))) + (thc/instantiate-component :panel :copy-panel + :children-labels [:copy-icon-and-text-panel]))) (defn- propagate-all-component-changes [file] (-> file diff --git a/common/test/common_tests/logic/swap_keeps_id_test.cljc b/common/test/common_tests/logic/swap_keeps_id_test.cljc index 6ecc3583b2..e6478beeb1 100644 --- a/common/test/common_tests/logic/swap_keeps_id_test.cljc +++ b/common/test/common_tests/logic/swap_keeps_id_test.cljc @@ -30,7 +30,7 @@ copy (ths/get-shape file :copy01) ;; ==== Action - file' (tho/swap-component file copy :circle {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :circle {:new-shape-label :copy02 :keep-touched? true}) copy' (ths/get-shape file' :copy02)] ;; Both copies have the same id diff --git a/common/test/common_tests/logic/variants_switch_test.cljc b/common/test/common_tests/logic/variants_switch_test.cljc index f01da5f268..c991f35ab6 100644 --- a/common/test/common_tests/logic/variants_switch_test.cljc +++ b/common/test/common_tests/logic/variants_switch_test.cljc @@ -35,7 +35,7 @@ copy01 (ths/get-shape file :copy01) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) copy01' (ths/get-shape file' :copy02)] (thf/dump-file file :keys [:width]) @@ -61,7 +61,7 @@ rect01 (get-in page [:objects (-> copy01 :shapes first)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -100,7 +100,7 @@ copy01 (ths/get-shape file :copy01) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) copy01' (ths/get-shape file' :copy02)] (thf/dump-file file :keys [:width]) @@ -137,7 +137,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -180,7 +180,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -257,25 +257,19 @@ ;; The copy clean has no overrides - - - copy-clean (ths/get-shape file :copy-clean) copy-clean-t (ths/get-shape file :copy-clean-t) ;; Override font size on copy-font-size file (update-attr file :copy-font-size-t font-size-path-0 "25") - copy-font-size (ths/get-shape file :copy-font-size) copy-font-size-t (ths/get-shape file :copy-font-size-t) ;; Override text on copy-text file (update-attr file :copy-text-t text-path-0 "text overriden") - copy-text (ths/get-shape file :copy-text) copy-text-t (ths/get-shape file :copy-text-t) ;; Override both on copy-both file (update-attr file :copy-both-t font-size-path-0 "25") file (update-attr file :copy-both-t text-path-0 "text overriden") - copy-both (ths/get-shape file :copy-both) copy-both-t (ths/get-shape file :copy-both-t) @@ -283,10 +277,10 @@ file' (-> file - (tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) - (tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) - (tho/swap-component copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) - (tho/swap-component copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) page' (thf/current-page file') copy-clean' (ths/get-shape file' :copy-clean-2) copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)]) @@ -387,25 +381,19 @@ ;; The copy clean has no overrides - - - copy-clean (ths/get-shape file :copy-clean) copy-clean-t (ths/get-shape file :copy-clean-t) ;; Override font size on copy-font-size file (update-attr file :copy-font-size-t font-size-path-0 "25") - copy-font-size (ths/get-shape file :copy-font-size) copy-font-size-t (ths/get-shape file :copy-font-size-t) ;; Override text on copy-text file (update-attr file :copy-text-t text-path-0 "text overriden") - copy-text (ths/get-shape file :copy-text) copy-text-t (ths/get-shape file :copy-text-t) ;; Override both on copy-both file (update-attr file :copy-both-t font-size-path-0 "25") file (update-attr file :copy-both-t text-path-0 "text overriden") - copy-both (ths/get-shape file :copy-both) copy-both-t (ths/get-shape file :copy-both-t) @@ -413,10 +401,10 @@ file' (-> file - (tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) - (tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) - (tho/swap-component copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) - (tho/swap-component copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) page' (thf/current-page file') copy-clean' (ths/get-shape file' :copy-clean-2) copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)]) @@ -515,25 +503,19 @@ ;; The copy clean has no overrides - - - copy-clean (ths/get-shape file :copy-clean) copy-clean-t (ths/get-shape file :copy-clean-t) ;; Override font size on copy-font-size file (update-attr file :copy-font-size-t font-size-path-0 "25") - copy-font-size (ths/get-shape file :copy-font-size) copy-font-size-t (ths/get-shape file :copy-font-size-t) ;; Override text on copy-text file (update-attr file :copy-text-t text-path-0 "text overriden") - copy-text (ths/get-shape file :copy-text) copy-text-t (ths/get-shape file :copy-text-t) ;; Override both on copy-both file (update-attr file :copy-both-t font-size-path-0 "25") file (update-attr file :copy-both-t text-path-0 "text overriden") - copy-both (ths/get-shape file :copy-both) copy-both-t (ths/get-shape file :copy-both-t) @@ -541,10 +523,10 @@ file' (-> file - (tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) - (tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) - (tho/swap-component copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) - (tho/swap-component copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) page' (thf/current-page file') copy-clean' (ths/get-shape file' :copy-clean-2) copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)]) @@ -645,25 +627,19 @@ ;; The copy clean has no overrides - - - copy-clean (ths/get-shape file :copy-clean) copy-clean-t (ths/get-shape file :copy-clean-t) ;; Override font size on copy-font-size file (update-attr file :copy-font-size-t font-size-path-0 "25") - copy-font-size (ths/get-shape file :copy-font-size) copy-font-size-t (ths/get-shape file :copy-font-size-t) ;; Override text on copy-text file (update-attr file :copy-text-t text-path-0 "text overriden") - copy-text (ths/get-shape file :copy-text) copy-text-t (ths/get-shape file :copy-text-t) ;; Override both on copy-both file (update-attr file :copy-both-t font-size-path-0 "25") file (update-attr file :copy-both-t text-path-0 "text overriden") - copy-both (ths/get-shape file :copy-both) copy-both-t (ths/get-shape file :copy-both-t) @@ -671,10 +647,10 @@ file' (-> file - (tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) - (tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) - (tho/swap-component copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) - (tho/swap-component copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true})) page' (thf/current-page file') copy-clean' (ths/get-shape file' :copy-clean-2) copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)]) @@ -774,14 +750,12 @@ file (change-structure file :copy-structure-clean-t) - copy-structure-clean (ths/get-shape file :copy-structure-clean) copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t) ;; Duplicate a text line in copy-structure-clean, updating ;; both lines with the same attrs file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25") (change-structure :copy-structure-unif-t)) - copy-structure-unif (ths/get-shape file :copy-structure-unif) copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t) ;; Duplicate a text line in copy-structure-clean, updating @@ -789,7 +763,6 @@ file (-> (change-structure file :copy-structure-mixed-t) (update-attr :copy-structure-mixed-t font-size-path-0 "35") (update-attr :copy-structure-mixed-t font-size-path-1 "40")) - copy-structure-mixed (ths/get-shape file :copy-structure-mixed) copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t) @@ -797,9 +770,9 @@ file' (-> file - (tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) - (tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) - (tho/swap-component copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) page' (thf/current-page file') copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2) copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)]) @@ -908,14 +881,12 @@ file (change-structure file :copy-structure-clean-t) - copy-structure-clean (ths/get-shape file :copy-structure-clean) copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t) ;; Duplicate a text line in copy-structure-clean, updating ;; both lines with the same attrs file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25") (change-structure :copy-structure-unif-t)) - copy-structure-unif (ths/get-shape file :copy-structure-unif) copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t) ;; Duplicate a text line in copy-structure-clean, updating @@ -923,7 +894,6 @@ file (-> (change-structure file :copy-structure-mixed-t) (update-attr :copy-structure-mixed-t font-size-path-0 "35") (update-attr :copy-structure-mixed-t font-size-path-1 "40")) - copy-structure-mixed (ths/get-shape file :copy-structure-mixed) copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t) @@ -931,9 +901,9 @@ file' (-> file - (tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) - (tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) - (tho/swap-component copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) page' (thf/current-page file') copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2) copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)]) @@ -1038,14 +1008,12 @@ file (change-structure file :copy-structure-clean-t) - copy-structure-clean (ths/get-shape file :copy-structure-clean) copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t) ;; Duplicate a text line in copy-structure-clean, updating ;; both lines with the same attrs file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25") (change-structure :copy-structure-unif-t)) - copy-structure-unif (ths/get-shape file :copy-structure-unif) copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t) ;; Duplicate a text line in copy-structure-clean, updating @@ -1053,7 +1021,6 @@ file (-> (change-structure file :copy-structure-mixed-t) (update-attr :copy-structure-mixed-t font-size-path-0 "35") (update-attr :copy-structure-mixed-t font-size-path-1 "40")) - copy-structure-mixed (ths/get-shape file :copy-structure-mixed) copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t) @@ -1061,9 +1028,9 @@ file' (-> file - (tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) - (tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) - (tho/swap-component copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) page' (thf/current-page file') copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2) copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)]) @@ -1169,14 +1136,12 @@ file (change-structure file :copy-structure-clean-t) - copy-structure-clean (ths/get-shape file :copy-structure-clean) copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t) ;; Duplicate a text line in copy-structure-clean, updating ;; both lines with the same attrs file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25") (change-structure :copy-structure-unif-t)) - copy-structure-unif (ths/get-shape file :copy-structure-unif) copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t) ;; Duplicate a text line in copy-structure-clean, updating @@ -1184,7 +1149,6 @@ file (-> (change-structure file :copy-structure-mixed-t) (update-attr :copy-structure-mixed-t font-size-path-0 "35") (update-attr :copy-structure-mixed-t font-size-path-1 "40")) - copy-structure-mixed (ths/get-shape file :copy-structure-mixed) copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t) @@ -1192,9 +1156,9 @@ file' (-> file - (tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) - (tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) - (tho/swap-component copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) + (tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true}) + (tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true})) page' (thf/current-page file') copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2) copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)]) @@ -1290,7 +1254,6 @@ :children-labels [:copy-cp01])) page (thf/current-page file) - copy01 (ths/get-shape file :copy01) copy-cp01 (ths/get-shape file :copy-cp01) copy-cp01-rect-id (-> copy-cp01 :shapes first) @@ -1309,7 +1272,7 @@ ;; ==== Action ;; Switch :c01 for :c02 - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) copy02 (ths/get-shape file' :copy02) copy-cp02' (ths/get-shape-by-id file' (-> copy02 :shapes first)) copy-cp02-rect' (ths/get-shape-by-id file' (-> copy-cp02' :shapes first))] @@ -1337,17 +1300,16 @@ :children-labels [:copy-cp01])) copy01 (ths/get-shape file :copy01) - copy-cp01 (ths/get-shape file :copy-cp01) external02 (thc/get-component file :external02) ;; On :c01, swap the copy of :external01 for a copy of :external02 file (-> file - (tho/swap-component copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false})) + (tho/swap-component-in-shape :copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false})) copy-cp02 (ths/get-shape file :copy-cp02) ;; ==== Action ;; Switch :c01 for :c02 - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) copy02' (ths/get-shape file' :copy02) copy-cp02' (ths/get-shape file' :copy-cp02)] @@ -1376,12 +1338,11 @@ page (thf/current-page file) copy01 (ths/get-shape file :copy01) - copy-cp01 (ths/get-shape file :copy-cp01) external02 (thc/get-component file :external02) ;; On :c01, swap the copy of :external01 for a copy of :external02 file (-> file - (tho/swap-component copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false})) + (tho/swap-component-in-shape :copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false})) copy-cp02 (ths/get-shape file :copy-cp02) copy-cp02-rect-id (-> copy-cp02 :shapes first) @@ -1396,7 +1357,7 @@ ;; ==== Action ;; Switch :c01 for :c02 - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) copy02' (ths/get-shape file' :copy02) copy-cp02' (ths/get-shape file' :copy-cp02) @@ -1463,7 +1424,7 @@ ;; ==== Action - file' (tho/swap-component file c01-in-copy :c02 {:new-shape-label :c02-in-copy :keep-touched? true}) + file' (tho/swap-component-in-shape file :c01-in-copy :c02 {:new-shape-label :c02-in-copy :keep-touched? true}) page' (thf/current-page file') c02-in-copy' (ths/get-shape file' :c02-in-copy) @@ -1515,7 +1476,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1564,7 +1525,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1613,7 +1574,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1660,7 +1621,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1714,7 +1675,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1763,7 +1724,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1812,7 +1773,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1859,7 +1820,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1910,7 +1871,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -1956,7 +1917,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2023,7 +1984,7 @@ text01 (get-in page [:objects (:id text01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2055,7 +2016,7 @@ rect01 (get-in page [:objects (-> copy01 :shapes first)]) ;; ==== Action - Try to switch to a component with different shape type - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2098,7 +2059,7 @@ path01 (get-in page [:objects (:id path01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2146,7 +2107,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2190,7 +2151,7 @@ rect01 (get-in page [:objects (-> copy01 :shapes first)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2243,7 +2204,7 @@ old-position-data (:position-data text01) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2306,7 +2267,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2357,7 +2318,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2411,7 +2372,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2468,7 +2429,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2532,7 +2493,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2588,7 +2549,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2653,7 +2614,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) @@ -2710,7 +2671,7 @@ rect01 (get-in page [:objects (:id rect01)]) ;; ==== Action - file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) page' (thf/current-page file') copy02' (ths/get-shape file' :copy02) diff --git a/common/test/common_tests/types/components_test.cljc b/common/test/common_tests/types/components_test.cljc index 36394f29a2..684d45db99 100644 --- a/common/test/common_tests/types/components_test.cljc +++ b/common/test/common_tests/types/components_test.cljc @@ -6,9 +6,13 @@ (ns common-tests.types.components-test (:require + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] [app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.shapes :as ths] [app.common.types.component :as ctk] + [app.common.types.file :as ctf] [clojure.test :as t])) (t/use-fixtures :each thi/test-fixture) @@ -39,3 +43,357 @@ (t/is (= (ctk/get-swap-slot s4) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f")) (t/is (= (ctk/get-swap-slot s5) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f")) (t/is (nil? (ctk/get-swap-slot s6))))) + +(t/deftest test-find-near-match + + (t/testing "shapes not in a component have no near match" + (let [file + ;; :frame1 [:name Frame1] + ;; :child1 [:name Rect1] + (-> (thf/sample-file :file1) + (tho/add-frame-with-child :frame1 :shape1)) + + page (thf/current-page file) + + frame1 (ths/get-shape file :frame1) + shape1 (ths/get-shape file :shape1) + + near-match1 (ctf/find-near-match file page {} frame1) + near-match2 (ctf/find-near-match file page {} shape1)] + + (t/is (nil? near-match1)) + (t/is (nil? near-match2)))) + + (t/testing "shapes in a copy get the ref-shape" + (let [file + ;; {:main-root} [:name Frame1] # [Component :component1] + ;; :main-child1 [:name Rect1] + ;; :main-child2 [:name Rect2] + ;; :main-child3 [:name Rect3] + ;; + ;; :copy-root [:name Frame1] #--> [Component :component1] :main-root + ;; [:name Rect1] ---> :main-child1 + ;; [:name Rect2] ---> :main-child2 + ;; [:name Rect3] ---> :main-child3 + (-> (thf/sample-file :file1) + (tho/add-component-with-many-children-and-copy :component1 + :main-root [:main-child1 :main-child2 :main-child3] + :copy-root)) + + page (thf/current-page file) + + main-root (ths/get-shape file :main-root) + main-child1 (ths/get-shape file :main-child1) + main-child2 (ths/get-shape file :main-child2) + main-child3 (ths/get-shape file :main-child3) + copy-root (ths/get-shape file :copy-root) + copy-child1 (ths/get-shape-by-id file (nth (:shapes copy-root) 0)) + copy-child2 (ths/get-shape-by-id file (nth (:shapes copy-root) 1)) + copy-child3 (ths/get-shape-by-id file (nth (:shapes copy-root) 2)) + + near-main-root (ctf/find-near-match file page {} main-root) + near-main-child1 (ctf/find-near-match file page {} main-child1) + near-main-child2 (ctf/find-near-match file page {} main-child2) + near-main-child3 (ctf/find-near-match file page {} main-child3) + near-copy-root (ctf/find-near-match file page {} copy-root) + near-copy-child1 (ctf/find-near-match file page {} copy-child1) + near-copy-child2 (ctf/find-near-match file page {} copy-child2) + near-copy-child3 (ctf/find-near-match file page {} copy-child3)] + + (t/is (nil? near-main-root)) + (t/is (nil? near-main-child1)) + (t/is (nil? near-main-child2)) + (t/is (nil? near-main-child3)) + (t/is (nil? near-copy-root)) + (t/is (= (:id near-copy-child1) (thi/id :main-child1))) + (t/is (= (:id near-copy-child2) (thi/id :main-child2))) + (t/is (= (:id near-copy-child3) (thi/id :main-child3))))) + + (t/testing "shapes in nested not swapped copies get the ref-shape" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; :nested-child [:name Rect1] ---> :main1-child + ;; + ;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame1] @--> [Component :component1] :nested-head + ;; :copy2-nested-child [:name Rect1] ---> :nested-child + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head + :nested-head-params {:children-labels [:nested-child]}) + (thc/instantiate-component :component2 :copy2 + :children-labels [:copy2-nested-head :copy2-nested-child])) + + page (thf/current-page file) + + main1-root (ths/get-shape file :main1-root) + main1-child (ths/get-shape file :main1-child) + main2-root (ths/get-shape file :main2-root) + nested-head (ths/get-shape file :nested-head) + nested-child (ths/get-shape file :nested-child) + copy2 (ths/get-shape file :copy2) + copy2-nested-head (ths/get-shape file :copy2-nested-head) + copy2-nested-child (ths/get-shape file :copy2-nested-child) + + near-main1-root (ctf/find-near-match file page {} main1-root) + near-main1-child (ctf/find-near-match file page {} main1-child) + near-main2-root (ctf/find-near-match file page {} main2-root) + near-nested-head (ctf/find-near-match file page {} nested-head) + near-nested-child (ctf/find-near-match file page {} nested-child) + near-copy2 (ctf/find-near-match file page {} copy2) + near-copy2-nested-head (ctf/find-near-match file page {} copy2-nested-head) + near-copy2-nested-child (ctf/find-near-match file page {} copy2-nested-child)] + + (t/is (nil? near-main1-root)) + (t/is (nil? near-main1-child)) + (t/is (nil? near-main2-root)) + (t/is (nil? near-nested-head)) + (t/is (= (:id near-nested-child) (thi/id :main1-child))) + (t/is (nil? near-copy2)) + (t/is (= (:id near-copy2-nested-head) (thi/id :nested-head))) + (t/is (= (:id near-copy2-nested-child) (thi/id :nested-child))))) + + (t/testing "shapes in swapped copies get the swap slot" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested-head [:name Frame1] @--> [Component :component1] :main1-root + ;; :nested-child [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame3] @--> [Component :component3] :main3-root + ;; {swap-slot :nested-head} + ;; [:name Rect3] ---> :main3-child + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested-head + :nested-head-params {:children-labels [:nested-child]}) + (thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head]) + (tho/add-simple-component :component3 :main3-root :main3-child + :root-params {:name "Frame3"} + :child-params {:name "Rect3"}) + (tho/swap-component-in-first-child :copy2 :component3)) + + page (thf/current-page file) + + main1-root (ths/get-shape file :main1-root) + main1-child (ths/get-shape file :main1-child) + main2-root (ths/get-shape file :main2-root) + nested-head (ths/get-shape file :nested-head) + nested-child (ths/get-shape file :nested-child) + copy2 (ths/get-shape file :copy2) + copy2-nested-head (ths/get-shape file :copy2-nested-head) + copy2-nested-child (ths/get-shape-by-id file (first (:shapes copy2-nested-head))) + + near-main1-root (ctf/find-near-match file page {} main1-root) + near-main1-child (ctf/find-near-match file page {} main1-child) + near-main2-root (ctf/find-near-match file page {} main2-root) + near-nested-head (ctf/find-near-match file page {} nested-head) + near-nested-child (ctf/find-near-match file page {} nested-child) + near-copy2 (ctf/find-near-match file page {} copy2) + near-copy2-nested-head (ctf/find-near-match file page {} copy2-nested-head) + near-copy2-nested-child (ctf/find-near-match file page {} copy2-nested-child)] + + (t/is (nil? near-main1-root)) + (t/is (nil? near-main1-child)) + (t/is (nil? near-main2-root)) + (t/is (nil? near-nested-head)) + (t/is (= (:id near-nested-child) (thi/id :main1-child))) + (t/is (nil? near-copy2)) + (t/is (= (:id near-copy2-nested-head) (thi/id :nested-head))) + (t/is (= (:id near-copy2-nested-child) (thi/id :main3-child))))) + + (t/testing "shapes in second level nested copies under swapped get the shape in the new main" + (let [file + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested2-head [:name Frame1] @--> [Component :component1] :main1-root + ;; :nested2-child [:name Rect1] ---> :main1-child + ;; + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main4-root} [:name Frame4] # [Component :component4] + ;; :nested4-head [:name Frame3] @--> [Component :component1] :main3-root + ;; :nested4-child [:name Rect3] ---> :main3-child + ;; + ;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame4] @--> [Component :component4] :main4-root + ;; {swap-slot :nested2-head} + ;; [:name Frame3] @--> :nested4-head + ;; [:name Rect3] ---> :nested4-child + (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested2-head + :nested-head-params {:children-labels [:nested2-child]}) + (thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head]) + (tho/add-nested-component :component3 :main3-root :main3-child + :component4 :main4-root :nested4-head + :root1-params {:name "Frame3"} + :main1-child-params {:name "Rect3"} + :main2-root-params {:name "Frame4"} + :nested-head-params {:children-labels [:nested4-child]}) + (tho/swap-component-in-first-child :copy2 :component4)) + + page (thf/current-page file) + + main1-root (ths/get-shape file :main1-root) + main1-child (ths/get-shape file :main1-child) + main2-root (ths/get-shape file :main2-root) + nested2-head (ths/get-shape file :nested2-head) + nested2-child (ths/get-shape file :nested2-child) + main3-root (ths/get-shape file :main3-root) + main3-child (ths/get-shape file :main3-child) + main4-root (ths/get-shape file :main4-root) + nested4-head (ths/get-shape file :nested4-head) + nested4-child (ths/get-shape file :nested4-child) + copy2 (ths/get-shape file :copy2) + copy2-nested-head (ths/get-shape file :copy2-nested-head) + copy2-nested4-head (ths/get-shape-by-id file (first (:shapes copy2-nested-head))) + copy2-nested4-child (ths/get-shape-by-id file (first (:shapes copy2-nested4-head))) + + near-main1-root (ctf/find-near-match file page {} main1-root) + near-main1-child (ctf/find-near-match file page {} main1-child) + near-main2-root (ctf/find-near-match file page {} main2-root) + near-nested2-head (ctf/find-near-match file page {} nested2-head) + near-nested2-child (ctf/find-near-match file page {} nested2-child) + near-main3-root (ctf/find-near-match file page {} main3-root) + near-main3-child (ctf/find-near-match file page {} main3-child) + near-main4-root (ctf/find-near-match file page {} main4-root) + near-nested4-head (ctf/find-near-match file page {} nested4-head) + near-nested4-child (ctf/find-near-match file page {} nested4-child) + near-copy2 (ctf/find-near-match file page {} copy2) + near-copy2-nested-head (ctf/find-near-match file page {} copy2-nested-head) + near-copy2-nested4-head (ctf/find-near-match file page {} copy2-nested4-head) + near-copy2-nested4-child (ctf/find-near-match file page {} copy2-nested4-child)] + + (t/is (nil? near-main1-root)) + (t/is (nil? near-main1-child)) + (t/is (nil? near-main2-root)) + (t/is (nil? near-nested2-head)) + (t/is (= (:id near-nested2-child) (thi/id :main1-child))) + (t/is (nil? near-main3-root)) + (t/is (nil? near-main3-child)) + (t/is (nil? near-main4-root)) + (t/is (nil? near-nested4-head)) + (t/is (= (:id near-nested4-child) (thi/id :main3-child))) + (t/is (nil? near-copy2)) + (t/is (= (:id near-copy2-nested-head) (thi/id :nested2-head))) + (t/is (= (:id near-copy2-nested4-head) (thi/id :nested4-head))) + (t/is (= (:id near-copy2-nested4-child) (thi/id :nested4-child))))) + + (t/testing "component in external libraries still work well" + (let [library1 + ;; {:main1-root} [:name Frame1] # [Component :component1] + ;; :main1-child [:name Rect1] + ;; + ;; {:main2-root} [:name Frame2] # [Component :component2] + ;; :nested2-head [:name Frame1] @--> [Component :component1] :main1-root + ;; :nested2-child [:name Rect1] ---> :main1-child + (-> (thf/sample-file :library1) + (tho/add-nested-component :component1 :main1-root :main1-child + :component2 :main2-root :nested2-head + :nested-head-params {:children-labels [:nested2-child]})) + library2 + ;; {:main3-root} [:name Frame3] # [Component :component3] + ;; :main3-child [:name Rect3] + ;; + ;; {:main4-root} [:name Frame4] # [Component :component4] + ;; :nested4-head [:name Frame3] @--> [Component :component1] :main3-root + ;; :nested4-child [:name Rect3] ---> :main3-child + (-> (thf/sample-file :library2) + (tho/add-nested-component :component3 :main3-root :main3-child + :component4 :main4-root :nested4-head + :root1-params {:name "Frame3"} + :main1-child-params {:name "Rect3"} + :main2-root-params {:name "Frame4"} + :nested-head-params {:children-labels [:nested4-child]})) + + file + ;; :copy2 [:name Frame2] #--> [Component :component2] :main2-root + ;; :copy2-nested-head [:name Frame4] @--> [Component :component4] :main4-root + ;; {swap-slot :nested2-head} + ;; [:name Frame3] @--> :nested4-head + ;; [:name Rect3] ---> :nested4-child + (-> (thf/sample-file :file1) + (thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head] + :library library1) + (tho/swap-component-in-first-child :copy2 :component4 :library library2)) + + page-library1 (thf/current-page library1) + page-library2 (thf/current-page library2) + page-file (thf/current-page file) + libraries {(:id library1) library1 + (:id library2) library2} + + main1-root (ths/get-shape library1 :main1-root) + main1-child (ths/get-shape library1 :main1-child) + main2-root (ths/get-shape library1 :main2-root) + nested2-head (ths/get-shape library1 :nested2-head) + nested2-child (ths/get-shape library1 :nested2-child) + main3-root (ths/get-shape library2 :main3-root) + main3-child (ths/get-shape library2 :main3-child) + main4-root (ths/get-shape library2 :main4-root) + nested4-head (ths/get-shape library2 :nested4-head) + nested4-child (ths/get-shape library2 :nested4-child) + copy2 (ths/get-shape file :copy2) + copy2-nested-head (ths/get-shape file :copy2-nested-head) + copy2-nested4-head (ths/get-shape-by-id file (first (:shapes copy2-nested-head))) + copy2-nested4-child (ths/get-shape-by-id file (first (:shapes copy2-nested4-head))) + + near-main1-root (ctf/find-near-match file page-file libraries main1-root) + near-main1-child (ctf/find-near-match file page-file libraries main1-child) + near-main2-root (ctf/find-near-match file page-file libraries main2-root) + near-nested2-head (ctf/find-near-match library1 page-library1 libraries nested2-head) + near-nested2-child (ctf/find-near-match library1 page-library1 libraries nested2-child) + near-main3-root (ctf/find-near-match file page-file libraries main3-root) + near-main3-child (ctf/find-near-match file page-file libraries main3-child) + near-main4-root (ctf/find-near-match file page-file libraries main4-root) + near-nested4-head (ctf/find-near-match library2 page-library2 libraries nested4-head) + near-nested4-child (ctf/find-near-match library2 page-library2 libraries nested4-child) + near-copy2 (ctf/find-near-match file page-file libraries copy2) + near-copy2-nested-head (ctf/find-near-match file page-file libraries copy2-nested-head) + near-copy2-nested4-head (ctf/find-near-match file page-file libraries copy2-nested4-head) + near-copy2-nested4-child (ctf/find-near-match file page-file libraries copy2-nested4-child)] + + (thf/dump-file library1 :keys [:name :swap-slot-label] :show-refs? true) + (t/is (some? main1-root)) + (t/is (some? main1-child)) + (t/is (some? main2-root)) + (t/is (some? nested2-head)) + (t/is (some? nested2-child)) + (t/is (some? main3-root)) + (t/is (some? main3-child)) + (t/is (some? main4-root)) + (t/is (some? nested4-head)) + (t/is (some? nested4-child)) + (t/is (some? copy2)) + (t/is (some? copy2-nested-head)) + (t/is (some? copy2-nested4-head)) + (t/is (some? copy2-nested4-child)) + + (t/is (nil? near-main1-root)) + (t/is (nil? near-main1-child)) + (t/is (nil? near-main2-root)) + (t/is (nil? near-nested2-head)) + (t/is (= (:id near-nested2-child) (thi/id :main1-child))) + (t/is (nil? near-main3-root)) + (t/is (nil? near-main3-child)) + (t/is (nil? near-main4-root)) + (t/is (nil? near-nested4-head)) + (t/is (= (:id near-nested4-child) (thi/id :main3-child))) + (t/is (nil? near-copy2)) + (t/is (= (:id near-copy2-nested-head) (thi/id :nested2-head))) + (t/is (= (:id near-copy2-nested4-head) (thi/id :nested4-head))) + (t/is (= (:id near-copy2-nested4-child) (thi/id :nested4-child)))))) diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs index 042f943e2c..f529705d9f 100644 --- a/frontend/src/app/main/data/changes.cljs +++ b/frontend/src/app/main/data/changes.cljs @@ -23,10 +23,11 @@ [potok.v2.core :as ptk])) ;; Change this to :info :debug or :trace to debug this module -(log/set-level! :info) +(log/set-level! :warn) (def page-change? #{:add-page :mod-page :del-page :mov-page}) + (def update-layout-attr? #{:hidden}) @@ -207,6 +208,7 @@ ;; Prevent commit changes by a viewer team member (it really should never happen) (when (:can-edit permissions) + (log/trace :hint "commit-changes" :redo-changes redo-changes) (let [selected (dm/get-in state [:workspace-local :selected])] (rx/of (-> params (assoc :undo-group undo-group) From de577a803cb40bf8ace0ac794613aa27e1903f5b Mon Sep 17 00:00:00 2001 From: Juanfran Date: Tue, 14 Apr 2026 12:12:37 +0200 Subject: [PATCH 142/288] :tada: Add get-org-member-team-counts endpoint to Nitrate API --- backend/src/app/rpc/management/nitrate.clj | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 338a59f28b..fc4459e865 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -296,6 +296,48 @@ RETURNING id, name;") (profile-to-map profile))) +;; ---- API: get-org-member-team-counts + +(def ^:private sql:get-org-member-team-counts + "SELECT tpr.profile_id, COUNT(DISTINCT t.id) AS team_count + FROM team_profile_rel AS tpr + JOIN team AS t ON t.id = tpr.team_id + WHERE t.id = ANY(?) + AND t.deleted_at IS NULL + AND t.is_default IS FALSE + GROUP BY tpr.profile_id;") + +(def ^:private schema:get-org-member-team-counts-params + [:map [:team-ids [:or ::sm/uuid [:vector ::sm/uuid]]]]) + +(def ^:private schema:get-org-member-team-counts-result + [:vector [:map + [:profile-id ::sm/uuid] + [:team-count ::sm/int]]]) + +(sv/defmethod ::get-org-member-team-counts + "Get the number of non-default teams each profile belongs to within a set of teams." + {::doc/added "2.15" + ::sm/params schema:get-org-member-team-counts-params + ::sm/result schema:get-org-member-team-counts-result + ::rpc/auth false} + [cfg {:keys [team-ids]}] + (let [team-ids (cond + (uuid? team-ids) + [team-ids] + + (and (vector? team-ids) (every? uuid? team-ids)) + team-ids + + :else + [])] + (if (empty? team-ids) + [] + (db/run! cfg (fn [{:keys [::db/conn]}] + (let [ids-array (db/create-array conn "uuid" team-ids)] + (db/exec! conn [sql:get-org-member-team-counts ids-array]))))))) + + ;; API: invite-to-org (sv/defmethod ::invite-to-org From c63b9583a2985e004aebef50dee4f4b3592b232d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Tue, 14 Apr 2026 11:45:55 +0200 Subject: [PATCH 143/288] :sparkles: Add callback to nitrate billing url --- frontend/src/app/main/data/nitrate.cljs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index d4f4ed533c..6c53126cec 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -65,9 +65,12 @@ [] (st/emit! (rt/nav-raw :href "/control-center/?action=create-org"))) +(def go-to-subscription-url (u/join cf/public-uri "#/settings/subscriptions")) + (defn go-to-nitrate-billing [] - (st/emit! (rt/nav-raw :href "/control-center/licenses/billing"))) + (let [href (dm/str "/control-center/licenses/billing?callback=" (js/encodeURIComponent go-to-subscription-url))] + (st/emit! (rt/nav-raw :href href)))) (defn go-to-buy-nitrate-license ([subscription] @@ -78,8 +81,6 @@ href (dm/str "/control-center/licenses/start?" (u/map->query-string params))] (st/emit! (rt/nav-raw :href href))))) -(def go-to-subscription-url (u/join cf/public-uri "#/settings/subscriptions")) - (defn is-valid-license? [profile] (and (contains? cf/flags :nitrate) From b0caa15516c2452bed120dcf1149b62feb0e3ed1 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 15 Apr 2026 09:16:16 +0200 Subject: [PATCH 144/288] :tada: Add test to bug (#8928) --- .../playwright/ui/specs/tokens/apply.spec.js | 48 +++++++++++++++++++ .../ui/specs/tokens/remapping.spec.js | 2 +- .../options/menus/color_selection.cljs | 3 +- .../workspace/sidebar/options/menus/fill.cljs | 2 +- frontend/translations/en.po | 8 ++++ frontend/translations/es.po | 8 ++++ 6 files changed, 68 insertions(+), 3 deletions(-) diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js index a866fefe6a..fccad0cfe9 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -981,3 +981,51 @@ test("Bug: 13960, User select shapes with different opacity and input show mixed await expect(layerMenuSection .getByRole('textbox', { name: 'Opacity' })).toHaveAttribute("placeholder", "Mixed"); }); + +test("BUG: 13930, Token colors are shown on selected colors section", async ({ + page, +}) => { + const { workspacePage, tokensSidebar, tokenContextMenuForToken } = + await setupTokensFileRender(page); + + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers + .getByTestId("layer-row") + .filter({ hasText: "Button" }) + .click(); + + await page.getByRole("tab", { name: "Tokens" }).click(); + + await unfoldTokenType(tokensSidebar, "color"); + + await tokensSidebar + .getByRole("button", { name: "black" }) + .click({ button: "right" }); + await tokenContextMenuForToken.getByText("Fill").click(); + + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers + .getByTestId("layer-row") + .filter({ hasText: "Rectangle" }) + .first() + .click({ modifiers: ["Shift"] }); + + await expect( + workspacePage.page.getByRole("region", { name: "Color selection section" }), + ).toBeVisible(); + + await workspacePage.page + .getByRole("button", { name: "Resolved value: #7f9cf5" }) + .click(); + await expect( + workspacePage.page.getByRole("region", { name: "Color selection section" }), + ).toBeVisible(); + + await expect( + workspacePage.page + .getByTestId("colorpicker") + .getByRole("button", { name: "colors.black" }), + ).toBeVisible(); +}); diff --git a/frontend/playwright/ui/specs/tokens/remapping.spec.js b/frontend/playwright/ui/specs/tokens/remapping.spec.js index 44163bfdfa..55472cb4a1 100644 --- a/frontend/playwright/ui/specs/tokens/remapping.spec.js +++ b/frontend/playwright/ui/specs/tokens/remapping.spec.js @@ -687,7 +687,7 @@ test.describe("Remapping group of tokens", () => { await expect(lighterNode).toBeVisible(); // Verify that the applied token reference has been updated in the right sidebar for the selected shape - const fillSection = rightSidebar.getByTestId("fill-section"); + const fillSection = rightSidebar.getByRole("region", { name: "Fill section" }); await expect(fillSection).toBeVisible(); const tokenReference = fillSection.getByLabel("lighter.primary", { diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs index 31dd4ad3a5..cbc9e6c31a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs @@ -192,7 +192,8 @@ (conj prev-colors color)) (st/emit! (dwta/apply-token-on-color-selected color-operations token)))))] - [:div {:class (stl/css :element-set)} + [:section {:class (stl/css :element-set) + :aria-label (tr "workspace.options.selection-color.section")} [:div {:class (stl/css :element-title)} [:> title-bar* {:collapsable has-colors? :collapsed (not open?) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 107150a16e..529eee6b36 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -194,7 +194,7 @@ (dom/set-attribute! checkbox "indeterminate" true) (dom/remove-attribute! checkbox "indeterminate")))) - [:div {:class (stl/css :fill-section) :data-testid "fill-section"} + [:section {:class (stl/css :fill-section) :aria-label (tr "workspace.options.fill.section")} [:div {:class (stl/css :fill-title)} [:> title-bar* {:collapsable has-fills? :collapsed (not open?) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 65c38e5ccd..dfabd64c24 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -6468,6 +6468,10 @@ msgstr "Export unexpectedly slow" msgid "workspace.options.fill" msgstr "Fill" +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.fill.section" +msgstr "Fill section" + #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:208 msgid "workspace.options.fill.add-fill" msgstr "Add fill" @@ -7083,6 +7087,10 @@ msgstr "More library colors" msgid "workspace.options.more-token-colors" msgstr "More color tokens" +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.selection-color.section" +msgstr "Color selection section" + #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:229, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:241 msgid "workspace.options.opacity" msgstr "Opacity" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 59b7eb0bd5..922b552159 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -6363,6 +6363,10 @@ msgstr "Exportación lenta" msgid "workspace.options.fill" msgstr "Relleno" +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.fill.section" +msgstr "Sección de relleno" + #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs:208 msgid "workspace.options.fill.add-fill" msgstr "Añadir relleno" @@ -6978,6 +6982,10 @@ msgstr "Más colores de la biblioteca" msgid "workspace.options.more-token-colors" msgstr "Más tokens de color" +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +msgid "workspace.options.selection-color.section" +msgstr "Sección de colores seleccionados" + #: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:229, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs:241 msgid "workspace.options.opacity" msgstr "Opacidad" From 5dec75fe6234eecf613cd7d10163567df9b246ca Mon Sep 17 00:00:00 2001 From: Clayton <118192227+claytonlin1110@users.noreply.github.com> Date: Wed, 15 Apr 2026 03:38:53 -0500 Subject: [PATCH 145/288] :books: Clarify manifest version 2 for relative plugin asset paths (#8992) Signed-off-by: Clayton --- docs/plugins/create-a-plugin.md | 12 ++++++++++-- docs/plugins/getting-started.md | 7 +++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index 42fc096dbe..9a25d101b9 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -219,8 +219,9 @@ Now that everything is in place you need a manifest.js { "name": "Plugin name", "description": "Plugin description", - "code": "/plugin.js", - "icon": "/icon.png", + "version": 2, + "code": "plugin.js", + "icon": "icon.png", "permissions": [ "content:read", "content:write", @@ -234,6 +235,13 @@ Now that everything is in place you need a manifest.js } ``` +

+Use "version": 2 when your +code and icon values +are relative paths. Version 2 resolves these assets from the manifest location. +If omitted, Penpot treats the manifest as version 1. +

+ ### Icon The plugin icon must be an image file. All image formats are valid, so you can use whichever format works best for your needs. Although there is no specific size requirement, it is recommended that the icon be 56x56 pixels in order to ensure its optimal appearance across all devices. diff --git a/docs/plugins/getting-started.md b/docs/plugins/getting-started.md index abfbc508b1..ea993640a9 100644 --- a/docs/plugins/getting-started.md +++ b/docs/plugins/getting-started.md @@ -131,6 +131,7 @@ The manifest.json file contains the basic infor { "name": "Your plugin name", "description": "Your plugin description", + "version": 2, "code": "plugin.js", "icon": "Your icon", "permissions": [ @@ -147,6 +148,12 @@ The manifest.json file contains the basic infor } ``` +

+Set "version": 2 in your +manifest.json if you use relative paths for +code or icon. +

+ #### Properties - **Name and description**: your plugin's basic information, which will be displayed in the plugin manager modal. From 431056404c12d0de7c4ac03ebb674d157dd08424 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Wed, 15 Apr 2026 11:24:01 +0200 Subject: [PATCH 146/288] :tada: Save tokens tree state in local storage (#8922) --- .../data/workspace/tokens/library_edit.cljs | 95 ++++++++++++++++--- .../ui/workspace/tokens/management/group.cljs | 13 ++- .../app/main/ui/workspace/tokens/sets.cljs | 1 - 3 files changed, 93 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs index 947e8ad456..4de3430986 100644 --- a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs +++ b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs @@ -23,6 +23,7 @@ [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.tokens.propagation :as dwtp] [app.util.i18n :refer [tr]] + [app.util.storage :as storage] [beicon.v2.core :as rx] [cuerdas.core :as str] [potok.v2.core :as ptk])) @@ -67,6 +68,41 @@ ;; TOKENS TREE - Type folders ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Helper functions for localStorage persistence +(defn- get-unfolded-token-types-from-storage + [file-id set-id] + (get-in storage/user [:app.main.ui.workspace.tokens/unfolded-token-types file-id set-id] #{})) + +(defn- save-unfolded-token-types-in-storage + [file-id set-id types] + (swap! storage/user update :app.main.ui.workspace.tokens/unfolded-token-types + assoc-in [file-id set-id] (vec types))) + +;; Helper functions for app state persistence +(defn- make-unfolded-token-types-state + [file-id set-id types] + {:file-id file-id + :set-id set-id + :types (set (or types #{}))}) + +(defn- get-unfolded-token-types-from-state + [state] + (let [value (get-in state [:workspace-tokens :unfolded-token-types])] + (or (:types value) #{}))) + +(defn restore-unfolded-token-types + "Loads unfolded token types from localStorage for the current file and set" + [] + (ptk/reify ::restore-unfolded-token-types + ptk/UpdateEvent + (update [_ state] + (let [file-id (:current-file-id state) + set-id (get-in state [:workspace-tokens :selected-token-set-id]) + stored (get-unfolded-token-types-from-storage file-id set-id)] + (assoc-in state + [:workspace-tokens :unfolded-token-types] + (make-unfolded-token-types-state file-id set-id stored)))))) + (defn open-token-type ([types type] (conj (or types #{}) type)) @@ -74,8 +110,16 @@ (ptk/reify ::open-token-type ptk/UpdateEvent (update [_ state] - (update-in state [:workspace-tokens :unfolded-token-types] - #(open-token-type % type)))))) + (let [file-id (:current-file-id state) + set-id (get-in state [:workspace-tokens :selected-token-set-id]) + types (get-unfolded-token-types-from-state state) + new-types (open-token-type types type) + new-state (assoc-in state + [:workspace-tokens :unfolded-token-types] + (make-unfolded-token-types-state file-id set-id new-types))] + (save-unfolded-token-types-in-storage file-id set-id + new-types) + new-state))))) (defn close-token-type ([types type] @@ -84,27 +128,47 @@ (ptk/reify ::close-token-type ptk/UpdateEvent (update [_ state] - (update-in state [:workspace-tokens :unfolded-token-types] - #(close-token-type % type)))))) + (let [file-id (:current-file-id state) + set-id (get-in state [:workspace-tokens :selected-token-set-id]) + types (get-unfolded-token-types-from-state state) + new-types (close-token-type types type) + new-state (assoc-in state + [:workspace-tokens :unfolded-token-types] + (make-unfolded-token-types-state file-id set-id new-types))] + (save-unfolded-token-types-in-storage file-id set-id + new-types) + new-state))))) -(defn toggle-token-type +(defn + toggle-token-type [type] (ptk/reify ::toggle-token-type ptk/UpdateEvent (update [_ state] - (update-in state [:workspace-tokens :unfolded-token-types] - (fn [types] - (if (contains? (or types #{}) type) - (close-token-type types type) - (open-token-type types type))))))) + (let [file-id (:current-file-id state) + set-id (get-in state [:workspace-tokens :selected-token-set-id]) + types (get-unfolded-token-types-from-state state) + new-types (if (contains? types type) + (close-token-type types type) + (open-token-type types type)) + new-state (assoc-in state + [:workspace-tokens :unfolded-token-types] + (make-unfolded-token-types-state file-id set-id new-types))] + (save-unfolded-token-types-in-storage file-id set-id + new-types) + new-state)))) (defn clear-tokens-types [] (ptk/reify ::clear-tokens-types ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-tokens :unfolded-token-types] [])))) - + (let [file-id (:current-file-id state) + set-id (get-in state [:workspace-tokens :selected-token-set-id])] + (save-unfolded-token-types-in-storage file-id set-id #{}) + (assoc-in state + [:workspace-tokens :unfolded-token-types] + (make-unfolded-token-types-state file-id set-id #{})))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; TOKENS TREE - Toggle tree nodes @@ -658,7 +722,12 @@ (ptk/reify ::set-selected-token-set-id ptk/UpdateEvent (update [_ state] - (update state :workspace-tokens assoc :selected-token-set-id id)))) + (let [file-id (:current-file-id state) + stored (get-unfolded-token-types-from-storage file-id id)] + (-> state + (update :workspace-tokens assoc :selected-token-set-id id) + (assoc-in [:workspace-tokens :unfolded-token-types] + (make-unfolded-token-types-state file-id id stored))))))) (defn start-token-set-edition [edition-id] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs index 83228ecaac..a9b6914b9f 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs @@ -76,9 +76,14 @@ (get dwta/token-properties type) folded-token-paths (mf/deref ref:folded-token-paths) - unfolded-token-types (mf/deref ref:unfolded-token-types) + unfolded-token-types-state (mf/deref ref:unfolded-token-types) - is-type-unfolded (contains? (set unfolded-token-types) type) + current-file (mf/deref refs/file) + + is-same-file-set? (and (= (:file-id unfolded-token-types-state) (:id current-file)) + (= (:set-id unfolded-token-types-state) selected-token-set-id)) + is-type-unfolded (and is-same-file-set? + (contains? (set (:types unfolded-token-types-state)) type)) editing-ref (mf/deref refs/workspace-editor-state) edition (mf/deref refs/selected-edition) @@ -157,6 +162,10 @@ :level :warning :timeout 3000}))))))))] + (mf/use-effect + (fn [] + (st/emit! (dwtl/restore-unfolded-token-types)))) + [:div {:class (stl/css :token-section-wrapper) :data-testid (dm/str "section-" (name type))} [:> layer-button* {:label title diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs index 49938e462b..56621eceac 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs @@ -17,7 +17,6 @@ (defn- on-select-token-set-click [id] (st/emit! (dwtl/clear-tokens-paths) - (dwtl/clear-tokens-types) (dwtl/set-selected-token-set-id id))) (defn- on-toggle-token-set-click [name] From f5591ed22ef033d9b42472abb7b84a4951c8a471 Mon Sep 17 00:00:00 2001 From: Juanfran Date: Wed, 15 Apr 2026 09:58:12 +0200 Subject: [PATCH 147/288] :bug: Forward email when adding user to Nitrate organization --- backend/src/app/nitrate.clj | 6 ++- backend/src/app/rpc/commands/teams.clj | 43 ++++++++++--------- .../app/rpc/commands/teams_invitations.clj | 2 +- backend/src/app/rpc/commands/verify_token.clj | 2 +- frontend/src/app/main/data/nitrate.cljs | 3 +- 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 92c0745670..f062bbaedf 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -250,9 +250,11 @@ schema:team params))) (defn- add-profile-to-org-api - [cfg {:keys [profile-id org-id team-id] :as params}] + [cfg {:keys [profile-id org-id team-id email] :as params}] (let [baseuri (cf/get :nitrate-backend-uri) - params (assoc params :request-params {:user-id profile-id :team-id team-id})] + request-params (cond-> {:user-id profile-id :team-id team-id} + (some? email) (assoc :email email)) + params (assoc params :request-params request-params)] (request-to-nitrate cfg :post (str baseuri "/api/organizations/" diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 25cbe48640..242178a37b 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -542,26 +542,29 @@ (defn initialize-user-in-nitrate-org "If needed, create a default team for the user on the organization, and notify Nitrate that an user has been added to an org." - [cfg profile-id org-id] - (assert (db/connection-map? cfg) - "expected cfg with valid connection") - (when (contains? cf/flags :nitrate) - (db/tx-run! - cfg - (fn [{:keys [::db/conn] :as tx-cfg}] - (let [org-id org-id - default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id org-id) - default-team-id (:id default-team) - result (nitrate/call tx-cfg :add-profile-to-org {:profile-id profile-id - :team-id default-team-id - :org-id org-id})] - (when (not (:is-member result)) - (ex/raise :type :internal - :code :failed-add-profile-org-nitrate - :context {:profile-id profile-id - :org-id org-id - :default-team-id default-team-id})) - default-team-id))))) + ([cfg profile-id org-id] + (initialize-user-in-nitrate-org cfg profile-id org-id nil)) + ([cfg profile-id org-id email] + (assert (db/connection-map? cfg) + "expected cfg with valid connection") + (when (contains? cf/flags :nitrate) + (db/tx-run! + cfg + (fn [{:keys [::db/conn] :as tx-cfg}] + (let [org-id org-id + default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id org-id) + default-team-id (:id default-team) + result (nitrate/call tx-cfg :add-profile-to-org (cond-> {:profile-id profile-id + :team-id default-team-id + :org-id org-id} + (some? email) (assoc :email email)))] + (when (not (:is-member result)) + (ex/raise :type :internal + :code :failed-add-profile-org-nitrate + :context {:profile-id profile-id + :org-id org-id + :default-team-id default-team-id})) + default-team-id)))))) (defn add-profile-to-team! ([cfg params] diff --git a/backend/src/app/rpc/commands/teams_invitations.clj b/backend/src/app/rpc/commands/teams_invitations.clj index 730a3c8887..0e496aac37 100644 --- a/backend/src/app/rpc/commands/teams_invitations.clj +++ b/backend/src/app/rpc/commands/teams_invitations.clj @@ -137,7 +137,7 @@ (if organization ;; Insert the invited member to the org (when (contains? cf/flags :nitrate) - (teams/initialize-user-in-nitrate-org cfg (:id member) (:id organization))) + (teams/initialize-user-in-nitrate-org cfg (:id member) (:id organization) email)) ;; Insert the invited member to the team (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true})) diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index 9a6fa8de9b..6e4532b3ad 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -111,7 +111,7 @@ accepted-team-id (if org-id ;; Insert the invited member to the org (when (contains? cf/flags :nitrate) - (teams/initialize-user-in-nitrate-org cfg id-member org-id)) + (teams/initialize-user-in-nitrate-org cfg id-member org-id member-email)) ;; Insert the invited member to the team (do (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}) team-id))] diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index 6c53126cec..2697cbf1c1 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -57,7 +57,8 @@ (let [href (dm/str "/control-center/org/" (u/percent-encode organization-slug) "/" - (u/percent-encode (str organization-id)))] + (u/percent-encode (str organization-id)) + "/people/")] (st/emit! (rt/nav-raw :href href))) (st/emit! (rt/nav-raw :href "/control-center/"))))) From c10f945473b2b7259f496008159fe0a75157238a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Wed, 15 Apr 2026 11:10:23 +0200 Subject: [PATCH 148/288] :sparkles: Add nitrate subscription expected cancel date --- .../src/app/main/ui/settings/subscription.cljs | 8 +++++++- .../src/app/main/ui/settings/subscription.scss | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index ee884c056e..b7e9b6c3a4 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -29,6 +29,7 @@ [{:keys [card-title card-title-icon price-value price-period + cancel-at benefits-title benefits cta-text cta-link @@ -57,7 +58,10 @@ (when (and price-value price-period) [:div {:class (stl/css :plan-price)} [:span {:class (stl/css :plan-price-value)} price-value] - [:span {:class (stl/css :plan-price-period)} " / " price-period]])] + [:span {:class (stl/css :plan-price-period)} " / " price-period]]) + (when cancel-at + [:div {:class (stl/css :plan-cancel)} + [:span {:class (stl/css :plan-cancel-date)} cancel-at]])] (when benefits-title [:h5 {:class (stl/css :benefits-title)} benefits-title]) [:ul {:class (stl/css :benefits-list)} (for [benefit benefits] @@ -511,6 +515,8 @@ ;; TODO add translations for this texts when we have the definitive ones [:> plan-card* {:card-title "Business Nitrate" :card-title-icon i/character-b + :cancel-at (when (:cancel-at nitrate-license) + (dm/str "Active until " (ct/format-inst (:cancel-at nitrate-license) "d MMMM, yyyy"))) :benefits-title "Loren ipsum", :benefits ["Loren ipsum", "Loren ipsum", diff --git a/frontend/src/app/main/ui/settings/subscription.scss b/frontend/src/app/main/ui/settings/subscription.scss index ca37947dd6..e881541721 100644 --- a/frontend/src/app/main/ui/settings/subscription.scss +++ b/frontend/src/app/main/ui/settings/subscription.scss @@ -137,6 +137,20 @@ color: var(--color-foreground-primary); } +.plan-cancel { + align-items: center; + background-color: var(--color-background-secondary); + border-radius: var(--sp-xs); + display: flex; + padding-inline: var(--sp-s); +} + +.plan-cancel-date { + @include t.use-typography("body-medium"); + + color: var(--color-foreground-primary); +} + .benefits-list { margin-block: 0; } From 44536e2eaaa3ece55cf505db2c956fd64669daf9 Mon Sep 17 00:00:00 2001 From: Stas Haas Date: Tue, 14 Apr 2026 14:11:36 +0200 Subject: [PATCH 149/288] :globe_with_meridians: Add translations for: German Currently translated at 95.2% (1975 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/ --- frontend/translations/de.po | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 2e57efc6ed..27b531580a 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-03-26 19:09+0000\n" -"Last-Translator: Marius \n" +"PO-Revision-Date: 2026-04-15 13:09+0000\n" +"Last-Translator: Stas Haas \n" "Language-Team: German \n" "Language: de\n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.17-dev\n" +"X-Generator: Weblate 5.17.1-dev\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -7192,7 +7192,7 @@ msgstr "Anmerkung erstellen" #: src/app/main/ui/workspace/context_menu.cljs:382 msgid "workspace.shape.menu.create-artboard-from-selection" -msgstr "Auswahl auf Zeichenfläche" +msgstr "Auswahl als Zeichenfläche" #: src/app/main/ui/workspace/context_menu.cljs:592 msgid "workspace.shape.menu.create-component" @@ -8450,3 +8450,7 @@ msgstr "Der Wert für die Streuung darf nicht negativ sein" #: src/app/main/errors.cljs:303 msgid "errors.deprecated.contact.before" msgstr "Penpot unterstützt diese Assets nicht mehr. Sie können uns allerdings" + +#: src/app/main/ui/static.cljs:314 +msgid "errors.webgl-context-lost.main-message" +msgstr "Ups! Der Canvas-Kontext ist verloren gegangen" From e131fba6756fa52a9ab84d0c1b30e3b94d5f50b8 Mon Sep 17 00:00:00 2001 From: Ingrid Pigueron Date: Wed, 15 Apr 2026 14:52:39 +0200 Subject: [PATCH 150/288] :globe_with_meridians: Add translations for: French Currently translated at 95.9% (1989 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/ --- frontend/translations/fr.po | 62 ++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 58f4dda302..1ad1d1e6d4 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -1,15 +1,15 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-02-16 08:35+0000\n" -"Last-Translator: Anonymous \n" -"Language-Team: French " -"\n" +"PO-Revision-Date: 2026-04-15 13:09+0000\n" +"Last-Translator: Ingrid Pigueron \n" +"Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" -"X-Generator: Weblate 5.16-dev\n" +"X-Generator: Weblate 5.17.1-dev\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -8503,3 +8503,55 @@ msgstr "Les versions auto-enregistrées seront gardées %s jours." #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Cliquez pour fermer le chemin" + +#: src/app/main/ui/dashboard/sidebar.cljs:347 +msgid "dashboard.create-new-org" +msgstr "Créer une organisation" + +#: src/app/main/errors.cljs:105 +msgid "errors.unexpected-exception" +msgstr "Erreur inattendue : %s" + +#: src/app/main/ui/static.cljs:318 +msgid "labels.reload-page" +msgstr "Recharger la page" + +#: src/app/main/ui/settings/subscription.cljs:50 +msgid "subscription.settings.recommended" +msgstr "Recommandé" + +#: src/app/main/ui/dashboard/team.cljs:933 +msgid "team.invitations-selected" +msgid_plural "team.invitations-selected" +msgstr[0] "1 invitation sélectionnée" +msgstr[1] "%s invitations sélectionnées" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:471 +msgid "workspace.layout-item.height-100" +msgstr "Hauteur 100 %" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:444 +msgid "workspace.layout-item.width-100" +msgstr "Largeur 100 %" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:612 +msgid "workspace.options.interaction-animation-direction-left" +msgstr "Gauche" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:608 +msgid "workspace.options.interaction-animation-direction-right" +msgstr "Droite" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:620 +msgid "workspace.options.interaction-animation-direction-up" +msgstr "Haut" + +#: src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs:108, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:401 +#, fuzzy +msgid "workspace.options.orientation.horizontal" +msgstr "Horizontal" + +#: src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs:104, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:397 +#, fuzzy +msgid "workspace.options.orientation.vertical" +msgstr "Vertical" From 3829443046fc016c57330dbc7e734fad25e9b140 Mon Sep 17 00:00:00 2001 From: Juanfran Date: Wed, 15 Apr 2026 14:59:40 +0200 Subject: [PATCH 151/288] :bug: Skip onboarding modal for nitrate entry users --- frontend/src/app/main/ui/dashboard.cljs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index f28068a27b..4440cacc21 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -16,6 +16,7 @@ [app.main.data.nitrate :as dnt] [app.main.data.notifications :as notif] [app.main.data.plugins :as dp] + [app.main.data.profile :as dprof] [app.main.data.project :as dpj] [app.main.refs :as refs] [app.main.router :as rt] @@ -268,7 +269,8 @@ (mf/with-effect [] (when (dnt/nitrate-entry-popup-pending?) (dnt/consume-nitrate-entry-popup!) - (st/emit! (dnt/show-nitrate-popup :nitrate-form))))) + (st/emit! (dprof/update-profile-props {:onboarding-viewed true}) + (dnt/show-nitrate-popup :nitrate-form))))) (mf/defc dashboard* [{:keys [profile project-id team-id search-term plugin-url template section]}] From 78381873eba030882ebca1b9792c87d5c9159812 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Thu, 16 Apr 2026 04:03:28 -0400 Subject: [PATCH 152/288] :sparkles: Edit ruler guide position by double-clicking the guide pill (#8987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drag-and-drop is the only way to move a ruler guide today, which makes hitting an exact pixel painful. Double-clicking the guide pill now swaps the position label for a numeric input — Enter commits, Escape cancels — so users can type a precise value relative to the guide's frame (or canvas). Closes #2311 Signed-off-by: eureka0928 --- CHANGES.md | 1 + .../main/ui/workspace/viewport/guides.cljs | 125 +++++++++++++++--- 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f7ba9d16c0..f154f154bd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -73,6 +73,7 @@ - Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714) - Persist asset search query and section filter when switching sidebar tabs (by @eureka0928) [Github #2913](https://github.com/penpot/penpot/issues/2913) - Add delete and duplicate buttons to typography dialog (by @eureka0928) [Github #5270](https://github.com/penpot/penpot/issues/5270) +- Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index d542d983c9..bb6df6e965 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.viewport.guides (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] @@ -23,6 +24,8 @@ [app.main.ui.formats :as fmt] [app.main.ui.workspace.viewport.rulers :as rulers] [app.util.dom :as dom] + [app.util.keyboard :as kbd] + [cuerdas.core :as str] [rumext.v2 :as mf])) (def ^:const guide-width 1) @@ -286,6 +289,18 @@ (let [axis (get guide :axis) + read-only? + (mf/use-ctx ctx/workspace-read-only?) + + is-editing* + (mf/use-state false) + + is-editing + (deref is-editing*) + + input-ref + (mf/use-ref nil) + handle-change-position (mf/use-fn (mf/deps on-guide-change) @@ -329,7 +344,55 @@ frame-guide-outside? (and (some? frame) - (not (is-guide-inside-frame? (assoc guide :position pos) frame)))] + (not (is-guide-inside-frame? (assoc guide :position pos) frame))) + + frame-offset + (if (some? frame) + (if (= axis :x) (:x frame) (:y frame)) + 0) + + accept-editing + (mf/use-fn + (mf/deps frame-offset on-guide-change guide) + (fn [] + ;; Enter both fires this and triggers a blur that calls it again; + ;; bail out on the second invocation when the input is already gone. + (when-let [input (mf/ref-val input-ref)] + (let [parsed (-> input dom/get-value str/trim d/parse-double)] + (reset! is-editing* false) + (when (and (some? parsed) (some? on-guide-change)) + (on-guide-change (assoc guide :position (+ parsed frame-offset)))))))) + + cancel-editing + (mf/use-fn + #(reset! is-editing* false)) + + on-input-key-down + (mf/use-fn + (mf/deps accept-editing cancel-editing) + (fn [event] + (cond + (kbd/enter? event) + (do (dom/prevent-default event) + (dom/stop-propagation event) + (accept-editing)) + + (kbd/esc? event) + (do (dom/prevent-default event) + (dom/stop-propagation event) + (cancel-editing))))) + + on-double-click + (mf/use-fn + (mf/deps read-only?) + (fn [event] + (when-not read-only? + (dom/stop-propagation event) + (reset! is-editing* true))))] + + (mf/with-effect [is-editing] + (when is-editing + (some-> (mf/ref-val input-ref) dom/select-text!))) (when (or (nil? frame) (and (cfh/root-frame? frame) @@ -349,7 +412,8 @@ :on-pointer-down on-pointer-down :on-pointer-up on-pointer-up :on-lost-pointer-capture on-lost-pointer-capture - :on-pointer-move on-pointer-move}])) + :on-pointer-move on-pointer-move + :on-double-click on-double-click}])) (if (some? frame) (let [{:keys [l1-x1 l1-y1 l1-x2 l1-y2 @@ -398,9 +462,12 @@ guide-opacity-hover guide-opacity)}}])) - (when (or is-hover (:hover @state)) + (when (or is-hover (:hover @state) is-editing) (let [{:keys [rect-x rect-y rect-width rect-height text-x text-y]} - (guide-pill-axis pos vbox zoom axis)] + (guide-pill-axis pos vbox zoom axis) + display-value (fmt/format-number (- pos frame-offset)) + input-w (/ guide-pill-width zoom) + input-h (/ guide-pill-height zoom)] [:g.guide-pill [:rect {:x rect-x :y rect-y @@ -408,18 +475,46 @@ :height rect-height :rx guide-pill-corner-radius :ry guide-pill-corner-radius - :style {:fill guide-color}}] + :style {:fill guide-color} + :on-double-click on-double-click}] - [:text {:x text-x - :y text-y - :text-anchor "middle" - :dominant-baseline "middle" - :transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")")) - :style {:font-size (/ rulers/font-size zoom) - :font-family rulers/font-family - :fill colors/white}} - ;; If the guide is associated to a frame we show the position relative to the frame - (fmt/format-number (- pos (if (= axis :x) (:x frame) (:y frame))))]]))]))) + (if is-editing + [:foreignObject {:x (- text-x (/ input-w 2)) + :y (- text-y (/ input-h 2)) + :width input-w + :height input-h + :transform (when (= axis :y) + (str "rotate(-90 " text-x "," text-y ")"))} + [:input {:ref input-ref + :type "number" + :step "any" + :default-value display-value + :auto-focus true + :on-key-down on-input-key-down + :on-blur accept-editing + :on-pointer-down dom/stop-propagation + :style {:width "100%" + :height "100%" + :border "none" + :outline "none" + :padding 0 + :margin 0 + :background "transparent" + :color colors/white + :font-family rulers/font-family + :font-size (str (/ rulers/font-size zoom) "px") + :text-align "center" + :-moz-appearance "textfield"}}]] + [:text {:x text-x + :y text-y + :text-anchor "middle" + :dominant-baseline "middle" + :transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")")) + :style {:font-size (/ rulers/font-size zoom) + :font-family rulers/font-family + :fill colors/white}} + ;; If the guide is associated to a frame we show the position relative to the frame + display-value])]))]))) (mf/defc new-guide-area* [{:keys [vbox zoom axis get-hover-frame disabled-guides]}] From b2f173675ed09e2936304ca1e4d65165a94bd187 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 16 Apr 2026 10:56:44 +0200 Subject: [PATCH 153/288] :paperclip: Fix changelog --- CHANGES.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f154f154bd..27df6df0c3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -37,6 +37,10 @@ - Add guide locking and fix locked elements not selectable in viewer (by @Dexterity104) [Github #8358](https://github.com/penpot/penpot/issues/8358) - Apply styles to selection (by @AzazelN28) [Taiga #13647](https://tree.taiga.io/project/penpot/task/13647) - Reorder prototyping overlay options to show Position before Relative to (by @rockchris099) [Github #2910](https://github.com/penpot/penpot/issues/2910) +- Persist asset search query and section filter when switching sidebar tabs (by @eureka0928) [Github #2913](https://github.com/penpot/penpot/issues/2913) +- Add delete and duplicate buttons to typography dialog (by @eureka0928) [Github #5270](https://github.com/penpot/penpot/issues/5270) +- Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311) + ### :bug: Bugs fixed @@ -71,9 +75,6 @@ - Access Tokens look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114) - Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714) -- Persist asset search query and section filter when switching sidebar tabs (by @eureka0928) [Github #2913](https://github.com/penpot/penpot/issues/2913) -- Add delete and duplicate buttons to typography dialog (by @eureka0928) [Github #5270](https://github.com/penpot/penpot/issues/5270) -- Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311) ### :bug: Bugs fixed From 81061013b1fdac281f1ebe42608b35365d9fd0fe Mon Sep 17 00:00:00 2001 From: aliworksx08 <57456290+aliworksx08@users.noreply.github.com> Date: Thu, 16 Apr 2026 03:12:37 -0600 Subject: [PATCH 154/288] :sparkles: Add openid-attr support and dot notation for OIDC attribute (#8946) * :sparkles: Add openid-attr support and dot notation for OIDC attribute paths * :recycle: Simplify OIDC: add dot-notation for attr paths and retain sub claim * :recycle: Fix OIDC: fix * :bug: Fix OIDC nested attr lookup for dot notation * :recycle: Remove unused OIDC openid-attr support --------- Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh --- backend/src/app/auth/oidc.clj | 15 ++++---- backend/test/backend_tests/auth_oidc_test.clj | 35 +++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 backend/test/backend_tests/auth_oidc_test.clj diff --git a/backend/src/app/auth/oidc.clj b/backend/src/app/auth/oidc.clj index fa819c5e0c..9c292ff2c2 100644 --- a/backend/src/app/auth/oidc.clj +++ b/backend/src/app/auth/oidc.clj @@ -401,8 +401,9 @@ (defn- parse-attr-path [provider path] - (let [[fitem & items] (str/split path "__")] - (into [(keyword (:type provider) fitem)] (map keyword) items))) + (let [separator (if (str/includes? path "__") "__" ".") + [fitem & items] (str/split path separator)] + (into [(keyword (:type provider) (str/kebab fitem))] (map keyword) items))) (defn- build-redirect-uri [] @@ -488,9 +489,9 @@ (let [attr-ph (parse-attr-path provider "nickname")] (get-in props attr-ph))))] - (let [info (assoc info :provider-id (str (:id provider))) - props (qualify-props provider info) - email (get-email props)] + (let [info (assoc info :provider-id (str (:id provider))) + props (qualify-props provider info) + email (get-email props)] {:backend (:type provider) :fullname (or (get-name props) email) :email email @@ -553,9 +554,9 @@ claims (get-id-token-claims provider tdata) info (case (get provider :user-info-source) - :token (dissoc claims :exp :iss :iat :aud :sub :sid) + :token (dissoc claims :exp :iss :iat :aud :sid) :userinfo (fetch-user-info cfg provider tdata) - (or (some-> claims (dissoc :exp :iss :iat :aud :sub :sid)) + (or (some-> claims (dissoc :exp :iss :iat :aud :sid)) (fetch-user-info cfg provider tdata))) info (process-user-info provider tdata info)] diff --git a/backend/test/backend_tests/auth_oidc_test.clj b/backend/test/backend_tests/auth_oidc_test.clj new file mode 100644 index 0000000000..2a451195c5 --- /dev/null +++ b/backend/test/backend_tests/auth_oidc_test.clj @@ -0,0 +1,35 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns backend-tests.auth-oidc-test + (:require + [app.auth.oidc :as oidc] + [clojure.test :as t])) + +(def ^:private oidc-provider + {:id "oidc" + :type "oidc"}) + +(t/deftest parse-attr-path-supports-dot-and-double-underscore + (t/is + (= [:oidc/resource-access :penpot_roles :roles] + (#'oidc/parse-attr-path oidc-provider "resource_access__penpot_roles__roles"))) + (t/is + (= [:oidc/ocs :data :email] + (#'oidc/parse-attr-path oidc-provider "ocs.data.email")))) + +(t/deftest process-user-info-supports-dot-notation-nested-attrs + (let [provider (assoc oidc-provider + :email-attr "ocs.data.email" + :name-attr "ocs.data.display-name") + info (#'oidc/process-user-info provider + {} + {:email_verified true + :ocs {:data {:email "nextcloud@example.com" + :display-name "Nextcloud User"}}})] + (t/is (= "nextcloud@example.com" (:email info))) + (t/is (= "Nextcloud User" (:fullname info))) + (t/is (true? (:email-verified info))))) From ac472c615a5282a2ed67a0a542b9428ae1d951dc Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 16 Apr 2026 11:18:11 +0200 Subject: [PATCH 155/288] :bug: Fix nitrate invitations org ux review --- .gitignore | 1 + .../resources/app/email/invite-to-org/en.html | 21 ++++++++----- backend/src/app/nitrate.clj | 23 +++++++++++--- backend/src/app/rpc/commands/verify_token.clj | 30 +++++++++++++++---- .../src/app/main/ui/auth/verify_token.cljs | 11 ++++++- frontend/translations/en.po | 3 ++ frontend/translations/es.po | 3 ++ 7 files changed, 73 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 100be94717..7d65539d22 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,4 @@ /**/node_modules /**/.yarn/* /.pnpm-store +/.vscode diff --git a/backend/resources/app/email/invite-to-org/en.html b/backend/resources/app/email/invite-to-org/en.html index 912b67746b..0a02932e99 100644 --- a/backend/resources/app/email/invite-to-org/en.html +++ b/backend/resources/app/email/invite-to-org/en.html @@ -174,12 +174,12 @@
+ width="100%"> @@ -187,7 +187,7 @@ @@ -195,14 +195,19 @@ diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index f062bbaedf..7f27ca0240 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -1,6 +1,7 @@ (ns app.nitrate "Module that make calls to the external nitrate aplication" (:require + [app.common.exceptions :as ex] [app.common.json :as json] [app.common.logging :as l] [app.common.schema :as sm] @@ -130,7 +131,8 @@ (def ^:private schema:profile-org [:map [:is-member :boolean] - [:organization-id ::sm/uuid]]) + [:organization-id ::sm/uuid] + [:default-team-id [:maybe ::sm/uuid]]]) ;; TODO Unify with schemas on backend/src/app/http/management.clj @@ -214,6 +216,17 @@ team-id) schema:organization params))) +(defn- get-org-membership-api + [cfg {:keys [profile-id org-id] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri)] + (request-to-nitrate cfg :get + (str baseuri + "/api/organizations/" + org-id + "/members/" + profile-id) + schema:profile-org params))) + (defn- get-org-membership-by-team-api [cfg {:keys [profile-id team-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] @@ -308,6 +321,7 @@ (when (contains? cf/flags :nitrate) {:get-team-org (partial get-team-org-api cfg) :set-team-org (partial set-team-org-api cfg) + :get-org-membership (partial get-org-membership-api cfg) :get-org-membership-by-team (partial get-org-membership-by-team-api cfg) :get-org-summary (partial get-org-summary-api cfg) :add-profile-to-org (partial add-profile-to-org-api cfg) @@ -369,9 +383,10 @@ :is-default (:is-default params)) result (call cfg :set-team-org params)] (when (nil? result) - (throw (ex-info "Failed to set team organization" - {:team-id (:id team) - :organization-id (:organization-id params)}))) + (ex/raise :type :internal + :code :failed-to-set-team-org + :context {:team-id (:id team) + :organization-id (:organization-id params)})) team)) diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index 6e4532b3ad..9392d25648 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -16,6 +16,7 @@ [app.http.session :as session] [app.loggers.audit :as audit] [app.main :as-alias main] + [app.nitrate :as nitrate] [app.rpc :as-alias rpc] [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] @@ -175,13 +176,13 @@ org-id (assoc :org-id org-id))) profile (db/get* conn :profile {:id profile-id} - {:columns [:id :email]}) - registration-disabled? (not (contains? cf/flags :registration))] + {:columns [:id :email :default-team-id]}) + registration-disabled? (not (contains? cf/flags :registration)) - (when (nil? invitation) - (ex/raise :type :validation - :code :invalid-token - :hint "no invitation associated with the token")) + org-invitation? (and (contains? cf/flags :nitrate) org-id) + membership (when org-invitation? + (nitrate/call cfg :get-org-membership {:profile-id profile-id + :org-id org-id}))] (if profile (do @@ -191,6 +192,23 @@ :code :invalid-token :hint "logged-in user does not matches the invitation")) + (when (:is-member membership) + (ex/raise :type :validation + :code :already-an-org-member + :team-id (:default-team-id membership) + :hint "the user is already a member of the organization")) + + (when (and org-invitation? (not (:organization-id membership))) + (ex/raise :type :validation + :code :org-not-found + :team-id (:default-team-id profile) + :hint "the organization doesn't exist")) + + (when (nil? invitation) + (ex/raise :type :validation + :code :invalid-token + :hint "no invitation associated with the token")) + ;; if we have logged-in user and it matches the invitation we proceed ;; with accepting the invitation and joining the current profile to the diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs index 9a3a33c47b..e3c55c7239 100644 --- a/frontend/src/app/main/ui/auth/verify_token.cljs +++ b/frontend/src/app/main/ui/auth/verify_token.cljs @@ -76,8 +76,17 @@ (fn [tdata] (handle-token tdata)) (fn [cause] - (let [{:keys [type code] :as error} (ex-data cause)] + (let [{:keys [type code team-id] :as error} (ex-data cause)] (cond + (= :invalid-token-already-member code) + (st/emit! + (rt/nav :dashboard-recent {:team-id team-id})) + + (= :org-not-found code) + (st/emit! + (rt/nav :dashboard-recent {:team-id team-id}) + (ntf/error (tr "errors.org-not-found"))) + (or (= :validation type) (= :invalid-token code) (= :token-expired (:reason error))) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index dfabd64c24..fda3a9a5c1 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1667,6 +1667,9 @@ msgstr "Email or password is incorrect." msgid "errors.wrong-old-password" msgstr "Old password is incorrect" +msgid "errors.org-not-found" +msgstr "That organization doesn't exists" + #: src/app/main/ui/settings/feedback.cljs:120 msgid "feedback.description" msgstr "Description" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 922b552159..16379e8e5d 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1636,6 +1636,9 @@ msgstr "El email o la contraseña son incorrectos." msgid "errors.wrong-old-password" msgstr "La contraseña anterior no es correcta" +msgid "errors.org-not-found" +msgstr "Esa organización no existe" + #: src/app/main/ui/settings/feedback.cljs:120 msgid "feedback.description" msgstr "Descripción" From 65a0fcb15b2c6ae10e2d316771f8e49233777c48 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 16 Apr 2026 11:45:37 +0200 Subject: [PATCH 156/288] :bug: Fix on nitrate leave org default org team must be deleted if empty --- backend/src/app/rpc/commands/nitrate.clj | 33 +++++- .../test/backend_tests/rpc_nitrate_test.clj | 107 ++++++++++++++++-- 2 files changed, 125 insertions(+), 15 deletions(-) diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index d16bd8cd22..b5a4c6064d 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -40,6 +40,13 @@ AND t.id = ANY(?) AND t.deleted_at IS NULL") +(def ^:private sql:get-team-files-count + "SELECT count(*) AS total + FROM file AS f + JOIN project AS p ON (p.id = f.project_id) + WHERE p.team_id = ? + AND f.deleted_at IS NULL") + (def ^:private schema:leave-org [:map [:org-id ::sm/uuid] @@ -100,6 +107,21 @@ valid-teams-to-leave-ids (into valid-teams-to-transfer-ids valid-teams-to-exit-ids) + ;; Get all the teams ids + all-teams-ids (into #{} d/xf:map-id (:teams org-summary)) + + ;; Get all the ids of the teams that will be processed: + ;; all the ids on teams-to-leave, teams-to-delete and default-team-id + selected-team-ids (-> (into #{default-team-id} teams-to-delete) + (into d/xf:map-id teams-to-leave)) + + ;; Check that we are processing all the teams + all-teams-selected? (= all-teams-ids selected-team-ids) + + default-team-files-count (-> (db/exec-one! conn [sql:get-team-files-count default-team-id]) + :total) + delete-default-team? (= default-team-files-count 0) + ;; for every team in teams-to-leave, check that: ;; - if it has a reassign-to, it belongs to valid-teams-to-transfer and ;; the reassign-to is a member of the team and not the current user; @@ -123,7 +145,8 @@ (when (or (not valid-teams-to-delete?) (not valid-teams-to-leave?) - (not valid-default-team-id?)) + (not valid-default-team-id?) + (not all-teams-selected?)) (ex/raise :type :validation :code :not-valid-teams)) @@ -135,8 +158,12 @@ (doseq [{:keys [id reassign-to]} teams-to-leave] (teams/leave-team cfg {:profile-id profile-id :id id :reassign-to reassign-to})) - ;; Rename default-team-id - (db/exec! conn [sql:prefix-team-name-and-unset-default org-prefix default-team-id]) + ;; Delete default-team-id if empty; otherwise keep it and prefix the name. + (if delete-default-team? + (do + (db/update! conn :team {:is-default false} {:id default-team-id}) + (teams/delete-team cfg {:profile-id profile-id :team-id default-team-id})) + (db/exec! conn [sql:prefix-team-name-and-unset-default org-prefix default-team-id])) ;; Api call to nitrate (nitrate/call cfg :remove-profile-from-org {:profile-id profile-id :org-id org-id}) diff --git a/backend/test/backend_tests/rpc_nitrate_test.clj b/backend/test/backend_tests/rpc_nitrate_test.clj index 0a0c35296f..d098013aa5 100644 --- a/backend/test/backend_tests/rpc_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_nitrate_test.clj @@ -48,9 +48,15 @@ (let [profile-owner (th/create-profile* 1 {:is-active true}) profile-user (th/create-profile* 2 {:is-active true}) + org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) + project (th/create-project* 99 {:profile-id (:id profile-user) + :team-id (:id org-default-team)}) + _ (th/create-file* 99 {:profile-id (:id profile-user) + :project-id (:id project)}) + org-id (uuid/random) ;; The user's personal penpot team in the org context - your-penpot-id (:default-team-id profile-user) + your-penpot-id (:id org-default-team) org-summary (make-org-summary :org-id org-id @@ -78,14 +84,81 @@ (t/is (str/starts-with? (:name team) "[Test Org] ")) (t/is (false? (:is-default team)))))))) +(t/deftest leave-org-deletes-org-default-team-when-empty + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + org-default-team (th/create-team* 98 {:profile-id (:id profile-user)}) + + org-id (uuid/random) + your-penpot-id (:id org-default-team) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :default-team-id your-penpot-id + :teams-to-delete [] + :teams-to-leave []} + out (th/command! data)] + + (t/is (th/success? out)) + + ;; Empty org default team should be soft-deleted. + (let [team (th/db-get :team {:id your-penpot-id} {::db/remove-deleted false})] + (t/is (some? (:deleted-at team)))))))) + +(t/deftest leave-org-keeps-and-renames-org-default-team-when-has-files + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + org-default-team (th/create-team* 97 {:profile-id (:id profile-user)}) + project (th/create-project* 97 {:profile-id (:id profile-user) + :team-id (:id org-default-team)}) + _ (th/create-file* 97 {:profile-id (:id profile-user) + :project-id (:id project)}) + + org-id (uuid/random) + your-penpot-id (:id org-default-team) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :default-team-id your-penpot-id + :teams-to-delete [] + :teams-to-leave []} + out (th/command! data)] + + (t/is (th/success? out)) + + ;; Non-empty org default team should remain and be renamed. + (let [team (th/db-get :team {:id your-penpot-id})] + (t/is (str/starts-with? (:name team) "[Test Org] ")) + (t/is (false? (:is-default team))) + (t/is (nil? (:deleted-at team)))))))) + (t/deftest leave-org-with-teams-to-delete (let [profile-owner (th/create-profile* 1 {:is-active true}) profile-user (th/create-profile* 2 {:is-active true}) ;; profile-user is the sole owner/member of team1 team1 (th/create-team* 1 {:profile-id (:id profile-user)}) + org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) org-id (uuid/random) - your-penpot-id (:default-team-id profile-user) + your-penpot-id (:id org-default-team) org-summary (make-org-summary :org-id org-id @@ -118,9 +191,10 @@ _ (th/create-team-role* {:team-id (:id team1) :profile-id (:id profile-owner) :role :editor}) + org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) org-id (uuid/random) - your-penpot-id (:default-team-id profile-user) + your-penpot-id (:id org-default-team) org-summary (make-org-summary :org-id org-id @@ -161,9 +235,10 @@ _ (th/create-team-role* {:team-id (:id team1) :profile-id (:id profile-user) :role :editor}) + org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) org-id (uuid/random) - your-penpot-id (:default-team-id profile-user) + your-penpot-id (:id org-default-team) org-summary (make-org-summary :org-id org-id @@ -196,8 +271,9 @@ (t/deftest leave-org-error-org-owner-cannot-leave (let [profile-owner (th/create-profile* 1 {:is-active true}) + org-default-team (th/create-team* 99 {:profile-id (:id profile-owner)}) org-id (uuid/random) - your-penpot-id (:default-team-id profile-owner) + your-penpot-id (:id org-default-team) ;; profile-owner IS the org owner in the org-summary org-summary (make-org-summary @@ -223,8 +299,9 @@ (t/deftest leave-org-error-invalid-default-team-id (let [profile-owner (th/create-profile* 1 {:is-active true}) profile-user (th/create-profile* 2 {:is-active true}) + org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) org-id (uuid/random) - your-penpot-id (:default-team-id profile-user) + your-penpot-id (:id org-default-team) org-summary (make-org-summary :org-id org-id @@ -253,9 +330,10 @@ ;; profile-user is the sole owner/member of both team1 and team2 team1 (th/create-team* 1 {:profile-id (:id profile-user)}) team2 (th/create-team* 2 {:profile-id (:id profile-user)}) + org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) org-id (uuid/random) - your-penpot-id (:default-team-id profile-user) + your-penpot-id (:id org-default-team) org-summary (make-org-summary :org-id org-id @@ -286,9 +364,10 @@ _ (th/create-team-role* {:team-id (:id team1) :profile-id (:id profile-owner) :role :editor}) + org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) org-id (uuid/random) - your-penpot-id (:default-team-id profile-user) + your-penpot-id (:id org-default-team) org-summary (make-org-summary :org-id org-id @@ -319,9 +398,10 @@ _ (th/create-team-role* {:team-id (:id team1) :profile-id (:id profile-owner) :role :editor}) + org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) org-id (uuid/random) - your-penpot-id (:default-team-id profile-user) + your-penpot-id (:id org-default-team) org-summary (make-org-summary :org-id org-id @@ -351,9 +431,10 @@ _ (th/create-team-role* {:team-id (:id team1) :profile-id (:id profile-owner) :role :editor}) + org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) org-id (uuid/random) - your-penpot-id (:default-team-id profile-user) + your-penpot-id (:id org-default-team) org-summary (make-org-summary :org-id org-id @@ -385,9 +466,10 @@ _ (th/create-team-role* {:team-id (:id team1) :profile-id (:id profile-owner) :role :editor}) + org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) org-id (uuid/random) - your-penpot-id (:default-team-id profile-user) + your-penpot-id (:id org-default-team) org-summary (make-org-summary :org-id org-id @@ -418,9 +500,10 @@ _ (th/create-team-role* {:team-id (:id team1) :profile-id (:id profile-user) :role :editor}) + org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) org-id (uuid/random) - your-penpot-id (:default-team-id profile-user) + your-penpot-id (:id org-default-team) org-summary (make-org-summary :org-id org-id From 39f4c13493522bfa73dd78b657f8fcbe469ce050 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 16 Apr 2026 11:46:05 +0200 Subject: [PATCH 157/288] :sparkles: Add nitrate remove team from org --- backend/src/app/nitrate.clj | 12 +++ backend/src/app/rpc/commands/nitrate.clj | 28 +++++++ backend/src/app/rpc/commands/teams.clj | 4 +- backend/src/app/rpc/management/nitrate.clj | 22 ++---- backend/src/app/rpc/notifications.clj | 24 ++++++ frontend/src/app/main/data/dashboard.cljs | 8 +- frontend/src/app/main/data/nitrate.cljs | 12 +++ frontend/src/app/main/ui/confirm.cljs | 5 +- frontend/src/app/main/ui/dashboard/team.cljs | 76 ++++++++++++++++++- frontend/src/app/main/ui/dashboard/team.scss | 79 ++++++++++++++++++++ frontend/translations/en.po | 27 +++++++ frontend/translations/es.po | 27 +++++++ 12 files changed, 298 insertions(+), 26 deletions(-) create mode 100644 backend/src/app/rpc/notifications.clj diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 7f27ca0240..48374660e0 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -286,6 +286,17 @@ "/remove-user") nil params))) +(defn- remove-team-from-org-api + [cfg {:keys [team-id organization-id] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri) + params (assoc params :request-params {:team-id team-id})] + (request-to-nitrate cfg :post + (str baseuri + "/api/organizations/" + organization-id + "/remove-team") + nil params))) + (defn- delete-team-api [cfg {:keys [team-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] @@ -327,6 +338,7 @@ :add-profile-to-org (partial add-profile-to-org-api cfg) :remove-profile-from-org (partial remove-profile-from-org-api cfg) :delete-team (partial delete-team-api cfg) + :remove-team-from-org (partial remove-team-from-org-api cfg) :get-subscription (partial get-subscription-api cfg) :connectivity (partial get-connectivity-api cfg)})) diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index b5a4c6064d..71eaedb2b0 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -8,6 +8,7 @@ [app.rpc :as-alias rpc] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] + [app.rpc.notifications :as notifications] [app.util.services :as sv])) @@ -169,3 +170,30 @@ (nitrate/call cfg :remove-profile-from-org {:profile-id profile-id :org-id org-id}) nil)) + + +(def ^:private schema:remove-team-from-org + [:map + [:team-id ::sm/uuid] + [:organization-id ::sm/uuid]]) + +(sv/defmethod ::remove-team-from-org + {::doc/added "2.16" + ::sm/params schema:remove-team-from-org} + [cfg {:keys [::rpc/profile-id team-id organization-id organization-name]}] + (let [perms (teams/get-permissions cfg profile-id team-id) + team (teams/get-team-info cfg {:id team-id})] + + (when-not (:is-owner perms) + (ex/raise :type :validation + :code :insufficient-permissions)) + + (when (:is-default team) + (ex/raise :type :validation + :code :cant-remove-default-team)) + + ;; Api call to nitrate + (nitrate/call cfg :remove-team-from-org {:team-id team-id :organization-id organization-id}) + + (notifications/notify-team-change cfg team-id nil nil organization-name "dashboard.team-no-longer-belong-org") + nil)) \ No newline at end of file diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 242178a37b..9b089e3b2c 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -471,8 +471,8 @@ ;; --- COMMAND QUERY: get-team-info (defn get-team-info - [{:keys [::db/conn] :as cfg} {:keys [id] :as params}] - (-> (db/get* conn :team + [cfg {:keys [id] :as params}] + (-> (db/get* cfg :team {:id id} {::sql/columns [:id :is-default :features]}) (decode-row))) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index fc4459e865..59a442e9c7 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -13,16 +13,15 @@ [app.common.schema :as sm] [app.common.types.profile :refer [schema:profile, schema:basic-profile]] [app.common.types.team :refer [schema:team]] - [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] - [app.msgbus :as mbus] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.commands.teams-invitations :as ti] [app.rpc.doc :as doc] + [app.rpc.notifications :as notifications] [app.util.services :as sv])) @@ -89,19 +88,7 @@ [:organization-id ::sm/uuid] [:organization-name ::sm/text]]) -(defn notify-team-change - [cfg team-id team-name organization-id organization-name notification] - (let [msgbus (::mbus/msgbus cfg)] - (mbus/pub! msgbus - ;;TODO There is a bug on dashboard with teams notifications. - ;;For now we send it to uuid/zero instead of team-id - :topic uuid/zero - :message {:type :team-org-change - :team-id team-id - :team-name team-name - :organization-id organization-id - :organization-name organization-name - :notification notification}))) + (sv/defmethod ::notify-team-change @@ -110,7 +97,8 @@ ::sm/params schema:notify-team-change ::rpc/auth false} [cfg {:keys [id organization-id organization-name]}] - (notify-team-change cfg id nil organization-id organization-name nil)) + (notifications/notify-team-change cfg id nil organization-id organization-name nil) + nil) ;; ---- API: notify-user-added-to-organization @@ -248,7 +236,7 @@ RETURNING id, name;") ;; Notify users (doseq [team updated-teams] - (notify-team-change cfg (:id team) (:name team) nil org-name "dashboard.org-deleted")))))))) + (notifications/notify-team-change cfg (:id team) (:name team) nil org-name "dashboard.org-deleted")))))))) ;; ---- API: get-profile-by-email diff --git a/backend/src/app/rpc/notifications.clj b/backend/src/app/rpc/notifications.clj new file mode 100644 index 0000000000..fc3b4b1752 --- /dev/null +++ b/backend/src/app/rpc/notifications.clj @@ -0,0 +1,24 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.rpc.notifications + (:require + [app.common.uuid :as uuid] + [app.msgbus :as mbus])) + +(defn notify-team-change + [cfg team-id team-name organization-id organization-name notification] + (let [msgbus (::mbus/msgbus cfg)] + (mbus/pub! msgbus + ;;TODO There is a bug on dashboard with teams notifications. + ;;For now we send it to uuid/zero instead of team-id + :topic uuid/zero + :message {:type :team-org-change + :team-id team-id + :team-name team-name + :organization-id organization-id + :organization-name organization-name + :notification notification}))) \ No newline at end of file diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 6a1d8d1646..7e98a0eaa4 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -702,9 +702,11 @@ (if (contains? cf/flags :nitrate) (d/update-in-when state [:teams team-id] (fn [team] - (cond-> (assoc team - :organization-id organization-id - :organization-name organization-name) + (cond-> team + (some? organization-id) (assoc :organization-id organization-id) + (nil? organization-id) (dissoc :organization-id) + (some? organization-name) (assoc :organization-name organization-name) + (nil? organization-name) (dissoc :organization-name) team-name (assoc :name team-name)))) state)))) diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index 2697cbf1c1..ba7124a5a2 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -111,3 +111,15 @@ :type :toast :level :success})))) (rx/catch on-error)))))) + + +(defn remove-team-from-org + [{:keys [team-id organization-id organization-name] :as params}] + (ptk/reify ::remove-team-from-org + ptk/WatchEvent + (watch [_ _ _] + (->> (rp/cmd! ::remove-team-from-org {:team-id team-id :organization-id organization-id :organization-name organization-name}) + (rx/mapcat + (fn [_] + (rx/of + (modal/hide)))))))) \ No newline at end of file diff --git a/frontend/src/app/main/ui/confirm.cljs b/frontend/src/app/main/ui/confirm.cljs index f9ba97b7e2..522641e93c 100644 --- a/frontend/src/app/main/ui/confirm.cljs +++ b/frontend/src/app/main/ui/confirm.cljs @@ -34,7 +34,8 @@ items cancel-label accept-label - accept-style] :as props}] + accept-style + hint-level] :as props}] (let [on-accept (or on-accept identity) on-cancel (or on-cancel identity) message (or message (tr "ds.confirm-title")) @@ -84,7 +85,7 @@ (when (and (string? scd-message) (not= scd-message "")) [:h3 {:class (stl/css :modal-scd-msg)} scd-message]) (when (string? hint) - [:> context-notification* {:level :info + [:> context-notification* {:level (or hint-level :info) :appearance :ghost} hint]) (when (string? error-msg) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 96afda2563..6dc642c40d 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -14,6 +14,7 @@ [app.main.data.common :as dcm] [app.main.data.event :as ev] [app.main.data.modal :as modal] + [app.main.data.nitrate :as dnt] [app.main.data.notifications :as ntf] [app.main.data.team :as dtm] [app.main.refs :as refs] @@ -21,6 +22,7 @@ [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.forms :as fm] + [app.main.ui.components.org-avatar :refer [org-avatar*]] [app.main.ui.dashboard.change-owner] [app.main.ui.dashboard.subscription :refer [members-cta* show-subscription-members-banner? @@ -44,6 +46,9 @@ (def ^:private menu-icon (deprecated-icon/icon-xref :menu (stl/css :menu-icon))) +(def ^:private org-menu-icon + (deprecated-icon/icon-xref :menu (stl/css :org-menu-icon))) + (def ^:private warning-icon (deprecated-icon/icon-xref :msg-warning (stl/css :warning-icon))) @@ -1274,7 +1279,8 @@ (mf/defc team-settings-page* {::mf/props :obj} [{:keys [team]}] - (let [finput (mf/use-ref) + (let [nitrate? (contains? cfg/flags :nitrate) + finput (mf/use-ref) members (get team :members) stats (get team :stats) @@ -1285,12 +1291,49 @@ can-edit (or (:is-owner permissions) (:is-admin permissions)) + show-org-options-menu* + (mf/use-state false) + + show-org-options-menu? + (deref show-org-options-menu*) + + on-show-options-click + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (swap! show-org-options-menu* not))) + + close-org-options-menu + (mf/use-fn #(reset! show-org-options-menu* false)) + on-image-click (mf/use-fn #(dom/click (mf/ref-val finput))) on-file-selected (fn [file] - (st/emit! (dtm/update-team-photo file)))] + (st/emit! (dtm/update-team-photo file))) + + remove-team-from-org-fn + (mf/use-fn + (mf/deps team) + (fn [] + (st/emit! (dnt/remove-team-from-org {:team-id (:id team) + :organization-id (:organization-id team) + :organization-name (:organization-name team)})))) + + on-remove-team-from-org + (mf/use-fn + (mf/deps team) + (fn [] + (let [params {:type :confirm + :title (tr "modals.remove-team-org.title") + :message (tr "modals.remove-team-org.text" (:name team) (:organization-name team)) + :hint (tr "modals.remove-team-org.info") + :hint-level :default + :accept-label (tr "modals.remove-team-org.accept") + :on-accept remove-team-from-org-fn + :accept-style :danger}] + (st/emit! (modal/show params)))))] (mf/with-effect [team] (dom/set-html-title (tr "title.team-settings" @@ -1324,6 +1367,35 @@ [:div {:class (stl/css :block-text)} (:name team)]] + (when nitrate? + [:div {:class (stl/css :block)} + [:div {:class (stl/css :block-label)} + (tr "dashboard.team-organization")] + (if (:organization-id team) + [:div {:class (stl/css :block-content)} + [:div {:class (stl/css :org-block-content)} + [:> org-avatar* {:org team :size "xxxl"}] + [:span {:class (stl/css :block-text)} + (:organization-name team)] + + (when (and (:is-owner permissions) (not (:is-default team))) + [:* + [:> button* {:variant "ghost" + :type "button" + :class (stl/css-case :org-options-btn (not show-org-options-menu?) :org-options-btn-open show-org-options-menu?) + :on-click on-show-options-click} + org-menu-icon + + [:& dropdown {:show show-org-options-menu? :on-close close-org-options-menu :dropdown-id "org-options"} + [:ul {:class (stl/css :org-dropdown) + :role "listbox"} + [:li {:on-click on-remove-team-from-org + :class (stl/css :org-dropdown-item)} + (tr "dashboard.team-organization.remove")]]]]])]] + [:div {:class (stl/css :block-content)} + [:span {:class (stl/css :block-text)} + (tr "dashboard.team-organization.none")]])]) + [:div {:class (stl/css :block)} [:div {:class (stl/css :block-label)} (tr "dashboard.team-members")] diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss index 6d6d0a369b..b04c895bdd 100644 --- a/frontend/src/app/main/ui/dashboard/team.scss +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -51,6 +51,7 @@ .block-text { color: var(--color-foreground-primary); + text-wrap: nowrap; } .block-content { @@ -869,3 +870,81 @@ margin-block-start: var(--sp-xxxl); gap: var(--sp-s); } + +.org-block-content { + display: grid; + grid-template-columns: var(--sp-xxxl) 1fr var(--sp-xxxl); + align-items: center; + gap: var(--sp-m); + width: max-content; +} + +.org-options-btn { + padding: 0; + justify-content: center; + + --stroke-color: var(--color-foreground-primary); + + &:hover { + --stroke-color: var(--color-accent-primary); + } +} + +.org-options-btn-open { + padding: 0; + justify-content: center; + + --stroke-color: var(--color-accent-primary); + + background-color: var(--color-background-tertiary); + position: relative; +} + +.org-menu-icon { + display: flex; + justify-content: center; + align-items: center; + height: $sz-16; + width: $sz-16; + color: transparent; + fill: none; + stroke-width: $b-1; + stroke: var(--stroke-color); +} + +.org-dropdown { + box-shadow: var(--el-shadow-dark); + display: flex; + flex-direction: column; + gap: var(--sp-xs); + position: absolute; + padding: var(--sp-xs); + border-radius: $br-8; + z-index: var(--z-index-dropdown); + color: var(--color-foreground-primary); + background-color: var(--color-background-tertiary); + border: $b-2 solid var(--color-background-quaternary); + margin: 0; + top: var(--sp-xxxl); + width: fit-content; + min-width: $sz-160; +} + +.org-dropdown-item { + @include t.use-typography("body-small"); + + display: flex; + align-items: center; + justify-content: space-between; + height: $sz-28; + width: 100%; + padding: px2rem(6); + border-radius: $br-8; + cursor: pointer; + text-transform: none; + white-space: nowrap; + + &:hover { + background-color: var(--color-background-quaternary); + } +} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index fda3a9a5c1..b9484bebf8 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -344,6 +344,9 @@ msgstr "Restore file" msgid "dashboard.org-deleted" msgstr "The %s organization has been deleted." +msgid "dashboard.team-no-longer-belong-org" +msgstr "This team no longer belongs to the organization %s" + #: src/app/main/ui/dashboard/placeholder.cljs:41 msgid "dashboard.add-file" msgstr "Add file" @@ -1126,6 +1129,18 @@ msgstr "Team info" msgid "dashboard.team-members" msgstr "Team members" +msgid "dashboard.team-organization" +msgstr "Team organization" + +msgid "dashboard.team-organization.none" +msgstr "This team is not part of any organization" + +msgid "dashboard.team-organization.change" +msgstr "Change team organization" + +msgid "dashboard.team-organization.remove" +msgstr "Remove team from organization" + #: src/app/main/ui/dashboard/team.cljs:1344 msgid "dashboard.team-projects" msgstr "Team projects" @@ -9089,6 +9104,18 @@ msgstr "Go to dashboard" msgid "webgl.modals.webgl-unavailable.cta-troubleshooting" msgstr "Troubleshooting guide" +msgid "modals.remove-team-org.title" +msgstr "REMOVE TEAM FROM THE ORGANIZATION" + +msgid "modals.remove-team-org.text" +msgstr "Are you sure you want to remove the '%s' team from the '%s' organization?" + +msgid "modals.remove-team-org.info" +msgstr "Projects and files will remain available to team members, but the organization's settings will no longer apply." + +msgid "modals.remove-team-org.accept" +msgstr "Remove from organization" + #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Click to close the path" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 16379e8e5d..d040d050f0 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -353,6 +353,9 @@ msgstr "Restaurar archivo" msgid "dashboard.org-deleted" msgstr "La organización %s se ha borrado." +msgid "dashboard.team-no-longer-belong-org" +msgstr "Este equipo ya no pertenece a la organización %s" + #: src/app/main/ui/dashboard/placeholder.cljs:41 msgid "dashboard.add-file" msgstr "Añadir archivo" @@ -1130,6 +1133,18 @@ msgstr "Información del equipo" msgid "dashboard.team-members" msgstr "Integrantes del equipo" +msgid "dashboard.team-organization" +msgstr "Organización del equipo" + +msgid "dashboard.team-organization.none" +msgstr "Este equipo no pertenece a ninguna organización" + +msgid "dashboard.team-organization.change" +msgstr "Cambiar el equipo de organización" + +msgid "dashboard.team-organization.remove" +msgstr "Eliminar equipo de la organización" + #: src/app/main/ui/dashboard/team.cljs:1344 msgid "dashboard.team-projects" msgstr "Proyectos del equipo" @@ -8867,6 +8882,18 @@ msgstr "Ir al panel" msgid "webgl.modals.webgl-unavailable.cta-troubleshooting" msgstr "Guía de solución de problemas" +msgid "modals.remove-team-org.title" +msgstr "ELIMINAR EQUIPO DE LA ORGANIZACIÓN" + +msgid "modals.remove-team-org.text" +msgstr "¿Estás seguro de que quieres eliminar el equipo %s de la organización %s?" + +msgid "modals.remove-team-org.info" +msgstr "Los proyectos y archivos seguirán estando disponibles para los miembros del equipo, pero la configuración de la organización dejará de aplicarse." + +msgid "modals.remove-team-org.accept" +msgstr "Eliminar de la organización" + #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Pulsar para cerrar la ruta" From 7f409eadd425b99c519b148fc0b00abc9d6159b5 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 16 Apr 2026 13:49:48 +0200 Subject: [PATCH 158/288] :recycle: Update copy to be more specific (#9028) --- CHANGES.md | 1 + frontend/translations/de.po | 4 ---- frontend/translations/en.po | 2 +- frontend/translations/es.po | 2 +- frontend/translations/he.po | 4 ---- frontend/translations/hi.po | 4 ---- frontend/translations/it.po | 4 ---- frontend/translations/nl.po | 4 ---- frontend/translations/sv.po | 4 ---- frontend/translations/tr.po | 4 ---- 10 files changed, 3 insertions(+), 30 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 27df6df0c3..8bf257d487 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -64,6 +64,7 @@ - Fix app crash when selecting shapes with one hidden [Taiga #13959](https://tree.taiga.io/project/penpot/issue/13959) - Fix opacity mixed value [Taiga #13960](https://tree.taiga.io/project/penpot/issue/13960) - Fix gap input throwing an error [Github #8984](https://github.com/penpot/penpot/pull/8984) +- Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990) ## 2.15.0 (Unreleased) diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 510ad2f093..9d08659eef 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -7987,10 +7987,6 @@ msgstr "Schriftfamilie oder eine durch Kommas (,) getrennte Liste von Schriften" msgid "workspace.tokens.token-name" msgstr "Name" -#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:268, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:223 -msgid "workspace.tokens.token-name-duplication-validation-error" -msgstr "Unter diesem Speicherort existiert bereits ein Token: %s" - #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:265, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:220 msgid "workspace.tokens.token-name-length-validation-error" msgstr "Der Name muss mindestens 1 Zeichen lang sein" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index b9484bebf8..9715b7e27b 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -8678,7 +8678,7 @@ msgstr "Name" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:268, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:223 msgid "workspace.tokens.token-name-duplication-validation-error" -msgstr "A token already exists at the path: %s" +msgstr "A token already exists at the path: %s or at a prefix thereof." #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:265, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:220 msgid "workspace.tokens.token-name-length-validation-error" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index d040d050f0..09779522cc 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -8476,7 +8476,7 @@ msgstr "Nombre" #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:268, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:223 msgid "workspace.tokens.token-name-duplication-validation-error" -msgstr "Ya existe un token en la ruta: %s" +msgstr "Ya existe un token en la ruta: %s o en un prefijo del mismo." #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:265, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:220 msgid "workspace.tokens.token-name-length-validation-error" diff --git a/frontend/translations/he.po b/frontend/translations/he.po index a66e623ff0..b5e5953db5 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -7997,10 +7997,6 @@ msgstr "משפחת גופנים או רשימת גופנים מופרדת בפס msgid "workspace.tokens.token-name" msgstr "שם" -#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:268, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:223 -msgid "workspace.tokens.token-name-duplication-validation-error" -msgstr "כבר קיים אסימון בנתיב: %s" - #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:265, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:220 msgid "workspace.tokens.token-name-length-validation-error" msgstr "אורך השם חייב להיות תו אחד לפחות" diff --git a/frontend/translations/hi.po b/frontend/translations/hi.po index a169abe7a7..25d94de048 100644 --- a/frontend/translations/hi.po +++ b/frontend/translations/hi.po @@ -8151,10 +8151,6 @@ msgstr "फ़ॉन्ट परिवार या अल्पविराम msgid "workspace.tokens.token-name" msgstr "नाम" -#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:268, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:223 -msgid "workspace.tokens.token-name-duplication-validation-error" -msgstr "पथ पर एक token पहले से मौजूद है: %s" - #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:265, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:220 msgid "workspace.tokens.token-name-length-validation-error" msgstr "नाम कम से कम 1 अक्षर का होना चाहिए" diff --git a/frontend/translations/it.po b/frontend/translations/it.po index 75f3d4ed1f..550a39073a 100644 --- a/frontend/translations/it.po +++ b/frontend/translations/it.po @@ -8383,10 +8383,6 @@ msgstr "Famiglia di caratteri o elenco di caratteri separati da virgola (,)" msgid "workspace.tokens.token-name" msgstr "Nome" -#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:268, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:223 -msgid "workspace.tokens.token-name-duplication-validation-error" -msgstr "Un token con questo nome esiste già nel percorso: %s" - #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:265, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:220 msgid "workspace.tokens.token-name-length-validation-error" msgstr "Il nome deve essere di almeno 1 carattere" diff --git a/frontend/translations/nl.po b/frontend/translations/nl.po index d78724baee..65fe20fe03 100644 --- a/frontend/translations/nl.po +++ b/frontend/translations/nl.po @@ -8399,10 +8399,6 @@ msgstr "Lettertypefamilie of lijst met lettertypen gescheiden door komma (,)" msgid "workspace.tokens.token-name" msgstr "Naam" -#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:268, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:223 -msgid "workspace.tokens.token-name-duplication-validation-error" -msgstr "Er bestaat al een token op het pad: %s" - #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:265, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:220 msgid "workspace.tokens.token-name-length-validation-error" msgstr "Naam moet minimaal 1 teken zijn" diff --git a/frontend/translations/sv.po b/frontend/translations/sv.po index 6d57efaf41..891b1b5e17 100644 --- a/frontend/translations/sv.po +++ b/frontend/translations/sv.po @@ -8071,10 +8071,6 @@ msgstr "Typsnittsfamilj eller lista över typsnitt separerade med kommatecken (, msgid "workspace.tokens.token-name" msgstr "Namn" -#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:268, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:223 -msgid "workspace.tokens.token-name-duplication-validation-error" -msgstr "En token finns redan på denna sökväg: %s" - #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:265, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:220 msgid "workspace.tokens.token-name-length-validation-error" msgstr "Namnet måste innehålla minst 1 tecken" diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index 72c1bfbedf..e9da3bbc47 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -8357,10 +8357,6 @@ msgstr "Yazı tipi ailesi veya virgülle (,) ayrılan yazı tipi listesi" msgid "workspace.tokens.token-name" msgstr "Ad" -#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:268, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:223 -msgid "workspace.tokens.token-name-duplication-validation-error" -msgstr "Bu yolda zaten bir token var: %s" - #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:265, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:220 msgid "workspace.tokens.token-name-length-validation-error" msgstr "Ad en az 1 karakterden oluşmalıdır" From 32d9688c3c7c31a16946298d872a9c56064f27b3 Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Thu, 16 Apr 2026 18:20:44 +0200 Subject: [PATCH 159/288] :wrench: Add short tag to DocherHub release (#8864) --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21c0eb6de2..053dd3ff0e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,13 +64,14 @@ jobs: echo "$PUB_DOCKER_PASSWORD" | skopeo login --username "$PUB_DOCKER_USERNAME" --password-stdin docker.io IMAGES=("frontend" "backend" "exporter" "storybook") + SHORT_TAG=${TAG%.*} for image in "${IMAGES[@]}"; do skopeo copy --all \ docker://$DOCKER_REGISTRY/$image:$TAG \ docker://docker.io/penpotapp/$image:$TAG - for alias in main latest; do + for alias in main latest "$SHORT_TAG"; do skopeo copy --all \ docker://$DOCKER_REGISTRY/$image:$TAG \ docker://docker.io/penpotapp/$image:$alias From e54e02b7364ae1f09d28fd360e071d0b6d680f0e Mon Sep 17 00:00:00 2001 From: Ingrid Pigueron Date: Thu, 16 Apr 2026 13:32:19 +0200 Subject: [PATCH 160/288] :globe_with_meridians: Add translations for: French Currently translated at 96.8% (2009 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/ --- frontend/translations/fr.po | 250 +++++++++++++++++++++++++----------- 1 file changed, 173 insertions(+), 77 deletions(-) diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 1ad1d1e6d4..d633216f26 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-04-15 13:09+0000\n" +"PO-Revision-Date: 2026-04-16 18:09+0000\n" "Last-Translator: Ingrid Pigueron \n" "Language-Team: French \n" @@ -110,7 +110,7 @@ msgstr "Lien de récupération de mot de passe envoyé." #: src/app/main/ui/auth/verify_token.cljs:49 msgid "auth.notifications.team-invitation-accepted" -msgstr "Vous avez rejoint l’équipe avec succès" +msgstr "Vous avez bien rejoint l’équipe" #: src/app/main/ui/auth/login.cljs:188, src/app/main/ui/auth/register.cljs:174 msgid "auth.password" @@ -230,8 +230,8 @@ msgstr "Tous les utilisateurs de Penpot" #: src/app/main/ui/viewer/share_link.cljs:204 msgid "common.share-link.confirm-deletion-link-description" msgstr "" -"Êtes-vous certain de vouloir supprimer ce lien ? Si oui, plus personne ne " -"pourra y accéder" +"Voulez -vous vraiment supprimer ce lien ? Si oui, plus personne ne pourra y " +"accéder" #: src/app/main/ui/viewer/share_link.cljs:259, src/app/main/ui/viewer/share_link.cljs:289 msgid "common.share-link.current-tag" @@ -247,7 +247,7 @@ msgstr "Obtenir le lien" #: src/app/main/ui/viewer/share_link.cljs:142 msgid "common.share-link.link-copied-success" -msgstr "Lien copié avec succès" +msgstr "Lien copié" #: src/app/main/ui/viewer/share_link.cljs:231 msgid "common.share-link.manage-ops" @@ -312,7 +312,7 @@ msgstr "Faites une équipe  !" #: src/app/main/ui/dashboard/projects.cljs #, unused msgid "dasboard.tutorial-hero.info" -msgstr "Apprenez les bases de Penpot en s'amusant avec ce tutoriel pratique." +msgstr "Apprenez les bases de Penpot en vous amusant avec ce tutoriel pratique." #: src/app/main/ui/dashboard/projects.cljs #, unused @@ -357,11 +357,11 @@ msgstr "Générer un nouveau jeton" #: src/app/main/ui/settings/access_tokens.cljs:64 msgid "dashboard.access-tokens.create.success" -msgstr "Jeton d'accès créé avec succès." +msgstr "Le jeton d'accès a bien été créé." #: src/app/main/ui/settings/access_tokens.cljs:286 msgid "dashboard.access-tokens.empty.add-one" -msgstr "Pressez le bouton \"Générer un nouveau jeton\" pour en générer un." +msgstr "Appuyez sur le bouton \"Générer un nouveau jeton\" pour en générer un." #: src/app/main/ui/settings/access_tokens.cljs:285 msgid "dashboard.access-tokens.empty.no-access-tokens" @@ -369,19 +369,19 @@ msgstr "Vous n'avez pas encore de jeton." #: src/app/main/ui/settings/access_tokens.cljs:135 msgid "dashboard.access-tokens.expiration-180-days" -msgstr "180 jours" +msgstr "180 jours" #: src/app/main/ui/settings/access_tokens.cljs:132 msgid "dashboard.access-tokens.expiration-30-days" -msgstr "30 jours" +msgstr "30 jours" #: src/app/main/ui/settings/access_tokens.cljs:133 msgid "dashboard.access-tokens.expiration-60-days" -msgstr "60 jours" +msgstr "60 jours" #: src/app/main/ui/settings/access_tokens.cljs:134 msgid "dashboard.access-tokens.expiration-90-days" -msgstr "90 jours" +msgstr "90 jours" #: src/app/main/ui/settings/access_tokens.cljs:131 msgid "dashboard.access-tokens.expiration-never" @@ -389,11 +389,11 @@ msgstr "Jamais" #: src/app/main/ui/settings/access_tokens.cljs:268 msgid "dashboard.access-tokens.expired-on" -msgstr "A expiré le %s" +msgstr "Est arrivé à expiration le %s" #: src/app/main/ui/settings/access_tokens.cljs:269 msgid "dashboard.access-tokens.expires-on" -msgstr "Expire le %s" +msgstr "Arrive à expiration le %s" #: src/app/main/ui/settings/access_tokens.cljs:267 msgid "dashboard.access-tokens.no-expiration" @@ -412,7 +412,7 @@ msgstr "" #: src/app/main/ui/settings/access_tokens.cljs:142 msgid "dashboard.access-tokens.token-will-expire" -msgstr "Le jeton expirera le %s" +msgstr "Le jeton arrivera à expiration le %s" #: src/app/main/ui/settings/access_tokens.cljs:143 msgid "dashboard.access-tokens.token-will-not-expire" @@ -453,18 +453,18 @@ msgstr "Votre Penpot" #: src/app/main/ui/dashboard/deleted.cljs:262 msgid "dashboard.delete-all-forever-confirmation.description" msgstr "" -"Êtes-vous sûr de vouloir supprimer tous vos projets et fichiers effacés " -"pour toujours ? Cette action est irréversible." +"Voulez-vous vraiment supprimer définitivement tous vos projets et fichiers " +"effacés ? Cette action est irréversible." #: src/app/main/ui/dashboard/file_menu.cljs:221 msgid "dashboard.delete-file-forever-confirmation.description" msgstr "" -"Êtes-vous sûr de vouloir supprimer %s pour toujours ? Cette action est " +"Voulez-vous vraiment supprimer définitivement %s ? Cette action est " "irréversible." #: src/app/main/data/dashboard.cljs:778 msgid "dashboard.delete-files-success-notification" -msgstr "%s fichiers ont été effacés avec succès." +msgstr "%s fichiers ont bien été effacés." #: src/app/main/ui/dashboard/deleted.cljs:51, src/app/main/ui/dashboard/deleted.cljs:53, src/app/main/ui/dashboard/deleted.cljs:261, src/app/main/ui/dashboard/deleted.cljs:263, src/app/main/ui/dashboard/file_menu.cljs:220, src/app/main/ui/dashboard/file_menu.cljs:222 msgid "dashboard.delete-forever-confirmation.title" @@ -477,13 +477,13 @@ msgstr "Supprimer le projet" #: src/app/main/ui/dashboard/deleted.cljs:52 msgid "dashboard.delete-project-forever-confirmation.description" msgstr "" -"Êtes-vous sûr de vouloir supprimer définitivement le projet %s ? Vous allez " -"le supprimer définitivement avec tous les fichiers qu'il contient. Cette " -"action est irréversible." +"Voulez-vous vraiment supprimer définitivement le projet %s ? Vous allez le " +"supprimer définitivement avec tous les fichiers qu'il contient. Cette action " +"est irréversible." #: src/app/main/data/dashboard.cljs:777, src/app/main/data/dashboard.cljs:811 msgid "dashboard.delete-success-notification" -msgstr "%s a été supprimé avec succès." +msgstr "%s a bien été supprimé." #: src/app/main/ui/dashboard/sidebar.cljs:495 msgid "dashboard.delete-team" @@ -586,7 +586,7 @@ msgstr "Commencez à fabriquer des choses géniales" #, unused msgid "dashboard.errors.error-on-delete-file" -msgstr "Il y a eu une erreur lors de la suppression du fichier %s." +msgstr "Une erreur s'est produite lors de la suppression du fichier %s." #: src/app/main/data/dashboard.cljs:781 msgid "dashboard.errors.error-on-delete-files" @@ -718,10 +718,10 @@ msgstr "" #, markdown msgid "dashboard.fonts.hero-text2" msgstr "" -"Ne téléchargez que des polices que vous possédez ou dont la license vous " +"Ne téléchargez que des polices que vous possédez ou dont la licence vous " "permet de les utiliser dans Penpot. Vous trouverez plus d'informations dans " -"la section Propriété des Contenus des [conditions générales d'utilisation " -"de Penpot](%s). Vous pouvez également vous renseigner sur les [licenses de " +"la section Propriété des Contenus des [conditions générales d'utilisation de " +"Penpot](%s). Vous pouvez également vous renseigner sur les [licences de " "polices](https://www.typography.com/faq)." #: src/app/main/ui/dashboard/fonts.cljs:214 @@ -810,7 +810,7 @@ msgstr "Médias en cours de traitement" #: src/app/main/ui/dashboard/import.cljs:125 msgid "dashboard.import.progress.process-page" -msgstr "Traitement de la page : %s" +msgstr "Traitement de la page : %s" #: src/app/main/ui/dashboard/import.cljs:131 msgid "dashboard.import.progress.process-typographies" @@ -822,7 +822,7 @@ msgstr "Envoi des données au serveur (%s/%s)" #: src/app/main/ui/dashboard/import.cljs:122 msgid "dashboard.import.progress.upload-media" -msgstr "Envoi du fichier : %s" +msgstr "Envoi du fichier : %s" #: src/app/main/ui/dashboard/team.cljs:765 msgid "dashboard.invitation-modal.delete" @@ -938,7 +938,7 @@ msgstr "Les paramètres des notifications ont été mis à jour" #: src/app/main/ui/settings/password.cljs:38 msgid "dashboard.notifications.password-saved" -msgstr "Mot de passe enregistré avec succès !" +msgstr "Mot de passe enregistré !" #: src/app/main/ui/dashboard/comments.cljs:45 msgid "dashboard.notifications.view" @@ -1049,7 +1049,7 @@ msgstr "Tout restaurer" #: src/app/main/data/dashboard.cljs:903 msgid "dashboard.restore-files-success-notification" -msgstr "%s fichiers ont été restaurés avec succès." +msgstr "%s fichiers ont bien été restaurés." #: src/app/main/ui/dashboard/deleted.cljs:82 msgid "dashboard.restore-project-button" @@ -1065,7 +1065,7 @@ msgstr "Restaurer le projet" #: src/app/main/data/dashboard.cljs:875, src/app/main/data/dashboard.cljs:902, src/app/main/data/dashboard.cljs:939, src/app/main/ui/dashboard/file_menu.cljs:198 msgid "dashboard.restore-success-notification" -msgstr "%s a été restauré avec succès." +msgstr "%s a bien été restauré." #: src/app/main/ui/settings/profile.cljs:78 msgid "dashboard.save-settings" @@ -1189,7 +1189,7 @@ msgstr "Votre projet a bien été dupliqué" #: src/app/main/ui/dashboard/file_menu.cljs:132, src/app/main/ui/dashboard/grid.cljs:634, src/app/main/ui/dashboard/sidebar.cljs:166 msgid "dashboard.success-move-file" -msgstr "Votre fichier a été déplacé avec succès" +msgstr "Votre fichier a bien été déplacé" #: src/app/main/ui/dashboard/file_menu.cljs:131 msgid "dashboard.success-move-files" @@ -1229,7 +1229,7 @@ msgstr "Les fichiers supprimés resteront dans la corbeille pendant" #: src/app/main/ui/dashboard/deleted.cljs:300 msgid "dashboard.trash-info-text-part2" -msgstr " %s jours. " +msgstr " %s jours. " #: src/app/main/ui/dashboard/deleted.cljs:301 msgid "dashboard.trash-info-text-part3" @@ -1247,7 +1247,7 @@ msgstr "Écrivez pour rechercher" #: src/app/main/ui/dashboard/file_menu.cljs:319, src/app/main/ui/workspace/main_menu.cljs:642 msgid "dashboard.unpublish-shared" -msgstr "Retirer la Bibliothèque" +msgstr "Supprimer la bibliothèque" #: src/app/main/ui/settings/options.cljs:74 msgid "dashboard.update-settings" @@ -1285,7 +1285,7 @@ msgstr "Créer un webhook" #: src/app/main/ui/dashboard/team.cljs:1031 msgid "dashboard.webhooks.create.success" -msgstr "Webhook créé avec succès." +msgstr "Webhook créé." #: src/app/main/ui/dashboard/team.cljs:1136 msgid "dashboard.webhooks.description" @@ -1305,7 +1305,7 @@ msgstr "Aucun webhook créé jusqu’à présent." #, unused msgid "dashboard.webhooks.update.success" -msgstr "Webhook mis à jour avec succès." +msgstr "Webhook mis à jour." #: src/app/main/ui/settings.cljs:34 msgid "dashboard.your-account-title" @@ -1453,21 +1453,21 @@ msgstr "Domaine non autorisé" #: src/app/main/ui/auth/recovery_request.cljs:57, src/app/main/ui/auth/register.cljs:98, src/app/main/ui/auth/register.cljs:101, src/app/main/ui/dashboard/team.cljs:627, src/app/main/ui/settings/change_email.cljs:37 msgid "errors.email-has-permanent-bounces" -msgstr "L'adresse e-mail « %s » a un taux de rebond trop élevé." +msgstr "L'adresse e-mail « %s » a un taux de rebond trop élevé." #: src/app/main/ui/dashboard/team.cljs:196, src/app/main/ui/dashboard/team.cljs:858, src/app/main/ui/onboarding/team_choice.cljs:110 msgid "errors.email-spam-or-permanent-bounces" -msgstr "L'e-mail \"%s\" a été signalé comme spam ou a été rejeté." +msgstr "L'e-mail « %s » a été signalé comme spam ou a été rejeté." #: src/app/main/errors.cljs:279 msgid "errors.feature-mismatch" msgstr "" -"Il semble que vous ouvrez un fichier qui a la fonctionnalité '%s' activée, " +"Vous semblez ouvrir un fichier pour lequel la fonctionnalité « %s » activée, " "mais votre interface Penpot ne la prend pas en charge ou l'a désactivée." #: src/app/main/errors.cljs:283, src/app/main/errors.cljs:297 msgid "errors.feature-not-supported" -msgstr "La fonctionnalité '%s' n'est pas prise en charge." +msgstr "La fonctionnalité « %s » n'est pas prise en charge." #: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:296, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:240 msgid "errors.field-max-length" @@ -1491,7 +1491,7 @@ msgid "errors.file-feature-mismatch" msgstr "" "Il semble y avoir une incompatibilité entre les fonctionnalités actives et " "celles du fichier que tentez d'ouvrir. Vous devez activer les migrations " -"pour '%s' avant de pouvoir ouvrir le fichier." +"pour « %s » avant de pouvoir ouvrir le fichier." #: src/app/main/data/auth.cljs:347, src/app/main/ui/auth/login.cljs:104, src/app/main/ui/auth/register.cljs:110, src/app/main/ui/auth/register.cljs:304, src/app/main/ui/auth/verify_token.cljs:94, src/app/main/ui/dashboard/team.cljs:199, src/app/main/ui/dashboard/team.cljs:861, src/app/main/ui/onboarding/team_choice.cljs:113, src/app/main/ui/settings/access_tokens.cljs:79, src/app/main/ui/settings/feedback.cljs:84 msgid "errors.generic" @@ -1538,7 +1538,7 @@ msgstr "Invitation non valide" #: src/app/main/ui/static.cljs:75 msgid "errors.invite-invalid.info" -msgstr "Cette invitation est peut-être été annulée ou a expiré." +msgstr "Cette invitation a peut-être été annulée ou est arrivée à expiration." #: src/app/main/ui/auth/login.cljs:89 msgid "errors.ldap-disabled" @@ -1553,7 +1553,7 @@ msgstr "" #: src/app/main/ui/dashboard/team.cljs:187, src/app/main/ui/dashboard/team.cljs:849, src/app/main/ui/onboarding/team_choice.cljs:101 msgid "errors.maximum-invitations-by-request-reached" msgstr "" -"Le nombre maximum (%s) d'e-mails qui peuvent être invités dans une seule " +"Le nombre maximal (%s) d'e-mails qui peuvent être invités dans une seule " "demande est atteint" #: src/app/main/data/workspace/media.cljs:190 @@ -1563,8 +1563,7 @@ msgstr "L’image est trop grande." #: src/app/main/data/media.cljs:70, src/app/main/data/workspace/media.cljs:193 msgid "errors.media-type-mismatch" msgstr "" -"Il semble que le contenu de l’image ne correspond pas à l’extension de " -"fichier." +"Le contenu de l’image semble ne pas correspondre à l’extension de fichier." #: src/app/main/data/media.cljs:67, src/app/main/data/workspace/media.cljs:178, src/app/main/data/workspace/media.cljs:181, src/app/main/data/workspace/media.cljs:184, src/app/main/data/workspace/media.cljs:187 msgid "errors.media-type-not-allowed" @@ -1622,7 +1621,7 @@ msgstr "Le fichier SVG n'est pas valide ou est mal formé" #: src/app/main/errors.cljs:270 msgid "errors.team-feature-mismatch" -msgstr "Fonctionnalité incompatible détectée '%s'" +msgstr "Fonctionnalité incompatible détectée « %s »" #: src/app/main/ui/dashboard/sidebar.cljs:373, src/app/main/ui/dashboard/team.cljs:393 msgid "errors.team-leave.insufficient-members" @@ -1632,17 +1631,17 @@ msgstr "" #: src/app/main/ui/dashboard/sidebar.cljs:376, src/app/main/ui/dashboard/team.cljs:396 msgid "errors.team-leave.member-does-not-exists" -msgstr "Le membre que vous essayez d'assigner n'existe pas." +msgstr "Le membre que vous essayez d'affecter n'existe pas." #: src/app/main/ui/dashboard/sidebar.cljs:379, src/app/main/ui/dashboard/team.cljs:399 msgid "errors.team-leave.owner-cant-leave" msgstr "" -"Le propriétaire ne peut pas quitter l'équipe, vous devez réassigner le rôle " +"Le propriétaire ne peut pas quitter l'équipe, vous devez réaffecter le rôle " "de propriétaire." #: src/app/main/ui/workspace/tokens/sets/helpers.cljs:26, src/app/main/ui/workspace/tokens/sets/helpers.cljs:45 msgid "errors.token-set-already-exists" -msgstr "Une collection avec le même nom existe déjà" +msgstr "Il existe déjà une collection portant ce nom" #: src/app/main/data/tokens.cljs: #, unused @@ -1652,12 +1651,12 @@ msgstr "Impossible de dupliquer une collection inconnue" #: src/app/main/data/workspace/tokens/library_edit.cljs:337 msgid "errors.token-set-exists-on-drop" msgstr "" -"Impossible de déposer, une collection avec le même nom existe déjà dans ce " +"Impossible de déposer. Il existe déjà une collection portant ce nom à ce " "chemin." #: src/app/main/data/workspace/tokens/library_edit.cljs:125, src/app/main/data/workspace/tokens/library_edit.cljs:144 msgid "errors.token-theme-already-exists" -msgstr "Une option de thème avec le même nom existe déjà" +msgstr "Il existe déjà une option de thème portant ce nom" #: src/app/main/data/media.cljs:73 msgid "errors.unexpected-error" @@ -2412,7 +2411,7 @@ msgstr "Événement" #: src/app/main/ui/dashboard/team.cljs:668 msgid "labels.expired-invitation" -msgstr "Expirée" +msgstr "Arrivée à expiration" #: src/app/main/ui/exports/assets.cljs:172, src/app/main/ui/workspace/tokens/sidebar.cljs:134 msgid "labels.export" @@ -2743,7 +2742,7 @@ msgstr "Supprimer" #: src/app/main/ui/dashboard/team.cljs:355 msgid "labels.remove-member" -msgstr "Retirer le membre" +msgstr "Supprimer le membre" #: src/app/main/ui/dashboard/file_menu.cljs:299, src/app/main/ui/dashboard/project_menu.cljs:88, src/app/main/ui/dashboard/sidebar.cljs:471, src/app/main/ui/workspace/sidebar/assets/groups.cljs:167, src/app/main/ui/workspace/sidebar/versions.cljs:192, src/app/main/ui/workspace/tokens/sets/context_menu.cljs:63 msgid "labels.rename" @@ -3553,7 +3552,7 @@ msgstr "" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs #, unused msgid "modals.remove-shared-confirm.message" -msgstr "Retirer « %s » en tant que bibliothèque partagée" +msgstr "Supprimer « %s » en tant que bibliothèque partagée" #: src/app/main/ui/workspace/nudge.cljs:52 msgid "modals.small-nudge" @@ -3568,14 +3567,14 @@ msgstr[1] "Dépublier" #: src/app/main/ui/delete_shared.cljs:50 msgid "modals.unpublish-shared-confirm.message" msgid_plural "modals.unpublish-shared-confirm.message" -msgstr[0] "Vous êtes sûr de vouloir retirer cette bibliothèque ?" -msgstr[1] "Vous êtes sûr de vouloir retirer ces bibliothèques ?" +msgstr[0] "Voulez-vous vraiment supprimer cette bibliothèque ?" +msgstr[1] "Voulez-vous vraiment supprimer ces bibliothèques ?" #: src/app/main/ui/delete_shared.cljs:45 msgid "modals.unpublish-shared-confirm.title" msgid_plural "modals.unpublish-shared-confirm.title" -msgstr[0] "Retirer la bibliothèque" -msgstr[1] "Retirer les bibliothèques" +msgstr[0] "Supprimer la bibliothèque" +msgstr[1] "Supprimer les bibliothèques" #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs #, unused @@ -3739,7 +3738,7 @@ msgstr "" #: src/app/main/ui/settings/options.cljs:27, src/app/main/ui/settings/profile.cljs:30 msgid "notifications.profile-saved" -msgstr "Profil enregistré avec succès !" +msgstr "Profil enregistré !" #: src/app/main/ui/settings/change_email.cljs:46 msgid "notifications.validation-email-sent" @@ -4696,11 +4695,11 @@ msgstr "Activer/désactiver les calques" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:190 msgid "shortcuts.toggle-layout-flex" -msgstr "Ajouter/supprimer flex layout" +msgstr "Ajouter/supprimer la disposition flex" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:191 msgid "shortcuts.toggle-layout-grid" -msgstr "Ajouter / Retirer grid layout" +msgstr "Ajouter/Supprimer la disposition en grille" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:192 msgid "shortcuts.toggle-lock" @@ -5056,8 +5055,8 @@ msgstr "Mettre à niveau votre abonnement" #, markdown msgid "subscription.workspace.versions.warning.enterprise.subtext-owner" msgstr "" -"Si vous souhaitez augmenter cette limite, écrivez-nous à l'adressse " -"[%s](mailto)" +"Si vous souhaitez augmenter cette limite, écrivez-nous à l'adresse [%s]" +"(mailto)" #: src/app/main/ui/workspace/sidebar/versions.cljs:59 #, markdown @@ -6424,7 +6423,7 @@ msgstr "Naviguer vers" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:52 msgid "workspace.options.interaction-navigate-to-dest" -msgstr "Naviguer vers : %s" +msgstr "Accéder à : %s" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:53, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:55, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:57, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:357 msgid "workspace.options.interaction-none" @@ -6444,7 +6443,7 @@ msgstr "Ouvrir la superposition" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:54 msgid "workspace.options.interaction-open-overlay-dest" -msgstr "Ouvrir la superposition : %s" +msgstr "Ouvrir la superposition : %s" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:61, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:351 msgid "workspace.options.interaction-open-url" @@ -6513,7 +6512,7 @@ msgstr "Activer/désactiver la superposition" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:56 msgid "workspace.options.interaction-toggle-overlay-dest" -msgstr "Activer/désactiver la superposition : %s" +msgstr "Activer/désactiver la superposition : %s" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:415 msgid "workspace.options.interaction-trigger" @@ -7498,7 +7497,7 @@ msgstr "Chemin" #: src/app/main/ui/workspace/context_menu.cljs:549 msgid "workspace.shape.menu.remove-flex" -msgstr "Retirer flex layout" +msgstr "Supprimer la disposition flex" #: src/app/main/ui/workspace/context_menu.cljs:552 msgid "workspace.shape.menu.remove-grid" @@ -7550,7 +7549,7 @@ msgstr "Afficher le composant principal" #: src/app/main/ui/workspace/context_menu.cljs:314 msgid "workspace.shape.menu.thumbnail-remove" -msgstr "Retirer la miniature" +msgstr "Supprimer la miniature" #: src/app/main/ui/workspace/context_menu.cljs:316 msgid "workspace.shape.menu.thumbnail-set" @@ -7721,7 +7720,7 @@ msgstr "En créer un." #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:235 msgid "workspace.tokens.create-token" -msgstr "Créer un nouveau token %s" +msgstr "Créer un token %s" #: src/app/main/ui/workspace/tokens/management/context_menu.cljs:353 msgid "workspace.tokens.delete" @@ -7753,7 +7752,7 @@ msgstr "Modifier les thèmes" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:234 msgid "workspace.tokens.edit-token" -msgstr "Modifier le token" +msgstr "Modifier le token %s" #: src/app/main/data/workspace/tokens/errors.cljs:41 msgid "workspace.tokens.empty-input" @@ -7892,9 +7891,8 @@ msgstr "Erreur lors de l'importation : nom du token non valide au format JSON." msgid "workspace.tokens.invalid-json-token-name-detail" msgstr "" "« %s » n'est pas un nom de token valide.\n" -"Les noms des tokens ne doivent pas comporter de lettres et de chiffres " -"séparés par des caractères « . » et ne doivent pas commencer par le symbole " -"« $ »." +"Les noms de token ne doivent pas comporter de lettres et de chiffres séparés " +"par des caractères « . » et ne doivent pas commencer par le symbole « $ »." #: src/app/main/data/workspace/tokens/errors.cljs:81 msgid "workspace.tokens.invalid-text-case-token-value" @@ -8132,7 +8130,7 @@ msgstr "" #: src/app/main/ui/workspace/tokens/style_dictionary.cljs:259 #, unused msgid "workspace.tokens.token-not-resolved" -msgstr "Impossible de trouver une référence de token ayant comme nom : %s" +msgstr "Impossible de trouver une référence de token portant le nom : %s" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:267 msgid "workspace.tokens.token-value" @@ -8144,7 +8142,7 @@ msgstr "Entrez une valeur ou un alias avec {alias}" #: src/app/main/ui/workspace/tokens/management.cljs:67 msgid "workspace.tokens.tokens-section-title" -msgstr "TOKENS - %s" +msgstr "TOKENS – %s" #: src/app/main/ui/workspace/tokens/sidebar.cljs:122 msgid "workspace.tokens.tools" @@ -8498,7 +8496,7 @@ msgstr "" #: src/app/main/ui/workspace/sidebar/versions.cljs:431 msgid "workspace.versions.warning.text" -msgstr "Les versions auto-enregistrées seront gardées %s jours." +msgstr "Les versions auto-enregistrées seront conservées %s jours." #, unused msgid "workspace.viewport.click-to-close-path" @@ -8555,3 +8553,101 @@ msgstr "Horizontal" #, fuzzy msgid "workspace.options.orientation.vertical" msgstr "Vertical" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:232 +msgid "workspace.tokens.shadow-add-shadow" +msgstr "Ajouter une ombre" + +#: src/app/main/data/workspace/tokens/errors.cljs:109 +msgid "workspace.tokens.shadow-blur-range" +msgstr "Le flou de l'ombre doit être supérieur ou égal à 0." + +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1215 +#, unused +msgid "workspace.tokens.shadow-title" +msgstr "Ombres" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:281 +msgid "workspace.tokens.shadow-token-blur-value-error" +msgstr "La valeur de flou ne peut pas être négative" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:287 +#, unused +msgid "workspace.tokens.shadow-token-spread-value-error" +msgstr "La valeur de la portée ne peut pas être négative" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:139, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:141 +#, fuzzy +msgid "workspace.tokens.shadow-x" +msgstr "X" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:150, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:152 +#, fuzzy +msgid "workspace.tokens.shadow-y" +msgstr "Y" + +#: src/app/main/data/workspace/tokens/errors.cljs:105 +msgid "workspace.tokens.invalid-shadow-type-token-value" +msgstr "" +"Type d'ombre non valide : seuls les types « innerShadow » ou « dropShadow » " +"sont acceptés" + +#: src/app/main/data/workspace/tokens/errors.cljs:117 +msgid "workspace.tokens.invalid-token-value-shadow" +msgstr "Valeur non valide : doit référencer un token d'ombre composite." + +#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:303 +msgid "workspace.tokens.missing-reference" +msgstr "Référence manquante" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:89 +msgid "workspace.tokens.remap-warning-time" +msgstr "Cette action pourrait prendre un instant." + +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988 +#, unused +msgid "workspace.tokens.shadow-color" +msgstr "Couleur" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:123 +msgid "workspace.tokens.shadow-remove-shadow" +msgstr "Supprimer l'ombre" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:173, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:174, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:177 +msgid "workspace.tokens.shadow-spread" +msgstr "Portée" + +#: src/app/main/data/workspace/tokens/errors.cljs:113 +msgid "workspace.tokens.shadow-spread-range" +msgstr "La portée de l'ombre doit être supérieure ou égale à 0." + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:51 +#, unused +msgid "workspace.tokens.theme-name-already-exists" +msgstr "Il existe déjà un thème portant ce nom" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:100 +#, unused +msgid "workspace.tokens.theme.disable" +msgstr "Désactiver" + +#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:96 +#, unused +msgid "workspace.tokens.theme.enable" +msgstr "Activer" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:268, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:223 +msgid "workspace.tokens.token-name-duplication-validation-error" +msgstr "Il existe déjà un token au chemin : %s" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:265, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:220 +msgid "workspace.tokens.token-name-length-validation-error" +msgstr "Le nom doit comporter au moins 1 caractère" + +#: src/app/main/ui/workspace/top_toolbar.cljs:231 +msgid "workspace.toolbar.debug" +msgstr "Outils de débogage" + +#: src/app/main/ui/static.cljs:315 +msgid "errors.webgl-context-lost.desc-message" +msgstr "WebGL ne fonctionne plus. Rechargez la page pour le réinitialiser" From a206d57443b3a36af87cb421806f538f54da9f83 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Wed, 15 Apr 2026 22:31:44 +0200 Subject: [PATCH 161/288] :sparkles: Add team to a nitrate organization --- backend/src/app/nitrate.clj | 4 +- backend/src/app/rpc/commands/nitrate.clj | 80 +++++++++++-- backend/src/app/rpc/commands/teams.clj | 37 +++--- .../test/backend_tests/rpc_nitrate_test.clj | 5 +- frontend/src/app/main/data/nitrate.cljs | 18 ++- frontend/src/app/main/data/team.cljs | 9 ++ .../app/main/ui/components/org_avatar.cljs | 13 +- .../app/main/ui/components/org_avatar.scss | 5 + .../src/app/main/ui/dashboard/sidebar.cljs | 41 +++---- frontend/src/app/main/ui/dashboard/team.cljs | 113 +++++++++++++++++- frontend/src/app/main/ui/dashboard/team.scss | 31 +++++ .../src/app/main/ui/ds/controls/combobox.cljs | 34 ++++-- .../src/app/main/ui/ds/controls/combobox.mdx | 33 +++++ .../src/app/main/ui/ds/controls/combobox.scss | 5 + .../main/ui/ds/controls/shared/option.cljs | 17 ++- .../main/ui/ds/controls/shared/option.scss | 5 + .../ds/controls/shared/options_dropdown.cljs | 5 + .../ui/ds/controls/shared/render_option.cljs | 1 + frontend/translations/en.po | 18 +++ frontend/translations/es.po | 18 +++ 20 files changed, 414 insertions(+), 78 deletions(-) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 48374660e0..9aaff500a3 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -131,8 +131,8 @@ (def ^:private schema:profile-org [:map [:is-member :boolean] - [:organization-id ::sm/uuid] - [:default-team-id [:maybe ::sm/uuid]]]) + [:organization-id {:optional true} [:maybe ::sm/uuid]] + [:default-team-id {:optional true} [:maybe ::sm/uuid]]]) ;; TODO Unify with schemas on backend/src/app/http/management.clj diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index 71eaedb2b0..62d25a781b 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -12,6 +12,31 @@ [app.util.services :as sv])) + +(defn assert-is-owner [cfg profile-id team-id] + (let [perms (teams/get-permissions cfg profile-id team-id)] + (when-not (:is-owner perms) + (ex/raise :type :validation + :code :insufficient-permissions)))) + +(defn assert-not-default-team [cfg team-id] + (let [team (teams/get-team-info cfg {:id team-id})] + (when (:is-default team) + (ex/raise :type :validation + :code :cant-move-default-team)))) + +(defn assert-membership [cfg profile-id organization-id] + (let [membership (nitrate/call cfg :get-org-membership {:profile-id profile-id + :org-id organization-id})] + (when-not (:organization-id membership) + (ex/raise :type :validation + :code :organization-doesnt-exists)) + + (when-not (:is-member membership) + (ex/raise :type :validation + :code :user-doesnt-belong-organization)))) + + (def schema:connectivity [:map {:title "nitrate-connectivity"} [:licenses ::sm/boolean]]) @@ -151,6 +176,8 @@ (ex/raise :type :validation :code :not-valid-teams)) + (assert-membership cfg profile-id org-id) + ;; delete the teams-to-delete (doseq [id teams-to-delete] (teams/delete-team cfg {:profile-id profile-id :team-id id})) @@ -175,25 +202,52 @@ (def ^:private schema:remove-team-from-org [:map [:team-id ::sm/uuid] - [:organization-id ::sm/uuid]]) + [:organization-id ::sm/uuid] + [:organization-name ::sm/text]]) (sv/defmethod ::remove-team-from-org {::doc/added "2.16" ::sm/params schema:remove-team-from-org} [cfg {:keys [::rpc/profile-id team-id organization-id organization-name]}] - (let [perms (teams/get-permissions cfg profile-id team-id) - team (teams/get-team-info cfg {:id team-id})] - (when-not (:is-owner perms) - (ex/raise :type :validation - :code :insufficient-permissions)) + (assert-is-owner cfg profile-id team-id) + (assert-not-default-team cfg team-id) + (assert-membership cfg profile-id organization-id) - (when (:is-default team) - (ex/raise :type :validation - :code :cant-remove-default-team)) + ;; Api call to nitrate + (nitrate/call cfg :remove-team-from-org {:team-id team-id :organization-id organization-id}) - ;; Api call to nitrate - (nitrate/call cfg :remove-team-from-org {:team-id team-id :organization-id organization-id}) + ;; Notify connected users + (notifications/notify-team-change cfg team-id nil nil organization-name "dashboard.team-no-longer-belong-org") + nil) - (notifications/notify-team-change cfg team-id nil nil organization-name "dashboard.team-no-longer-belong-org") - nil)) \ No newline at end of file + +(def ^:private schema:add-team-to-org + [:map + [:team-id ::sm/uuid] + [:organization-id ::sm/uuid] + [:organization-name ::sm/text]]) + +(sv/defmethod ::add-team-to-org + {::rpc/auth true + ::doc/added "2.16" + ::sm/params schema:add-team-to-org + ::db/transaction true} + [cfg {:keys [::rpc/profile-id team-id organization-id organization-name]}] + + (assert-is-owner cfg profile-id team-id) + (assert-not-default-team cfg team-id) + (assert-membership cfg profile-id organization-id) + + (let [team-members (db/query cfg :team-profile-rel {:team-id team-id})] + ;; Add teammates to the org if needed + (doseq [{member-id :profile-id} team-members + :when (not= member-id profile-id)] + (teams/initialize-user-in-nitrate-org cfg member-id organization-id))) + + ;; Api call to nitrate + (nitrate/call cfg :set-team-org {:team-id team-id :organization-id organization-id :is-default false}) + + ;; Notify connected users + (notifications/notify-team-change cfg team-id nil organization-id organization-name "dashboard.team-belong-org") + nil) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 9b089e3b2c..722b48bb61 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -551,20 +551,29 @@ (db/tx-run! cfg (fn [{:keys [::db/conn] :as tx-cfg}] - (let [org-id org-id - default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id org-id) - default-team-id (:id default-team) - result (nitrate/call tx-cfg :add-profile-to-org (cond-> {:profile-id profile-id - :team-id default-team-id - :org-id org-id} - (some? email) (assoc :email email)))] - (when (not (:is-member result)) - (ex/raise :type :internal - :code :failed-add-profile-org-nitrate - :context {:profile-id profile-id - :org-id org-id - :default-team-id default-team-id})) - default-team-id)))))) + + (let [membership (nitrate/call cfg :get-org-membership {:profile-id profile-id + :org-id org-id})] + ;; Only when the user doesn't belong to the organization yet + (when (and + (some? (:organization-id membership)) ;; the organization exists + (not (:is-member membership))) ;; the user is not a member of the org yet + + + (let [org-id org-id + default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id org-id) + default-team-id (:id default-team) + result (nitrate/call tx-cfg :add-profile-to-org (cond-> {:profile-id profile-id + :team-id default-team-id + :org-id org-id} + (some? email) (assoc :email email)))] + (when (not (:is-member result)) + (ex/raise :type :internal + :code :failed-add-profile-org-nitrate + :context {:profile-id profile-id + :org-id org-id + :default-team-id default-team-id})) + default-team-id)))))))) (defn add-profile-to-team! ([cfg params] diff --git a/backend/test/backend_tests/rpc_nitrate_test.clj b/backend/test/backend_tests/rpc_nitrate_test.clj index d098013aa5..084c8417a8 100644 --- a/backend/test/backend_tests/rpc_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_nitrate_test.clj @@ -33,11 +33,14 @@ (defn- nitrate-call-mock "Creates a mock for nitrate/call that returns the given org-summary for - :get-org-summary and nil for any other method." + :get-org-summary, a valid membership for :get-org-membership, and nil for + any other method." [org-summary] (fn [_cfg method _params] (case method :get-org-summary org-summary + :get-org-membership {:is-member true + :organization-id (:id org-summary)} nil))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index ba7124a5a2..c6ece1a5fe 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -90,13 +90,13 @@ (dm/get-in profile [:subscription :status])))) (defn leave-org - [{:keys [org-id org-name default-team-id teams-to-delete teams-to-leave on-error] :as params}] + [{:keys [id org-name default-team-id teams-to-delete teams-to-leave on-error] :as params}] (ptk/reify ::leave-org ptk/WatchEvent (watch [_ state _] (let [profile-team-id (dm/get-in state [:profile :default-team-id])] - (->> (rp/cmd! ::leave-org {:org-id org-id + (->> (rp/cmd! ::leave-org {:org-id id :org-name org-name :default-team-id default-team-id :teams-to-delete teams-to-delete @@ -121,5 +121,15 @@ (->> (rp/cmd! ::remove-team-from-org {:team-id team-id :organization-id organization-id :organization-name organization-name}) (rx/mapcat (fn [_] - (rx/of - (modal/hide)))))))) \ No newline at end of file + (rx/of (modal/hide)))))))) + + +(defn add-team-to-org + [{:keys [team-id organization-id organization-name] :as params}] + (ptk/reify ::add-team-to-org + ptk/WatchEvent + (watch [_ _ _] + (->> (rp/cmd! ::add-team-to-org {:team-id team-id :organization-id organization-id :organization-name organization-name}) + (rx/mapcat + (fn [_] + (rx/of (modal/hide)))))))) \ No newline at end of file diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs index 0756e71823..57fb1299ca 100644 --- a/frontend/src/app/main/data/team.cljs +++ b/frontend/src/app/main/data/team.cljs @@ -588,3 +588,12 @@ (rx/map shared-files-fetched))))))) +(defn team->organization [team] + {:id (:organization-id team) + :slug (:organization-slug team) + :owner-id (:organization-owner-id team) + :avatar-bg-url (:organization-avatar-bg-url team) + :custom-photo (:organization-custom-photo team) + :name (:organization-name team) + :default-team-id (:id team)}) + diff --git a/frontend/src/app/main/ui/components/org_avatar.cljs b/frontend/src/app/main/ui/components/org_avatar.cljs index c4430af12d..43521a1dd7 100644 --- a/frontend/src/app/main/ui/components/org_avatar.cljs +++ b/frontend/src/app/main/ui/components/org_avatar.cljs @@ -14,8 +14,8 @@ {::mf/props :obj} [{:keys [org size]}] (let [name (:name org) - custom-photo (:organization-custom-photo org) - avatar-bg (:organization-avatar-bg-url org) + custom-photo (:custom-photo org) + avatar-bg (:avatar-bg-url org) initials (d/get-initials name)] (if custom-photo @@ -23,11 +23,13 @@ :class (stl/css-case :org-avatar true :org-avatar-custom true :org-avatar-xxxl (= size "xxxl") - :org-avatar-xxl (= size "xxl")) + :org-avatar-xxl (= size "xxl") + :org-avatar-xl (= size "xl")) :alt name}] [:div {:class (stl/css-case :org-avatar true :org-avatar-xxxl (= size "xxxl") - :org-avatar-xxl (= size "xxl")) + :org-avatar-xxl (= size "xxl") + :org-avatar-xl (= size "xl")) :aria-hidden "true"} [:img {:src avatar-bg :class (stl/css :org-avatar-bg) @@ -35,5 +37,6 @@ (when (seq initials) [:span {:class (stl/css-case :org-avatar-initials true :size-initials-xxxl (= size "xxxl") - :size-initials-xxl (= size "xxl"))} + :size-initials-xxl (= size "xxl") + :size-initials-xxl (= size "xl"))} ;; Keep the initials as xxl to make them legible initials])]))) diff --git a/frontend/src/app/main/ui/components/org_avatar.scss b/frontend/src/app/main/ui/components/org_avatar.scss index ab7ee242a1..b72591568b 100644 --- a/frontend/src/app/main/ui/components/org_avatar.scss +++ b/frontend/src/app/main/ui/components/org_avatar.scss @@ -28,6 +28,11 @@ height: var(--sp-xxl); } +.org-avatar-xl { + width: var(--sp-xl); + height: var(--sp-xl); +} + .org-avatar-bg { position: absolute; inset: 0; diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 9e8de37d3e..3ae14e1281 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -322,18 +322,16 @@ (mf/deps organization profile) (fn [] ;; Navigate to active org if user owns it, otherwise to last visited org - (if (and (:organization-id organization) - (= (:id profile) (:organization-owner-id organization))) + (if (and (:id organization) + (= (:id profile) (:owner-id organization))) (dnt/go-to-nitrate-cc organization) (dnt/go-to-nitrate-cc)))) - default-team-id (or (->> organizations - vals - (filter :is-default) - first - :id) + empty-org (d/seek #(nil? (:id %)) organizations) + default-team-id (or (:default-team-id empty-org) (:default-team-id profile)) - organizations (dissoc organizations default-team-id) + + organizations (filter :id organizations) is-valid-license? (dnt/is-valid-license? profile)] @@ -348,15 +346,15 @@ (when (= default-team-id (:default-team-id organization)) tick-icon)] - (for [org-item (remove :is-default (vals organizations))] + (for [org-item organizations] [:> dropdown-menu-item* {:on-click on-org-click - :data-value (:id org-item) + :data-value (:default-team-id org-item) :class (stl/css :org-dropdown-item) - :key (str (:id org-item))} + :key (str (:default-team-id org-item))} [:> org-avatar* {:org org-item :size "xxl"}] [:span {:class (stl/css :team-text) :title (:name org-item)} (:name org-item)] - (when (= (:id org-item) (:default-team-id organization)) + (when (= (:default-team-id org-item) (:default-team-id organization)) tick-icon)]) [:hr {:role "separator" :class (stl/css :team-separator)}] @@ -642,7 +640,7 @@ (concat teams-to-transfer)) teams-to-delete (map :id teams-to-delete)] - (st/emit! (dnt/leave-org {:org-id (:organization-id organization) + (st/emit! (dnt/leave-org {:id (:id organization) :default-team-id default-team-id :teams-to-delete teams-to-delete :teams-to-leave teams-to-leave @@ -691,11 +689,6 @@ (tr "dashboard.leave-org")]])) -(defn- team->org [team] - (assoc (dm/select-keys team [:id :organization-id :organization-slug :organization-owner-id :organization-avatar-bg-url]) - :name (:organization-name team) - :default-team-id (:id team))) - (mf/defc sidebar-org-switch* [{:keys [team profile]}] (let [teams (mf/deref refs/teams) @@ -705,20 +698,20 @@ (->> teams vals (filter :is-default) - (map team->org) + (map dtm/team->organization) (d/index-by :id))) show-dropdown? (or (dnt/is-valid-license? profile) (> (count orgs) 1)) - current-org (team->org team) + current-org (dtm/team->organization team) org-teams (mf/with-memo [teams current-org] (->> teams vals - (filter #(= (:organization-id %) (:organization-id current-org))))) + (filter #(= (:organization-id %) (:id current-org))))) - default-org? (nil? (:organization-id current-org)) + default-org? (nil? (:id current-org)) show-orgs-menu* (mf/use-state false) @@ -787,7 +780,7 @@ (:name current-org)]])] arrow-icon] (if (or default-org? - (= (:id profile) (:organization-owner-id current-org))) + (= (:id profile) (:owner-id current-org))) [:div {:class (stl/css :org-options)}] [:> button* {:variant "ghost" :type "button" @@ -803,7 +796,7 @@ :class (stl/css :dropdown :teams-dropdown) :organization current-org :profile profile - :organizations orgs}] + :organizations (vals orgs)}] ;; Orgs options [:> org-options-dropdown* {:show show-org-options-menu? :on-close close-org-options-menu diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 6dc642c40d..2643916289 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -30,11 +30,13 @@ [app.main.ui.dashboard.team-form] [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.controls.combobox :refer [combobox*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.icons :as deprecated-icon] [app.main.ui.notifications.badge :refer [badge-notification]] [app.main.ui.notifications.context-notification :refer [context-notification]] [app.util.dom :as dom] + [app.util.forms :as uforms] [app.util.i18n :as i18n :refer [tr]] [beicon.v2.core :as rx] [cuerdas.core :as str] @@ -792,6 +794,77 @@ (tr "labels.continue") (tr "labels.resend"))]]]]]) + +(def schema:organization-form [:map {:title "SelectOrgForm"} + [:selected-id ::sm/uuid]]) + +(mf/defc render-org-combobox-avatar* + [{:keys [avatar]}] + [:> org-avatar* {:org (:organization avatar) + :size (:size avatar)}]) + +(mf/defc select-organization-modal + {::mf/register modal/components + ::mf/register-as :select-organization-modal} + [{:keys [organizations on-confirm]}] + (let [options (mf/with-memo [organizations] + (mapv (fn [organization] + {:id (str (:id organization)) + :label (:name organization) + :avatar {:render-fn render-org-combobox-avatar* + :organization organization + :size "xl"}}) + organizations)) + + form (fm/use-form :schema schema:organization-form :initial {}) + + on-change + (mf/use-fn + (mf/deps form) + (fn [id] + (uforms/on-input-change form :selected-id id))) + + on-confirm' + (mf/use-fn + (mf/deps on-confirm form) + (fn [] + (on-confirm (dm/get-in @form [:clean-data :selected-id]))))] + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-select-org-container :modal-container)} + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-select-org-title)} + (tr "dashboard.select-org-modal.title")] + + [:button {:class (stl/css :modal-close-btn) + :on-click modal/hide!} deprecated-icon/close]] + + [:div + [:div {:class (stl/css :modal-select-org-content)} + (tr "dashboard.select-org-modal.choose")] + [:> combobox* {:id "selected-id" + :class (stl/css :team-member) + :options options + :default-selected (or (some-> (get-in @form [:data :selected-id]) str) "") + :placeholder (tr "dashboard.select-org-modal.select") + :on-change on-change}]] + + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons :modal-invitation-action-buttons)} + + [:> button* + {:class (stl/css :cancel-button) + :variant "secondary" + :type "button" + :on-click modal/hide!} + (tr "labels.cancel")] + [:> button* + {:class (stl/css :accept-btn) + :variant "primary" + :type "button" + :disabled (not (:valid @form)) + :on-click on-confirm'} + (tr "dashboard.select-org-modal.accept")]]]]])) + (mf/defc invitation-section* {::mf/props :obj ::mf/private true} @@ -1291,6 +1364,13 @@ can-edit (or (:is-owner permissions) (:is-admin permissions)) + organizations (mf/deref refs/teams) + organizations (mf/with-memo [organizations] + (->> (vals organizations) + (filter :is-default) + (filter :organization-id) + (map dtm/team->organization))) + show-org-options-menu* (mf/use-state false) @@ -1333,7 +1413,24 @@ :accept-label (tr "modals.remove-team-org.accept") :on-accept remove-team-from-org-fn :accept-style :danger}] - (st/emit! (modal/show params)))))] + (st/emit! (modal/show params))))) + + on-add-team-to-org-confirm + (mf/use-fn + (mf/deps team) + (fn [organization-id] + (let [organization (d/seek #(= organization-id (:id %)) organizations)] + (when organization + (st/emit! (dnt/add-team-to-org {:team-id (:id team) + :organization-id organization-id + :organization-name (:name organization)})))))) + + on-add-team-to-org + (mf/use-fn + (mf/deps organizations on-add-team-to-org-confirm) + (fn [] + (st/emit! (modal/show :select-organization-modal {:organizations organizations + :on-confirm on-add-team-to-org-confirm}))))] (mf/with-effect [team] (dom/set-html-title (tr "title.team-settings" @@ -1374,7 +1471,7 @@ (if (:organization-id team) [:div {:class (stl/css :block-content)} [:div {:class (stl/css :org-block-content)} - [:> org-avatar* {:org team :size "xxxl"}] + [:> org-avatar* {:org (dtm/team->organization team) :size "xxxl"}] [:span {:class (stl/css :block-text)} (:organization-name team)] @@ -1392,9 +1489,15 @@ [:li {:on-click on-remove-team-from-org :class (stl/css :org-dropdown-item)} (tr "dashboard.team-organization.remove")]]]]])]] - [:div {:class (stl/css :block-content)} - [:span {:class (stl/css :block-text)} - (tr "dashboard.team-organization.none")]])]) + [:* + [:div {:class (stl/css :block-content)} + [:span {:class (stl/css :block-text)} + (tr "dashboard.team-organization.none")]] + (when (and (pos? (count organizations)) + (not (:is-default team))) + [:div {:class (stl/css :block-content)} + [:span {:class (stl/css :block-text)} + [:a {:on-click on-add-team-to-org} (tr "dashboard.team-organization.add")]]])])]) [:div {:class (stl/css :block)} [:div {:class (stl/css :block-label)} diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss index b04c895bdd..73e8b56f6a 100644 --- a/frontend/src/app/main/ui/dashboard/team.scss +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -871,6 +871,33 @@ gap: var(--sp-s); } +// SELECT ORGANIZATION MODAL + +.modal-select-org-container { + overflow: hidden; + display: flex; + flex-direction: column; + width: $sz-512; +} + +.modal-select-org-content { + @include t.use-typography("body-medium"); + + color: var(--color-foreground-secondary); + overflow: auto; + margin-block-end: var(--sp-s); +} + +.modal-select-org-title { + @include t.use-typography("title-medium"); + + color: var(--color-foreground-primary); + text-transform: uppercase; + height: $sz-40; +} + +// ORGANIZATIONS SETTINGS + .org-block-content { display: grid; grid-template-columns: var(--sp-xxxl) 1fr var(--sp-xxxl); @@ -948,3 +975,7 @@ background-color: var(--color-background-quaternary); } } + +a { + color: var(--modal-link-foreground-color); +} diff --git a/frontend/src/app/main/ui/ds/controls/combobox.cljs b/frontend/src/app/main/ui/ds/controls/combobox.cljs index f8fcc566b6..c5a74811d2 100644 --- a/frontend/src/app/main/ui/ds/controls/combobox.cljs +++ b/frontend/src/app/main/ui/ds/controls/combobox.cljs @@ -10,7 +10,7 @@ (:require [app.common.data :as d] [app.main.constants :refer [max-input-length]] - [app.main.ui.ds.controls.select :refer [get-option handle-focus-change]] + [app.main.ui.ds.controls.select :refer [handle-focus-change]] [app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown* schema:option]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.util.dom :as dom] @@ -67,7 +67,7 @@ (mf/with-memo [options filter-id] (->> options (filterv (fn [option] - (let [option (str/lower (get option :id)) + (let [option (str/lower (get option :label)) filter (str/lower filter-id)] (str/includes? option filter)))) (not-empty))) @@ -113,7 +113,7 @@ on-blur (mf/use-fn - (mf/deps on-change) + (mf/deps on-change options selected-id) (fn [event] (dom/stop-propagation event) (let [target (dom/get-related-target event) @@ -123,7 +123,12 @@ (reset! focused-id* nil) (when (fn? on-change) (when-let [input-node (mf/ref-val input-ref)] - (on-change (dom/get-input-value input-node)))))))) + (let [input-value (dom/get-input-value input-node) + selected-option (d/seek #(= selected-id (get % :id)) options) + value (if (some? selected-option) + selected-id + input-value)] + (on-change value)))))))) on-input-click (mf/use-fn @@ -209,11 +214,19 @@ selected-option (mf/with-memo [options selected-id] (when (d/not-empty? options) - (get-option options selected-id))) + (d/seek #(= selected-id (get % :id)) options))) icon (when selected-option - (get selected-option :icon))] + (get selected-option :icon)) + + avatar + (when selected-option + (get selected-option :avatar)) + + render-avatar-fn + (when avatar + (get avatar :render-fn))] (mf/with-effect [dropdown-options] (mf/set-ref-val! options-ref dropdown-options)) @@ -241,11 +254,14 @@ :on-click on-click} [:span {:class (stl/css-case :header true - :header-icon (some? icon))} + :header-icon (some? icon) + :header-avatar (fn? render-avatar-fn))} (when icon [:> icon* {:icon-id icon :size "s" :aria-hidden true}]) + (when (fn? render-avatar-fn) + [:> render-avatar-fn {:avatar avatar}]) [:input {:id id :ref input-ref :type "text" @@ -259,7 +275,9 @@ :data-testid "combobox-input" :max-length (d/nilv max-length max-input-length) :disabled disabled - :value (d/nilv selected-id "") + :value (if (str/empty? (:id selected-option)) + (d/nilv selected-id "") + (d/nilv (:label selected-option) "")) :placeholder placeholder :on-change on-input-change :on-click on-input-click diff --git a/frontend/src/app/main/ui/ds/controls/combobox.mdx b/frontend/src/app/main/ui/ds/controls/combobox.mdx index eff4b6b18d..58075b8aed 100644 --- a/frontend/src/app/main/ui/ds/controls/combobox.mdx +++ b/frontend/src/app/main/ui/ds/controls/combobox.mdx @@ -53,6 +53,39 @@ These are available in the `app.main.ds.foundations.assets.icon` namespace. ]}] ``` + +### Avatars + +Each option of `combobox*` also accepts an optional `avatar` map. +Avatar rendering is defined per option with `:render-fn`, so each avatar type can provide its own UI. The renderer should be a component function that receives the full `avatar` map. + +```clj +;; Example renderer for organization avatars +(mf/defc render-org-avatar* + [{:keys [avatar]}] + (when (= :organization (:type avatar)) + [:> org-avatar* {:org (:organization avatar) + :size (:size avatar)}])) + +[:> combobox* + {:options [{:label "Design Team" + :id "org-design" + :avatar {:render-fn render-org-avatar* + :size "s" + :organization {:name "Design Team" + :organization-avatar-bg-url "https://example.com/avatar-bg.svg" + :organization-custom-photo nil}}} + {:label "Engineering" + :id "org-engineering" + :avatar {:render-fn render-org-avatar* + :size "s" + :organization {:name "Engineering" + :organization-avatar-bg-url nil + :organization-custom-photo "https://example.com/custom-photo.png"}}}]}] +``` + +The same pattern can be used later for other avatar kinds, for example `:team`, by adding a different `:render-fn` in those options. + ## Usage guidelines (design) ### Where to Use diff --git a/frontend/src/app/main/ui/ds/controls/combobox.scss b/frontend/src/app/main/ui/ds/controls/combobox.scss index 70ad818514..519243a8fb 100644 --- a/frontend/src/app/main/ui/ds/controls/combobox.scss +++ b/frontend/src/app/main/ui/ds/controls/combobox.scss @@ -64,6 +64,11 @@ color: var(--combobox-icon-color); } +.header-avatar { + grid-template-columns: auto 1fr; + gap: var(--sp-s); +} + .input { all: unset; diff --git a/frontend/src/app/main/ui/ds/controls/shared/option.cljs b/frontend/src/app/main/ui/ds/controls/shared/option.cljs index 0542268bc1..b313bf4263 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/option.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/option.cljs @@ -22,6 +22,12 @@ [:focused {:optional true} :boolean] [:dimmed {:optional true} :boolean] [:label {:optional true} :string] + [:avatar {:optional true} + [:maybe + [:map + [:size {:optional true} :string] + [:organization {:optional true} :any] + [:render-fn {:optional true} fn?]]]] [:aria-label {:optional true} [:maybe :string]] [:on-click {:optional true} fn?]] [:fn {:error/message "invalid data: missing required props"} @@ -33,9 +39,13 @@ (mf/defc option* {::mf/schema schema:option} - [{:keys [id ref label icon aria-label on-click selected focused dimmed] :rest props}] - (let [class (stl/css-case :option true + [{:keys [id ref label icon avatar aria-label on-click selected focused dimmed] :rest props}] + (let [render-avatar-fn (when avatar + (get avatar :render-fn)) + + class (stl/css-case :option true :option-with-icon (some? icon) + :option-with-avatar (fn? render-avatar-fn) :option-selected selected :option-current focused)] @@ -57,6 +67,9 @@ :aria-hidden (when label true) :aria-label (when (not label) aria-label)}]) + (when (fn? render-avatar-fn) + [:> render-avatar-fn {:avatar avatar}]) + [:span {:class (stl/css-case :option-text true :option-text-dimmed dimmed)} label] diff --git a/frontend/src/app/main/ui/ds/controls/shared/option.scss b/frontend/src/app/main/ui/ds/controls/shared/option.scss index bc693a14d4..2b3e749770 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/option.scss +++ b/frontend/src/app/main/ui/ds/controls/shared/option.scss @@ -36,6 +36,11 @@ grid-template-columns: auto 1fr auto; } +.option-with-avatar { + grid-template-columns: auto 1fr auto; + gap: var(--sp-s); +} + .option-text { white-space: nowrap; overflow: hidden; diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs index e3f7b778d7..e868e4ab2d 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs @@ -37,6 +37,11 @@ [:value {:optional true} :keyword] [:icon {:optional true} schema:icon-list] [:label {:optional true} :string] + [:avatar {:optional true} + [:map + [:size {:optional true} :string] + [:organization {:optional true} :any] + [:render-fn {:optional true} fn?]]] [:aria-label {:optional true} :string]]) (def ^:private schema:options-dropdown diff --git a/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs b/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs index b3dec0b710..8237f63f81 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs @@ -60,6 +60,7 @@ :label (get option :label) :aria-label (get option :aria-label) :icon (get option :icon) + :avatar (get option :avatar) :ref ref :role "option" :focused (= id focused) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 9715b7e27b..3836f109e6 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -347,6 +347,9 @@ msgstr "The %s organization has been deleted." msgid "dashboard.team-no-longer-belong-org" msgstr "This team no longer belongs to the organization %s" +msgid "dashboard.team-belong-org" +msgstr "This team now belongs to %s" + #: src/app/main/ui/dashboard/placeholder.cljs:41 msgid "dashboard.add-file" msgstr "Add file" @@ -1135,6 +1138,21 @@ msgstr "Team organization" msgid "dashboard.team-organization.none" msgstr "This team is not part of any organization" +msgid "dashboard.team-organization.add" +msgstr "Add to an organization" + +msgid "dashboard.select-org-modal.title" +msgstr "Add team to an organization" + +msgid "dashboard.select-org-modal.choose" +msgstr "Choose an organization:" + +msgid "dashboard.select-org-modal.select" +msgstr "Select an organization" + +msgid "dashboard.select-org-modal.accept" +msgstr "ADD TO ORGANIZATION" + msgid "dashboard.team-organization.change" msgstr "Change team organization" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 09779522cc..c150221253 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -356,6 +356,9 @@ msgstr "La organización %s se ha borrado." msgid "dashboard.team-no-longer-belong-org" msgstr "Este equipo ya no pertenece a la organización %s" +msgid "dashboard.team-belong-org" +msgstr "Este equipo ahora pertenece a la organización %s" + #: src/app/main/ui/dashboard/placeholder.cljs:41 msgid "dashboard.add-file" msgstr "Añadir archivo" @@ -1139,6 +1142,21 @@ msgstr "Organización del equipo" msgid "dashboard.team-organization.none" msgstr "Este equipo no pertenece a ninguna organización" +msgid "dashboard.team-organization.add" +msgstr "Añadir a una organización" + +msgid "dashboard.select-org-modal.title" +msgstr "Añadir el equipo a una organización" + +msgid "dashboard.select-org-modal.choose" +msgstr "Elige una organización:" + +msgid "dashboard.select-org-modal.select" +msgstr "Elige una organización" + +msgid "dashboard.select-org-modal.accept" +msgstr "AÑADIR A UNA ORGANIZACIÓN" + msgid "dashboard.team-organization.change" msgstr "Cambiar el equipo de organización" From c5a2b592a28222937cdfd33f6a4297451a938cca Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Fri, 17 Apr 2026 11:21:16 +0200 Subject: [PATCH 162/288] :sparkles: Move team to another nitrate organization --- frontend/src/app/main/ui/dashboard/team.cljs | 53 ++++++++++++++++---- frontend/translations/en.po | 12 +++++ frontend/translations/es.po | 12 +++++ 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 2643916289..1813fa819a 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -806,15 +806,17 @@ (mf/defc select-organization-modal {::mf/register modal/components ::mf/register-as :select-organization-modal} - [{:keys [organizations on-confirm]}] - (let [options (mf/with-memo [organizations] + [{:keys [organizations current-organization-id on-confirm title-key choose-key placeholder-key accept-key cancel-key]}] + (let [valid-organizations (mf/with-memo [organizations] + (remove #(= (:id %) current-organization-id) organizations)) + options (mf/with-memo [valid-organizations] (mapv (fn [organization] {:id (str (:id organization)) :label (:name organization) :avatar {:render-fn render-org-combobox-avatar* :organization organization :size "xl"}}) - organizations)) + valid-organizations)) form (fm/use-form :schema schema:organization-form :initial {}) @@ -833,19 +835,19 @@ [:div {:class (stl/css :modal-select-org-container :modal-container)} [:div {:class (stl/css :modal-header)} [:h2 {:class (stl/css :modal-select-org-title)} - (tr "dashboard.select-org-modal.title")] + (tr title-key)] [:button {:class (stl/css :modal-close-btn) :on-click modal/hide!} deprecated-icon/close]] [:div [:div {:class (stl/css :modal-select-org-content)} - (tr "dashboard.select-org-modal.choose")] + (tr choose-key)] [:> combobox* {:id "selected-id" :class (stl/css :team-member) :options options :default-selected (or (some-> (get-in @form [:data :selected-id]) str) "") - :placeholder (tr "dashboard.select-org-modal.select") + :placeholder (tr placeholder-key) :on-change on-change}]] [:div {:class (stl/css :modal-footer)} @@ -856,14 +858,14 @@ :variant "secondary" :type "button" :on-click modal/hide!} - (tr "labels.cancel")] + (tr cancel-key)] [:> button* {:class (stl/css :accept-btn) :variant "primary" :type "button" :disabled (not (:valid @form)) :on-click on-confirm'} - (tr "dashboard.select-org-modal.accept")]]]]])) + (tr accept-key)]]]]])) (mf/defc invitation-section* {::mf/props :obj @@ -1371,6 +1373,13 @@ (filter :organization-id) (map dtm/team->organization))) + can-change-organization? (mf/with-memo [organizations] + (> (count organizations) 1)) + + can-add-to-organization? (mf/with-memo [organizations] + (and (pos? (count organizations)) + (not (:is-default team)))) + show-org-options-menu* (mf/use-state false) @@ -1430,7 +1439,26 @@ (mf/deps organizations on-add-team-to-org-confirm) (fn [] (st/emit! (modal/show :select-organization-modal {:organizations organizations - :on-confirm on-add-team-to-org-confirm}))))] + :current-organization-id (:organization-id team) + :on-confirm on-add-team-to-org-confirm + :title-key "dashboard.select-org-modal.title" + :choose-key "dashboard.select-org-modal.choose" + :placeholder-key "dashboard.select-org-modal.select" + :accept-key "dashboard.select-org-modal.accept" + :cancel-key "labels.cancel"})))) + + on-change-team-org + (mf/use-fn + (mf/deps organizations on-add-team-to-org-confirm) + (fn [] + (st/emit! (modal/show :select-organization-modal {:organizations organizations + :current-organization-id (:organization-id team) + :on-confirm on-add-team-to-org-confirm + :title-key "dashboard.change-org-modal.title" + :choose-key "dashboard.change-org-modal.choose" + :placeholder-key "dashboard.change-org-modal.select" + :accept-key "dashboard.change-org-modal.accept" + :cancel-key "labels.cancel"}))))] (mf/with-effect [team] (dom/set-html-title (tr "title.team-settings" @@ -1486,6 +1514,10 @@ [:& dropdown {:show show-org-options-menu? :on-close close-org-options-menu :dropdown-id "org-options"} [:ul {:class (stl/css :org-dropdown) :role "listbox"} + (when can-change-organization? + [:li {:on-click on-change-team-org + :class (stl/css :org-dropdown-item)} + (tr "dashboard.team-organization.change")]) [:li {:on-click on-remove-team-from-org :class (stl/css :org-dropdown-item)} (tr "dashboard.team-organization.remove")]]]]])]] @@ -1493,8 +1525,7 @@ [:div {:class (stl/css :block-content)} [:span {:class (stl/css :block-text)} (tr "dashboard.team-organization.none")]] - (when (and (pos? (count organizations)) - (not (:is-default team))) + (when can-add-to-organization? [:div {:class (stl/css :block-content)} [:span {:class (stl/css :block-text)} [:a {:on-click on-add-team-to-org} (tr "dashboard.team-organization.add")]]])])]) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 3836f109e6..88d8069247 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1153,6 +1153,18 @@ msgstr "Select an organization" msgid "dashboard.select-org-modal.accept" msgstr "ADD TO ORGANIZATION" +msgid "dashboard.change-org-modal.title" +msgstr "CHANGE TEAM'S ORGANIZATION" + +msgid "dashboard.change-org-modal.choose" +msgstr "Move to:" + +msgid "dashboard.change-org-modal.select" +msgstr "Select an organization" + +msgid "dashboard.change-org-modal.accept" +msgstr "MOVE TEAM" + msgid "dashboard.team-organization.change" msgstr "Change team organization" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index c150221253..0c319b4277 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1157,6 +1157,18 @@ msgstr "Elige una organización" msgid "dashboard.select-org-modal.accept" msgstr "AÑADIR A UNA ORGANIZACIÓN" +msgid "dashboard.change-org-modal.title" +msgstr "CAMBIAR EL EQUIPO DE ORGANIZACIÓN" + +msgid "dashboard.change-org-modal.choose" +msgstr "Mover a:" + +msgid "dashboard.change-org-modal.select" +msgstr "Elige una organización" + +msgid "dashboard.change-org-modal.accept" +msgstr "MOVER EL EQUIPO" + msgid "dashboard.team-organization.change" msgstr "Cambiar el equipo de organización" From d772632b0842124a15c09b02fd5d1d17e71b8537 Mon Sep 17 00:00:00 2001 From: wdeveloper16 Date: Fri, 17 Apr 2026 16:56:29 +0200 Subject: [PATCH 163/288] :sparkles: Allow customising the OIDC login button label (#9026) * :sparkles: Allow customising the OIDC login button label (#7027) * :books: Add CHANGES entry and docs for PENPOT_OIDC_NAME (#7027) --------- Co-authored-by: wdeveloper16 --- CHANGES.md | 2 +- docker/images/files/config.js | 1 + docker/images/files/nginx-entrypoint.sh | 9 +++++++++ docs/technical-guide/configuration.md | 10 ++++++++++ frontend/src/app/config.cljs | 1 + frontend/src/app/main/ui/auth/login.cljs | 2 +- 6 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8bf257d487..262bcea16d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -40,7 +40,7 @@ - Persist asset search query and section filter when switching sidebar tabs (by @eureka0928) [Github #2913](https://github.com/penpot/penpot/issues/2913) - Add delete and duplicate buttons to typography dialog (by @eureka0928) [Github #5270](https://github.com/penpot/penpot/issues/5270) - Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311) - +- Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027) ### :bug: Bugs fixed diff --git a/docker/images/files/config.js b/docker/images/files/config.js index 7bc9ce9404..621331c252 100644 --- a/docker/images/files/config.js +++ b/docker/images/files/config.js @@ -1,2 +1,3 @@ // Frontend configuration //var penpotFlags = ""; +//var penpotOIDCName = ""; diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index 4512d06495..adf5844dea 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -21,7 +21,16 @@ update_flags() { fi } +update_oidc_name() { + if [ -n "$PENPOT_OIDC_NAME" ]; then + echo "$(sed \ + -e "s|^//var penpotOIDCName = .*;|var penpotOIDCName = \"$PENPOT_OIDC_NAME\";|g" \ + "$1")" > "$1" + fi +} + update_flags /var/www/app/js/config.js +update_oidc_name /var/www/app/js/config.js ######################################### ## Nginx Config diff --git a/docs/technical-guide/configuration.md b/docs/technical-guide/configuration.md index ad4579fcde..4c70936dc7 100644 --- a/docs/technical-guide/configuration.md +++ b/docs/technical-guide/configuration.md @@ -242,6 +242,16 @@ register with another method. PENPOT_FLAGS: [...] enable-oidc-registration ``` +__Since version 2.16.0__ + +Allows customising the label shown on the OIDC login button (defaults to "OpenID"). + +```bash +# Frontend +PENPOT_OIDC_NAME: +``` +
+ #### Azure Active Directory using OpenID Connect Allows integrating with Azure Active Directory as authentication provider: diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index a76bdf87dc..52f4524c87 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -154,6 +154,7 @@ true)))) (def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI")) +(def oidc-name (obj/get global "penpotOIDCName")) (def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI")) (def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) (def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index e632b7176e..67674878d2 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -239,7 +239,7 @@ (when (contains? cf/flags :login-with-oidc) [:& bl/button-link {:on-click login-with-oidc :icon deprecated-icon/brand-openid - :label (tr "auth.login-with-oidc-submit") + :label (or (not-empty cf/oidc-name) (tr "auth.login-with-oidc-submit")) :class (stl/css :login-btn :btn-oidc-auth)}])])) (mf/defc login-dialog* From e14de6ea30090ca7c35e26ed2117549916bafefd Mon Sep 17 00:00:00 2001 From: Stas Haas Date: Fri, 17 Apr 2026 14:22:31 +0200 Subject: [PATCH 164/288] :globe_with_meridians: Add translations for: German Currently translated at 95.2% (1976 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/ --- frontend/translations/de.po | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 27b531580a..3fbb0f0585 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-04-15 13:09+0000\n" +"PO-Revision-Date: 2026-04-18 13:10+0000\n" "Last-Translator: Stas Haas \n" "Language-Team: German \n" @@ -8454,3 +8454,8 @@ msgstr "Penpot unterstützt diese Assets nicht mehr. Sie können uns allerdings" #: src/app/main/ui/static.cljs:314 msgid "errors.webgl-context-lost.main-message" msgstr "Ups! Der Canvas-Kontext ist verloren gegangen" + +#: src/app/main/ui/ds/product/loader.cljs:25 +msgid "loader.tips.03.message" +msgstr "" +"Gestalten Sie flexibel mit vertrauten, CSS-ähnlichen Layout-Steuerelementen." From f0c68fb8263c28631f06ac28d3e5b84f002de155 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Mon, 20 Apr 2026 05:02:23 -0400 Subject: [PATCH 165/288] :sparkles: Add search bar to color palette (#8994) * :sparkles: Add search bar to color palette Fixes #7653 Signed-off-by: eureka0928 * :recycle: Use search icon toggle for color palette search Address UX feedback: replace always-visible search input with a search icon that toggles the input on click. Hide the search functionality when no colors exist. Move CHANGES.md entry to 2.16.0 section. Signed-off-by: eureka0928 --------- Signed-off-by: eureka0928 Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + .../app/main/ui/workspace/color_palette.cljs | 74 +++++++++++++++++-- .../app/main/ui/workspace/color_palette.scss | 18 +++++ 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 262bcea16d..f9b9322ff3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -40,6 +40,7 @@ - Persist asset search query and section filter when switching sidebar tabs (by @eureka0928) [Github #2913](https://github.com/penpot/penpot/issues/2913) - Add delete and duplicate buttons to typography dialog (by @eureka0928) [Github #5270](https://github.com/penpot/penpot/issues/5270) - Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311) +- Add a search bar to filter colors in the color palette toolbar (by @eureka0928) [Github #7653](https://github.com/penpot/penpot/issues/7653) - Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/ui/workspace/color_palette.cljs b/frontend/src/app/main/ui/workspace/color_palette.cljs index 7a8dae308c..5e2eb534f1 100644 --- a/frontend/src/app/main/ui/workspace/color_palette.cljs +++ b/frontend/src/app/main/ui/workspace/color_palette.cljs @@ -15,7 +15,10 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.color-bullet :as cb] + [app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.context :as ctx] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.utilities.swatch :refer [swatch*]] [app.main.ui.icons :as deprecated-icon] [app.util.color :as uc] @@ -23,6 +26,7 @@ [app.util.i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.object :as obj] + [app.util.strings :refer [matches-search]] [okulary.core :as l] [potok.v2.core :as ptk] [rumext.v2 :as mf])) @@ -59,21 +63,54 @@ {::mf/wrap [mf/memo]} [{:keys [colors size width selected]}] (let [state (mf/use-state #(do {:show-menu false})) + search-term* (mf/use-state "") + search-term (deref search-term*) + search-open* (mf/use-state false) + search-open? (deref search-open*) + has-colors? (seq colors) + + filtered-colors + (mf/with-memo [colors search-term] + (if (empty? search-term) + colors + (filterv #(matches-search (or (uc/get-color-name %) "") search-term) + colors))) + + on-search-change + (mf/use-fn #(reset! search-term* %)) + + on-toggle-search + (mf/use-fn + (fn [_] + (when @search-open* + (reset! search-term* "")) + (swap! search-open* not))) + + on-search-clear + (mf/use-fn + (fn [_] + (reset! search-term* "") + (reset! search-open* false))) + offset-step (cond (<= size 64) 40 (<= size 80) 72 :else 72) + ;; Reserve room for the search bar, icon button, or nothing + search-width (cond (not has-colors?) 0 + search-open? 192 + :else 32) buttons-size (cond - (<= size 64) 164 - :else 132) + (<= size 64) (+ 164 search-width) + :else (+ 132 search-width)) width (- width buttons-size) visible (int (/ width offset-step)) - show-arrows? (> (count colors) visible) + show-arrows? (> (count filtered-colors) visible) visible (if show-arrows? (int (/ (- width 48) offset-step)) visible) offset (:offset @state 0) - max-offset (- (count colors) + max-offset (- (count filtered-colors) visible) container (mf/use-ref nil) bullet-size (cond @@ -121,16 +158,35 @@ width (obj/get dom "clientWidth")] (swap! state assoc :width width))) - (mf/with-effect [width colors] + (mf/with-effect [width filtered-colors] (when (not= 0 (:offset @state)) (swap! state assoc :offset 0))) + (mf/with-effect [has-colors?] + (when-not has-colors? + (reset! search-open* false) + (reset! search-term* ""))) + [:div {:class (stl/css-case :color-palette true :no-text (< size 64)) :style #js {"--bullet-size" (dm/str bullet-size "px") "--color-cell-width" (dm/str color-cell-width "px")}} + (when has-colors? + [:div {:class (stl/css-case :palette-search search-open? + :palette-search-collapsed (not search-open?))} + (when search-open? + [:> search-bar* {:on-change on-search-change + :on-clear on-search-clear + :value search-term + :placeholder (tr "workspace.assets.search") + :auto-focus true}]) + [:> icon-button* {:variant "ghost" + :icon i/search + :on-click on-toggle-search + :aria-label (tr "workspace.assets.search")}]]) + (when show-arrows? [:button {:class (stl/css :left-arrow) :disabled (= offset 0) @@ -138,18 +194,20 @@ [:div {:class (stl/css :color-palette-content) :ref container :on-wheel on-scroll} - (if (empty? colors) + (if (empty? filtered-colors) [:div {:class (stl/css :color-palette-empty) :style {:position "absolute" :left "50%" :top "50%" :transform "translate(-50%, -50%)"}} - (tr "workspace.libraries.colors.empty-palette")] + (if (empty? search-term) + (tr "workspace.libraries.colors.empty-palette") + (tr "workspace.assets.not-found"))] [:div {:class (stl/css :color-palette-inside) :style {:position "relative" :max-width (str width "px") :right (str (* offset-step offset) "px")}} - (for [[idx item] (map-indexed vector colors)] + (for [[idx item] (map-indexed vector filtered-colors)] [:> palette-item* {:color item :key idx :size size :selected selected}])])] (when show-arrows? diff --git a/frontend/src/app/main/ui/workspace/color_palette.scss b/frontend/src/app/main/ui/workspace/color_palette.scss index 2e23c45844..026b57eb73 100644 --- a/frontend/src/app/main/ui/workspace/color_palette.scss +++ b/frontend/src/app/main/ui/workspace/color_palette.scss @@ -11,6 +11,24 @@ display: flex; } +.palette-search, +.palette-search-collapsed { + display: flex; + align-items: center; + padding-inline: deprecated.$s-4; +} + +.palette-search { + gap: deprecated.$s-4; + width: deprecated.$s-192; + min-width: deprecated.$s-192; +} + +.palette-search-collapsed { + width: deprecated.$s-32; + min-width: deprecated.$s-32; +} + .left-arrow, .right-arrow { @include deprecated.buttonStyle; From 42ebee88d6bc83fe6ad45dae5e0a0158751d709a Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Mon, 20 Apr 2026 05:03:50 -0400 Subject: [PATCH 166/288] :sparkles: Add paste to replace (Cmd+Shift+V) (#9033) Paste clipboard contents in place of the currently selected shape, inheriting its position, parent, and z-index. The replaced shape is deleted in the same transaction for a single undo step. Signed-off-by: eureka0928 --- CHANGES.md | 1 + .../app/main/data/workspace/clipboard.cljs | 56 +++++++++++++------ .../app/main/data/workspace/shortcuts.cljs | 5 ++ 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f9b9322ff3..b695345f6d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -33,6 +33,7 @@ - Make links in comments clickable (by @eureka0928) [Github #1602](https://github.com/penpot/penpot/issues/1602) - Add visibility toggle for strokes (by @eureka0928) [Github #7438](https://github.com/penpot/penpot/issues/7438) - Sort asset library subfolders alphabetically at every nesting level (by @eureka0928) [Github #2572](https://github.com/penpot/penpot/issues/2572) +- Add Paste to replace (Cmd+Shift+V) to replace the selected shape with clipboard contents (by @eureka0928) [Github #4240](https://github.com/penpot/penpot/issues/4240) - Differentiate incoming and outgoing interaction link colors (by @claytonlin1110) [Github #7794](https://github.com/penpot/penpot/issues/7794) - Add guide locking and fix locked elements not selectable in viewer (by @Dexterity104) [Github #8358](https://github.com/penpot/penpot/issues/8358) - Apply styles to selection (by @AzazelN28) [Taiga #13647](https://tree.taiga.io/project/penpot/task/13647) diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index 4c3e60f7d4..3017d94b2f 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -18,6 +18,7 @@ [app.common.geom.shapes :as gsh] [app.common.geom.shapes.grid-layout :as gslg] [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] [app.common.schema :as sm] [app.common.transit :as t] [app.common.types.component :as ctc] @@ -260,7 +261,7 @@ :allowHTMLPaste (features/active-feature? @st/state "text-editor/v2-html-paste")}) (defn- create-paste-from-blob - [in-viewport?] + [in-viewport? replace?] (fn [blob] (let [type (.-type blob)] (cond @@ -281,7 +282,9 @@ (rx/filter map?) (rx/map (fn [pdata] - (assoc pdata :in-viewport in-viewport?))) + (-> pdata + (assoc :in-viewport in-viewport?) + (assoc :replace replace?)))) (rx/mapcat (fn [pdata] (case (:type pdata) @@ -293,8 +296,6 @@ (->> (rx/from (.text blob)) (rx/map paste-text)))))) -(def default-paste-from-blob (create-paste-from-blob false)) - (defn- clipboard-permission-error? "Check if the given error is a clipboard permission error (NotAllowedError DOMException)." @@ -313,14 +314,15 @@ (defn paste-from-clipboard "Perform a `paste` operation using the Clipboard API." - [] - (ptk/reify ::paste-from-clipboard - ptk/WatchEvent - (watch [_ _ _] - (->> (clipboard/from-navigator default-options) - (rx/mapcat default-paste-from-blob) - (rx/take 1) - (rx/catch on-clipboard-permission-error))))) + ([] (paste-from-clipboard nil)) + ([{:keys [replace?]}] + (ptk/reify ::paste-from-clipboard + ptk/WatchEvent + (watch [_ _ _] + (->> (clipboard/from-navigator default-options) + (rx/mapcat (create-paste-from-blob false (boolean replace?))) + (rx/take 1) + (rx/catch on-clipboard-permission-error)))))) (defn paste-from-event "Perform a `paste` operation from user emmited event." @@ -337,7 +339,7 @@ (if is-editing? (rx/empty) (->> (clipboard/from-synthetic-clipboard-event event default-options) - (rx/mapcat (create-paste-from-blob in-viewport?)))))))) + (rx/mapcat (create-paste-from-blob in-viewport? false)))))))) (defn copy-selected-svg [] @@ -722,7 +724,7 @@ (update change :obj process-rchange-shape media-idx) change)) - (calculate-paste-position [state pobjects selected position] + (calculate-paste-position [state pobjects selected position replace-id] (let [page-objects (dsh/lookup-page-objects state) selected-objs (map (d/getf pobjects) selected) first-selected-obj (first selected-objs) @@ -736,9 +738,20 @@ tree-root (get-tree-root-shapes pobjects) only-one-root-shape? (and (< 1 (count pobjects)) - (= 1 (count tree-root)))] + (= 1 (count tree-root))) + replaced (some->> replace-id (get page-objects))] (cond + ;; Paste in place: center pasted content on the replaced shape and + ;; reparent to its container. The replaced shape is deleted below + ;; so the new content takes its z-index slot. + (some? replaced) + (let [delta (gpt/subtract (gsh/shape->center replaced) + (grc/rect->center wrapper)) + parent-id (:parent-id replaced) + target-index (cfh/get-position-on-parent page-objects replace-id)] + [parent-id delta target-index]) + ;; Paste next to selected frame, if selected is itself or of the same size as the copied (and (selected-frame? state) (or (any-same-frame-from-selected? state (keys pobjects)) @@ -854,10 +867,17 @@ position (deref ms/mouse-position) + ;; Replace mode is only valid with a single selected shape. + ;; In that case we drop the pasted content at its position and + ;; delete it in the same transaction. + page-selected (dsh/lookup-selected state) + replace-id (when (and (:replace pdata) (= 1 (count page-selected))) + (first page-selected)) + ;; Calculate position for the pasted elements [candidate-parent-id delta - index] (calculate-paste-position state objects selected position) + index] (calculate-paste-position state objects selected position replace-id) page-objects (:objects page) @@ -899,6 +919,10 @@ (map :id) (pcb/resize-parents changes)) + changes (if (some? replace-id) + (second (cls/generate-delete-shapes changes #{replace-id} {})) + changes) + orig-shapes (map (d/getf all-objects) selected) children-after (-> (pcb/get-objects changes) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 3c147d7a7f..a89662e8d3 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -104,6 +104,11 @@ :subsections [:edit] :fn (constantly nil)} + :paste-replace {:tooltip (ds/meta (ds/shift "V")) + :command (ds/c-mod "shift+v") + :subsections [:edit] + :fn #(emit-when-no-readonly (dw/paste-from-clipboard {:replace? true}))} + :copy-props {:tooltip (ds/meta (ds/alt "c")) :command (ds/c-mod "alt+c") :subsections [:edit] From b2c9e08d428c27afcc2bad9f0b8d9b30137522a1 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Fri, 17 Apr 2026 11:36:39 +0200 Subject: [PATCH 167/288] :bug: Fix bad check on leave nitrate org --- backend/src/app/rpc/commands/nitrate.clj | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index 62d25a781b..6bd5a7f855 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -133,17 +133,6 @@ valid-teams-to-leave-ids (into valid-teams-to-transfer-ids valid-teams-to-exit-ids) - ;; Get all the teams ids - all-teams-ids (into #{} d/xf:map-id (:teams org-summary)) - - ;; Get all the ids of the teams that will be processed: - ;; all the ids on teams-to-leave, teams-to-delete and default-team-id - selected-team-ids (-> (into #{default-team-id} teams-to-delete) - (into d/xf:map-id teams-to-leave)) - - ;; Check that we are processing all the teams - all-teams-selected? (= all-teams-ids selected-team-ids) - default-team-files-count (-> (db/exec-one! conn [sql:get-team-files-count default-team-id]) :total) delete-default-team? (= default-team-files-count 0) @@ -171,8 +160,7 @@ (when (or (not valid-teams-to-delete?) (not valid-teams-to-leave?) - (not valid-default-team-id?) - (not all-teams-selected?)) + (not valid-default-team-id?)) (ex/raise :type :validation :code :not-valid-teams)) From ae66317d6c7fd72529040f2d29605101c1cf2a38 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Fri, 17 Apr 2026 18:09:21 +0200 Subject: [PATCH 168/288] :sparkles: Add nitrate api to remove user from org --- backend/src/app/rpc/commands/nitrate.clj | 127 ++++++---- backend/src/app/rpc/commands/teams.clj | 12 +- backend/src/app/rpc/management/nitrate.clj | 53 +++++ backend/src/app/rpc/notifications.clj | 12 + .../rpc_management_nitrate_test.clj | 220 ++++++++++++++++++ .../test/backend_tests/rpc_nitrate_test.clj | 156 +++++++++++++ frontend/src/app/main/data/common.cljs | 6 +- frontend/src/app/main/data/dashboard.cljs | 18 ++ frontend/src/app/main/data/team.cljs | 23 +- frontend/src/app/main/ui/dashboard/team.cljs | 4 +- frontend/translations/en.po | 3 + frontend/translations/es.po | 3 + 12 files changed, 575 insertions(+), 62 deletions(-) diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index 6bd5a7f855..f02c36197f 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -57,6 +57,7 @@ (def ^:private sql:get-member-teams-info "SELECT t.id, + t.is_default, tpr.is_owner, (SELECT count(*) FROM team_profile_rel WHERE team_id = t.id) AS num_members, (SELECT array_agg(profile_id) FROM team_profile_rel WHERE team_id = t.id) AS member_ids @@ -76,6 +77,7 @@ (def ^:private schema:leave-org [:map [:org-id ::sm/uuid] + [:org-name ::sm/text] [:default-team-id ::sm/uuid] [:teams-to-delete [:vector ::sm/uuid]] @@ -85,62 +87,72 @@ [:id ::sm/uuid] [:reassign-to {:optional true} ::sm/uuid]]]]]) -(sv/defmethod ::leave-org - {::rpc/auth true - ::doc/added "2.15" - ::sm/params schema:leave-org - ::db/transaction true} - [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id org-id default-team-id teams-to-delete teams-to-leave] :as params}] + +(defn- get-organization-teams-for-user + [{:keys [::db/conn] :as cfg} org-summary profile-id] + (let [org-team-ids (->> (:teams org-summary) + (map :id)) + ids-array (db/create-array conn "uuid" org-team-ids)] + (db/exec! conn [sql:get-member-teams-info profile-id ids-array]))) + +(defn- calculate-valid-teams + ([org-teams default-team-id] + (let [;; valid default team is the one which id is default-team-id + valid-default-team (d/seek #(= default-team-id (:id %)) org-teams) + + ;; Remove your-penpot for the rest of validations + org-teams (remove #(= default-team-id (:id %)) org-teams) + + ;; valid teams to delete are those that the user is owner, and only have one member + valid-teams-to-delete-ids (->> org-teams + (filter #(and (:is-owner %) + (= (:num-members %) 1))) + (map :id) + (into #{})) + ;; valid teams to transfer are those that the user is owner, and have more than one member + valid-teams-to-transfer (->> org-teams + (filter #(and (:is-owner %) + (> (:num-members %) 1)))) + + ;; valid teams to exit are those that the user isn't owner, and have more than one member + valid-teams-to-exit (->> org-teams + (filter #(and (not (:is-owner %)) + (> (:num-members %) 1))))] + {:valid-teams-to-delete-ids valid-teams-to-delete-ids + :valid-teams-to-transfer valid-teams-to-transfer + :valid-teams-to-exit valid-teams-to-exit + :valid-default-team valid-default-team}))) + +(defn get-valid-teams [cfg org-id profile-id default-team-id] (let [org-summary (nitrate/call cfg :get-org-summary {:org-id org-id}) + org-teams (get-organization-teams-for-user cfg org-summary profile-id)] + (calculate-valid-teams org-teams default-team-id))) - org-name (:name org-summary) - org-prefix (str "[" (d/sanitize-string org-name) "] ") +(defn- assert-valid-teams [cfg profile-id org-id default-team-id teams-to-delete teams-to-leave] + (let [org-summary (nitrate/call cfg :get-org-summary {:org-id org-id}) + org-teams (get-organization-teams-for-user cfg org-summary profile-id) + {:keys [valid-teams-to-delete-ids + valid-teams-to-transfer + valid-teams-to-exit + valid-default-team]} (calculate-valid-teams org-teams default-team-id) - your-penpot-ids (->> (:teams org-summary) - (filter :is-your-penpot) - (map :id) - (into #{})) - valid-default-team-id? (contains? your-penpot-ids default-team-id) - org-team-ids (->> (:teams org-summary) - (remove :is-your-penpot) - (map :id)) - ids-array (db/create-array conn "uuid" org-team-ids) - teams (db/exec! conn [sql:get-member-teams-info profile-id ids-array]) - teams-by-id (d/index-by :id teams) - - ;; valid teams to delete are those that the user is owner, and only have one member - valid-teams-to-delete-ids (->> teams - (filter #(and (:is-owner %) - (= (:num-members %) 1))) - (map :id) - (into #{})) - - valid-teams-to-delete? (= valid-teams-to-delete-ids (into #{} teams-to-delete)) - - ;; valid teams to transfer are those that the user is owner, and have more than one member - valid-teams-to-transfer (->> teams - (filter #(and (:is-owner %) - (> (:num-members %) 1)))) - valid-teams-to-transfer-ids (->> valid-teams-to-transfer (map :id) (into #{})) - - ;; valid teams to exit are those that the user isn't owner, and have more than one member - valid-teams-to-exit (->> teams - (filter #(and (not (:is-owner %)) - (> (:num-members %) 1)))) valid-teams-to-exit-ids (->> valid-teams-to-exit (map :id) (into #{})) - + valid-teams-to-transfer-ids (->> valid-teams-to-transfer (map :id) (into #{})) valid-teams-to-leave-ids (into valid-teams-to-transfer-ids valid-teams-to-exit-ids) - default-team-files-count (-> (db/exec-one! conn [sql:get-team-files-count default-team-id]) - :total) - delete-default-team? (= default-team-files-count 0) + valid-default-team-id? (some? valid-default-team) + + + + valid-teams-to-delete? (= valid-teams-to-delete-ids (into #{} teams-to-delete)) ;; for every team in teams-to-leave, check that: ;; - if it has a reassign-to, it belongs to valid-teams-to-transfer and ;; the reassign-to is a member of the team and not the current user; ;; - if it hasn't a reassign-to, check that it belongs to valid-teams-to-exit + teams-by-id (d/index-by :id org-teams) valid-teams-to-leave? (and (= valid-teams-to-leave-ids (->> teams-to-leave (map :id) (into #{}))) (every? (fn [{:keys [id reassign-to]}] @@ -151,8 +163,7 @@ (contains? members reassign-to))) (contains? valid-teams-to-exit-ids id))) teams-to-leave))] - - + ;; the org owner cannot leave (when (= (:owner-id org-summary) profile-id) (ex/raise :type :validation :code :org-owner-cannot-leave)) @@ -162,7 +173,22 @@ (not valid-teams-to-leave?) (not valid-default-team-id?)) (ex/raise :type :validation - :code :not-valid-teams)) + :code :not-valid-teams)))) + + +(defn leave-org [{:keys [::db/conn] :as cfg} {:keys [profile-id org-id org-name default-team-id teams-to-delete teams-to-leave skip-validation] :as params}] + (let [org-prefix (str "[" (d/sanitize-string org-name) "] ") + + default-team-files-count (-> (db/exec-one! conn [sql:get-team-files-count default-team-id]) + :total) + delete-default-team? (= default-team-files-count 0)] + + + + + ;; assert that the received teams are valid, checking the different constraints + (when-not skip-validation + (assert-valid-teams cfg profile-id org-id default-team-id teams-to-delete teams-to-leave)) (assert-membership cfg profile-id org-id) @@ -187,6 +213,15 @@ nil)) +(sv/defmethod ::leave-org + {::rpc/auth true + ::doc/added "2.15" + ::sm/params schema:leave-org + ::db/transaction true} + [cfg {:keys [::rpc/profile-id] :as params}] + (leave-org cfg (assoc params :profile-id profile-id))) + + (def ^:private schema:remove-team-from-org [:map [:team-id ::sm/uuid] diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 722b48bb61..8638252338 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -685,7 +685,7 @@ ;; --- Mutation: Leave Team (defn leave-team - [{:keys [::db/conn]} {:keys [profile-id id reassign-to]}] + [{:keys [::db/conn ::mbus/msgbus]} {:keys [profile-id id reassign-to]}] (let [perms (get-permissions conn profile-id id) members (get-team-members conn id)] @@ -716,7 +716,15 @@ ;; assign owner role to new profile (db/update! conn :team-profile-rel (get types.team/permissions-for-role :owner) - {:team-id id :profile-id reassign-to})) + {:team-id id :profile-id reassign-to}) + + ;; notify new owner + (mbus/pub! msgbus + :topic reassign-to + :message {:type :team-role-change + :topic reassign-to + :team-id id + :role :owner})) ;; and finally, if all other conditions does not match and the ;; current profile is owner, we dont allow it because there diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 59a442e9c7..e173b1a0a6 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -17,6 +17,7 @@ [app.db :as db] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] + [app.rpc.commands.nitrate :as cnit] [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.commands.teams-invitations :as ti] @@ -340,3 +341,55 @@ RETURNING id, name;") (db/tx-run! cfg ti/create-org-invitation params) nil) + + +;; API: remove-from-org + +(def ^:private sql:get-reassign-to + "SELECT tpr.profile_id + FROM team_profile_rel AS tpr + WHERE tpr.team_id = ? + AND tpr.profile_id <> ? + AND tpr.is_owner IS NOT TRUE + ORDER BY CASE + WHEN tpr.is_admin IS TRUE THEN 1 + ELSE 2 + END, + tpr.created_at, + tpr.profile_id + LIMIT 1;") + +(defn add-reassign-to [cfg profile-id team-to-transfer] + (let [reassign-to (-> (db/exec-one! cfg [sql:get-reassign-to (:id team-to-transfer) profile-id]) + :profile-id)] + (when-not reassign-to + (ex/raise :type :validation + :code :nobody-to-reassign-team)) + + (assoc team-to-transfer :reassign-to reassign-to))) + +(sv/defmethod ::remove-from-org + "Remove an user from an organization" + {::doc/added "2.16" + ::sm/params [:map + [:profile-id ::sm/uuid] + [:org-id ::sm/uuid] + [:org-name ::sm/text] + [:default-team-id ::sm/uuid]] + ::db/transaction true} + [cfg {:keys [profile-id org-id org-name default-team-id] :as params}] + (let [{:keys [valid-teams-to-delete-ids + valid-teams-to-transfer + valid-teams-to-exit]} (cnit/get-valid-teams cfg org-id profile-id default-team-id) + add-reassign-to (partial add-reassign-to cfg profile-id) + + valid-teams-to-leave (into valid-teams-to-exit + (map add-reassign-to valid-teams-to-transfer))] + + (cnit/leave-org cfg (assoc params + :teams-to-delete valid-teams-to-delete-ids + :teams-to-leave valid-teams-to-leave + :skip-validation true)) + (notifications/notify-user-removed-from-org cfg profile-id org-id org-name "dashboard.user-no-longer-belong-org") + nil)) + diff --git a/backend/src/app/rpc/notifications.clj b/backend/src/app/rpc/notifications.clj index fc3b4b1752..05f69f9781 100644 --- a/backend/src/app/rpc/notifications.clj +++ b/backend/src/app/rpc/notifications.clj @@ -21,4 +21,16 @@ :team-name team-name :organization-id organization-id :organization-name organization-name + :notification notification}))) + + +(defn notify-user-removed-from-org + [cfg profile-id organization-id organization-name notification] + (let [msgbus (::mbus/msgbus cfg)] + (mbus/pub! msgbus + :topic profile-id + :message {:type :user-org-change + :topic profile-id + :organization-id organization-id + :organization-name organization-name :notification notification}))) \ No newline at end of file diff --git a/backend/test/backend_tests/rpc_management_nitrate_test.clj b/backend/test/backend_tests/rpc_management_nitrate_test.clj index bceafbc72e..1a938bee22 100644 --- a/backend/test/backend_tests/rpc_management_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_management_nitrate_test.clj @@ -13,6 +13,7 @@ [app.db :as-alias db] [app.email :as email] [app.msgbus :as mbus] + [app.nitrate :as nitrate] [app.rpc :as-alias rpc] [backend-tests.helpers :as th] [clojure.set :as set] @@ -218,3 +219,222 @@ (t/is (not (th/success? ko-out))) (t/is (= :not-found (th/ex-type (:error ko-out)))) (t/is (= :profile-not-found (th/ex-code (:error ko-out)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Tests: remove-from-org +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- make-org-summary + [& {:keys [org-id org-name owner-id your-penpot-teams org-teams] + :or {your-penpot-teams [] org-teams []}}] + {:id org-id + :name org-name + :owner-id owner-id + :teams (into + (mapv (fn [id] {:id id :is-your-penpot true}) your-penpot-teams) + (mapv (fn [id] {:id id :is-your-penpot false}) org-teams))}) + +(defn- nitrate-call-mock + [org-summary] + (fn [_cfg method _params] + (case method + :get-org-summary org-summary + nil))) + +(t/deftest remove-from-org-happy-path-no-extra-teams + ;; User is only in its default team (which has files); it should be + ;; kept, renamed and unset as default. A notification must be sent. + (let [org-owner (th/create-profile* 1 {:is-active true}) + user (th/create-profile* 2 {:is-active true}) + org-team (th/create-team* 1 {:profile-id (:id user)}) + project (th/create-project* 1 {:profile-id (:id user) + :team-id (:id org-team)}) + _ (th/create-file* 1 {:profile-id (:id user) + :project-id (:id project)}) + org-id (uuid/random) + org-summary (make-org-summary + :org-id org-id + :org-name "Acme Org" + :owner-id (:id org-owner) + :your-penpot-teams [(:id org-team)] + :org-teams []) + calls (atom []) + out (with-redefs [nitrate/call (nitrate-call-mock org-summary) + mbus/pub! (fn [_bus & {:keys [topic message]}] + (swap! calls conj {:topic topic :message message}))] + (management-command-with-nitrate! + {::th/type :remove-from-org + ::rpc/profile-id (:id org-owner) + :profile-id (:id user) + :org-id org-id + :org-name "Acme Org" + :default-team-id (:id org-team)}))] + (t/is (th/success? out)) + (t/is (nil? (:result out))) + + ;; default team preserved, renamed and unset as default + (let [team (th/db-get :team {:id (:id org-team)})] + (t/is (false? (:is-default team))) + (t/is (str/starts-with? (:name team) "[Acme Org] "))) + + ;; exactly one notification sent to the user + (t/is (= 1 (count @calls))) + (let [msg (-> @calls first :message)] + (t/is (= :user-org-change (:type msg))) + (t/is (= (:id user) (:topic msg))) + (t/is (= org-id (:organization-id msg))) + (t/is (= "Acme Org" (:organization-name msg))) + (t/is (= "dashboard.user-no-longer-belong-org" (:notification msg)))))) + +(t/deftest remove-from-org-deletes-empty-default-team + ;; When the default team has no files it should be soft-deleted. + (let [org-owner (th/create-profile* 1 {:is-active true}) + user (th/create-profile* 2 {:is-active true}) + org-team (th/create-team* 2 {:profile-id (:id user)}) + org-id (uuid/random) + org-summary (make-org-summary + :org-id org-id + :org-name "Acme Org" + :owner-id (:id org-owner) + :your-penpot-teams [(:id org-team)] + :org-teams []) + out (with-redefs [nitrate/call (nitrate-call-mock org-summary) + mbus/pub! (fn [& _] nil)] + (management-command-with-nitrate! + {::th/type :remove-from-org + ::rpc/profile-id (:id org-owner) + :profile-id (:id user) + :org-id org-id + :org-name "Acme Org" + :default-team-id (:id org-team)}))] + (t/is (th/success? out)) + (let [team (th/db-get :team {:id (:id org-team)} {::db/remove-deleted false})] + (t/is (some? (:deleted-at team)))))) + +(t/deftest remove-from-org-deletes-sole-owner-team + ;; When the user is the sole member of an org team it should be deleted. + (let [org-owner (th/create-profile* 1 {:is-active true}) + user (th/create-profile* 2 {:is-active true}) + extra-team (th/create-team* 3 {:profile-id (:id user)}) + org-team (th/create-team* 99 {:profile-id (:id user)}) + org-id (uuid/random) + org-summary (make-org-summary + :org-id org-id + :org-name "Acme Org" + :owner-id (:id org-owner) + :your-penpot-teams [(:id org-team)] + :org-teams [(:id extra-team)]) + out (with-redefs [nitrate/call (nitrate-call-mock org-summary) + mbus/pub! (fn [& _] nil)] + (management-command-with-nitrate! + {::th/type :remove-from-org + ::rpc/profile-id (:id org-owner) + :profile-id (:id user) + :org-id org-id + :org-name "Acme Org" + :default-team-id (:id org-team)}))] + (t/is (th/success? out)) + (let [team (th/db-get :team {:id (:id extra-team)} {::db/remove-deleted false})] + (t/is (some? (:deleted-at team)))))) + +(t/deftest remove-from-org-transfers-ownership-of-multi-member-team + ;; When the user owns a team that has another non-owner member, ownership + ;; is transferred to that member by the endpoint automatically. + (let [org-owner (th/create-profile* 1 {:is-active true}) + user (th/create-profile* 2 {:is-active true}) + candidate (th/create-profile* 3 {:is-active true}) + extra-team (th/create-team* 4 {:profile-id (:id user)}) + _ (th/create-team-role* {:team-id (:id extra-team) + :profile-id (:id candidate) + :role :editor}) + org-team (th/create-team* 99 {:profile-id (:id user)}) + org-id (uuid/random) + org-summary (make-org-summary + :org-id org-id + :org-name "Acme Org" + :owner-id (:id org-owner) + :your-penpot-teams [(:id org-team)] + :org-teams [(:id extra-team)]) + out (with-redefs [nitrate/call (nitrate-call-mock org-summary) + mbus/pub! (fn [& _] nil)] + (management-command-with-nitrate! + {::th/type :remove-from-org + ::rpc/profile-id (:id org-owner) + :profile-id (:id user) + :org-id org-id + :org-name "Acme Org" + :default-team-id (:id org-team)}))] + (t/is (th/success? out)) + ;; user no longer in extra-team + (let [rel (th/db-get :team-profile-rel {:team-id (:id extra-team) :profile-id (:id user)})] + (t/is (nil? rel))) + ;; candidate promoted to owner + (let [rel (th/db-get :team-profile-rel {:team-id (:id extra-team) :profile-id (:id candidate)})] + (t/is (true? (:is-owner rel)))))) + +(t/deftest remove-from-org-exits-non-owned-team + ;; When the user is a non-owner member of an org team, they simply leave. + (let [org-owner (th/create-profile* 1 {:is-active true}) + user (th/create-profile* 2 {:is-active true}) + extra-team (th/create-team* 5 {:profile-id (:id org-owner)}) + _ (th/create-team-role* {:team-id (:id extra-team) + :profile-id (:id user) + :role :editor}) + org-team (th/create-team* 99 {:profile-id (:id user)}) + org-id (uuid/random) + org-summary (make-org-summary + :org-id org-id + :org-name "Acme Org" + :owner-id (:id org-owner) + :your-penpot-teams [(:id org-team)] + :org-teams [(:id extra-team)]) + out (with-redefs [nitrate/call (nitrate-call-mock org-summary) + mbus/pub! (fn [& _] nil)] + (management-command-with-nitrate! + {::th/type :remove-from-org + ::rpc/profile-id (:id org-owner) + :profile-id (:id user) + :org-id org-id + :org-name "Acme Org" + :default-team-id (:id org-team)}))] + (t/is (th/success? out)) + ;; user no longer a member of extra-team + (let [rel (th/db-get :team-profile-rel {:team-id (:id extra-team) :profile-id (:id user)})] + (t/is (nil? rel))) + ;; team still exists for the owner + (let [team (th/db-get :team {:id (:id extra-team)})] + (t/is (some? team))))) + +(t/deftest remove-from-org-error-nobody-to-reassign + ;; When the user owns a multi-member team but every other member is + ;; also an owner, the auto-selection query finds nobody and raises. + (let [other-owner (th/create-profile* 1 {:is-active true}) + user (th/create-profile* 2 {:is-active true}) + extra-team (th/create-team* 6 {:profile-id (:id user)}) + ;; add other-owner to the team and make them co-owner directly in DB + _ (th/create-team-role* {:team-id (:id extra-team) + :profile-id (:id other-owner) + :role :editor}) + _ (th/db-update! :team-profile-rel + {:is-owner true :is-admin false} + {:team-id (:id extra-team) :profile-id (:id other-owner)}) + org-team (th/create-team* 99 {:profile-id (:id user)}) + org-id (uuid/random) + org-summary (make-org-summary + :org-id org-id + :org-name "Acme Org" + :owner-id (:id other-owner) + :your-penpot-teams [(:id org-team)] + :org-teams [(:id extra-team)]) + out (with-redefs [nitrate/call (nitrate-call-mock org-summary) + mbus/pub! (fn [& _] nil)] + (management-command-with-nitrate! + {::th/type :remove-from-org + ::rpc/profile-id (:id other-owner) + :profile-id (:id user) + :org-id org-id + :org-name "Acme Org" + :default-team-id (:id org-team)}))] + (t/is (not (th/success? out))) + (t/is (= :validation (th/ex-type (:error out)))) + (t/is (= :nobody-to-reassign-team (th/ex-code (:error out)))))) diff --git a/backend/test/backend_tests/rpc_nitrate_test.clj b/backend/test/backend_tests/rpc_nitrate_test.clj index 084c8417a8..dd8dd20ae2 100644 --- a/backend/test/backend_tests/rpc_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_nitrate_test.clj @@ -10,6 +10,7 @@ [app.db :as-alias db] [app.nitrate :as nitrate] [app.rpc :as-alias rpc] + [app.rpc.commands.nitrate] [backend-tests.helpers :as th] [clojure.test :as t] [cuerdas.core :as str])) @@ -72,6 +73,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) :org-id org-id + :org-name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave []} @@ -106,6 +108,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) :org-id org-id + :org-name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave []} @@ -140,6 +143,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) :org-id org-id + :org-name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave []} @@ -174,6 +178,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) :org-id org-id + :org-name "Test Org" :default-team-id your-penpot-id :teams-to-delete [(:id team1)] :teams-to-leave []} @@ -210,6 +215,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) :org-id org-id + :org-name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave [{:id (:id team1) :reassign-to (:id profile-owner)}]} @@ -254,6 +260,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) :org-id org-id + :org-name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave [{:id (:id team1)}]} @@ -290,6 +297,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-owner) :org-id org-id + :org-name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave []} @@ -318,6 +326,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) :org-id org-id + :org-name "Test Org" :default-team-id (uuid/random) :teams-to-delete [] :teams-to-leave []} @@ -327,6 +336,147 @@ (t/is (= :validation (th/ex-type (:error out)))) (t/is (= :not-valid-teams (th/ex-code (:error out)))))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Unit Tests for calculate-valid-teams + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:private calculate-valid-teams + (or (ns-resolve 'app.rpc.commands.nitrate 'calculate-valid-teams) + (throw (ex-info "Unable to resolve calculate-valid-teams" + {:ns 'app.rpc.commands.nitrate + :symbol 'calculate-valid-teams})))) + +(defn- make-team [id & {:keys [is-owner num-members member-ids] + :or {is-owner false num-members 1 member-ids []}}] + {:id id :is-owner is-owner :num-members num-members :member-ids member-ids}) + +(t/deftest calculate-valid-teams-no-org-teams + (let [default-id (uuid/random) + default-team (make-team default-id) + result (calculate-valid-teams [default-team] default-id)] + (t/is (= default-team (:valid-default-team result))) + (t/is (empty? (:valid-teams-to-delete-ids result))) + (t/is (empty? (:valid-teams-to-transfer result))) + (t/is (empty? (:valid-teams-to-exit result))))) + +(t/deftest calculate-valid-teams-default-not-found + (let [default-id (uuid/random) + other-id (uuid/random) + other-team (make-team other-id) + ;; default-id is not in org-teams at all + result (calculate-valid-teams [other-team] default-id)] + (t/is (nil? (:valid-default-team result))))) + +(t/deftest calculate-valid-teams-sole-owner-team + (let [default-id (uuid/random) + team-id (uuid/random) + default (make-team default-id) + solo-team (make-team team-id :is-owner true :num-members 1) + result (calculate-valid-teams [default solo-team] default-id)] + (t/is (contains? (:valid-teams-to-delete-ids result) team-id)) + (t/is (empty? (:valid-teams-to-transfer result))) + (t/is (empty? (:valid-teams-to-exit result))))) + +(t/deftest calculate-valid-teams-owned-multi-member-team + (let [default-id (uuid/random) + team-id (uuid/random) + default (make-team default-id) + ;; owner of a team with 3 members — must be transferred + multi-team (make-team team-id :is-owner true :num-members 3) + result (calculate-valid-teams [default multi-team] default-id)] + (t/is (empty? (:valid-teams-to-delete-ids result))) + (t/is (= [team-id] (map :id (:valid-teams-to-transfer result)))) + (t/is (empty? (:valid-teams-to-exit result))))) + +(t/deftest calculate-valid-teams-non-owner-multi-member-team + (let [default-id (uuid/random) + team-id (uuid/random) + default (make-team default-id) + ;; non-owner member of a team with 2 members — can just exit + exit-team (make-team team-id :is-owner false :num-members 2) + result (calculate-valid-teams [default exit-team] default-id)] + (t/is (empty? (:valid-teams-to-delete-ids result))) + (t/is (empty? (:valid-teams-to-transfer result))) + (t/is (= [team-id] (map :id (:valid-teams-to-exit result)))))) + +(t/deftest calculate-valid-teams-mixed + (let [default-id (uuid/random) + solo-id (uuid/random) + transfer-id (uuid/random) + exit-id (uuid/random) + default (make-team default-id) + solo-team (make-team solo-id :is-owner true :num-members 1) + transfer-team (make-team transfer-id :is-owner true :num-members 2) + exit-team (make-team exit-id :is-owner false :num-members 3) + result (calculate-valid-teams [default solo-team transfer-team exit-team] default-id)] + (t/is (= #{solo-id} (:valid-teams-to-delete-ids result))) + (t/is (= [transfer-id] (map :id (:valid-teams-to-transfer result)))) + (t/is (= [exit-id] (map :id (:valid-teams-to-exit result)))) + (t/is (= default-id (:id (:valid-default-team result)))))) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Integration: combined delete + leave + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest leave-org-combined-delete-and-leave + (let [profile-owner (th/create-profile* 1 {:is-active true}) + profile-user (th/create-profile* 2 {:is-active true}) + ;; team1: profile-user is sole owner — must delete + team1 (th/create-team* 1 {:profile-id (:id profile-user)}) + ;; team2: profile-user owns it, profile-owner is also member — must transfer + team2 (th/create-team* 2 {:profile-id (:id profile-user)}) + _ (th/create-team-role* {:team-id (:id team2) + :profile-id (:id profile-owner) + :role :editor}) + ;; team3: profile-owner owns it, profile-user is non-owner member — can exit + team3 (th/create-team* 3 {:profile-id (:id profile-owner)}) + _ (th/create-team-role* {:team-id (:id team3) + :profile-id (:id profile-user) + :role :editor}) + org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) + + org-id (uuid/random) + your-penpot-id (:id org-default-team) + + org-summary (make-org-summary + :org-id org-id + :org-name "Test Org" + :owner-id (:id profile-owner) + :your-penpot-teams [your-penpot-id] + :org-teams [(:id team1) (:id team2) (:id team3)])] + + (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + (let [data {::th/type :leave-org + ::rpc/profile-id (:id profile-user) + :org-id org-id + :org-name "Test Org" + :default-team-id your-penpot-id + :teams-to-delete [(:id team1)] + :teams-to-leave [{:id (:id team2) :reassign-to (:id profile-owner)} + {:id (:id team3)}]} + out (th/command! data)] + + (t/is (th/success? out)) + + ;; team1 should be soft-deleted + (let [team (th/db-get :team {:id (:id team1)} {::db/remove-deleted false})] + (t/is (some? (:deleted-at team)))) + + ;; profile-user should no longer be a member of team2 + (let [rel (th/db-get :team-profile-rel {:team-id (:id team2) :profile-id (:id profile-user)})] + (t/is (nil? rel))) + + ;; profile-owner should now own team2 + (let [rel (th/db-get :team-profile-rel {:team-id (:id team2) :profile-id (:id profile-owner)})] + (t/is (true? (:is-owner rel)))) + + ;; profile-user should no longer be a member of team3 + (let [rel (th/db-get :team-profile-rel {:team-id (:id team3) :profile-id (:id profile-user)})] + (t/is (nil? rel))) + + ;; team3 itself should still exist (profile-owner is still there) + (let [team (th/db-get :team {:id (:id team3)})] + (t/is (some? team))))))) (t/deftest leave-org-error-teams-to-delete-incomplete (let [profile-owner (th/create-profile* 1 {:is-active true}) profile-user (th/create-profile* 2 {:is-active true}) @@ -350,6 +500,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) :org-id org-id + :org-name "Test Org" :default-team-id your-penpot-id :teams-to-delete [(:id team1)] :teams-to-leave []} @@ -384,6 +535,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) :org-id org-id + :org-name "Test Org" :default-team-id your-penpot-id :teams-to-delete [(:id team1)] :teams-to-leave []} @@ -418,6 +570,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) :org-id org-id + :org-name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave []} @@ -451,6 +604,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) :org-id org-id + :org-name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave [{:id (:id team1) :reassign-to (:id profile-user)}]} @@ -486,6 +640,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) :org-id org-id + :org-name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave [{:id (:id team1) :reassign-to (:id profile-other)}]} @@ -520,6 +675,7 @@ (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) :org-id org-id + :org-name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave [{:id (:id team1) :reassign-to (:id profile-owner)}]} diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index cecb34d2ad..3ac4f1eee6 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -199,8 +199,10 @@ (ptk/reify ::change-team-role ptk/WatchEvent - (watch [_ _ _] - (rx/of (ntf/info (get-change-role-msg role)))) + (watch [_ state _] + (let [current-team-id (:current-team-id state)] + (when (= team-id current-team-id) + (rx/of (ntf/info (get-change-role-msg role)))))) ptk/UpdateEvent (update [_ state] diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 7e98a0eaa4..eddcb5a503 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -23,6 +23,7 @@ [app.main.data.helpers :as dsh] [app.main.data.modal :as modal] [app.main.data.notifications :as ntf] + [app.main.data.team :as dtm] [app.main.data.websocket :as dws] [app.main.repo :as rp] [app.main.store :as st] @@ -710,6 +711,22 @@ team-name (assoc :name team-name)))) state)))) +(defn- handle-user-org-change + [{:keys [organization-id organization-name notification]}] + (ptk/reify ::handle-user-org-change + ptk/WatchEvent + (watch [_ state _] + (when (and notification (contains? cf/flags :nitrate)) + (let [team-id (:current-team-id state) + team (dm/get-in state [:teams team-id])] + (rx/of (ntf/show {:content (tr notification organization-name) + :type :toast + :level :info + :timeout nil}) + (dtm/fetch-teams) + ;; When the user is currently on a team of the org + (when (= organization-id (:organization-id team)) + (dcm/go-to-dashboard-recent {:team-id :default})))))))) (defn- process-message [{:keys [type] :as msg}] @@ -718,6 +735,7 @@ :team-role-change (handle-change-team-role msg) :team-membership-change (dcm/team-membership-change msg) :team-org-change (handle-change-team-org msg) + :user-org-change (handle-user-org-change msg) nil)) diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs index 57fb1299ca..f3bb5207bf 100644 --- a/frontend/src/app/main/data/team.cljs +++ b/frontend/src/app/main/data/team.cljs @@ -41,16 +41,19 @@ ptk/UpdateEvent (update [_ state] - (reduce (fn [state {:keys [id organization-id] :as team}] - (let [team-updated (cond-> (merge (dm/get-in state [:teams id]) team) - (not organization-id) (dissoc :organization-id - :organization-name - :organization-slug - :organization-owner-id - :organization-avatar-bg-url))] - (update state :teams assoc id team-updated))) - state - teams)))) + (let [team-ids (map :id teams) + ;; Delete old teams from state + state (update state :teams #(select-keys % team-ids))] + (reduce (fn [state {:keys [id organization-id] :as team}] + (let [team-updated (cond-> (merge (dm/get-in state [:teams id]) team) + (not organization-id) (dissoc :organization-id + :organization-name + :organization-slug + :organization-owner-id + :organization-avatar-bg-url))] + (update state :teams assoc id team-updated))) + state + teams))))) (defn fetch-teams [] diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 1813fa819a..32e4b012c1 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -544,7 +544,7 @@ (tr "dashboard.your-penpot") (:name team))))) - (mf/with-effect [] + (mf/with-effect [team] (st/emit! (dtm/fetch-members))) [:* @@ -1063,7 +1063,7 @@ (tr "dashboard.your-penpot") (:name team))))) - (mf/with-effect [] + (mf/with-effect [team] (st/emit! (dtm/fetch-invitations))) [:* diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 88d8069247..e211bdd349 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -350,6 +350,9 @@ msgstr "This team no longer belongs to the organization %s" msgid "dashboard.team-belong-org" msgstr "This team now belongs to %s" +msgid "dashboard.user-no-longer-belong-org" +msgstr "You are no longer a member of the organization %s" + #: src/app/main/ui/dashboard/placeholder.cljs:41 msgid "dashboard.add-file" msgstr "Add file" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 0c319b4277..96a033f662 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -359,6 +359,9 @@ msgstr "Este equipo ya no pertenece a la organización %s" msgid "dashboard.team-belong-org" msgstr "Este equipo ahora pertenece a la organización %s" +msgid "dashboard.user-no-longer-belong-org" +msgstr "Ya no perteneces a la organización %s" + #: src/app/main/ui/dashboard/placeholder.cljs:41 msgid "dashboard.add-file" msgstr "Añadir archivo" From 73b55ee47e5f418243cd1e0184abf915f16bb200 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 20 Apr 2026 09:30:41 +0200 Subject: [PATCH 169/288] :sparkles: Add nitrate api method get-remove-from-org-summary --- backend/src/app/rpc/commands/nitrate.clj | 3 +- backend/src/app/rpc/management/nitrate.clj | 30 ++++ .../rpc_management_nitrate_test.clj | 149 ++++++++++++++++++ 3 files changed, 181 insertions(+), 1 deletion(-) diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index f02c36197f..d8a233931d 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -176,7 +176,8 @@ :code :not-valid-teams)))) -(defn leave-org [{:keys [::db/conn] :as cfg} {:keys [profile-id org-id org-name default-team-id teams-to-delete teams-to-leave skip-validation] :as params}] +(defn leave-org + [{:keys [::db/conn] :as cfg} {:keys [profile-id org-id org-name default-team-id teams-to-delete teams-to-leave skip-validation] :as params}] (let [org-prefix (str "[" (d/sanitize-string org-name) "] ") default-team-files-count (-> (db/exec-one! conn [sql:get-team-files-count default-team-id]) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index e173b1a0a6..2a22193d01 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -393,3 +393,33 @@ RETURNING id, name;") (notifications/notify-user-removed-from-org cfg profile-id org-id org-name "dashboard.user-no-longer-belong-org") nil)) +;; API: get-remove-from-org-summary + +(def ^:private schema:get-remove-from-org-summary-result + [:map + [:teams-to-delete ::sm/int] + [:teams-to-transfer ::sm/int] + [:teams-to-exit ::sm/int]]) + +(sv/defmethod ::get-remove-from-org-summary + "Get a summary of the teams that would be deleted, transferred, or exited + if the user were removed from the organization" + {::doc/added "2.16" + ::sm/params [:map + [:profile-id ::sm/uuid] + [:org-id ::sm/uuid] + [:default-team-id ::sm/uuid]] + ::sm/result schema:get-remove-from-org-summary-result + ::db/transaction true} + [cfg {:keys [profile-id org-id default-team-id]}] + (let [{:keys [valid-teams-to-delete-ids + valid-teams-to-transfer + valid-teams-to-exit + valid-default-team]} (cnit/get-valid-teams cfg org-id profile-id default-team-id)] + (when-not valid-default-team + (ex/raise :type :validation + :code :not-valid-teams)) + {:teams-to-delete (count valid-teams-to-delete-ids) + :teams-to-transfer (count valid-teams-to-transfer) + :teams-to-exit (count valid-teams-to-exit)})) + diff --git a/backend/test/backend_tests/rpc_management_nitrate_test.clj b/backend/test/backend_tests/rpc_management_nitrate_test.clj index 1a938bee22..86a6d2ecb7 100644 --- a/backend/test/backend_tests/rpc_management_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_management_nitrate_test.clj @@ -239,6 +239,9 @@ (fn [_cfg method _params] (case method :get-org-summary org-summary + :get-org-membership {:organization-id (:id org-summary) + :is-member true} + :remove-profile-from-org nil nil))) (t/deftest remove-from-org-happy-path-no-extra-teams @@ -438,3 +441,149 @@ (t/is (not (th/success? out))) (t/is (= :validation (th/ex-type (:error out)))) (t/is (= :nobody-to-reassign-team (th/ex-code (:error out)))))) + +;; Tests: get-remove-from-org-summary +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest get-remove-from-org-summary-no-extra-teams + ;; User only has a default team — nothing to delete/transfer/exit. + (let [org-owner (th/create-profile* 1 {:is-active true}) + user (th/create-profile* 2 {:is-active true}) + org-team (th/create-team* 1 {:profile-id (:id user)}) + org-id (uuid/random) + org-summary (make-org-summary + :org-id org-id + :org-name "Acme Org" + :owner-id (:id org-owner) + :your-penpot-teams [(:id org-team)] + :org-teams []) + out (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + (management-command-with-nitrate! + {::th/type :get-remove-from-org-summary + ::rpc/profile-id (:id org-owner) + :profile-id (:id user) + :org-id org-id + :default-team-id (:id org-team)}))] + (t/is (th/success? out)) + (t/is (= {:teams-to-delete 0 + :teams-to-transfer 0 + :teams-to-exit 0} + (:result out))))) + +(t/deftest get-remove-from-org-summary-with-teams-to-delete + ;; User owns a sole-member extra org team → 1 to delete. + (let [org-owner (th/create-profile* 1 {:is-active true}) + user (th/create-profile* 2 {:is-active true}) + extra-team (th/create-team* 3 {:profile-id (:id user)}) + org-team (th/create-team* 99 {:profile-id (:id user)}) + org-id (uuid/random) + org-summary (make-org-summary + :org-id org-id + :org-name "Acme Org" + :owner-id (:id org-owner) + :your-penpot-teams [(:id org-team)] + :org-teams [(:id extra-team)]) + out (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + (management-command-with-nitrate! + {::th/type :get-remove-from-org-summary + ::rpc/profile-id (:id org-owner) + :profile-id (:id user) + :org-id org-id + :default-team-id (:id org-team)}))] + (t/is (th/success? out)) + (t/is (= {:teams-to-delete 1 + :teams-to-transfer 0 + :teams-to-exit 0} + (:result out))))) + +(t/deftest get-remove-from-org-summary-with-teams-to-transfer + ;; User owns a multi-member extra org team → 1 to transfer. + (let [org-owner (th/create-profile* 1 {:is-active true}) + user (th/create-profile* 2 {:is-active true}) + candidate (th/create-profile* 3 {:is-active true}) + extra-team (th/create-team* 4 {:profile-id (:id user)}) + _ (th/create-team-role* {:team-id (:id extra-team) + :profile-id (:id candidate) + :role :editor}) + org-team (th/create-team* 99 {:profile-id (:id user)}) + org-id (uuid/random) + org-summary (make-org-summary + :org-id org-id + :org-name "Acme Org" + :owner-id (:id org-owner) + :your-penpot-teams [(:id org-team)] + :org-teams [(:id extra-team)]) + out (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + (management-command-with-nitrate! + {::th/type :get-remove-from-org-summary + ::rpc/profile-id (:id org-owner) + :profile-id (:id user) + :org-id org-id + :default-team-id (:id org-team)}))] + (t/is (th/success? out)) + (t/is (= {:teams-to-delete 0 + :teams-to-transfer 1 + :teams-to-exit 0} + (:result out))))) + +(t/deftest get-remove-from-org-summary-with-teams-to-exit + ;; User is a non-owner member of an org team → 1 to exit. + (let [org-owner (th/create-profile* 1 {:is-active true}) + user (th/create-profile* 2 {:is-active true}) + extra-team (th/create-team* 5 {:profile-id (:id org-owner)}) + _ (th/create-team-role* {:team-id (:id extra-team) + :profile-id (:id user) + :role :editor}) + org-team (th/create-team* 99 {:profile-id (:id user)}) + org-id (uuid/random) + org-summary (make-org-summary + :org-id org-id + :org-name "Acme Org" + :owner-id (:id org-owner) + :your-penpot-teams [(:id org-team)] + :org-teams [(:id extra-team)]) + out (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + (management-command-with-nitrate! + {::th/type :get-remove-from-org-summary + ::rpc/profile-id (:id org-owner) + :profile-id (:id user) + :org-id org-id + :default-team-id (:id org-team)}))] + (t/is (th/success? out)) + (t/is (= {:teams-to-delete 0 + :teams-to-transfer 0 + :teams-to-exit 1} + (:result out))))) + +(t/deftest get-remove-from-org-summary-does-not-mutate + ;; Calling the summary endpoint must not modify any teams. + (let [org-owner (th/create-profile* 1 {:is-active true}) + user (th/create-profile* 2 {:is-active true}) + extra-team (th/create-team* 6 {:profile-id (:id user)}) + org-team (th/create-team* 99 {:profile-id (:id user)}) + org-id (uuid/random) + org-summary (make-org-summary + :org-id org-id + :org-name "Acme Org" + :owner-id (:id org-owner) + :your-penpot-teams [(:id org-team)] + :org-teams [(:id extra-team)]) + _ (with-redefs [nitrate/call (nitrate-call-mock org-summary)] + (management-command-with-nitrate! + {::th/type :get-remove-from-org-summary + ::rpc/profile-id (:id org-owner) + :profile-id (:id user) + :org-id org-id + :default-team-id (:id org-team)}))] + ;; Both teams must still exist and be undeleted + (let [t1 (th/db-get :team {:id (:id org-team)})] + (t/is (some? t1)) + (t/is (nil? (:deleted-at t1)))) + (let [t2 (th/db-get :team {:id (:id extra-team)})] + (t/is (some? t2)) + (t/is (nil? (:deleted-at t2)))) + ;; User must still be a member of both teams + (let [rel1 (th/db-get :team-profile-rel {:team-id (:id org-team) :profile-id (:id user)})] + (t/is (some? rel1))) + (let [rel2 (th/db-get :team-profile-rel {:team-id (:id extra-team) :profile-id (:id user)})] + (t/is (some? rel2))))) From 003b54421dcc5c6e5651d4cd09c0e4b577525ce2 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Mon, 20 Apr 2026 12:23:47 +0200 Subject: [PATCH 170/288] :bug: Fix empty warning on login (#9056) --- frontend/src/app/main/ui/auth/login.cljs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index 67674878d2..4382f95327 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -36,8 +36,8 @@ (mf/defc demo-warning* [] [:> context-notification* - {:level :warning - :content (tr "auth.demo-warning")}]) + {:level :warning} + (tr "auth.demo-warning")]) (defn create-demo-profile [] From adea81ceeec64c0ec16f259ee9c652287c9cd588 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Mon, 20 Apr 2026 15:32:55 +0200 Subject: [PATCH 171/288] :recycle: Update icon on typography section and scss files (#9021) * :recycle: Update icon on new buttons * :recycle: Update scss on typography section --- .../styles/common/refactor/design-tokens.scss | 44 +- .../styles/common/refactor/mixins.scss | 2 + .../sidebar/assets/typographies.scss | 1 + .../sidebar/options/menus/typography.cljs | 51 +- .../sidebar/options/menus/typography.scss | 645 +++++++----------- frontend/translations/es.po | 2 +- 6 files changed, 296 insertions(+), 449 deletions(-) diff --git a/frontend/resources/styles/common/refactor/design-tokens.scss b/frontend/resources/styles/common/refactor/design-tokens.scss index 2acb81398a..9d894e65a0 100644 --- a/frontend/resources/styles/common/refactor/design-tokens.scss +++ b/frontend/resources/styles/common/refactor/design-tokens.scss @@ -10,11 +10,9 @@ // BASE COLORS --canvas-background-color: var(--color-background-primary); --canvas-fill-color: var(--color-canvas); - --scrollbar-background-color: var(--color-foreground-secondary); --panel-background-color: var(--color-background-primary); --panel-border-color: var(--color-background-quaternary); - --app-background: var(--color-background-primary); --loader-background: var(--color-background-primary); @@ -26,7 +24,6 @@ --button-foreground-color-disabled: var(--color-foreground-disabled); --button-background-color-disabled: var(--color-background-quaternary); --button-border-color-disabled: var(--color-background-quaternary); - --button-primary-background-color-rest: var(--color-accent-primary); --button-primary-border-color-rest: var(--color-accent-primary); --button-primary-foreground-color-rest: var(--color-background-secondary); @@ -39,7 +36,6 @@ --button-primary-background-color-focus: var(--color-background-tertiary); --button-primary-border-color-focus: var(--color-accent-primary); --button-primary-foreground-color-focus: var(--color-foreground-secondary); - --button-secondary-background-color-rest: var(--color-background-tertiary); --button-secondary-border-color-rest: var(--color-background-tertiary); --button-secondary-foreground-color-rest: var(--color-foreground-secondary); @@ -52,7 +48,6 @@ --button-secondary-background-color-focus: var(--color-background-tertiary); --button-secondary-border-color-focus: var(--color-accent-primary); --button-secondary-foreground-color-focus: var(--color-foreground-secondary); - --button-tertiary-foreground-color-rest: var(--color-foreground-secondary); --button-tertiary-background-color-hover: var(--color-background-quaternary); --button-tertiary-border-color-hover: var(--color-background-quaternary); @@ -63,16 +58,13 @@ --button-tertiary-background-color-focus: var(--color-background-tertiary); --button-tertiary-border-color-focus: var(--color-accent-primary); --button-tertiary-foreground-color-focus: var(--color-foreground-primary); - --expand-button-icon-border-width: 0; --expand-button-icon-border-width-selected: 0; - --button-icon-foreground-color: var(--color-foreground-secondary); --button-icon-foreground-color-hover: var(--color-foreground-secondary); --button-icon-background-color-selected: var(--color-background-quaternary); --button-icon-foreground-color-selected: var(--color-accent-primary); --button-icon-border-color-selected: var(--color-background-quaternary); - --button-radio-background-color-rest: var(--color-background-tertiary); --button-radio-border-color-rest: var(--color-background-tertiary); --button-radio-foreground-color-rest: var(--color-foreground-secondary); @@ -84,20 +76,16 @@ --button-radio-background-color-focus: var(--color-background-tertiary); --button-radio-border-color-focus: var(--color-accent-primary); --button-radio-foreground-color-focus: var(--color-foreground-secondary); - --button-warning-background-color-rest: var(--status-color-warning-500); --button-warning-border-color-rest: var(--status-color-warning-500); --button-warning-foreground-color-rest: var(--color-background-secondary); - --button-disabled-background-color-rest: var(--color-background-disabled); --button-disabled-border-color-rest: var(--color-background-disabled); --button-disabled-foreground-color-rest: var(--color-foreground-disabled); - --button-constraint-background-color-rest: var(--color-foreground-secondary); --button-constraint-border-color-rest: var(--color-background-tertiary); --button-constraint-border-color-hover: var(--color-accent-primary-muted); --button-constraint-background-color-hover: var(--color-accent-primary); - --constraint-widget-background-color: var(--color-background-tertiary); --constraint-center-area-background-color: var(--color-background-primary); @@ -144,7 +132,6 @@ --palette-button-shadow-initial: var(--color-background-primary); --palette-button-shadow-final: transparent; --palette-handler-background-color: var(--color-background-quaternary); - --color-bullet-background-color: var(--app-white); // We don't want this color to change with palette --color-bullet-border-color: var(--color-background-quaternary); --color-bullet-border-color-selected: var(--color-accent-primary); @@ -183,7 +170,6 @@ --input-border-color-error: var(--status-color-error-500); --input-border-color-success: var(--color-accent-primary); --input-details-color: var(--color-background-primary); - --input-checkbox-background-color-rest: var(--color-background-quaternary); --input-checkbox-border-color-rest: var(--color-foreground-secondary); --input-checkbox-border-color-active: var(--color-background-quaternary); @@ -200,7 +186,6 @@ --input-checkbox-background-color-active: var(--color-accent-primary); --input-checkbox-foreground-color-active: var(--color-background-primary); --input-checkbox-text-foreground-color: var(--color-foreground-secondary); - --menu-background-color: var(--color-background-tertiary); --menu-foreground-color: var(--color-foreground-primary); --menu-icon-foreground-color: var(--color-foreground-secondary); @@ -219,7 +204,6 @@ --menu-background-color-disabled: var(--color-background-primary); --menu-foreground-color-disabled: var(--color-foreground-secondary); --menu-border-color-disabled: var(--color-background-quaternary); - --context-menu-background-color: var(--color-background-tertiary); --context-menu-foreground-color: var(--color-foreground-secondary); --context-menu-background-color-selected: var(--color-background-quaternary); @@ -243,34 +227,27 @@ --assets-component-border-selected: var(--color-accent-tertiary); --assets-component-second-border-selected: var(--color-background-primary); --assets-component-hightlight: var(--color-accent-secondary); - --radio-btns-background-color: var(--color-background-tertiary); --radio-btn-background-color-selected: var(--color-background-quaternary); --radio-btn-foreground-color: var(--color-foreground-secondary); --radio-btn-foreground-color-selected: var(--color-accent-primary); --radio-btn-border-color: var(--color-background-tertiary); --radio-btn-border-color-selected: var(--color-background-quaternary); - --library-name-foreground-color: var(--color-foreground-primary); --library-content-foreground-color: var(--color-foreground-secondary); - --dropdown-background-color: var(--color-background-tertiary); --dropdown-separator-color: var(--color-background-primary); --profile-drowpdown-background-color: var(--color-background-primary); - --not-found-background-color: var(--color-background-tertiary); --not-found-foreground-color: var(--color-foreground-secondary); - --entry-foreground-color: var(--color-foreground-secondary); --entry-background-color: var(--color-background-tertiary); --entry-background-color-disabled: var(--color-background-primary); --entry-border-color-disabled: var(--color-background-quaternary); --entry-foreground-color-hover: var(--color-foreground-primary); --entry-background-color-hover: var(--color-background-quaternary); - --empty-message-background-color: var(--color-background-tertiary); --empty-message-foreground-color: var(--color-foreground-secondary); - --user-count-background-color: var(--color-accent-primary); --user-count-foreground-color: var(--color-background-secondary); @@ -323,32 +300,25 @@ --alert-text-foreground-color-default: var(--color-foreground-primary); --alert-icon-foreground-color-default: var(--color-foreground-primary); --alert-border-color-default: var(--color-background-quaternary); - --alert-background-color-success: var(--color-background-success); --alert-text-foreground-color-success: var(--color-foreground-primary); --alert-icon-foreground-color-success: var(--color-accent-success); --alert-border-color-success: var(--color-accent-success); - --alert-background-color-warning: var(--color-background-warning); --alert-text-foreground-color-warning: var(--color-foreground-primary); --alert-icon-foreground-color-warning: var(--color-accent-warning); --alert-border-color-warning: var(--color-accent-warning); - --alert-background-color-error: var(--color-background-error); --alert-text-foreground-color-error: var(--color-foreground-primary); --alert-icon-foreground-color-error: var(--color-accent-error); --alert-border-color-error: var(--color-accent-error); - --alert-background-color-info: var(--color-background-info); --alert-text-foreground-color-info: var(--color-foreground-primary); --alert-icon-foreground-color-info: var(--color-accent-info); --alert-border-color-info: var(--color-accent-info); - --alert-text-foreground-color-focus: var(--color-accent-primary); --alert-border-color-focus: var(--color-accent-primary); - --notification-foreground-color-default: var(--color-foreground-secondary); - --element-foreground-warning: var(--status-color-warning-500); --element-foreground-error: var(--status-color-error-500); @@ -368,21 +338,16 @@ --search-bar-foreground-color: var(--color-foreground-primary); --search-bar-icon-foreground-color: var(--color-foreground-secondary); --search-bar-icon-foreground-color-hover: var(--color-accent-primary); - --pill-background-color: var(--color-background-tertiary); --pill-foreground-color: var(--color-foreground-primary); - --link-foreground-color: var(--color-accent-primary); - --register-confirmation-color: var(--status-color-success-200); //TODO: review this color - + --register-confirmation-color: var(--status-color-success-200); // TODO: review this color --resize-area-background-color: var(--color-background-primary); --resize-area-border-color: var(--color-background-quaternary); - --profile-section-background-color: var(--color-background-tertiary); --dashboard-list-background-color: var(--color-background-tertiary); --dashboard-list-foreground-color: var(--color-foreground-primary); --dashboard-list-text-foreground-color: var(--color-foreground-secondary); - --communication-tag-background-color: var(--color-foreground-primary); --communication-tag-foreground-color: var(--color-background-tertiary); @@ -404,7 +369,7 @@ // TODO: we should not put these functional tokens here, but rather in the components they belong to --new-team-button-background-color: var(--color-background-primary); - //DASHBOARD + // DASHBOARD --sidebar-element-foreground-color: var(--color-foreground-secondary); --sidebar-element-background-color-hover: var(--color-background-secondary); --sidebar-element-foreground-color-hover: var(--color-accent-primary); @@ -422,21 +387,16 @@ --tab-background-color-selected: var(--color-background-primary); --tab-border-color: var(--color-background-tertiary); --tab-border-color-selected: var(--color-background-secondary); - --radio-btns-background-color: var(--color-background-tertiary); --radio-btn-background-color-selected: var(--color-background-primary); --radio-btn-foreground-color: var(--color-foreground-secondary); --radio-btn-foreground-color-selected: var(--color-accent-primary); --radio-btn-border-color: var(--color-background-tertiary); --radio-btn-border-color-selected: var(--color-background-secondary); - --button-icon-background-color-selected: var(--color-background-primary); --button-icon-border-color-selected: var(--color-background-secondary); - --assets-item-name-foreground-color: var(--color-foreground-primary); - --text-editor-selection-background-color: var(--la-tertiary-70); --expand-button-icon-border-width-selected: 2px; - --colorpicker-background-color: var(--color-background-primary); } diff --git a/frontend/resources/styles/common/refactor/mixins.scss b/frontend/resources/styles/common/refactor/mixins.scss index c4d07d09e2..356c2ca7da 100644 --- a/frontend/resources/styles/common/refactor/mixins.scss +++ b/frontend/resources/styles/common/refactor/mixins.scss @@ -137,6 +137,7 @@ @mixin inspectValue { @include bodySmallTypography; + display: inline-block; width: fit-content; padding: 0; @@ -167,6 +168,7 @@ 0% { transform: rotate(0deg); } + 100% { transform: rotate(359deg); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.scss index d00eeb20f9..d44f702823 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.scss @@ -27,6 +27,7 @@ margin-bottom: deprecated.$s-4; border-radius: deprecated.$br-8; background-color: var(--assets-item-background-color); + max-inline-size: var(--options-width); } .dragging { diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 90124b22aa..ba2380bfba 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -28,8 +28,9 @@ [app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.components.select :refer [select]] [app.main.ui.context :as ctx] + [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] - [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.icons :as deprecated-icon] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -82,8 +83,10 @@ :on-click on-click} [:div {:class (stl/css-case :font-item true :selected is-current)} - [:span {:class (stl/css :label)} (:name font)] - [:span {:class (stl/css :icon)} (when is-current deprecated-icon/tick)]]])) + [:span {:class (stl/css :font-item-label)} (:name font)] + (when is-current + [:> icon* {:icon-id i/tick + :size "s"}])]])) (declare row-renderer) @@ -191,7 +194,7 @@ :placeholder (tr "workspace.options.search-font")}] (when (and recent-fonts show-recent) [:section {:class (stl/css :show-recent)} - [:p {:class (stl/css :title)} (tr "workspace.options.recent-fonts")] + [:p {:class (stl/css :header-title)} (tr "workspace.options.recent-fonts")] (for [[idx font] (d/enumerate recent-fonts)] [:> font-item* {:key (dm/str "font-" idx) :font font @@ -316,10 +319,11 @@ (some? font) [:* - [:span {:class (stl/css :name)} + [:span {:class (stl/css :font-option-name)} (:name font)] - [:span {:class (stl/css :icon)} - deprecated-icon/arrow]] + [:> icon* {:icon-id i/arrow-down + :class (stl/css :dropdown-icon) + :size "s"}]] :else (tr "dashboard.fonts.deleted-placeholder"))] @@ -524,15 +528,15 @@ [:div {:class (stl/css :action-btns)} (when show-actions? [:* - [:> icon-button* {:variant "ghost" + [:> icon-button* {:variant "action" :aria-label (tr "workspace.assets.duplicate") :on-click on-duplicate - :icon i/add}] - [:> icon-button* {:variant "ghost" + :icon i/clipboard}] + [:> icon-button* {:variant "action" :aria-label (tr "workspace.assets.delete") :on-click on-delete :icon i/delete}]]) - [:> icon-button* {:variant "ghost" + [:> icon-button* {:variant "action" :aria-label (tr "labels.close") :on-click on-close :icon i/tick}]]] @@ -555,9 +559,10 @@ (:name typography)] [:span {:class (stl/css :typography-font)} (:name font-data)] - [:div {:class (stl/css :action-btn) - :on-click on-close} - deprecated-icon/menu]] + [:> icon-button* {:variant "ghost" + :aria-label (tr "labels.close") + :on-click on-close + :icon i/menu}]] [:div {:class (stl/css :info-row)} [:span {:class (stl/css :info-label)} (tr "workspace.assets.typography.font-style")] @@ -580,8 +585,8 @@ [:span {:class (stl/css :info-content)} (:text-transform typography)]] (when-not local? - [:a {:class (stl/css :link-btn) - :on-click navigate-to-library} + [:> button* {:variant "secondary" + :on-click navigate-to-library} (tr "workspace.assets.typography.go-to-edit")])])]))) (mf/defc typography-entry @@ -686,12 +691,14 @@ (:name font-data)])]) [:div {:class (stl/css :element-set-actions)} (when ^boolean on-detach - [:button {:class (stl/css :element-set-actions-button) - :on-click on-detach} - deprecated-icon/detach]) - [:button {:class (stl/css :menu-btn) - :on-click on-open} - deprecated-icon/menu]]] + [:> icon-button* {:variant "action" + :aria-label (tr "settings.detach") + :on-click on-detach + :icon i/detach}]) + [:> icon-button* {:variant "action" + :aria-label (tr "labels.open") + :on-click on-open + :icon i/menu}]]] [:& typography-advanced-options {:visible? open? diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss index 29213fd2ac..d01a04d186 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss @@ -5,64 +5,59 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; +@use "ds/typography.scss" as t; +@use "ds/_sizes.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/mixins.scss" as *; +@use "ds/_utils.scss" as *; .typography-entry { + --actions-visibility: hidden; + --typography-entry-background-color: var(--color-background-tertiary); + --typography-entry-foreground-color: var(--color-foreground-primary); + --typography-entry-border-color: transparent; + display: flex; flex-direction: row; align-items: center; - height: deprecated.$s-32; - width: 100%; - border-radius: deprecated.$br-8; - background-color: var(--assets-item-background-color); - color: var(--assets-item-name-foreground-color-hover); + block-size: $sz-32; + inline-size: 100%; + border-radius: $br-8; + background-color: var(--typography-entry-background-color); + color: var(--typography-entry-foreground-color); + border: $b-1 solid var(--typography-entry-border-color); &:hover, &:focus-within { - background-color: var(--assets-item-background-color-hover); - color: var(--assets-item-name-foreground-color-hover); + --typography-entry-background-color: var(--color-background-quaternary); + --typography-entry-foreground-color: var(--color-foreground-primary); } &.selected { - border: deprecated.$s-1 solid var(--assets-item-border-color); - } - - .element-set-actions { - display: flex; - visibility: hidden; - - .element-set-actions-button, - .menu-btn { - @extend %button-tertiary; - - height: deprecated.$s-32; - width: deprecated.$s-28; - - svg { - @extend %button-icon; - } - - &:active { - background-color: transparent; - } - } + --typography-entry-border-color: var(--color-accent-primary); } &:hover { - background-color: var(--assets-item-background-color-hover); + --typography-entry-background-color: var(--color-background-quaternary); .element-set-actions { - visibility: visible; + --actions-visibility: visible; } } } +.element-set-actions { + display: flex; + visibility: var(--actions-visibility); +} + .typography-selection-wrapper { display: grid; - grid-template-columns: deprecated.$s-24 auto 1fr; + grid-template-columns: $sz-24 auto 1fr; flex: 1; - height: 100%; - width: 100%; - padding: 0 deprecated.$s-12; + block-size: 100%; + inline-size: 100%; + padding: 0 var(--sp-m); &.is-selectable { cursor: pointer; @@ -70,455 +65,333 @@ } .typography-sample { - @include deprecated.flexCenter; - - min-width: deprecated.$s-24; - height: deprecated.$s-32; - color: var(--assets-item-name-foreground-color); + display: flex; + justify-content: center; + align-items: center; + min-inline-size: $sz-24; + block-size: $sz-32; + color: var(--color-foreground-secondary); } .typography-name, .typography-font { - @include deprecated.bodySmallTypography; - @include deprecated.textEllipsis; + @include t.use-typography("body-small"); + @include text-ellipsis; display: block; align-self: center; - margin-left: deprecated.$s-6; + margin-inline-start: px2rem(6); } .typography-name { - color: var(--assets-item-name-foreground-color); + color: var(--color-foreground-primary); } .typography-font { - color: var(--assets-item-name-foreground-color-rest); + color: var(--color-foreground-secondary); } .font-name-wrapper { - @include deprecated.bodySmallTypography; + --font-name-wrapper-foreground-color: var(--color-foreground-primary); + --font-name-wrapper-background-color: var(--color-background-tertiary); + --font-name-wrapper-border-color: transparent; + + @include t.use-typography("body-small"); display: flex; align-items: center; - height: deprecated.$s-32; - width: 100%; - border-radius: deprecated.$br-8; - border: deprecated.$s-1 solid transparent; + block-size: $sz-32; + inline-size: 100%; + border-radius: $br-8; + border: $b-1 solid var(--font-name-wrapper-border-color); box-sizing: border-box; - background-color: var(--assets-item-background-color); - margin-bottom: deprecated.$s-4; - padding: deprecated.$s-8 deprecated.$s-0 deprecated.$s-8 deprecated.$s-12; - - .typography-sample-input { - @include deprecated.flexCenter; - - width: deprecated.$s-24; - height: 100%; - font-size: deprecated.$fs-16; - color: var(--assets-item-name-foreground-color-hover); - } - - .adv-typography-name { - @include deprecated.removeInputStyle; - - font-size: deprecated.$fs-12; - color: var(--input-foreground-color-active); - flex-grow: 1; - padding-left: deprecated.$s-6; - margin: 0; - } - - .action-btn { - @extend %button-tertiary; - @include deprecated.flexCenter; - - width: deprecated.$s-28; - height: deprecated.$s-28; - - svg { - @extend %button-icon-small; - - stroke: var(--icon-foreground); - } - - &:active { - background-color: transparent; - } - } - - .action-btns { - display: flex; - align-items: center; - gap: deprecated.$s-2; - } + background-color: var(--font-name-wrapper-background-color); + margin-block-end: var(--sp-s); + padding: var(--sp-s) 0 var(--sp-s) var(--sp-m); &:focus-within { - border: deprecated.$s-1 solid var(--input-border-color-active); - - .adv-typography-name { - color: var(--input-foreground-color-active); - } + --font-name-wrapper-border-color: var(--color-accent-primary); } &:hover { - background-color: var(--assets-item-background-color-hover); + --font-name-wrapper-background-color: var(--color-background-quaternary); } } +.typography-sample-input { + display: flex; + justify-content: center; + align-items: center; + inline-size: $sz-24; + block-size: 100%; + font-size: px2rem(16); + color: var(--color-foreground-primary); +} + +.adv-typography-name { + font-size: px2rem(12); + color: var(--font-name-wrapper-foreground-color); + flex-grow: 1; + padding-inline-start: px2rem(6); + margin: 0; + border: none; + background: none; + outline: none; +} + +.action-btns { + display: flex; + align-items: center; + gap: var(--sp-xxs); +} + .advanced-options-wrapper { - height: 100%; - width: 100%; - background-color: var(--assets-title-background-color); + block-size: 100%; + inline-size: 100%; + background-color: var(--color-background-primary); } .typography-info-wrapper { - @include deprecated.flexColumn; + display: flex; + flex-direction: column; + gap: var(--sp-xxs); + margin-block-end: var(--sp-m); +} - margin-bottom: deprecated.$s-12; +.typography-name-wrapper { + @extend %asset-element; - .typography-name-wrapper { - @extend %asset-element; + display: grid; + grid-template-columns: $sz-24 auto 1fr $sz-28; + flex: 1; + block-size: $sz-32; + inline-size: 100%; + padding: 0 0 0 var(--sp-m); + background-color: var(--color-background-quaternary); + margin-block-end: var(--sp-xs); +} - display: grid; - grid-template-columns: deprecated.$s-24 auto 1fr deprecated.$s-28; - flex: 1; - height: deprecated.$s-32; - width: 100%; - padding: 0 0 0 deprecated.$s-12; - background-color: var(--assets-item-background-color-hover); - margin-bottom: deprecated.$s-4; +.typography-sample { + display: flex; + justify-content: center; + align-items: center; + min-inline-size: $sz-24; + font-size: px2rem(16); + block-size: $sz-32; + padding: 0; + color: var(--color-foreground-primary); +} - .typography-sample { - @include deprecated.flexCenter; +.typography-name, +.typography-font { + @include t.use-typography("body-small"); + @include text-ellipsis; - min-width: deprecated.$s-24; - font-size: deprecated.$fs-16; - height: deprecated.$s-32; - padding: 0; - color: var(--assets-item-name-foreground-color-hover); - } + display: flex; + align-items: center; + justify-content: flex-start; + margin-inline-start: px2rem(6); + min-inline-size: 0; + color: var(--color-foreground-primary); +} - .typography-name { - @include deprecated.bodySmallTypography; - @include deprecated.textEllipsis; +.info-row { + --calculated-width: calc(var(--right-sidebar-width) - $sz-48); - display: flex; - align-items: center; - justify-content: flex-start; - margin-left: deprecated.$s-6; - color: var(--assets-item-name-foreground-color-hover); - } + display: grid; + grid-template-columns: 50% 50%; + block-size: $sz-32; + padding-left: var(--sp-xs); +} - .typography-font { - @include deprecated.bodySmallTypography; - @include deprecated.textEllipsis; +.info-label, +.info-content { + @include t.use-typography("body-small"); + @include text-ellipsis; - margin-left: deprecated.$s-6; - display: flex; - align-items: center; - justify-content: flex-start; - min-width: 0; - color: var(--assets-item-name-foreground-color); - } - - .action-btn { - @extend %button-tertiary; - - width: deprecated.$s-28; - height: deprecated.$s-32; - - svg { - @extend %button-icon; - } - - &:active { - background-color: transparent; - } - } - } - - .info-row { - display: grid; - grid-template-columns: 50% 50%; - height: deprecated.$s-32; - - --calculated-width: calc(var(--right-sidebar-width) - deprecated.$s-48); - - padding-left: deprecated.$s-2; - - .info-label { - @include deprecated.bodySmallTypography; - @include deprecated.textEllipsis; - - width: calc(var(--calculated-width) / 2); - padding-top: deprecated.$s-8; - color: var(--assets-item-name-foreground-color); - } - - .info-content { - @include deprecated.bodySmallTypography; - @include deprecated.textEllipsis; - - padding-top: deprecated.$s-8; - width: calc(var(--calculated-width) / 2); - color: var(--assets-item-name-foreground-color-hover); - } - } - - .link-btn { - @include deprecated.uppercaseTitleTipography; - @extend %button-secondary; - - width: 100%; - height: deprecated.$s-32; - border-radius: deprecated.$br-8; - - &:hover { - background-color: var(--button-secondary-background-color-hover); - color: var(--button-secondary-foreground-color-hover); - border: deprecated.$s-1 solid var(--button-secondary-border-color-hover); - text-decoration: none; - - svg { - stroke: var(--button-secondary-foreground-color-hover); - } - } - - &:focus { - background-color: var(--button-secondary-background-color-focus); - color: var(--button-secondary-foreground-color-focus); - border: deprecated.$s-1 solid var(--button-secondary-border-color-focus); - - svg { - stroke: var(--button-secondary-foreground-color-focus); - } - } - } + inline-size: calc(var(--calculated-width) / 2); + padding-block-start: var(--sp-s); + color: var(--color-foreground-primary); } .text-options { - @include deprecated.flexColumn; - - max-width: var(--options-width); + display: flex; + flex-direction: column; + gap: var(--sp-xs); + max-inline-size: var(--options-width); &:not(.text-options-full-size) { position: relative; } +} - .font-option { - @include deprecated.bodySmallTypography; - @extend %asset-element; +.font-option { + @include t.use-typography("body-small"); + @extend %asset-element; - padding: deprecated.$s-8 0 deprecated.$s-8 deprecated.$s-8; - cursor: pointer; + padding: var(--sp-s); + cursor: pointer; +} - .name { - flex-grow: 1; - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 100%; - } +.font-option-name { + @include text-ellipsis; - .icon { - @include deprecated.flexCenter; + flex-grow: 1; + inline-size: 100%; +} - height: deprecated.$s-28; - width: deprecated.$s-28; +.font-modifiers { + display: flex; + gap: var(--sp-xs); +} - svg { - @extend %button-icon-small; +.font-size-options { + @extend %asset-element; + @include t.use-typography("body-small"); - stroke: var(--icon-foreground); - transform: rotate(90deg); - } - } - } + flex-grow: 1; + inline-size: px2rem(60); + margin: 0; + padding: 0; + border: $b-1 solid var(--color-background-tertiary); + position: relative; +} - .font-modifiers { +.font-variant-options { + padding: 0; + flex-grow: 2; +} + +.typography-variations { + display: flex; + align-items: center; + gap: var(--sp-xs); +} + +.spacing-options { + display: flex; + align-items: center; + gap: var(--sp-xs); +} + +.line-height, +.letter-spacing { + @extend %input-element; + @include t.use-typography("body-small"); + + .icon { display: flex; - gap: deprecated.$s-4; + justify-content: center; + align-items: center; + inline-size: $sz-28; - .font-size-options { - @extend %asset-element; - @include deprecated.bodySmallTypography; - - flex-grow: 1; - width: deprecated.$s-60; - margin: 0; - padding: 0; - border: deprecated.$s-1 solid var(--input-border-color); - position: relative; - - .icon { - @include deprecated.flexCenter; - - height: deprecated.$s-28; - min-width: deprecated.$s-28; - - svg { - @extend %button-icon-small; - - stroke: var(--icon-foreground); - transform: rotate(90deg); - } - } - } - - .font-variant-options { - padding: 0; - flex-grow: 2; - } - } - - .typography-variations { - @include deprecated.flexRow; - - .spacing-options { - @include deprecated.flexRow; - - .line-height, - .letter-spacing { - @extend %input-element; - @include deprecated.bodySmallTypography; - - .icon { - @include deprecated.flexCenter; - - width: deprecated.$s-28; - - svg { - @extend %button-icon-small; - } - } - } - } - - .text-transform { - @extend %asset-element; - - width: fit-content; - padding: 0; - background-color: var(--radio-btns-background-color); - - &:hover { - background-color: var(--radio-btns-background-color); - } + svg { + @extend %button-icon-small; } } } -.font-size-select { - @include deprecated.removeInputStyle; - @include deprecated.bodySmallTypography; +.text-transform { + @extend %asset-element; - height: deprecated.$s-32; - height: 100%; - width: 100%; + inline-size: fit-content; + padding: 0; + background-color: var(--color-background-tertiary); +} + +.font-size-select { + @include t.use-typography("body-small"); + + block-size: 100%; + inline-size: 100%; margin: 0; - padding: deprecated.$s-8; + padding: var(--sp-s); + border: none; + background: none; + outline: none; .numeric-input { @extend %input-base; - @include deprecated.bodySmallTypography; + @include t.use-typography("body-small"); padding: 0; } } .font-selector { - @include deprecated.flexCenter; - + display: flex; + justify-content: center; + align-items: center; position: absolute; top: 0; left: 0; right: 0; - height: 100%; - width: 100%; - z-index: deprecated.$z-index-4; + block-size: 100%; + inline-size: 100%; + z-index: var(--z-index-dropdown); } .show-recent { - border-radius: deprecated.$br-8 deprecated.$br-8 0 0; - background: var(--dropdown-background-color); - border: deprecated.$s-1 solid var(--color-background-quaternary); + border-radius: $br-8 $br-8 0 0; + background: var(--color-background-tertiary); + border: $b-1 solid var(--color-background-quaternary); border-block-end: none; } .font-selector-dropdown { - width: 100%; + inline-size: 100%; &:not(.font-selector-dropdown-full-size) { display: flex; flex-direction: column; flex-grow: 1; - height: 100%; - } - - .header { - display: grid; - row-gap: deprecated.$s-2; - - .title { - @include deprecated.uppercaseTitleTipography; - - color: var(--title-foreground-color); - margin: 0; - padding: deprecated.$s-12; - } + block-size: 100%; } } +.header { + display: grid; + row-gap: var(--sp-xxs); +} + +.header-title { + @include t.use-typography("headline-small"); + + color: var(--color-foreground-secondary); + margin: 0; + padding: var(--sp-m); +} + .font-wrapper { - padding-bottom: deprecated.$s-4; + padding-bottom: var(--sp-xs); cursor: pointer; } .font-item { @extend %asset-element; - margin-bottom: deprecated.$s-4; - border-radius: deprecated.$br-8; + margin-bottom: var(--sp-xs); + border-radius: $br-8; display: flex; - .icon { - @include deprecated.flexCenter; - - height: deprecated.$s-28; - width: deprecated.$s-28; - - svg { - @extend %button-icon-small; - - stroke: var(--icon-foreground); - } - } - &.selected { - color: var(--assets-item-name-foreground-color-hover); - - .icon { - svg { - stroke: var(--assets-item-name-foreground-color-hover); - } - } - } - - .label { - @include deprecated.bodySmallTypography; - @include deprecated.textEllipsis; - - flex-grow: 1; - min-width: 0; + color: var(--color-foreground-primary); } } +.font-item-label { + @include t.use-typography("body-small"); + @include text-ellipsis; + + flex-grow: 1; + min-inline-size: 0; +} + .font-selector-dropdown-full-size { - height: calc(100vh - 48px); // TODO: ugly hack :( Find a workaround for this. + block-size: calc(100vh - 48px); // TODO: ugly hack :( Find a workaround for this. display: grid; grid-template-rows: auto 1fr; - padding: deprecated.$s-2 deprecated.$s-12 deprecated.$s-12 deprecated.$s-12; + padding: var(--sp-xxs) var(--sp-m) var(--sp-m) var(--sp-m); } .fonts-list { @@ -526,23 +399,23 @@ display: flex; flex-direction: column; flex: 1 1 auto; - min-height: 100%; - width: 100%; - height: 100%; - padding: deprecated.$s-2; - border-radius: deprecated.$br-8; - background-color: var(--dropdown-background-color); + min-block-size: 100%; + inline-size: 100%; + block-size: 100%; + padding: var(--sp-xxs); + border-radius: $br-8; + background-color: var(--color-background-tertiary); overflow: hidden; &:not(.fonts-list-full-size) { - margin-block-start: deprecated.$s-2; + margin-block-start: var(--sp-xxs); } } .fonts-list-full-size { border-start-start-radius: 0; border-start-end-radius: 0; - border: deprecated.$s-1 solid var(--color-background-quaternary); + border: $b-1 solid var(--color-background-quaternary); // TODO: this should belong to typography-entry , but atm we don't have a clear // way of accessing whether we are in fullsize mode or not @@ -550,3 +423,7 @@ padding-inline-end: 0; } } + +.dropdown-icon { + color: var(--color-foreground-secondary); +} diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 96a033f662..55fdd29246 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -5630,7 +5630,7 @@ msgstr "Estilo de fuente" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:546 msgid "workspace.assets.typography.go-to-edit" -msgstr "Ir al archivo de la biblioteca del estilo para editar" +msgstr "Editar en biblioteca de estilo" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:536 msgid "workspace.assets.typography.letter-spacing" From 876b8d645db57876289bb60aee7df00e4fc7bfd0 Mon Sep 17 00:00:00 2001 From: Juan de la Cruz Date: Mon, 20 Apr 2026 15:33:42 +0200 Subject: [PATCH 172/288] :tada: Add new page separators feature (#8561) * :tada: Add new page separators feature * :paperclip: Add PR feedback changes * :bug: Fix page sitemap icons --------- Co-authored-by: Andrey Antukh --- CHANGES.md | 2 + .../src/app/main/data/workspace/pages.cljs | 23 ++- .../app/main/ui/ds/buttons/icon_button.cljs | 5 +- .../main/ui/ds/foundations/assets/icon.cljs | 18 +-- .../main/ui/workspace/sidebar/sitemap.cljs | 139 ++++++++++-------- .../main/ui/workspace/sidebar/sitemap.scss | 27 +++- 6 files changed, 130 insertions(+), 84 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b695345f6d..1e02ac6c45 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -43,6 +43,8 @@ - Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311) - Add a search bar to filter colors in the color palette toolbar (by @eureka0928) [Github #7653](https://github.com/penpot/penpot/issues/7653) - Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027) +- Add page separators in Workspace [Taiga #13611](https://tree.taiga.io/project/penpot/us/13611?milestone=262806) + ### :bug: Bugs fixed diff --git a/frontend/src/app/main/data/workspace/pages.cljs b/frontend/src/app/main/data/workspace/pages.cljs index 5865cb969d..222a4a5c0e 100644 --- a/frontend/src/app/main/data/workspace/pages.cljs +++ b/frontend/src/app/main/data/workspace/pages.cljs @@ -328,11 +328,24 @@ (ptk/reify ::rename-page ptk/WatchEvent (watch [it state _] - (let [page (dsh/lookup-page state id) - changes (-> (pcb/empty-changes it) - (pcb/with-page page) - (pcb/mod-page page {:name name}))] - (rx/of (dch/commit-changes changes)))))) + (let [page (dsh/lookup-page state id) + changes (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/mod-page page {:name name})) + pages (-> (dsh/lookup-file-data state) :pages) + index (d/index-of pages id) + prev-id (when (and (some? index) (pos? index)) + (nth pages (dec index) nil)) + next-id (when (some? index) + (nth pages (inc index) nil)) + fallback-page-id (or prev-id next-id) + separator? (= "---" (str/trim name))] + (rx/concat + (rx/of (dch/commit-changes changes)) + (when (and separator? + (= id (:current-page-id state)) + (some? fallback-page-id)) + (rx/of (dcm/go-to-workspace :page-id fallback-page-id)))))))) (defn- delete-page-components [changes page] diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.cljs b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs index bcfd24240e..45b0b7b1b7 100644 --- a/frontend/src/app/main/ui/ds/buttons/icon_button.cljs +++ b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs @@ -19,6 +19,7 @@ [:tooltip-class {:optional true} [:maybe :string]] [:type {:optional true} [:maybe [:enum "button" "submit" "reset"]]] [:icon-class {:optional true} :string] + [:icon-size {:optional true} [:maybe [:enum "s" "m" "l"]]] [:icon [:and :string [:fn #(contains? icon-list %)]]] [:aria-label :string] @@ -30,7 +31,7 @@ (mf/defc icon-button* {::mf/schema schema:icon-button ::mf/memo true} - [{:keys [class icon icon-class variant aria-label children tooltip-placement tooltip-class type] :rest props}] + [{:keys [class icon icon-class icon-size variant aria-label children tooltip-placement tooltip-class type] :rest props}] (let [variant (d/nilv variant "primary") @@ -60,5 +61,5 @@ :placement tooltip-placement :id tooltip-id} [:> :button props - [:> icon* {:icon-id icon :aria-hidden true :class icon-class}] + [:> icon* {:icon-id icon :aria-hidden true :class icon-class :size icon-size}] children]])) diff --git a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs index 0203bf9999..f2ccc02d4a 100644 --- a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs +++ b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs @@ -322,22 +322,16 @@ (mf/defc icon* {::mf/schema schema:icon} [{:keys [icon-id size class] :rest props}] - (let [props (mf/spread-props props - {:class [class (stl/css :icon)] - :width icon-size-m - :height icon-size-m}) - - size-px (cond (= size "l") icon-size-l + (let [size-px (cond (= size "l") icon-size-l (= size "s") icon-size-s :else icon-size-m) - offset (if (or (= size "s") (= size "m")) - (/ (- icon-size-m size-px) 2) - 0)] + props (mf/spread-props props + {:class [class (stl/css :icon)] + :width size-px + :height size-px})] [:> :svg props [:use {:href (dm/str "#icon-" icon-id) :width size-px - :height size-px - :x offset - :y offset}]])) + :height size-px}]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index 9c62ed393f..3c683a80ef 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.types.page :as ctp] [app.main.data.common :as dcm] [app.main.data.helpers :as dsh] [app.main.data.modal :as modal] @@ -19,9 +20,8 @@ [app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.context :as ctx] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] - [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.main.ui.hooks :as hooks] - [app.main.ui.icons :as deprecated-icon] [app.main.ui.notifications.badge :refer [badge-notification]] [app.render-wasm.api :as wasm.api] [app.util.dom :as dom] @@ -50,8 +50,10 @@ each object change)" [page-id] (l/derived (fn [fdata] - (-> (dsh/get-page fdata page-id) - (dissoc :objects))) + (let [page (dsh/get-page fdata page-id)] + (-> page + (assoc :empty? (ctp/is-empty? page)) + (dissoc :objects)))) refs/workspace-data =)) @@ -62,29 +64,32 @@ (mf/defc page-item {::mf/wrap-props false} [{:keys [page index deletable? selected? editing? hovering? current-page-id]}] - (let [input-ref (mf/use-ref) - id (:id page) - delete-fn (mf/use-fn (mf/deps id) #(st/emit! (dw/delete-page id))) - navigate-fn (mf/use-fn (mf/deps id) #(st/emit! :interrupt (dcm/go-to-workspace :page-id id))) - read-only? (mf/use-ctx ctx/workspace-read-only?) + (let [input-ref (mf/use-ref) + id (:id page) + name (:name page "") + is-separator? (and (= "---" (str/trim name)) (:empty? page)) + delete-fn (mf/use-fn (mf/deps id) #(st/emit! (dw/delete-page id))) + navigate-fn (mf/use-fn (mf/deps id) #(st/emit! :interrupt (dcm/go-to-workspace :page-id id))) + read-only? (mf/use-ctx ctx/workspace-read-only?) on-click (mf/use-fn - (mf/deps id current-page-id) + (mf/deps id current-page-id is-separator?) (fn [] - ;; For the wasm renderer, apply a blur effect to the viewport canvas - ;; when we navigate to a different page. - (if (and (features/active-feature? @st/state "render-wasm/v1") - (not= id current-page-id)) - (do - (wasm.api/capture-canvas-pixels) - (wasm.api/apply-canvas-blur) - ;; NOTE: it seems we need two RAF so the blur is actually applied and visible - ;; in the canvas :( - (timers/raf - (fn [] - (timers/raf navigate-fn)))) - (navigate-fn)))) + (when-not is-separator? + ;; For the wasm renderer, apply a blur effect to the viewport canvas + ;; when we navigate to a different page. + (if (and (features/active-feature? @st/state "render-wasm/v1") + (not= id current-page-id)) + (do + (wasm.api/capture-canvas-pixels) + (wasm.api/apply-canvas-blur) + ;; NOTE: it seems we need two RAF so the blur is actually applied and visible + ;; in the canvas :( + (timers/raf + (fn [] + (timers/raf navigate-fn)))) + (navigate-fn))))) on-delete (mf/use-fn @@ -106,11 +111,14 @@ on-blur (mf/use-fn + (mf/deps id is-separator?) (fn [event] - (let [name (str/trim (dom/get-target-val event))] - (when-not (str/empty? name) - (st/emit! (dw/rename-page id name))) - (st/emit! (dw/stop-rename-page-item))))) + (let [new-name (str/trim (dom/get-target-val event))] + (if (str/empty? new-name) + (when is-separator? + (st/emit! (dw/delete-page id))) + (st/emit! (dw/rename-page id new-name)))) + (st/emit! (dw/stop-rename-page-item)))) on-key-down (mf/use-fn @@ -166,40 +174,49 @@ (dom/select-text! edit-input)) nil))) - [:li {:class (stl/css-case - :page-element true - :selected selected? - :dnd-over-top (= (:over dprops) :top) - :dnd-over-bot (= (:over dprops) :bot)) - :ref dref} - [:div {:class (stl/css-case - :element-list-body true - :hover hovering? - :selected selected?) - :data-testid (dm/str "page-" id) - :tab-index "0" - :on-click on-click - :on-double-click on-double-click - :on-context-menu on-context-menu} - [:div {:class (stl/css :page-icon)} - deprecated-icon/document] - - (if editing? - [:* - [:input {:class (stl/css :element-name) - :type "text" - :ref input-ref - :on-blur on-blur - :on-key-down on-key-down - :auto-focus true - :default-value (:name page "")}]] - [:* - [:span {:class (stl/css :page-name) :title (:name page) :data-testid "page-name"} - (:name page)] - [:div {:class (stl/css :page-actions)} - (when (and deletable? (not read-only?)) - [:button {:on-click on-delete} - deprecated-icon/delete])]])]])) + (let [selected? (and selected? (not is-separator?))] + [:li {:class (stl/css-case + :page-element true + :separator is-separator? + :selected selected? + :dnd-over-top (= (:over dprops) :top) + :dnd-over-bot (= (:over dprops) :bot)) + :ref dref} + [:div {:class (stl/css-case + :element-list-body true + :separator-body is-separator? + :hover (and hovering? (not is-separator?)) + :selected selected?) + :data-testid (dm/str "page-" id) + :tab-index "0" + :on-click on-click + :on-double-click on-double-click + :on-context-menu on-context-menu} + (if (and is-separator? (not editing?)) + [:div {:class (stl/css :page-separator) + :data-testid "page-separator"}] + [:* + (when-not is-separator? + [:div {:class (stl/css :page-icon)} + [:> icon* {:icon-id i/document :size "s"}]]) + (if editing? + [:input {:class (stl/css :element-name) + :type "text" + :ref input-ref + :on-blur on-blur + :on-key-down on-key-down + :auto-focus true + :default-value name}] + [:* + [:span {:class (stl/css :page-name) :title name :data-testid "page-name"} + name] + [:div {:class (stl/css :page-actions)} + (when (and deletable? (not read-only?)) + [:> icon-button* {:variant "ghost" + :aria-label (tr "modals.delete-page.title") + :on-click on-delete + :icon-size "s" + :icon i/delete}])]])])]]))) ;; --- Page Item Wrapper diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss index 51b5f9fc5e..3a7a3233ac 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss @@ -5,6 +5,7 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; +@use "ds/_borders.scss" as *; .sitemap { position: relative; @@ -99,8 +100,6 @@ svg { @extend %button-icon-small; - height: deprecated.$s-12; - width: deprecated.$s-12; color: transparent; fill: none; stroke: var(--icon-foreground); @@ -109,6 +108,8 @@ .page-actions { height: deprecated.$s-32; + display: flex; + align-items: center; button { @include deprecated.buttonStyle; @@ -121,8 +122,6 @@ svg { @extend %button-icon-small; - height: deprecated.$s-12; - width: deprecated.$s-12; color: transparent; fill: none; stroke: var(--icon-foreground); @@ -253,6 +252,26 @@ } } +.element-list-body.separator-body { + height: auto; + min-height: var(--sp-xxxl); + padding: 0; +} + +.page-separator { + width: 100%; + height: $b-1; + margin: var(--sp-s); + background-color: var(--color-background-quaternary); +} + +.page-element.separator:hover .element-list-body, +.page-element.separator.hover .element-list-body { + color: var(--layer-row-foreground-color); + background-color: transparent; + box-shadow: none; +} + .title-spacing-sitemap { padding-inline-start: deprecated.$s-8; margin-block: deprecated.$s-8 deprecated.$s-4; From e9105f3670d98d59be7969db0b6ed12c26cd1248 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Mon, 20 Apr 2026 23:58:53 +0200 Subject: [PATCH 173/288] :recycle: Fix linter errors under legacy resources scss (#9035) --- docs/technical-guide/developer/ui.md | 19 +- frontend/package.json | 2 +- frontend/resources/styles/common/base.scss | 13 +- .../common/dependencies/_hljs-dark-theme.scss | 4 +- .../dependencies/_hljs-light-theme.scss | 6 +- .../common/dependencies/animations.scss | 71 +--- .../styles/common/dependencies/fonts.scss | 3 +- .../styles/common/dependencies/highlight.scss | 4 +- .../styles/common/dependencies/reset.scss | 16 +- .../styles/common/refactor/animations.scss | 10 - .../styles/common/refactor/basic-rules.scss | 309 ++++++++++++------ .../styles/common/refactor/color-defs.scss | 18 +- .../common/refactor/common-dashboard.scss | 6 + .../common/refactor/common-refactor.scss | 24 +- .../styles/common/refactor/focus.scss | 40 +-- .../styles/common/refactor/fonts.scss | 1 - .../styles/common/refactor/mixins.scss | 44 +-- .../styles/common/refactor/shadows.scss | 8 +- .../styles/common/refactor/themes.scss | 4 +- .../common/refactor/themes/default-theme.scss | 1 - .../common/refactor/themes/light-theme.scss | 1 - frontend/resources/styles/debug.scss | 4 +- frontend/resources/styles/main-default.scss | 23 +- .../resources/styles/main/partials/texts.scss | 2 +- frontend/src/app/main/ui/alert.scss | 6 +- frontend/src/app/main/ui/auth/common.scss | 18 +- .../app/main/ui/auth/recovery_request.scss | 2 +- frontend/src/app/main/ui/auth/register.scss | 8 +- frontend/src/app/main/ui/comments.scss | 24 +- .../app/main/ui/components/color_bullet.scss | 10 +- .../main/ui/components/context_menu_a11y.scss | 4 +- .../app/main/ui/components/copy_button.scss | 10 +- .../src/app/main/ui/components/forms.scss | 2 +- .../src/app/main/ui/components/progress.scss | 12 +- .../app/main/ui/components/radio_buttons.scss | 10 +- .../app/main/ui/components/search_bar.scss | 2 +- .../src/app/main/ui/components/select.scss | 12 +- .../app/main/ui/components/tab_container.scss | 10 +- .../src/app/main/ui/components/title_bar.scss | 2 +- frontend/src/app/main/ui/confirm.scss | 12 +- .../app/main/ui/dashboard/change_owner.scss | 6 +- .../src/app/main/ui/dashboard/comments.scss | 4 +- .../src/app/main/ui/dashboard/import.scss | 20 +- .../src/app/main/ui/dashboard/sidebar.scss | 32 +- .../app/main/ui/dashboard/subscription.scss | 10 +- .../src/app/main/ui/dashboard/team_form.scss | 10 +- .../src/app/main/ui/debug/icons_preview.scss | 4 +- frontend/src/app/main/ui/exports/assets.scss | 64 ++-- frontend/src/app/main/ui/exports/files.scss | 52 +-- .../src/app/main/ui/inspect/annotation.scss | 4 +- frontend/src/app/main/ui/inspect/code.scss | 14 +- frontend/src/app/main/ui/inspect/exports.scss | 10 +- .../app/main/ui/inspect/right_sidebar.scss | 8 +- .../src/app/main/ui/notifications/badge.scss | 4 +- .../notifications/context_notification.scss | 4 +- .../src/app/main/ui/onboarding/questions.scss | 12 +- .../app/main/ui/onboarding/team_choice.cljs | 2 +- .../app/main/ui/onboarding/team_choice.scss | 28 +- frontend/src/app/main/ui/releases.cljs | 4 +- frontend/src/app/main/ui/releases/v2_0.scss | 12 +- frontend/src/app/main/ui/releases/v2_1.scss | 8 +- frontend/src/app/main/ui/releases/v2_10.scss | 12 +- frontend/src/app/main/ui/releases/v2_11.scss | 12 +- frontend/src/app/main/ui/releases/v2_12.scss | 12 +- frontend/src/app/main/ui/releases/v2_13.scss | 12 +- frontend/src/app/main/ui/releases/v2_14.scss | 12 +- frontend/src/app/main/ui/releases/v2_2.scss | 8 +- frontend/src/app/main/ui/releases/v2_3.scss | 12 +- frontend/src/app/main/ui/releases/v2_4.scss | 12 +- frontend/src/app/main/ui/releases/v2_5.scss | 12 +- frontend/src/app/main/ui/releases/v2_6.scss | 12 +- frontend/src/app/main/ui/releases/v2_7.scss | 12 +- frontend/src/app/main/ui/releases/v2_8.scss | 12 +- frontend/src/app/main/ui/releases/v2_9.scss | 12 +- .../app/main/ui/settings/change_email.scss | 10 +- .../app/main/ui/settings/delete_account.scss | 10 +- .../src/app/main/ui/settings/sidebar.scss | 6 +- .../app/main/ui/settings/subscription.scss | 2 +- frontend/src/app/main/ui/static.scss | 4 +- frontend/src/app/main/ui/viewer.scss | 12 +- frontend/src/app/main/ui/viewer/comments.scss | 8 +- frontend/src/app/main/ui/viewer/header.scss | 44 +-- frontend/src/app/main/ui/viewer/inspect.scss | 2 +- .../src/app/main/ui/viewer/interactions.scss | 8 +- frontend/src/app/main/ui/viewer/login.scss | 6 +- .../src/app/main/ui/viewer/share_link.scss | 32 +- .../src/app/main/ui/viewer/thumbnails.scss | 14 +- .../app/main/ui/workspace/color_palette.scss | 8 +- .../ui/workspace/color_palette_ctx_menu.scss | 12 +- .../workspace/colorpicker/color_inputs.scss | 10 +- .../ui/workspace/colorpicker/gradients.scss | 2 +- .../ui/workspace/colorpicker/harmony.scss | 4 +- .../main/ui/workspace/colorpicker/hsva.scss | 4 +- .../src/app/main/ui/workspace/comments.scss | 8 +- .../app/main/ui/workspace/context_menu.scss | 10 +- .../app/main/ui/workspace/left_header.scss | 14 +- frontend/src/app/main/ui/workspace/nudge.scss | 10 +- .../src/app/main/ui/workspace/palette.scss | 6 +- .../src/app/main/ui/workspace/plugins.scss | 22 +- .../app/main/ui/workspace/right_header.scss | 10 +- .../src/app/main/ui/workspace/sidebar.scss | 12 +- .../app/main/ui/workspace/sidebar/assets.scss | 16 +- .../ui/workspace/sidebar/assets/colors.scss | 14 +- .../ui/workspace/sidebar/assets/common.scss | 14 +- .../sidebar/assets/file_library.scss | 6 +- .../ui/workspace/sidebar/assets/groups.scss | 8 +- .../main/ui/workspace/sidebar/layer_name.scss | 10 +- .../app/main/ui/workspace/sidebar/layers.scss | 36 +- .../sidebar/options/drawing/frame.scss | 6 +- .../workspace/sidebar/options/menus/blur.scss | 14 +- .../options/menus/color_selection.scss | 6 +- .../sidebar/options/menus/constraints.scss | 10 +- .../sidebar/options/menus/exports.scss | 8 +- .../sidebar/options/menus/frame_grid.scss | 38 +-- .../sidebar/options/menus/grid_cell.scss | 12 +- .../sidebar/options/menus/measures.scss | 8 +- .../sidebar/options/menus/svg_attrs.scss | 10 +- .../workspace/sidebar/options/menus/text.scss | 4 +- .../main/ui/workspace/sidebar/shortcuts.scss | 12 +- .../main/ui/workspace/sidebar/sitemap.scss | 18 +- .../app/main/ui/workspace/text_palette.scss | 14 +- .../ui/workspace/text_palette_ctx_menu.scss | 12 +- .../tokens/management/context_menu.scss | 2 +- .../app/main/ui/workspace/tokens/sets.scss | 14 +- .../workspace/tokens/sets/context_menu.scss | 2 +- .../main/ui/workspace/tokens/sets/lists.scss | 14 +- .../workspace/tokens/themes/create_modal.scss | 4 +- .../tokens/themes/theme_selector.scss | 10 +- .../app/main/ui/workspace/top_toolbar.scss | 4 +- .../viewport/grid_layout_editor.scss | 2 +- .../main/ui/workspace/viewport/top_bar.scss | 2 +- 131 files changed, 955 insertions(+), 906 deletions(-) diff --git a/docs/technical-guide/developer/ui.md b/docs/technical-guide/developer/ui.md index 2a6fb81102..a9bdfaa6f3 100644 --- a/docs/technical-guide/developer/ui.md +++ b/docs/technical-guide/developer/ui.md @@ -199,6 +199,7 @@ Remember that nesting selector increases specificity, and it's usually not neede fill: var(--icon-color); } ``` + Note: Thanks to CSS Modules, identical class names defined in different files are scoped locally and do not cause naming collisions. ### Use CSS logical properties @@ -228,17 +229,21 @@ Note: Although `width` and `height` are physical properties, their use is allowe Avoid hardcoded values like `px`, `rem`, or raw SASS variables `($s-*)`. Use semantic, named variables provided by the Design System to ensure consistency and scalability. #### Spacing (margins, paddings, gaps...) + Use variables from `frontend/src/app/main/ui/ds/spacing.scss`. These are predefined and approved by the design team — **do not add or modify values without design approval**. #### Fixed dimensions + For fixed dimensions (e.g., modals' widths) defined by design and not layout-driven, use or define variables in `frontend/src/app/main/ui/ds/_sizes.scss`. To use them: ```scss @use "ds/_sizes.scss" as *; ``` + Note: Since these values haven't been semantically defined yet, we’re temporarily using SASS variables instead of named CSS custom properties. #### Border Widths + Use border thickness variables from `frontend/src/app/main/ui/ds/_borders.scss`. To import: ```scss @@ -288,16 +293,16 @@ Replace plain text tags with `text*` or `heading*` components from the Design Sy ```clojure ... [app.main.ui.ds.foundations.typography :as t] - [app.main.ui.ds.foundations.typography.heading :refer [heading*]] + [app.main.ui.ds.foundations.typography.heading :refer [heading*]] [app.main.ui.ds.foundations.typography.text :refer [text*]] ... [:> heading* {:level 2 :typography t/headline-medium - :class (stl/css :modal-title)} + :class (stl/css :modal-title)} title] - [:> text* {:as "div" - :typography t/body-medium + [:> text* {:as "div" + :typography t/body-medium :class (stl/css :modal-content)} "Content"] ``` @@ -308,11 +313,12 @@ When applying typography in SCSS, use the proper mixin from the Design System. ```scss .class { - @include headlineLargeTypography; + @include headline-large-typography; } ``` ✅ **DO: Use the DS mixin** + ```scss @use "ds/typography.scss" as t; @@ -320,10 +326,10 @@ When applying typography in SCSS, use the proper mixin from the Design System. @include t.use-typography("body-small"); } ``` + You can find the full list of available typography tokens in [Storybook](https://design.penpot.app/storybook/?path=/docs/foundations-typography--docs). If the design you are implementing doesn't match any of them, ask a designer. - ### Use custom properties within components Reduce the need for one-off SASS variables by leveraging [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties) in your component styles. This keeps component theming flexible and composable. @@ -664,7 +670,6 @@ We use three **levels of tokens**: We can leverage component tokens to easily implement variants as explained [here](/technical-guide/developer/ui/#use-custom-properties-within-components). - ### Using icons and SVG assets Please refer to the Storybook [documentation for icons](https://hourly.penpot.dev/storybook/?path=/docs/foundations-assets-icon--docs) and other [SVG assets](https://hourly.penpot.dev/storybook/?path=/docs/foundations-assets-rawsvg--docs) (logos, illustrations, etc.). diff --git a/frontend/package.json b/frontend/package.json index 73218a3814..82047594c1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,7 +31,7 @@ "fmt:scss": "prettier -c resources/styles -c src/**/*.scss -w", "lint:clj": "clj-kondo --parallel --lint ../common/src src/", "lint:js": "exit 0", - "lint:scss": "pnpx stylelint 'src/**/*.scss'", + "lint:scss": "pnpx stylelint '{src,resources}/**/*.scss'", "build:test": "clojure -M:dev:shadow-cljs compile test", "test": "pnpm run build:wasm && pnpm run build:test && node target/tests/test.js", "test:storybook": "vitest run --project=storybook", diff --git a/frontend/resources/styles/common/base.scss b/frontend/resources/styles/common/base.scss index eea8d93655..37df67b3e8 100644 --- a/frontend/resources/styles/common/base.scss +++ b/frontend/resources/styles/common/base.scss @@ -30,6 +30,7 @@ body { &.cursor-drag-scrub { cursor: ew-resize !important; + * { cursor: ew-resize !important; } @@ -120,16 +121,15 @@ hr { input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; + appearance: none; margin: 0; } input[type="number"] { - -moz-appearance: textfield; + appearance: textfield; } [contenteditable] { - -webkit-user-select: text; user-select: text; } @@ -139,15 +139,12 @@ select { font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs14; margin-bottom: $size-4; - -webkit-appearance: none; - -moz-appearance: none; + appearance: none; } [draggable] { - -moz-user-select: none; - -khtml-user-select: none; - -webkit-user-select: none; user-select: none; + /* Required to make elements draggable in old WebKit */ -khtml-user-drag: element; -webkit-user-drag: element; diff --git a/frontend/resources/styles/common/dependencies/_hljs-dark-theme.scss b/frontend/resources/styles/common/dependencies/_hljs-dark-theme.scss index ddfa3a09e7..4654c54c94 100644 --- a/frontend/resources/styles/common/dependencies/_hljs-dark-theme.scss +++ b/frontend/resources/styles/common/dependencies/_hljs-dark-theme.scss @@ -82,7 +82,7 @@ .hljs-section { /* prettylights-syntax-markup-heading */ color: #316dca; - font-weight: bold; + font-weight: 700; } .hljs-bullet { @@ -99,7 +99,7 @@ .hljs-strong { /* prettylights-syntax-markup-bold */ color: #adbac7; - font-weight: bold; + font-weight: 700; } .hljs-addition { diff --git a/frontend/resources/styles/common/dependencies/_hljs-light-theme.scss b/frontend/resources/styles/common/dependencies/_hljs-light-theme.scss index ea2d601f76..78397f2cf4 100644 --- a/frontend/resources/styles/common/dependencies/_hljs-light-theme.scss +++ b/frontend/resources/styles/common/dependencies/_hljs-light-theme.scss @@ -11,7 +11,7 @@ .hljs { color: #24292e; - background: #ffffff; + background: #fff; } .hljs-doctag, @@ -83,7 +83,7 @@ .hljs-section { /* prettylights-syntax-markup-heading */ color: #005cc5; - font-weight: bold; + font-weight: 700; } .hljs-bullet { @@ -100,7 +100,7 @@ .hljs-strong { /* prettylights-syntax-markup-bold */ color: #24292e; - font-weight: bold; + font-weight: 700; } .hljs-addition { diff --git a/frontend/resources/styles/common/dependencies/animations.scss b/frontend/resources/styles/common/dependencies/animations.scss index ea30c21e10..ac8b99d7d9 100644 --- a/frontend/resources/styles/common/dependencies/animations.scss +++ b/frontend/resources/styles/common/dependencies/animations.scss @@ -7,13 +7,11 @@ */ .animated { - -webkit-animation-duration: 1s; animation-duration: 1s; - -webkit-animation-fill-mode: both; animation-fill-mode: both; } -@-webkit-keyframes fadeIn { +@keyframes fade-in { 0% { opacity: 0; } @@ -23,79 +21,22 @@ } } -@keyframes fadeIn { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } +.fade-in { + animation-name: fade-in; } -.fadeIn { - -webkit-animation-name: fadeIn; - animation-name: fadeIn; -} - -@-webkit-keyframes fadeInDown { +@keyframes fade-in-down { 0% { opacity: 0; - -webkit-transform: translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0); } 100% { opacity: 1; - -webkit-transform: none; transform: none; } } -@keyframes fadeInDown { - 0% { - opacity: 0; - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -.fadeInDown { - -webkit-animation-name: fadeInDown; - animation-name: fadeInDown; -} - -@keyframes loaderColor { - 0% { - fill: #513b56; - } - - 33% { - fill: #348aa7; - } - - 66% { - fill: #5dd39e; - } - - 100% { - fill: #513b56; - } -} - -//pencil loader animation -@keyframes linePencil { - 0% { - transform: translateY(0); - } - - 100% { - transform: translateY(-150px); - } +.fade-in-down { + animation-name: fade-in-down; } diff --git a/frontend/resources/styles/common/dependencies/fonts.scss b/frontend/resources/styles/common/dependencies/fonts.scss index 146f7099fd..ed4e821f7e 100644 --- a/frontend/resources/styles/common/dependencies/fonts.scss +++ b/frontend/resources/styles/common/dependencies/fonts.scss @@ -10,7 +10,7 @@ $style-name, $file, $unicode-range, - $weight: unquote("normal"), + $weight: string.unquote("normal"), $style: string.unquote("normal") ) { $filepath: "../fonts/" + $file; @@ -22,6 +22,7 @@ url($filepath + ".ttf") format("truetype"); font-weight: string.unquote($weight); font-style: string.unquote($style); + @if $unicode-range { unicode-range: $unicode-range; } diff --git a/frontend/resources/styles/common/dependencies/highlight.scss b/frontend/resources/styles/common/dependencies/highlight.scss index 9d53084cb7..a7bebe984c 100644 --- a/frontend/resources/styles/common/dependencies/highlight.scss +++ b/frontend/resources/styles/common/dependencies/highlight.scss @@ -7,9 +7,9 @@ @use "sass:meta"; :root { - @include meta.load-css("./_hljs-dark-theme.scss"); + @include meta.load-css("./_hljs-dark-theme"); } .light { - @include meta.load-css("./_hljs-light-theme.scss"); + @include meta.load-css("./_hljs-light-theme"); } diff --git a/frontend/resources/styles/common/dependencies/reset.scss b/frontend/resources/styles/common/dependencies/reset.scss index 39e198d8d7..d86c883697 100644 --- a/frontend/resources/styles/common/dependencies/reset.scss +++ b/frontend/resources/styles/common/dependencies/reset.scss @@ -11,12 +11,13 @@ License: none (public domain) div { vertical-align: top; } + img { display: block; } // #Reset & Basics (Inspired by E. Meyers) -//================================================== +// ================================================== a, abbr, acronym, @@ -100,7 +101,9 @@ var, video { border: 0; font: inherit; + /* stylelint-disable-next-line declaration-property-unit-allowed-list */ font-size: 100%; + // TODO: Changing line-height to 1 (as it should be) makes the visual tests // fail with a max pixel diff ratio of 0.005. // We should tackle this later. @@ -124,6 +127,7 @@ nav, section { display: block; } + body { line-height: 1; } @@ -138,10 +142,10 @@ q { quotes: none; } -blockquote:before, -blockquote:after, -q:before, -q:after { +blockquote::before, +blockquote::after, +q::before, +q::after { content: ""; } @@ -151,5 +155,5 @@ table { } select { - -webkit-appearance: none; + appearance: none; } diff --git a/frontend/resources/styles/common/refactor/animations.scss b/frontend/resources/styles/common/refactor/animations.scss index 44cdf2ee65..a35832ba1a 100644 --- a/frontend/resources/styles/common/refactor/animations.scss +++ b/frontend/resources/styles/common/refactor/animations.scss @@ -5,16 +5,6 @@ // Copyright (c) KALEIDOS INC @mixin animation($delay, $duration, $animation) { - -webkit-animation-delay: $delay; - -webkit-animation-duration: $duration; - -webkit-animation-name: $animation; - -webkit-animation-fill-mode: both; - - -moz-animation-delay: $delay; - -moz-animation-duration: $duration; - -moz-animation-name: $animation; - -moz-animation-fill-mode: both; - animation-delay: $delay; animation-duration: $duration; animation-name: $animation; diff --git a/frontend/resources/styles/common/refactor/basic-rules.scss b/frontend/resources/styles/common/refactor/basic-rules.scss index 6fa5d52d8f..5cb82f6c55 100644 --- a/frontend/resources/styles/common/refactor/basic-rules.scss +++ b/frontend/resources/styles/common/refactor/basic-rules.scss @@ -14,9 +14,10 @@ // SCROLLBAR %new-scrollbar { scrollbar-width: thin; - scrollbar-color: rgba(170, 181, 186, 0.3) transparent; + scrollbar-color: rgb(170 181 186 / 0.3) transparent; + &:hover { - scrollbar-color: rgba(170, 181, 186, 0.7) transparent; + scrollbar-color: rgb(170 181 186 / 0.7) transparent; } // These rules do not apply in chrome - 121 or higher @@ -27,18 +28,20 @@ height: $s-12; width: $s-12; } + ::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background-color: transparent; } ::-webkit-scrollbar-thumb { - background-color: rgba(170, 181, 186, 0.3); + background-color: rgb(170 181 186 / 0.3); background-clip: content-box; border: $s-2 solid transparent; border-radius: $br-8; + &:hover { - background-color: rgba(170, 181, 186, 0.7); + background-color: rgb(170 181 186 / 0.7); outline: none; } } @@ -48,48 +51,53 @@ color: var(--text-editor-selection-foreground-color); } - ::placeholder, - ::-webkit-input-placeholder { - @include bodySmallTypography; + ::placeholder { + @include body-small-typography; + color: var(--input-placeholder-color); } } // BUTTONS %button-primary { - @include buttonStyle; - @include flexCenter; - @include headlineSmallTypography; + @include button-style; + @include flex-center; + @include headline-small-typography; + background-color: var(--button-primary-background-color-rest); border: $s-1 solid var(--button-primary-border-color-rest); color: var(--button-primary-foreground-color-rest); border-radius: $br-8; min-height: $s-32; - svg, - span svg { + + svg { stroke: var(--button-primary-foreground-color-rest); } - @include focusPrimary; + + @include focus-primary; + &:hover { background-color: var(--button-primary-background-color-hover); border: $s-1 solid var(--button-primary-border-color-hover); color: var(--button-primary-foreground-color-hover); text-decoration: none; - svg, - span svg { + + svg { stroke: var(--button-primary-foreground-color-hover); } } + &:active { background-color: var(--button-primary-background-color-active); border: $s-1 solid var(--button-primary-border-color-active); color: var(--button-primary-foreground-color-active); outline: none; - svg, - span svg { + + svg { stroke: var(--button-primary-foreground-color-active); } } + &:global(.disabled), &[disabled], &:disabled { @@ -101,37 +109,42 @@ } %button-secondary { - @include buttonStyle; - @include flexCenter; + @include button-style; + @include flex-center; + border-radius: $br-8; background-color: var(--button-secondary-background-color-rest); border: $s-1 solid var(--button-secondary-border-color-rest); color: var(--button-secondary-foreground-color-rest); - svg, - span svg { + + svg { stroke: var(--button-secondary-foreground-color-rest); } - @include focusSecondary; + + @include focus-secondary; + &:hover { background-color: var(--button-secondary-background-color-hover); border: $s-1 solid var(--button-secondary-border-color-hover); color: var(--button-secondary-foreground-color-hover); text-decoration: none; - svg, - span svg { + + svg { stroke: var(--button-secondary-foreground-color-hover); } } + &:active { outline: none; background-color: var(--button-secondary-background-color-active); border: $s-1 solid var(--button-secondary-border-color-active); color: var(--button-secondary-foreground-color-active); - svg, - span svg { + + svg { stroke: var(--button-secondary-foreground-color-active); } } + &:global(.disabled), &[disabled], &:disabled { @@ -143,36 +156,41 @@ } %button-tertiary { - @include buttonStyle; - @include flexCenter; + @include button-style; + @include flex-center; + --button-tertiary-border-width: #{$s-2}; + border-radius: $br-8; color: var(--button-tertiary-foreground-color-rest); background-color: transparent; border: var(--button-tertiary-border-width) solid transparent; display: grid; place-content: center; - svg, - span svg { + + svg { stroke: var(--button-tertiary-foreground-color-rest); } - @include focusTertiary; + + @include focus-tertiary; + &:hover { background-color: var(--button-tertiary-background-color-hover); color: var(--button-tertiary-foreground-color-hover); border-color: var(--button-secondary-border-color-hover); - svg, - span svg { + + svg { stroke: var(--button-tertiary-foreground-color-hover); } } + &:active { outline: none; border-color: transparent; background-color: var(--button-tertiary-background-color-active); color: var(--button-tertiary-foreground-color-active); - svg, - span svg { + + svg { stroke: var(--button-tertiary-foreground-color-active); } } @@ -184,8 +202,7 @@ cursor: unset; pointer-events: none; - svg, - span svg { + svg { stroke: var(--button-foreground-color-disabled); } } @@ -196,54 +213,61 @@ border-color: var(--button-icon-border-color-selected); background-color: var(--button-icon-background-color-selected); color: var(--button-icon-foreground-color-selected); + svg { stroke: var(--button-icon-foreground-color-selected); } } .button-radio { - @include buttonStyle; - @include flexCenter; + @include button-style; + @include flex-center; + border-radius: $br-8; color: var(--button-radio-foreground-color-rest); border-color: $s-1 solid var(--button-radio-background-color-rest); - svg, - span svg { + + svg { stroke: var(--button-radio-foreground-color-rest); } - @include focusRadio; + + @include focus-radio; + &:hover { background-color: var(--button-radio-background-color-rest); color: var(--button-radio-foreground-color-hover); border: $s-1 solid transparent; - svg, - span svg { + + svg { stroke: var(--button-radio-foreground-color-hover); } } + &:active { outline: none; border: $s-1 solid transparent; background-color: var(--button-radio-background-color-active); color: var(--button-radio-foreground-color-active); - svg, - span svg { + + svg { stroke: var(--button-radio-foreground-color-active); } } } .button-warning { - @include buttonStyle; - @include flexCenter; + @include button-style; + @include flex-center; + background-color: var(--button-warning-background-color-rest); border: $s-1 solid var(--button-warning-border-color-rest); color: var(--button-warning-foreground-color-rest); } %button-disabled { - @include buttonStyle; - @include flexCenter; + @include button-style; + @include flex-center; + background-color: var(--button-background-color-disabled); border: $s-1 solid var(--button-border-color-disabled); color: var(--button-foreground-color-disabled); @@ -251,14 +275,16 @@ } %button-tag { - @include buttonStyle; - @include flexCenter; + @include button-style; + @include flex-center; @include focus; + &:hover { svg { stroke: var(--title-foreground-color-hover); } } + &:active { border: none; background-color: transparent; @@ -266,7 +292,8 @@ } %button-icon { - @include flexCenter; + @include flex-center; + height: $s-16; width: $s-16; color: transparent; @@ -276,19 +303,22 @@ %button-icon-small { @extend %button-icon; + height: $s-12; width: $s-12; stroke-width: 1.33px; } .button-constraint { - @include buttonStyle; + @include button-style; + width: $s-32; height: $s-4; border-radius: $br-8; background-color: var(--button-constraint-background-color-rest); padding: 0; margin: 0; + &:hover { outline: $s-4 solid var(--button-constraint-border-color-hover); background-color: var(--button-constraint-background-color-hover); @@ -297,8 +327,9 @@ // INPUTS %input-base { - @include removeInputStyle; - @include textEllipsis; + @include remove-input-style; + @include text-ellipsis; + height: $s-28; width: 100%; flex-grow: 1; @@ -306,6 +337,7 @@ padding: 0 0 0 $s-6; border-radius: $br-8; color: var(--input-foreground-color-active); + &[disabled] { opacity: 0.5; pointer-events: none; @@ -313,23 +345,30 @@ } .input-icon { - @include flexCenter; + @include flex-center; + min-width: $s-12; height: $s-32; + svg { @extend %button-icon-small; } } -.input-label { - @include headlineSmallTypography; - @include flexCenter; +%input-label { + @include headline-small-typography; + @include flex-center; + width: $s-20; padding-left: $s-8; height: $s-32; color: var(--input-foreground-color); } +.input-label { + @extend %input-label; +} + %input-element { display: flex; align-items: center; @@ -338,17 +377,22 @@ background-color: var(--input-background-color); border: $s-1 solid var(--input-border-color); color: var(--input-foreground-color); + &:not(:focus-within) { cursor: ew-resize; + input { cursor: ew-resize; } } + span, label { - @extend .input-label; + @extend %input-label; + svg { @extend %button-icon-small; + stroke: var(--input-foreground-color); } } @@ -361,43 +405,55 @@ color: var(--input-placeholder-color); } - @include focusInput; + @include focus-input; + &:hover { border: $s-1 solid var(--input-border-color-hover); background-color: var(--input-background-color-hover); + span { color: var(--input-foreground-color-hover); } + input { color: var(--input-foreground-color-hover); } } + &:active { border: $s-1 solid var(--input-border-color-active); background-color: var(--input-background-color-active); + span { color: var(--input-foreground-color-active); } + input { color: var(--input-foreground-color-active); } } + &:focus, &:focus-within { border: $s-1 solid var(--input-border-color-focus); background-color: var(--input-background-color-focus); + span { color: var(--input-foreground-color-focus); } + input { color: var(--input-foreground-color-focus); } + &:hover { border: $s-1 solid var(--input-border-color-focus); background-color: var(--input-background-color-focus); + span { color: var(--input-foreground-color-focus); } + input { color: var(--input-foreground-color-focus); } @@ -406,12 +462,15 @@ } %input-element-label { - @include bodySmallTypography; + @include body-small-typography; + display: flex; align-items: flex-start; padding: 0; + input { @extend %input-base; + padding-left: $s-8; display: flex; align-items: flex-start; @@ -424,10 +483,13 @@ color: var(--input-foreground-color-active); background-color: var(--input-background-color); } + ::placeholder { - @include bodySmallTypography; + @include body-small-typography; + color: var(--input-placeholder-color); } + &:hover { input { color: var(--input-foreground-color-active); @@ -449,18 +511,21 @@ background-color: var(--input-background-color-disabled); border: $s-1 solid var(--input-border-color-disabled); color: var(--input-foreground-color-disabled); + input { pointer-events: none; cursor: default; color: var(--input-foreground-color-disabled); } - span svg { + + svg { stroke: var(--input-foreground-color-disabled); } } %checkbox-icon { - @include flexCenter; + @include flex-center; + width: $s-16; height: $s-16; min-width: $s-16; @@ -468,15 +533,18 @@ background-color: var(--input-checkbox-background-color-rest); border: $s-1 solid var(--input-checkbox-border-color-rest); border-radius: $br-4; + svg { width: $s-16; height: $s-16; display: none; stroke: var(--input-checkbox-inactive-foreground-color); } + &:hover { border-color: var(--input-checkbox-border-color-hover); } + &:focus { border-color: var(--input-checkbox-border-color-focus); } @@ -484,8 +552,10 @@ &:global(.checked) { border-color: var(--input-checkbox-border-color-active); background-color: var(--input-checkbox-background-color-active); + svg { @extend %button-icon-small; + stroke: var(--input-checkbox-foreground-color-active); } } @@ -493,8 +563,10 @@ &:global(.intermediate) { background-color: var(--input-checkbox-background-color-intermediate); border-color: var(--input-checkbox-border-color-intermediate); + svg { @extend %button-icon-small; + stroke: var(--input-checkbox-foreground-color-intermediate); } } @@ -502,6 +574,7 @@ &:global(.unchecked) { background-color: var(--input-checkbox-background-color-rest); border: $s-1 solid var(--input-checkbox-background-color-rest); + svg { display: none; } @@ -511,19 +584,24 @@ %input-checkbox { display: flex; align-items: center; + label { - @include bodySmallTypography; + @include body-small-typography; + display: flex; align-items: center; gap: $s-6; cursor: pointer; color: var(--input-checkbox-text-foreground-color); + span { @extend %checkbox-icon; } + input { margin: 0; } + &:hover { span { border-color: var(--input-checkbox-border-color-hover); @@ -542,8 +620,10 @@ %input-with-label { display: flex; flex-direction: column; + label { - @include bodySmallTypography; + @include body-small-typography; + display: flex; flex-direction: column; justify-content: flex-start; @@ -553,7 +633,8 @@ input { @extend %input-base; - @include bodySmallTypography; + @include body-small-typography; + border-radius: $br-8; height: $s-32; min-height: $s-32; @@ -561,15 +642,18 @@ background-color: var(--input-background-color); border: $s-1 solid var(--input-border-color); color: var(--input-foreground-color-active); + &:focus-within, &:active { input { color: var(--input-foreground-color-active); } + background-color: var(--input-background-color-active); border: $s-1 solid var(--input-border-color-active); } } + &:global(.disabled) { @extend %disabled-input; } @@ -581,9 +665,10 @@ } } -//MODALS +// MODALS %modal-background { - @include menuShadow; + @include menu-shadow; + position: absolute; display: flex; flex-direction: column; @@ -595,7 +680,8 @@ } %modal-overlay-base { - @include flexCenter; + @include flex-center; + position: fixed; left: 0; top: 0; @@ -619,18 +705,21 @@ %modal-close-btn-base { @extend %button-tertiary; + position: absolute; top: $s-8; right: $s-6; height: $s-32; width: $s-28; + svg { @extend %button-icon; } } .modal-hint-base { - @include bodySmallTypography; + @include body-small-typography; + color: var(--modal-title-foreground-color); border-top: $s-1 solid var(--modal-hint-border-color); border-bottom: $s-1 solid var(--modal-hint-border-color); @@ -644,7 +733,8 @@ %modal-cancel-btn { @extend %button-secondary; - @include uppercaseTitleTipography; + @include uppercase-title-typography; + padding: $s-8 $s-24; border-radius: $br-8; height: $s-32; @@ -653,7 +743,8 @@ %modal-accept-btn { @extend %button-primary; - @include uppercaseTitleTipography; + @include uppercase-title-typography; + padding: $s-8 $s-24; border-radius: $br-8; height: $s-32; @@ -662,7 +753,8 @@ %modal-danger-btn { @extend %button-primary; - @include uppercaseTitleTipography; + @include uppercase-title-typography; + padding: $s-8 $s-24; border-radius: $br-8; height: $s-32; @@ -677,7 +769,8 @@ // FIXME: This is used multiple times accross the app. We should design this in // the DS and create a proper component for it. %asset-element { - @include bodySmallTypography; + @include body-small-typography; + display: flex; align-items: center; height: $s-32; @@ -685,6 +778,7 @@ padding: $s-8 $s-12; background-color: var(--assets-item-background-color); color: var(--assets-item-name-foreground-color-hover); + &:hover { background-color: var(--assets-item-background-color-hover); color: var(--assets-item-name-foreground-color-hover); @@ -692,14 +786,16 @@ } %shortcut-base { - @include flexCenter; + @include flex-center; + gap: $s-2; color: var(--menu-shortcut-foreground-color); } %shortcut-key-base { - @include bodySmallTypography; - @include flexCenter; + @include body-small-typography; + @include flex-center; + height: $s-20; padding: $s-2 $s-6; border-radius: $br-6; @@ -707,7 +803,8 @@ } %mixed-bar { - @include bodySmallTypography; + @include body-small-typography; + display: flex; align-items: center; flex-grow: 1; @@ -736,6 +833,7 @@ border-radius: $br-circle; transform: translate(calc(-1 * $s-12), calc(-1 * $s-12)); z-index: $z-index-1; + &:hover, &:active { border-color: var(--colorpicker-details-color-selected); @@ -747,14 +845,19 @@ margin-left: 0; color: var(--entry-foreground-color-hover); } + button { @extend %button-tertiary; + display: none; + svg { @extend %button-icon-small; + stroke: var(--icon-foreground); } } + &:hover { button { display: flex; @@ -768,9 +871,11 @@ grid-template-columns: 1fr 3fr; gap: $s-4; height: $s-32; + :global(.attr-label) { - @include bodySmallTypography; - @include twoLineTextEllipsis; + @include body-small-typography; + @include two-line-text-ellipsis; + width: $s-92; margin: auto 0; color: var(--entry-foreground-color); @@ -781,17 +886,20 @@ grid-area: content; display: flex; color: var(--entry-foreground-color-hover); - @include bodySmallTypography; + + @include body-small-typography; } } %copy-button-children { - @include bodySmallTypography; + @include body-small-typography; + color: var(--color-foreground-primary); text-align: left; margin: 0; padding: 0; height: fit-content; + &:hover { div { color: var(--entry-foreground-color-hover); @@ -801,8 +909,9 @@ // SELECTS AND DROPDOWNS %menu-dropdown { - @include menuShadow; - @include flexColumn; + @include menu-shadow; + @include flex-column; + position: absolute; padding: $s-4; border-radius: $br-8; @@ -814,7 +923,8 @@ } %menu-item-base { - @include bodySmallTypography; + @include body-small-typography; + display: flex; align-items: center; justify-content: space-between; @@ -823,13 +933,15 @@ padding: $s-6; border-radius: $br-8; cursor: pointer; + &:hover { background-color: var(--menu-background-color-hover); } } %dropdown-element-base { - @include bodySmallTypography; + @include body-small-typography; + display: flex; align-items: center; gap: $s-8; @@ -840,24 +952,29 @@ color: var(--menu-foreground-color-rest); span { - @include flexCenter; - @include textEllipsis; + @include flex-center; + @include text-ellipsis; + svg { @extend %button-icon-small; + stroke: var(--icon-foreground); } } + &:hover { background-color: var(--menu-background-color-hover); color: var(--menu-foreground-color); - span svg { + + svg { stroke: var(--menu-foreground-color-hover); } } } %dropdown-wrapper { - @include menuShadow; + @include menu-shadow; + position: absolute; top: $s-32; left: 0; @@ -868,15 +985,15 @@ margin-top: $s-1; border-radius: $br-8; z-index: $z-index-4; - overflow-y: auto; - overflow-x: hidden; + overflow: hidden auto; background-color: var(--menu-background-color); color: var(--menu-foreground-color); border: $s-2 solid var(--panel-border-color); } %select-wrapper { - @include bodySmallTypography; + @include body-small-typography; + position: relative; display: flex; align-items: center; diff --git a/frontend/resources/styles/common/refactor/color-defs.scss b/frontend/resources/styles/common/refactor/color-defs.scss index f3f1df5e20..47f933d991 100644 --- a/frontend/resources/styles/common/refactor/color-defs.scss +++ b/frontend/resources/styles/common/refactor/color-defs.scss @@ -11,53 +11,47 @@ // Dark background --db-primary-60: #{color.change(#18181a, $alpha: 0.6)}; // used on overlay dark mode - //Dark foreground + // Dark foreground --df-secondary: #8f9da3; // Used on button disabled background dark mode, grid metadata and some svg --df-secondary-40: #{color.change(#8f9da3, $alpha: 0.4)}; // Used on button disabled foreground dark mode - //Dark accent + // Dark accent --da-tertiary-10: #{color.change(#00d1b8, $alpha: 0.1)}; // selection rect dark mode --da-tertiary-70: #{color.change(#00d1b8, $alpha: 0.7)}; // selection rect background dark mode // LIGHT // Light background - --lb-primary-60: #{color.change(#ffffff, $alpha: 0.6)}; // overlay color light mode + --lb-primary-60: #{color.change(#fff, $alpha: 0.6)}; // overlay color light mode --lb-quaternary: #eef0f2; // background disabled token - //Light foreground + // Light foreground --lf-secondary-40: #{color.change(#495e74, $alpha: 0.4)}; // foreground disabled token - //Light accent + // Light accent --la-tertiary-10: #{color.change(#8c33eb, $alpha: 0.1)}; // selection rect light mode --la-tertiary-70: #{color.change(#8c33eb, $alpha: 0.7)}; // selection rect background light mode // STATUS COLOR --status-color-success-200: #a7e8d9; // Used on Register confirmation text --status-color-success-500: #2d9f8f; // Used on accept icon, and status widget - --status-color-warning-500: #f5a91b; // Used on status widget, some buttons and warnings icons and elements - --status-color-error-500: #ff3277; // Used on discard icon, some borders and svg, and on status widget - --status-color-info-500: #0e9be9; // used on pixel grid and status widget // APP COLORS - --app-white: #ffffff; // Used in several places + --app-white: #fff; // Used in several places --app-black: #000; // Used on interactions, measurements and editor files // SOCIAL LOGIN BUTTONS --google-login-background: #4285f4; --google-login-background-hover: #{color.adjust(#4285f4, $lightness: -15%)}; --google-login-foreground: var(--app-white); - --github-login-background: #4c4c4c; --github-login-background-hover: #{color.adjust(#4c4c4c, $lightness: -15%)}; --github-login-foreground: var(--app-white); - --oidc-login-background: #b3b3b3; --oidc-login-background-hover: #{color.adjust(#b3b3b3, $lightness: -15%)}; --oidc-login-foreground: var(--app-white); - --gitlab-login-background: #fc6d26; --gitlab-login-background-hover: #{color.adjust(#fc6d26, $lightness: -15%)}; --gitlab-login-foreground: var(--app-white); diff --git a/frontend/resources/styles/common/refactor/common-dashboard.scss b/frontend/resources/styles/common/refactor/common-dashboard.scss index 708e83eb6b..75f52ad936 100644 --- a/frontend/resources/styles/common/refactor/common-dashboard.scss +++ b/frontend/resources/styles/common/refactor/common-dashboard.scss @@ -29,6 +29,7 @@ .btn-secondary { flex-shrink: 0; height: $s-32; + svg { height: $s-16; width: $s-16; @@ -57,6 +58,7 @@ height: $s-40; padding: $s-4 $s-24; font-weight: $fw400; + &:hover { color: var(--color-background-secondary); text-decoration: none; @@ -124,10 +126,12 @@ font-size: $s-16; color: var(--color-foreground-secondary); border-color: transparent; + &:hover { color: var(--color-foreground-primary); } } + &.active { a { color: var(--color-foreground-primary); @@ -139,6 +143,7 @@ .btn-primary { @extend %button-primary; + text-transform: uppercase; font-size: $fs-14; font-weight: $fw400; @@ -146,6 +151,7 @@ .btn-secondary { @extend %button-secondary; + color: var(--color-foreground-primary); font-size: $fs-12; text-transform: uppercase; diff --git a/frontend/resources/styles/common/refactor/common-refactor.scss b/frontend/resources/styles/common/refactor/common-refactor.scss index a6098ee978..173fd6c7da 100644 --- a/frontend/resources/styles/common/refactor/common-refactor.scss +++ b/frontend/resources/styles/common/refactor/common-refactor.scss @@ -4,17 +4,17 @@ // // Copyright (c) KALEIDOS INC -//################################################# +// ################################################# // MAIN STYLES -//################################################# +// ################################################# -@forward "./fonts.scss"; -@forward "./spacing.scss"; -@forward "./borders.scss"; -@forward "./opacity.scss"; -@forward "./shadows.scss"; -@forward "./z-index.scss"; -@forward "./mixins.scss"; -@forward "./focus.scss"; -@forward "./animations.scss"; -@forward "./basic-rules.scss"; +@forward "./fonts"; +@forward "./spacing"; +@forward "./borders"; +@forward "./opacity"; +@forward "./shadows"; +@forward "./z-index"; +@forward "./mixins"; +@forward "./focus"; +@forward "./animations"; +@forward "./basic-rules"; diff --git a/frontend/resources/styles/common/refactor/focus.scss b/frontend/resources/styles/common/refactor/focus.scss index 0ac2dde780..8e01cab247 100644 --- a/frontend/resources/styles/common/refactor/focus.scss +++ b/frontend/resources/styles/common/refactor/focus.scss @@ -6,44 +6,46 @@ @use "./spacing.scss" as *; -@mixin focusType($type) { - $realType: ""; +@mixin focus-type($type) { + $real-type: ""; + @if $type { - $realType: $type + "-"; + $real-type: $type + "-"; } + &:focus-visible { outline: none; - background-color: var(--button-#{$realType}background-color-focus); - border: $s-1 solid var(--button-#{$realType}border-color-focus); - color: var(--button-#{$realType}foreground-color-focus); - svg, - span svg { - stroke: var(--button-#{$realType}foreground-color-focus); + background-color: var(--button-#{$real-type}background-color-focus); + border: $s-1 solid var(--button-#{$real-type}border-color-focus); + color: var(--button-#{$real-type}foreground-color-focus); + + svg { + stroke: var(--button-#{$real-type}foreground-color-focus); } } } -@mixin focusPrimary { - @include focusType(primary); +@mixin focus-primary { + @include focus-type(primary); } -@mixin focusSecondary { - @include focusType(secondary); +@mixin focus-secondary { + @include focus-type(secondary); } -@mixin focusTertiary { - @include focusType(tertiary); +@mixin focus-tertiary { + @include focus-type(tertiary); } -@mixin focusRadio { - @include focusType(radio); +@mixin focus-radio { + @include focus-type(radio); } @mixin focus { - @include focusType(null); + @include focus-type(null); } -@mixin focusInput { +@mixin focus-input { &:focus-within { color: var(--input-foreground-color-active); background-color: var(--input-background-color-active); diff --git a/frontend/resources/styles/common/refactor/fonts.scss b/frontend/resources/styles/common/refactor/fonts.scss index 015555225a..86f95cc303 100644 --- a/frontend/resources/styles/common/refactor/fonts.scss +++ b/frontend/resources/styles/common/refactor/fonts.scss @@ -8,7 +8,6 @@ // Typography scale $fs-base: 16; - $fs-10: math.div(10, $fs-base) + rem; $fs-11: 0.688rem; $fs-12: math.div(12, $fs-base) + rem; diff --git a/frontend/resources/styles/common/refactor/mixins.scss b/frontend/resources/styles/common/refactor/mixins.scss index 356c2ca7da..9ec8d1996b 100644 --- a/frontend/resources/styles/common/refactor/mixins.scss +++ b/frontend/resources/styles/common/refactor/mixins.scss @@ -7,37 +7,37 @@ @use "./fonts.scss" as *; @use "./spacing.scss" as *; -@mixin flexCenter { +@mixin flex-center { display: flex; justify-content: center; align-items: center; } -@mixin flexColumn($gap: $s-4) { +@mixin flex-column($gap: $s-4) { display: flex; flex-direction: column; gap: #{$gap}; } -@mixin flexRow { +@mixin flex-row { display: flex; align-items: center; gap: $s-4; } -@mixin buttonStyle { +@mixin button-style { border: none; background: none; cursor: pointer; } -@mixin removeInputStyle { +@mixin remove-input-style { border: none; background: none; outline: none; } -@mixin uppercaseTitleTipography { +@mixin uppercase-title-typography { font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-11; font-weight: $fw500; @@ -45,28 +45,28 @@ text-transform: uppercase; } -@mixin bigTitleTipography { +@mixin big-title-typography { font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-24; font-weight: $fw400; line-height: 1.2; } -@mixin medTitleTipography { +@mixin med-title-typography { font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-20; font-weight: $fw400; line-height: 1.2; } -@mixin smallTitleTipography { +@mixin small-title-typography { font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-14; font-weight: $fw400; line-height: 1.2; } -@mixin headlineLargeTypography { +@mixin headline-large-typography { font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-18; line-height: 1.2; @@ -74,7 +74,7 @@ font-weight: $fw400; } -@mixin headlineMediumTypography { +@mixin headline-medium-typography { font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-16; line-height: 1.4; @@ -82,7 +82,7 @@ font-weight: $fw400; } -@mixin headlineSmallTypography { +@mixin headline-small-typography { font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-12; line-height: 1.2; @@ -90,35 +90,35 @@ font-weight: $fw500; } -@mixin bodyLargeTypography { +@mixin body-large-typography { font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-16; line-height: 1.5; font-weight: $fw400; } -@mixin bodyMediumTypography { +@mixin body-medium-typography { font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-14; line-height: 1.4; font-weight: $fw400; } -@mixin bodySmallTypography { +@mixin body-small-typography { font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-12; font-weight: $fw400; line-height: 1.4; } -@mixin codeTypography { +@mixin code-typography { font-family: "robotomono", monospace; font-size: $fs-12; font-weight: $fw400; line-height: 1.2; } -@mixin textEllipsis { +@mixin text-ellipsis { display: block; max-width: 99%; overflow: hidden; @@ -126,7 +126,7 @@ white-space: nowrap; } -@mixin twoLineTextEllipsis { +@mixin two-line-text-ellipsis { max-width: 99%; overflow: hidden; text-overflow: ellipsis; @@ -135,8 +135,8 @@ -webkit-box-orient: vertical; } -@mixin inspectValue { - @include bodySmallTypography; +@mixin inspect-value { + @include body-small-typography; display: inline-block; width: fit-content; @@ -146,7 +146,7 @@ color: var(--menu-foreground-color); } -@mixin copyWrapperBase { +@mixin copy-wrapper-base { position: relative; min-height: $s-32; width: $s-144; @@ -155,7 +155,7 @@ box-sizing: border-box; } -@mixin hiddenElement { +@mixin hidden-element { cursor: default; pointer-events: none; box-sizing: border-box; diff --git a/frontend/resources/styles/common/refactor/shadows.scss b/frontend/resources/styles/common/refactor/shadows.scss index c936ca115d..ee825fa4c5 100644 --- a/frontend/resources/styles/common/refactor/shadows.scss +++ b/frontend/resources/styles/common/refactor/shadows.scss @@ -6,10 +6,6 @@ @use "./spacing.scss" as *; -@mixin menuShadow { - box-shadow: 0px 0px $s-12 0px var(--menu-shadow-color); -} - -@mixin alertShadow { - box-shadow: 0px $s-4 $s-4 var(--menu-shadow-color); +@mixin menu-shadow { + box-shadow: 0 0 $s-12 0 var(--menu-shadow-color); } diff --git a/frontend/resources/styles/common/refactor/themes.scss b/frontend/resources/styles/common/refactor/themes.scss index 9a5a9a1e64..cb4ab93a0f 100644 --- a/frontend/resources/styles/common/refactor/themes.scss +++ b/frontend/resources/styles/common/refactor/themes.scss @@ -4,5 +4,5 @@ // // Copyright (c) KALEIDOS INC -@forward "./themes/default-theme.scss"; -@forward "./themes/light-theme.scss"; +@forward "./themes/default-theme"; +@forward "./themes/light-theme"; diff --git a/frontend/resources/styles/common/refactor/themes/default-theme.scss b/frontend/resources/styles/common/refactor/themes/default-theme.scss index f7d092338a..11ac2c8e89 100644 --- a/frontend/resources/styles/common/refactor/themes/default-theme.scss +++ b/frontend/resources/styles/common/refactor/themes/default-theme.scss @@ -10,6 +10,5 @@ --color-background-disabled: var(--df-secondary); --color-foreground-disabled: var(--df-secondary-40); --color-accent-tertiary-muted: var(--da-tertiary-10); // selection rect - --overlay-color: var(--db-primary-60); } diff --git a/frontend/resources/styles/common/refactor/themes/light-theme.scss b/frontend/resources/styles/common/refactor/themes/light-theme.scss index 8baec1aa94..69e6259a0a 100644 --- a/frontend/resources/styles/common/refactor/themes/light-theme.scss +++ b/frontend/resources/styles/common/refactor/themes/light-theme.scss @@ -10,6 +10,5 @@ --color-background-disabled: var(--lb-quaternary); --color-foreground-disabled: var(--lf-secondary-40); --color-accent-tertiary-muted: var(--la-tertiary-10); - --overlay-color: var(--lb-primary-60); } diff --git a/frontend/resources/styles/debug.scss b/frontend/resources/styles/debug.scss index be3edc5228..227b18941f 100644 --- a/frontend/resources/styles/debug.scss +++ b/frontend/resources/styles/debug.scss @@ -10,9 +10,9 @@ // debugging. body { - color: yellow; + color: rgb(255 255 0); } .deprecated-icon { - fill: red !important; + fill: rgb(255 0 0) !important; } diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index 5b6c1cb247..d9048c610c 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -4,29 +4,28 @@ // // Copyright (c) KALEIDOS INC -//################################################# +// ################################################# // MAIN STYLES -//################################################# +// ################################################# @forward "common/dependencies/reset"; -@forward "common/refactor/color-defs.scss"; +@forward "common/refactor/color-defs"; @forward "common/dependencies/fonts"; @forward "common/dependencies/animations"; -@forward "common/dependencies/highlight.scss"; -@forward "common/dependencies/storybook.scss"; +@forward "common/dependencies/highlight"; +@forward "common/dependencies/storybook"; +@forward "common/refactor/themes"; +@forward "common/refactor/design-tokens"; -@forward "common/refactor/themes.scss"; -@forward "common/refactor/design-tokens.scss"; - -//################################################# +// ################################################# // Layouts -//################################################# +// ################################################# @forward "common/base"; -//################################################# +// ################################################# // Commons -//################################################# +// ################################################# // TODO: remove this stylesheet once the new text editor is in place // https: //tree.taiga.io/project/penpot/us/8165 diff --git a/frontend/resources/styles/main/partials/texts.scss b/frontend/resources/styles/main/partials/texts.scss index aab38a4966..ad945dc69b 100644 --- a/frontend/resources/styles/main/partials/texts.scss +++ b/frontend/resources/styles/main/partials/texts.scss @@ -2,7 +2,7 @@ .rich-text { color: var(--app-black); height: 100%; - font-family: sourcesanspro; + font-family: sans-serif, "sourcesanspro"; div { line-height: inherit; diff --git a/frontend/src/app/main/ui/alert.scss b/frontend/src/app/main/ui/alert.scss index 0776a29250..b3d0144fc1 100644 --- a/frontend/src/app/main/ui/alert.scss +++ b/frontend/src/app/main/ui/alert.scss @@ -23,7 +23,7 @@ } .modal-title { - @include deprecated.headlineMediumTypography; + @include deprecated.headline-medium-typography; color: var(--modal-title-foreground-color); } @@ -33,7 +33,7 @@ } .modal-content { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; margin-bottom: deprecated.$s-24; } @@ -57,7 +57,7 @@ .modal-scd-msg, .modal-subtitle, .modal-msg { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-text-foreground-color); line-height: 1.5; diff --git a/frontend/src/app/main/ui/auth/common.scss b/frontend/src/app/main/ui/auth/common.scss index 048aef58ec..dc438a1d97 100644 --- a/frontend/src/app/main/ui/auth/common.scss +++ b/frontend/src/app/main/ui/auth/common.scss @@ -33,19 +33,19 @@ } .auth-title { - @include deprecated.bigTitleTipography; + @include deprecated.big-title-typography; color: var(--title-foreground-color-hover); } .auth-subtitle { - @include deprecated.smallTitleTipography; + @include deprecated.small-title-typography; color: var(--title-foreground-color); } .auth-tagline { - @include deprecated.smallTitleTipography; + @include deprecated.small-title-typography; margin: 0; color: var(--title-foreground-color); @@ -65,7 +65,7 @@ .login-button, .login-ldap-button { @extend %button-primary; - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; height: deprecated.$s-40; width: 100%; @@ -81,7 +81,7 @@ .go-back-link { @extend %button-secondary; - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; height: deprecated.$s-40; } @@ -105,7 +105,7 @@ .account-text, .recovery-text, .demo-account-text { - @include deprecated.smallTitleTipography; + @include deprecated.small-title-typography; text-align: right; color: var(--title-foreground-color); @@ -116,7 +116,7 @@ .recovery-link, .forgot-pass-link, .demo-account-link { - @include deprecated.smallTitleTipography; + @include deprecated.small-title-typography; text-align: left; background-color: transparent; @@ -138,14 +138,14 @@ .register-btn, .recover-btn { @extend %button-primary; - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; height: deprecated.$s-40; width: 100%; } .login-btn { - @include deprecated.smallTitleTipography; + @include deprecated.small-title-typography; display: flex; align-items: center; diff --git a/frontend/src/app/main/ui/auth/recovery_request.scss b/frontend/src/app/main/ui/auth/recovery_request.scss index bad82e2767..b4c053b104 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.scss +++ b/frontend/src/app/main/ui/auth/recovery_request.scss @@ -12,7 +12,7 @@ } .notification-text-email { - @include deprecated.medTitleTipography; + @include deprecated.med-title-typography; font-size: deprecated.$fs-20; color: var(--register-confirmation-color); diff --git a/frontend/src/app/main/ui/auth/register.scss b/frontend/src/app/main/ui/auth/register.scss index c6525ed145..47445a3633 100644 --- a/frontend/src/app/main/ui/auth/register.scss +++ b/frontend/src/app/main/ui/auth/register.scss @@ -27,7 +27,7 @@ gap: deprecated.$s-24; .auth-title { - @include deprecated.medTitleTipography; + @include deprecated.med-title-typography; } } @@ -44,13 +44,13 @@ } .notification-text { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--title-foreground-color); } .notification-text-email { - @include deprecated.medTitleTipography; + @include deprecated.med-title-typography; font-size: deprecated.$fs-20; color: var(--register-confirmation-color); @@ -75,7 +75,7 @@ } .terms-register { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; gap: deprecated.$s-4; diff --git a/frontend/src/app/main/ui/comments.scss b/frontend/src/app/main/ui/comments.scss index 051a6cd613..9da2078d38 100644 --- a/frontend/src/app/main/ui/comments.scss +++ b/frontend/src/app/main/ui/comments.scss @@ -23,7 +23,7 @@ } .error-text { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--color-foreground-error); } @@ -40,11 +40,11 @@ } .location-text { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; } .author { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; align-items: center; @@ -56,13 +56,13 @@ } .author-fullname { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; color: var(--comment-title-color); } .author-timeago { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; color: var(--comment-subtitle-color); } @@ -120,7 +120,7 @@ } .cover { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; cursor: pointer; display: flex; @@ -131,7 +131,7 @@ } .item { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--color-foreground-primary); overflow-wrap: break-word; @@ -140,7 +140,7 @@ } .replies { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; gap: deprecated.$s-8; @@ -245,7 +245,7 @@ } .floating-thread-header-left { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--color-foreground-primary); } @@ -272,11 +272,11 @@ flex-direction: column; gap: deprecated.$s-8; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; } .checkbox-wrapper { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-16; height: deprecated.$s-24; @@ -381,7 +381,7 @@ } .comment-input { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; background: var(--input-background-color); border-radius: deprecated.$br-8; diff --git a/frontend/src/app/main/ui/components/color_bullet.scss b/frontend/src/app/main/ui/components/color_bullet.scss index 6cab83cb79..7972780483 100644 --- a/frontend/src/app/main/ui/components/color_bullet.scss +++ b/frontend/src/app/main/ui/components/color_bullet.scss @@ -79,8 +79,8 @@ } .color-text { - @include deprecated.twoLineTextEllipsis; - @include deprecated.bodySmallTypography; + @include deprecated.two-line-text-ellipsis; + @include deprecated.body-small-typography; width: deprecated.$s-80; text-align: center; @@ -89,15 +89,15 @@ color: var(--palette-text-color); &.small-text { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; max-height: deprecated.$s-16; } } .big-text { - @include deprecated.inspectValue; - @include deprecated.twoLineTextEllipsis; + @include deprecated.inspect-value; + @include deprecated.two-line-text-ellipsis; line-height: 1; color: var(--palette-text-color); diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.scss b/frontend/src/app/main/ui/components/context_menu_a11y.scss index 8f38c78277..b87754a356 100644 --- a/frontend/src/app/main/ui/components/context_menu_a11y.scss +++ b/frontend/src/app/main/ui/components/context_menu_a11y.scss @@ -24,7 +24,7 @@ } .context-menu-items { - @include deprecated.menuShadow; + @include deprecated.menu-shadow; position: absolute; top: deprecated.$s-12; @@ -50,7 +50,7 @@ display: flex; .context-menu-action { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; align-items: center; diff --git a/frontend/src/app/main/ui/components/copy_button.scss b/frontend/src/app/main/ui/components/copy_button.scss index b8f8f67789..63f2f5069b 100644 --- a/frontend/src/app/main/ui/components/copy_button.scss +++ b/frontend/src/app/main/ui/components/copy_button.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .copy-button { - @include deprecated.buttonStyle; + @include deprecated.button-style; width: 100%; height: deprecated.$s-32; @@ -17,7 +17,7 @@ box-sizing: border-box; .icon-btn { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-32; min-width: deprecated.$s-28; @@ -56,8 +56,8 @@ } .copy-wrapper { - @include deprecated.buttonStyle; - @include deprecated.copyWrapperBase; + @include deprecated.button-style; + @include deprecated.copy-wrapper-base; width: 100%; height: fit-content; @@ -65,7 +65,7 @@ border: deprecated.$s-1 solid transparent; .icon-btn { - @include deprecated.flexCenter; + @include deprecated.flex-center; position: absolute; top: 0; diff --git a/frontend/src/app/main/ui/components/forms.scss b/frontend/src/app/main/ui/components/forms.scss index 3bd61df854..2242c66868 100644 --- a/frontend/src/app/main/ui/components/forms.scss +++ b/frontend/src/app/main/ui/components/forms.scss @@ -334,7 +334,7 @@ overflow-y: hidden; .inside-input { - @include deprecated.removeInputStyle; + @include deprecated.remove-input-style; @include t.use-typography("body-small"); @include text-ellipsis; diff --git a/frontend/src/app/main/ui/components/progress.scss b/frontend/src/app/main/ui/components/progress.scss index dcdf538c8a..646571f7f8 100644 --- a/frontend/src/app/main/ui/components/progress.scss +++ b/frontend/src/app/main/ui/components/progress.scss @@ -8,7 +8,7 @@ // PROGRESS WIDGET .progress-widget { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-28; height: deprecated.$s-28; @@ -59,7 +59,7 @@ } .title { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; display: grid; grid-template-columns: auto 1fr; @@ -72,7 +72,7 @@ } .progress { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; padding-left: deprecated.$s-8; margin: 0; @@ -81,8 +81,8 @@ } .retry-btn { - @include deprecated.buttonStyle; - @include deprecated.bodySmallTypography; + @include deprecated.button-style; + @include deprecated.body-small-typography; display: inline; text-align: left; @@ -92,7 +92,7 @@ } .progress-close-button { - @include deprecated.buttonStyle; + @include deprecated.button-style; padding: 0; margin-inline-end: deprecated.$s-8; diff --git a/frontend/src/app/main/ui/components/radio_buttons.scss b/frontend/src/app/main/ui/components/radio_buttons.scss index 51ca7a4c81..f8c06c4715 100644 --- a/frontend/src/app/main/ui/components/radio_buttons.scss +++ b/frontend/src/app/main/ui/components/radio_buttons.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .radio-btn-wrapper { - @include deprecated.flexCenter; + @include deprecated.flex-center; border-radius: deprecated.$br-8; height: deprecated.$s-32; @@ -18,9 +18,9 @@ .radio-icon { --radio-icon-border-color: var(--radio-btn-border-color); - @include deprecated.buttonStyle; - @include deprecated.flexCenter; - @include deprecated.focusRadio; + @include deprecated.button-style; + @include deprecated.flex-center; + @include deprecated.focus-radio; height: deprecated.$s-32; flex-grow: 1; @@ -39,7 +39,7 @@ } .title-name { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; color: var(--radio-btn-foreground-color); } diff --git a/frontend/src/app/main/ui/components/search_bar.scss b/frontend/src/app/main/ui/components/search_bar.scss index 4534d76345..22294f1d94 100644 --- a/frontend/src/app/main/ui/components/search_bar.scss +++ b/frontend/src/app/main/ui/components/search_bar.scss @@ -22,7 +22,7 @@ } .search-input-wrapper { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-32; width: 100%; diff --git a/frontend/src/app/main/ui/components/select.scss b/frontend/src/app/main/ui/components/select.scss index 72eae37864..e75034f6f3 100644 --- a/frontend/src/app/main/ui/components/select.scss +++ b/frontend/src/app/main/ui/components/select.scss @@ -13,7 +13,7 @@ --text-color: var(--menu-foreground-color); @extend %new-scrollbar; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; position: relative; display: grid; @@ -56,7 +56,7 @@ } .dropdown-button { - @include deprecated.flexCenter; + @include deprecated.flex-center; margin-inline-end: var(--sp-xxs); @@ -69,7 +69,7 @@ } .current-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-24; padding-right: deprecated.$s-4; @@ -100,7 +100,7 @@ @extend %dropdown-element-base; .icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-24; width: deprecated.$s-24; @@ -119,7 +119,7 @@ } .check-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; svg { @extend %button-icon-small; @@ -144,5 +144,5 @@ } .current-label { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; } diff --git a/frontend/src/app/main/ui/components/tab_container.scss b/frontend/src/app/main/ui/components/tab_container.scss index 71cc4371ad..89c5692c55 100644 --- a/frontend/src/app/main/ui/components/tab_container.scss +++ b/frontend/src/app/main/ui/components/tab_container.scss @@ -31,7 +31,7 @@ } .tab-container-tab-title { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: 100%; width: 100%; @@ -50,7 +50,7 @@ } .content { - @include deprecated.headlineSmallTypography; + @include deprecated.headline-small-typography; text-align: center; white-space: nowrap; @@ -79,8 +79,8 @@ } .collapse-sidebar { - @include deprecated.flexCenter; - @include deprecated.buttonStyle; + @include deprecated.flex-center; + @include deprecated.button-style; height: 100%; width: deprecated.$s-24; @@ -89,7 +89,7 @@ border-radius: deprecated.$br-5; svg { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-16; width: deprecated.$s-16; diff --git a/frontend/src/app/main/ui/components/title_bar.scss b/frontend/src/app/main/ui/components/title_bar.scss index 1ba37cde67..de605369bc 100644 --- a/frontend/src/app/main/ui/components/title_bar.scss +++ b/frontend/src/app/main/ui/components/title_bar.scss @@ -67,7 +67,7 @@ } .icon-text-btn { - @include deprecated.buttonStyle; + @include deprecated.button-style; display: flex; align-items: center; diff --git a/frontend/src/app/main/ui/confirm.scss b/frontend/src/app/main/ui/confirm.scss index e3106e528a..0f14a6e305 100644 --- a/frontend/src/app/main/ui/confirm.scss +++ b/frontend/src/app/main/ui/confirm.scss @@ -23,7 +23,7 @@ } .modal-title { - @include deprecated.headlineMediumTypography; + @include deprecated.headline-medium-typography; color: var(--modal-title-foreground-color); } @@ -35,21 +35,21 @@ } .modal-content { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; } .modal-item-element { - @include deprecated.flexRow; + @include deprecated.flex-row; } .modal-component-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; color: var(--color-foreground-secondary); } .modal-component-name { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--color-foreground-secondary); } @@ -61,7 +61,7 @@ .modal-scd-msg, .modal-subtitle, .modal-msg { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-text-foreground-color); } diff --git a/frontend/src/app/main/ui/dashboard/change_owner.scss b/frontend/src/app/main/ui/dashboard/change_owner.scss index 6fa30819ca..fe20d59db6 100644 --- a/frontend/src/app/main/ui/dashboard/change_owner.scss +++ b/frontend/src/app/main/ui/dashboard/change_owner.scss @@ -21,7 +21,7 @@ } .modal-title { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; color: var(--modal-title-foreground-color); } @@ -31,14 +31,14 @@ } .modal-content { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; margin-bottom: deprecated.$s-24; } .input-wrapper { @extend %input-with-label; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; } .action-buttons { diff --git a/frontend/src/app/main/ui/dashboard/comments.scss b/frontend/src/app/main/ui/dashboard/comments.scss index 71f6fe7ebd..2d5948199c 100644 --- a/frontend/src/app/main/ui/dashboard/comments.scss +++ b/frontend/src/app/main/ui/dashboard/comments.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .dashboard-comments-section { - @include deprecated.flexCenter; + @include deprecated.flex-center; position: relative; border-radius: deprecated.$br-8; @@ -66,7 +66,7 @@ } .dropdown { - @include deprecated.menuShadow; + @include deprecated.menu-shadow; background-color: var(--color-background-tertiary); border-radius: deprecated.$br-8; diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss index 133d0c8dbe..b428e4b0d7 100644 --- a/frontend/src/app/main/ui/dashboard/import.scss +++ b/frontend/src/app/main/ui/dashboard/import.scss @@ -22,7 +22,7 @@ } .modal-title { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; color: var(--modal-title-foreground-color); } @@ -32,7 +32,7 @@ } .modal-content { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; flex: 1; overflow: hidden auto; @@ -62,7 +62,7 @@ .modal-scd-msg, .modal-subtitle, .modal-msg { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--modal-text-foreground-color); line-height: 1.5; @@ -72,10 +72,10 @@ display: flex; .file-name { - @include deprecated.flexRow; + @include deprecated.flex-row; .file-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-24; width: deprecated.$s-16; @@ -93,13 +93,13 @@ .file-name-edit { @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; flex-grow: 1; } .file-name-label { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; align-items: center; @@ -107,7 +107,7 @@ flex-grow: 1; .icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-16; width: deprecated.$s-16; @@ -121,7 +121,7 @@ } .edit-entry-buttons { - @include deprecated.flexRow; + @include deprecated.flex-row; button { @extend %button-tertiary; @@ -153,7 +153,7 @@ color: var(--modal-text-foreground-color); .linked-library-tag { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-24; width: deprecated.$s-24; diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index 922f6e99e7..0fe9571e05 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -79,7 +79,7 @@ } .current-team { - @include deprecated.buttonStyle; + @include deprecated.button-style; display: grid; align-items: center; @@ -98,7 +98,7 @@ } .team-text { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; @include t.use-typography("title-small"); width: auto; @@ -115,7 +115,7 @@ // This icon still use the old svg .penpot-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; svg { fill: var(--icon-foreground); @@ -125,7 +125,7 @@ } .team-picture { - @include deprecated.flexCenter; + @include deprecated.flex-center; border-radius: 50%; height: var(--sp-xxl); @@ -140,8 +140,8 @@ } .switch-options { - @include deprecated.buttonStyle; - @include deprecated.flexCenter; + @include deprecated.button-style; + @include deprecated.flex-center; max-width: var(--sp-xxl); min-width: deprecated.$s-28; @@ -199,7 +199,7 @@ } .icon-wrapper { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: var(--sp-xxl); height: var(--sp-xxl); @@ -301,7 +301,7 @@ } .element-title { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; width: deprecated.$s-256; color: var(--color-foreground-primary); @@ -366,8 +366,8 @@ } .search-btn { - @include deprecated.buttonStyle; - @include deprecated.flexCenter; + @include deprecated.button-style; + @include deprecated.flex-center; position: absolute; right: 0; @@ -401,7 +401,7 @@ } .profile { - @include deprecated.buttonStyle; + @include deprecated.button-style; display: grid; grid-template-columns: auto 1fr; @@ -412,7 +412,7 @@ .profile-fullname { @include t.use-typography("title-small"); - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; align-self: center; max-width: var(--sp-l) 0; @@ -454,7 +454,7 @@ } .profile-dropdown-item .open-arrow { - @include deprecated.flexCenter; + @include deprecated.flex-center; } .profile-dropdown-item .open-arrow svg { @@ -504,7 +504,7 @@ .menu-version { @include t.use-typography("code-font"); - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; color: var(--color-foreground-secondary); margin-inline-start: var(--sp-s); @@ -545,7 +545,7 @@ } .upgrade-plan-section { - @include deprecated.buttonStyle; + @include deprecated.button-style; display: flex; justify-content: space-between; @@ -638,7 +638,7 @@ } .current-org { - @include deprecated.buttonStyle; + @include deprecated.button-style; text-transform: none; display: grid; diff --git a/frontend/src/app/main/ui/dashboard/subscription.scss b/frontend/src/app/main/ui/dashboard/subscription.scss index 24f92ab331..0edb685c61 100644 --- a/frontend/src/app/main/ui/dashboard/subscription.scss +++ b/frontend/src/app/main/ui/dashboard/subscription.scss @@ -26,7 +26,7 @@ } .cta-top-section { - @include deprecated.buttonStyle; + @include deprecated.button-style; display: grid; color: var(--color-foreground-secondary); @@ -44,7 +44,7 @@ } .icon-dropdown { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: 100%; width: var(--sp-l); @@ -70,7 +70,7 @@ .cta-bottom-section .content { @include t.use-typography("body-medium"); - @include deprecated.buttonStyle; + @include deprecated.button-style; color: var(--color-foreground-secondary); display: inline-block; @@ -120,7 +120,7 @@ } .cta-link { - @include deprecated.buttonStyle; + @include deprecated.button-style; align-self: end; margin-inline-start: var(--sp-xs); @@ -147,7 +147,7 @@ } .manage-subscription-link { - @include deprecated.buttonStyle; + @include deprecated.button-style; @include t.use-typography("body-medium"); color: var(--color-accent-tertiary); diff --git a/frontend/src/app/main/ui/dashboard/team_form.scss b/frontend/src/app/main/ui/dashboard/team_form.scss index 354a177dd0..592ca7a94d 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.scss +++ b/frontend/src/app/main/ui/dashboard/team_form.scss @@ -19,7 +19,7 @@ } .modal-title { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; color: var(--modal-title-foreground-color); } @@ -38,13 +38,13 @@ .group-name-input { @extend %input-element-label; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; margin-bottom: deprecated.$s-8; label { - @include deprecated.flexColumn; - @include deprecated.bodySmallTypography; + @include deprecated.flex-column; + @include deprecated.body-small-typography; align-items: flex-start; width: 100%; @@ -53,7 +53,7 @@ height: 100%; input { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; } } } diff --git a/frontend/src/app/main/ui/debug/icons_preview.scss b/frontend/src/app/main/ui/debug/icons_preview.scss index 082723835e..a5a83fdf11 100644 --- a/frontend/src/app/main/ui/debug/icons_preview.scss +++ b/frontend/src/app/main/ui/debug/icons_preview.scss @@ -9,7 +9,7 @@ } .title { - @include deprecated.bigTitleTipography; + @include deprecated.big-title-typography; color: var(--color-foreground-primary); } @@ -32,7 +32,7 @@ color: var(--color-foreground-primary); overflow-wrap: break-word; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; svg { width: var(--cell-size); diff --git a/frontend/src/app/main/ui/exports/assets.scss b/frontend/src/app/main/ui/exports/assets.scss index 338daf19e7..cc690c5d3b 100644 --- a/frontend/src/app/main/ui/exports/assets.scss +++ b/frontend/src/app/main/ui/exports/assets.scss @@ -8,7 +8,7 @@ // PROGRESS WIDGET .export-progress-widget { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-28; height: deprecated.$s-28; @@ -59,7 +59,7 @@ } .export-progress-title { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; display: grid; grid-template-columns: auto 1fr; @@ -72,7 +72,7 @@ } .progress { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; padding-left: deprecated.$s-8; margin: 0; @@ -81,8 +81,8 @@ } .retry-btn { - @include deprecated.buttonStyle; - @include deprecated.bodySmallTypography; + @include deprecated.button-style; + @include deprecated.body-small-typography; display: inline; text-align: left; @@ -92,7 +92,7 @@ } .progress-close-button { - @include deprecated.buttonStyle; + @include deprecated.button-style; padding: 0; margin-inline-end: deprecated.$s-8; @@ -129,7 +129,7 @@ } .modal-title { - @include deprecated.headlineMediumTypography; + @include deprecated.headline-medium-typography; color: var(--modal-title-foreground-color); } @@ -140,12 +140,12 @@ .modal-content, .no-selection { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; margin-bottom: deprecated.$s-24; .modal-link { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; text-decoration: none; cursor: pointer; @@ -153,15 +153,15 @@ } .selection-header { - @include deprecated.flexRow; + @include deprecated.flex-row; height: deprecated.$s-32; margin-bottom: deprecated.$s-4; .selection-btn { - @include deprecated.buttonStyle; + @include deprecated.button-style; @extend %input-checkbox; - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-24; width: deprecated.$s-24; @@ -174,7 +174,7 @@ } .selection-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-text-foreground-color); } @@ -203,21 +203,21 @@ } .selection-list { - @include deprecated.flexColumn; + @include deprecated.flex-column; max-height: deprecated.$s-400; overflow-y: auto; padding-bottom: deprecated.$s-12; .selection-row { - @include deprecated.flexRow; + @include deprecated.flex-row; background-color: var(--entry-background-color); min-height: deprecated.$s-40; border-radius: deprecated.$br-8; .selection-btn { - @include deprecated.buttonStyle; + @include deprecated.button-style; display: grid; grid-template-columns: min-content auto 1fr auto auto; @@ -229,7 +229,7 @@ .checkbox-wrapper { @extend %input-checkbox; - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-24; width: deprecated.$s-24; @@ -241,8 +241,8 @@ } .selection-name { - @include deprecated.bodyLargeTypography; - @include deprecated.textEllipsis; + @include deprecated.body-large-typography; + @include deprecated.text-ellipsis; flex-grow: 1; color: var(--modal-text-foreground-color); @@ -250,8 +250,8 @@ } .selection-scale { - @include deprecated.bodyLargeTypography; - @include deprecated.textEllipsis; + @include deprecated.body-large-typography; + @include deprecated.text-ellipsis; min-width: deprecated.$s-108; padding: deprecated.$s-12; @@ -259,8 +259,8 @@ } .selection-extension { - @include deprecated.bodyLargeTypography; - @include deprecated.textEllipsis; + @include deprecated.body-large-typography; + @include deprecated.text-ellipsis; min-width: deprecated.$s-72; padding: deprecated.$s-12; @@ -269,7 +269,7 @@ } .image-wrapper { - @include deprecated.flexCenter; + @include deprecated.flex-center; min-height: deprecated.$s-32; min-width: deprecated.$s-32; @@ -306,7 +306,7 @@ .modal-scd-msg, .modal-subtitle, .modal-msg { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-text-foreground-color); } @@ -321,7 +321,7 @@ align-items: flex-start; .modal-subtitle { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } @@ -333,16 +333,16 @@ } .option-content { - @include deprecated.flexColumn; - @include deprecated.bodyLargeTypography; + @include deprecated.flex-column; + @include deprecated.body-large-typography; } .file-entry { .file-name { - @include deprecated.flexRow; + @include deprecated.flex-row; .file-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-16; width: deprecated.$s-16; @@ -355,8 +355,8 @@ } .file-name-label { - @include deprecated.bodyLargeTypography; - @include deprecated.textEllipsis; + @include deprecated.body-large-typography; + @include deprecated.text-ellipsis; } } diff --git a/frontend/src/app/main/ui/exports/files.scss b/frontend/src/app/main/ui/exports/files.scss index ed33acb665..c3c09b3fa4 100644 --- a/frontend/src/app/main/ui/exports/files.scss +++ b/frontend/src/app/main/ui/exports/files.scss @@ -26,7 +26,7 @@ } .modal-title { - @include deprecated.headlineMediumTypography; + @include deprecated.headline-medium-typography; color: var(--modal-title-foreground-color); } @@ -36,12 +36,12 @@ } .modal-content { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; margin-bottom: deprecated.$s-24; .modal-link { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; text-decoration: none; cursor: pointer; @@ -49,15 +49,15 @@ } .selection-header { - @include deprecated.flexRow; + @include deprecated.flex-row; height: deprecated.$s-32; margin-bottom: deprecated.$s-4; .selection-btn { - @include deprecated.buttonStyle; + @include deprecated.button-style; @extend %input-checkbox; - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-24; width: deprecated.$s-24; @@ -70,7 +70,7 @@ } .selection-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-text-foreground-color); } @@ -99,21 +99,21 @@ } .selection-list { - @include deprecated.flexColumn; + @include deprecated.flex-column; max-height: deprecated.$s-400; overflow-y: auto; padding-bottom: deprecated.$s-12; .selection-row { - @include deprecated.flexRow; + @include deprecated.flex-row; background-color: var(--entry-background-color); min-height: deprecated.$s-40; border-radius: deprecated.$br-8; .selection-btn { - @include deprecated.buttonStyle; + @include deprecated.button-style; display: grid; grid-template-columns: min-content auto 1fr auto auto; @@ -125,7 +125,7 @@ .checkbox-wrapper { @extend %input-checkbox; - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-24; width: deprecated.$s-24; @@ -137,8 +137,8 @@ } .selection-name { - @include deprecated.bodyLargeTypography; - @include deprecated.textEllipsis; + @include deprecated.body-large-typography; + @include deprecated.text-ellipsis; flex-grow: 1; color: var(--modal-text-foreground-color); @@ -146,8 +146,8 @@ } .selection-scale { - @include deprecated.bodyLargeTypography; - @include deprecated.textEllipsis; + @include deprecated.body-large-typography; + @include deprecated.text-ellipsis; min-width: deprecated.$s-108; padding: deprecated.$s-12; @@ -155,8 +155,8 @@ } .selection-extension { - @include deprecated.bodyLargeTypography; - @include deprecated.textEllipsis; + @include deprecated.body-large-typography; + @include deprecated.text-ellipsis; min-width: deprecated.$s-72; padding: deprecated.$s-12; @@ -165,7 +165,7 @@ } .image-wrapper { - @include deprecated.flexCenter; + @include deprecated.flex-center; min-height: deprecated.$s-32; min-width: deprecated.$s-32; @@ -202,7 +202,7 @@ .modal-scd-msg, .modal-subtitle, .modal-msg { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-text-foreground-color); } @@ -217,7 +217,7 @@ align-items: flex-start; .modal-subtitle { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); padding: 0.25rem 0; @@ -230,16 +230,16 @@ } .option-content { - @include deprecated.flexColumn; - @include deprecated.bodyLargeTypography; + @include deprecated.flex-column; + @include deprecated.body-large-typography; } .file-entry { .file-name { - @include deprecated.flexRow; + @include deprecated.flex-row; .file-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-16; width: deprecated.$s-16; @@ -252,8 +252,8 @@ } .file-name-label { - @include deprecated.bodyLargeTypography; - @include deprecated.textEllipsis; + @include deprecated.body-large-typography; + @include deprecated.text-ellipsis; } } diff --git a/frontend/src/app/main/ui/inspect/annotation.scss b/frontend/src/app/main/ui/inspect/annotation.scss index 863d3f6ede..75054e7e3a 100644 --- a/frontend/src/app/main/ui/inspect/annotation.scss +++ b/frontend/src/app/main/ui/inspect/annotation.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .attributes-block { - @include deprecated.flexColumn; + @include deprecated.flex-column; } .title-spacing-annotation { @@ -15,7 +15,7 @@ } .annotation-content { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--entry-foreground-color); } diff --git a/frontend/src/app/main/ui/inspect/code.scss b/frontend/src/app/main/ui/inspect/code.scss index bf9bd04168..4f455da3a2 100644 --- a/frontend/src/app/main/ui/inspect/code.scss +++ b/frontend/src/app/main/ui/inspect/code.scss @@ -22,7 +22,7 @@ .download-button { @extend %button-secondary; - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; height: deprecated.$s-32; width: 100%; @@ -30,7 +30,7 @@ } .code-block { - @include deprecated.codeTypography; + @include deprecated.code-typography; display: flex; flex-direction: column; @@ -63,7 +63,7 @@ } .code-lang { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; display: flex; align-items: center; @@ -95,7 +95,7 @@ } .code-lang-select { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; width: deprecated.$s-72; border: deprecated.$s-1 solid transparent; @@ -104,7 +104,7 @@ } .code-lang-option { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; width: deprecated.$s-72; height: deprecated.$s-32; @@ -120,7 +120,7 @@ } .toggle-btn { - @include deprecated.buttonStyle; + @include deprecated.button-style; display: flex; align-items: center; @@ -129,7 +129,7 @@ stroke: var(--title-foreground-color); .collapsabled-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-24; border-radius: deprecated.$br-8; diff --git a/frontend/src/app/main/ui/inspect/exports.scss b/frontend/src/app/main/ui/inspect/exports.scss index c3b5318d0e..690e83d06d 100644 --- a/frontend/src/app/main/ui/inspect/exports.scss +++ b/frontend/src/app/main/ui/inspect/exports.scss @@ -36,13 +36,13 @@ } .element-set-content { - @include deprecated.flexColumn; + @include deprecated.flex-column; margin-bottom: deprecated.$s-4; } .multiple-exports { - @include deprecated.flexRow; + @include deprecated.flex-row; grid-column: 1 / span 9; } @@ -52,7 +52,7 @@ } .actions { - @include deprecated.flexRow; + @include deprecated.flex-row; } .element-group { @@ -102,14 +102,14 @@ .suffix-input { @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; grid-column: span 3; } .export-btn { @extend %button-secondary; - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; height: deprecated.$s-32; width: 100%; diff --git a/frontend/src/app/main/ui/inspect/right_sidebar.scss b/frontend/src/app/main/ui/inspect/right_sidebar.scss index b0327b4673..1bc6a19a52 100644 --- a/frontend/src/app/main/ui/inspect/right_sidebar.scss +++ b/frontend/src/app/main/ui/inspect/right_sidebar.scss @@ -56,8 +56,8 @@ } .layer-title { - @include deprecated.bodySmallTypography; - @include deprecated.textEllipsis; + @include deprecated.body-small-typography; + @include deprecated.text-ellipsis; block-size: $sz-32; padding: var(--sp-s) 0; @@ -70,8 +70,8 @@ } .layer-subtitle { - @include deprecated.bodySmallTypography; - @include deprecated.textEllipsis; + @include deprecated.body-small-typography; + @include deprecated.text-ellipsis; color: var(--assets-item-name-foreground-color-rest); } diff --git a/frontend/src/app/main/ui/notifications/badge.scss b/frontend/src/app/main/ui/notifications/badge.scss index 62fb7ee090..54f46964ce 100644 --- a/frontend/src/app/main/ui/notifications/badge.scss +++ b/frontend/src/app/main/ui/notifications/badge.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .badge-notification { - @include deprecated.smallTitleTipography; + @include deprecated.small-title-typography; --badge-notification-bg-color: var(--alert-background-color-default); --badge-notification-fg-color: var(--alert-text-foreground-color-default); @@ -31,7 +31,7 @@ } .small { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; min-height: deprecated.$s-20; border-radius: deprecated.$br-6; diff --git a/frontend/src/app/main/ui/notifications/context_notification.scss b/frontend/src/app/main/ui/notifications/context_notification.scss index c455fca32b..aa38cea54a 100644 --- a/frontend/src/app/main/ui/notifications/context_notification.scss +++ b/frontend/src/app/main/ui/notifications/context_notification.scss @@ -68,7 +68,7 @@ } .context-text { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; align-self: center; color: var(--context-notification-fg-color); @@ -81,7 +81,7 @@ .link, .contain-html .context-text a { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; align-self: center; display: inline; diff --git a/frontend/src/app/main/ui/onboarding/questions.scss b/frontend/src/app/main/ui/onboarding/questions.scss index 909f08b75a..24ca56f535 100644 --- a/frontend/src/app/main/ui/onboarding/questions.scss +++ b/frontend/src/app/main/ui/onboarding/questions.scss @@ -29,7 +29,7 @@ // STEP CONTAINER .paginator { - @include deprecated.smallTitleTipography; + @include deprecated.small-title-typography; height: deprecated.$s-20; text-align: right; @@ -50,7 +50,7 @@ .radio-btns label, .select-class span { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; } // STEP 1 @@ -62,7 +62,7 @@ } .modal-title { - @include deprecated.bigTitleTipography; + @include deprecated.big-title-typography; color: var(--modal-title-foreground-color); min-height: deprecated.$s-32; @@ -70,7 +70,7 @@ } .modal-subtitle { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); margin: 0; @@ -78,7 +78,7 @@ } .modal-text { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-text-foreground-color); margin: 0; @@ -137,7 +137,7 @@ } .input-spacing input { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; } // STEP-4 diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs index bb896e49b5..38b4afc5d7 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.cljs +++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs @@ -236,7 +236,7 @@ [:div {:class (stl/css-case :modal-overlay true)} - [:div.animated.fadeIn {:class (stl/css :modal-container)} + [:div.animated.fade-in {:class (stl/css :modal-container)} [:h1 {:class (stl/css :modal-title)} (tr "onboarding-v2.welcome.title")] [:div {:class (stl/css :modal-sections)} diff --git a/frontend/src/app/main/ui/onboarding/team_choice.scss b/frontend/src/app/main/ui/onboarding/team_choice.scss index ade731846f..8b6487e53d 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.scss +++ b/frontend/src/app/main/ui/onboarding/team_choice.scss @@ -34,7 +34,7 @@ } .paginator { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; position: absolute; top: deprecated.$s-40; @@ -54,13 +54,13 @@ } .modal-title { - @include deprecated.bigTitleTipography; + @include deprecated.big-title-typography; color: var(--modal-title-foreground-color); } .modal-subtitle { - @include deprecated.medTitleTipography; + @include deprecated.med-title-typography; color: var(--modal-title-foreground-color); } @@ -70,34 +70,34 @@ } .modal-text { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-text-foreground-color); margin: 0; } .modal-desc { - @include deprecated.smallTitleTipography; + @include deprecated.small-title-typography; margin: 0; color: var(--modal-title-foreground-color); } .team-features { - @include deprecated.flexColumn; + @include deprecated.flex-column; gap: deprecated.$s-16; margin: 0; } .feature { - @include deprecated.flexRow; + @include deprecated.flex-row; gap: deprecated.$s-16; } .icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-32; width: deprecated.$s-32; @@ -150,7 +150,7 @@ .first-block, .second-block { - @include deprecated.flexColumn; + @include deprecated.flex-column; gap: deprecated.$s-16; } @@ -165,8 +165,8 @@ @extend %input-element-label; label { - @include deprecated.flexColumn; - @include deprecated.bodySmallTypography; + @include deprecated.flex-column; + @include deprecated.body-small-typography; align-items: flex-start; width: 100%; @@ -175,7 +175,7 @@ height: 100%; input { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; margin-top: deprecated.$s-8; } @@ -201,7 +201,7 @@ } .role-title { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; margin-block-end: deprecated.$s-8; color: var(--modal-title-foreground-color); @@ -213,7 +213,7 @@ } .modal-hint { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--modal-text-foreground-color); text-align: right; diff --git a/frontend/src/app/main/ui/releases.cljs b/frontend/src/app/main/ui/releases.cljs index a5d2f5610b..50c6634d09 100644 --- a/frontend/src/app/main/ui/releases.cljs +++ b/frontend/src/app/main/ui/releases.cljs @@ -53,7 +53,7 @@ (let [slide* (mf/use-state :start) slide (deref slide*) - klass* (mf/use-state "fadeInDown") + klass* (mf/use-state "fade-in-down") klass (deref klass*) navigate @@ -78,7 +78,7 @@ (mf/with-effect [slide] (when (not= :start slide) - (reset! klass* "fadeIn")) + (reset! klass* "fade-in")) (let [sem (tm/schedule 300 #(reset! klass* nil))] (fn [] (reset! klass* nil) diff --git a/frontend/src/app/main/ui/releases/v2_0.scss b/frontend/src/app/main/ui/releases/v2_0.scss index 77d6a4ced1..f47c7c9043 100644 --- a/frontend/src/app/main/ui/releases/v2_0.scss +++ b/frontend/src/app/main/ui/releases/v2_0.scss @@ -38,8 +38,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -49,7 +49,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -68,20 +68,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; diff --git a/frontend/src/app/main/ui/releases/v2_1.scss b/frontend/src/app/main/ui/releases/v2_1.scss index ccf5348282..4b9913e040 100644 --- a/frontend/src/app/main/ui/releases/v2_1.scss +++ b/frontend/src/app/main/ui/releases/v2_1.scss @@ -38,8 +38,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -49,7 +49,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -62,7 +62,7 @@ } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); diff --git a/frontend/src/app/main/ui/releases/v2_10.scss b/frontend/src/app/main/ui/releases/v2_10.scss index 68603d9658..40c8f5316f 100644 --- a/frontend/src/app/main/ui/releases/v2_10.scss +++ b/frontend/src/app/main/ui/releases/v2_10.scss @@ -42,8 +42,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -53,7 +53,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -72,20 +72,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; diff --git a/frontend/src/app/main/ui/releases/v2_11.scss b/frontend/src/app/main/ui/releases/v2_11.scss index 68603d9658..40c8f5316f 100644 --- a/frontend/src/app/main/ui/releases/v2_11.scss +++ b/frontend/src/app/main/ui/releases/v2_11.scss @@ -42,8 +42,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -53,7 +53,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -72,20 +72,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; diff --git a/frontend/src/app/main/ui/releases/v2_12.scss b/frontend/src/app/main/ui/releases/v2_12.scss index 68603d9658..40c8f5316f 100644 --- a/frontend/src/app/main/ui/releases/v2_12.scss +++ b/frontend/src/app/main/ui/releases/v2_12.scss @@ -42,8 +42,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -53,7 +53,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -72,20 +72,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; diff --git a/frontend/src/app/main/ui/releases/v2_13.scss b/frontend/src/app/main/ui/releases/v2_13.scss index 68603d9658..40c8f5316f 100644 --- a/frontend/src/app/main/ui/releases/v2_13.scss +++ b/frontend/src/app/main/ui/releases/v2_13.scss @@ -42,8 +42,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -53,7 +53,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -72,20 +72,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; diff --git a/frontend/src/app/main/ui/releases/v2_14.scss b/frontend/src/app/main/ui/releases/v2_14.scss index 68603d9658..40c8f5316f 100644 --- a/frontend/src/app/main/ui/releases/v2_14.scss +++ b/frontend/src/app/main/ui/releases/v2_14.scss @@ -42,8 +42,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -53,7 +53,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -72,20 +72,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; diff --git a/frontend/src/app/main/ui/releases/v2_2.scss b/frontend/src/app/main/ui/releases/v2_2.scss index ede5b103bf..beb1bdf674 100644 --- a/frontend/src/app/main/ui/releases/v2_2.scss +++ b/frontend/src/app/main/ui/releases/v2_2.scss @@ -38,8 +38,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -49,7 +49,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -62,7 +62,7 @@ } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); diff --git a/frontend/src/app/main/ui/releases/v2_3.scss b/frontend/src/app/main/ui/releases/v2_3.scss index 68603d9658..40c8f5316f 100644 --- a/frontend/src/app/main/ui/releases/v2_3.scss +++ b/frontend/src/app/main/ui/releases/v2_3.scss @@ -42,8 +42,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -53,7 +53,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -72,20 +72,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; diff --git a/frontend/src/app/main/ui/releases/v2_4.scss b/frontend/src/app/main/ui/releases/v2_4.scss index 68603d9658..40c8f5316f 100644 --- a/frontend/src/app/main/ui/releases/v2_4.scss +++ b/frontend/src/app/main/ui/releases/v2_4.scss @@ -42,8 +42,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -53,7 +53,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -72,20 +72,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; diff --git a/frontend/src/app/main/ui/releases/v2_5.scss b/frontend/src/app/main/ui/releases/v2_5.scss index 68603d9658..40c8f5316f 100644 --- a/frontend/src/app/main/ui/releases/v2_5.scss +++ b/frontend/src/app/main/ui/releases/v2_5.scss @@ -42,8 +42,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -53,7 +53,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -72,20 +72,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; diff --git a/frontend/src/app/main/ui/releases/v2_6.scss b/frontend/src/app/main/ui/releases/v2_6.scss index 68603d9658..40c8f5316f 100644 --- a/frontend/src/app/main/ui/releases/v2_6.scss +++ b/frontend/src/app/main/ui/releases/v2_6.scss @@ -42,8 +42,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -53,7 +53,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -72,20 +72,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; diff --git a/frontend/src/app/main/ui/releases/v2_7.scss b/frontend/src/app/main/ui/releases/v2_7.scss index 68603d9658..40c8f5316f 100644 --- a/frontend/src/app/main/ui/releases/v2_7.scss +++ b/frontend/src/app/main/ui/releases/v2_7.scss @@ -42,8 +42,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -53,7 +53,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -72,20 +72,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; diff --git a/frontend/src/app/main/ui/releases/v2_8.scss b/frontend/src/app/main/ui/releases/v2_8.scss index 68603d9658..40c8f5316f 100644 --- a/frontend/src/app/main/ui/releases/v2_8.scss +++ b/frontend/src/app/main/ui/releases/v2_8.scss @@ -42,8 +42,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -53,7 +53,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -72,20 +72,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; diff --git a/frontend/src/app/main/ui/releases/v2_9.scss b/frontend/src/app/main/ui/releases/v2_9.scss index 68603d9658..40c8f5316f 100644 --- a/frontend/src/app/main/ui/releases/v2_9.scss +++ b/frontend/src/app/main/ui/releases/v2_9.scss @@ -42,8 +42,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -53,7 +53,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -72,20 +72,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; diff --git a/frontend/src/app/main/ui/settings/change_email.scss b/frontend/src/app/main/ui/settings/change_email.scss index dd4489d99b..60044a05a4 100644 --- a/frontend/src/app/main/ui/settings/change_email.scss +++ b/frontend/src/app/main/ui/settings/change_email.scss @@ -21,7 +21,7 @@ } .modal-title { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; color: var(--modal-title-foreground-color); } @@ -31,19 +31,19 @@ } .modal-content { - @include deprecated.flexColumn; - @include deprecated.bodySmallTypography; + @include deprecated.flex-column; + @include deprecated.body-small-typography; gap: deprecated.$s-24; margin-bottom: deprecated.$s-24; } .fields-row { - @include deprecated.flexColumn; + @include deprecated.flex-column; } .select-title { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--modal-title-foreground-color); } diff --git a/frontend/src/app/main/ui/settings/delete_account.scss b/frontend/src/app/main/ui/settings/delete_account.scss index 3f07f30774..4b0b6408c9 100644 --- a/frontend/src/app/main/ui/settings/delete_account.scss +++ b/frontend/src/app/main/ui/settings/delete_account.scss @@ -21,7 +21,7 @@ } .modal-title { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; color: var(--modal-title-foreground-color); } @@ -31,19 +31,19 @@ } .modal-content { - @include deprecated.flexColumn; - @include deprecated.bodySmallTypography; + @include deprecated.flex-column; + @include deprecated.body-small-typography; gap: deprecated.$s-24; margin-bottom: deprecated.$s-24; } .fields-row { - @include deprecated.flexColumn; + @include deprecated.flex-column; } .select-title { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--modal-title-foreground-color); } diff --git a/frontend/src/app/main/ui/settings/sidebar.scss b/frontend/src/app/main/ui/settings/sidebar.scss index 501a9dd137..e9571a7ab4 100644 --- a/frontend/src/app/main/ui/settings/sidebar.scss +++ b/frontend/src/app/main/ui/settings/sidebar.scss @@ -69,12 +69,12 @@ } .element-title { - @include deprecated.textEllipsis; - @include deprecated.bodyMediumTypography; + @include deprecated.text-ellipsis; + @include deprecated.body-medium-typography; } .back-to-dashboard { - @include deprecated.buttonStyle; + @include deprecated.button-style; display: flex; align-items: center; diff --git a/frontend/src/app/main/ui/settings/subscription.scss b/frontend/src/app/main/ui/settings/subscription.scss index e881541721..98ad84d385 100644 --- a/frontend/src/app/main/ui/settings/subscription.scss +++ b/frontend/src/app/main/ui/settings/subscription.scss @@ -168,7 +168,7 @@ .cta-button { @include t.use-typography("body-medium"); - @include deprecated.buttonStyle; + @include deprecated.button-style; align-items: center; color: var(--color-accent-primary); diff --git a/frontend/src/app/main/ui/static.scss b/frontend/src/app/main/ui/static.scss index 7a5a694402..32c80dce5e 100644 --- a/frontend/src/app/main/ui/static.scss +++ b/frontend/src/app/main/ui/static.scss @@ -202,13 +202,13 @@ } .project-name { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; color: var(--title-foreground-color); } .file-name { - @include deprecated.smallTitleTipography; + @include deprecated.small-title-typography; text-transform: none; color: var(--title-foreground-color-hover); diff --git a/frontend/src/app/main/ui/viewer.scss b/frontend/src/app/main/ui/viewer.scss index b64ebe2b79..6fbf27ce92 100644 --- a/frontend/src/app/main/ui/viewer.scss +++ b/frontend/src/app/main/ui/viewer.scss @@ -24,7 +24,7 @@ } .empty-state { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--empty-message-foreground-color); display: grid; @@ -47,7 +47,7 @@ } .thumbnails-close { - @include deprecated.buttonStyle; + @include deprecated.button-style; grid-row: 1 / span 2; grid-column: 1 / span 1; @@ -81,7 +81,7 @@ .viewer-go-prev, .viewer-go-next { @extend %button-secondary; - @include deprecated.flexCenter; + @include deprecated.flex-center; position: absolute; right: deprecated.$s-8; @@ -128,7 +128,7 @@ .reset-button { @extend %button-secondary; - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-32; width: deprecated.$s-28; @@ -144,8 +144,8 @@ } .counter { - @include deprecated.flexCenter; - @include deprecated.bodySmallTypography; + @include deprecated.flex-center; + @include deprecated.body-small-typography; border-radius: deprecated.$br-8; width: deprecated.$s-64; diff --git a/frontend/src/app/main/ui/viewer/comments.scss b/frontend/src/app/main/ui/viewer/comments.scss index f417ea86b9..a6b2882ad2 100644 --- a/frontend/src/app/main/ui/viewer/comments.scss +++ b/frontend/src/app/main/ui/viewer/comments.scss @@ -8,7 +8,7 @@ // COMMENT DROPDOWN ON HEADER .view-options { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; align-items: center; @@ -31,7 +31,7 @@ } .dropdown-title { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; flex-grow: 1; color: var(--input-foreground-color-active); @@ -44,7 +44,7 @@ .icon, .icon-dropdown { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: 100%; width: deprecated.$s-16; @@ -64,7 +64,7 @@ @extend %dropdown-element-base; .icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: 100%; width: deprecated.$s-16; diff --git a/frontend/src/app/main/ui/viewer/header.scss b/frontend/src/app/main/ui/viewer/header.scss index 091df03fde..c80da08171 100644 --- a/frontend/src/app/main/ui/viewer/header.scss +++ b/frontend/src/app/main/ui/viewer/header.scss @@ -45,39 +45,39 @@ } .sitemap-zone { - @include deprecated.flexColumn; + @include deprecated.flex-column; position: relative; width: 100%; } .project-name { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; color: var(--title-foreground-color); } .sitemap-text { - @include deprecated.flexRow; + @include deprecated.flex-row; } .breadcrumb { - @include deprecated.bodySmallTypography; - @include deprecated.flexRow; + @include deprecated.body-small-typography; + @include deprecated.flex-row; color: var(--title-foreground-color); cursor: pointer; } .breadcrumb-text { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; max-width: 12vw; // This is a fallback max-width: 12cqw; // This is a unit refered to container } .icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-16; width: deprecated.$s-16; @@ -107,7 +107,7 @@ @extend %dropdown-element-base; .icon-check { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: 100%; width: deprecated.$s-16; @@ -125,8 +125,8 @@ } .current-frame { - @include deprecated.bodySmallTypography; - @include deprecated.flexRow; + @include deprecated.body-small-typography; + @include deprecated.flex-row; flex-grow: 1; color: var(--title-foreground-color-hover); @@ -138,7 +138,7 @@ } .frame-name { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; max-width: 17vw; // This is a fallback max-width: 17cqw; // This is a unit refered to container @@ -146,14 +146,14 @@ // SECTION BUTTONS .mode-zone { - @include deprecated.flexRow; + @include deprecated.flex-row; height: 100%; } .mode-zone-btn { @extend %button-tertiary; - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-32; width: deprecated.$s-28; @@ -170,7 +170,7 @@ // OPTION AREA .options-zone { - @include deprecated.flexRow; + @include deprecated.flex-row; position: relative; justify-content: flex-end; @@ -187,7 +187,7 @@ .fullscreen-btn { @extend %button-tertiary; - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-32; width: deprecated.$s-28; @@ -209,7 +209,7 @@ .edit-btn { @extend %button-tertiary; - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-32; width: deprecated.$s-28; @@ -223,7 +223,7 @@ .go-log-btn { @extend %button-tertiary; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; height: deprecated.$s-32; padding: 0 deprecated.$s-8; @@ -233,15 +233,15 @@ // ZOOM WIDGET .zoom-widget { - @include deprecated.buttonStyle; - @include deprecated.flexCenter; + @include deprecated.button-style; + @include deprecated.flex-center; height: deprecated.$s-28; min-width: deprecated.$s-64; border-radius: deprecated.$br-8; .label { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--button-tertiary-foreground-color-rest); } @@ -286,7 +286,7 @@ border-radius: deprecated.$br-8; .zoom-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-24; height: deprecated.$s-32; @@ -306,7 +306,7 @@ } .zoom-text { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: 100%; min-width: deprecated.$s-64; diff --git a/frontend/src/app/main/ui/viewer/inspect.scss b/frontend/src/app/main/ui/viewer/inspect.scss index 616c127985..171340752b 100644 --- a/frontend/src/app/main/ui/viewer/inspect.scss +++ b/frontend/src/app/main/ui/viewer/inspect.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .inspect-svg-wrapper { - @include deprecated.flexCenter; + @include deprecated.flex-center; position: relative; flex-direction: column; diff --git a/frontend/src/app/main/ui/viewer/interactions.scss b/frontend/src/app/main/ui/viewer/interactions.scss index c7245a5910..d52fb6d933 100644 --- a/frontend/src/app/main/ui/viewer/interactions.scss +++ b/frontend/src/app/main/ui/viewer/interactions.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .view-options { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; align-items: center; @@ -21,7 +21,7 @@ } .dropdown-title { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; flex-grow: 1; color: var(--input-foreground-color-active); @@ -49,7 +49,7 @@ min-height: deprecated.$s-32; .icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: 100%; width: deprecated.$s-16; @@ -78,7 +78,7 @@ .icon, .icon-dropdown { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: 100%; width: deprecated.$s-16; diff --git a/frontend/src/app/main/ui/viewer/login.scss b/frontend/src/app/main/ui/viewer/login.scss index 7fd5afb70a..965a7e9ccf 100644 --- a/frontend/src/app/main/ui/viewer/login.scss +++ b/frontend/src/app/main/ui/viewer/login.scss @@ -21,7 +21,7 @@ } .modal-title { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; color: var(--modal-title-foreground-color); } @@ -31,8 +31,8 @@ } .modal-content { - @include deprecated.flexColumn; - @include deprecated.bodySmallTypography; + @include deprecated.flex-column; + @include deprecated.body-small-typography; gap: deprecated.$s-24; max-height: deprecated.$s-400; diff --git a/frontend/src/app/main/ui/viewer/share_link.scss b/frontend/src/app/main/ui/viewer/share_link.scss index 1eea68ad9e..a0dc26278d 100644 --- a/frontend/src/app/main/ui/viewer/share_link.scss +++ b/frontend/src/app/main/ui/viewer/share_link.scss @@ -26,7 +26,7 @@ } .share-link-title { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; color: var(--modal-title-foreground-color); } @@ -36,20 +36,20 @@ } .modal-content { - @include deprecated.bodySmallTypography; - @include deprecated.flexColumn; + @include deprecated.body-small-typography; + @include deprecated.flex-column; gap: deprecated.$s-24; } .share-link-section { - @include deprecated.flexColumn; + @include deprecated.flex-column; gap: deprecated.$s-8; } .hint-wrapper { - @include deprecated.flexRow; + @include deprecated.flex-row; } .hint { @@ -58,7 +58,7 @@ } .custon-input-wrapper { - @include deprecated.flexRow; + @include deprecated.flex-row; border-radius: deprecated.$br-8; height: deprecated.$s-32; @@ -68,7 +68,7 @@ .input-text { @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--input-foreground-color-active); padding-left: deprecated.$s-8; @@ -83,7 +83,7 @@ .copy-button { @extend %button-secondary; - @include deprecated.flexRow; + @include deprecated.flex-row; gap: deprecated.$s-8; height: deprecated.$s-32; @@ -97,14 +97,14 @@ } .description { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--modal-text-foreground-color); margin-bottom: deprecated.$s-24; } .actions { - @include deprecated.flexRow; + @include deprecated.flex-row; justify-content: flex-end; } @@ -122,14 +122,14 @@ } .permissions-section { - @include deprecated.flexColumn; + @include deprecated.flex-column; gap: deprecated.$s-8; } .manage-permissions { - @include deprecated.buttonStyle; - @include deprecated.uppercaseTitleTipography; + @include deprecated.button-style; + @include deprecated.uppercase-title-typography; color: var(--menu-foreground-color-rest); height: deprecated.$s-32; @@ -139,7 +139,7 @@ } .icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; margin-right: deprecated.$s-6; @@ -182,7 +182,7 @@ } .select-all-row { - @include deprecated.flexRow; + @include deprecated.flex-row; justify-content: space-between; height: deprecated.$s-32; @@ -207,7 +207,7 @@ .count-pages, .current-tag { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--input-foreground-color); } diff --git a/frontend/src/app/main/ui/viewer/thumbnails.scss b/frontend/src/app/main/ui/viewer/thumbnails.scss index 15d12a6794..10bc4e7d31 100644 --- a/frontend/src/app/main/ui/viewer/thumbnails.scss +++ b/frontend/src/app/main/ui/viewer/thumbnails.scss @@ -33,13 +33,13 @@ } .counter { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--viewer-thumbnails-control-foreground-color); } .actions { - @include deprecated.flexRow; + @include deprecated.flex-row; width: deprecated.$s-60; } @@ -77,7 +77,7 @@ .right-scroll-handler, .left-scroll-handler { @extend %button-tertiary; - @include deprecated.flexCenter; + @include deprecated.flex-center; grid-column: 3 / span 1; grid-row: 1 / span 1; @@ -121,7 +121,7 @@ } .thumbnail-item { - @include deprecated.buttonStyle; + @include deprecated.button-style; display: flex; flex-direction: column; @@ -129,7 +129,7 @@ } .thumbnail-preview { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-132; min-height: deprecated.$s-132; @@ -153,8 +153,8 @@ } .thumbnail-info { - @include deprecated.bodySmallTypography; - @include deprecated.textEllipsis; + @include deprecated.body-small-typography; + @include deprecated.text-ellipsis; text-align: center; color: var(--viewer-thumbnails-control-foreground-color); diff --git a/frontend/src/app/main/ui/workspace/color_palette.scss b/frontend/src/app/main/ui/workspace/color_palette.scss index 026b57eb73..2d240b86a8 100644 --- a/frontend/src/app/main/ui/workspace/color_palette.scss +++ b/frontend/src/app/main/ui/workspace/color_palette.scss @@ -31,8 +31,8 @@ .left-arrow, .right-arrow { - @include deprecated.buttonStyle; - @include deprecated.flexCenter; + @include deprecated.button-style; + @include deprecated.flex-center; position: relative; height: 100%; @@ -124,14 +124,14 @@ height: 100%; &.no-text { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-32; } } .color-palette-empty { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--palette-text-color); } diff --git a/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss b/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss index 79e7cf868e..5aa8ee06a7 100644 --- a/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss +++ b/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss @@ -36,7 +36,7 @@ width: 100%; .library-name { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--context-menu-foreground-color); display: grid; @@ -47,7 +47,7 @@ max-width: deprecated.$s-400; .lib-name { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; max-width: deprecated.$s-380; } @@ -60,11 +60,11 @@ .icon-wrapper { margin-left: deprecated.$s-4; - @include deprecated.flexCenter; + @include deprecated.flex-center; svg { @extend %button-icon-small; - @include deprecated.flexCenter; + @include deprecated.flex-center; stroke: var(--icon-foreground); } @@ -85,10 +85,10 @@ color: var(--context-menu-foreground-color-selected); .icon-wrapper { - @include deprecated.flexCenter; + @include deprecated.flex-center; svg { - @include deprecated.flexCenter; + @include deprecated.flex-center; @extend %button-icon-small; stroke: var(--context-menu-foreground-color-selected); diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss index e1e3a60acf..fe5b93d679 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .color-values { - @include deprecated.flexColumn; + @include deprecated.flex-column; margin-top: deprecated.$s-8; @@ -16,11 +16,11 @@ } .colors-row { - @include deprecated.flexRow; + @include deprecated.flex-row; .input-wrapper { @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; width: deprecated.$s-84; display: flex; @@ -29,11 +29,11 @@ } .hex-alpha-wrapper { - @include deprecated.flexRow; + @include deprecated.flex-row; .input-wrapper { @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; width: deprecated.$s-84; diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss b/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss index b2efc2b627..af6a439328 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss @@ -141,7 +141,7 @@ .offset-input-wrapper { @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; width: deprecated.$s-92; } diff --git a/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss b/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss index c3a3cced1e..74b34eab5d 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss @@ -15,7 +15,7 @@ } .hue-wheel-wrapper { - @include deprecated.flexCenter; + @include deprecated.flex-center; position: relative; } @@ -39,7 +39,7 @@ } .handlers-wrapper { - @include deprecated.flexRow; + @include deprecated.flex-row; height: deprecated.$s-200; width: deprecated.$s-52; diff --git a/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss b/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss index e8222e902c..17e0b52fc6 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .hsva-selector { - @include deprecated.flexColumn; + @include deprecated.flex-column; padding: deprecated.$s-4; row-gap: deprecated.$s-8; @@ -20,7 +20,7 @@ } .hsva-selector-label { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; display: flex; align-items: center; diff --git a/frontend/src/app/main/ui/workspace/comments.scss b/frontend/src/app/main/ui/workspace/comments.scss index 0b49ea07c8..9b956523ca 100644 --- a/frontend/src/app/main/ui/workspace/comments.scss +++ b/frontend/src/app/main/ui/workspace/comments.scss @@ -23,7 +23,7 @@ } .mode-dropdown-wrapper { - @include deprecated.buttonStyle; + @include deprecated.button-style; @extend %asset-element; background-color: var(--color-background-tertiary); @@ -46,7 +46,7 @@ } .arrow-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-24; width: deprecated.$s-24; @@ -78,7 +78,7 @@ justify-content: space-between; .icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-24; width: deprecated.$s-24; @@ -91,7 +91,7 @@ } .label { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; } &:hover { diff --git a/frontend/src/app/main/ui/workspace/context_menu.scss b/frontend/src/app/main/ui/workspace/context_menu.scss index bef5f39fd9..53d1d0baf2 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.scss +++ b/frontend/src/app/main/ui/workspace/context_menu.scss @@ -15,7 +15,7 @@ .context-list, .workspace-context-submenu { - @include deprecated.menuShadow; + @include deprecated.menu-shadow; display: grid; width: deprecated.$s-240; @@ -46,20 +46,20 @@ cursor: pointer; .title { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--menu-foreground-color); } .shortcut { - @include deprecated.flexCenter; + @include deprecated.flex-center; gap: deprecated.$s-2; color: var(--menu-shortcut-foreground-color); .shortcut-key { - @include deprecated.bodySmallTypography; - @include deprecated.flexCenter; + @include deprecated.body-small-typography; + @include deprecated.flex-center; height: deprecated.$s-20; padding: deprecated.$s-2 deprecated.$s-6; diff --git a/frontend/src/app/main/ui/workspace/left_header.scss b/frontend/src/app/main/ui/workspace/left_header.scss index 1007aeeb44..1522c0a2d5 100644 --- a/frontend/src/app/main/ui/workspace/left_header.scss +++ b/frontend/src/app/main/ui/workspace/left_header.scss @@ -14,7 +14,7 @@ } .main-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-32; height: deprecated.$s-32; @@ -38,8 +38,8 @@ .project-name, .file-name { - @include deprecated.uppercaseTitleTipography; - @include deprecated.textEllipsis; + @include deprecated.uppercase-title-typography; + @include deprecated.text-ellipsis; height: deprecated.$s-16; width: 100%; @@ -49,7 +49,7 @@ } .file-name { - @include deprecated.smallTitleTipography; + @include deprecated.small-title-typography; text-transform: none; color: var(--title-foreground-color-hover); @@ -59,11 +59,11 @@ } .file-name-label { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; } .file-name-input { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: 100%; margin: 0; @@ -82,7 +82,7 @@ } .shared-badge { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-16; height: deprecated.$s-32; diff --git a/frontend/src/app/main/ui/workspace/nudge.scss b/frontend/src/app/main/ui/workspace/nudge.scss index 82797d3b3e..8b62ca7f10 100644 --- a/frontend/src/app/main/ui/workspace/nudge.scss +++ b/frontend/src/app/main/ui/workspace/nudge.scss @@ -21,7 +21,7 @@ } .modal-title { - @include deprecated.headlineMediumTypography; + @include deprecated.headline-medium-typography; color: var(--modal-title-foreground-color); } @@ -31,18 +31,18 @@ } .modal-content { - @include deprecated.flexColumn; + @include deprecated.flex-column; gap: deprecated.$s-24; - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; margin-bottom: deprecated.$s-24; } .input-wrapper { @extend %input-with-label; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; label { text-transform: none; @@ -50,7 +50,7 @@ } .modal-msg { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-text-foreground-color); line-height: 1.5; diff --git a/frontend/src/app/main/ui/workspace/palette.scss b/frontend/src/app/main/ui/workspace/palette.scss index f7d77f70b6..53201a64bd 100644 --- a/frontend/src/app/main/ui/workspace/palette.scss +++ b/frontend/src/app/main/ui/workspace/palette.scss @@ -72,7 +72,7 @@ } .palette-item { - @include deprecated.flexCenter; + @include deprecated.flex-center; border-radius: deprecated.$br-8; opacity: deprecated.$op-10; @@ -127,8 +127,8 @@ } .handler { - @include deprecated.buttonStyle; - @include deprecated.flexCenter; + @include deprecated.button-style; + @include deprecated.flex-center; width: deprecated.$s-12; height: 100%; diff --git a/frontend/src/app/main/ui/workspace/plugins.scss b/frontend/src/app/main/ui/workspace/plugins.scss index d51dc2ff68..06d774ded8 100644 --- a/frontend/src/app/main/ui/workspace/plugins.scss +++ b/frontend/src/app/main/ui/workspace/plugins.scss @@ -52,7 +52,7 @@ } .modal-title { - @include deprecated.headlineMediumTypography; + @include deprecated.headline-medium-typography; margin-block-end: deprecated.$s-32; color: var(--modal-title-foreground-color); @@ -86,7 +86,7 @@ .primary-button { @extend %button-primary; - @include deprecated.headlineSmallTypography; + @include deprecated.headline-small-typography; padding: deprecated.$s-0 deprecated.$s-16; } @@ -98,13 +98,13 @@ .cancel-button { @extend %button-secondary; - @include deprecated.headlineSmallTypography; + @include deprecated.headline-small-typography; padding: deprecated.$s-0 deprecated.$s-16; } .search-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-20; padding: 0 0 0 deprecated.$s-8; @@ -163,13 +163,13 @@ } .plugin-title { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--color-foreground-primary); } .plugin-summary { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--color-foreground-secondary); } @@ -202,7 +202,7 @@ } .plugins-empty-text { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--color-foreground-primary); } @@ -212,7 +212,7 @@ div.input-error { } .info { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; margin-top: deprecated.$s-4; @@ -262,14 +262,14 @@ div.input-error { } .permissions-list-text { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; margin: 0; color: var(--color-foreground-secondary); } .permissions-disclaimer { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; padding: deprecated.$s-16; background: var(--color-background-quaternary); @@ -283,7 +283,7 @@ div.input-error { } .discover { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--color-foreground-secondary); margin-top: deprecated.$s-24; diff --git a/frontend/src/app/main/ui/workspace/right_header.scss b/frontend/src/app/main/ui/workspace/right_header.scss index 2f6bbbeb52..50cee33d7a 100644 --- a/frontend/src/app/main/ui/workspace/right_header.scss +++ b/frontend/src/app/main/ui/workspace/right_header.scss @@ -28,7 +28,7 @@ } .zoom-widget { - @include deprecated.buttonStyle; + @include deprecated.button-style; display: flex; align-items: center; @@ -39,7 +39,7 @@ border-radius: deprecated.$br-8; .label { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; height: 100%; padding: deprecated.$s-8 0; @@ -79,7 +79,7 @@ } .zoom-text { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: 100%; min-width: deprecated.$s-48; @@ -181,14 +181,14 @@ } .persistence-status-widget { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-28; height: deprecated.$s-28; } .status-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-24; height: deprecated.$s-24; diff --git a/frontend/src/app/main/ui/workspace/sidebar.scss b/frontend/src/app/main/ui/workspace/sidebar.scss index 42708ad9f6..5747dbcd19 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/sidebar.scss @@ -102,8 +102,8 @@ .collapse-sidebar-button { --collapse-icon-color: var(--color-foreground-secondary); - @include deprecated.flexCenter; - @include deprecated.buttonStyle; + @include deprecated.flex-center; + @include deprecated.button-style; height: 100%; width: deprecated.$s-24; @@ -117,7 +117,7 @@ } .collapsed-sidebar { - @include deprecated.flexCenter; + @include deprecated.flex-center; position: absolute; top: deprecated.$s-48; @@ -129,7 +129,7 @@ } .collapsed-title { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-36; width: deprecated.$s-24; @@ -138,7 +138,7 @@ } .collapsed-button { - @include deprecated.buttonStyle; + @include deprecated.button-style; height: deprecated.$s-24; width: deprecated.$s-16; @@ -146,7 +146,7 @@ border-radius: deprecated.$br-5; svg { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-16; width: deprecated.$s-16; diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.scss b/frontend/src/app/main/ui/workspace/sidebar/assets.scss index ebf3dc0f89..d04fac7aa2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.scss @@ -19,7 +19,7 @@ .libraries-button { @extend %button-secondary; - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; gap: deprecated.$s-2; height: deprecated.$s-32; @@ -42,7 +42,7 @@ .add-library-button { @extend %button-primary; - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; gap: deprecated.$s-2; height: deprecated.$s-32; @@ -52,8 +52,8 @@ } .section-button { - @include deprecated.flexCenter; - @include deprecated.buttonStyle; + @include deprecated.flex-center; + @include deprecated.button-style; height: deprecated.$s-32; width: deprecated.$s-32; @@ -106,8 +106,8 @@ } .sections-container { - @include deprecated.menuShadow; - @include deprecated.flexColumn; + @include deprecated.menu-shadow; + @include deprecated.flex-column; position: absolute; top: deprecated.$s-84; @@ -120,7 +120,7 @@ } .section-item { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; align-items: center; @@ -131,7 +131,7 @@ } .section-btn { - @include deprecated.buttonStyle; + @include deprecated.button-style; } .assets-header { diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss index 2e44a4ba11..aaa1b09f37 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss @@ -40,9 +40,9 @@ $assets-button-width: deprecated.$s-28; border: deprecated.$s-1 solid var(--input-border-color-focus); input.element-name { - @include deprecated.textEllipsis; - @include deprecated.bodySmallTypography; - @include deprecated.removeInputStyle; + @include deprecated.text-ellipsis; + @include deprecated.body-small-typography; + @include deprecated.remove-input-style; flex-grow: 1; margin: 0; @@ -56,7 +56,7 @@ $assets-button-width: deprecated.$s-28; } .bullet-block { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: 100%; justify-content: flex-start; @@ -64,8 +64,8 @@ $assets-button-width: deprecated.$s-28; } .name-block { - @include deprecated.bodySmallTypography; - @include deprecated.textEllipsis; + @include deprecated.body-small-typography; + @include deprecated.text-ellipsis; margin: 0; color: var(--assets-item-name-foreground-color); @@ -81,7 +81,7 @@ $assets-button-width: deprecated.$s-28; } .element-name { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; color: var(--color-foreground-primary); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss index 4773f97583..2188db46a8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .title-name { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; display: flex; align-items: center; @@ -16,7 +16,7 @@ } .title-tokens { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; text-transform: capitalize; } @@ -26,12 +26,12 @@ } .section-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; padding-right: deprecated.$s-2; svg { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-16; width: deprecated.$s-16; @@ -47,7 +47,7 @@ } .num-assets { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: 100%; padding-left: deprecated.$s-8; @@ -62,8 +62,8 @@ } .drag-counter { - @include deprecated.bodySmallTypography; - @include deprecated.textEllipsis; + @include deprecated.body-small-typography; + @include deprecated.text-ellipsis; position: absolute; bottom: 0; diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss index c4c2315d64..e8c63bb18f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss @@ -37,7 +37,7 @@ } .special-title { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; color: var(--title-foreground-color-hover); margin-left: deprecated.$s-2; @@ -75,7 +75,7 @@ } .no-found-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; background-color: var(--not-found-background-color); border-radius: deprecated.$br-circle; @@ -92,7 +92,7 @@ } .no-found-text { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--not-found-foreground-color); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss index 12a50ea556..0152e5e52c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss @@ -40,7 +40,7 @@ } .path { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; margin-left: deprecated.$s-2; text-transform: initial; @@ -60,7 +60,7 @@ } .modal-title { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; color: var(--modal-title-foreground-color); } @@ -70,14 +70,14 @@ } .modal-content { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; margin-bottom: deprecated.$s-24; } .input-wrapper { @extend %input-with-label; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; margin-bottom: deprecated.$s-8; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss b/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss index 290f4460e9..659444daaf 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss @@ -11,8 +11,8 @@ --element-name-comp-color: var(--context-hover-color, var(--layer-row-component-foreground-color)); --element-name-opacity: var(--context-hover-opacity, deprecated.$op-7); - @include deprecated.textEllipsis; - @include deprecated.bodySmallTypography; + @include deprecated.text-ellipsis; + @include deprecated.body-small-typography; color: var(--element-name-color); flex-grow: 1; @@ -41,9 +41,9 @@ --element-name-input-border-color: var(--input-border-color-focus); --element-name-input-color: var(--layer-row-foreground-color); - @include deprecated.textEllipsis; - @include deprecated.bodySmallTypography; - @include deprecated.removeInputStyle; + @include deprecated.text-ellipsis; + @include deprecated.body-small-typography; + @include deprecated.remove-input-style; flex-grow: 1; height: deprecated.$s-28; diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.scss b/frontend/src/app/main/ui/workspace/sidebar/layers.scss index 7fb33a72da..048aa08549 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.scss @@ -20,8 +20,8 @@ gap: deprecated.$s-4; .filter-button { - @include deprecated.flexCenter; - @include deprecated.buttonStyle; + @include deprecated.flex-center; + @include deprecated.button-style; height: deprecated.$s-32; width: deprecated.$s-32; @@ -65,7 +65,7 @@ } .page-name { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; padding: 0 deprecated.$s-12; color: var(--title-foreground-color); @@ -88,7 +88,7 @@ } .focus-title { - @include deprecated.buttonStyle; + @include deprecated.button-style; display: grid; grid-template-columns: auto 1fr auto; @@ -98,7 +98,7 @@ } .back-button { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-32; width: deprecated.$s-24; @@ -113,22 +113,22 @@ } .focus-name { - @include deprecated.textEllipsis; - @include deprecated.bodySmallTypography; + @include deprecated.text-ellipsis; + @include deprecated.body-small-typography; padding-left: deprecated.$s-4; color: var(--title-foreground-color); } .focus-mode-tag-wrapper { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: 100%; margin-right: deprecated.$s-12; } .active-filters { - @include deprecated.flexRow; + @include deprecated.flex-row; flex-wrap: wrap; margin: 0 deprecated.$s-12; @@ -151,8 +151,8 @@ } .layer-filter-name { - @include deprecated.flexCenter; - @include deprecated.bodySmallTypography; + @include deprecated.flex-center; + @include deprecated.body-small-typography; color: var(--pill-foreground-color); } @@ -169,7 +169,7 @@ width: deprecated.$s-192; .filter-menu-item { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; align-items: center; @@ -285,7 +285,7 @@ } .scope-label { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--color-foreground-secondary); cursor: pointer; @@ -302,7 +302,7 @@ } .replace-input-wrapper { - @include deprecated.flexCenter; + @include deprecated.flex-center; flex: 1; height: deprecated.$s-32; @@ -346,8 +346,8 @@ } .replace-button { - @include deprecated.bodySmallTypography; - @include deprecated.buttonStyle; + @include deprecated.body-small-typography; + @include deprecated.button-style; flex: 1; height: deprecated.$s-28; @@ -378,14 +378,14 @@ } .match-count { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--color-foreground-secondary); white-space: nowrap; } .no-matches { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--color-foreground-secondary); white-space: nowrap; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss index 7a89a734af..1599bcad25 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss @@ -25,7 +25,7 @@ border-radius: deprecated.$br-8; .collapsed-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; cursor: pointer; @@ -49,7 +49,7 @@ } .select-name { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; justify-content: flex-start; @@ -83,7 +83,7 @@ } .check-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; svg { @extend %button-icon-small; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss index 7445581652..2d1c6263b7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss @@ -21,7 +21,7 @@ } .element-set-content { - @include deprecated.flexColumn; + @include deprecated.flex-column; margin-bottom: deprecated.$s-8; } @@ -61,7 +61,7 @@ } .label { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; flex-grow: 1; display: flex; @@ -82,21 +82,21 @@ } .actions { - @include deprecated.flexRow; + @include deprecated.flex-row; } &.hidden { .blur-info { - @include deprecated.hiddenElement; + @include deprecated.hidden-element; .show-more { - @include deprecated.hiddenElement; + @include deprecated.hidden-element; border: deprecated.$s-1 solid var(--input-border-color-disabled); } .label { - @include deprecated.hiddenElement; + @include deprecated.hidden-element; border: deprecated.$s-1 solid var(--input-border-color-disabled); } @@ -106,7 +106,7 @@ .second-row { @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; width: deprecated.$s-92; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss index 9d8c30d5c0..d138c82ac2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss @@ -34,18 +34,18 @@ .element-content { grid-column: span 8; - @include deprecated.flexColumn; + @include deprecated.flex-column; margin-bottom: deprecated.$s-8; } .selected-color-group { - @include deprecated.flexColumn; + @include deprecated.flex-column; } .more-colors-btn { @extend %button-secondary; - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; height: deprecated.$s-32; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss index 5aec6eef23..e0fbb84843 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss @@ -29,7 +29,7 @@ .constraints-center, .constraints-right, .constraints-bottom { - @include deprecated.flexCenter; + @include deprecated.flex-center; grid-area: top; } @@ -37,8 +37,8 @@ .constraint-btn, .constraint-btn-special, .constraint-btn-rotated { - @include deprecated.buttonStyle; - @include deprecated.flexCenter; + @include deprecated.button-style; + @include deprecated.flex-center; width: 100%; height: 100%; @@ -152,7 +152,7 @@ } label { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; align-items: center; @@ -161,7 +161,7 @@ color: var(--input-checkbox-text-foreground-color); .check-mark { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-16; height: deprecated.$s-16; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss index a1b4706f9a..f224403269 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss @@ -27,7 +27,7 @@ } .multiple-exports { - @include deprecated.flexRow; + @include deprecated.flex-row; grid-column: 1 / span 9; @@ -36,7 +36,7 @@ } .actions { - @include deprecated.flexRow; + @include deprecated.flex-row; } } @@ -78,12 +78,12 @@ grid-column: span 3; @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; } .export-btn { @extend %button-secondary; - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; grid-column: 1 / span 9; height: deprecated.$s-32; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss index 52171dde2e..75b108f768 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss @@ -21,7 +21,7 @@ } .element-set-content { - @include deprecated.flexColumn; + @include deprecated.flex-column; grid-column: span 8; margin: deprecated.$s-4 0 deprecated.$s-8 0; @@ -87,7 +87,7 @@ .numeric-input { @extend %input-base; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; } } @@ -108,14 +108,14 @@ .numeric-input { @extend %input-base; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; margin: 0; padding: 0; } span { - @include deprecated.flexCenter; + @include deprecated.flex-center; svg { @extend %button-icon; @@ -126,35 +126,35 @@ &.hidden { .show-options { - @include deprecated.hiddenElement; + @include deprecated.hidden-element; border: deprecated.$s-1 solid var(--input-border-color-disabled); } .type-select-wrapper, .editable-select-wrapper { - @include deprecated.hiddenElement; + @include deprecated.hidden-element; .column-select, .grid-type-select { - @include deprecated.hiddenElement; + @include deprecated.hidden-element; border: deprecated.$s-1 solid var(--input-border-color-disabled); } .column-select { - @include deprecated.hiddenElement; + @include deprecated.hidden-element; border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; .numeric-input { - @include deprecated.hiddenElement; + @include deprecated.hidden-element; } } } .grid-size { - @include deprecated.hiddenElement; + @include deprecated.hidden-element; border: deprecated.$s-1 solid var(--input-border-color-disabled); @@ -181,20 +181,20 @@ } .actions { - @include deprecated.flexRow; + @include deprecated.flex-row; grid-column: span 2; } .grid-advanced-options { - @include deprecated.flexColumn; + @include deprecated.flex-column; margin-top: deprecated.$s-4; } .column-row, .square-row { - @include deprecated.flexColumn; + @include deprecated.flex-column; position: relative; } @@ -230,7 +230,7 @@ .height { @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; .icon-text { padding-top: deprecated.$s-1; @@ -240,7 +240,7 @@ .gutter, .margin { @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; .icon { &.rotated svg { @@ -250,8 +250,8 @@ } .more-options { - @include deprecated.menuShadow; - @include deprecated.flexColumn; + @include deprecated.menu-shadow; + @include deprecated.flex-column; position: absolute; top: calc(deprecated.$s-2 + deprecated.$s-28); @@ -267,7 +267,7 @@ background-color: var(--menu-background-color); .option-btn { - @include deprecated.buttonStyle; + @include deprecated.button-style; display: flex; align-items: center; @@ -292,7 +292,7 @@ width: deprecated.$s-108; .btn-options { - @include deprecated.buttonStyle; + @include deprecated.button-style; @extend %dropdown-element-base; width: 100%; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss index c54da28b43..9e4aa4ca3b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss @@ -8,7 +8,7 @@ @use "../../../sidebar/common/sidebar.scss" as sidebar; .grid-cell-menu-container { - @include deprecated.flexColumn; + @include deprecated.flex-column; margin-top: deprecated.$s-8; gap: deprecated.$s-16; @@ -28,7 +28,7 @@ } .row { - @include deprecated.flexRow; + @include deprecated.flex-row; } .cell-mode :global(label) { @@ -37,7 +37,7 @@ .edit-grid-btn { @extend %button-secondary; - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; width: 100%; padding: deprecated.$s-8; @@ -45,14 +45,14 @@ .area-input { @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; width: 100%; padding: deprecated.$s-8; } .grid-coord-group { - @include deprecated.flexRow; + @include deprecated.flex-row; border-radius: deprecated.$br-8; padding-left: deprecated.$s-4; @@ -67,7 +67,7 @@ .coord-input { @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; border-left: deprecated.$s-1 solid var(--panel-background-color); diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss index e6266df974..e3605152d8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss @@ -36,7 +36,7 @@ border-radius: deprecated.$br-8; .collapsed-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; cursor: pointer; @@ -60,7 +60,7 @@ } .select-name { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; justify-content: flex-start; @@ -94,7 +94,7 @@ } .check-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; svg { @extend %button-icon-small; @@ -150,7 +150,7 @@ .y-position, .rotation { @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; .icon-text { padding-top: deprecated.$s-1; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss index ccbb34de36..d8fabe2295 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss @@ -16,7 +16,7 @@ } .element-set-content { - @include deprecated.flexColumn; + @include deprecated.flex-column; margin: deprecated.$s-4 0 0 0; } @@ -27,8 +27,8 @@ } .attr-name { - @include deprecated.bodySmallTypography; - @include deprecated.twoLineTextEllipsis; + @include deprecated.body-small-typography; + @include deprecated.two-line-text-ellipsis; width: deprecated.$s-88; margin: auto deprecated.$s-4; @@ -39,7 +39,7 @@ .attr-input { @extend %input-element; - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; width: deprecated.$s-124; } @@ -66,7 +66,7 @@ } .attr-title { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; font-size: deprecated.$fs-10; text-transform: uppercase; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss index 8805b83543..e589c4dd79 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss @@ -18,7 +18,7 @@ } .element-content { - @include deprecated.flexColumn; + @include deprecated.flex-column; grid-column: span 8; margin-top: deprecated.$s-4; @@ -29,7 +29,7 @@ } .multiple-text { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; flex-grow: 1; color: var(--input-foreground-color-active); diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss index 0e68e8ee26..8279a4970c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss @@ -28,7 +28,7 @@ } .not-found { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--empty-message-foreground-color); margin: deprecated.$s-12; @@ -45,7 +45,7 @@ .section-title, .subsection-title { - @include deprecated.uppercaseTitleTipography; + @include deprecated.uppercase-title-typography; display: flex; align-items: center; @@ -87,21 +87,21 @@ background-color: var(--pill-background-color); .command-name { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; margin-left: deprecated.$s-2; color: var(--pill-foreground-color); } .keys { - @include deprecated.flexCenter; + @include deprecated.flex-center; gap: deprecated.$s-2; color: var(--pill-foreground-color); .key { - @include deprecated.bodySmallTypography; - @include deprecated.flexCenter; + @include deprecated.body-small-typography; + @include deprecated.flex-center; text-transform: capitalize; height: deprecated.$s-20; diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss index 3a7a3233ac..dd0370c5ed 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss @@ -56,7 +56,7 @@ } .page-element { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; min-height: deprecated.$s-32; width: 100%; @@ -84,14 +84,14 @@ color: var(--layer-row-foreground-color); .page-name { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; flex-grow: 1; padding-left: deprecated.$s-2; } .page-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; height: deprecated.$s-32; width: deprecated.$s-24; @@ -112,8 +112,8 @@ align-items: center; button { - @include deprecated.buttonStyle; - @include deprecated.flexCenter; + @include deprecated.button-style; + @include deprecated.flex-center; width: deprecated.$s-24; height: 100%; @@ -130,15 +130,15 @@ } .element-name { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; color: var(--layer-row-foreground-color-focus); } input.element-name { - @include deprecated.textEllipsis; - @include deprecated.bodySmallTypography; - @include deprecated.removeInputStyle; + @include deprecated.text-ellipsis; + @include deprecated.body-small-typography; + @include deprecated.remove-input-style; flex-grow: 1; height: deprecated.$s-28; diff --git a/frontend/src/app/main/ui/workspace/text_palette.scss b/frontend/src/app/main/ui/workspace/text_palette.scss index f5947604e8..8090f88e7c 100644 --- a/frontend/src/app/main/ui/workspace/text_palette.scss +++ b/frontend/src/app/main/ui/workspace/text_palette.scss @@ -13,8 +13,8 @@ .left-arrow, .right-arrow { - @include deprecated.buttonStyle; - @include deprecated.flexCenter; + @include deprecated.button-style; + @include deprecated.flex-center; position: relative; height: 100%; @@ -89,7 +89,7 @@ } .typography-item { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; flex-direction: column; @@ -106,7 +106,7 @@ } .typography-name { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; height: deprecated.$s-16; width: deprecated.$s-120; @@ -114,7 +114,7 @@ } .typography-font { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; height: deprecated.$s-16; width: deprecated.$s-120; @@ -122,7 +122,7 @@ } .typography-data { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; height: deprecated.$s-16; width: deprecated.$s-120; @@ -152,7 +152,7 @@ } .text-palette-empty { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--palette-text-color); } diff --git a/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss b/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss index 83e01b672b..0a8a3985d7 100644 --- a/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss +++ b/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss @@ -33,7 +33,7 @@ } .library-name { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; color: var(--context-menu-foreground-color); display: grid; @@ -41,7 +41,7 @@ max-width: deprecated.$s-400; .lib-name { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; max-width: deprecated.$s-380; } @@ -54,10 +54,10 @@ .icon-wrapper { margin-left: deprecated.$s-4; - @include deprecated.flexCenter; + @include deprecated.flex-center; svg { - @include deprecated.flexCenter; + @include deprecated.flex-center; @extend %button-icon-small; stroke: var(--icon-foreground); @@ -67,10 +67,10 @@ &.selected, &:hover { .icon-wrapper { - @include deprecated.flexCenter; + @include deprecated.flex-center; svg { - @include deprecated.flexCenter; + @include deprecated.flex-center; @extend %button-icon-small; stroke: var(--context-menu-foreground-color-selected); diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.scss b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.scss index fdbdcbe98d..284040b470 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.scss @@ -22,7 +22,7 @@ .context-list, .token-context-submenu { - @include deprecated.menuShadow; + @include deprecated.menu-shadow; display: grid; width: deprecated.$s-240; diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.scss b/frontend/src/app/main/ui/workspace/tokens/sets.scss index 5209ff4bed..f7141b973b 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sets.scss @@ -30,7 +30,7 @@ } .set-item-container { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; align-items: center; @@ -68,7 +68,7 @@ } .set-name { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; flex-grow: 1; padding-left: deprecated.$s-2; @@ -142,8 +142,8 @@ } .collapsabled-icon { - @include deprecated.buttonStyle; - @include deprecated.flexCenter; + @include deprecated.button-style; + @include deprecated.flex-center; height: deprecated.$s-24; border-radius: deprecated.$br-8; @@ -154,9 +154,9 @@ } .editing-node { - @include deprecated.textEllipsis; - @include deprecated.bodySmallTypography; - @include deprecated.removeInputStyle; + @include deprecated.text-ellipsis; + @include deprecated.body-small-typography; + @include deprecated.remove-input-style; border: deprecated.$s-1 solid var(--input-border-color-focus); border-radius: deprecated.$br-8; diff --git a/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss b/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss index a09d7699b1..53e6e72901 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss @@ -13,7 +13,7 @@ } .context-list { - @include deprecated.menuShadow; + @include deprecated.menu-shadow; display: grid; width: deprecated.$s-240; diff --git a/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss b/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss index 5209ff4bed..f7141b973b 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss @@ -30,7 +30,7 @@ } .set-item-container { - @include deprecated.bodySmallTypography; + @include deprecated.body-small-typography; display: flex; align-items: center; @@ -68,7 +68,7 @@ } .set-name { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; flex-grow: 1; padding-left: deprecated.$s-2; @@ -142,8 +142,8 @@ } .collapsabled-icon { - @include deprecated.buttonStyle; - @include deprecated.flexCenter; + @include deprecated.button-style; + @include deprecated.flex-center; height: deprecated.$s-24; border-radius: deprecated.$br-8; @@ -154,9 +154,9 @@ } .editing-node { - @include deprecated.textEllipsis; - @include deprecated.bodySmallTypography; - @include deprecated.removeInputStyle; + @include deprecated.text-ellipsis; + @include deprecated.body-small-typography; + @include deprecated.remove-input-style; border: deprecated.$s-1 solid var(--input-border-color-focus); border-radius: deprecated.$br-8; diff --git a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss index c252590b18..1526c63b7a 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss +++ b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss @@ -126,7 +126,7 @@ .group-title-name { flex-grow: 1; - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; } .theme-group-rows-wrapper { @@ -152,7 +152,7 @@ } .theme-name-row { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; flex-grow: 1; } diff --git a/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.scss b/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.scss index 7d7171536e..cf9a2077a4 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.scss +++ b/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.scss @@ -43,7 +43,7 @@ } .group { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; display: block; padding: deprecated.$s-8; @@ -61,13 +61,13 @@ } .dropdown-button { - @include deprecated.flexCenter; + @include deprecated.flex-center; color: var(--color-foreground-secondary); } .current-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; width: deprecated.$s-24; padding-right: deprecated.$s-4; @@ -115,7 +115,7 @@ } .check-icon { - @include deprecated.flexCenter; + @include deprecated.flex-center; color: var(--icon-foreground-primary); visibility: hidden; @@ -131,7 +131,7 @@ } .current-label { - @include deprecated.textEllipsis; + @include deprecated.text-ellipsis; } .dropdown-portal { diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.scss b/frontend/src/app/main/ui/workspace/top_toolbar.scss index fe603de407..f513b7b048 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.scss +++ b/frontend/src/app/main/ui/workspace/top_toolbar.scss @@ -84,8 +84,8 @@ } .toolbar-handler { - @include deprecated.flexCenter; - @include deprecated.buttonStyle; + @include deprecated.flex-center; + @include deprecated.button-style; position: absolute; left: 0; diff --git a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss index a2f8f0b345..9009fe8d76 100644 --- a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss +++ b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss @@ -119,7 +119,7 @@ } .grid-actions-container { - @include deprecated.flexRow; + @include deprecated.flex-row; background: var(--panel-background-color); border-radius: deprecated.$br-12; diff --git a/frontend/src/app/main/ui/workspace/viewport/top_bar.scss b/frontend/src/app/main/ui/workspace/viewport/top_bar.scss index 484ae669d9..5ee297d756 100644 --- a/frontend/src/app/main/ui/workspace/viewport/top_bar.scss +++ b/frontend/src/app/main/ui/workspace/viewport/top_bar.scss @@ -24,7 +24,7 @@ } .viewport-actions-container { - @include deprecated.flexRow; + @include deprecated.flex-row; background: var(--panel-background-color); border-radius: deprecated.$br-12; From cd9151bf9fbefe8144e163ebf68074ed4e5b475c Mon Sep 17 00:00:00 2001 From: Xaviju Date: Tue, 21 Apr 2026 09:54:30 +0200 Subject: [PATCH 174/288] :bug: Fix duplicate modal title (#9064) --- .../workspace/tokens/management/forms/rename_node_modal.cljs | 4 +++- .../app/main/ui/workspace/tokens/management/token_tree.scss | 1 - frontend/translations/ca.po | 4 ++++ frontend/translations/en.po | 4 ++++ frontend/translations/es.po | 4 ++++ 5 files changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs index c786852441..194b731f03 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs @@ -55,7 +55,9 @@ [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)} - (tr "workspace.tokens.rename-group")] + (if (= variant "rename") + (tr "workspace.tokens.rename-group") + (tr "workspace.tokens.duplicate-group"))] [:> fc/form-input* {:id "rename-node" :name :name :label (tr "workspace.tokens.token-name") diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss index 0794172692..9edd7caf40 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss @@ -12,7 +12,6 @@ padding-block-end: var(--sp-s); display: flex; flex-wrap: wrap; - gap: var(--sp-s); padding-inline-start: calc(var(--node-spacing)); & .node-parent { diff --git a/frontend/translations/ca.po b/frontend/translations/ca.po index 59edb27cf1..2739db1bce 100644 --- a/frontend/translations/ca.po +++ b/frontend/translations/ca.po @@ -4288,6 +4288,10 @@ msgstr "Aquest procés pot trigar una mica" msgid "workspace.tokens.rename-group" msgstr "Canviar nom del grup de tokens" +#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs +msgid "workspace.tokens.duplicate-group" +msgstr "Duplicar grup de tokens" + #: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs msgid "workspace.tokens.rename-group-name-hint" msgstr "Els teus tokens es renomenaran automàticament a %s.(sufix).(token)" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e211bdd349..2b325e4e11 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -8782,6 +8782,10 @@ msgstr "Tokens can't be applied while editing text. Select the text layer instea msgid "workspace.tokens.rename-group" msgstr "Rename Tokens Group" +#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs +msgid "workspace.tokens.duplicate-group" +msgstr "Duplicate Tokens Group" + #: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs msgid "workspace.tokens.rename-group-name-hint" msgstr "Your tokens will automatically be renamed to %s.(suffix).(tokenName)" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 55fdd29246..6855d5fa84 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -8574,6 +8574,10 @@ msgstr "No se pueden aplicar tokens mientras se edita texto. Seleccione la capa msgid "workspace.tokens.rename-group" msgstr "Renombrar grupo de tokens" +#: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs +msgid "workspace.tokens.duplicate-group" +msgstr "Duplicar grupo de tokens" + #: src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs msgid "workspace.tokens.rename-group-name-hint" msgstr "Tus tokens serán automáticamente renombrados a %s.(sufijo).(token)" From 78c48f1953a477c8b09ac62c8e7555733b94bd80 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Tue, 21 Apr 2026 13:33:38 +0200 Subject: [PATCH 175/288] :bug: Fix broken update library notification link UI (#9070) * :bug: Fix broken update library notification link UI * :recycle: Format and lint --- .../src/app/main/ui/notifications/inline_notification.scss | 2 ++ .../app/main/ui/workspace/sidebar/options/menus/text.cljs | 4 ++-- .../ui/workspace/tokens/management/node_context_menu.cljs | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/ui/notifications/inline_notification.scss b/frontend/src/app/main/ui/notifications/inline_notification.scss index ee71bc5c3d..4679db4026 100644 --- a/frontend/src/app/main/ui/notifications/inline_notification.scss +++ b/frontend/src/app/main/ui/notifications/inline_notification.scss @@ -20,5 +20,7 @@ } .link { + @extend %link; + margin: 0; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 55f81f1600..35e37a9db2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -15,8 +15,8 @@ [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.shortcuts :as sc] [app.main.data.workspace.texts :as dwt] - [app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.texts-v3 :as dwt-v3] + [app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.wasm-text :as dwwt] [app.main.features :as features] @@ -509,4 +509,4 @@ :options (resolve-delay dropdown-options) :selected selected-token-id :align "right" - :ref set-option-ref}])])) \ No newline at end of file + :ref set-option-ref}])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs index 574376ed2b..6978243ec3 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs @@ -38,7 +38,7 @@ top (+ (get-in mdata [:position :y]) 5) left (+ (get-in mdata [:position :x]) 5) container (hooks/use-portal-container :popup) - + rename-node (mf/use-fn (mf/deps mdata on-rename-node) (fn [] @@ -46,7 +46,7 @@ type (get mdata :type)] (when node (on-rename-node node type))))) - + duplicate-node (mf/use-fn (mf/deps mdata on-duplicate-node) (fn [] @@ -76,7 +76,7 @@ (mf/set-ref-val! dropdown-direction-change* (inc (mf/ref-val dropdown-direction-change*))))))) ;; FIXME: perf optimization - + (when is-open? (mf/portal (mf/html From f18670ed004972cb84f1bece249d1faf9e278377 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 21 Apr 2026 14:39:06 +0200 Subject: [PATCH 176/288] :bug: Fix errors from numeric input design review (#8993) * :bug: Fix blur input after enter value * :bug: Catch error on invalid maths * :bug: Fix race condition * :tada: Add tests that cover issues * :bug: Fix padding applying only to one side * :bug: Fix show broken pill when reference is on not active set --- .../playwright/ui/specs/tokens/apply.spec.js | 421 +++++++++++++++++- .../main/ui/ds/controls/numeric_input.cljs | 99 ++-- .../ds/controls/shared/options_dropdown.cljs | 2 +- .../ui/ds/controls/shared/render_option.cljs | 1 + .../ui/ds/controls/shared/token_option.cljs | 12 +- .../ui/ds/controls/utilities/token_field.cljs | 18 +- .../options/menus/input_wrapper_tokens.cljs | 2 +- .../options/menus/layout_container.cljs | 24 +- .../sidebar/options/menus/layout_item.cljs | 5 +- .../management/forms/controls/utils.cljs | 3 +- frontend/src/app/util/simple_math.cljs | 11 +- .../frontend_tests/util_simple_math_test.cljs | 22 +- 12 files changed, 538 insertions(+), 82 deletions(-) diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js index fccad0cfe9..5b20d089d0 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -5,6 +5,7 @@ import { setupTokensFileRender, setupTypographyTokensFileRender, unfoldTokenType, + createToken, } from "./helpers"; test.beforeEach(async ({ page }) => { @@ -813,7 +814,7 @@ test.describe("Tokens: Apply token", () => { // Check if token pill is visible on right sidebar const layoutItemSectionSidebar = rightSidebar.getByRole("region", { - name: "layout item menu", + name: "Layout item section", }); await expect(layoutItemSectionSidebar).toBeVisible(); const marginPillMd = layoutItemSectionSidebar.getByRole("button", { @@ -931,8 +932,7 @@ test.describe("Tokens: Detach token", () => { test("Bug: 13959, User select shapes with different hidden state.", async ({ page, }) => { - const { workspacePage } = - await setupTokensFileRender(page); + const { workspacePage } = await setupTokensFileRender(page); await page.getByRole("tab", { name: "Layers" }).click(); @@ -955,8 +955,7 @@ test("Bug: 13959, User select shapes with different hidden state.", async ({ test("Bug: 13960, User select shapes with different opacity and input show mixed state.", async ({ page, }) => { - const { workspacePage } = - await setupTokensFileRender(page); + const { workspacePage } = await setupTokensFileRender(page); await page.getByRole("tab", { name: "Layers" }).click(); @@ -965,21 +964,22 @@ test("Bug: 13960, User select shapes with different opacity and input show mixed name: "Layer menu section", }); await expect(layerMenuSection).toBeVisible(); - await layerMenuSection - .getByRole('textbox', { name: 'Opacity' }) - .fill('50'); + await layerMenuSection.getByRole("textbox", { name: "Opacity" }).fill("50"); await expect(layerMenuSection).toBeVisible(); await workspacePage.layers .getByTestId("layer-row") .nth(0) .click({ modifiers: ["Shift"] }); - await expect(layerMenuSection - .getByRole('textbox', { name: 'Opacity' })).toBeVisible(); - await expect(layerMenuSection - .getByRole('textbox', { name: 'Opacity' })).toBeVisible(); + await expect( + layerMenuSection.getByRole("textbox", { name: "Opacity" }), + ).toBeVisible(); + await expect( + layerMenuSection.getByRole("textbox", { name: "Opacity" }), + ).toBeVisible(); - await expect(layerMenuSection - .getByRole('textbox', { name: 'Opacity' })).toHaveAttribute("placeholder", "Mixed"); + await expect( + layerMenuSection.getByRole("textbox", { name: "Opacity" }), + ).toHaveAttribute("placeholder", "Mixed"); }); test("BUG: 13930, Token colors are shown on selected colors section", async ({ @@ -1029,3 +1029,396 @@ test("BUG: 13930, Token colors are shown on selected colors section", async ({ .getByRole("button", { name: "colors.black" }), ).toBeVisible(); }); + +test.describe("Numeric Input and Token Integration Tests", () => { + test("Token pill persists after blur in gap inputs", async ({ page }) => { + // Setup the workspace with token features enabled + const { workspacePage, tokensSidebar, tokenContextMenuForToken } = + await setupTokensFileRender(page, { + flags: ["enable-token-combobox", "enable-feature-token-input"], + }); + + // Transform a rectangle into a flex container to expose gap properties + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers.getByTestId("layer-row").nth(1).click(); + + const layoutSection = + workspacePage.rightSidebar.getByTestId("inspect-layout"); + + const addLayoutButton = layoutSection + .getByRole("button", { name: "Add layout" }) + .first(); + await addLayoutButton.click(); + await page.getByText("Flex layout").click(); + + // Apply a spacing token to the Column gap property + const tokensTabButton = page.getByRole("tab", { name: "Tokens" }); + await tokensTabButton.click(); + await unfoldTokenType(tokensSidebar, "spacing"); + + await tokensSidebar + .getByRole("button", { name: "spacing.lg" }) + .click({ button: "right" }); + + await tokenContextMenuForToken.getByText("Column gap").click(); + + // Verify that the token pill appears in the layout section, check after blur + await expect( + page + .getByTestId("inspect-layout") + .getByRole("button", { name: "spacing.lg" }), + ).toBeVisible(); + + await page + .getByTestId("inspect-layout") + .getByRole("textbox", { name: "Vertical padding" }) + .click(); + + await expect( + page + .getByTestId("inspect-layout") + .getByRole("button", { name: "spacing.lg" }), + ).toBeVisible(); + }); + + test("Padding tokens are applied to both vertical or horizontal properties", async ({ + page, + }) => { + // Setup the workspace with token features enabled + const { workspacePage, tokensSidebar, tokenContextMenuForToken } = + await setupTokensFileRender(page, { + flags: ["enable-token-combobox", "enable-feature-token-input"], + }); + + // Transform a rectangle into a flex container to expose gap properties + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers.getByTestId("layer-row").nth(1).click(); + + const layoutSection = + workspacePage.rightSidebar.getByTestId("inspect-layout"); + + const addLayoutButton = layoutSection + .getByRole("button", { name: "Add layout" }) + .first(); + await addLayoutButton.click(); + await page.getByText("Flex layout").click(); + + // Apply a spacing token to the Column gap property + const tokensTabButton = page.getByRole("tab", { name: "Tokens" }); + await tokensTabButton.click(); + await unfoldTokenType(tokensSidebar, "spacing"); + + await tokensSidebar + .getByRole("button", { name: "spacing.lg" }) + .click({ button: "right" }); + + await tokenContextMenuForToken.getByText("Horizontal").click(); + + // Verify that the token pill appears in the layout section, check after blur + await expect( + page + .getByTestId("inspect-layout") + .getByRole("button", { name: "spacing.lg" }), + ).toBeVisible(); + + await layoutSection + .getByRole("button", { name: "Show 4 sided padding options" }) + .click(); + + await expect( + page + .getByTestId("inspect-layout") + .getByRole("button", { name: "spacing.lg" }), + ).toHaveCount(2); + + await layoutSection + .getByRole("button", { name: "Show 4 sided padding options" }) + .click(); + + await expect( + page + .getByTestId("inspect-layout") + .getByRole("button", { name: "spacing.lg" }), + ).toBeVisible(); + }); + + test("Token pill persists after blur in min/max width inputs", async ({ + page, + }) => { + // Setup the workspace with token features enabled + const { workspacePage } = await setupTokensFileRender(page, { + flags: ["enable-token-combobox", "enable-feature-token-input"], + }); + + // Create a flex container to expose min/max width properties + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers.getByTestId("layer-row").nth(2).click(); + + const layoutSection = + workspacePage.rightSidebar.getByTestId("inspect-layout"); + + const addLayoutButton = layoutSection + .getByRole("button", { name: "Add layout" }) + .first(); + await addLayoutButton.click(); + await page.getByText("Flex layout").click(); + + // Verify that the flex container (Flex board) is created + await expect( + page.getByRole("button", { name: "Flex board" }), + ).toBeVisible(); + + // Select element inside flex container to access to layout constrains inputs + // Apply token to min width property + await workspacePage.layers + .getByTestId("layer-row") + .nth(2) + .getByTestId("toggle-content") + .click(); + + await workspacePage.layers.getByTestId("layer-row").nth(3).click(); + + const layoutItemSection = page.getByRole("region", { + name: "Layout item section", + }); + + await layoutItemSection.getByTestId("behaviour-h-fill").click(); + + const constraintsSection = layoutItemSection.getByRole("region", { + name: "layout item size constraints", + }); + await expect(constraintsSection).toBeVisible(); + + await constraintsSection + .getByRole("button", { name: "Open token list" }) + .nth(0) + .click(); + + await expect( + page.getByRole("option", { name: "dimension.md" }), + ).toBeVisible(); + await page.getByRole("option", { name: "dimension.md" }).click(); + + await expect( + constraintsSection.getByRole("button", { name: "dimension.md" }), + ).toBeVisible(); + + // Focus another input (Max width) to trigger blur and check if token pill persists + await constraintsSection + .getByRole("textbox", { name: "Max width" }) + .click(); + + await expect( + constraintsSection.getByRole("button", { name: "dimension.md" }), + ).toBeVisible(); + }); + + test("Invalid formula reverts to previous value in padding inputs", async ({ + page, + }) => { + const { workspacePage, tokensSidebar, tokenContextMenuForToken } = + await setupTokensFileRender(page, { + flags: ["enable-token-combobox", "enable-feature-token-input"], + }); + + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers.getByTestId("layer-row").nth(1).click(); + + const layoutSection = + workspacePage.rightSidebar.getByTestId("inspect-layout"); + + const addLayoutButton = layoutSection + .getByRole("button", { name: "Add layout" }) + .first(); + + await addLayoutButton.click(); + + await page.getByText("Flex layout").click(); + + // Apply a spacing token to the Column gap property + const tokensTabButton = page.getByRole("tab", { name: "Tokens" }); + await tokensTabButton.click(); + await unfoldTokenType(tokensSidebar, "spacing"); + + await tokensSidebar + .getByRole("button", { name: "spacing.lg" }) + .click({ button: "right" }); + + await tokenContextMenuForToken.getByText("Column gap").click(); + + const verticalPaddingInput = layoutSection.getByRole("textbox", { + name: "Vertical padding", + }); + + // Enter a valid value first + await verticalPaddingInput.fill("23"); + await verticalPaddingInput.press("Enter"); + // Wait for potential error handling + await page.waitForTimeout(500); + + expect(await verticalPaddingInput.inputValue()).toMatch("23"); + + // Enter invalid expression + await verticalPaddingInput.fill("abc+1"); + await verticalPaddingInput.press("Enter"); + + // Wait for potential error handling + await page.waitForTimeout(500); + + // Value should revert to previous valid value + expect(await verticalPaddingInput.inputValue()).toMatch("23"); + + // Should NOT contain invalid characters + expect(await verticalPaddingInput.inputValue()).not.toContain("abc"); + }); + + test("Division by zero reverts to previous value", async ({ page }) => { + const { workspacePage, tokensSidebar, tokenContextMenuForToken } = + await setupTokensFileRender(page, { + flags: ["enable-token-combobox", "enable-feature-token-input"], + }); + + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers.getByTestId("layer-row").nth(1).click(); + + const layoutSection = + workspacePage.rightSidebar.getByTestId("inspect-layout"); + + const addLayoutButton = layoutSection + .getByRole("button", { name: "Add layout" }) + .first(); + + await addLayoutButton.click(); + + await page.getByText("Flex layout").click(); + + // Apply a spacing token to the Column gap property + const tokensTabButton = page.getByRole("tab", { name: "Tokens" }); + await tokensTabButton.click(); + await unfoldTokenType(tokensSidebar, "spacing"); + + await tokensSidebar + .getByRole("button", { name: "spacing.lg" }) + .click({ button: "right" }); + + await tokenContextMenuForToken.getByText("Column gap").click(); + + const verticalPaddingInput = layoutSection.getByRole("textbox", { + name: "Vertical padding", + }); + + // Enter a valid value first + await verticalPaddingInput.fill("23"); + await verticalPaddingInput.press("Enter"); + // Wait for potential error handling + await page.waitForTimeout(500); + + expect(await verticalPaddingInput.inputValue()).toMatch("23"); + + // Enter invalid expression + await verticalPaddingInput.fill("10/0"); + await verticalPaddingInput.press("Enter"); + + // Wait for potential error handling + await page.waitForTimeout(500); + + // Value should revert to previous valid value + expect(await verticalPaddingInput.inputValue()).toMatch("23"); + + // Should NOT contain invalid characters + expect(await verticalPaddingInput.inputValue()).not.toContain("10/0"); + + // Value should revert + expect(await verticalPaddingInput.inputValue()).toMatch(/^(\d+|--)$/); + expect(await verticalPaddingInput.inputValue()).not.toBe("Infinity"); + }); + + test("Negative expression result handled correctly", async ({ page }) => { + const { workspacePage, tokensSidebar, tokenContextMenuForToken } = + await setupTokensFileRender(page, { + flags: ["enable-token-combobox", "enable-feature-token-input"], + }); + + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers.getByTestId("layer-row").nth(1).click(); + const widthInput = workspacePage.rightSidebar.getByRole("textbox", { + name: "Width", + }); + await expect(widthInput).toBeVisible(); + + // Enter a valid value first + await widthInput.fill("23"); + await widthInput.press("Enter"); + + // Wait for potential error handling + await page.waitForTimeout(500); + expect(await widthInput.inputValue()).toMatch("23"); + + // Enter a negative expression + await widthInput.fill("10-50"); + await widthInput.press("Enter"); + + // Wait for potential error handling + await page.waitForTimeout(500); + + expect(await widthInput.inputValue()).toMatch("0.01"); + + // Should NOT negative values + expect(await widthInput.inputValue()).not.toContain("-40"); + }); + + test("Token pill show broken reference when set is not activated", async ({ + page, + }) => { + // Setup the workspace with token features enabled + const { + workspacePage, + tokensSidebar, + tokenContextMenuForToken, + tokenThemesSetsSidebar, + } = await setupTokensFileRender(page, { + flags: ["enable-token-combobox", "enable-feature-token-input"], + }); + // Create a token with a reference value in other set. + await createToken(page, "Dimensions", "reference-token", "Value", "{card.padding}"); + + + // Apply this token to a shape + await page.getByRole("tab", { name: "Layers" }).click(); + + await workspacePage.layers.getByTestId("layer-row").nth(1).click(); + + const tokensTabButton = page.getByRole("tab", { name: "Tokens" }); + await tokensTabButton.click(); + await unfoldTokenType(tokensSidebar, "dimensions"); + + await tokensSidebar + .getByRole("button", { name: "reference-token" }) + .click({ button: "right" }); + + await tokenContextMenuForToken.getByText("X", { exact: true }).click(); + + //Check if token is applied and visible on right sidebar + const measuresSection = page.getByRole("region", { + name: "shape-measures-section", + }); + await expect(measuresSection).toBeVisible(); + + await expect(measuresSection.getByRole('button', { name: 'reference-token' })).toBeVisible(); + + // Deactivate token set where reference token exist to make token broken + await tokenThemesSetsSidebar.getByRole('button', { name: 'theme' }).getByRole('checkbox').click(); + + // Check if token pill show broken reference state + const brokenPill = measuresSection.getByRole("button", { + name: "is not in any active set", + }); + await expect(brokenPill).toHaveCount(2); + }); +}); diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs index 1f624bdb7f..54da21ed03 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs @@ -109,6 +109,12 @@ j))) indices))) +(defn- find-token-by-name + [data name] + (some (fn [tokens-data] + (some #(when (= (:name %) name) %) tokens-data)) + (vals data))) + (def ^:private schema:icon [:and :string [:fn #(contains? icon-list %)]]) @@ -153,7 +159,7 @@ icon disabled inner-class min max max-length step is-selected-on-focus nillable - tokens applied-token empty-to-end + tokens applied-token-name empty-to-end on-change on-change-start on-change-end on-blur on-focus on-detach property align ref name @@ -166,9 +172,16 @@ tokens (if (object? tokens) (mfu/bean tokens) tokens) - value (if (= :multiple applied-token) + + value (if (= :multiple applied-token-name) :multiple value) + + token-applied (mf/with-memo [tokens applied-token-name] + (find-token-by-name tokens applied-token-name)) + + token-has-errors? (-> token-applied :errors seq boolean) + is-multiple? (= :multiple value) value (cond is-multiple? nil @@ -203,8 +216,8 @@ is-open* (mf/use-state false) is-open (deref is-open*) - token-applied* (mf/use-state applied-token) - token-applied (deref token-applied*) + token-applied-name* (mf/use-state applied-token-name) + token-applied-name (deref token-applied-name*) focused-id* (mf/use-state nil) focused-id (deref focused-id*) @@ -215,6 +228,10 @@ raw-value* (mf/use-ref nil) last-value* (mf/use-ref nil) + ;; Flag to prevent effect from overwriting token during selection + ;; This prevents race condition between blur and token selection + token-selection-in-progress* (mf/use-ref false) + ;; Refs wrapper-ref (mf/use-ref nil) nodes-ref (mf/use-ref nil) @@ -237,8 +254,8 @@ selected-id* (mf/use-state (fn [] - (if applied-token - (:id (get-option-by-name dropdown-options applied-token)) + (if applied-token-name + (:id (get-option-by-name dropdown-options applied-token-name)) nil))) selected-id (deref selected-id*) @@ -272,7 +289,7 @@ (if-let [parsed (parse-value raw-value (mf/ref-val last-value*) min max nillable)] (when-not (= parsed (mf/ref-val last-value*)) (mf/set-ref-val! last-value* parsed) - (reset! token-applied* nil) + (reset! token-applied-name* nil) (when (fn? on-change) (on-change parsed)) @@ -283,7 +300,7 @@ (do (mf/set-ref-val! last-value* nil) (mf/set-ref-val! raw-value* "") - (reset! token-applied* nil) + (reset! token-applied-name* nil) (update-input "") (when (fn? on-change) (on-change nil))) @@ -291,7 +308,7 @@ (let [fallback-value (or (mf/ref-val last-value*) default)] (mf/set-ref-val! raw-value* fallback-value) (mf/set-ref-val! last-value* fallback-value) - (reset! token-applied* nil) + (reset! token-applied-name* nil) (update-input (fmt/format-number fallback-value)) (when (and (fn? on-change) (not= fallback-value (str value))) @@ -318,13 +335,15 @@ (mf/use-fn (mf/deps apply-token) (fn [id value name] + (mf/set-ref-val! token-selection-in-progress* true) (reset! selected-id* id) (reset! focused-id* nil) (reset! is-open* false) - (reset! token-applied* name) + (reset! token-applied-name* name) (apply-token value name) (ts/schedule-on-idle (fn [] + (mf/set-ref-val! token-selection-in-progress* false) (when token-wrapper-ref (dom/focus! (mf/ref-val token-wrapper-ref))))))) @@ -354,7 +373,7 @@ (on-token-apply focused-id value name) (reset! filter-id* "")))) - on-blur + handle-blur (mf/use-fn (mf/deps apply-value on-blur) (fn [event] @@ -369,7 +388,8 @@ (when (mf/ref-val dirty-ref) (apply-value (mf/ref-val raw-value*))) (when (fn? on-blur) - (on-blur event)))) + (on-blur event)) + (dom/blur! (mf/ref-val ref)))) on-key-down (mf/use-fn @@ -409,8 +429,9 @@ value (get option :resolved-value) name (get option :name)] (on-token-apply option-id value name) - (reset! filter-id* "")))) - (on-blur event)) + (reset! filter-id* "") + (handle-blur event)))) + (handle-blur event)) esc? (do @@ -485,7 +506,7 @@ (when-not (or disabled is-open is-multiple?) (let [node (mf/ref-val ref) is-focused (and (some? node) (dom/active? node)) - has-token (some? (deref token-applied*))] + has-token (some? (deref token-applied-name*))] (when-not (or is-focused has-token) (let [client-x (.-clientX event) parsed (parse-value (mf/ref-val raw-value*) (mf/ref-val last-value*) min max nillable) @@ -569,16 +590,16 @@ detach-token (mf/use-fn - (mf/deps on-detach tokens disabled token-applied) + (mf/deps on-detach tokens disabled token-applied-name) (fn [event] (when-not disabled (dom/prevent-default event) (dom/stop-propagation event) - (reset! token-applied* nil) + (reset! token-applied-name* nil) (reset! selected-id* nil) (reset! focused-id* nil) (when on-detach - (on-detach token-applied)) + (on-detach token-applied-name)) (ts/schedule-on-idle (fn [] (dom/focus! (mf/ref-val ref))))))) @@ -640,7 +661,7 @@ (tr "labels.mixed-values") placeholder) :default-value (or (mf/ref-val last-value*) (fmt/format-number value)) - :on-blur on-blur + :on-blur handle-blur :on-key-down on-key-down :on-focus on-focus :on-change store-raw-value @@ -665,10 +686,10 @@ :max-length max-length}) token-props - (when (and token-applied (not= :multiple token-applied)) - (let [token (get-option-by-name dropdown-options token-applied) + (when (and token-applied-name (not= :multiple token-applied-name)) + (let [token (get-option-by-name dropdown-options token-applied-name) id (get token :id) - label (or (get token :name) applied-token) + label (or (get token :name) applied-token-name) token-value (or (get token :resolved-value) (or (mf/ref-val last-value*) (fmt/format-number value))) @@ -683,7 +704,8 @@ :on-focus on-focus :on-token-key-down on-token-key-down :disabled disabled - :on-blur on-blur + :on-blur handle-blur + :token-has-errors token-has-errors? :class inner-class :property property :is-open is-open @@ -704,7 +726,7 @@ :token-detach-btn-ref token-detach-btn-ref :detach-token detach-token})))] - (mf/with-effect [value default applied-token] + (mf/with-effect [value default applied-token-name] (let [value' (cond is-multiple? "" @@ -714,22 +736,27 @@ :else (fmt/format-number (d/parse-double value default)))] - (mf/set-ref-val! raw-value* value') (mf/set-ref-val! last-value* value') - (reset! token-applied* applied-token) - (if applied-token - (let [token-id (:id (get-option-by-name dropdown-options applied-token))] - (reset! selected-id* token-id)) - (reset! selected-id* nil)) + + ;; Only sync token state if not in the middle of a selection + ;; This prevents race condition between blur and token selection + (when-not (mf/ref-val token-selection-in-progress*) + (reset! token-applied-name* applied-token-name) + (if applied-token-name + (let [token-id (:id (get-option-by-name dropdown-options applied-token-name))] + (reset! selected-id* token-id)) + (reset! selected-id* nil))) (when-let [node (mf/ref-val ref)] (dom/set-value! node value')))) - (mf/with-effect [applied-token] - (when (nil? applied-token) - (reset! token-applied* nil) - (reset! selected-id* nil))) + (mf/with-effect [applied-token-name] + (when (nil? applied-token-name) + ;; Only clear if not in the middle of a selection + (when-not (mf/ref-val token-selection-in-progress*) + (reset! token-applied-name* nil) + (reset! selected-id* nil)))) (mf/with-layout-effect [on-mouse-wheel] (when-let [node (mf/ref-val ref)] @@ -746,8 +773,8 @@ :on-pointer-up on-scrub-pointer-up :on-lost-pointer-capture on-scrub-lost-pointer-capture} - (if (and (some? token-applied) - (not= :multiple token-applied)) + (if (and (some? token-applied-name) + (not= :multiple token-applied-name)) [:> token-field* token-props] [:> input-field* input-props]) diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs index e868e4ab2d..3ed3d6b3a3 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs @@ -32,7 +32,7 @@ [:map [:id {:optional true} :string] [:resolved-value {:optional true} - [:or :int :string :float]] + [:maybe [:or :int :string :float]]] [:name {:optional true} :string] [:value {:optional true} :keyword] [:icon {:optional true} schema:icon-list] diff --git a/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs b/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs index 8237f63f81..ef015467be 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs @@ -48,6 +48,7 @@ :id id :name name :resolved (get option :resolved-value) + :value (get option :value) :ref ref :role "option" :focused (= id focused) diff --git a/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs b/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs index 45189a3156..2d989bca02 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs @@ -18,7 +18,8 @@ [:map [:id {:optiona true} :string] [:ref some?] - [:resolved {:optional true} [:or :int :string :float :map]] + [:resolved {:optional true} [:maybe [:or :int :string :float :map]]] + [:value {:optional true} [:maybe [:or :int :string :float :map]]] [:name {:optional true} :string] [:on-click {:optional true} fn?] [:selected {:optional true} :boolean] @@ -26,7 +27,7 @@ (mf/defc token-option* {::mf/schema schema:token-option} - [{:keys [id name on-click selected ref focused resolved] :rest props}] + [{:keys [id name on-click selected ref focused resolved value] :rest props}] (let [internal-id (mf/use-id) id (d/nilv id internal-id) element-ref (mf/use-ref nil)] @@ -61,5 +62,8 @@ :ref element-ref} name]] (when (and resolved (not (map? resolved))) - [:> :span {:class (stl/css :option-pill)} - resolved])])) + [:span {:class (stl/css :option-pill)} + resolved]) + (when (and (nil? resolved) value) + [:span {:class (stl/css :option-pill)} + "--"])])) diff --git a/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs b/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs index 04c5900961..86146fe36b 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs +++ b/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs @@ -39,11 +39,15 @@ {::mf/schema schema:token-field} [{:keys [id label value slot-start disabled class on-click on-token-key-down on-blur detach-token tooltip-placement - token-wrapper-ref token-detach-btn-ref on-focus property is-open]}] + token-wrapper-ref token-detach-btn-ref on-focus property is-open + token-has-errors]}] (let [set-active? (some? id) - content (if set-active? - label - (tr "ds.inputs.token-field.no-active-token-option" label)) + + content (cond + token-has-errors (tr "workspace.tokens.ref-not-valid") + (not set-active?) (tr "ds.inputs.token-field.no-active-token-option" label) + :else label) + default-id (mf/use-id) id (d/nilv id default-id) pill-ref (mf/use-ref nil) @@ -80,13 +84,15 @@ [:button {:on-click on-click :ref pill-ref :class (stl/css-case :pill true - :no-set-pill (not set-active?) + :no-set-pill (or (not set-active?) + token-has-errors) :pill-disabled disabled) :disabled disabled :aria-labelledby (dm/str id "-pill") :on-key-down on-token-key-down} value - (when-not set-active? + (when (or (not set-active?) + token-has-errors) [:div {:class (stl/css :pill-dot)}])]]] (when-not ^boolean disabled diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.cljs index 099a38b5c9..5ca03e3d29 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.cljs @@ -25,7 +25,7 @@ (tr "settings.multiple") "--")) :class [class (stl/css :numeric-input-wrapper)] - :applied-token applied-token + :applied-token-name applied-token :tokens (if (delay? tokens) @tokens tokens) :align align :on-detach on-detach-attr diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs index 0d16dd4664..dd992ce08b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs @@ -337,7 +337,7 @@ (mf/deps on-change ids) (fn [value attr event] (let [on-change-fn #(on-change :simple attr % event)] - (soc/emit-value-or-token value on-change-fn ids #{attr})))) + (soc/emit-value-or-token value on-change-fn ids attr)))) on-detach-token (mf/use-fn @@ -364,10 +364,10 @@ (mf/use-fn (mf/deps on-focus) #(on-focus :p2)) on-p1-change - (mf/use-fn (mf/deps on-change') #(on-change' % :p1)) + (mf/use-fn (mf/deps on-change') #(on-change' % #{:p1 :p3})) on-p2-change - (mf/use-fn (mf/deps on-change') #(on-change' % :p2))] + (mf/use-fn (mf/deps on-change') #(on-change' % #{:p2 :p4}))] [:div {:class (stl/css :paddings-simple)} (if token-numeric-inputs @@ -460,12 +460,8 @@ (mf/use-fn (mf/deps on-change ids) (fn [value attr event] - (if (or (string? value) (number? value)) - (on-change :multiple attr value event) - (do - (st/emit! (dwta/apply-token-from-input {:token (first value) - :attrs #{attr} - :shape-ids ids})))))) + (let [on-change-fn #(on-change :multiple attr % event)] + (soc/emit-value-or-token value on-change-fn ids #{attr})))) on-focus (mf/use-fn @@ -642,7 +638,7 @@ :value p4}]])])) (mf/defc padding-section* - [{:keys [type on-type-change on-change] :as props}] + [{:keys [type on-type-change] :as props}] (let [on-type-change' (mf/use-fn (mf/deps on-type-change) @@ -650,9 +646,7 @@ (let [type (-> (dom/get-current-target event) (dom/get-data "type")) type (if (= type "multiple") :simple :multiple)] - (on-type-change type)))) - - props (mf/spread-object props {:on-change on-change})] + (on-type-change type))))] (mf/with-effect [] ;; on destroy component @@ -1182,10 +1176,10 @@ (fn [type prop val] (let [val (mth/finite val 0)] (cond - (and (= type :simple) (= prop :p1)) + (and (= type :simple) (or (= prop :p1) (= prop #{:p1 :p3}))) (st/emit! (dwsl/update-layout ids {:layout-padding {:p1 val :p3 val}})) - (and (= type :simple) (= prop :p2)) + (and (= type :simple) (or (= prop :p2) (= prop #{:p2 :p4}))) (st/emit! (dwsl/update-layout ids {:layout-padding {:p2 val :p4 val}})) (some? prop) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs index c30905a99f..769d3a182c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs @@ -586,7 +586,8 @@ on-layout-item-max-h-change (mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-max-h))] - [:div {:class (stl/css :advanced-options)} + [:section {:class (stl/css :advanced-options) + :aria-label "Layout item size constraints"} (when (= (:layout-item-h-sizing values) :fill) [:div {:class (stl/css :horizontal-fill)} (if token-numeric-inputs @@ -834,7 +835,7 @@ (st/emit! (dwsl/update-layout-child ids {:layout-item-z-index value}))))] [:section {:class (stl/css :element-set) - :aria-label "layout item menu"} + :aria-label "Layout item section"} [:div {:class (stl/css :element-title)} [:> title-bar* {:collapsable has-content? :collapsed (not open?) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs index 99c08fcf1d..39a4675dc0 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs @@ -9,7 +9,8 @@ [token] {:id (str (get token :id)) :type :token - :resolved-value (or (get token :resolved-value) (get token :value)) + :value (get token :value) + :resolved-value (get token :resolved-value) :name (get token :name)}) (defn- generate-dropdown-options diff --git a/frontend/src/app/util/simple_math.cljs b/frontend/src/app/util/simple_math.cljs index 7b203fdd12..ca6f05536a 100644 --- a/frontend/src/app/util/simple_math.cljs +++ b/frontend/src/app/util/simple_math.cljs @@ -90,7 +90,16 @@ init-value (or init-value 0)] (s/assert number? init-value) (if-not (insta/failure? result) - (interpret result init-value) + (try + (let [value (interpret result init-value)] + ;; Check for division by zero (Infinity or -Infinity) + (if (or (js/Number.isFinite value) (nil? value)) + value + nil)) + (catch :default err + (js/console.debug (str "Expression evaluation error: " (ex-message err)) + (str "Expression: '" expr "'")) + nil)) (let [text (:text result) index (:index result) expecting (->> result diff --git a/frontend/test/frontend_tests/util_simple_math_test.cljs b/frontend/test/frontend_tests/util_simple_math_test.cljs index 15bb2198c2..eeae0ed40f 100644 --- a/frontend/test/frontend_tests/util_simple_math_test.cljs +++ b/frontend/test/frontend_tests/util_simple_math_test.cljs @@ -8,7 +8,6 @@ (:require [app.common.math :as cm] [app.util.simple-math :as sm] - [cljs.pprint :refer [pprint]] [cljs.test :as t :include-macros true])) (t/deftest test-parser-inst @@ -88,3 +87,24 @@ result2 (sm/expr-eval "(20,333 + 10%) * (1 / 3)" 20)] (t/is (cm/close? result1 result2 7.44433333))))) +(t/deftest test-error-handling + (t/testing "Division by zero should return nil" + (let [result (sm/expr-eval "10/0" 999)] + (t/is (= result nil)))) + + (t/testing "Expression with division by zero should return nil" + (let [result (sm/expr-eval "(10 + 5) / 0" 999)] + (t/is (= result nil)))) + + (t/testing "Invalid syntax should return nil" + (let [result (sm/expr-eval "asdasd+2" 999)] + (t/is (= result nil)))) + + (t/testing "Empty expression with no init-value should return nil" + (let [result (sm/expr-eval "" nil)] + (t/is (= result nil)))) + + (t/testing "Partial invalid expression should return nil" + (let [result (sm/expr-eval "10 + abc" 100)] + (t/is (= result nil))))) + From 66e34950b2c590137023be1a10fcdc244e05ac00 Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Tue, 21 Apr 2026 15:39:35 +0200 Subject: [PATCH 177/288] :wrench: Add main-staging workflow --- .github/workflows/build-main-staging.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/build-main-staging.yml diff --git a/.github/workflows/build-main-staging.yml b/.github/workflows/build-main-staging.yml new file mode 100644 index 0000000000..33ee46947c --- /dev/null +++ b/.github/workflows/build-main-staging.yml @@ -0,0 +1,22 @@ +name: _MAIN-STAGING + +on: + workflow_dispatch: + schedule: + - cron: '26 5-20 * * 1-5' + +jobs: + build-bundle: + uses: ./.github/workflows/build-bundle.yml + secrets: inherit + with: + gh_ref: "main-staging" + build_wasm: "yes" + build_storybook: "yes" + + build-docker: + needs: build-bundle + uses: ./.github/workflows/build-docker.yml + secrets: inherit + with: + gh_ref: "main-staging" From cd320c0cd67d34d3d7d97ecda782a6b99f4b9007 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Tue, 21 Apr 2026 14:00:40 +0200 Subject: [PATCH 178/288] :sparkles: On profile deletion, remove the user from nitrate too --- backend/src/app/nitrate.clj | 39 +++++++++++++++++------- backend/src/app/rpc/commands/nitrate.clj | 9 +++++- backend/src/app/rpc/commands/profile.clj | 3 ++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 9aaff500a3..226b5d3bef 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -1,3 +1,9 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + (ns app.nitrate "Module that make calls to the external nitrate aplication" (:require @@ -286,6 +292,16 @@ "/remove-user") nil params))) +(defn- remove-profile-from-all-orgs-api + [cfg {:keys [profile-id] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri)] + (request-to-nitrate cfg :post + (str baseuri + "/api/users/" + profile-id + "/remove-organizations") + nil params))) + (defn- remove-team-from-org-api [cfg {:keys [team-id organization-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri) @@ -330,17 +346,18 @@ (defmethod ig/init-key ::client [_ cfg] (when (contains? cf/flags :nitrate) - {:get-team-org (partial get-team-org-api cfg) - :set-team-org (partial set-team-org-api cfg) - :get-org-membership (partial get-org-membership-api cfg) - :get-org-membership-by-team (partial get-org-membership-by-team-api cfg) - :get-org-summary (partial get-org-summary-api cfg) - :add-profile-to-org (partial add-profile-to-org-api cfg) - :remove-profile-from-org (partial remove-profile-from-org-api cfg) - :delete-team (partial delete-team-api cfg) - :remove-team-from-org (partial remove-team-from-org-api cfg) - :get-subscription (partial get-subscription-api cfg) - :connectivity (partial get-connectivity-api cfg)})) + {:get-team-org (partial get-team-org-api cfg) + :set-team-org (partial set-team-org-api cfg) + :get-org-membership (partial get-org-membership-api cfg) + :get-org-membership-by-team (partial get-org-membership-by-team-api cfg) + :get-org-summary (partial get-org-summary-api cfg) + :add-profile-to-org (partial add-profile-to-org-api cfg) + :remove-profile-from-org (partial remove-profile-from-org-api cfg) + :remove-profile-from-all-orgs (partial remove-profile-from-all-orgs-api cfg) + :delete-team (partial delete-team-api cfg) + :remove-team-from-org (partial remove-team-from-org-api cfg) + :get-subscription (partial get-subscription-api cfg) + :connectivity (partial get-connectivity-api cfg)})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; UTILS diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index d8a233931d..ac9c412911 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -1,4 +1,12 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + (ns app.rpc.commands.nitrate + "Nitrate API for Penpot. Provides nitrate-related endpoints to be called + from Penpot frontend." (:require [app.common.data :as d] [app.common.exceptions :as ex] @@ -12,7 +20,6 @@ [app.util.services :as sv])) - (defn assert-is-owner [cfg profile-id team-id] (let [perms (teams/get-permissions cfg profile-id team-id)] (when-not (:is-owner perms) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index efe99c4a70..cfc4a1c66e 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -462,6 +462,9 @@ {:deleted-at deleted-at} {:id profile-id}) + ;; Api call to nitrate + (nitrate/call cfg :remove-profile-from-all-orgs {:profile-id profile-id}) + ;; Schedule cascade deletion to a worker (wrk/submit! {::db/conn conn ::wrk/task :delete-object From e1d3106f61f551ad891d2dc52fe403414b5c05ed Mon Sep 17 00:00:00 2001 From: Dexterity <173429049+Dexterity104@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:31:39 -0400 Subject: [PATCH 179/288] :sparkles: Add color customization for ruler guides (#8986) * :sparkles: Add customizable colors for ruler guides * :sparkles: Update CHANGES.md * :lipstick: Move guide color menu styles to SCSS * :lipstick: Fix trailing whitespace in guides.cljs --------- Signed-off-by: Dexterity <173429049+Dexterity104@users.noreply.github.com> Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + common/src/app/common/types/page.cljc | 3 +- frontend/src/app/main/data/workspace.cljs | 10 +++++ .../src/app/main/data/workspace/guides.cljs | 17 +++++++ .../app/main/ui/workspace/context_menu.cljs | 45 +++++++++++++++++++ .../app/main/ui/workspace/context_menu.scss | 27 +++++++++++ .../main/ui/workspace/viewport/guides.cljs | 28 ++++++++++-- frontend/src/app/plugins/ruler_guides.cljs | 16 +++++++ frontend/translations/en.po | 6 +++ 9 files changed, 148 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 06a817c131..6304ec2b43 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -38,6 +38,7 @@ - Add guide locking and fix locked elements not selectable in viewer (by @Dexterity104) [Github #8358](https://github.com/penpot/penpot/issues/8358) - Apply styles to selection (by @AzazelN28) [Taiga #13647](https://tree.taiga.io/project/penpot/task/13647) - Reorder prototyping overlay options to show Position before Relative to (by @rockchris099) [Github #2910](https://github.com/penpot/penpot/issues/2910) +- Add customizable colors for ruler guides (by @Dexterity104) [Github #5199](https://github.com/penpot/penpot/issues/5199) - Persist asset search query and section filter when switching sidebar tabs (by @eureka0928) [Github #2913](https://github.com/penpot/penpot/issues/2913) - Add delete and duplicate buttons to typography dialog (by @eureka0928) [Github #5270](https://github.com/penpot/penpot/issues/5270) - Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311) diff --git a/common/src/app/common/types/page.cljc b/common/src/app/common/types/page.cljc index 0d4041aaa0..0f3e05f97a 100644 --- a/common/src/app/common/types/page.cljc +++ b/common/src/app/common/types/page.cljc @@ -34,7 +34,8 @@ [:id ::sm/uuid] [:axis [::sm/one-of #{:x :y}]] [:position ::sm/safe-number] - [:frame-id {:optional true} [:maybe ::sm/uuid]]]) + [:frame-id {:optional true} [:maybe ::sm/uuid]] + [:color {:optional true} [:maybe ctc/schema:hex-color]]]) (def schema:guides [:map-of {:gen/max 2} ::sm/uuid schema:guide]) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 75939e4858..eb1f1744b9 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1195,6 +1195,16 @@ (-> params (assoc :kind :grid-cells :grid grid :cells cells)))))))) +(defn show-guide-context-menu + [{:keys [position guide] :as params}] + (dm/assert! (gpt/point? position)) + (ptk/reify ::show-guide-context-menu + ptk/WatchEvent + (watch [_ _ _] + (rx/of (show-context-menu + (-> params (assoc :kind :guide + :guide guide))))))) + (def hide-context-menu (ptk/reify ::hide-context-menu ptk/UpdateEvent diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs index 16762ad3ed..3946e3efbe 100644 --- a/frontend/src/app/main/data/workspace/guides.cljs +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -152,6 +152,23 @@ (map build-move-event) (rx/from)))))) +(defn update-guide-color + [guide-id color] + (ptk/reify ::update-guide-color + ptk/WatchEvent + (watch [it state _] + (let [{:keys [guides] :as page} (dsh/lookup-page state) + guide (get guides guide-id)] + (when (some? guide) + (let [updated-guide (if (some? color) + (assoc guide :color color) + (dissoc guide :color)) + changes + (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/set-guide guide-id updated-guide))] + (rx/of (dwc/commit-changes changes)))))))) + (defn set-hover-guide [id hover?] (ptk/reify ::set-hover-guide diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 23552907fe..7b645384bb 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -899,6 +899,50 @@ :disabled (not has-copied-tracks?)}]])) +(def guide-color-presets + ["#ff3277" "#4dabf7" "#51cf66" "#fcc419" "#ff922b" "#cc5de8" "#ffffff" "#868e96"]) + +(mf/defc guide-color-context-menu* + {::mf/props :obj + ::mf/private true} + [{:keys [mdata]}] + (let [{:keys [guide]} mdata + guide-id (:id guide) + current-color (or (:color guide) (first guide-color-presets)) + + do-set-color + (mf/use-fn + (mf/deps guide-id) + (fn [event] + (let [color (dom/get-data (dom/get-current-target event) "color")] + (st/emit! dw/hide-context-menu + (dwg/update-guide-color guide-id color))))) + + do-remove-guide + (mf/use-fn + (mf/deps guide) + (fn [] + (st/emit! dw/hide-context-menu + (dwg/remove-guide guide))))] + + [:* + [:li {:class (stl/css :context-menu-item :guide-color-label)} + [:span {:class (stl/css :title)} + (tr "workspace.context-menu.guides.change-color")]] + [:li {:class (stl/css :guide-color-swatches)} + (for [color guide-color-presets] + [:span {:key color + :class (stl/css-case + :guide-color-swatch true + :selected (= color current-color)) + :data-color color + :on-click do-set-color + :title color + :style {:background-color color}}])] + [:> menu-separator* {}] + [:> menu-entry* {:title (tr "workspace.context-menu.guides.remove") + :on-click do-remove-guide}]])) + ;; FIXME: optimize because it is rendered always (mf/defc context-menu* @@ -936,4 +980,5 @@ :page [:> page-item-context-menu* {:mdata mdata}] :grid-track [:> grid-track-context-menu* {:mdata mdata}] :grid-cells [:> grid-cells-context-menu* {:mdata mdata}] + :guide [:> guide-color-context-menu* {:mdata mdata}] [:> viewport-context-menu* {:mdata mdata}]))]]])) diff --git a/frontend/src/app/main/ui/workspace/context_menu.scss b/frontend/src/app/main/ui/workspace/context_menu.scss index 53d1d0baf2..a6d1c799a3 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.scss +++ b/frontend/src/app/main/ui/workspace/context_menu.scss @@ -138,3 +138,30 @@ pointer-events: none; opacity: 0.6; } + +.guide-color-label { + cursor: default; + pointer-events: none; +} + +.guide-color-swatches { + display: flex; + flex-wrap: wrap; + gap: deprecated.$s-6; + padding: deprecated.$s-4 deprecated.$s-6 deprecated.$s-8; + list-style: none; +} + +.guide-color-swatch { + width: deprecated.$s-20; + height: deprecated.$s-20; + border-radius: 50%; + cursor: pointer; + flex-shrink: 0; + box-sizing: border-box; + border: deprecated.$s-2 solid var(--panel-border-color); + + &.selected { + border: deprecated.$s-2 solid var(--menu-foreground-color); + } +} diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index bb6df6e965..c79cafe702 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -31,7 +31,8 @@ (def ^:const guide-width 1) (def ^:const guide-opacity 0.7) (def ^:const guide-opacity-hover 1) -(def ^:const guide-color colors/new-danger) +(def ^:const default-guide-color colors/new-danger) + (def ^:const guide-pill-width 34) (def ^:const guide-pill-height 20) (def ^:const guide-pill-corner-radius 4) @@ -285,10 +286,14 @@ (mf/defc guide* {::mf/wrap [mf/memo]} [{:keys [guide is-hover on-guide-change get-hover-frame vbox zoom - hover-frame disabled-guides frame-modifier frame-transform]}] + hover-frame disabled-guides frame-modifier frame-transform + on-guide-context-menu]}] (let [axis (get guide :axis) + guide-color + (or (:color guide) default-guide-color) + read-only? (mf/use-ctx ctx/workspace-read-only?) @@ -303,7 +308,7 @@ handle-change-position (mf/use-fn - (mf/deps on-guide-change) + (mf/deps on-guide-change guide) (fn [changes] (when on-guide-change (on-guide-change (merge guide changes))))) @@ -399,7 +404,13 @@ (not (ctst/rotated-frame? frame)))) [:g.guide-area {:opacity (when frame-guide-outside? 0)} (when-not disabled-guides - (let [{:keys [x y width height]} (guide-area-axis pos vbox zoom frame axis)] + (let [{:keys [x y width height]} (guide-area-axis pos vbox zoom frame axis) + on-context-menu + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (when on-guide-context-menu + (on-guide-context-menu event guide)))] [:rect {:x x :y y :width width @@ -413,6 +424,7 @@ :on-pointer-up on-pointer-up :on-lost-pointer-capture on-lost-pointer-capture :on-pointer-move on-pointer-move + :on-context-menu on-context-menu :on-double-click on-double-click}])) (if (some? frame) @@ -597,6 +609,13 @@ (st/emit! (dw/update-guides guide)) (st/emit! (dw/remove-guide guide))))) + on-guide-context-menu + (mf/use-fn + (fn [event guide] + (let [position (dom/get-client-position event)] + (st/emit! (dw/show-guide-context-menu {:position position + :guide guide}))))) + frame-modifiers (-> (group-by :id modifiers) (update-vals (comp :transform first)))] @@ -628,4 +647,5 @@ :frame-transform (get frame-modifiers frame-id) :get-hover-frame get-hover-frame :on-guide-change on-guide-change + :on-guide-context-menu on-guide-context-menu :disabled-guides disabled-guides}]))])) diff --git a/frontend/src/app/plugins/ruler_guides.cljs b/frontend/src/app/plugins/ruler_guides.cljs index 9cf7535660..8c90b74ca9 100644 --- a/frontend/src/app/plugins/ruler_guides.cljs +++ b/frontend/src/app/plugins/ruler_guides.cljs @@ -94,6 +94,22 @@ value)] (st/emit! (dwgu/update-guides (assoc guide :position position))))))} + :color + {:this true + :get + (fn [self] + (-> self u/proxy->ruler-guide :color)) + + :set + (fn [self value] + (cond + (not (r/check-permission plugin-id "content:write")) + (u/not-valid plugin-id :color "Plugin doesn't have 'content:write' permission") + + :else + (let [guide (u/proxy->ruler-guide self)] + (st/emit! (dwgu/update-guides (assoc guide :color value))))))} + :remove (fn [] (let [guide (u/locate-ruler-guide file-id page-id id)] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 2b325e4e11..f86456267e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5730,6 +5730,12 @@ msgstr "Copy columns" msgid "workspace.context-menu.grid-cells.paste-tracks" msgstr "Paste" +msgid "workspace.context-menu.guides.change-color" +msgstr "Guide color" + +msgid "workspace.context-menu.guides.remove" +msgstr "Remove guide" + #: src/app/main/ui/workspace/context_menu.cljs:754 msgid "workspace.context-menu.grid-track.column.add-after" msgstr "Add 1 column to the right" From bb91c063907190c37ae420d3a8dfe88e546eda8c Mon Sep 17 00:00:00 2001 From: Dexterity <173429049+Dexterity104@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:32:07 -0400 Subject: [PATCH 180/288] :bug: Show check icon after copying team invitation link (#8996) Co-authored-by: Andrey Antukh --- frontend/src/app/main/ui/dashboard/team.cljs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 32e4b012c1..82a2c103d6 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -38,6 +38,7 @@ [app.util.dom :as dom] [app.util.forms :as uforms] [app.util.i18n :as i18n :refer [tr]] + [app.util.timers :as tm] [beicon.v2.core :as rx] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -613,7 +614,10 @@ {::mf/props :obj ::mf/private true} [{:keys [invitation team-id]}] - (let [email (:email invitation) + (let [email (:email invitation) + copied* (mf/use-state false) + copied? (deref copied*) + on-error (mf/use-fn (mf/deps email) @@ -639,6 +643,8 @@ on-copy-success (mf/use-fn (fn [] + (reset! copied* true) + (tm/schedule 1000 #(reset! copied* false)) (st/emit! (ntf/success (tr "notifications.invitation-link-copied")) (modal/hide)))) @@ -656,7 +662,7 @@ [:> icon-button* {:variant "ghost" :aria-label (tr "labels.copy-invitation-link") :on-click on-copy - :icon "clipboard"}])) + :icon (if copied? "tick" "clipboard")}])) (mf/defc invitation-row* {::mf/wrap [mf/memo] From 95b2d7b08348126375247db978d697cb21cde5e6 Mon Sep 17 00:00:00 2001 From: moorsecopers99 <46223049+moorsecopers99@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:19:30 +0300 Subject: [PATCH 181/288] :bug: Add ability to delete uploaded profile avatar (#9068) Fixes #9067. Adds a delete button that appears on hover over an uploaded profile photo; clicking it opens a confirm modal and, on accept, clears the stored photo so the generated fallback avatar is shown again. A new :delete-profile-photo RPC schedules the old storage object for garbage collection and sets photo-id to null. Signed-off-by: moorsecopers99 Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + backend/src/app/rpc/commands/profile.clj | 19 ++++++++++++ .../test/backend_tests/rpc_profile_test.clj | 15 +++++++++- frontend/src/app/main/data/profile.cljs | 17 +++++++++++ .../src/app/main/ui/settings/profile.cljs | 23 ++++++++++++++- .../src/app/main/ui/settings/profile.scss | 29 +++++++++++++++++++ frontend/translations/en.po | 6 ++++ 7 files changed, 108 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6304ec2b43..241f593a73 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -70,6 +70,7 @@ - Fix opacity mixed value [Taiga #13960](https://tree.taiga.io/project/penpot/issue/13960) - Fix gap input throwing an error [Github #8984](https://github.com/penpot/penpot/pull/8984) - Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990) +- Allow deleting the profile avatar after uploading [Github #9067](https://github.com/penpot/penpot/issues/9067) ## 2.15.0 (Unreleased) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index cfc4a1c66e..2199df39ea 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -314,6 +314,25 @@ (climit/invoke! generate-thumbnail file))] (sto/put-object! storage params))) +;; --- MUTATION: Delete Photo + +(sv/defmethod ::delete-profile-photo + {::doc/added "2.16" + ::sm/params [:map] + ::sm/result :nil + ::db/transaction true} + [{:keys [::db/conn ::sto/storage]} {:keys [::rpc/profile-id]}] + (let [profile (get-profile conn profile-id ::db/for-update true)] + (when-let [id (:photo-id profile)] + (sto/touch-object! storage id)) + + (db/update! conn :profile + {:photo-id nil} + {:id profile-id} + {::db/return-keys false}) + + nil)) + ;; --- MUTATION: Request Email Change (declare ^:private request-email-change!) diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 1cdf16a99f..d4cfedf871 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -125,7 +125,20 @@ out (th/command! data)] ;; (th/print-result! out) - (t/is (nil? (:error out))))))) + (t/is (nil? (:error out))))) + + (t/testing "delete photo clears photo-id" + (let [data {::th/type :delete-profile-photo + ::rpc/profile-id (:id profile)} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:result out)))) + + (let [data {::th/type :get-profile + ::rpc/profile-id (:id profile)} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (nil? (:photo-id (:result out)))))))) (t/deftest profile-deletion-1 (let [prof (th/create-profile* 1) diff --git a/frontend/src/app/main/data/profile.cljs b/frontend/src/app/main/data/profile.cljs index 66ded6fc8b..2260a734f8 100644 --- a/frontend/src/app/main/data/profile.cljs +++ b/frontend/src/app/main/data/profile.cljs @@ -348,6 +348,23 @@ (rx/map (constantly (refresh-profile))) (rx/catch on-error)))))) +(def delete-photo + (ptk/reify ::delete-photo + ev/Event + (-data [_] {}) + + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:profile :photo-id] nil)) + + ptk/WatchEvent + (watch [_ _ _] + (->> (rp/cmd! :delete-profile-photo {}) + (rx/map (constantly (refresh-profile))) + (rx/catch (fn [cause] + (js/console.error "delete-photo failed" cause) + (rx/of (refresh-profile)))))))) + (defn fetch-file-comments-users [{:keys [team-id]}] (assert (uuid? team-id) "expected a valid uuid for `team-id`") diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs index 763ee3c836..be2665337d 100644 --- a/frontend/src/app/main/ui/settings/profile.cljs +++ b/frontend/src/app/main/ui/settings/profile.cljs @@ -16,6 +16,7 @@ [app.main.store :as st] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.forms :as fm] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) @@ -92,6 +93,7 @@ [] (let [input-ref (mf/use-ref nil) profile (mf/deref refs/profile) + has-photo? (some? (:photo-id profile)) photo (mf/with-memo [profile] @@ -103,13 +105,32 @@ on-file-selected (fn [file] - (st/emit! (du/update-photo file)))] + (st/emit! (du/update-photo file))) + + on-delete-click + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (st/emit! (modal/show + {:type :confirm + :title (tr "labels.delete-profile-photo.title") + :message (tr "labels.delete-profile-photo.message") + :accept-label (tr "labels.delete") + :on-accept (fn [_] (st/emit! du/delete-photo))}))))] [:form {:class (stl/css :avatar-form)} [:div {:class (stl/css :image-change-field)} [:span {:class (stl/css :update-overlay) :on-click on-image-click} (tr "labels.update")] [:img {:src photo}] + (when has-photo? + [:button {:type "button" + :class (stl/css :delete-overlay) + :title (tr "labels.delete") + :aria-label (tr "labels.delete") + :on-click on-delete-click + :data-testid "profile-image-delete"} + [:> icon* {:icon-id i/delete :size "m"}]]) [:& file-uploader {:accept "image/jpeg,image/png" :multi false :ref input-ref diff --git a/frontend/src/app/main/ui/settings/profile.scss b/frontend/src/app/main/ui/settings/profile.scss index 115c33864a..ce9d3b3b0f 100644 --- a/frontend/src/app/main/ui/settings/profile.scss +++ b/frontend/src/app/main/ui/settings/profile.scss @@ -280,6 +280,31 @@ form.avatar-form { z-index: $z-index-modal; } + .delete-overlay { + position: absolute; + top: $s-4; + inset-inline-end: $s-4; + display: flex; + align-items: center; + justify-content: center; + width: $s-32; + height: $s-32; + padding: 0; + border: none; + border-radius: 50%; + background: var(--color-background-primary); + color: var(--color-foreground-primary); + cursor: pointer; + opacity: 0; + transition: opacity 0.15s ease-in-out; + z-index: calc(#{$z-index-modal} + 1); + + &:hover { + background: var(--color-background-quaternary); + color: var(--color-accent-primary); + } + } + input[type="file"] { width: 100%; height: 100%; @@ -294,6 +319,10 @@ form.avatar-form { .update-overlay { opacity: 0.8; } + + .delete-overlay { + opacity: 1; + } } } diff --git a/frontend/translations/en.po b/frontend/translations/en.po index f86456267e..9b0bbc2ad7 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -2594,6 +2594,12 @@ msgstr "Delete %s files" msgid "labels.deleted" msgstr "Deleted" +msgid "labels.delete-profile-photo.title" +msgstr "Delete profile photo" + +msgid "labels.delete-profile-photo.message" +msgstr "Are you sure you want to delete your profile photo?" + #: src/app/main/ui/onboarding/questions.cljs:86 msgid "labels.developer" msgstr "Development" From 6723e3bbea54129826e736b90a1c93054b9e8c67 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 21 Apr 2026 22:50:54 +0200 Subject: [PATCH 182/288] :bug: Fix issues after staging merge --- .../workspace/sidebar/options/menus/text.cljs | 50 ++++++------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 08c2e73869..499f065a1d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -127,7 +127,7 @@ :icon i/text-bottom}]}]])) (mf/defc grow-options* - [{:keys [ids values on-blur on-change]}] + [{:keys [ids values on-blur]}] (let [grow-type (:grow-type values) editor-instance (mf/deref refs/workspace-editor) @@ -233,18 +233,17 @@ {::mf/wrap [#(mf/memo' % check-props)]} [{:keys [ids type values applied-tokens]}] - (let [file-id (mf/use-ctx ctx/current-file-id) - typographies (mf/deref refs/workspace-file-typography) - editor-instance (mf/deref refs/workspace-editor) - libraries (mf/deref refs/files) - token-row (contains? cf/flags :token-typography-row) + (let [file-id (mf/use-ctx ctx/current-file-id) + typographies (mf/deref refs/workspace-file-typography) + libraries (mf/deref refs/files) + token-row (contains? cf/flags :token-typography-row) ;; --- UI state - menu-state* (mf/use-state {:main-menu true - :more-options false}) - menu-state (deref menu-state*) - main-menu-open? (:main-menu menu-state) - more-options-open? (:more-options menu-state) + menu-state* (mf/use-state {:main-menu true + :more-options false}) + menu-state (deref menu-state*) + main-menu-open? (:main-menu menu-state) + more-options-open? (:more-options menu-state) token-dropdown-open* (mf/use-state false) token-dropdown-open? (deref token-dropdown-open*) @@ -255,13 +254,13 @@ current-token-name (deref current-token-name*) ;; --- Available tokens - active-tokens (mf/use-ctx ctx/active-tokens-by-type) - typography-tokens (mf/with-memo [active-tokens] (csu/filter-tokens-for-input active-tokens :typography)) + active-tokens (mf/use-ctx ctx/active-tokens-by-type) + typography-tokens (mf/with-memo [active-tokens] (csu/filter-tokens-for-input active-tokens :typography)) ;; --- Dropdown - listbox-id (mf/use-id) - nodes-ref (mf/use-ref nil) - dropdown-ref (mf/use-ref nil) + listbox-id (mf/use-id) + nodes-ref (mf/use-ref nil) + dropdown-ref (mf/use-ref nil) dropdown-options (mf/with-memo [typography-tokens] @@ -374,23 +373,6 @@ (st/emit! (dwl/add-typography (assoc typography :id id) false)) (emit-update! ids {:typography-ref-id id :typography-ref-file file-id})))) - on-grow-type-change - (mf/use-fn - (mf/deps ids editor-instance) - (fn [{:keys [grow-type]}] - (let [uid (js/Symbol)] - (st/emit! (dwu/start-undo-transaction uid)) - (when (features/active-feature? @st/state "text-editor/v2") - (let [content (when editor-instance - (content/dom->cljs (dwt/get-editor-root editor-instance)))] - (when (some? content) - (st/emit! (dwt/v2-update-text-shape-content (first ids) content :finalize? true))))) - (st/emit! (dwsh/update-shapes ids #(assoc % :grow-type grow-type))) - (when (features/active-feature? @st/state "render-wasm/v1") - (st/emit! (dwwt/resize-wasm-text-all ids) - (ptk/data-event :layout/update {:ids ids}))) - (ts/schedule #(st/emit! (dwu/commit-undo-transaction uid)))))) - handle-detach-typography (mf/use-fn (mf/deps on-change) @@ -503,7 +485,7 @@ [:div {:class (stl/css :text-align-options)} [:> text-align-options* common-props] - [:> grow-options* (mf/spread-props common-props {:on-change on-grow-type-change})] + [:> grow-options* common-props] [:> icon-button* {:variant "ghost" :aria-label (tr "labels.options") :data-testid "text-align-options-button" From 11c970a94545bc839ccd125c79624eb047f8d90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Fri, 17 Apr 2026 12:27:38 +0200 Subject: [PATCH 183/288] :sparkles: Add nitrate trial text --- frontend/src/app/main/data/nitrate.cljs | 17 +++++++++-------- .../src/app/main/ui/dashboard/subscription.cljs | 4 +++- .../src/app/main/ui/nitrate/nitrate_form.cljs | 10 ++++++++-- .../src/app/main/ui/nitrate/nitrate_form.scss | 4 ++++ .../src/app/main/ui/settings/subscription.cljs | 17 +++++++++-------- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index c6ece1a5fe..68bc7e8151 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -41,13 +41,14 @@ nitrate-entry-pending-popup-key))) (defn show-nitrate-popup - [popup-type] - (ptk/reify ::show-nitrate-popup - ptk/WatchEvent - (watch [_ _ _] - (->> (rp/cmd! ::get-nitrate-connectivity {}) - (rx/map (fn [connectivity] - (modal/show popup-type (or connectivity {})))))))) + ([popup-type] (show-nitrate-popup popup-type {})) + ([popup-type extra-props] + (ptk/reify ::show-nitrate-popup + ptk/WatchEvent + (watch [_ _ _] + (->> (rp/cmd! ::get-nitrate-connectivity {}) + (rx/map (fn [connectivity] + (modal/show popup-type (merge (or connectivity {}) extra-props))))))))) (defn go-to-nitrate-cc ([] @@ -132,4 +133,4 @@ (->> (rp/cmd! ::add-team-to-org {:team-id team-id :organization-id organization-id :organization-name organization-name}) (rx/mapcat (fn [_] - (rx/of (modal/hide)))))))) \ No newline at end of file + (rx/of (modal/hide)))))))) diff --git a/frontend/src/app/main/ui/dashboard/subscription.cljs b/frontend/src/app/main/ui/dashboard/subscription.cljs index 043046f08d..743de20347 100644 --- a/frontend/src/app/main/ui/dashboard/subscription.cljs +++ b/frontend/src/app/main/ui/dashboard/subscription.cljs @@ -163,7 +163,9 @@ [:> button* {:variant "primary" :type "button" :class (stl/css :nitrate-bottom-button) - :on-click handle-click} "UPGRADE TO NITRATE"]]])))) + :on-click handle-click} (if (:subscription profile) + "UPGRADE TO NITRATE" + "Try 14 days for free")]]])))) (mf/defc team* [{:keys [is-owner team]}] diff --git a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs index 6e763a1568..ef3bc46d38 100644 --- a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs +++ b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs @@ -10,6 +10,7 @@ [app.common.schema :as sm] [app.main.data.modal :as modal] [app.main.data.nitrate :as dnt] + [app.main.refs :as refs] [app.main.ui.components.forms :as fm] [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] @@ -27,6 +28,7 @@ [connectivity] (let [online? (:licenses connectivity) + profile (mf/deref refs/profile) initial (mf/with-memo [] {:subscription "yearly"}) form (fm/use-form :schema schema:nitrate-form @@ -72,7 +74,9 @@ [:> button* {:variant "primary" :on-click on-click :class (stl/css :modal-button)} - "UPGRADE TO NITRATE"] + (if (:subscription profile) + "UPGRADE TO NITRATE" + "Try it free for 14 days")] [:div {:class (stl/css :modal-text-small :modal-info)} "Cancel anytime before your next billing cycle."]]] @@ -83,7 +87,9 @@ [:div {:class (stl/css :contact)} [:p {:class (stl/css :modal-text-large)} - "Contact us to upgrade to Nitrate:"] + (if (:subscription profile) + "Contact us to upgrade to Nitrate:" + "Contact us to try Nitrate for 14 days:")] [:p {:class (stl/css :modal-text-large)} [:a {:class (stl/css :link) :href "mailto:sales@penpot.app"} "sales@penpot.app"]]])]]]])) diff --git a/frontend/src/app/main/ui/nitrate/nitrate_form.scss b/frontend/src/app/main/ui/nitrate/nitrate_form.scss index 363297493c..d21a24c26c 100644 --- a/frontend/src/app/main/ui/nitrate/nitrate_form.scss +++ b/frontend/src/app/main/ui/nitrate/nitrate_form.scss @@ -106,3 +106,7 @@ margin-block-start: $sz-96; color: var(--color-foreground-primary); } + +.link { + color: var(--color-accent-primary); +} diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index b7e9b6c3a4..d5a22232bc 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -454,12 +454,12 @@ open-subscription-modal (mf/use-fn - (mf/deps subscription-editors) + (mf/deps subscription-editors nitrate-license) (fn [subscription-type current-subscription] (st/emit! (ev/event {::ev/name "open-subscription-modal" ::ev/origin "settings:in-app"})) (if (= subscription-type "nitrate") - (st/emit! (dnt/show-nitrate-popup :nitrate-dialog)) + (st/emit! (dnt/show-nitrate-popup :nitrate-dialog {:nitrate-license nitrate-license})) (st/emit! (modal/show :management-dialog {:subscription-type subscription-type @@ -649,10 +649,11 @@ :benefits ["Crea organizaciones y añade personas, que usarán Penpot con las reglas que configures." "Acceso exclusivo al Control Center" "Lorem ipsum"] - :cta-text (tr "subscription.settings.subscribe") + :cta-text (if nitrate-license (tr "subscription.settings.subscribe") "Try 14 days for free") :cta-link #(open-subscription-modal "nitrate" subscription) :cta-text-with-icon (tr "subscription.settings.more-information") - :cta-link-with-icon go-to-pricing-page}])]]])) + :cta-link-with-icon go-to-pricing-page + :show-button-cta (not nitrate-license)}])]]])) (def ^:private schema:nitrate-form @@ -662,7 +663,7 @@ (mf/defc subscribe-nitrate-dialog {::mf/register modal/components ::mf/register-as :nitrate-dialog} - [connectivity] + [{:keys [nitrate-license] :as connectivity}] ;; TODO add translations for this texts when we have the definitive ones (let [online? (:licenses connectivity) initial (mf/with-memo [] @@ -730,15 +731,15 @@ :on-click handle-close-dialog}] [:> fm/submit-button* - {:label "TRY 14 DAYS FOR FREE" + {:label (if nitrate-license (tr "subscription.settings.subscribe") "TRY 14 DAYS FOR FREE") :class (stl/css :primary-button)}]]]]]] [:div {:class (stl/css :modal-content :modal-contact-content)} [:div {:class (stl/css :modal-text)} "Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum"] [:div {:class (stl/css :modal-text)} - "Contact us to upgrade to Nitrate:"] + (if nitrate-license "Contact us to upgrade to Nitrate:" "Contact us to try Nitrate for 14 days:")] [:div {:class (stl/css :modal-text)} - [:a {:class (stl/css :link) :href "mailto:sales@penpot.app"} + [:a {:class (stl/css :cta-button) :href "mailto:sales@penpot.app"} "sales@penpot.app"]]])]])) From 6c19c7c0c48b48175e8e272e4a7ef64206bba1fc Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 21 Apr 2026 21:03:47 +0000 Subject: [PATCH 184/288] :zap: Memoize static options in vertical-align* component Wrap the radio-button options vector in `mf/with-memo []` so the vector allocation and `(tr ...)` calls happen once per component mount instead of on every render. Also document the translation memoization rule in frontend/AGENTS.md: `(tr ...)` must never be called at namespace level (locale is runtime-only), and static option lists should always be wrapped in `mf/with-memo []`. Signed-off-by: Andrey Antukh --- frontend/AGENTS.md | 25 +++++++++++++++++ .../workspace/sidebar/options/menus/text.cljs | 28 +++++++++++-------- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 0e33a32f30..681528b4f3 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -329,6 +329,31 @@ CSS modules pattern): - [ ] Selectors are flat (no deep nesting). +### Translations (`tr`) and Memoization + +`(tr "some.key")` resolves the translation string from the **currently active +locale at call time**. This has two consequences: + +- **Never call `(tr ...)` at namespace level** (inside a `def` or `defonce`). + Doing so would freeze the label to the locale active at module load time and + break runtime language switching. +- **Always call `(tr ...)` at render time** — either directly in the component + body or inside a `mf/with-memo` / `mf/use-memo` block. + +When a component renders a **static list of options** whose labels come from +`(tr ...)` (e.g. radio button options, select options), wrap the vector in +`mf/with-memo []` with no dependencies. This ensures the vector and its +`(tr ...)` calls are evaluated once per component mount instead of on every +render, while still respecting the render-time requirement: + +```clojure +(let [options (mf/with-memo [] + [{:value "top" :label (tr "some.key.top")} + {:value "center" :label (tr "some.key.center")} + {:value "bottom" :label (tr "some.key.bottom")}])] + ...) +``` + ### Performance Macros (`app.common.data.macros`) Always prefer these macros over their `clojure.core` equivalents — they compile to faster JavaScript: diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 499f065a1d..d13594b525 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -102,6 +102,21 @@ (mf/defc vertical-align* [{:keys [values on-change on-blur]}] (let [vertical-align (or (:vertical-align values) "top") + options + (mf/with-memo [] + [{:value "top" + :id "vertical-text-align-top" + :label (tr "workspace.options.text-options.align-top") + :icon i/text-top} + {:value "center" + :id "vertical-text-align-center" + :label (tr "workspace.options.text-options.align-middle") + :icon i/text-middle} + {:value "bottom" + :id "vertical-text-align-bottom" + :label (tr "workspace.options.text-options.align-bottom") + :icon i/text-bottom}]) + handle-change (mf/use-fn (mf/deps on-change on-blur) @@ -113,18 +128,7 @@ [:> radio-buttons* {:selected vertical-align :on-change handle-change :name "vertical-align-text-options" - :options [{:value "top" - :id "vertical-text-align-top" - :label (tr "workspace.options.text-options.align-top") - :icon i/text-top} - {:value "center" - :id "vertical-text-align-center" - :label (tr "workspace.options.text-options.align-middle") - :icon i/text-middle} - {:value "bottom" - :id "vertical-text-align-bottom" - :label (tr "workspace.options.text-options.align-bottom") - :icon i/text-bottom}]}]])) + :options options}]])) (mf/defc grow-options* [{:keys [ids values on-blur]}] From 466f27eb7cc2b4dd503408f35a65c3da02d82d0b Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 21 Apr 2026 21:14:24 +0000 Subject: [PATCH 185/288] :zap: Memoize static options in text-align-options* component Wrap the four radio-button option maps in mf/with-memo [] so the vector and its (tr ...) calls are evaluated once per mount instead of on every render. Signed-off-by: Andrey Antukh --- .../workspace/sidebar/options/menus/text.cljs | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index d13594b525..9ef7db17e8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -48,7 +48,26 @@ (mf/defc text-align-options* [{:keys [values on-change on-blur]}] - (let [handle-change + (let [options + (mf/with-memo [] + [{:value "left" + :id "text-align-left" + :label (tr "workspace.options.text-options.text-align-left") + :icon i/text-align-left} + {:value "center" + :id "text-align-center" + :label (tr "workspace.options.text-options.text-align-center") + :icon i/text-align-center} + {:value "right" + :id "text-align-right" + :label (tr "workspace.options.text-options.text-align-right") + :icon i/text-align-right} + {:value "justify" + :id "text-align-justify" + :label (tr "workspace.options.text-options.text-align-justify") + :icon i/text-justify}]) + + handle-change (mf/use-fn (mf/deps on-change on-blur) (fn [value] @@ -59,22 +78,7 @@ [:> radio-buttons* {:selected (:text-align values) :on-change handle-change :name "align-text-options" - :options [{:value "left" - :id "text-align-left" - :label (tr "workspace.options.text-options.text-align-left") - :icon i/text-align-left} - {:value "center" - :id "text-align-center" - :label (tr "workspace.options.text-options.text-align-center") - :icon i/text-align-center} - {:value "right" - :id "text-align-right" - :label (tr "workspace.options.text-options.text-align-right") - :icon i/text-align-right} - {:value "justify" - :id "text-align-justify" - :label (tr "workspace.options.text-options.text-align-justify") - :icon i/text-justify}]}]])) + :options options}]])) (mf/defc text-direction-options* [{:keys [values on-change on-blur]}] From dfd992aa498edec48572ef9a6657193c9e3cef8e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 21 Apr 2026 21:15:09 +0000 Subject: [PATCH 186/288] :zap: Memoize static options in text-direction-options* component Wrap the two radio-button option maps in mf/with-memo [] so the vector and its (tr ...) calls are evaluated once per mount instead of on every render. Signed-off-by: Andrey Antukh --- .../workspace/sidebar/options/menus/text.cljs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 9ef7db17e8..119131c983 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -83,6 +83,17 @@ (mf/defc text-direction-options* [{:keys [values on-change on-blur]}] (let [direction (:text-direction values) + options + (mf/with-memo [] + [{:value "ltr" + :id "ltr-text-direction" + :label (tr "workspace.options.text-options.direction-ltr") + :icon i/text-ltr} + {:value "rtl" + :id "rtl-text-direction" + :label (tr "workspace.options.text-options.direction-rtl") + :icon i/text-rtl}]) + handle-change (mf/use-fn (mf/deps on-change on-blur direction) @@ -94,14 +105,7 @@ [:> radio-buttons* {:selected direction :on-change handle-change :name "text-direction-options" - :options [{:value "ltr" - :id "ltr-text-direction" - :label (tr "workspace.options.text-options.direction-ltr") - :icon i/text-ltr} - {:value "rtl" - :id "rtl-text-direction" - :label (tr "workspace.options.text-options.direction-rtl") - :icon i/text-rtl}]}]])) + :options options}]])) (mf/defc vertical-align* [{:keys [values on-change on-blur]}] From b8aa243c2b585cf7f91d9f193fba8210d8a48904 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 21 Apr 2026 21:16:08 +0000 Subject: [PATCH 187/288] :zap: Memoize static options in grow-options* component Wrap the three radio-button option maps in mf/with-memo [] so the vector and its (tr ...) calls are evaluated once per mount instead of on every render. Signed-off-by: Andrey Antukh --- .../workspace/sidebar/options/menus/text.cljs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 119131c983..1480002220 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -142,6 +142,20 @@ [{:keys [ids values on-blur]}] (let [grow-type (:grow-type values) editor-instance (mf/deref refs/workspace-editor) + options + (mf/with-memo [] + [{:value "fixed" + :id "text-fixed-grow" + :label (tr "workspace.options.text-options.grow-fixed") + :icon i/text-fixed} + {:value "auto-width" + :id "text-auto-width-grow" + :label (tr "workspace.options.text-options.grow-auto-width") + :icon i/text-auto-width} + {:value "auto-height" + :id "text-auto-height-grow" + :label (tr "workspace.options.text-options.grow-auto-height") + :icon i/text-auto-height}]) handle-change (mf/use-fn @@ -170,18 +184,7 @@ [:> radio-buttons* {:selected (d/name grow-type) :on-change handle-change :name "grow-text-options" - :options [{:value "fixed" - :id "text-fixed-grow" - :label (tr "workspace.options.text-options.grow-fixed") - :icon i/text-fixed} - {:value "auto-width" - :id "text-auto-width-grow" - :label (tr "workspace.options.text-options.grow-auto-width") - :icon i/text-auto-width} - {:value "auto-height" - :id "text-auto-height-grow" - :label (tr "workspace.options.text-options.grow-auto-height") - :icon i/text-auto-height}]}]])) + :options options}]])) (mf/defc text-decoration-options* [{:keys [values on-change on-blur token-applied]}] From a94a7221fbbab9385c2a0c7aa36fc74dc78373d0 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 21 Apr 2026 21:16:57 +0000 Subject: [PATCH 188/288] :zap: Memoize options in text-decoration-options* component Wrap the two radio-button option maps in mf/with-memo [token-applied] so the vector and its (tr ...) calls are evaluated only when the token-applied prop changes, not on every render. Signed-off-by: Andrey Antukh --- .../workspace/sidebar/options/menus/text.cljs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 1480002220..63138c6fb9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -190,6 +190,19 @@ [{:keys [values on-change on-blur token-applied]}] (let [token-row (contains? cf/flags :token-typography-row) text-decoration (some-> (:text-decoration values) d/name) + options + (mf/with-memo [token-applied] + [{:value "underline" + :id "underline-text-decoration" + :disabled (and token-row (some? token-applied)) + :label (tr "workspace.options.text-options.underline" (sc/get-tooltip :underline)) + :icon i/text-underlined} + {:value "line-through" + :id "line-through-text-decoration" + :disabled (and token-row (some? token-applied)) + :label (tr "workspace.options.text-options.strikethrough" (sc/get-tooltip :line-through)) + :icon i/text-stroked}]) + handle-change (mf/use-fn (mf/deps on-change on-blur) @@ -206,16 +219,7 @@ :name "text-decoration-options" :disabled (and token-row (some? token-applied)) :allow-empty true - :options [{:value "underline" - :id "underline-text-decoration" - :disabled (and token-row (some? token-applied)) - :label (tr "workspace.options.text-options.underline" (sc/get-tooltip :underline)) - :icon i/text-underlined} - {:value "line-through" - :id "line-through-text-decoration" - :disabled (and token-row (some? token-applied)) - :label (tr "workspace.options.text-options.strikethrough" (sc/get-tooltip :line-through)) - :icon i/text-stroked}]}]])) + :options options}]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Helpers From d28c0ea066cd071275c910b6d59ddab743af7b70 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 21 Apr 2026 21:17:33 +0000 Subject: [PATCH 189/288] :zap: Memoize label computation in text-menu* component Wrap the (case type (tr ...) ...) expression in mf/with-memo [type] so the translation is resolved only when the type prop changes instead of on every render. Signed-off-by: Andrey Antukh --- .../main/ui/workspace/sidebar/options/menus/text.cljs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 63138c6fb9..bb5c2aa3f6 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -323,10 +323,12 @@ :attributes #{:typography} :token token :on-update-shape dwta/update-typography}))))) - label (case type - :multiple (tr "workspace.options.text-options.title-selection") - :group (tr "workspace.options.text-options.title-group") - (tr "workspace.options.text-options.title")) + label + (mf/with-memo [type] + (case type + :multiple (tr "workspace.options.text-options.title-selection") + :group (tr "workspace.options.text-options.title-group") + (tr "workspace.options.text-options.title"))) set-option-ref (mf/use-fn (fn [node] From 74d1288003640b82f08ada46accac16e389ffd70 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 21 Apr 2026 21:23:42 +0000 Subject: [PATCH 190/288] :zap: Hoist token-typography-row? flag check to namespace level (contains? cf/flags :token-typography-row) is a pure constant: cf/flags is immutable after startup. Define it once as a private namespace-level var token-typography-row? instead of re-evaluating the check on every render in text-decoration-options* and text-menu*. Signed-off-by: Andrey Antukh --- .../workspace/sidebar/options/menus/text.cljs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index bb5c2aa3f6..57c5e2f977 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -42,6 +42,15 @@ [potok.v2.core :as ptk] [rumext.v2 :as mf])) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Constants +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:private token-typography-row-enabled? + "True when the token-typography-row feature flag is enabled. + Evaluated once at module load time; cf/flags is immutable after startup." + (contains? cf/flags :token-typography-row)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Sub-components ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -188,18 +197,17 @@ (mf/defc text-decoration-options* [{:keys [values on-change on-blur token-applied]}] - (let [token-row (contains? cf/flags :token-typography-row) - text-decoration (some-> (:text-decoration values) d/name) + (let [text-decoration (some-> (:text-decoration values) d/name) options (mf/with-memo [token-applied] [{:value "underline" :id "underline-text-decoration" - :disabled (and token-row (some? token-applied)) + :disabled (and token-typography-row-enabled? (some? token-applied)) :label (tr "workspace.options.text-options.underline" (sc/get-tooltip :underline)) :icon i/text-underlined} {:value "line-through" :id "line-through-text-decoration" - :disabled (and token-row (some? token-applied)) + :disabled (and token-typography-row-enabled? (some? token-applied)) :label (tr "workspace.options.text-options.strikethrough" (sc/get-tooltip :line-through)) :icon i/text-stroked}]) @@ -217,7 +225,7 @@ text-decoration) :on-change handle-change :name "text-decoration-options" - :disabled (and token-row (some? token-applied)) + :disabled (and token-typography-row-enabled? (some? token-applied)) :allow-empty true :options options}]])) @@ -255,8 +263,6 @@ (let [file-id (mf/use-ctx ctx/current-file-id) typographies (mf/deref refs/workspace-file-typography) libraries (mf/deref refs/files) - token-row (contains? cf/flags :token-typography-row) - ;; --- UI state menu-state* (mf/use-state {:main-menu true :more-options false}) @@ -455,7 +461,7 @@ :title label :class (stl/css :title-spacing-text)} [:* - (when (and token-row (some? (resolve-delay typography-tokens)) (not typography)) + (when (and token-typography-row-enabled? (some? (resolve-delay typography-tokens)) (not typography)) [:> icon-button* {:variant "ghost" :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown") :on-click toggle-token-dropdown @@ -471,7 +477,7 @@ (when main-menu-open? [:div {:class (stl/css :element-content)} (cond - (and token-row current-token-name) + (and token-typography-row-enabled? current-token-name) [:> token-typography-row* {:token-name current-token-name :detach-token detach-token :active-tokens (resolve-delay typography-tokens)}] @@ -519,7 +525,7 @@ [:> text-decoration-options* (mf/spread-props common-props {:token-applied current-token-name})] [:> text-direction-options* common-props]])]) - (when (and token-row token-dropdown-open?) + (when (and token-typography-row-enabled? token-dropdown-open?) [:> searchable-options-dropdown* {:on-click on-option-click :id listbox-id :options (resolve-delay dropdown-options) From 7751d9a69b46384a61c39968d67632b14aa0613f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 06:35:22 +0000 Subject: [PATCH 191/288] :zap: Remove spurious deps from toggle callbacks in text-menu* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toggle-main-menu and toggle-more-options close over a state atom and need no external deps. The declared deps (main-menu-open? / more-options-open?) were unused inside the function bodies, causing each callback to be reallocated on every toggle — self-reinforcing churn. Drop the mf/deps calls to make both callbacks stable. Signed-off-by: Andrey Antukh --- .../src/app/main/ui/workspace/sidebar/options/menus/text.cljs | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 57c5e2f977..22f79291d5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -348,12 +348,10 @@ ;; --- Toggles toggle-main-menu (mf/use-fn - (mf/deps main-menu-open?) #(swap! menu-state* update :main-menu not)) toggle-more-options (mf/use-fn - (mf/deps more-options-open?) #(swap! menu-state* update :more-options not)) toggle-token-dropdown From 81faa5a7281aea05004743694e43ab49ae4f2314 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 06:36:31 +0000 Subject: [PATCH 192/288] :zap: Replace duplicate on-blur lambda with stable on-text-blur ref The inline (fn [] (ts/schedule ...)) passed as :on-blur to text-options was an exact copy of the on-text-blur callback already defined via mf/use-fn earlier in the same let block. Pass on-text-blur directly to avoid allocating a new function object on every render. Signed-off-by: Andrey Antukh --- .../app/main/ui/workspace/sidebar/options/menus/text.cljs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 22f79291d5..c1d9424574 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -500,13 +500,7 @@ :values values :on-change on-change :show-recent true - :on-blur - (fn [] - (ts/schedule - 100 - (fn [] - (when (not= "INPUT" (-> (dom/get-active) dom/get-tag-name)) - (dom/focus! (txu/get-text-editor-content))))))}]) + :on-blur on-text-blur}]) [:div {:class (stl/css :text-align-options)} [:> text-align-options* common-props] From ad974f4047d07b448cd8598795156a07f901dcc6 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 20 Apr 2026 12:23:51 +0200 Subject: [PATCH 193/288] :lipstick: Unify naming on nitrate-api --- .../resources/app/email/invite-to-org/en.html | 2 +- .../resources/app/email/invite-to-org/en.subj | 2 +- .../resources/app/email/invite-to-org/en.txt | 2 +- backend/src/app/email.clj | 2 +- backend/src/app/nitrate.clj | 16 +- backend/src/app/rpc/commands/nitrate.clj | 24 +-- backend/src/app/rpc/commands/teams.clj | 16 +- .../app/rpc/commands/teams_invitations.clj | 12 +- backend/src/app/rpc/commands/verify_token.clj | 24 +-- backend/src/app/rpc/management/nitrate.clj | 26 +-- backend/src/app/rpc/notifications.clj | 2 +- .../rpc_management_nitrate_test.clj | 116 ++++++------- .../test/backend_tests/rpc_nitrate_test.clj | 156 +++++++++--------- frontend/src/app/main/data/nitrate.cljs | 8 +- .../src/app/main/ui/auth/verify_token.cljs | 4 +- .../app/main/ui/dashboard/change_owner.scss | 3 + .../src/app/main/ui/dashboard/sidebar.cljs | 57 ++++--- 17 files changed, 240 insertions(+), 232 deletions(-) diff --git a/backend/resources/app/email/invite-to-org/en.html b/backend/resources/app/email/invite-to-org/en.html index 0a02932e99..45ec53596a 100644 --- a/backend/resources/app/email/invite-to-org/en.html +++ b/backend/resources/app/email/invite-to-org/en.html @@ -205,7 +205,7 @@
- Hi{{ user-name|abbreviate:25 }}, + Hi{% if user-name %} {{ user-name|abbreviate:25 }}{% endif %},
- {{invited-by|abbreviate:25}} sent you an invitation to join the organization {{ org-name|abbreviate:25 }}: + {{invited-by|abbreviate:25}} sent you an invitation to join the organization:
- - {{ org-initials }} - - + + + + +
+ {{org-initials}} +
“{{ org-name|abbreviate:25 }}” -
+
- “{{ org-name|abbreviate:25 }}” + “{{ organization-name|abbreviate:25 }}”
diff --git a/backend/resources/app/email/invite-to-org/en.subj b/backend/resources/app/email/invite-to-org/en.subj index d61232bfd3..765d186236 100644 --- a/backend/resources/app/email/invite-to-org/en.subj +++ b/backend/resources/app/email/invite-to-org/en.subj @@ -1 +1 @@ -{{invited-by|abbreviate:25}} has invited you to join the organization “{{ org-name|abbreviate:25 }}” \ No newline at end of file +{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization-name|abbreviate:25 }}” diff --git a/backend/resources/app/email/invite-to-org/en.txt b/backend/resources/app/email/invite-to-org/en.txt index 65317deb6e..8e22eba453 100644 --- a/backend/resources/app/email/invite-to-org/en.txt +++ b/backend/resources/app/email/invite-to-org/en.txt @@ -1,6 +1,6 @@ Hello! -{{invited-by|abbreviate:25}} has invited you to join the organization “{{ org-name|abbreviate:25 }}”. +{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization-name|abbreviate:25 }}”. Accept invitation using this link: diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index bd68ec92ee..f44e80c6c6 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -415,7 +415,7 @@ (def ^:private schema:invite-to-org [:map [:invited-by ::sm/text] - [:org-name ::sm/text] + [:organization-name ::sm/text] [:org-initials ::sm/text] [:org-logo ::sm/uri] [:user-name [:maybe ::sm/text]] diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 226b5d3bef..afb1f78775 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -223,12 +223,12 @@ schema:organization params))) (defn- get-org-membership-api - [cfg {:keys [profile-id org-id] :as params}] + [cfg {:keys [profile-id organization-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] (request-to-nitrate cfg :get (str baseuri "/api/organizations/" - org-id + organization-id "/members/" profile-id) schema:profile-org params))) @@ -246,12 +246,12 @@ (defn- get-org-summary-api - [cfg {:keys [org-id] :as params}] + [cfg {:keys [organization-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri)] (request-to-nitrate cfg :get (str baseuri "/api/organizations/" - org-id + organization-id "/summary") schema:org-summary params))) @@ -269,7 +269,7 @@ schema:team params))) (defn- add-profile-to-org-api - [cfg {:keys [profile-id org-id team-id email] :as params}] + [cfg {:keys [profile-id organization-id team-id email] :as params}] (let [baseuri (cf/get :nitrate-backend-uri) request-params (cond-> {:user-id profile-id :team-id team-id} (some? email) (assoc :email email)) @@ -277,18 +277,18 @@ (request-to-nitrate cfg :post (str baseuri "/api/organizations/" - org-id + organization-id "/add-user") schema:profile-org params))) (defn- remove-profile-from-org-api - [cfg {:keys [profile-id org-id] :as params}] + [cfg {:keys [profile-id organization-id] :as params}] (let [baseuri (cf/get :nitrate-backend-uri) params (assoc params :request-params {:user-id profile-id})] (request-to-nitrate cfg :post (str baseuri "/api/organizations/" - org-id + organization-id "/remove-user") nil params))) diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index ac9c412911..7a81d05e4e 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -34,7 +34,7 @@ (defn assert-membership [cfg profile-id organization-id] (let [membership (nitrate/call cfg :get-org-membership {:profile-id profile-id - :org-id organization-id})] + :organization-id organization-id})] (when-not (:organization-id membership) (ex/raise :type :validation :code :organization-doesnt-exists)) @@ -83,8 +83,8 @@ (def ^:private schema:leave-org [:map - [:org-id ::sm/uuid] - [:org-name ::sm/text] + [:id ::sm/uuid] + [:name ::sm/text] [:default-team-id ::sm/uuid] [:teams-to-delete [:vector ::sm/uuid]] @@ -130,13 +130,13 @@ :valid-teams-to-exit valid-teams-to-exit :valid-default-team valid-default-team}))) -(defn get-valid-teams [cfg org-id profile-id default-team-id] - (let [org-summary (nitrate/call cfg :get-org-summary {:org-id org-id}) +(defn get-valid-teams [cfg organization-id profile-id default-team-id] + (let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id}) org-teams (get-organization-teams-for-user cfg org-summary profile-id)] (calculate-valid-teams org-teams default-team-id))) -(defn- assert-valid-teams [cfg profile-id org-id default-team-id teams-to-delete teams-to-leave] - (let [org-summary (nitrate/call cfg :get-org-summary {:org-id org-id}) +(defn- assert-valid-teams [cfg profile-id organization-id default-team-id teams-to-delete teams-to-leave] + (let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id}) org-teams (get-organization-teams-for-user cfg org-summary profile-id) {:keys [valid-teams-to-delete-ids valid-teams-to-transfer @@ -184,8 +184,8 @@ (defn leave-org - [{:keys [::db/conn] :as cfg} {:keys [profile-id org-id org-name default-team-id teams-to-delete teams-to-leave skip-validation] :as params}] - (let [org-prefix (str "[" (d/sanitize-string org-name) "] ") + [{:keys [::db/conn] :as cfg} {:keys [profile-id id name default-team-id teams-to-delete teams-to-leave skip-validation] :as params}] + (let [org-prefix (str "[" (d/sanitize-string name) "] ") default-team-files-count (-> (db/exec-one! conn [sql:get-team-files-count default-team-id]) :total) @@ -196,9 +196,9 @@ ;; assert that the received teams are valid, checking the different constraints (when-not skip-validation - (assert-valid-teams cfg profile-id org-id default-team-id teams-to-delete teams-to-leave)) + (assert-valid-teams cfg profile-id id default-team-id teams-to-delete teams-to-leave)) - (assert-membership cfg profile-id org-id) + (assert-membership cfg profile-id id) ;; delete the teams-to-delete (doseq [id teams-to-delete] @@ -216,7 +216,7 @@ (db/exec! conn [sql:prefix-team-name-and-unset-default org-prefix default-team-id])) ;; Api call to nitrate - (nitrate/call cfg :remove-profile-from-org {:profile-id profile-id :org-id org-id}) + (nitrate/call cfg :remove-profile-from-org {:profile-id profile-id :organization-id id}) nil)) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 8638252338..58a1fff217 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -542,9 +542,9 @@ (defn initialize-user-in-nitrate-org "If needed, create a default team for the user on the organization, and notify Nitrate that an user has been added to an org." - ([cfg profile-id org-id] - (initialize-user-in-nitrate-org cfg profile-id org-id nil)) - ([cfg profile-id org-id email] + ([cfg profile-id organization-id] + (initialize-user-in-nitrate-org cfg profile-id organization-id nil)) + ([cfg profile-id organization-id email] (assert (db/connection-map? cfg) "expected cfg with valid connection") (when (contains? cf/flags :nitrate) @@ -553,25 +553,25 @@ (fn [{:keys [::db/conn] :as tx-cfg}] (let [membership (nitrate/call cfg :get-org-membership {:profile-id profile-id - :org-id org-id})] + :organization-id organization-id})] ;; Only when the user doesn't belong to the organization yet (when (and (some? (:organization-id membership)) ;; the organization exists (not (:is-member membership))) ;; the user is not a member of the org yet - (let [org-id org-id - default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id org-id) + (let [organization-id organization-id + default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id organization-id) default-team-id (:id default-team) result (nitrate/call tx-cfg :add-profile-to-org (cond-> {:profile-id profile-id :team-id default-team-id - :org-id org-id} + :organization-id organization-id} (some? email) (assoc :email email)))] (when (not (:is-member result)) (ex/raise :type :internal :code :failed-add-profile-org-nitrate :context {:profile-id profile-id - :org-id org-id + :organization-id organization-id :default-team-id default-team-id})) default-team-id)))))))) diff --git a/backend/src/app/rpc/commands/teams_invitations.clj b/backend/src/app/rpc/commands/teams_invitations.clj index 0e496aac37..aaa4f22bc3 100644 --- a/backend/src/app/rpc/commands/teams_invitations.clj +++ b/backend/src/app/rpc/commands/teams_invitations.clj @@ -50,15 +50,15 @@ returning *") (defn- create-invitation-token - [cfg {:keys [profile-id valid-until org-id org-name team-id member-id member-email role]}] + [cfg {:keys [profile-id valid-until organization-id organization-name team-id member-id member-email role]}] (tokens/generate cfg {:iss :team-invitation :exp valid-until :profile-id profile-id :role role :team-id team-id - :org-id org-id - :org-name org-name + :organization-id organization-id + :organization-name organization-name :member-email member-email :member-id member-id})) @@ -178,8 +178,8 @@ :invitation-id (:id invitation) :valid-until expire :team-id (:id team) - :org-id (:id organization) - :org-name (:name organization) + :organization-id (:id organization) + :organization-name (:name organization) :member-email (:email-to invitation) :member-id (:id member) :role role} @@ -210,7 +210,7 @@ :to email :invited-by (:fullname profile) :user-name (:fullname member) - :org-name (:name organization) + :organization-name (:name organization) :org-logo (:logo organization) :org-initials (d/get-initials (:name organization)) :token itoken diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index 9392d25648..e99e74c3c5 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -89,7 +89,7 @@ (defn- accept-invitation [{:keys [::db/conn] :as cfg} - {:keys [team-id org-id role member-email] :as claims} invitation member] + {:keys [team-id organization-id role member-email] :as claims} invitation member] (let [;; Update the role if there is an invitation role (or (some-> invitation :role keyword) role) id-member (:id member)] @@ -109,10 +109,10 @@ :profile-id id-member} (get types.team/permissions-for-role role)) - accepted-team-id (if org-id + accepted-team-id (if organization-id ;; Insert the invited member to the org (when (contains? cf/flags :nitrate) - (teams/initialize-user-in-nitrate-org cfg id-member org-id member-email)) + (teams/initialize-user-in-nitrate-org cfg id-member organization-id member-email)) ;; Insert the invited member to the team (do (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}) team-id))] @@ -134,7 +134,7 @@ (db/delete! conn :team-invitation (cond-> {:email-to member-email} team-id (assoc :team-id team-id) - org-id (assoc :org-id org-id))) + organization-id (assoc :organization-id organization-id))) ;; Delete any request (only applicable for team invitations) (when team-id @@ -151,11 +151,11 @@ [:profile-id ::sm/uuid] [:role types.team/schema:role] [:team-id {:optional true} ::sm/uuid] - [:org-id {:optional true} ::sm/uuid] + [:organization-id {:optional true} ::sm/uuid] [:member-email ::sm/email] [:member-id {:optional true} ::sm/uuid]] - [:fn {:error/message "team-id or org-id must be present"} - (fn [m] (or (:team-id m) (:org-id m)))]]) + [:fn {:error/message "team-id or organization-id must be present"} + (fn [m] (or (:team-id m) (:organization-id m)))]]) (def valid-team-invitation-claims? (sm/lazy-validator schema:team-invitation-claims)) @@ -163,7 +163,7 @@ (defmethod process-token :team-invitation [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id token] :as params} - {:keys [member-id team-id org-id member-email] :as claims}] + {:keys [member-id team-id organization-id member-email] :as claims}] (when-not (valid-team-invitation-claims? claims) (ex/raise :type :validation @@ -173,16 +173,16 @@ (let [invitation (db/get* conn :team-invitation (cond-> {:email-to member-email} team-id (assoc :team-id team-id) - org-id (assoc :org-id org-id))) + organization-id (assoc :organization-id organization-id))) profile (db/get* conn :profile {:id profile-id} {:columns [:id :email :default-team-id]}) registration-disabled? (not (contains? cf/flags :registration)) - org-invitation? (and (contains? cf/flags :nitrate) org-id) + org-invitation? (and (contains? cf/flags :nitrate) organization-id) membership (when org-invitation? (nitrate/call cfg :get-org-membership {:profile-id profile-id - :org-id org-id}))] + :organization-id organization-id}))] (if profile (do @@ -240,7 +240,7 @@ (cond-> (assoc claims :state :created) ;; when the invitation is to an org, instead of a team, add the ;; accepted-team-id as :org-team-id - (:org-id claims) + (:organization-id claims) (assoc :org-team-id accepted-team-id))))) ;; If we have not logged-in user, and invitation comes with member-id we diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 2a22193d01..f68787fb1c 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -217,7 +217,7 @@ RETURNING id, name;") (def ^:private schema:notify-org-deletion [:map - [:org-name ::sm/text] + [:organization-name ::sm/text] [:teams [:vector ::sm/uuid]]]) (sv/defmethod ::notify-org-deletion @@ -225,9 +225,9 @@ RETURNING id, name;") of the deletion to the connected users" {::doc/added "2.15" ::sm/params schema:notify-org-deletion} - [cfg {:keys [teams org-name]}] + [cfg {:keys [teams organization-name]}] (when (seq teams) - (let [org-prefix (str "[" (d/sanitize-string org-name) "] ")] + (let [org-prefix (str "[" (d/sanitize-string organization-name) "] ")] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] @@ -237,7 +237,7 @@ RETURNING id, name;") ;; Notify users (doseq [team updated-teams] - (notifications/notify-team-change cfg (:id team) (:name team) nil org-name "dashboard.org-deleted")))))))) + (notifications/notify-team-change cfg (:id team) (:name team) nil organization-name "dashboard.org-deleted")))))))) ;; ---- API: get-profile-by-email @@ -373,24 +373,26 @@ RETURNING id, name;") {::doc/added "2.16" ::sm/params [:map [:profile-id ::sm/uuid] - [:org-id ::sm/uuid] - [:org-name ::sm/text] + [:organization-id ::sm/uuid] + [:organization-name ::sm/text] [:default-team-id ::sm/uuid]] ::db/transaction true} - [cfg {:keys [profile-id org-id org-name default-team-id] :as params}] + [cfg {:keys [profile-id organization-id organization-name default-team-id] :as params}] (let [{:keys [valid-teams-to-delete-ids valid-teams-to-transfer - valid-teams-to-exit]} (cnit/get-valid-teams cfg org-id profile-id default-team-id) + valid-teams-to-exit]} (cnit/get-valid-teams cfg organization-id profile-id default-team-id) add-reassign-to (partial add-reassign-to cfg profile-id) valid-teams-to-leave (into valid-teams-to-exit (map add-reassign-to valid-teams-to-transfer))] (cnit/leave-org cfg (assoc params + :id organization-id + :name organization-name :teams-to-delete valid-teams-to-delete-ids :teams-to-leave valid-teams-to-leave :skip-validation true)) - (notifications/notify-user-removed-from-org cfg profile-id org-id org-name "dashboard.user-no-longer-belong-org") + (notifications/notify-user-org-change cfg profile-id organization-id organization-name "dashboard.user-no-longer-belong-org") nil)) ;; API: get-remove-from-org-summary @@ -407,15 +409,15 @@ RETURNING id, name;") {::doc/added "2.16" ::sm/params [:map [:profile-id ::sm/uuid] - [:org-id ::sm/uuid] + [:organization-id ::sm/uuid] [:default-team-id ::sm/uuid]] ::sm/result schema:get-remove-from-org-summary-result ::db/transaction true} - [cfg {:keys [profile-id org-id default-team-id]}] + [cfg {:keys [profile-id organization-id default-team-id]}] (let [{:keys [valid-teams-to-delete-ids valid-teams-to-transfer valid-teams-to-exit - valid-default-team]} (cnit/get-valid-teams cfg org-id profile-id default-team-id)] + valid-default-team]} (cnit/get-valid-teams cfg organization-id profile-id default-team-id)] (when-not valid-default-team (ex/raise :type :validation :code :not-valid-teams)) diff --git a/backend/src/app/rpc/notifications.clj b/backend/src/app/rpc/notifications.clj index 05f69f9781..3bdb2c8a3b 100644 --- a/backend/src/app/rpc/notifications.clj +++ b/backend/src/app/rpc/notifications.clj @@ -24,7 +24,7 @@ :notification notification}))) -(defn notify-user-removed-from-org +(defn notify-user-org-change [cfg profile-id organization-id organization-name notification] (let [msgbus (::mbus/msgbus cfg)] (mbus/pub! msgbus diff --git a/backend/test/backend_tests/rpc_management_nitrate_test.clj b/backend/test/backend_tests/rpc_management_nitrate_test.clj index 86a6d2ecb7..0e85b2d312 100644 --- a/backend/test/backend_tests/rpc_management_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_management_nitrate_test.clj @@ -162,8 +162,8 @@ extra-team (th/create-team* 1 {:profile-id (:id profile)}) default-team (th/db-get :team {:id (:default-team-id profile)}) teams [(:id default-team) (:id extra-team)] - org-name "Acme / Design" - expected-start (str "[" (d/sanitize-string org-name) "] ") + organization-name "Acme / Design" + expected-start (str "[" (d/sanitize-string organization-name) "] ") calls (atom []) out (with-redefs [mbus/pub! (fn [_cfg & {:keys [topic message]}] (swap! calls conj {:topic topic @@ -171,7 +171,7 @@ (management-command-with-nitrate! {::th/type :notify-org-deletion ::rpc/profile-id (:id profile) :teams teams - :org-name org-name})) + :organization-name organization-name})) updated (map #(th/db-get :team {:id %} {::db/remove-deleted false}) teams)] (t/is (th/success? out)) (t/is (= 2 (count @calls))) @@ -181,7 +181,7 @@ (doseq [call @calls] (t/is (= uuid/zero (:topic call))) (t/is (= :team-org-change (-> call :message :type))) - (t/is (= org-name (-> call :message :organization-name))) + (t/is (= organization-name (-> call :message :organization-name))) (t/is (= "dashboard.org-deleted" (-> call :message :notification)))))) (t/deftest get-profile-by-email-success-and-not-found @@ -225,10 +225,10 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- make-org-summary - [& {:keys [org-id org-name owner-id your-penpot-teams org-teams] + [& {:keys [organization-id organization-name owner-id your-penpot-teams org-teams] :or {your-penpot-teams [] org-teams []}}] - {:id org-id - :name org-name + {:id organization-id + :name organization-name :owner-id owner-id :teams (into (mapv (fn [id] {:id id :is-your-penpot true}) your-penpot-teams) @@ -254,10 +254,10 @@ :team-id (:id org-team)}) _ (th/create-file* 1 {:profile-id (:id user) :project-id (:id project)}) - org-id (uuid/random) + organization-id (uuid/random) org-summary (make-org-summary - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :owner-id (:id org-owner) :your-penpot-teams [(:id org-team)] :org-teams []) @@ -269,8 +269,8 @@ {::th/type :remove-from-org ::rpc/profile-id (:id org-owner) :profile-id (:id user) - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :default-team-id (:id org-team)}))] (t/is (th/success? out)) (t/is (nil? (:result out))) @@ -285,7 +285,7 @@ (let [msg (-> @calls first :message)] (t/is (= :user-org-change (:type msg))) (t/is (= (:id user) (:topic msg))) - (t/is (= org-id (:organization-id msg))) + (t/is (= organization-id (:organization-id msg))) (t/is (= "Acme Org" (:organization-name msg))) (t/is (= "dashboard.user-no-longer-belong-org" (:notification msg)))))) @@ -294,10 +294,10 @@ (let [org-owner (th/create-profile* 1 {:is-active true}) user (th/create-profile* 2 {:is-active true}) org-team (th/create-team* 2 {:profile-id (:id user)}) - org-id (uuid/random) + organization-id (uuid/random) org-summary (make-org-summary - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :owner-id (:id org-owner) :your-penpot-teams [(:id org-team)] :org-teams []) @@ -307,8 +307,8 @@ {::th/type :remove-from-org ::rpc/profile-id (:id org-owner) :profile-id (:id user) - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :default-team-id (:id org-team)}))] (t/is (th/success? out)) (let [team (th/db-get :team {:id (:id org-team)} {::db/remove-deleted false})] @@ -320,10 +320,10 @@ user (th/create-profile* 2 {:is-active true}) extra-team (th/create-team* 3 {:profile-id (:id user)}) org-team (th/create-team* 99 {:profile-id (:id user)}) - org-id (uuid/random) + organization-id (uuid/random) org-summary (make-org-summary - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :owner-id (:id org-owner) :your-penpot-teams [(:id org-team)] :org-teams [(:id extra-team)]) @@ -333,8 +333,8 @@ {::th/type :remove-from-org ::rpc/profile-id (:id org-owner) :profile-id (:id user) - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :default-team-id (:id org-team)}))] (t/is (th/success? out)) (let [team (th/db-get :team {:id (:id extra-team)} {::db/remove-deleted false})] @@ -351,10 +351,10 @@ :profile-id (:id candidate) :role :editor}) org-team (th/create-team* 99 {:profile-id (:id user)}) - org-id (uuid/random) + organization-id (uuid/random) org-summary (make-org-summary - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :owner-id (:id org-owner) :your-penpot-teams [(:id org-team)] :org-teams [(:id extra-team)]) @@ -364,8 +364,8 @@ {::th/type :remove-from-org ::rpc/profile-id (:id org-owner) :profile-id (:id user) - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :default-team-id (:id org-team)}))] (t/is (th/success? out)) ;; user no longer in extra-team @@ -384,10 +384,10 @@ :profile-id (:id user) :role :editor}) org-team (th/create-team* 99 {:profile-id (:id user)}) - org-id (uuid/random) + organization-id (uuid/random) org-summary (make-org-summary - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :owner-id (:id org-owner) :your-penpot-teams [(:id org-team)] :org-teams [(:id extra-team)]) @@ -397,8 +397,8 @@ {::th/type :remove-from-org ::rpc/profile-id (:id org-owner) :profile-id (:id user) - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :default-team-id (:id org-team)}))] (t/is (th/success? out)) ;; user no longer a member of extra-team @@ -422,10 +422,10 @@ {:is-owner true :is-admin false} {:team-id (:id extra-team) :profile-id (:id other-owner)}) org-team (th/create-team* 99 {:profile-id (:id user)}) - org-id (uuid/random) + organization-id (uuid/random) org-summary (make-org-summary - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :owner-id (:id other-owner) :your-penpot-teams [(:id org-team)] :org-teams [(:id extra-team)]) @@ -435,8 +435,8 @@ {::th/type :remove-from-org ::rpc/profile-id (:id other-owner) :profile-id (:id user) - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :default-team-id (:id org-team)}))] (t/is (not (th/success? out))) (t/is (= :validation (th/ex-type (:error out)))) @@ -450,10 +450,10 @@ (let [org-owner (th/create-profile* 1 {:is-active true}) user (th/create-profile* 2 {:is-active true}) org-team (th/create-team* 1 {:profile-id (:id user)}) - org-id (uuid/random) + organization-id (uuid/random) org-summary (make-org-summary - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :owner-id (:id org-owner) :your-penpot-teams [(:id org-team)] :org-teams []) @@ -462,7 +462,7 @@ {::th/type :get-remove-from-org-summary ::rpc/profile-id (:id org-owner) :profile-id (:id user) - :org-id org-id + :organization-id organization-id :default-team-id (:id org-team)}))] (t/is (th/success? out)) (t/is (= {:teams-to-delete 0 @@ -476,10 +476,10 @@ user (th/create-profile* 2 {:is-active true}) extra-team (th/create-team* 3 {:profile-id (:id user)}) org-team (th/create-team* 99 {:profile-id (:id user)}) - org-id (uuid/random) + organization-id (uuid/random) org-summary (make-org-summary - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :owner-id (:id org-owner) :your-penpot-teams [(:id org-team)] :org-teams [(:id extra-team)]) @@ -488,7 +488,7 @@ {::th/type :get-remove-from-org-summary ::rpc/profile-id (:id org-owner) :profile-id (:id user) - :org-id org-id + :organization-id organization-id :default-team-id (:id org-team)}))] (t/is (th/success? out)) (t/is (= {:teams-to-delete 1 @@ -506,10 +506,10 @@ :profile-id (:id candidate) :role :editor}) org-team (th/create-team* 99 {:profile-id (:id user)}) - org-id (uuid/random) + organization-id (uuid/random) org-summary (make-org-summary - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :owner-id (:id org-owner) :your-penpot-teams [(:id org-team)] :org-teams [(:id extra-team)]) @@ -518,7 +518,7 @@ {::th/type :get-remove-from-org-summary ::rpc/profile-id (:id org-owner) :profile-id (:id user) - :org-id org-id + :organization-id organization-id :default-team-id (:id org-team)}))] (t/is (th/success? out)) (t/is (= {:teams-to-delete 0 @@ -535,10 +535,10 @@ :profile-id (:id user) :role :editor}) org-team (th/create-team* 99 {:profile-id (:id user)}) - org-id (uuid/random) + organization-id (uuid/random) org-summary (make-org-summary - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :owner-id (:id org-owner) :your-penpot-teams [(:id org-team)] :org-teams [(:id extra-team)]) @@ -547,7 +547,7 @@ {::th/type :get-remove-from-org-summary ::rpc/profile-id (:id org-owner) :profile-id (:id user) - :org-id org-id + :organization-id organization-id :default-team-id (:id org-team)}))] (t/is (th/success? out)) (t/is (= {:teams-to-delete 0 @@ -561,10 +561,10 @@ user (th/create-profile* 2 {:is-active true}) extra-team (th/create-team* 6 {:profile-id (:id user)}) org-team (th/create-team* 99 {:profile-id (:id user)}) - org-id (uuid/random) + organization-id (uuid/random) org-summary (make-org-summary - :org-id org-id - :org-name "Acme Org" + :organization-id organization-id + :organization-name "Acme Org" :owner-id (:id org-owner) :your-penpot-teams [(:id org-team)] :org-teams [(:id extra-team)]) @@ -573,7 +573,7 @@ {::th/type :get-remove-from-org-summary ::rpc/profile-id (:id org-owner) :profile-id (:id user) - :org-id org-id + :organization-id organization-id :default-team-id (:id org-team)}))] ;; Both teams must still exist and be undeleted (let [t1 (th/db-get :team {:id (:id org-team)})] diff --git a/backend/test/backend_tests/rpc_nitrate_test.clj b/backend/test/backend_tests/rpc_nitrate_test.clj index dd8dd20ae2..d8f4142a60 100644 --- a/backend/test/backend_tests/rpc_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_nitrate_test.clj @@ -23,10 +23,10 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- make-org-summary - [& {:keys [org-id org-name owner-id your-penpot-teams org-teams] + [& {:keys [organization-id organization-name owner-id your-penpot-teams org-teams] :or {your-penpot-teams [] org-teams []}}] - {:id org-id - :name org-name + {:id organization-id + :name organization-name :owner-id owner-id :teams (into (mapv (fn [id] {:id id :is-your-penpot true}) your-penpot-teams) @@ -58,13 +58,13 @@ _ (th/create-file* 99 {:profile-id (:id profile-user) :project-id (:id project)}) - org-id (uuid/random) + organization-id (uuid/random) ;; The user's personal penpot team in the org context your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [])] @@ -72,8 +72,8 @@ (with-redefs [nitrate/call (nitrate-call-mock org-summary)] (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave []} @@ -94,12 +94,12 @@ profile-user (th/create-profile* 2 {:is-active true}) org-default-team (th/create-team* 98 {:profile-id (:id profile-user)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [])] @@ -107,8 +107,8 @@ (with-redefs [nitrate/call (nitrate-call-mock org-summary)] (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave []} @@ -129,12 +129,12 @@ _ (th/create-file* 97 {:profile-id (:id profile-user) :project-id (:id project)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [])] @@ -142,8 +142,8 @@ (with-redefs [nitrate/call (nitrate-call-mock org-summary)] (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave []} @@ -164,12 +164,12 @@ team1 (th/create-team* 1 {:profile-id (:id profile-user)}) org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [(:id team1)])] @@ -177,8 +177,8 @@ (with-redefs [nitrate/call (nitrate-call-mock org-summary)] (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [(:id team1)] :teams-to-leave []} @@ -201,12 +201,12 @@ :role :editor}) org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [(:id team1)])] @@ -214,8 +214,8 @@ (with-redefs [nitrate/call (nitrate-call-mock org-summary)] (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave [{:id (:id team1) :reassign-to (:id profile-owner)}]} @@ -246,12 +246,12 @@ :role :editor}) org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [(:id team1)])] @@ -259,8 +259,8 @@ (with-redefs [nitrate/call (nitrate-call-mock org-summary)] (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave [{:id (:id team1)}]} @@ -282,13 +282,13 @@ (t/deftest leave-org-error-org-owner-cannot-leave (let [profile-owner (th/create-profile* 1 {:is-active true}) org-default-team (th/create-team* 99 {:profile-id (:id profile-owner)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) ;; profile-owner IS the org owner in the org-summary org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [])] @@ -296,8 +296,8 @@ (with-redefs [nitrate/call (nitrate-call-mock org-summary)] (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-owner) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave []} @@ -311,12 +311,12 @@ (let [profile-owner (th/create-profile* 1 {:is-active true}) profile-user (th/create-profile* 2 {:is-active true}) org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [])] @@ -325,8 +325,8 @@ ;; Pass a random UUID that is not in the your-penpot-teams list (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id (uuid/random) :teams-to-delete [] :teams-to-leave []} @@ -435,12 +435,12 @@ :role :editor}) org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [(:id team1) (:id team2) (:id team3)])] @@ -448,8 +448,8 @@ (with-redefs [nitrate/call (nitrate-call-mock org-summary)] (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [(:id team1)] :teams-to-leave [{:id (:id team2) :reassign-to (:id profile-owner)} @@ -485,12 +485,12 @@ team2 (th/create-team* 2 {:profile-id (:id profile-user)}) org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [(:id team1) (:id team2)])] @@ -499,8 +499,8 @@ ;; Only team1 is listed; team2 is also a sole-owner team and must be included (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [(:id team1)] :teams-to-leave []} @@ -520,12 +520,12 @@ :role :editor}) org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [(:id team1)])] @@ -534,8 +534,8 @@ ;; team1 has 2 members so it is not a valid deletion candidate (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [(:id team1)] :teams-to-leave []} @@ -555,12 +555,12 @@ :role :editor}) org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [(:id team1)])] @@ -569,8 +569,8 @@ ;; team1 must be transferred (owner + multiple members) but is absent (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave []} @@ -589,12 +589,12 @@ :role :editor}) org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [(:id team1)])] @@ -603,8 +603,8 @@ ;; reassign-to points to the profile that is leaving — not allowed (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave [{:id (:id team1) :reassign-to (:id profile-user)}]} @@ -625,12 +625,12 @@ :role :editor}) org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [(:id team1)])] @@ -639,8 +639,8 @@ ;; profile-other is not a member of team1 (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave [{:id (:id team1) :reassign-to (:id profile-other)}]} @@ -660,12 +660,12 @@ :role :editor}) org-default-team (th/create-team* 99 {:profile-id (:id profile-user)}) - org-id (uuid/random) + organization-id (uuid/random) your-penpot-id (:id org-default-team) org-summary (make-org-summary - :org-id org-id - :org-name "Test Org" + :organization-id organization-id + :organization-name "Test Org" :owner-id (:id profile-owner) :your-penpot-teams [your-penpot-id] :org-teams [(:id team1)])] @@ -674,8 +674,8 @@ ;; profile-user is not the owner so providing reassign-to is invalid (let [data {::th/type :leave-org ::rpc/profile-id (:id profile-user) - :org-id org-id - :org-name "Test Org" + :id organization-id + :name "Test Org" :default-team-id your-penpot-id :teams-to-delete [] :teams-to-leave [{:id (:id team1) :reassign-to (:id profile-owner)}]} diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index 68bc7e8151..dd2619d37e 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -91,14 +91,14 @@ (dm/get-in profile [:subscription :status])))) (defn leave-org - [{:keys [id org-name default-team-id teams-to-delete teams-to-leave on-error] :as params}] + [{:keys [id name default-team-id teams-to-delete teams-to-leave on-error] :as params}] (ptk/reify ::leave-org ptk/WatchEvent (watch [_ state _] (let [profile-team-id (dm/get-in state [:profile :default-team-id])] - (->> (rp/cmd! ::leave-org {:org-id id - :org-name org-name + (->> (rp/cmd! ::leave-org {:id id + :name name :default-team-id default-team-id :teams-to-delete teams-to-delete :teams-to-leave teams-to-leave}) @@ -108,7 +108,7 @@ (dt/fetch-teams) (dcm/go-to-dashboard-recent :team-id profile-team-id) (modal/hide) - (ntf/show {:content (tr "dasboard.leave-org.toast" org-name) + (ntf/show {:content (tr "dasboard.leave-org.toast" name) :type :toast :level :success})))) (rx/catch on-error)))))) diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs index 3ae5cb8fc9..6ac2a1b60f 100644 --- a/frontend/src/app/main/ui/auth/verify_token.cljs +++ b/frontend/src/app/main/ui/auth/verify_token.cljs @@ -43,14 +43,14 @@ (st/emit! (da/login-from-token tdata))) (defmethod handle-token :team-invitation - [{:keys [state team-id org-team-id org-name invitation-token] :as tdata}] + [{:keys [state team-id org-team-id organization-name invitation-token] :as tdata}] (case state :created (if org-team-id (st/emit! (du/refresh-profile) (dcm/go-to-dashboard-recent :team-id org-team-id) - (ntf/success (tr "auth.notifications.org-invitation-accepted" org-name))) + (ntf/success (tr "auth.notifications.org-invitation-accepted" organization-name))) (st/emit! (du/refresh-profile) (dcm/go-to-dashboard-recent :team-id team-id) diff --git a/frontend/src/app/main/ui/dashboard/change_owner.scss b/frontend/src/app/main/ui/dashboard/change_owner.scss index fe20d59db6..13f5968eb8 100644 --- a/frontend/src/app/main/ui/dashboard/change_owner.scss +++ b/frontend/src/app/main/ui/dashboard/change_owner.scss @@ -7,9 +7,12 @@ @use "refactor/common-refactor.scss" as deprecated; @use "ds/typography.scss" as t; @use "ds/_sizes.scss" as *; +@use "ds/z-index.scss" as *; .modal-overlay { @extend %modal-overlay-base; + + z-index: var(--z-index-notifications); } .modal-container { diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 3ae14e1281..d1c055bedf 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -640,7 +640,9 @@ (concat teams-to-transfer)) teams-to-delete (map :id teams-to-delete)] + (st/emit! (dnt/leave-org {:id (:id organization) + :name (:name organization) :default-team-id default-team-id :teams-to-delete teams-to-delete :teams-to-leave teams-to-leave @@ -649,32 +651,33 @@ on-leave-clicked (mf/use-fn (mf/deps leave-fn profile organization teams-to-transfer num-teams-to-leave num-teams-to-delete num-teams-to-transfer) - (cond - (and (pos? num-teams-to-delete) - (zero? num-teams-to-transfer)) - #(st/emit! (modal/show - {:type :confirm - :title (tr "modals.before-leave-org.title" (:name organization)) - :message (tr "modals.before-leave-org.message") - :accept-label (tr "modals.leave-org-confirm.accept") - :on-accept leave-fn - :error-msg (tr "modals.before-leave-org.warning")})) - (pos? num-teams-to-transfer) - #(st/emit! - (modal/show - {:type :leave-and-reassign-org - :profile profile - :teams-to-transfer teams-to-transfer - :num-teams-to-delete num-teams-to-delete - :accept leave-fn})) + (fn [] + (cond + (and (pos? num-teams-to-delete) + (zero? num-teams-to-transfer)) + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.before-leave-org.title" (:name organization)) + :message (tr "modals.before-leave-org.message") + :accept-label (tr "modals.leave-org-confirm.accept") + :on-accept leave-fn + :error-msg (tr "modals.before-leave-org.warning")})) + (pos? num-teams-to-transfer) + (st/emit! + (modal/show + {:type :leave-and-reassign-org + :profile profile + :teams-to-transfer teams-to-transfer + :num-teams-to-delete num-teams-to-delete + :accept leave-fn})) - :else - #(st/emit! (modal/show - {:type :confirm - :title (tr "modals.leave-org-confirm.title" (:name organization)) - :message (tr "modals.leave-org-confirm.message") - :accept-label (tr "modals.leave-org-confirm.accept") - :on-accept leave-fn}))))] + :else + (st/emit! (modal/show + {:type :confirm + :title (tr "modals.leave-org-confirm.title" (:name organization)) + :message (tr "modals.leave-org-confirm.message") + :accept-label (tr "modals.leave-org-confirm.accept") + :on-accept leave-fn})))))] (mf/use-effect (fn [] ;; We need all the team members of the owned teams @@ -817,10 +820,10 @@ (mf/defc sidebar-team-switch* [{:keys [team profile]}] (let [nitrate? (contains? cf/flags :nitrate) - org-id (when nitrate? (:organization-id team)) + organization-id (when nitrate? (:organization-id team)) teams (cond->> (mf/deref refs/teams) nitrate? - (filter #(= (-> % val :organization-id) org-id)) + (filter #(= (-> % val :organization-id) organization-id)) nitrate? (into {})) From 534701f04f34b7568b556a17376f85aa6bb5ac1f Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Tue, 21 Apr 2026 16:49:07 +0200 Subject: [PATCH 194/288] :bug: Fix org options space should be hidden when there are no options --- frontend/src/app/main/ui/dashboard/sidebar.cljs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index d1c055bedf..9663111d98 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -782,9 +782,8 @@ [:span {:class (stl/css :team-text)} (:name current-org)]])] arrow-icon] - (if (or default-org? - (= (:id profile) (:owner-id current-org))) - [:div {:class (stl/css :org-options)}] + (when-not (or default-org? + (= (:id profile) (:owner-id current-org))) [:> button* {:variant "ghost" :type "button" :class (stl/css :org-options-btn) From b67394199b8cede0145ccd4751aa82ed85e8a4f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Wed, 22 Apr 2026 09:37:09 +0200 Subject: [PATCH 195/288] :sparkles: Add the ability to upload organization profile image * :sparkles: Upload org logo * :paperclip: Code review * :paperclip: Code review 2 * :paperclip: Code review 3 --- backend/src/app/nitrate.clj | 5 +++- backend/src/app/rpc/management/nitrate.clj | 33 ++++++++++++++++++++++ backend/src/app/storage.clj | 1 + backend/src/app/storage/gc_touched.clj | 1 + 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index afb1f78775..d096a4e9d2 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -115,7 +115,8 @@ [:slug ::sm/text] [:is-your-penpot :boolean] [:owner-id ::sm/uuid] - [:avatar-bg-url [::sm/text]]]) + [:avatar-bg-url [::sm/text]] + [:logo-id {:optional true} [:maybe ::sm/uuid]]]) (def ^:private schema:org-summary [:map @@ -393,6 +394,8 @@ :organization-slug (:slug org) :organization-owner-id (:owner-id org) :organization-avatar-bg-url (:avatar-bg-url org) + :organization-custom-photo (when-let [logo-id (:logo-id org)] + (str (cf/get :public-uri) "/assets/by-id/" logo-id)) :is-default (or (:is-default team) (true? (:is-your-penpot org)))) team)) (catch Throwable cause diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index f68787fb1c..539adb8fbf 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -15,6 +15,7 @@ [app.common.types.team :refer [schema:team]] [app.config :as cf] [app.db :as db] + [app.media :as media] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] [app.rpc.commands.nitrate :as cnit] @@ -23,6 +24,7 @@ [app.rpc.commands.teams-invitations :as ti] [app.rpc.doc :as doc] [app.rpc.notifications :as notifications] + [app.storage :as sto] [app.util.services :as sv])) @@ -81,6 +83,37 @@ (->> (db/exec! cfg [sql:get-teams current-user-id]) (map #(select-keys % [:id :name]))))) +;; ---- API: upload-org-logo + +(def ^:private schema:upload-org-logo + [:map + [:content media/schema:upload] + [:organization-id ::sm/uuid] + [:previous-id {:optional true} ::sm/uuid]]) + +(def ^:private schema:upload-org-logo-result + [:map [:id ::sm/uuid]]) + +(sv/defmethod ::upload-org-logo + "Store an organization logo in penpot storage and return its ID. + Accepts an optional previous-id to mark the old logo for garbage + collection when replacing an existing one." + {::doc/added "2.16" + ::sm/params schema:upload-org-logo + ::sm/result schema:upload-org-logo-result} + [{:keys [::sto/storage]} {:keys [content organization-id previous-id]}] + (when previous-id + (sto/touch-object! storage previous-id)) + (let [hash (sto/calculate-hash (:path content)) + data (-> (sto/content (:path content)) + (sto/wrap-with-hash hash)) + obj (sto/put-object! storage {::sto/content data + ::sto/deduplicate? true + :bucket "organization" + :content-type (:mtype content) + :organization-id organization-id})] + {:id (:id obj)})) + ;; ---- API: notify-team-change (def ^:private schema:notify-team-change diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj index 0fe48c2911..dc28a9e802 100644 --- a/backend/src/app/storage.clj +++ b/backend/src/app/storage.clj @@ -44,6 +44,7 @@ "file-object-thumbnail" "file-thumbnail" "profile" + "organization" "tempfile" "file-data" "file-data-fragment" diff --git a/backend/src/app/storage/gc_touched.clj b/backend/src/app/storage/gc_touched.clj index f00140d04e..971a5afd1d 100644 --- a/backend/src/app/storage/gc_touched.clj +++ b/backend/src/app/storage/gc_touched.clj @@ -166,6 +166,7 @@ "profile" (process-objects! conn has-profile-refs? bucket objects) "file-data" (process-objects! conn has-file-data-refs? bucket objects) "tempfile" (process-objects! conn (constantly false) bucket objects) + "organization" (process-objects! conn (constantly false) bucket objects) (ex/raise :type :internal :code :unexpected-unknown-reference :hint (dm/fmt "unknown reference '%'" bucket)))) From c02f0a2bc99a76bc8800dc4abe77e609791669df Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 22 Apr 2026 11:09:44 +0200 Subject: [PATCH 196/288] :bug: Fix grow options (#9097) * :bug: Fix grow options * :bug: Fix props when hidden is nil --- .../app/main/ui/workspace/sidebar/options/menus/text.cljs | 2 +- .../main/ui/workspace/sidebar/options/rows/stroke_row.cljs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index c1d9424574..0c388627e0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -504,7 +504,7 @@ [:div {:class (stl/css :text-align-options)} [:> text-align-options* common-props] - [:> grow-options* common-props] + [:> grow-options* (mf/spread-props common-props {:ids ids})] [:> icon-button* {:variant "ghost" :aria-label (tr "labels.options") :data-testid "text-align-options-button" diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index d1c0a80497..723a658466 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -51,6 +51,7 @@ ids]}] (let [hidden? (:hidden stroke) + hidden? (if (nil? hidden?) false hidden?) token-numeric-inputs (features/use-feature "tokens/numeric-input") @@ -252,7 +253,7 @@ :options stroke-alignment-options :variant "icon-only" :data-testid "stroke.alignment" - :disabled hidden? + :disabled (if (= :multiple hidden?) true hidden?) :wrapper-class (stl/css :stroke-align-icon-select) :on-change on-alignment-change}] @@ -262,7 +263,7 @@ :wrapper-class (stl/css :stroke-style-icon-select) :data-testid "stroke.style" :variant "icon-only" - :disabled hidden? + :disabled (if (= :multiple hidden?) true hidden?) :dropdown-alignment :right :on-change on-style-change}])] From 09fca1c8201bd6352ad202dc6bc7973a4888f956 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 22 Apr 2026 11:33:43 +0200 Subject: [PATCH 197/288] :bug: Fix tooltip appearing two times when nested elements (#9031) --- .../src/app/main/ui/ds/tooltip/tooltip.cljs | 78 ++++++++++++++++--- 1 file changed, 69 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs index 36358fd80f..10e9638cf2 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs @@ -17,8 +17,52 @@ (def ^:private ^:const overlay-offset 32) +;; Global state for tooltip coordination (defonce active-tooltip (atom nil)) +;; Registry of visible tooltips to detect nested tooltips +;; Map: tooltip-id -> trigger-element +(defonce ^:private tooltip-registry (atom {})) + +;; Track tooltips that are "about to show" - used to prevent race conditions +;; when both parent and child schedule their show at the same time. +;; Map: tooltip-id -> trigger-element +(defonce ^:private pending-tooltips (atom {})) + +(defn- mark-pending + "Mark a tooltip as pending (scheduled to show soon). + Used to detect potential nested tooltips during race condition window." + [tooltip-id trigger-el] + (swap! pending-tooltips assoc tooltip-id trigger-el)) + +(defn- clear-pending + "Clear the pending state (tooltip showed or cancelled)." + [tooltip-id] + (swap! pending-tooltips dissoc tooltip-id)) + +(defn- register-tooltip + "Register this tooltip in the global registry when it becomes visible. + Used to detect nested tooltips." + [tooltip-id trigger-el] + (swap! tooltip-registry assoc tooltip-id trigger-el) + (clear-pending tooltip-id)) + +(defn- unregister-tooltip + "Unregister this tooltip from the global registry when it hides." + [tooltip-id] + (swap! tooltip-registry dissoc tooltip-id) + (clear-pending tooltip-id)) + +(defn- has-descendant-tooltip? + "Check if there's a registered or pending tooltip that is a descendant of trigger-el. + If so, we should NOT show the parent tooltip." + [trigger-el] + (let [all-tooltips (merge @tooltip-registry @pending-tooltips)] + (some (fn [[_ entry-el]] + (when (some? entry-el) + (dom/child? entry-el trigger-el))) + all-tooltips))) + (defn- clear-schedule [ref] (when-let [schedule (mf/ref-val ref)] @@ -191,15 +235,27 @@ (when-not (.-hidden js/document) (let [trigger-el (mf/ref-val trigger-ref)] (clear-schedule schedule-ref) - (add-schedule schedule-ref (d/nilv delay 300) - (fn [] - (when-let [active @active-tooltip] - (when (not= (:id active) tooltip-id) - (when-let [tooltip-el (dom/get-element (:id active))] - (dom/set-css-property! tooltip-el "display" "none")) - (reset! active-tooltip nil))) - (reset! active-tooltip {:id tooltip-id :trigger trigger-el}) - (reset! visible* true))))))) + + ;; Check if there's a registered or pending tooltip that is a descendant of our trigger. + ;; If so, skip showing this tooltip and let the innermost one show instead. + (when-not (has-descendant-tooltip? trigger-el) + ;; Mark as pending BEFORE scheduling (helps prevent race conditions) + (mark-pending tooltip-id trigger-el) + + (add-schedule schedule-ref (d/nilv delay 300) + (fn [] + ;; Double-check: don't show if another tooltip is now visible + (when-let [active @active-tooltip] + (when (not= (:id active) tooltip-id) + (when-let [tooltip-el (dom/get-element (:id active))] + (dom/set-css-property! tooltip-el "display" "none")) + (reset! active-tooltip nil))) + + ;; Register this tooltip as visible + (register-tooltip tooltip-id trigger-el) + + (reset! active-tooltip {:id tooltip-id :trigger trigger-el}) + (reset! visible* true)))))))) on-show-focus (mf/use-fn @@ -215,6 +271,10 @@ (fn [] (clear-schedule schedule-ref) (reset! visible* false) + + ;; Unregister from the global registry + (unregister-tooltip tooltip-id) + (when (= (:id @active-tooltip) tooltip-id) (reset! active-tooltip nil)))) From 2579527e646328c2564ae9255c3170640e4643a6 Mon Sep 17 00:00:00 2001 From: Edwin Rivera <58448072+edwin-rivera-dev@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:49:39 +0000 Subject: [PATCH 198/288] :tada: Add get-file-stats RPC command (#9074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :tada: Add get-file-stats RPC command Introduce a new lightweight RPC query that returns aggregate statistics for a single file: page count, shape counts by type, component/color/ typography counts, and inbound and outbound library reference counts. Mirrors the existing get-file-summary permission and decoding pattern. Useful for plugin authors enforcing per-file budgets, the @penpot/library npm SDK, and future admin dashboards. Purely additive — no migrations, no UI, no breaking changes. Signed-off-by: edwin-rivera-dev * :bug: Bind *load-fn* around file data walk in get-file-stats The binding previously wrapped only — a plain key lookup that does not realize any pointers — so by the time walked and accessed on each page, was unbound and every PointerMap dereference threw , failing the three new tests. Move inside the form so the walk runs with available, matching the existing pattern used in . Signed-off-by: Edwin Rivera --------- Signed-off-by: edwin-rivera-dev Signed-off-by: Edwin Rivera --- backend/src/app/rpc/commands/files.clj | 71 ++++++++++++++++ backend/test/backend_tests/rpc_file_test.clj | 89 ++++++++++++++++++++ common/src/app/common/files/stats.cljc | 74 ++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 common/src/app/common/files/stats.cljc diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 69c36a0e44..dd81c5c882 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -13,6 +13,7 @@ [app.common.features :as cfeat] [app.common.files.helpers :as cfh] [app.common.files.migrations :as fmg] + [app.common.files.stats :as cfs] [app.common.logging :as l] [app.common.schema :as sm] [app.common.schema.desc-js-like :as-alias smdj] @@ -606,6 +607,76 @@ (get-file-summary cfg id)) +;; --- COMMAND QUERY: get-file-stats + +(def ^:private sql:file-stats-library-counts + "SELECT + (SELECT COUNT(*) + FROM file_library_rel AS flr + JOIN file AS fl ON (fl.id = flr.library_file_id) + WHERE flr.file_id = ?::uuid + AND (fl.deleted_at IS NULL OR fl.deleted_at > now())) AS library_count, + (SELECT COUNT(*) + FROM file_library_rel AS flr + JOIN file AS fl ON (fl.id = flr.file_id) + WHERE flr.library_file_id = ?::uuid + AND (fl.deleted_at IS NULL OR fl.deleted_at > now())) AS referenced_by_count") + +(defn- get-file-stats-library-counts + [conn file-id] + (let [row (db/exec-one! conn [sql:file-stats-library-counts file-id file-id])] + {:library-count (or (:library-count row) 0) + :referenced-by-count (or (:referenced-by-count row) 0)})) + +(defn- get-file-stats + [{:keys [::db/conn] :as cfg} file-id] + (let [file (bfc/get-file cfg file-id) + base (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)] + (cfs/calc-file-stats (:data file))) + lib-cnt (get-file-stats-library-counts conn file-id)] + (-> base + (merge lib-cnt) + (assoc :file-id file-id + :revn (:revn file) + :updated-at (:modified-at file))))) + +(def ^:private schema:shape-counts + [:map {:title "FileStatsShapeCounts"} + [:total [::sm/int {:min 0}]] + [:by-type [:map-of :keyword [::sm/int {:min 0}]]]]) + +(def ^:private schema:get-file-stats-result + [:map {:title "FileStats"} + [:file-id ::sm/uuid] + [:page-count [::sm/int {:min 0}]] + [:shape-counts schema:shape-counts] + [:component-count [::sm/int {:min 0}]] + [:deleted-component-count [::sm/int {:min 0}]] + [:color-count [::sm/int {:min 0}]] + [:typography-count [::sm/int {:min 0}]] + [:library-count [::sm/int {:min 0}]] + [:referenced-by-count [::sm/int {:min 0}]] + [:revn [::sm/int {:min 0}]] + [:updated-at ::ct/inst]]) + +(def ^:private schema:get-file-stats + [:map {:title "get-file-stats"} + [:id ::sm/uuid]]) + +(sv/defmethod ::get-file-stats + "Return aggregate statistics for a single file: page count, shape + counts by type, component/color/typography counts, and inbound and + outbound library reference counts. Cheap alternative to `get-file` + when only metrics are needed." + {::doc/added "2.16" + ::sm/params schema:get-file-stats + ::sm/result schema:get-file-stats-result + ::db/transaction true} + [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id]}] + (check-read-permissions! conn profile-id id) + (get-file-stats cfg id)) + + ;; --- COMMAND QUERY: get-file-libraries (def ^:private schema:get-file-libraries diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index 281c834256..d45dec0453 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -2121,3 +2121,92 @@ (t/is (= 1 (count rows))) (t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z")) (t/is (nil? (:deleted-at row1)))))))) + +(t/deftest get-file-stats-empty-file + (let [profile (th/create-profile* 1 {:is-active true}) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + out (th/command! {::th/type :get-file-stats + ::rpc/profile-id (:id profile) + :id (:id file)})] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (= (:id file) (:file-id result))) + (t/is (pos? (:page-count result))) + (t/is (zero? (:component-count result))) + (t/is (zero? (:deleted-component-count result))) + (t/is (zero? (:color-count result))) + (t/is (zero? (:typography-count result))) + (t/is (zero? (:library-count result))) + (t/is (zero? (:referenced-by-count result))) + (t/is (contains? result :shape-counts)) + (t/is (zero? (get-in result [:shape-counts :total]))) + (t/is (= {} (get-in result [:shape-counts :by-type])))))) + +(t/deftest get-file-stats-with-shapes + (let [profile (th/create-profile* 1 {:is-active true}) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + page-id (-> file :data :pages first) + rect-id (uuid/random) + frame-id (uuid/random)] + + (update-file! + :file-id (:id file) + :profile-id (:id profile) + :revn 0 + :vern 0 + :changes + [{:type :add-obj + :page-id page-id + :id frame-id + :parent-id uuid/zero + :frame-id uuid/zero + :components-v2 true + :obj (cts/setup-shape + {:id frame-id + :name "frame" + :frame-id uuid/zero + :parent-id uuid/zero + :type :frame})} + {:type :add-obj + :page-id page-id + :id rect-id + :parent-id frame-id + :frame-id frame-id + :components-v2 true + :obj (cts/setup-shape + {:id rect-id + :name "rect" + :frame-id frame-id + :parent-id frame-id + :type :rect})}]) + + (let [out (th/command! {::th/type :get-file-stats + ::rpc/profile-id (:id profile) + :id (:id file)}) + result (:result out)] + + (t/is (nil? (:error out))) + (t/is (= 2 (get-in result [:shape-counts :total]))) + (t/is (= 1 (get-in result [:shape-counts :by-type :rect]))) + (t/is (= 1 (get-in result [:shape-counts :by-type :frame])))))) + +(t/deftest get-file-stats-forbidden + (let [owner (th/create-profile* 1 {:is-active true}) + other (th/create-profile* 2 {:is-active true}) + file (th/create-file* 1 {:profile-id (:id owner) + :project-id (:default-project-id owner) + :is-shared false}) + out (th/command! {::th/type :get-file-stats + ::rpc/profile-id (:id other) + :id (:id file)})] + + (t/is (not (nil? (:error out)))) + (let [edata (-> out :error ex-data)] + (t/is (= :not-found (:type edata)))))) diff --git a/common/src/app/common/files/stats.cljc b/common/src/app/common/files/stats.cljc new file mode 100644 index 0000000000..99a2315243 --- /dev/null +++ b/common/src/app/common/files/stats.cljc @@ -0,0 +1,74 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.files.stats + "Pure helpers that compute aggregate statistics for a file data map. + + Given a decoded file data structure (the value stored under `:data` + on a file row), produces a small map with page/shape/library counts. + Intended to be cheap — a single pass over each page's `:objects` + map, no database access, no side effects." + (:require + [app.common.uuid :as uuid])) + +(def empty-shape-counts + {:total 0 :by-type {}}) + +(defn- inc-type + [by-type shape-type] + (if (nil? shape-type) + by-type + (update by-type shape-type (fnil inc 0)))) + +(defn count-shapes-by-type + "Walk an `:objects` map of a single page and return + `{:total N :by-type {:rect N :frame N ...}}`. The synthetic root + shape at `uuid/zero` is skipped so it never contributes to totals." + [objects] + (if (empty? objects) + empty-shape-counts + (reduce-kv + (fn [acc id shape] + (if (= id uuid/zero) + acc + (-> acc + (update :total inc) + (update :by-type inc-type (:type shape))))) + empty-shape-counts + objects))) + +(defn- merge-shape-counts + [a b] + {:total (+ (:total a) (:total b)) + :by-type (merge-with + (:by-type a) (:by-type b))}) + +(defn- aggregate-shape-counts + [pages-index] + (transduce + (map (comp count-shapes-by-type :objects)) + (completing merge-shape-counts) + empty-shape-counts + (vals pages-index))) + +(defn calc-file-stats + "Given a decoded file data map with the standard keys + `:pages-index`, `:components`, `:deleted-components`, `:colors` + and `:typographies`, return per-file aggregates. + + The result is a plain map suitable for serialization; it never + contains any pointer-map or objects-map instances." + [fdata] + (let [pages-index (get fdata :pages-index) + components (get fdata :components) + deleted-components (get fdata :deleted-components) + colors (get fdata :colors) + typographies (get fdata :typographies)] + {:page-count (count pages-index) + :shape-counts (aggregate-shape-counts pages-index) + :component-count (count components) + :deleted-component-count (count deleted-components) + :color-count (count colors) + :typography-count (count typographies)})) From b6487015b82d70736111a7696a582c9440a48777 Mon Sep 17 00:00:00 2001 From: moorsecopers99 <46223049+moorsecopers99@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:12:48 +0300 Subject: [PATCH 199/288] :sparkles: Add loader feedback while importing and exporting files (#9024) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: Add loader feedback while importing and exporting files Show a loader icon with a status label ("Importing files…" / "Exporting files…") in the import and export dialog footers while the operation is running, so users get clear in-progress feedback and cannot retrigger the action by mistake. Closes #9020 Signed-off-by: moorsecopers99 * :sparkles: Address import/export loader feedback PR review - Show the loader beside file names in the import dialog while files are being imported (previously queued entries kept showing the Penpot logo until each one moved into :import-progress). - Drop the loader from the "Importing files…" / "Exporting files…" footer status, leaving just the text styled with the modal title color, per the design proposal. Signed-off-by: moorsecopers99 * :sparkles: Match design proposal for import/export progress feedback - Move the in-progress label from the modal footer into the modal body, under the file rows, styled italic with the modal title color. - Rename the labels to match the design wording: "Uploading file…" for import and "Downloading file…" for export. - Restore the disabled "Accept" button in the import footer during the import-progress phase, mirroring the disabled "Close" button used by export. Signed-off-by: moorsecopers99 * :bug: Rename deprecated bodySmallTypography mixin to body-small-typography Signed-off-by: moorsecopers99 --------- Signed-off-by: moorsecopers99 Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + .../src/app/main/ui/dashboard/import.cljs | 14 +++++++-- .../src/app/main/ui/dashboard/import.scss | 7 +++++ frontend/src/app/main/ui/exports/files.cljs | 29 ++++++++++++------- frontend/src/app/main/ui/exports/files.scss | 7 +++++ frontend/translations/en.po | 8 +++++ 6 files changed, 52 insertions(+), 14 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f00188f3a1..58b0da4de5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ - Show alpha percentage next to library color values to distinguish colors that differ only in opacity (by @rockchris099) [Github #6328](https://github.com/penpot/penpot/issues/6328) - Add "Clear artboard guides" option to right-click context menu for frames (by @eureka0928) [Github #6987](https://github.com/penpot/penpot/issues/6987) +- Add loader feedback while importing and exporting files [Github #9020](https://github.com/penpot/penpot/issues/9020) - Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912) - Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248) - Import Tokens from linked library (by @dfelinto) [Github #8391](https://github.com/penpot/penpot/pull/8391) diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 1510af2455..6b4fe68678 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -195,13 +195,14 @@ {::mf/props :obj ::mf/memo true ::mf/private true} - [{:keys [entries entry edition can-be-deleted on-edit on-change on-delete]}] + [{:keys [entries entry edition can-be-deleted importing? on-edit on-change on-delete]}] (let [status (:status entry) ;; FIXME: rename to format format (:type entry) loading? (or (= :analyze status) - (= :import-progress status)) + (= :import-progress status) + (and importing? (= :import-ready status))) analyze-error? (= :analyze-error status) import-success? (= :import-success status) import-error? (= :import-error status) @@ -498,6 +499,7 @@ :key (dm/str (:uri entry) "/" (:file-id entry)) :entry entry :entries entries + :importing? (= :import-progress status) :on-edit on-edit :on-change on-entry-change :on-delete on-entry-delete @@ -505,7 +507,13 @@ (when (some? template) [:> import-entry* {:entry (assoc template :status status) - :can-be-deleted false}])] + :can-be-deleted false}]) + + (when (= :import-progress status) + [:div {:class (stl/css :status-message) + :role "status" + :aria-live "polite"} + (tr "labels.uploading-file")])] [:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :action-buttons)} diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss index b428e4b0d7..7d8d0ff428 100644 --- a/frontend/src/app/main/ui/dashboard/import.scss +++ b/frontend/src/app/main/ui/dashboard/import.scss @@ -43,6 +43,13 @@ min-height: 40px; } +.status-message { + @include deprecated.body-small-typography; + + color: var(--modal-title-foreground-color); + font-style: italic; +} + .action-buttons { @extend %modal-action-btns; } diff --git a/frontend/src/app/main/ui/exports/files.cljs b/frontend/src/app/main/ui/exports/files.cljs index 60f19737e5..ec937809ec 100644 --- a/frontend/src/app/main/ui/exports/files.cljs +++ b/frontend/src/app/main/ui/exports/files.cljs @@ -174,15 +174,22 @@ :on-click on-accept}]]]] (= status :exporting) - [:* - [:div {:class (stl/css :modal-content)} - (for [file (:files state)] - [:> export-entry* {:file file :key (dm/str (:id file))}])] + (let [in-progress? (->> state :files (some :loading))] + [:* + [:div {:class (stl/css :modal-content)} + (for [file (:files state)] + [:> export-entry* {:file file :key (dm/str (:id file))}]) - [:div {:class (stl/css :modal-footer)} - [:div {:class (stl/css :action-buttons)} - [:input {:class (stl/css :accept-btn) - :type "button" - :value (tr "labels.close") - :disabled (->> state :files (some :loading)) - :on-click on-cancel}]]]])]])) + (when in-progress? + [:div {:class (stl/css :status-message) + :role "status" + :aria-live "polite"} + (tr "labels.downloading-file")])] + + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:input {:class (stl/css :accept-btn) + :type "button" + :value (tr "labels.close") + :disabled in-progress? + :on-click on-cancel}]]]]))]])) diff --git a/frontend/src/app/main/ui/exports/files.scss b/frontend/src/app/main/ui/exports/files.scss index c3c09b3fa4..e395c9c509 100644 --- a/frontend/src/app/main/ui/exports/files.scss +++ b/frontend/src/app/main/ui/exports/files.scss @@ -183,6 +183,13 @@ } } +.status-message { + @include deprecated.body-small-typography; + + color: var(--modal-title-foreground-color); + font-style: italic; +} + .action-buttons { @extend %modal-action-btns; } diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 577b480347..54a33820ec 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -2804,6 +2804,14 @@ msgstr "Libraries & Templates" msgid "labels.loading" msgstr "Loading…" +#: src/app/main/ui/dashboard/import.cljs +msgid "labels.uploading-file" +msgstr "Uploading file…" + +#: src/app/main/ui/exports/files.cljs +msgid "labels.downloading-file" +msgstr "Downloading file…" + #: src/app/main/ui/workspace/sidebar/versions.cljs:210 msgid "labels.lock" msgstr "Lock" From 112e81c3976c49a493fe7f7d4769ea9d3bd63120 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 12:58:58 +0200 Subject: [PATCH 200/288] :paperclip: Fix the version reference Caused by the recent version changes --- backend/src/app/rpc/commands/files.clj | 2 +- backend/src/app/rpc/commands/media.clj | 6 +++--- backend/src/app/rpc/commands/nitrate.clj | 4 ++-- backend/src/app/rpc/commands/profile.clj | 2 +- backend/src/app/rpc/management/nitrate.clj | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index dd81c5c882..3a71359aab 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -668,7 +668,7 @@ counts by type, component/color/typography counts, and inbound and outbound library reference counts. Cheap alternative to `get-file` when only metrics are needed." - {::doc/added "2.16" + {::doc/added "2.17" ::sm/params schema:get-file-stats ::sm/result schema:get-file-stats-result ::db/transaction true} diff --git a/backend/src/app/rpc/commands/media.clj b/backend/src/app/rpc/commands/media.clj index 5bea17d379..22fedd39b9 100644 --- a/backend/src/app/rpc/commands/media.clj +++ b/backend/src/app/rpc/commands/media.clj @@ -255,7 +255,7 @@ [:session-id ::sm/uuid]]) (sv/defmethod ::create-upload-session - {::doc/added "2.16" + {::doc/added "2.17" ::sm/params schema:create-upload-session ::sm/result schema:create-upload-session-result} [{:keys [::db/pool] :as cfg} @@ -293,7 +293,7 @@ [:index ::sm/int]]) (sv/defmethod ::upload-chunk - {::doc/added "2.16" + {::doc/added "2.17" ::sm/params schema:upload-chunk ::sm/result schema:upload-chunk-result} [{:keys [::db/pool] :as cfg} @@ -389,7 +389,7 @@ [:id {:optional true} ::sm/uuid]]) (sv/defmethod ::assemble-file-media-object - {::doc/added "2.16" + {::doc/added "2.17" ::sm/params schema:assemble-file-media-object ::climit/id [[:process-image/by-profile ::rpc/profile-id] [:process-image/global]]} diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index 7a81d05e4e..db552ae070 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -237,7 +237,7 @@ [:organization-name ::sm/text]]) (sv/defmethod ::remove-team-from-org - {::doc/added "2.16" + {::doc/added "2.17" ::sm/params schema:remove-team-from-org} [cfg {:keys [::rpc/profile-id team-id organization-id organization-name]}] @@ -261,7 +261,7 @@ (sv/defmethod ::add-team-to-org {::rpc/auth true - ::doc/added "2.16" + ::doc/added "2.17" ::sm/params schema:add-team-to-org ::db/transaction true} [cfg {:keys [::rpc/profile-id team-id organization-id organization-name]}] diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 2199df39ea..ce6c1d71c0 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -317,7 +317,7 @@ ;; --- MUTATION: Delete Photo (sv/defmethod ::delete-profile-photo - {::doc/added "2.16" + {::doc/added "2.17" ::sm/params [:map] ::sm/result :nil ::db/transaction true} diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 539adb8fbf..58a06e5c65 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -98,7 +98,7 @@ "Store an organization logo in penpot storage and return its ID. Accepts an optional previous-id to mark the old logo for garbage collection when replacing an existing one." - {::doc/added "2.16" + {::doc/added "2.17" ::sm/params schema:upload-org-logo ::sm/result schema:upload-org-logo-result} [{:keys [::sto/storage]} {:keys [content organization-id previous-id]}] @@ -403,7 +403,7 @@ RETURNING id, name;") (sv/defmethod ::remove-from-org "Remove an user from an organization" - {::doc/added "2.16" + {::doc/added "2.17" ::sm/params [:map [:profile-id ::sm/uuid] [:organization-id ::sm/uuid] @@ -439,7 +439,7 @@ RETURNING id, name;") (sv/defmethod ::get-remove-from-org-summary "Get a summary of the teams that would be deleted, transferred, or exited if the user were removed from the organization" - {::doc/added "2.16" + {::doc/added "2.17" ::sm/params [:map [:profile-id ::sm/uuid] [:organization-id ::sm/uuid] From d384f47253806990c2b91aa832d0168c9b5e664b Mon Sep 17 00:00:00 2001 From: Full Stack Developer <30417830+jsdevninja@users.noreply.github.com> Date: Wed, 22 Apr 2026 06:59:42 -0500 Subject: [PATCH 201/288] :bug: Fix internal error on layer prev/next sibling selection (#9003) Signed-off-by: jsdevninja --- .../app/main/data/workspace/selection.cljs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 45c323a860..d0ce0c5c2f 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -173,13 +173,17 @@ current (get objects first-selected) parent (get objects (:parent-id current)) sibling-ids (:shapes parent) - current-index (d/index-of sibling-ids first-selected) - sibling (if (= (dec (count sibling-ids)) current-index) - (first sibling-ids) - (nth sibling-ids (inc current-index)))] + ;; `index-of` is nil when the shape is not listed under the parent (stale + ;; selection or inconsistent tree). Do not call `nth` with `(dec nil)` — in + ;; ClojureScript that is -1 and throws (see penpot#7064). + current-index (some-> sibling-ids (d/index-of first-selected)) + sibling (when (some? current-index) + (if (= (dec (count sibling-ids)) current-index) + (first sibling-ids) + (nth sibling-ids (inc current-index) nil)))] (cond - (= 1 count-selected) + (and (= 1 count-selected) (some? sibling)) (rx/of (select-shape sibling)) (> count-selected 1) @@ -198,12 +202,13 @@ current (get objects first-selected) parent (get objects (:parent-id current)) sibling-ids (:shapes parent) - current-index (d/index-of sibling-ids first-selected) - sibling (if (= 0 current-index) - (last sibling-ids) - (nth sibling-ids (dec current-index)))] + current-index (some-> sibling-ids (d/index-of first-selected)) + sibling (when (some? current-index) + (if (= 0 current-index) + (last sibling-ids) + (nth sibling-ids (dec current-index) nil)))] (cond - (= 1 count-selected) + (and (= 1 count-selected) (some? sibling)) (rx/of (select-shape sibling)) (> count-selected 1) From 7d4092eebabc85087d5f6011570d533305912c74 Mon Sep 17 00:00:00 2001 From: Juanfran Date: Wed, 22 Apr 2026 13:29:40 +0200 Subject: [PATCH 202/288] :bug: Fix column name mismatch when accepting org invitation --- backend/src/app/rpc/commands/verify_token.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index e99e74c3c5..cc270b3ded 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -134,7 +134,7 @@ (db/delete! conn :team-invitation (cond-> {:email-to member-email} team-id (assoc :team-id team-id) - organization-id (assoc :organization-id organization-id))) + organization-id (assoc :org-id organization-id))) ;; Delete any request (only applicable for team invitations) (when team-id @@ -173,7 +173,7 @@ (let [invitation (db/get* conn :team-invitation (cond-> {:email-to member-email} team-id (assoc :team-id team-id) - organization-id (assoc :organization-id organization-id))) + organization-id (assoc :org-id organization-id))) profile (db/get* conn :profile {:id profile-id} {:columns [:id :email :default-team-id]}) From 7dbd602d1e4c2fb498827db3f7024c7461aeda53 Mon Sep 17 00:00:00 2001 From: Edwin Rivera <58448072+edwin-rivera-dev@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:19:58 +0000 Subject: [PATCH 203/288] :bug: Fix text export with custom fonts across SVG, PNG and JPG (#9094) * :bug: Fix text export with custom fonts across SVG, PNG and JPG Text layers using custom or non-standard fonts were rendered incorrectly on export regardless of the target format. The exporter was not resolving the font face correctly before rasterization/serialization, causing the output to fall back to a default glyph set and producing broken or misaligned text. This fix ensures font data is resolved and embedded consistently in the export pipeline for all output formats. Signed-off-by: Edwin Rivera * :books: Add entry to CHANGES.md under 2.17.0 Signed-off-by: edwin-rivera-dev --------- Signed-off-by: Edwin Rivera Signed-off-by: edwin-rivera-dev --- CHANGES.md | 1 + exporter/src/app/browser.cljs | 13 +++++++++++++ exporter/src/app/renderer/bitmap.cljs | 1 + exporter/src/app/renderer/pdf.cljs | 1 + exporter/src/app/renderer/svg.cljs | 1 + 5 files changed, 17 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 58b0da4de5..4f71cbe20f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -72,6 +72,7 @@ - Fix gap input throwing an error [Github #8984](https://github.com/penpot/penpot/pull/8984) - Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990) - Allow deleting the profile avatar after uploading [Github #9067](https://github.com/penpot/penpot/issues/9067) +- Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [Github #8516](https://github.com/penpot/penpot/issues/8516) ## 2.16.0 (Unreleased) diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index 526ae77380..798a9c3f44 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -47,6 +47,19 @@ [page ms] (.waitForTimeout ^js page ms)) +(defn wait-for-fonts + "Wait until the browser has finished loading all fonts" + ([page] (wait-for-fonts page nil)) + ([page {:keys [timeout] :or {timeout 15000}}] + (-> (.waitForFunction ^js page + "() => document.fonts && document.fonts.status === 'loaded'" + nil + #js {:timeout timeout}) + (p/catch (fn [cause] + (l/warn :hint "wait-for-fonts timed out; continuing anyway" + :cause (ex-message cause)) + (p/resolved nil)))))) + (defn wait-for ([locator] (wait-for locator nil)) ([locator {:keys [state timeout] :or {state "visible" timeout 10000}}] diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index 00c67ba508..bc8b06abf3 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -47,6 +47,7 @@ ;; navigate to the page and perform basic setup (bw/nav! page (str uri)) (bw/sleep page 1000) ; the good old fix with sleep + (bw/wait-for-fonts page) (bw/eval! page (js* "() => document.body.style.background = 'transparent'")) ;; take the screnshot of requested objects, one by one diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs index 25bcfc036b..27402db513 100644 --- a/exporter/src/app/renderer/pdf.cljs +++ b/exporter/src/app/renderer/pdf.cljs @@ -66,6 +66,7 @@ (sync-page-size! dom) (bw/screenshot dom {:full-page? true}) (bw/sleep page 2000) ; the good old fix with sleep + (bw/wait-for-fonts page) (bw/pdf page {:path path}) path))) diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 73558dbe5f..66e6dd61b2 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -338,6 +338,7 @@ ;; navigate to the page and perform basic setup (bw/nav! page (str uri)) (bw/sleep page 1000) ; the good old fix with sleep + (bw/wait-for-fonts page) ;; take the screnshot of requested objects, one by one (p/run (partial render-object page) objects) From 3fd976c551470bd4190ae6dad11045b607fd46c4 Mon Sep 17 00:00:00 2001 From: Dexterity <173429049+Dexterity104@users.noreply.github.com> Date: Wed, 22 Apr 2026 09:21:02 -0400 Subject: [PATCH 204/288] :bug: Fix UI bugs in account settings forms (#8997) Closes #8977 Closes #8979 Signed-off-by: Dexterity <173429049+Dexterity104@users.noreply.github.com> Co-authored-by: Andrey Antukh --- CHANGES.md | 2 ++ frontend/src/app/main/ui/components/forms.cljs | 14 +++++++++++++- frontend/src/app/main/ui/components/forms.scss | 3 +++ frontend/src/app/main/ui/settings/profile.cljs | 9 ++++----- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4f71cbe20f..794b299461 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -70,6 +70,8 @@ - Fix app crash when selecting shapes with one hidden [Taiga #13959](https://tree.taiga.io/project/penpot/issue/13959) - Fix opacity mixed value [Taiga #13960](https://tree.taiga.io/project/penpot/issue/13960) - Fix gap input throwing an error [Github #8984](https://github.com/penpot/penpot/pull/8984) +- Fix non-functional clear icon in change email modal inputs (by @Dexterity104) [Github #8977](https://github.com/penpot/penpot/issues/8977) +- Disable save button after saving account profile settings (by @Dexterity104) [Github #8979](https://github.com/penpot/penpot/issues/8979) - Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990) - Allow deleting the profile avatar after uploading [Github #9067](https://github.com/penpot/penpot/issues/9067) - Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [Github #8516](https://github.com/penpot/penpot/issues/8516) diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index e2bb4cd5cf..17a1708196 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -92,6 +92,15 @@ (when-not (get-in @form [:touched input-name]) (swap! form assoc-in [:touched input-name] true))) + on-clear + (fn [event] + (dom/prevent-default event) + (swap! form (fn [state] + (-> state + (assoc-in [:data input-name] "") + (assoc-in [:touched input-name] false)))) + (some-> (mf/ref-val input-ref) (dom/focus!))) + on-key-press (mf/use-fn (mf/deps input-ref) @@ -158,7 +167,10 @@ deprecated-icon/tick]) (when show-invalid? - [:span {:class (stl/css :invalid-icon)} + [:button {:class (stl/css :invalid-icon) + :type "button" + :tab-index "-1" + :on-click on-clear} deprecated-icon/close])])] (some? children) diff --git a/frontend/src/app/main/ui/components/forms.scss b/frontend/src/app/main/ui/components/forms.scss index 2242c66868..a8705671c6 100644 --- a/frontend/src/app/main/ui/components/forms.scss +++ b/frontend/src/app/main/ui/components/forms.scss @@ -123,6 +123,8 @@ .invalid-icon { inline-size: $sz-16; block-size: $sz-16; + padding: 0; + border: none; background: var(--input-border-color-error); border-radius: $br-circle; display: flex; @@ -131,6 +133,7 @@ position: absolute; inset-inline-end: var(--input-icon-padding); inset-block-start: calc(50% - var(--sp-s)); + cursor: pointer; svg { inline-size: $sz-12; diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs index be2665337d..b8903d4027 100644 --- a/frontend/src/app/main/ui/settings/profile.cljs +++ b/frontend/src/app/main/ui/settings/profile.cljs @@ -26,13 +26,12 @@ [:fullname [::sm/text {:max 250}]] [:email ::sm/email]]) -(defn- on-success - [_] - (st/emit! (ntf/success (tr "notifications.profile-saved")))) - (defn- on-submit [form _event] - (let [data (:clean-data @form)] + (let [data (:clean-data @form) + on-success (fn [_] + (swap! form assoc :touched {}) + (st/emit! (ntf/success (tr "notifications.profile-saved"))))] (st/emit! (du/update-profile data) (du/persist-profile {:on-success on-success})))) From 5bbb2c5cffb48dc346335aec32a03a2f301f2396 Mon Sep 17 00:00:00 2001 From: Renzo <170978465+RenzoMXD@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:46:38 +0200 Subject: [PATCH 205/288] :bug: Fix Copy as SVG for multi-shape selection (#838) (#9066) Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com> --- CHANGES.md | 1 + .../app/main/data/workspace/clipboard.cljs | 4 +- frontend/src/app/main/render.cljs | 40 ++++++++++++ frontend/src/app/util/clipboard.cljs | 19 ++++++ .../src/app/util/code_gen/markup_svg.cljs | 26 +++++--- .../test/frontend_tests/copy_as_svg_test.cljs | 61 +++++++++++++++++++ frontend/test/frontend_tests/runner.cljs | 2 + 7 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 frontend/test/frontend_tests/copy_as_svg_test.cljs diff --git a/CHANGES.md b/CHANGES.md index 794b299461..8cdf1ba7d0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,7 @@ ### :bug: Bugs fixed +- Fix Copy as SVG: emit a single valid SVG document when multiple shapes are selected, and publish `image/svg+xml` to the clipboard so the paste target works in Inkscape and other SVG-native tools [Github #838](https://github.com/penpot/penpot/issues/838) - Reset profile submenu state when the account menu closes (by @eureka0928) [Github #8947](https://github.com/penpot/penpot/issues/8947) - Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582) - Fix styles between grid layout inputs [Taiga #13526](https://tree.taiga.io/project/penpot/issue/13526) diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index 3017d94b2f..72f94ecdc4 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -358,7 +358,9 @@ shapes (mapv maybe-translate selected) svg-formatted (svg/generate-formatted-markup objects shapes)] - (clipboard/to-clipboard svg-formatted))))) + (clipboard/to-clipboard-multi + {"image/svg+xml" svg-formatted + "text/plain" svg-formatted}))))) (defn copy-selected-css [] diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index d60366592c..a53971c452 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -484,6 +484,46 @@ [:& ff/fontfaces-style {:fonts fonts}] [:& shape-wrapper {:shape object}]]]])) +(mf/defc objects-svg + {::mf/wrap [mf/memo]} + [{:keys [objects object-ids embed] :or {embed false} :as props}] + (let [shapes + (->> object-ids + (keep #(get objects %)) + (mapv (fn [object] + (cond-> object + (:hide-fill-on-export object) + (assoc :fills []))))) + + bounds + (->> shapes + (map #(gsb/get-object-bounds objects % {:ignore-margin? false})) + (grc/join-rects)) + + {:keys [width height]} bounds + vbox (format-viewbox bounds) + fonts (->> shapes + (mapcat #(ff/shape->fonts % objects)) + (distinct)) + + shape-wrapper + (mf/with-memo [objects] + (shape-wrapper-factory objects))] + + [:& (mf/provider export/include-metadata-ctx) {:value false} + [:& (mf/provider embed/context) {:value embed} + [:svg {:view-box vbox + :width (ust/format-precision width viewbox-decimal-precision) + :height (ust/format-precision height viewbox-decimal-precision) + :version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :style {:-webkit-print-color-adjust :exact} + :fill "none"} + [:& ff/fontfaces-style {:fonts fonts}] + (for [shape shapes] + [:& shape-wrapper {:key (dm/str (:id shape)) :shape shape}])]]])) + (defn render-to-canvas [objects canvas bounds scale object-id on-render] (let [width (.-width canvas) diff --git a/frontend/src/app/util/clipboard.cljs b/frontend/src/app/util/clipboard.cljs index 5f18798847..d06aa5c22e 100644 --- a/frontend/src/app/util/clipboard.cljs +++ b/frontend/src/app/util/clipboard.cljs @@ -88,3 +88,22 @@ (let [clipboard (unchecked-get js/navigator "clipboard") data (create-clipboard-item mimetype promise)] (.write ^js clipboard #js [data]))) + +(defn to-clipboard-multi + "Write multiple MIME representations as a single ClipboardItem. + `items` is a map of mime-type (string) -> string payload. + Falls back to `writeText` when the async Clipboard API is unavailable." + [items] + (let [clipboard (unchecked-get js/navigator "clipboard")] + (if (and clipboard (unchecked-get clipboard "write")) + (let [obj (reduce-kv + (fn [acc mime payload] + (let [blob (js/Blob. #js [payload] #js {:type mime})] + (unchecked-set acc mime (js/Promise.resolve blob)) + acc)) + #js {} items) + item (js/ClipboardItem. obj)] + (.write ^js clipboard #js [item])) + (when-let [text (or (get items "text/plain") + (first (vals items)))] + (.writeText ^js clipboard text))))) diff --git a/frontend/src/app/util/code_gen/markup_svg.cljs b/frontend/src/app/util/code_gen/markup_svg.cljs index 65044af6d7..6ab4ad0799 100644 --- a/frontend/src/app/util/code_gen/markup_svg.cljs +++ b/frontend/src/app/util/code_gen/markup_svg.cljs @@ -9,10 +9,9 @@ ["react-dom/server" :as rds] [app.main.render :as render] [app.util.code-beautify :as cb] - [cuerdas.core :as str] [rumext.v2 :as mf])) -(defn generate-svg +(defn- generate-single-svg [objects shape] (rds/renderToStaticMarkup (mf/element @@ -20,13 +19,26 @@ #js {:objects objects :object-id (-> shape :id)}))) +(defn- generate-multi-svg + [objects shapes] + (rds/renderToStaticMarkup + (mf/element + render/objects-svg + #js {:objects objects + :object-ids (mapv :id shapes)}))) + +(defn generate-svg + [objects shape] + (generate-single-svg objects shape)) + (defn generate-markup [objects shapes] - (->> shapes - (map #(generate-svg objects %)) - (str/join "\n"))) + (case (count shapes) + 0 "" + 1 (generate-single-svg objects (first shapes)) + (generate-multi-svg objects shapes))) (defn generate-formatted-markup [objects shapes] - (let [markup (generate-markup objects shapes)] - (cb/format-code markup "svg"))) + (-> (generate-markup objects shapes) + (cb/format-code "svg"))) diff --git a/frontend/test/frontend_tests/copy_as_svg_test.cljs b/frontend/test/frontend_tests/copy_as_svg_test.cljs new file mode 100644 index 0000000000..c2aee4a298 --- /dev/null +++ b/frontend/test/frontend_tests/copy_as_svg_test.cljs @@ -0,0 +1,61 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns frontend-tests.copy-as-svg-test + "Regression tests for the Copy as SVG action (issue #838). + + The bug: when multiple shapes were selected, `generate-markup` emitted + several sibling `` roots concatenated with newlines. External SVG + parsers (Inkscape, browsers) only read the first root, so multi-shape + selection appeared to copy only one shape. The fix wraps 2+ shapes in a + single `` root with a combined viewBox." + (:require + [app.common.test-helpers.files :as cthf] + [app.common.test-helpers.ids-map :as cthi] + [app.common.test-helpers.shapes :as cths] + [app.util.code-gen.markup-svg :as svg] + [cljs.test :refer [deftest is testing] :include-macros true])) + +(defn- setup-shapes + "Build a file with `n` sample rectangles on the current page. + Returns a map with `:objects` and `:shapes` keys, mirroring the inputs + that `copy-selected-svg` feeds into `generate-markup`." + [labels] + (let [file (reduce (fn [f label] + (cths/add-sample-shape f label)) + (cthf/sample-file :file1 :page-label :page1) + labels) + page (cthf/current-page file) + objects (:objects page) + shapes (mapv #(get objects (cthi/id %)) labels)] + {:objects objects :shapes shapes})) + +(defn- count-matches + [re s] + (count (re-seq re s))) + +(deftest empty-selection-yields-empty-string + (is (= "" (svg/generate-markup {} [])))) + +(deftest single-shape-produces-one-svg-root + (testing "Regression guard: the single-shape path stays unchanged" + (let [{:keys [objects shapes]} (setup-shapes [:rect-1]) + markup (svg/generate-markup objects shapes)] + (is (string? markup)) + (is (pos? (count markup))) + (is (= 1 (count-matches #" root")))) + +(deftest multi-shape-produces-single-svg-root + (testing "Fix for #838: multiple shapes share one outer " + (let [{:keys [objects shapes]} (setup-shapes [:rect-1 :rect-2 :rect-3]) + markup (svg/generate-markup objects shapes)] + (is (string? markup)) + (is (pos? (count markup))) + (is (= 1 (count-matches #" roots") + (is (= 1 (count-matches #"" markup)) + "multi-select must NOT emit multiple closing tags")))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index b7b53f8fbb..174ef34056 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -2,6 +2,7 @@ (:require [cljs.test :as t] [frontend-tests.basic-shapes-test] + [frontend-tests.copy-as-svg-test] [frontend-tests.data.repo-test] [frontend-tests.data.uploads-test] [frontend-tests.data.viewer-test] @@ -44,6 +45,7 @@ [] (t/run-tests 'frontend-tests.basic-shapes-test + 'frontend-tests.copy-as-svg-test 'frontend-tests.data.repo-test 'frontend-tests.errors-test 'frontend-tests.main-errors-test From 7428cfa684e8208570e8affc9c2e0fa2d4f92c33 Mon Sep 17 00:00:00 2001 From: Stas Haas Date: Wed, 22 Apr 2026 11:27:57 +0200 Subject: [PATCH 206/288] :globe_with_meridians: Add translations for: German Currently translated at 95.8% (1988 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/ --- frontend/translations/de.po | 52 ++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 3fbb0f0585..7edfd173ce 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-04-18 13:10+0000\n" +"PO-Revision-Date: 2026-04-23 10:09+0000\n" "Last-Translator: Stas Haas \n" "Language-Team: German \n" @@ -8459,3 +8459,53 @@ msgstr "Ups! Der Canvas-Kontext ist verloren gegangen" msgid "loader.tips.03.message" msgstr "" "Gestalten Sie flexibel mit vertrauten, CSS-ähnlichen Layout-Steuerelementen." + +#: src/app/main/ui/workspace/tokens/modals/import.cljs:132 +#, unused +msgid "workspace.tokens.choose-folder" +msgstr "Ordner auswählen" + +#: src/app/main/ui/workspace/tokens/modals/import.cljs:127 +#, unused +msgid "workspace.tokens.choose-file" +msgstr "Datei auswählen" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:498 +msgid "workspace.shape.menu.restore-variant" +msgstr "Variante wiederherstellen" + +#: src/app/main/ui/workspace/context_menu.cljs:619, src/app/main/ui/workspace/sidebar/assets/components.cljs:633, src/app/main/ui/workspace/sidebar/assets/groups.cljs:75, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1106 +msgid "workspace.shape.menu.combine-as-variants" +msgstr "Als Varianten kombinieren" + +#: src/app/main/ui/workspace/sidebar/assets/components.cljs:635 +msgid "workspace.shape.menu.combine-as-variants-error" +msgstr "Die Komponenten müssen sich auf derselben Seite befinden" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1157 +msgid "workspace.shape.menu.remove-variant-property" +msgstr "Eigenschaft entfernen" + +#: src/app/main/ui/workspace/sidebar/assets/common.cljs:512, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1073, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1221, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1307 +msgid "workspace.shape.menu.add-variant-property" +msgstr "Neue Eigenschaft hinzufügen" + +#: src/app/main/ui/workspace/plugins.cljs:287 +msgid "workspace.plugins.permissions.allow-localstorage" +msgstr "Daten im Browser speichern." + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:469 +msgid "workspace.options.size.unlock" +msgstr "Seitenverhältnis entsperren" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:469 +msgid "workspace.options.size.lock" +msgstr "Seitenverhältnis sperren" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:608 +msgid "workspace.options.interaction-animation-direction-right" +msgstr "Rechts" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:612 +msgid "workspace.options.interaction-animation-direction-left" +msgstr "Links" From a3c330d6e77cb5dd857cdf40b7b045a09204cb7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Wed, 22 Apr 2026 12:15:56 +0200 Subject: [PATCH 207/288] :sparkles: Add downgrade nitrate to unlimited modal --- .../app/main/ui/settings/subscription.cljs | 43 ++++++++++++++++++- .../app/main/ui/settings/subscription.scss | 25 ++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index d5a22232bc..e22a65ff01 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -464,7 +464,12 @@ (modal/show :management-dialog {:subscription-type subscription-type :current-subscription current-subscription - :editors subscription-editors :subscribe-to-trial (not (:type subscription))})))))] + :editors subscription-editors :subscribe-to-trial (not (:type subscription))}))))) + + open-contact-sales-modal + (mf/use-fn + (fn [subscription-type] + (st/emit! (modal/show :nitrate-contact-sales-dialog {:subscription-type subscription-type}))))] (mf/with-effect [] (dom/set-html-title (tr "subscription.labels"))) @@ -618,7 +623,7 @@ (tr "subscription.settings.unlimited.autosave-benefit"), (tr "subscription.settings.unlimited.bill")] :cta-text (if (:type subscription) (tr "subscription.settings.subscribe") (tr "subscription.settings.try-it-free")) - :cta-link #(open-subscription-modal "unlimited" subscription) + :cta-link (if (and (contains? cf/flags :nitrate) nitrate?) #(open-contact-sales-modal "Unlimited") #(open-subscription-modal "unlimited" subscription)) :cta-text-with-icon (tr "subscription.settings.more-information") :cta-link-with-icon go-to-pricing-page :recommended (= subscription-type "professional") @@ -742,5 +747,39 @@ [:a {:class (stl/css :cta-button) :href "mailto:sales@penpot.app"} "sales@penpot.app"]]])]])) +(mf/defc nitrate-contact-sales-dialog + {::mf/register modal/components + ::mf/register-as :nitrate-contact-sales-dialog} + [{:keys [subscription-type]}] + (let [handle-close-dialog + (mf/use-fn + (fn [] + (modal/hide!)))] + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog)} + [:button {:class (stl/css :close-btn) :on-click handle-close-dialog} + [:> icon* {:icon-id "close" + :size "m"}]] + [:div {:class (stl/css :modal-title :subscription-title)} + (str "Switch to " subscription-type " plan?")] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-text-medium)} + "When you downgrade:"] + [:ul {:class (stl/css :downgrade-list)} + [:li {:class (stl/css :downgrade-item)} "Your organization will be deleted."] + [:li {:class (stl/css :downgrade-item)} "The teams, projects and files will no longer be part of any organization but they will remain available."] + [:li {:class (stl/css :downgrade-item)} "Your total storage, auto-version history, and file recovery period will be limited."]] + + [:div {:class (stl/css :downgrade-warning)} + "To switch to this plan, please contact our sales team. +We’ll help you update your subscription and ensure everything is set up correctly."] + [:div {:class (stl/css :action-buttons)} + [:> button* {:variant "secondary" + :type "button" + :on-click handle-close-dialog} (tr "ds.confirm-cancel")] + [:> button* {:variant "primary" + :type "button" + :on-click #(dom/open-new-window "mailto:sales@penpot.app?subject=Switch%20to%20the%20Unlimited%20plan")} "Contact sales"]]]]])) diff --git a/frontend/src/app/main/ui/settings/subscription.scss b/frontend/src/app/main/ui/settings/subscription.scss index 98ad84d385..213624d901 100644 --- a/frontend/src/app/main/ui/settings/subscription.scss +++ b/frontend/src/app/main/ui/settings/subscription.scss @@ -108,7 +108,7 @@ border-radius: 6px; border: 1.75px solid var(--color-foreground-primary); stroke-width: 2.25px; - padding: deprecated.$s-1; + padding: px2rem(3); svg { block-size: var(--sp-m); @@ -381,3 +381,26 @@ .modal-contact-content { gap: var(--sp-xl); } + +.downgrade-warning { + @include t.use-typography("body-medium"); + + background-color: var(--color-background-tertiary); + border-radius: var(--sp-s); + padding-block: var(--sp-s); + padding-inline: var(--sp-m); + margin-block: var(--sp-m) var(--sp-xxxl); +} + +.downgrade-list { + list-style-position: outside; + list-style-type: disc; + margin-block: var(--sp-l) 0; + padding-inline-start: var(--sp-l); +} + +.downgrade-item { + @include t.use-typography("body-medium"); + + margin-block-end: var(--sp-l); +} From 7c1a29ccf768a86a0e95c0ab670137fc38b02751 Mon Sep 17 00:00:00 2001 From: Renzo <170978465+RenzoMXD@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:08:11 +0200 Subject: [PATCH 208/288] :bug: Remove corepack dependency from MCP server for Node.js 25+ (#9119) * :bug: Remove corepack dependency from MCP server for Node.js 25+ * :bug: Update --- CHANGES.md | 1 + mcp/bin/mcp-local.js | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8cdf1ba7d0..4e63461ac5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,7 @@ ### :bug: Bugs fixed +- Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled [Github #8877](https://github.com/penpot/penpot/issues/8877) - Fix Copy as SVG: emit a single valid SVG document when multiple shapes are selected, and publish `image/svg+xml` to the clipboard so the paste target works in Inkscape and other SVG-native tools [Github #838](https://github.com/penpot/penpot/issues/838) - Reset profile submenu state when the account menu closes (by @eureka0928) [Github #8947](https://github.com/penpot/penpot/issues/8947) - Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582) diff --git a/mcp/bin/mcp-local.js b/mcp/bin/mcp-local.js index 65a2b0f763..b77b245dea 100644 --- a/mcp/bin/mcp-local.js +++ b/mcp/bin/mcp-local.js @@ -5,6 +5,12 @@ const fs = require("fs"); const path = require("path"); const root = path.resolve(__dirname, ".."); +const pkg = require(path.join(root, "package.json")); + +function pnpmVersion() { + const match = (pkg.packageManager || "").match(/^pnpm@([^+]+)/); + return match ? match[1] : "latest"; +} function run(command) { execSync(command, { cwd: root, stdio: "inherit" }); @@ -19,13 +25,7 @@ if (fs.existsSync(distLock)) { } try { - run("corepack pnpm run bootstrap"); + run(`npx -y pnpm@${pnpmVersion()} run bootstrap`); } catch (error) { - if (error.code === "ENOENT") { - console.error( - "corepack is required but was not found. It ships with Node.js >= 16." - ); - process.exit(1); - } process.exit(error.status ?? 1); } From e280168de9a456421bd8f2bb9cec546ebbd2d05d Mon Sep 17 00:00:00 2001 From: wdeveloper16 Date: Fri, 24 Apr 2026 08:13:16 +0200 Subject: [PATCH 209/288] :sparkles: Add read-only preview mode for saved versions (#7622) (#8976) * :sparkles: Add read-only preview mode for saved versions (#7622) * :wrench: Address review feedback on version preview (#7622) * :bug: Fix version preview for WASM renderer (#7622) * :bug: Fix stylelint color-named and color-function-notation in preview banner (#7622) * :bug: Fix invalid-arity call to initialize-workspace in exit-preview (#7622) * :bug: Fix unclosed defn paren in exit-preview (#7622) * :recycle: Refactor version preview/restore flow Separate enter-preview and enter-restore flows with dedicated dialogs instead of a persistent banner. Removes preview-banner component in favor of inline actions dialog. Uses backup/restore pattern for exit-preview instead of full workspace reinitialization. Adds analytics events for preview/restore actions. Signed-off-by: Andrey Antukh * :zap: Extract on-name-input-focus as namespace-level private function The callback had no dependencies on component-local state or props, making it a pure function that can be hoisted to a defn-. This avoids recreating the same callback identity on every render of version-entry*. * :zap: Extract extract-id-from-event helper to deduplicate snapshot callbacks Three callbacks in snapshot-entry* shared the same DOM extraction logic (get current target, read data-id, parse UUID). Extracted into a private defn- to remove the duplication and simplify each callback. * :zap: Extract pure state-update callbacks from versions-toolbox* to namespace level Eight callbacks that only emit fixed Potok events with no meaningful deps were hoisted out of the component as defn- functions: - on-create-version - on-edit-version - on-cancel-version-edition - on-rename-version - on-delete-version - on-pin-version - on-lock-version - on-unlock-version These no longer need mf/use-fn wrappers since namespace-level functions have stable identity across renders, avoiding unnecessary callback recreation on each render cycle. * :sparkles: Rename filter parameter to filter-value in on-change-filter to avoid core shadowing The parameter name 'filter' shadowed clojure.core/filter within the function scope. Renamed to 'filter-value' for clarity and to prevent potential bugs if core/filter were needed in future changes. * :wrench: Fix linter warnings and errors across version-related namespaces frontend/src/app/main/ui/workspace.cljs: - Remove unused requires: app.common.data, app.main.data.notifications, app.main.data.workspace.versions frontend/src/app/main/data/workspace/versions.cljs: - Remove unused require: app.common.uuid - Fix duplicate reify type: enter-restore used ::restore-version (same as the private restore-version fn), renamed to ::enter-restore - Remove unused bindings: state in enter-restore, team-id in exit-preview and restore-version-from-plugin --------- Signed-off-by: Andrey Antukh Signed-off-by: wdeveloper16 Co-authored-by: wdeveloper16 Co-authored-by: Andrey Antukh --- backend/src/app/features/file_snapshots.clj | 7 +- .../src/app/rpc/commands/files_snapshot.clj | 38 ++++ frontend/src/app/main/data/persistence.cljs | 6 +- frontend/src/app/main/data/workspace.cljs | 3 +- .../src/app/main/data/workspace/versions.cljs | 194 ++++++++++++++---- .../main/ui/workspace/sidebar/versions.cljs | 164 ++++++++------- .../src/app/main/ui/workspace/viewport.cljs | 36 ++-- .../main/ui/workspace/viewport/top_bar.cljs | 12 +- .../app/main/ui/workspace/viewport_wasm.cljs | 35 ++-- frontend/translations/en.po | 12 ++ frontend/translations/es.po | 6 + 11 files changed, 363 insertions(+), 150 deletions(-) diff --git a/backend/src/app/features/file_snapshots.clj b/backend/src/app/features/file_snapshots.clj index 192030cbf8..e013b90d00 100644 --- a/backend/src/app/features/file_snapshots.clj +++ b/backend/src/app/features/file_snapshots.clj @@ -112,8 +112,9 @@ THEN (c.deleted_at IS NULL OR c.deleted_at >= ?::timestamptz) END")) -(defn- get-snapshot - "Get snapshot with decoded data" +(defn get-snapshot-data + "Get a fully decoded snapshot for read-only preview or restoration. + Returns the snapshot map with decoded :data field." [cfg file-id snapshot-id] (let [now (ct/now)] (->> (db/get-with-sql cfg [sql:get-snapshot file-id snapshot-id now] @@ -326,7 +327,7 @@ (sto/resolve cfg {::db/reuse-conn true}) snapshot - (get-snapshot cfg file-id snapshot-id)] + (get-snapshot-data cfg file-id snapshot-id)] (when-not snapshot (ex/raise :type :not-found diff --git a/backend/src/app/rpc/commands/files_snapshot.clj b/backend/src/app/rpc/commands/files_snapshot.clj index 8325772361..7736b66cd9 100644 --- a/backend/src/app/rpc/commands/files_snapshot.clj +++ b/backend/src/app/rpc/commands/files_snapshot.clj @@ -8,6 +8,7 @@ (:require [app.binfile.common :as bfc] [app.common.exceptions :as ex] + [app.common.features :as-alias cfeat] [app.common.schema :as sm] [app.common.time :as ct] [app.db :as db] @@ -35,6 +36,43 @@ (files/check-read-permissions! conn profile-id file-id) (fsnap/get-visible-snapshots conn file-id)))) +;; --- COMMAND QUERY: get-file-snapshot + +(def ^:private schema:get-file-snapshot + [:map {:title "get-file-snapshot"} + [:file-id ::sm/uuid] + [:id ::sm/uuid] + [:features {:optional true} ::cfeat/features]]) + +(sv/defmethod ::get-file-snapshot + "Retrieve a file bundle with data from a specific snapshot for + read-only preview. Does not modify any database state." + {::doc/added "2.16" + ::sm/params schema:get-file-snapshot + ::sm/result files/schema:file-with-permissions + ::db/transaction true} + [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id id] :as params}] + (let [perms (bfc/get-file-permissions conn profile-id file-id)] + (files/check-read-permissions! perms) + (let [snapshot (fsnap/get-snapshot-data cfg file-id id)] + (when-not snapshot + (ex/raise :type :not-found + :code :snapshot-not-found + :hint "unable to find snapshot with the provided id" + :snapshot-id id + :file-id file-id)) + ;; Load current file metadata only (no data decoding) then overlay + ;; the snapshot data so the client receives the same shape as a + ;; normal get-file response but with historical page/object content. + (let [base-file (bfc/get-file cfg file-id :load-data? false)] + (-> base-file + (assoc :data (:data snapshot)) + (assoc :version (:version snapshot)) + (assoc :features (:features snapshot)) + (assoc :revn (:revn snapshot)) + (assoc :vern (rand-int 100000)) + (assoc :permissions perms)))))) + (def ^:private schema:create-file-snapshot [:map [:file-id ::sm/uuid] diff --git a/frontend/src/app/main/data/persistence.cljs b/frontend/src/app/main/data/persistence.cljs index adcc70cbb3..c90c423f96 100644 --- a/frontend/src/app/main/data/persistence.cljs +++ b/frontend/src/app/main/data/persistence.cljs @@ -121,8 +121,10 @@ :features features} permissions (:permissions state)] - ;; Prevent commit changes by a team member without edition permission - (when (:can-edit permissions) + ;; Prevent saving changes when in version preview (read-only) mode + ;; or when the user does not have edition permission. + (when (and (:can-edit permissions) + (not (get-in state [:workspace-global :read-only?]))) (->> (rp/cmd! :update-file params) (rx/mapcat (fn [{:keys [revn lagged] :as response}] (log/debug :hint "changes persisted" :commit-id (dm/str commit-id) :lagged (count lagged)) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 461a611315..1d5acfed38 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -511,7 +511,8 @@ :workspace-persistence :workspace-presence :workspace-tokens - :workspace-undo) + :workspace-undo + :workspace-versions) (update :workspace-global dissoc :read-only?) (assoc-in [:workspace-global :options-mode] :design) (update :files d/update-vals #(dissoc % :data)))) diff --git a/frontend/src/app/main/data/workspace/versions.cljs b/frontend/src/app/main/data/workspace/versions.cljs index 85630cfccb..072f8c634a 100644 --- a/frontend/src/app/main/data/workspace/versions.cljs +++ b/frontend/src/app/main/data/workspace/versions.cljs @@ -8,23 +8,28 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.logging :as log] [app.common.schema :as sm] [app.common.time :as ct] [app.main.data.event :as ev] + [app.main.data.helpers :as dsh] [app.main.data.notifications :as ntf] [app.main.data.persistence :as dwp] [app.main.data.workspace :as dw] [app.main.data.workspace.pages :as dwpg] [app.main.data.workspace.thumbnails :as th] + [app.main.features :as features] [app.main.refs :as refs] [app.main.repo :as rp] + [app.util.i18n :refer [tr]] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) (defonce default-state {:status :loading :data nil - :editing nil}) + :editing nil + :preview-id nil}) (declare fetch-versions) @@ -122,32 +127,6 @@ (rx/take 1) (rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id snapshot-id})))) -(defn restore-version - [id origin] - (assert (uuid? id) "expected valid uuid for `id`") - (ptk/reify ::restore-version - ptk/WatchEvent - (watch [_ state _] - (let [file-id (:current-file-id state) - team-id (:current-team-id state) - event-name (case origin - :version "restore-pin-version" - :snapshot "restore-autosave" - :plugin "restore-version-plugin")] - - (rx/concat - (rx/of ::dwp/force-persist - (dw/remove-layout-flag :document-history)) - - (->> (wait-for-persistence file-id id) - (rx/map #(initialize-version))) - - (if event-name - (rx/of (ev/event {::ev/name event-name - :file-id file-id - :team-id team-id})) - (rx/empty))))))) - (defn delete-version [id] (assert (uuid? id) "expected valid uuid for `id`") @@ -193,6 +172,145 @@ (->> (rp/cmd! :unlock-file-snapshot {:id id}) (rx/map fetch-versions))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; RESTORE VERSION EVENTS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- restore-version + [id] + (assert (uuid? id) "expected valid uuid for `id`") + (ptk/reify ::restore-version + ptk/WatchEvent + (watch [_ state _] + (let [file-id (:current-file-id state)] + (rx/concat + (rx/of ::dwp/force-persist + (dw/remove-layout-flag :document-history)) + + (->> (wait-for-persistence file-id id) + (rx/map #(initialize-version)))))))) + +(defn enter-restore + [id] + (assert (uuid? id) "expected valid uuid for `id`") + (ptk/reify ::enter-restore + ptk/WatchEvent + (watch [_ _ _] + (let [output-s (rx/subject)] + (rx/merge + output-s + (rx/of (ntf/dialog + :content (tr "workspace.versions.restore-warning") + :controls :inline-actions + :cancel {:label (tr "workspace.updates.dismiss") + :callback #(do + (rx/push! output-s (ntf/hide :tag :restore-dialog)) + (rx/end! output-s))} + :accept {:label (tr "labels.restore") + :callback #(do + (rx/push! output-s (restore-version id)) + (rx/end! output-s))} + :tag :restore-dialog))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; PREVIEW VERSION EVENTS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- apply-snapshot + "Swap the file data in app state with the provided snapshot-file + response. Used by the version preview feature to show historical + file content without modifying the database" + [{:keys [id] :as snapshot}] + (ptk/reify ::apply-snapshot-data + ptk/UpdateEvent + (update [_ state] + (update state :files assoc id snapshot)))) + +(defn exit-preview + "Exit from preview mode and reload the live file data" + [] + (ptk/reify ::exit-preview + ptk/UpdateEvent + (update [_ state] + (let [backup (dm/get-in state [:workspace-versions :backup])] + (-> state + (update :workspace-versions dissoc :backup) + (update :workspace-global dissoc :read-only? :preview-id) + (update :files assoc (:id backup) backup)))) + + ptk/WatchEvent + (watch [_ state _] + (let [file-id (:current-file-id state) + page-id (:current-page-id state)] + + (rx/of (dwpg/initialize-page file-id page-id)))))) + +(defn enter-preview + "Load a snapshot into the workspace for read-only preview without + modifying any database state. Sets a read-only flag so no changes + are persisted while previewing and enter on the preview mode" + [id] + (assert (uuid? id) "expected valid uuid for `id`") + + (ptk/reify ::enter-preview + ptk/UpdateEvent + (update [_ state] + (let [file (dsh/lookup-file state)] + (-> state + (update :workspace-versions assoc :backup file) + (update :workspace-global assoc :read-only? true :preview-id id)))) + + ptk/WatchEvent + (watch [_ state _] + (let [file-id (:current-file-id state) + page-id (:current-page-id state) + team-id (:current-team-id state) + features (features/get-enabled-features state team-id) + snapshot (->> (dm/get-in state [:workspace-versions :data]) + (d/seek #(= id (:id %)))) + label (or (:label snapshot) + (tr "workspace.versions.preview.unnamed")) + output-s (rx/subject)] + (rx/merge + output-s + + (rx/of (ntf/dialog + :content (tr "workspace.versions.preview-banner-title" label) + :controls :inline-actions + :cancel {:label (tr "labels.exit") + :callback #(do + (rx/push! output-s (ntf/hide)) + (rx/push! output-s (exit-preview)) + (rx/end! output-s))} + :accept {:label (tr "labels.restore") + :callback #(do + (rx/push! output-s (ntf/hide)) + (rx/push! output-s (restore-version id)) + (rx/end! output-s))} + :tag :preview-dialog)) + + (->> (rp/cmd! :get-file-snapshot + {:file-id file-id + :id id + :features features}) + (rx/mapcat + (fn [snapshot] + (rx/of + ;; Swap the file data in state with snapshot content. + ;; Passing id sets workspace-file-version-id, which + ;; causes the WASM viewport to reload its shape buffer. + (apply-snapshot snapshot) + ;; Re-initialize the page to rebuild its search index + ;; and page-local state with the new snapshot + ;; objects. + (dwpg/initialize-page file-id page-id)))) + + (rx/catch (fn [err] + ;; On error roll back the read-only flag so the + ;; user is not stuck in a broken preview state. + (log/error :hint "failed to load snapshot" :cause err :file-id file-id :snapshot-id id) + (rx/of (exit-preview)))))))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PLUGINS SPECIFIC EVENTS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -246,20 +364,18 @@ (ptk/reify ::restore-version-from-plugins ptk/WatchEvent - (watch [_ state _] - (let [team-id (:current-team-id state)] - (rx/concat - (rx/of (ev/event {::ev/name "restore-version-plugin" - :file-id file-id - :team-id team-id}) - ::dwp/force-persist) + (watch [_ _ _] + (rx/concat + (rx/of (ev/event {::ev/name "restore-version" + ::ev/origin "plugins"}) + ::dwp/force-persist) - (->> (wait-for-persistence file-id id) - (rx/map #(initialize-version))) + (->> (wait-for-persistence file-id id) + (rx/map #(initialize-version))) - (->> (rx/of 1) - (rx/tap resolve) - (rx/ignore))))))) + (->> (rx/of 1) + (rx/tap resolve) + (rx/ignore)))))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/versions.cljs b/frontend/src/app/main/ui/workspace/sidebar/versions.cljs index 37edf428cd..0289f7c2f6 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/versions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/versions.cljs @@ -11,7 +11,7 @@ [app.common.time :as ct] [app.common.uuid :as uuid] [app.config :as cfg] - [app.main.data.notifications :as ntf] + [app.main.data.event :as ev] [app.main.data.workspace.versions :as dwv] [app.main.refs :as refs] [app.main.store :as st] @@ -77,20 +77,49 @@ (assoc item :index index))) (reverse))) -(defn- open-restore-version-dialog - [origin id] - (st/emit! (ntf/dialog - :content (tr "workspace.versions.restore-warning") - :controls :inline-actions - :cancel {:label (tr "workspace.updates.dismiss") - :callback #(st/emit! (ntf/hide))} - :accept {:label (tr "labels.restore") - :callback #(st/emit! (dwv/restore-version id origin))} - :tag :restore-dialog))) +(defn- on-name-input-focus + [event] + (dom/select-text! (dom/get-target event))) + +(defn- extract-id-from-event + [event] + (-> event dom/get-current-target (dom/get-data "id") uuid/parse)) + +(defn- on-create-version + [] + (st/emit! (dwv/create-version))) + +(defn- on-edit-version + [id _event] + (st/emit! (dwv/update-versions-state {:editing id}))) + +(defn- on-cancel-version-edition + [_id _event] + (st/emit! (dwv/update-versions-state {:editing nil}))) + +(defn- on-rename-version + [id label] + (st/emit! (dwv/rename-version id label))) + +(defn- on-delete-version + [id] + (st/emit! (dwv/delete-version id))) + +(defn- on-pin-version + [id] + (st/emit! (dwv/pin-version id))) + +(defn- on-lock-version + [id] + (st/emit! (dwv/lock-version id))) + +(defn- on-unlock-version + [id] + (st/emit! (dwv/unlock-version id))) (mf/defc version-entry* {::mf/private true} - [{:keys [entry current-profile on-restore on-delete on-rename on-lock on-unlock on-edit on-cancel-edit is-editing]}] + [{:keys [entry current-profile on-preview on-restore on-delete on-rename on-lock on-unlock on-edit on-cancel-edit is-editing]}] (let [show-menu? (mf/use-state false) profiles (mf/deref refs/profiles) @@ -108,6 +137,13 @@ (fn [event] (on-edit (:id entry) event))) + on-preview + (mf/use-fn + (mf/deps entry on-preview) + (fn [] + (when (fn? on-preview) + (on-preview (:id entry))))) + on-restore (mf/use-fn (mf/deps entry on-restore) @@ -136,11 +172,6 @@ (when on-unlock (on-unlock (:id entry))))) - on-name-input-focus - (mf/use-fn - (fn [event] - (dom/select-text! (dom/get-target event)))) - on-name-input-blur (mf/use-fn (mf/deps entry on-rename on-cancel-edit) @@ -191,6 +222,11 @@ :on-click on-edit} (tr "labels.rename")]) + [:li {:class (stl/css :menu-option) + :role "button" + :on-click on-preview} + (tr "workspace.versions.button.preview")] + [:li {:class (stl/css :menu-option) :role "button" :on-click on-restore} @@ -216,7 +252,7 @@ (tr "labels.delete")])])]])) (mf/defc snapshot-entry* - [{:keys [entry on-pin-snapshot on-restore-snapshot]}] + [{:keys [entry on-pin-snapshot on-restore-snapshot on-preview-snapshot]}] (let [open-menu* (mf/use-state nil) entry-ref (mf/use-ref nil) @@ -225,23 +261,22 @@ (mf/use-fn (mf/deps on-pin-snapshot) (fn [event] - (let [node (dom/get-current-target event) - id (-> node - (dom/get-data "id") - (uuid/parse))] - (when (fn? on-pin-snapshot) - (on-pin-snapshot id event))))) + (when (fn? on-pin-snapshot) + (on-pin-snapshot (extract-id-from-event event) event)))) on-restore-snapshot (mf/use-fn (mf/deps on-restore-snapshot) (fn [event] - (let [node (dom/get-current-target event) - id (-> node - (dom/get-data "id") - (uuid/parse))] - (when (fn? on-restore-snapshot) - (on-restore-snapshot id event))))) + (when (fn? on-restore-snapshot) + (on-restore-snapshot (extract-id-from-event event) event)))) + + on-preview-snapshot + (mf/use-fn + (mf/deps on-preview-snapshot) + (fn [event] + (when (fn? on-preview-snapshot) + (on-preview-snapshot (extract-id-from-event event) event)))) on-open-snapshot-menu (mf/use-fn @@ -266,6 +301,11 @@ :on-close #(reset! open-menu* nil)} [:ul {:class (stl/css :version-options-dropdown) :style {"--offset" (dm/str (:offset @open-menu*) "px")}} + [:li {:class (stl/css :menu-option) + :role "button" + :data-id (dm/str (:snapshot @open-menu*)) + :on-click on-preview-snapshot} + (tr "workspace.versions.button.preview")] [:li {:class (stl/css :menu-option) :role "button" :data-id (dm/str (:snapshot @open-menu*)) @@ -302,66 +342,50 @@ (= (:filter state) (:profile-id %))))) (group-snapshots))) - on-create-version + on-preview-version (mf/use-fn - (fn [] (st/emit! (dwv/create-version)))) + (fn [id] + (st/emit! (dwv/enter-preview id) + (ev/event {::ev/name "preview-version" + ::ev/origin "workspace:sidebar" + :type "pinned-version"})))) - on-edit-version + on-preview-snapshot (mf/use-fn (fn [id _event] - (st/emit! (dwv/update-versions-state {:editing id})))) - - on-cancel-version-edition - (mf/use-fn - (fn [_id _event] - (st/emit! (dwv/update-versions-state {:editing nil})))) - - on-rename-version - (mf/use-fn - (fn [id label] - (st/emit! (dwv/rename-version id label)))) + (st/emit! (dwv/enter-preview id) + (ev/event {::ev/name "preview-version" + ::ev/origin "workspace:sidebar" + :type "autosaved-version"})))) on-restore-version (mf/use-fn (fn [id _event] - (open-restore-version-dialog :version id))) + (st/emit! (dwv/enter-restore id) + (ev/event {::ev/name "restore-version" + ::ev/origin "workspace:sidebar" + :type "pinned-version"})))) on-restore-snapshot (mf/use-fn (fn [id _event] - (open-restore-version-dialog :snapshot id))) - - on-delete-version - (mf/use-fn - (fn [id] - (st/emit! (dwv/delete-version id)))) - - on-pin-version - (mf/use-fn - (fn [id] (st/emit! (dwv/pin-version id)))) - - on-lock-version - (mf/use-fn - (fn [id] - (st/emit! (dwv/lock-version id)))) - - on-unlock-version - (mf/use-fn - (fn [id] - (st/emit! (dwv/unlock-version id)))) + (st/emit! (dwv/enter-restore id) + (ev/event {::ev/name "restore-version" + ::ev/origin "workspace:sidebar" + :type "autosaved-version"})))) on-change-filter (mf/use-fn - (fn [filter] + (fn [filter-value] (cond - (= :all filter) + (= :all filter-value) (st/emit! (dwv/update-versions-state {:filter nil})) - (= :own filter) + (= :own filter-value) (st/emit! (dwv/update-versions-state {:filter (:id profile)})) :else - (st/emit! (dwv/update-versions-state {:filter filter}))))) + (st/emit! (dwv/update-versions-state {:filter filter-value}))))) options (mf/with-memo [users profile] @@ -415,6 +439,7 @@ :on-edit on-edit-version :on-cancel-edit on-cancel-version-edition :on-rename on-rename-version + :on-preview on-preview-version :on-restore on-restore-version :on-delete on-delete-version :on-lock on-lock-version @@ -423,6 +448,7 @@ :snapshot [:> snapshot-entry* {:key (:index entry) :entry entry + :on-preview-snapshot on-preview-snapshot :on-restore-snapshot on-restore-snapshot :on-pin-snapshot on-pin-version}] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index a7bb343fb4..5e7e5fd59c 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -97,6 +97,7 @@ {:keys [options-mode tooltip + preview-id show-distances? picking-color?]} wglobal @@ -314,23 +315,28 @@ (hooks/setup-shortcuts path-editing? path-drawing? text-editing? grid-editing?) (hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox) - [:div {:class (stl/css :viewport) :style #js {"--zoom" zoom} :data-testid "viewport"} - (when (:can-edit permissions) - (if read-only? - [:> view-only-bar* {}] - [:* - (when-not hide-ui? - [:> top-toolbar* {:layout layout}]) + [:div {:class (stl/css :viewport) :style {"--zoom" zoom} :data-testid "viewport"} + (cond + (some? preview-id) + nil - (when (and ^boolean path-editing? - ^boolean single-select?) - [:> path-edition-bar* {:shape editing-shape - :edit-path-state edit-path-state - :layout layout}]) + (and read-only? (:can-edit permissions)) + [:> view-only-bar* {}] - (when (and ^boolean grid-editing? - ^boolean single-select?) - [:> grid-edition-bar* {:shape editing-shape}])])) + :else + [:* + (when-not hide-ui? + [:> top-toolbar* {:layout layout}]) + + (when (and ^boolean path-editing? + ^boolean single-select?) + [:> path-edition-bar* {:shape editing-shape + :edit-path-state edit-path-state + :layout layout}]) + + (when (and ^boolean grid-editing? + ^boolean single-select?) + [:> grid-edition-bar* {:shape editing-shape}])]) [:div {:class (stl/css :viewport-overlays)} ;; The behaviour inside a foreign object is a bit different that in plain HTML so we wrap diff --git a/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs b/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs index b9f4f69cb4..2fca82347f 100644 --- a/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs @@ -20,14 +20,12 @@ ;; branch. (mf/defc view-only-bar* - {::mf/private true} [] - (let [handle-close-view-mode + (let [on-close (mf/use-fn - (fn [] - (st/emit! :interrupt - (dw/set-options-mode :design) - (dwc/set-workspace-read-only false))))] + #(st/emit! :interrupt + (dw/set-options-mode :design) + (dwc/set-workspace-read-only false)))] [:div {:class (stl/css :viewport-actions)} [:div {:class (stl/css :viewport-actions-container)} [:div {:class (stl/css :viewport-actions-title)} @@ -35,7 +33,7 @@ {:tag-name "span" :content (tr "workspace.top-bar.view-only")}]] [:button {:class (stl/css :done-btn) - :on-click handle-close-view-mode} + :on-click on-close} (tr "workspace.top-bar.read-only.done")]]])) (mf/defc path-edition-bar* diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index eb61350ba7..36d9d7e8e6 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -98,6 +98,7 @@ {:keys [options-mode tooltip show-distances? + preview-id picking-color?]} wglobal @@ -456,22 +457,28 @@ (hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox) [:div {:class (stl/css :viewport) :style #js {"--zoom" zoom} :data-testid "viewport"} - (when (:can-edit permissions) - (if read-only? - [:> view-only-bar* {}] - [:* - (when-not hide-ui? - [:> top-toolbar* {:layout layout}]) - (when (and ^boolean path-editing? - ^boolean single-select?) - [:> path-edition-bar* {:shape editing-shape - :edit-path-state edit-path-state - :layout layout}]) + (cond + (some? preview-id) + nil - (when (and ^boolean grid-editing? - ^boolean single-select?) - [:> grid-edition-bar* {:shape editing-shape}])])) + (and read-only? (:can-edit permissions)) + [:> view-only-bar* {}] + + :else + [:* + (when-not hide-ui? + [:> top-toolbar* {:layout layout}]) + + (when (and ^boolean path-editing? + ^boolean single-select?) + [:> path-edition-bar* {:shape editing-shape + :edit-path-state edit-path-state + :layout layout}]) + + (when (and ^boolean grid-editing? + ^boolean single-select?) + [:> grid-edition-bar* {:shape editing-shape}])]) [:div {:class (stl/css :viewport-overlays)} (when show-comments? diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 54a33820ec..41308a2694 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3049,6 +3049,9 @@ msgstr "Resend invitation" msgid "labels.restore" msgstr "Restore" +msgid "labels.exit" +msgstr "Exit" + #: src/app/main/ui/components/progress.cljs:80, src/app/main/ui/static.cljs:299, src/app/main/ui/static.cljs:308, src/app/main/ui/static.cljs:419 msgid "labels.retry" msgstr "Retry" @@ -9064,6 +9067,9 @@ msgstr "Autosaved %s" msgid "workspace.versions.button.pin" msgstr "Pin version" +msgid "workspace.versions.button.preview" +msgstr "Preview version" + #: src/app/main/ui/workspace/sidebar/versions.cljs:273 msgid "workspace.versions.button.restore" msgstr "Restore version" @@ -9108,6 +9114,12 @@ msgstr "This version is locked by %s and cannot be modified" msgid "workspace.versions.locked-by-you" msgstr "This version is locked by you" +msgid "workspace.versions.preview-banner-title" +msgstr "Previewing version: %s" + +msgid "workspace.versions.preview.unnamed" +msgstr "Unnamed version" + #: src/app/main/ui/workspace/sidebar/versions.cljs:83 msgid "workspace.versions.restore-warning" msgstr "Do you want to restore this version?" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 6855d5fa84..d0da81200b 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -2976,6 +2976,9 @@ msgstr "Reenviar invitacion" msgid "labels.restore" msgstr "Restaurar" +msgid "labels.exit" +msgstr "Salir" + #: src/app/main/ui/components/progress.cljs:80, src/app/main/ui/static.cljs:299, src/app/main/ui/static.cljs:308, src/app/main/ui/static.cljs:419 msgid "labels.retry" msgstr "Reintentar" @@ -8869,6 +8872,9 @@ msgstr "Versiones de %s" msgid "workspace.versions.loading" msgstr "Cargando..." +msgid "workspace.versions.preview-banner-title" +msgstr "Previsualizando version: %s" + #: src/app/main/ui/workspace/sidebar/versions.cljs:83 msgid "workspace.versions.restore-warning" msgstr "¿Quieres restaurar esta versión?" From 50bee5e1760f61be51c84ad9a9af9ce19d847920 Mon Sep 17 00:00:00 2001 From: wdeveloper16 Date: Fri, 24 Apr 2026 09:07:58 +0200 Subject: [PATCH 210/288] :sparkles: Add clipboard:read/write permissions to plugin system (#6980) (#9053) * :sparkles: Add clipboard:read/write permissions to plugin system (#6980) * :wrench: Fix prettier formatting in clipboard permission files --------- Co-authored-by: wdeveloper16 Co-authored-by: Andrey Antukh --- .../src/app/main/ui/workspace/plugins.cljs | 15 +++- frontend/src/app/plugins/register.cljs | 5 +- frontend/translations/en.po | 8 +++ .../plugins-runtime/src/lib/api/openUI.api.ts | 26 ++++++- .../src/lib/create-modal.spec.ts | 69 +++++++++++++++++++ .../plugins-runtime/src/lib/create-modal.ts | 10 +++ .../src/lib/modal/plugin-modal.spec.ts | 29 ++++++++ .../src/lib/modal/plugin-modal.ts | 11 ++- .../src/lib/models/manifest.schema.ts | 2 + .../src/lib/plugin-manager.spec.ts | 2 + .../plugins-runtime/src/lib/plugin-manager.ts | 18 ++++- 11 files changed, 188 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/plugins.cljs b/frontend/src/app/main/ui/workspace/plugins.cljs index fc319bf925..f4068c585a 100644 --- a/frontend/src/app/main/ui/workspace/plugins.cljs +++ b/frontend/src/app/main/ui/workspace/plugins.cljs @@ -302,7 +302,20 @@ [:div {:class (stl/css :permissions-list-entry)} deprecated-icon/oauth-1 [:p {:class (stl/css :permissions-list-text)} - (tr "workspace.plugins.permissions.allow-localstorage")]])]) + (tr "workspace.plugins.permissions.allow-localstorage")]]) + + (cond + (contains? permissions "clipboard:write") + [:div {:class (stl/css :permissions-list-entry)} + deprecated-icon/oauth-1 + [:p {:class (stl/css :permissions-list-text)} + (tr "workspace.plugins.permissions.clipboard-write")]] + + (contains? permissions "clipboard:read") + [:div {:class (stl/css :permissions-list-entry)} + deprecated-icon/oauth-1 + [:p {:class (stl/css :permissions-list-text)} + (tr "workspace.plugins.permissions.clipboard-read")]])]) (mf/defc plugins-permissions-dialog {::mf/register modal/components diff --git a/frontend/src/app/plugins/register.cljs b/frontend/src/app/plugins/register.cljs index e3792f3fc9..df9afb6380 100644 --- a/frontend/src/app/plugins/register.cljs +++ b/frontend/src/app/plugins/register.cljs @@ -54,7 +54,10 @@ (conj "library:read") (contains? permissions "comment:write") - (conj "comment:read")) + (conj "comment:read") + + (contains? permissions "clipboard:write") + (conj "clipboard:read")) plugin-url (u/uri plugin-url) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 41308a2694..5de7250a59 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7646,6 +7646,14 @@ msgstr "" msgid "workspace.plugins.permissions.allow-download" msgstr "Start file downloads." +#: src/app/main/ui/workspace/plugins.cljs +msgid "workspace.plugins.permissions.clipboard-read" +msgstr "Read the contents of your clipboard." + +#: src/app/main/ui/workspace/plugins.cljs +msgid "workspace.plugins.permissions.clipboard-write" +msgstr "Read and write to your clipboard." + #: src/app/main/ui/workspace/plugins.cljs:287 msgid "workspace.plugins.permissions.allow-localstorage" msgstr "Store data in the browser." diff --git a/plugins/libs/plugins-runtime/src/lib/api/openUI.api.ts b/plugins/libs/plugins-runtime/src/lib/api/openUI.api.ts index dbbeba184c..3e56ea15bb 100644 --- a/plugins/libs/plugins-runtime/src/lib/api/openUI.api.ts +++ b/plugins/libs/plugins-runtime/src/lib/api/openUI.api.ts @@ -10,7 +10,27 @@ export const openUIApi = z z.enum(['dark', 'light']), openUISchema.optional(), z.boolean().optional(), + z.boolean().optional(), + z.boolean().optional(), ) - .implement((title, url, theme, options, allowDownloads) => { - return createModal(title, url, theme, options, allowDownloads); - }); + .implement( + ( + title, + url, + theme, + options, + allowDownloads, + allowClipboardRead, + allowClipboardWrite, + ) => { + return createModal( + title, + url, + theme, + options, + allowDownloads, + allowClipboardRead, + allowClipboardWrite, + ); + }, + ); diff --git a/plugins/libs/plugins-runtime/src/lib/create-modal.spec.ts b/plugins/libs/plugins-runtime/src/lib/create-modal.spec.ts index 80d28fe56c..4cefab63c2 100644 --- a/plugins/libs/plugins-runtime/src/lib/create-modal.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/create-modal.spec.ts @@ -104,4 +104,73 @@ describe('createModal', () => { expect(modal.wrapper.style.width).toEqual('200px'); expect(modal.wrapper.style.height).toEqual('200px'); }); + + it('should set allow-clipboard-read attribute when allowClipboardRead is true', () => { + const theme: Theme = 'light'; + + createModal( + 'Test Modal', + 'https://example.com', + theme, + undefined, + false, + true, + false, + ); + + expect(modalMock.setAttribute).toHaveBeenCalledWith( + 'allow-clipboard-read', + 'true', + ); + expect(modalMock.setAttribute).not.toHaveBeenCalledWith( + 'allow-clipboard-write', + 'true', + ); + }); + + it('should set allow-clipboard-write attribute when allowClipboardWrite is true', () => { + const theme: Theme = 'light'; + + createModal( + 'Test Modal', + 'https://example.com', + theme, + undefined, + false, + false, + true, + ); + + expect(modalMock.setAttribute).toHaveBeenCalledWith( + 'allow-clipboard-write', + 'true', + ); + expect(modalMock.setAttribute).not.toHaveBeenCalledWith( + 'allow-clipboard-read', + 'true', + ); + }); + + it('should set both clipboard attributes when both are true', () => { + const theme: Theme = 'light'; + + createModal( + 'Test Modal', + 'https://example.com', + theme, + undefined, + false, + true, + true, + ); + + expect(modalMock.setAttribute).toHaveBeenCalledWith( + 'allow-clipboard-read', + 'true', + ); + expect(modalMock.setAttribute).toHaveBeenCalledWith( + 'allow-clipboard-write', + 'true', + ); + }); }); diff --git a/plugins/libs/plugins-runtime/src/lib/create-modal.ts b/plugins/libs/plugins-runtime/src/lib/create-modal.ts index 01422c76b6..d6cd2d1623 100644 --- a/plugins/libs/plugins-runtime/src/lib/create-modal.ts +++ b/plugins/libs/plugins-runtime/src/lib/create-modal.ts @@ -10,6 +10,8 @@ export function createModal( theme: Theme, options?: OpenUIOptions, allowDownloads?: boolean, + allowClipboardRead?: boolean, + allowClipboardWrite?: boolean, ) { const modal = document.createElement('plugin-modal') as PluginModalElement; @@ -44,6 +46,14 @@ export function createModal( modal.setAttribute('allow-downloads', 'true'); } + if (allowClipboardRead) { + modal.setAttribute('allow-clipboard-read', 'true'); + } + + if (allowClipboardWrite) { + modal.setAttribute('allow-clipboard-write', 'true'); + } + document.body.appendChild(modal); return modal; diff --git a/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts b/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts index d7b774c2c3..fb19f291ab 100644 --- a/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts @@ -99,6 +99,35 @@ describe('PluginModalElement', () => { modal.remove(); }); + it('should set iframe allow attribute for clipboard permissions', () => { + const modal = document.createElement('plugin-modal'); + modal.setAttribute('title', 'Test modal'); + modal.setAttribute('iframe-src', 'about:blank'); + modal.setAttribute('allow-clipboard-read', 'true'); + modal.setAttribute('allow-clipboard-write', 'true'); + document.body.appendChild(modal); + + const iframe = modal.shadowRoot?.querySelector('iframe'); + expect(iframe).toBeTruthy(); + expect(iframe?.allow).toContain('clipboard-read'); + expect(iframe?.allow).toContain('clipboard-write'); + + modal.remove(); + }); + + it('should not set clipboard allow attributes when permissions are absent', () => { + const modal = document.createElement('plugin-modal'); + modal.setAttribute('title', 'Test modal'); + modal.setAttribute('iframe-src', 'about:blank'); + document.body.appendChild(modal); + + const iframe = modal.shadowRoot?.querySelector('iframe'); + expect(iframe).toBeTruthy(); + expect(iframe?.allow).toBe(''); + + modal.remove(); + }); + it('should dispatch close event when close button is clicked', () => { const modal = document.createElement('plugin-modal'); modal.setAttribute('title', 'Test modal'); diff --git a/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.ts b/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.ts index c61ad7fce5..53ea472494 100644 --- a/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.ts +++ b/plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.ts @@ -52,6 +52,10 @@ export class PluginModalElement extends HTMLElement { const title = this.getAttribute('title'); const iframeSrc = this.getAttribute('iframe-src'); const allowDownloads = this.getAttribute('allow-downloads') || false; + const allowClipboardRead = + this.getAttribute('allow-clipboard-read') || false; + const allowClipboardWrite = + this.getAttribute('allow-clipboard-write') || false; if (!title || !iframeSrc) { throw new Error('title and iframe-src attributes are required'); @@ -95,7 +99,12 @@ export class PluginModalElement extends HTMLElement { const iframe = document.createElement('iframe'); iframe.src = iframeSrc; - iframe.allow = ''; + + const allowList: string[] = []; + if (allowClipboardRead) allowList.push('clipboard-read'); + if (allowClipboardWrite) allowList.push('clipboard-write'); + iframe.allow = allowList.join('; '); + iframe.sandbox.add( 'allow-scripts', 'allow-forms', diff --git a/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts b/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts index bd5895c02b..16d4fd5e28 100644 --- a/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts +++ b/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts @@ -19,6 +19,8 @@ export const manifestSchema = z.object({ 'comment:write', 'allow:downloads', 'allow:localstorage', + 'clipboard:read', + 'clipboard:write', ]), ), }); diff --git a/plugins/libs/plugins-runtime/src/lib/plugin-manager.spec.ts b/plugins/libs/plugins-runtime/src/lib/plugin-manager.spec.ts index 206f43ba34..d53f4f5296 100644 --- a/plugins/libs/plugins-runtime/src/lib/plugin-manager.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/plugin-manager.spec.ts @@ -123,6 +123,8 @@ describe('createPluginManager', () => { 'light', { width: 400, height: 300 }, true, + false, + false, ); expect(mockModal.setTheme).toHaveBeenCalledWith('light'); expect(mockModal.addEventListener).toHaveBeenCalledWith( diff --git a/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts b/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts index 0b4035794a..8b811f55eb 100644 --- a/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts +++ b/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts @@ -27,6 +27,14 @@ export async function createPluginManager( (s) => s === 'allow:downloads', ); + const allowClipboardRead = !!manifest.permissions.find( + (s) => s === 'clipboard:read', + ); + + const allowClipboardWrite = !!manifest.permissions.find( + (s) => s === 'clipboard:write', + ); + const themeChangeId = context.addListener('themechange', (theme: Theme) => { modal?.setTheme(theme); }); @@ -91,7 +99,15 @@ export async function createPluginManager( return; } - modal = openUIApi(name, modalUrl, theme, options, allowDownloads); + modal = openUIApi( + name, + modalUrl, + theme, + options, + allowDownloads, + allowClipboardRead, + allowClipboardWrite, + ); modal.setTheme(theme); From 8aacda22499b8c9aa8ec3f97665efffba581b472 Mon Sep 17 00:00:00 2001 From: Renzo <170978465+RenzoMXD@users.noreply.github.com> Date: Fri, 24 Apr 2026 09:08:31 +0200 Subject: [PATCH 211/288] :sparkles: Add Shift+Numpad0/1/2 zoom shortcut aliases (#2457) (#9063) Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com> --- CHANGES.md | 2 +- frontend/packages/mousetrap/index.js | 8 ++++++++ frontend/src/app/main/data/workspace/shortcuts.cljs | 8 ++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4e63461ac5..ad5948645b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -46,7 +46,7 @@ - Add a search bar to filter colors in the color palette toolbar (by @eureka0928) [Github #7653](https://github.com/penpot/penpot/issues/7653) - Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027) - Add page separators in Workspace [Taiga #13611](https://tree.taiga.io/project/penpot/us/13611?milestone=262806) - +- Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts [Github #2457](https://github.com/penpot/penpot/issues/2457) ### :bug: Bugs fixed diff --git a/frontend/packages/mousetrap/index.js b/frontend/packages/mousetrap/index.js index 5a0bc3e0bc..12bcbab1b9 100644 --- a/frontend/packages/mousetrap/index.js +++ b/frontend/packages/mousetrap/index.js @@ -187,6 +187,14 @@ function _addEvent(object, type, callback) { */ function _characterFromEvent(e) { + // Numpad digits as "num0".."num9" — keeps them separate from main-row bindings across NumLock states and event types. + if (e.code && e.code.indexOf('Numpad') === 0) { + var suffix = e.code.substring(6); + if (suffix.length === 1 && suffix >= '0' && suffix <= '9') { + return 'num' + suffix; + } + } + // for keypress events we should return the character as is if (e.type == 'keypress') { var character = String.fromCharCode(e.which); diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index a89662e8d3..e7ff9a99ed 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -514,17 +514,17 @@ :fn #(st/emit! (dw/decrease-zoom))} :reset-zoom {:tooltip (ds/shift "0") - :command "shift+0" + :command ["shift+0" "shift+num0"] :subsections [:zoom-workspace] :fn #(st/emit! dw/reset-zoom)} :fit-all {:tooltip (ds/shift "1") - :command "shift+1" + :command ["shift+1" "shift+num1"] :subsections [:zoom-workspace] :fn #(st/emit! dw/zoom-to-fit-all)} :zoom-selected {:tooltip (ds/shift "2") - :command ["shift+2" "@" "\""] + :command ["shift+2" "shift+num2" "@" "\""] :subsections [:zoom-workspace] :fn #(st/emit! dw/zoom-to-selected-shape)} @@ -626,7 +626,7 @@ (range 10) (map (fn [n] [(keyword (str "opacity-" n)) {:tooltip (str n) - :command (str n) + :command [(str n) (str "num" n)] :subsections [:modify-layers] :fn #(emit-when-no-readonly (dwly/pressed-opacity n))}]))))) From 6c7843f4b60d94cb05573874a3ae28fc28900f9d Mon Sep 17 00:00:00 2001 From: boskodev790 Date: Fri, 24 Apr 2026 02:09:49 -0500 Subject: [PATCH 212/288] :bug: Fix obfuscate-email crashing on malformed email or dotless domain (#9120) The viewer-side `obfuscate-email` helper used by `anonymize-member` when building share-link bundles called `clojure.string/split` on the raw email input and then on the extracted domain. Two failure modes: 1. When the stored email had no `@` (legacy data, LDAP-sourced UIDs, direct DB inserts, or fixtures that bypassed `::sm/email`), destructuring left `domain` bound to `nil` and the follow-up `(str/split nil "." 2)` raised `NullPointerException`. Because `obfuscate-email` runs inside `get-view-only-bundle`, the exception aborted the whole RPC response for share-link viewers, not just the field. 2. When the stored email used a single-label domain (`alice@localhost`), `(str/split "localhost" "." 2)` returned `["localhost"]`; destructuring bound `rest` to `nil` and the final `(str name "@****." rest)` produced a dangling-dot output `"****@****."` (nil coerces to empty in `str`). Guard both split calls with `(or x "")` so the chain is nil-safe, and emit the trailing `.` segment only when `rest` is present. Add three `deftest` groups covering the happy path, dotless domains, and malformed inputs (nil / empty / no-`@`), plus a CHANGES.md entry under the 2.17.0 Unreleased bugs-fixed section. Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + backend/src/app/rpc/commands/viewer.clj | 12 +++++++--- .../test/backend_tests/rpc_viewer_test.clj | 23 +++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ad5948645b..358adbed71 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,7 @@ ### :bug: Bugs fixed +- Fix `get-view-only-bundle` crashing when a share-link viewer encounters a team member whose email lacks `@` (NullPointerException in `obfuscate-email`) or whose domain has no `.` (previously produced a dangling-dot `****@****.`); now the viewer-side obfuscation is nil-safe and omits the trailing dot when the domain has no TLD - Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled [Github #8877](https://github.com/penpot/penpot/issues/8877) - Fix Copy as SVG: emit a single valid SVG document when multiple shapes are selected, and publish `image/svg+xml` to the clipboard so the paste target works in Inkscape and other SVG-native tools [Github #838](https://github.com/penpot/penpot/issues/838) - Reset profile submenu state when the account menu closes (by @eureka0928) [Github #8947](https://github.com/penpot/penpot/issues/8947) diff --git a/backend/src/app/rpc/commands/viewer.clj b/backend/src/app/rpc/commands/viewer.clj index d2b191aeb4..37adca244f 100644 --- a/backend/src/app/rpc/commands/viewer.clj +++ b/backend/src/app/rpc/commands/viewer.clj @@ -28,19 +28,25 @@ (update :pages-index select-keys allowed))) (defn obfuscate-email + "Obfuscate the `email` for share-link members so the viewer only sees a + partially redacted address. Accepts any string shape (including nil, + missing `@`, or a domain with no `.`) and falls back to a fully-masked + result rather than throwing — the function is called while building the + view-only bundle for anonymous viewers, so an NPE here would abort the + entire share-link response." [email] (let [[name domain] - (str/split email "@" 2) + (str/split (or email "") "@" 2) [_ rest] - (str/split domain "." 2) + (str/split (or domain "") "." 2) name (if (> (count name) 3) (str (subs name 0 1) (apply str (take (dec (count name)) (repeat "*")))) "****")] - (str name "@****." rest))) + (str name "@****" (when rest (str "." rest))))) (defn anonymize-member [member] diff --git a/backend/test/backend_tests/rpc_viewer_test.clj b/backend/test/backend_tests/rpc_viewer_test.clj index 6c68c12e34..1e69ed87af 100644 --- a/backend/test/backend_tests/rpc_viewer_test.clj +++ b/backend/test/backend_tests/rpc_viewer_test.clj @@ -9,6 +9,7 @@ [app.common.uuid :as uuid] [app.db :as db] [app.rpc :as-alias rpc] + [app.rpc.commands.viewer :as viewer] [backend-tests.helpers :as th] [clojure.test :as t] [datoteka.fs :as fs])) @@ -16,6 +17,28 @@ (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) +(t/deftest obfuscate-email-happy-path + (t/is (= "a****@****.com" (viewer/obfuscate-email "alice@example.com"))) + (t/is (= "a****@****.example.com" (viewer/obfuscate-email "alice@sub.example.com"))) + (t/is (= "****@****.com" (viewer/obfuscate-email "bob@bar.com")))) + +(t/deftest obfuscate-email-handles-domain-without-dot + ;; `localhost`-style domains have no `.`; the previous implementation produced + ;; a dangling-dot output like "a****@****." — now the trailing `.` is only + ;; emitted when there actually is a TLD segment to append. + (t/is (= "a****@****" (viewer/obfuscate-email "alice@localhost"))) + (t/is (= "****@****" (viewer/obfuscate-email "x@y")))) + +(t/deftest obfuscate-email-handles-malformed-input + ;; These shapes must not throw — `obfuscate-email` runs while building the + ;; view-only bundle for share-link viewers and an NPE here aborts the whole + ;; RPC response. The previous implementation called `clojure.string/split` + ;; on `nil` for the `no-@` case, raising NullPointerException. + (t/is (= "****@****" (viewer/obfuscate-email nil))) + (t/is (= "****@****" (viewer/obfuscate-email ""))) + (t/is (= "r***@****" (viewer/obfuscate-email "root"))) ; no `@`, count > 3 + (t/is (= "****@****" (viewer/obfuscate-email "bob")))) ; no `@`, count <= 3 + (t/deftest retrieve-bundle (let [prof (th/create-profile* 1 {:is-active true}) prof2 (th/create-profile* 2 {:is-active true}) From 841b2e156e657f6a889429448e71ae698ef1662f Mon Sep 17 00:00:00 2001 From: Juan Flores <112629487+juan-flores077@users.noreply.github.com> Date: Fri, 24 Apr 2026 00:11:31 -0700 Subject: [PATCH 213/288] :bug: Fix typography style creation with tokenized line-height (#9121) When a text element has a line-height coming from a design token, the value may be a number (e.g. 1.5) and fails frontend data validation expecting a string. Normalize line-height before creating the typography style so the operation succeeds without throwing an assertion error. Signed-off-by: juan-flores077 --- CHANGES.md | 2 +- frontend/src/app/main/data/workspace/texts.cljs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 358adbed71..cad6fe13a8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -78,7 +78,7 @@ - Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990) - Allow deleting the profile avatar after uploading [Github #9067](https://github.com/penpot/penpot/issues/9067) - Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [Github #8516](https://github.com/penpot/penpot/issues/8516) - +- Fix typography style creation with tokenized line-height (by @juan-flores077) [Github #8479](https://github.com/penpot/penpot/issues/8479) ## 2.16.0 (Unreleased) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index e3eccc05ab..bb604b3f2f 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -935,6 +935,12 @@ (d/concat-vec txt/text-font-attrs txt/text-spacing-attrs txt/text-transform-attrs))) + values (cond-> values + (number? (:line-height values)) + (update :line-height str) + + (number? (:letter-spacing values)) + (update :letter-spacing str)) typ-id (uuid/next) typ (-> (if multiple? From 361c1c574b8254f3d5ee815900abdeb0c267f566 Mon Sep 17 00:00:00 2001 From: FairyPiggyDev Date: Fri, 24 Apr 2026 03:12:13 -0400 Subject: [PATCH 214/288] :bug: Fix plugin parse-point returning plain map instead of Point record (#9129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugin parser's parse-point returned a plain `{:x … :y …}` map, but shape interaction schemas (for example schema:open-overlay-interaction) require the attribute to be a `::gpt/point` record. `(instance? Point {:x 0 :y 0})` is false, so validation silently rejected plugin `addInteraction` calls that passed `manualPositionLocation`; only a console warning was produced. Change parse-point to return a `gpt/point` record via `gpt/point`. All three call sites (parser.cljs:open-overlay, plugins/page.cljs, plugins/comments.cljs) continue to work because Point records support the same `:x`/`:y` access plain maps do. Add a unit test that covers nil input and verifies the returned value satisfies `gpt/point?`. Github #8409 Signed-off-by: FairyPigDev Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + frontend/src/app/plugins/parser.cljs | 11 +++++-- .../frontend_tests/plugins/parser_test.cljs | 33 +++++++++++++++++++ frontend/test/frontend_tests/runner.cljs | 2 ++ 4 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 frontend/test/frontend_tests/plugins/parser_test.cljs diff --git a/CHANGES.md b/CHANGES.md index cad6fe13a8..c83eb3249d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -78,6 +78,7 @@ - Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990) - Allow deleting the profile avatar after uploading [Github #9067](https://github.com/penpot/penpot/issues/9067) - Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [Github #8516](https://github.com/penpot/penpot/issues/8516) +- Fix plugin `addInteraction` silently rejecting `open-overlay` actions with `manualPositionLocation` [Github #8409](https://github.com/penpot/penpot/issues/8409) - Fix typography style creation with tokenized line-height (by @juan-flores077) [Github #8479](https://github.com/penpot/penpot/issues/8479) ## 2.16.0 (Unreleased) diff --git a/frontend/src/app/plugins/parser.cljs b/frontend/src/app/plugins/parser.cljs index 5d4148662d..29ec5c2bf7 100644 --- a/frontend/src/app/plugins/parser.cljs +++ b/frontend/src/app/plugins/parser.cljs @@ -7,6 +7,7 @@ (ns app.plugins.parser (:require [app.common.data :as d] + [app.common.geom.point :as gpt] [app.common.json :as json] [app.common.types.path :as path] [app.common.uuid :as uuid] @@ -26,10 +27,16 @@ (if (string? color) (-> color str/lower) color)) (defn parse-point + "Parses a point-like JS object into a `gpt/point` record. + + The schema for shape interactions (`schema:open-overlay-interaction`, + `::gpt/point`) requires a Point record — returning a plain map caused + plugin `addInteraction` calls with an `open-overlay` action and a + `manualPositionLocation` to be silently rejected. See issue #8409." [^js point] (when point - {:x (obj/get point "x") - :y (obj/get point "y")})) + (gpt/point (obj/get point "x") + (obj/get point "y")))) (defn parse-shape-type [type] diff --git a/frontend/test/frontend_tests/plugins/parser_test.cljs b/frontend/test/frontend_tests/plugins/parser_test.cljs new file mode 100644 index 0000000000..4b257b2023 --- /dev/null +++ b/frontend/test/frontend_tests/plugins/parser_test.cljs @@ -0,0 +1,33 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns frontend-tests.plugins.parser-test + (:require + [app.common.geom.point :as gpt] + [app.plugins.parser :as parser] + [cljs.test :as t :include-macros true])) + +(t/deftest test-parse-point-returns-gpt-point-record + ;; Regression test for issue #8409. + ;; + ;; The plugin parser used to return a plain map `{:x … :y …}`, but the + ;; shape-interaction schema expects `::gpt/point` (a Point record). + ;; Plugin `addInteraction` calls with an `open-overlay` action and + ;; `manualPositionLocation` were silently rejected by validation. + (t/testing "parse-point returns nil for nil input" + (t/is (nil? (parser/parse-point nil)))) + + (t/testing "parse-point returns a gpt/point record for valid input" + (let [result (parser/parse-point #js {:x 10 :y 20})] + (t/is (gpt/point? result)) + (t/is (= 10 (:x result))) + (t/is (= 20 (:y result))))) + + (t/testing "parse-point passes gpt/point? for a zero point" + (let [result (parser/parse-point #js {:x 0 :y 0})] + (t/is (gpt/point? result)) + (t/is (= 0 (:x result))) + (t/is (= 0 (:y result)))))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 174ef34056..f54d9b5002 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -20,6 +20,7 @@ [frontend-tests.logic.pasting-in-containers-test] [frontend-tests.main-errors-test] [frontend-tests.plugins.context-shapes-test] + [frontend-tests.plugins.parser-test] [frontend-tests.svg-fills-test] [frontend-tests.tokens.import-export-test] [frontend-tests.tokens.logic.token-actions-test] @@ -63,6 +64,7 @@ 'frontend-tests.logic.groups-test 'frontend-tests.logic.pasting-in-containers-test 'frontend-tests.plugins.context-shapes-test + 'frontend-tests.plugins.parser-test 'frontend-tests.svg-fills-test 'frontend-tests.tokens.import-export-test 'frontend-tests.tokens.logic.token-actions-test From 25e6b939ba49f726008168705c5f7b9483b2a5a5 Mon Sep 17 00:00:00 2001 From: Full Stack Developer <30417830+jsdevninja@users.noreply.github.com> Date: Fri, 24 Apr 2026 02:13:46 -0500 Subject: [PATCH 215/288] :sparkles: Show detailed messages on file import errors (#9004) * :sparkles: Show detailed messages on file import errors Signed-off-by: jsdevninja * :sparkles: Fix test * :sparkles: Fix build error --------- Signed-off-by: jsdevninja --- .../src/app/main/ui/dashboard/import.cljs | 11 +++++-- .../src/app/main/ui/dashboard/import.scss | 17 +++++++++- frontend/src/app/worker/import.cljs | 31 ++++++++++++++----- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 6b4fe68678..1aa4282d7a 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -295,7 +295,9 @@ import-error? [:div {:class (stl/css :error-message)} - (tr "labels.error")] + (if (some? (:error entry)) + (tr (:error entry)) + (tr "labels.error"))] (and (not import-success?) (some? progress)) [:div {:class (stl/css :progress-message)} (parse-progress-message progress)]) @@ -491,7 +493,12 @@ [:ul {:class (stl/css :import-error-list)} (for [entry entries] (when (contains? #{:import-error :analyze-error} (:status entry)) - [:li {:class (stl/css :import-error-list-enry)} (:name entry)]))] + [:li {:class (stl/css :import-error-list-enry) + :key (dm/str (or (:file-id entry) (:uri entry) (:name entry)))} + [:div (:name entry)] + (when-let [err (:error entry)] + [:div {:class (stl/css :import-error-detail)} + (tr err)])]))] [:div (tr "dashboard.import.import-error.message2")]] (for [entry entries] diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss index 7d8d0ff428..2d3cb22e67 100644 --- a/frontend/src/app/main/ui/dashboard/import.scss +++ b/frontend/src/app/main/ui/dashboard/import.scss @@ -149,10 +149,16 @@ .progress-message { display: flex; align-items: center; - height: deprecated.$s-32; + min-height: deprecated.$s-32; color: var(--modal-text-foreground-color); } + .error-message { + align-items: flex-start; + white-space: pre-wrap; + overflow-wrap: anywhere; + } + .linked-library { display: flex; align-items: center; @@ -258,3 +264,12 @@ .import-error-list-enry { padding: var(--sp-xs) 0; } + +.import-error-detail { + @include deprecated.body-small-typography; + + margin-top: var(--sp-xs); + color: var(--modal-text-foreground-color); + white-space: pre-wrap; + overflow-wrap: anywhere; +} diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 20c314f012..402da4ad5c 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -23,6 +23,22 @@ (log/set-level! :warn) +(defn- import-cause-message + "Prefer the server `:hint` (full text, e.g. SSE error payload), then `:explain` + when present; avoid the generic `stream exception` wrapper when a payload exists." + [cause default-msg] + (let [data (ex-data cause) + hint (some-> data :hint str/trim) + explain (some-> data :explain str/trim)] + (cond + (not (str/blank? hint)) hint + (not (str/blank? explain)) explain + :else + (let [msg (some-> (ex-message cause) str/trim)] + (if (or (str/blank? msg) (= msg "stream exception")) + default-msg + msg))))) + ;; Upload changes batches size (def ^:const change-batch-size 100) @@ -122,7 +138,7 @@ :error (tr "dashboard.import.analyze-error")})))) (rx/catch (fn [cause] - (let [error (or (ex-message cause) (tr "dashboard.import.analyze-error"))] + (let [error (import-cause-message cause (tr "dashboard.import.analyze-error"))] (rx/of (assoc file :error error :status :error)))))))) (defmethod impl/handler :analyze-import @@ -178,7 +194,7 @@ :project-id project-id :cause cause) (rx/of {:status :error - :error (ex-message cause) + :error (import-cause-message cause (tr "labels.error")) :file-id (:file-id data)}))))))) (->> (rx/from binfile-v3) @@ -212,8 +228,9 @@ :project-id project-id ::log/sync? true :cause cause) - (->> (rx/from entries) - (rx/map (fn [entry] - {:status :error - :error (ex-message cause) - :file-id (:file-id entry)})))))))))))) + (let [err (import-cause-message cause (tr "labels.error"))] + (->> (rx/from entries) + (rx/map (fn [entry] + {:status :error + :error err + :file-id (:file-id entry)}))))))))))))) From 078663b0faefd60174f76cf7dc6af319fcbd5f37 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 24 Apr 2026 09:52:51 +0200 Subject: [PATCH 216/288] :wrench: Fix rust linter errors --- frontend/src/app/main/ui/workspace/main_menu.cljs | 6 +++--- frontend/src/app/render_wasm/api.cljs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 9215f49d82..77203dc826 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -776,12 +776,12 @@ (mf/defc mcp-menu* {::mf/private true} [{:keys [on-close]}] - (let [plugins? (features/active-feature? @st/state "plugins/runtime") - + (let [plugins? (features/active-feature? @st/state "plugins/runtime") + profile (mf/deref refs/profile) mcp (mf/deref refs/mcp) tokens (mf/deref refs/access-tokens) - + expired? (some->> tokens (some #(when (= (:type %) "mcp") %)) :expires-at diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index fc79e7f584..9a06f0bcf1 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -691,7 +691,7 @@ (h/call wasm/internal-module "_add_shape_stroke_fill") (when (== cached-image? 0) (fetch-image shape-id image-id thumbnail?))) - + (some? color) (do (types.fills.impl/write-solid-fill offset dview opacity color) From 58fae0a04ded5ca3206ef1b26f106f3136712c4a Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 24 Apr 2026 10:10:00 +0200 Subject: [PATCH 217/288] :bug: Fix text.cljs error from staging merge --- .../ui/workspace/sidebar/options/menus/text.cljs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 974a943b55..504856d22e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -429,12 +429,12 @@ (when (not= "INPUT" (-> (dom/get-active) dom/get-tag-name)) (dom/focus! (txu/get-text-editor-content))))))) - opts (mf/props - {:ids ids - :values values - :on-change on-change - :show-recent true - :on-blur on-text-blur})] + common-props (mf/props + {:ids ids + :values values + :on-change on-change + :show-recent true + :on-blur on-text-blur})] (hooks/use-stream expand-stream @@ -498,11 +498,11 @@ :icon i/detach}]] :else - [:> text-options* opts]) + [:> text-options* common-props]) [:div {:class (stl/css :text-align-options)} [:> text-align-options* common-props] - [:> grow-options* (mf/spread-props common-props {:ids ids})] + [:> grow-options* common-props] [:> icon-button* {:variant "ghost" :aria-label (tr "labels.options") :data-testid "text-align-options-button" From 406167352885204e6ad5e47de60af173b6cb0457 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Fri, 24 Apr 2026 11:35:53 +0200 Subject: [PATCH 218/288] :sparkles: Add nitrate api endpoints to get and cancel org invitations (#9124) * :sparkles: Add nitrate api endpoints to get and cancel org invitations * :sparkles: MR changes --- backend/src/app/rpc/management/nitrate.clj | 82 +++++++ .../rpc_management_nitrate_test.clj | 203 ++++++++++++++++++ 2 files changed, 285 insertions(+) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 58a06e5c65..6762de6e80 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -16,6 +16,7 @@ [app.config :as cf] [app.db :as db] [app.media :as media] + [app.nitrate :as nitrate] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] [app.rpc.commands.nitrate :as cnit] @@ -375,6 +376,87 @@ RETURNING id, name;") nil) +;; API: get-org-invitations + +(def ^:private sql:get-org-invitations + "SELECT DISTINCT ON (email_to) + ti.id, + ti.org_id AS organization_id, + ti.email_to AS email, + ti.created_at AS sent_at, + p.fullname AS name, + p.photo_id + FROM team_invitation AS ti +LEFT JOIN profile AS p + ON p.email = ti.email_to + AND p.deleted_at IS NULL + WHERE ti.valid_until >= now() + AND (ti.org_id = ? OR ti.team_id = ANY(?)) + ORDER BY ti.email_to, ti.valid_until DESC, ti.created_at DESC;") + +(def ^:private schema:get-org-invitations-params + [:map + [:organization-id ::sm/uuid]]) + +(def ^:private schema:get-org-invitations-result + [:vector + [:map + [:id ::sm/uuid] + [:organization-id {:optional true} [:maybe ::sm/uuid]] + [:email ::sm/email] + [:sent-at ::sm/inst] + [:name {:optional true} [:maybe ::sm/text]] + [:photo-url {:optional true} ::sm/uri]]]) + +(sv/defmethod ::get-org-invitations + "Get valid invitations for an organization, returning at most one invitation per email." + {::doc/added "2.16" + ::sm/params schema:get-org-invitations-params + ::sm/result schema:get-org-invitations-result} + [cfg {:keys [organization-id]}] + (let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id}) + team-ids (->> (:teams org-summary) + (map :id) + (filter uuid?) + (into []))] + (db/run! cfg (fn [{:keys [::db/conn]}] + (let [ids-array (db/create-array conn "uuid" team-ids)] + (->> (db/exec! conn [sql:get-org-invitations organization-id ids-array]) + (mapv (fn [{:keys [photo-id] :as invitation}] + (cond-> (dissoc invitation :photo-id) + photo-id + (assoc :photo-url (files/resolve-public-uri photo-id))))))))))) + + +;; API: delete-org-invitations + +(def ^:private sql:delete-org-invitations + "DELETE FROM team_invitation AS ti + WHERE ti.email_to = ? + AND (ti.org_id = ? OR ti.team_id = ANY(?));") + +(def ^:private schema:delete-org-invitations-params + [:map + [:organization-id ::sm/uuid] + [:email ::sm/email]]) + +(sv/defmethod ::delete-org-invitations + "Delete all invitations for one email in an organization scope (org + org teams)." + {::doc/added "2.16" + ::sm/params schema:delete-org-invitations-params} + [cfg {:keys [organization-id email]}] + (let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id}) + clean-email (profile/clean-email email) + team-ids (->> (:teams org-summary) + (map :id) + (filter uuid?) + (into []))] + (db/run! cfg (fn [{:keys [::db/conn]}] + (let [ids-array (db/create-array conn "uuid" team-ids)] + (db/exec! conn [sql:delete-org-invitations clean-email organization-id ids-array])))) + nil)) + + ;; API: remove-from-org diff --git a/backend/test/backend_tests/rpc_management_nitrate_test.clj b/backend/test/backend_tests/rpc_management_nitrate_test.clj index 0e85b2d312..99cef73858 100644 --- a/backend/test/backend_tests/rpc_management_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_management_nitrate_test.clj @@ -220,6 +220,209 @@ (t/is (= :not-found (th/ex-type (:error ko-out)))) (t/is (= :profile-not-found (th/ex-code (:error ko-out)))))) +(t/deftest get-org-invitations-returns-valid-deduped-by-email + (let [profile (th/create-profile* 1 {:is-active true}) + team-1 (th/create-team* 1 {:profile-id (:id profile)}) + team-2 (th/create-team* 2 {:profile-id (:id profile)}) + org-id (uuid/random) + org-summary {:id org-id + :teams [{:id (:id team-1)} + {:id (:id team-2)}]} + params {::th/type :get-org-invitations + ::rpc/profile-id (:id profile) + :organization-id org-id}] + + ;; Same email appears in org and team invitations; only one should be returned. + (th/db-insert! :team-invitation + {:id (uuid/random) + :org-id org-id + :team-id nil + :email-to "dup@example.com" + :created-by (:id profile) + :role "editor" + :valid-until (ct/in-future "24h")}) + + (th/db-insert! :team-invitation + {:id (uuid/random) + :team-id (:id team-1) + :org-id nil + :email-to "dup@example.com" + :created-by (:id profile) + :role "admin" + :valid-until (ct/in-future "72h")}) + + (th/db-insert! :team-invitation + {:id (uuid/random) + :team-id (:id team-2) + :org-id nil + :email-to "valid@example.com" + :created-by (:id profile) + :role "editor" + :valid-until (ct/in-future "48h")}) + + ;; Expired invitation should be ignored. + (th/db-insert! :team-invitation + {:id (uuid/random) + :org-id org-id + :team-id nil + :email-to "expired@example.com" + :created-by (:id profile) + :role "editor" + :valid-until (ct/in-past "1h")}) + + (let [out (with-redefs [nitrate/call (fn [_cfg method _params] + (case method + :get-org-summary org-summary + nil))] + (management-command-with-nitrate! params)) + result (:result out) + emails (->> result (map :email) set) + dedup (->> result + (filter #(= "dup@example.com" (:email %))) + first)] + (t/is (th/success? out)) + (t/is (= #{"dup@example.com" "valid@example.com"} emails)) + (t/is (= 2 (count result))) + (t/is (some? (:id dedup))) + (t/is (some? (:sent-at dedup))) + (t/is (nil? (:organization-id dedup))) + (t/is (nil? (:team-id dedup))) + (t/is (nil? (:role dedup))) + (t/is (nil? (:valid-until dedup)))))) + +(t/deftest get-org-invitations-includes-org-level-invitations-when-no-teams + (let [profile (th/create-profile* 1 {:is-active true}) + org-id (uuid/random) + org-summary {:id org-id + :teams []} + params {::th/type :get-org-invitations + ::rpc/profile-id (:id profile) + :organization-id org-id}] + + (th/db-insert! :team-invitation + {:id (uuid/random) + :org-id org-id + :team-id nil + :email-to "org-only@example.com" + :created-by (:id profile) + :role "editor" + :valid-until (ct/in-future "24h")}) + + (let [out (with-redefs [nitrate/call (fn [_cfg method _params] + (case method + :get-org-summary org-summary + nil))] + (management-command-with-nitrate! params)) + result (:result out)] + (t/is (th/success? out)) + (t/is (= 1 (count result))) + (t/is (= "org-only@example.com" (-> result first :email))) + (t/is (some? (-> result first :sent-at)))))) + +(t/deftest get-org-invitations-returns-existing-profile-data + (let [profile (th/create-profile* 1 {:is-active true}) + invited (th/create-profile* 2 {:is-active true + :fullname "Invited User"}) + photo-id (uuid/random) + _ (th/db-insert! :storage-object {:id photo-id + :backend "assets-fs"}) + _ (th/db-update! :profile {:photo-id photo-id} {:id (:id invited)}) + org-id (uuid/random) + org-summary {:id org-id + :teams []} + params {::th/type :get-org-invitations + ::rpc/profile-id (:id profile) + :organization-id org-id}] + + (th/db-insert! :team-invitation + {:id (uuid/random) + :org-id org-id + :team-id nil + :email-to (:email invited) + :created-by (:id profile) + :role "editor" + :valid-until (ct/in-future "24h")}) + + (let [out (with-redefs [nitrate/call (fn [_cfg method _params] + (case method + :get-org-summary org-summary + nil))] + (management-command-with-nitrate! params)) + invitation (-> out :result first)] + (t/is (th/success? out)) + (t/is (= "Invited User" (:name invitation))) + (t/is (some? (:sent-at invitation))) + (t/is (str/ends-with? (:photo-url invitation) + (str "/assets/by-id/" photo-id)))))) + +(t/deftest delete-org-invitations-removes-org-and-org-team-invitations-for-email + (let [profile (th/create-profile* 1 {:is-active true}) + team-1 (th/create-team* 1 {:profile-id (:id profile)}) + team-2 (th/create-team* 2 {:profile-id (:id profile)}) + outside-team (th/create-team* 3 {:profile-id (:id profile)}) + org-id (uuid/random) + org-summary {:id org-id + :teams [{:id (:id team-1)} + {:id (:id team-2)}]} + target-email "target@example.com" + params {::th/type :delete-org-invitations + ::rpc/profile-id (:id profile) + :organization-id org-id + :email "TARGET@example.com"}] + + ;; Should be deleted: org-level invitation for same org+email. + (th/db-insert! :team-invitation + {:id (uuid/random) + :org-id org-id + :team-id nil + :email-to target-email + :created-by (:id profile) + :role "editor" + :valid-until (ct/in-future "24h")}) + + ;; Should be deleted: team-level invitation for teams belonging to org summary. + (th/db-insert! :team-invitation + {:id (uuid/random) + :team-id (:id team-1) + :org-id nil + :email-to target-email + :created-by (:id profile) + :role "editor" + :valid-until (ct/in-past "1h")}) + + ;; Should remain: different email. + (th/db-insert! :team-invitation + {:id (uuid/random) + :team-id (:id team-2) + :org-id nil + :email-to "other@example.com" + :created-by (:id profile) + :role "editor" + :valid-until (ct/in-future "24h")}) + + ;; Should remain: same email but outside org scope. + (th/db-insert! :team-invitation + {:id (uuid/random) + :team-id (:id outside-team) + :org-id nil + :email-to target-email + :created-by (:id profile) + :role "editor" + :valid-until (ct/in-future "24h")}) + + (let [out (with-redefs [nitrate/call (fn [_cfg method _params] + (case method + :get-org-summary org-summary + nil))] + (management-command-with-nitrate! params)) + remaining-target (th/db-query :team-invitation {:email-to target-email}) + remaining-other (th/db-query :team-invitation {:email-to "other@example.com"})] + (t/is (th/success? out)) + (t/is (nil? (:result out))) + (t/is (= 1 (count remaining-target))) + (t/is (= (:id outside-team) (:team-id (first remaining-target)))) + (t/is (= 1 (count remaining-other)))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Tests: remove-from-org ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; From 9ebd17f31f27f94ae703183c3e14920bb64356bf Mon Sep 17 00:00:00 2001 From: boskodev790 Date: Fri, 24 Apr 2026 05:14:46 -0500 Subject: [PATCH 219/288] :bug: Fix PENPOT_OIDC_USER_INFO_SOURCE flag being silently ignored (#9114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #9108. The `case` expression in `get-info` (`backend/src/app/auth/oidc.clj`) dispatched on `:token` and `:userinfo` keywords, but the provider map's `:user-info-source` value is a string — both from config (the malli schema in `app.config` pins it to one of `"token"`, `"userinfo"`, `"auto"`) and from the hard-coded Google / GitHub provider maps (which already write `"userinfo"`). Strings never equal keywords in Clojure `case`, so every call fell through to the auto-fallback that prefers ID-token claims and only hits the UserInfo endpoint when claims are empty. The net effect: setting `PENPOT_OIDC_USER_INFO_SOURCE=userinfo` did nothing, and OIDC flows whose IdP requires the UserInfo endpoint (so claims come back empty/partial) failed with "incomplete user info". - Extract a pure helper `select-user-info-source` that maps the raw config string to a dispatch keyword (`:token`, `:userinfo`, `:auto`), falling back to `:auto` for unknown / missing / accidentally-keyword values - Rewrite `get-info`'s `case` to dispatch on the helper's output so the arms unambiguously match the normalised keyword - Add vitest-style deftests in `auth_oidc_test.clj` pinning the three valid strings, the nil / "auto" / unknown fallback, and the reverse regression (a keyword input must not slip through as if it were the matching string) - Add a CHANGES.md entry under the 2.17.0 Unreleased `:bug: Bugs fixed` section linking back to #9108 Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + backend/src/app/auth/oidc.clj | 21 +++++++++++++++---- backend/test/backend_tests/auth_oidc_test.clj | 20 ++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2d0516328e..15477e736a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,7 @@ ### :bug: Bugs fixed +- Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored (`userinfo` / `token`) in the OIDC callback, causing "incomplete user info" failures during registration [Github #9108](https://github.com/penpot/penpot/issues/9108) - Fix `get-view-only-bundle` crashing when a share-link viewer encounters a team member whose email lacks `@` (NullPointerException in `obfuscate-email`) or whose domain has no `.` (previously produced a dangling-dot `****@****.`); now the viewer-side obfuscation is nil-safe and omits the trailing dot when the domain has no TLD - Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled [Github #8877](https://github.com/penpot/penpot/issues/8877) - Fix Copy as SVG: emit a single valid SVG document when multiple shapes are selected, and publish `image/svg+xml` to the clipboard so the paste target works in Inkscape and other SVG-native tools [Github #838](https://github.com/penpot/penpot/issues/838) diff --git a/backend/src/app/auth/oidc.clj b/backend/src/app/auth/oidc.clj index 9c292ff2c2..782dfabca7 100644 --- a/backend/src/app/auth/oidc.clj +++ b/backend/src/app/auth/oidc.clj @@ -548,16 +548,29 @@ (def ^:private valid-info? (sm/validator schema:info)) +(defn- select-user-info-source + "Normalise the provider's configured user-info source into a keyword the + dispatch below can match. The raw value comes from config as a string + per the malli schema in `app.config` (`\"token\"`, `\"userinfo\"`, or + `\"auto\"`) and from hard-coded per-provider maps as strings as well; + any unrecognised or missing value falls back to `:auto` (prefer claims, + use userinfo as fallback)." + [source] + (case source + "token" :token + "userinfo" :userinfo + :auto)) + (defn- get-info [cfg provider state code] (let [tdata (fetch-access-token cfg provider code) claims (get-id-token-claims provider tdata) - info (case (get provider :user-info-source) - :token (dissoc claims :exp :iss :iat :aud :sid) + info (case (select-user-info-source (get provider :user-info-source)) + :token (dissoc claims :exp :iss :iat :aud :sid) :userinfo (fetch-user-info cfg provider tdata) - (or (some-> claims (dissoc :exp :iss :iat :aud :sid)) - (fetch-user-info cfg provider tdata))) + :auto (or (some-> claims (dissoc :exp :iss :iat :aud :sid)) + (fetch-user-info cfg provider tdata))) info (process-user-info provider tdata info)] diff --git a/backend/test/backend_tests/auth_oidc_test.clj b/backend/test/backend_tests/auth_oidc_test.clj index 2a451195c5..bdf18e5541 100644 --- a/backend/test/backend_tests/auth_oidc_test.clj +++ b/backend/test/backend_tests/auth_oidc_test.clj @@ -33,3 +33,23 @@ (t/is (= "nextcloud@example.com" (:email info))) (t/is (= "Nextcloud User" (:fullname info))) (t/is (true? (:email-verified info))))) + +;; The provider's `:user-info-source` value arrives as a string (enforced by +;; the malli schema in `app.config` and used as-is by the hard-coded Google / +;; GitHub provider maps), so the dispatch must interpret strings — not +;; keywords — to actually honour `PENPOT_OIDC_USER_INFO_SOURCE=userinfo`. +(t/deftest select-user-info-source-interprets-config-strings + (t/testing "explicit string values map to keyword dispatch tokens" + (t/is (= :token (#'oidc/select-user-info-source "token"))) + (t/is (= :userinfo (#'oidc/select-user-info-source "userinfo")))) + + (t/testing "missing or explicit \"auto\" falls back to auto dispatch" + (t/is (= :auto (#'oidc/select-user-info-source "auto"))) + (t/is (= :auto (#'oidc/select-user-info-source nil)))) + + (t/testing "unknown values fall back to auto dispatch safely" + (t/is (= :auto (#'oidc/select-user-info-source "unknown"))) + ;; Guards against the reverse regression — a stray keyword value must + ;; not silently slip through as if it were the matching string. + (t/is (= :auto (#'oidc/select-user-info-source :token))) + (t/is (= :auto (#'oidc/select-user-info-source :userinfo))))) From 6c4ab8940d0e475b00735affbedd168814607ab7 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Fri, 24 Apr 2026 12:48:58 +0200 Subject: [PATCH 220/288] :bug: Fix colorpicker eyedropper on gradients tab (#9125) * :bug: Fix colorpicker eyedropper on gradients tab * :bug: Fix gradient test deleting opacity input --- CHANGES.md | 2 ++ .../playwright/ui/specs/colorpicker.spec.js | 16 ---------------- .../src/app/main/ui/workspace/colorpicker.cljs | 17 ----------------- 3 files changed, 2 insertions(+), 33 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 15477e736a..9e0e9d6bee 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -81,6 +81,8 @@ - Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [Github #8516](https://github.com/penpot/penpot/issues/8516) - Fix plugin `addInteraction` silently rejecting `open-overlay` actions with `manualPositionLocation` [Github #8409](https://github.com/penpot/penpot/issues/8409) - Fix typography style creation with tokenized line-height (by @juan-flores077) [Github #8479](https://github.com/penpot/penpot/issues/8479) +- Fix colorpicker layout so the eyedropper button is visible again [Taiga #14057](https://tree.taiga.io/project/penpot/issue/14057) + ## 2.16.0 (Unreleased) diff --git a/frontend/playwright/ui/specs/colorpicker.spec.js b/frontend/playwright/ui/specs/colorpicker.spec.js index a0e28eea07..225c7da433 100644 --- a/frontend/playwright/ui/specs/colorpicker.spec.js +++ b/frontend/playwright/ui/specs/colorpicker.spec.js @@ -85,14 +85,6 @@ test("Create a LINEAR gradient", async ({ page }) => { .last(); await inputOpacity2.fill("40"); - const inputOpacityGlobal = workspacePage.colorpicker.getByTestId( - "opacity-global-input", - ); - await inputOpacityGlobal.fill("50"); - await inputOpacityGlobal.press("Enter"); - await expect(inputOpacityGlobal).toHaveValue("50"); - await expect(inputOpacityGlobal).toBeVisible(); - await expect( workspacePage.page.getByText("Linear gradient") ).toBeVisible(); @@ -169,14 +161,6 @@ test("Create a RADIAL gradient", async ({ page }) => { .last(); await inputOpacity2.fill("100"); - const inputOpacityGlobal = workspacePage.colorpicker.getByTestId( - "opacity-global-input", - ); - await inputOpacityGlobal.fill("50"); - await inputOpacityGlobal.press("Enter"); - await expect(inputOpacityGlobal).toHaveValue("50"); - await expect(inputOpacityGlobal).toBeVisible(); - await expect( workspacePage.page.getByText("Radial gradient") ).toBeVisible(); diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index d6d4300848..cf9af4aacd 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -26,7 +26,6 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.file-uploader :refer [file-uploader]] - [app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.components.radio-buttons :refer [radio-buttons radio-button]] [app.main.ui.components.select :refer [select]] [app.main.ui.ds.foundations.assets.icon :as i] @@ -341,11 +340,6 @@ (mapv #(assoc %2 :offset (:offset %1)) stops new-stops)] (st/emit! (dc/update-colorpicker-stops stops))))) - handle-change-gradient-opacity - (mf/use-fn - (fn [value] - (st/emit! (dc/update-colorpicker-gradient-opacity (/ value 100))))) - render-wasm? (features/use-feature "render-wasm/v1") @@ -394,17 +388,6 @@ [:div {:class (stl/css :top-actions)} [:div {:class (stl/css :top-actions-right)} - (when (and (= color-style :direct-color) - (= :gradient selected-mode)) - [:div {:class (stl/css :opacity-input-wrapper)} - [:span {:class (stl/css :icon-text)} "%"] - [:> numeric-input* - {:value (-> data :opacity opacity->string) - :on-change handle-change-gradient-opacity - :default 100 - :data-testid "opacity-global-input" - :min 0 - :max 100}]]) (when (and (= color-style :direct-color) (or (not disable-gradient) (not disable-image))) From 38d67c8e9659be75487e335b3c1c8e5567ef83e8 Mon Sep 17 00:00:00 2001 From: Juan Flores <112629487+juan-flores077@users.noreply.github.com> Date: Fri, 24 Apr 2026 04:17:57 -0700 Subject: [PATCH 221/288] :bug: Fix Help & Learning submenu vertical alignment in account menu (#9138) The submenu opened by hovering Help & Learning in the user account menu rendered with a vertical offset, making it appear visually disconnected from its parent row and aligned instead with the Community Signed-off-by: Juan Flores <112629487+juan-flores077@users.noreply.github.com> --- CHANGES.md | 1 + frontend/src/app/main/ui/dashboard/sidebar.scss | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 9e0e9d6bee..16e1dc227f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -79,6 +79,7 @@ - Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990) - Allow deleting the profile avatar after uploading [Github #9067](https://github.com/penpot/penpot/issues/9067) - Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [Github #8516](https://github.com/penpot/penpot/issues/8516) +- Fix "Help & Learning" submenu vertical alignment in account menu (by @juan-flores077) [Github #9137](https://github.com/penpot/penpot/issues/9137) - Fix plugin `addInteraction` silently rejecting `open-overlay` actions with `manualPositionLocation` [Github #8409](https://github.com/penpot/penpot/issues/8409) - Fix typography style creation with tokenized line-height (by @juan-flores077) [Github #8479](https://github.com/penpot/penpot/issues/8479) - Fix colorpicker layout so the eyedropper button is visible again [Taiga #14057](https://tree.taiga.io/project/penpot/issue/14057) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index 0fe9571e05..0140ce7ba8 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -470,8 +470,13 @@ min-width: deprecated.$s-192; } +// Each submenu is positioned via its bottom edge; the visual top lands +// at `inset-block-end + submenu_height`. Help & Learning (3 items, +// taller) needs the same inset as Community (2 items, shorter) so that +// its top edge sits one row above Community — aligning with the +// "Help & Learning" trigger row in the parent menu. .sub-menu.help-learning { - inset-block-end: deprecated.$s-72; + inset-block-end: deprecated.$s-120; } .sub-menu.community { From 7e499c5e5f91818025a99be7dbedca09b3c50e23 Mon Sep 17 00:00:00 2001 From: moorsecopers99 <46223049+moorsecopers99@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:24:55 +0300 Subject: [PATCH 222/288] :bug: Fix Settings/Notifications submit button always active with no changes (#9091) The "Update Settings" button in Your Account > Settings and Notifications was always enabled, even when the form had no changes, and clicking it emitted a success notification despite no data being modified. Disable the submit button when the current form data equals its initial state, so it activates only when there are actual changes to persist. Signed-off-by: moorsecopers99 Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + frontend/src/app/main/ui/settings/notifications.cljs | 1 + frontend/src/app/main/ui/settings/options.cljs | 1 + 3 files changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 16e1dc227f..ad49f21045 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -79,6 +79,7 @@ - Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990) - Allow deleting the profile avatar after uploading [Github #9067](https://github.com/penpot/penpot/issues/9067) - Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [Github #8516](https://github.com/penpot/penpot/issues/8516) +- Fix Settings and Notifications "Update Settings" button enabled state when form has no changes (by @moorsecopers99) [Github #9090](https://github.com/penpot/penpot/issues/9090) - Fix "Help & Learning" submenu vertical alignment in account menu (by @juan-flores077) [Github #9137](https://github.com/penpot/penpot/issues/9137) - Fix plugin `addInteraction` silently rejecting `open-overlay` actions with `manualPositionLocation` [Github #8409](https://github.com/penpot/penpot/issues/8409) - Fix typography style creation with tokenized line-height (by @juan-flores077) [Github #8479](https://github.com/penpot/penpot/issues/8479) diff --git a/frontend/src/app/main/ui/settings/notifications.cljs b/frontend/src/app/main/ui/settings/notifications.cljs index 5779474c70..d9347b5ee9 100644 --- a/frontend/src/app/main/ui/settings/notifications.cljs +++ b/frontend/src/app/main/ui/settings/notifications.cljs @@ -82,6 +82,7 @@ [:> fm/submit-button* {:label (tr "dashboard.settings.notifications.submit") + :disabled (= (:data @form) (:initial @form)) :data-testid "submit-settings" :class (stl/css :update-btn)}]]]])) diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs index fc5e9e14af..50b4b47424 100644 --- a/frontend/src/app/main/ui/settings/options.cljs +++ b/frontend/src/app/main/ui/settings/options.cljs @@ -72,6 +72,7 @@ [:> fm/submit-button* {:label (tr "dashboard.update-settings") + :disabled (= (:data @form) (:initial @form)) :data-testid "submit-lang-change" :class (stl/css :btn-primary)}]])) From debfe5490f6caf31341d44d02d5b346faf5d1588 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 23 Apr 2026 13:26:01 +0200 Subject: [PATCH 223/288] :bug: Fix switching a team nitrate organization lose the background --- backend/src/app/nitrate.clj | 57 +++++++------------ backend/src/app/rpc/commands/nitrate.clj | 13 ++--- backend/src/app/rpc/management/nitrate.clj | 21 ++----- backend/src/app/rpc/notifications.clj | 7 +-- .../rpc_management_nitrate_test.clj | 28 +++++---- common/src/app/common/organization.cljc | 32 +++++++++++ common/src/app/common/types/team.cljc | 13 +++++ frontend/src/app/main/data/dashboard.cljs | 25 ++++---- frontend/src/app/main/data/nitrate.cljs | 4 +- frontend/src/app/main/ui/dashboard/team.cljs | 3 +- frontend/src/app/main/ui/dashboard/team.scss | 1 - .../main/ui/ds/controls/shared/option.scss | 1 + 12 files changed, 115 insertions(+), 90 deletions(-) create mode 100644 common/src/app/common/organization.cljc diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index d096a4e9d2..ad4318ec6c 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -10,9 +10,11 @@ [app.common.exceptions :as ex] [app.common.json :as json] [app.common.logging :as l] + [app.common.organization :as co] [app.common.schema :as sm] [app.common.schema.generators :as sg] [app.common.time :as ct] + [app.common.types.team :as ctt] [app.config :as cf] [app.http.client :as http] [app.rpc :as-alias rpc] @@ -108,16 +110,6 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def ^:private schema:organization - [:map - [:id ::sm/uuid] - [:name ::sm/text] - [:slug ::sm/text] - [:is-your-penpot :boolean] - [:owner-id ::sm/uuid] - [:avatar-bg-url [::sm/text]] - [:logo-id {:optional true} [:maybe ::sm/uuid]]]) - (def ^:private schema:org-summary [:map [:id ::sm/uuid] @@ -129,12 +121,6 @@ [:id ::sm/uuid] [:is-your-penpot :boolean]]]]]) -(def ^:private schema:team - [:map - [:id ::sm/uuid] - [:organization-id ::sm/uuid] - [:is-your-penpot :boolean]]) - (def ^:private schema:profile-org [:map [:is-member :boolean] @@ -221,7 +207,7 @@ (str baseuri "/api/teams/" team-id) - schema:organization params))) + ctt/schema:team-with-organization params))) (defn- get-org-membership-api [cfg {:keys [profile-id organization-id] :as params}] @@ -261,13 +247,18 @@ [cfg {:keys [organization-id team-id is-default] :as params}] (let [baseuri (cf/get :nitrate-backend-uri) params (assoc params :request-params {:team-id team-id - :is-your-penpot (true? is-default)})] - (request-to-nitrate cfg :post - (str baseuri - "/api/organizations/" - organization-id - "/add-team") - schema:team params))) + :is-your-penpot (true? is-default)}) + team (request-to-nitrate cfg :post + (str baseuri + "/api/organizations/" + organization-id + "/add-team") + ctt/schema:team-with-organization params) + custom-photo (when-let [logo-id (get-in team [:organization :logo-id])] + (str (cf/get :public-uri) "/assets/by-id/" logo-id))] + (cond-> team + custom-photo + (assoc-in [:organization :custom-photo] custom-photo)))) (defn- add-profile-to-org-api [cfg {:keys [profile-id organization-id team-id email] :as params}] @@ -385,18 +376,14 @@ Returns the original team unchanged if the request fails or org data is nil." [cfg team params] (try - (let [params (assoc (or params {}) :team-id (:id team)) - org (call cfg :get-team-org params)] + (let [params (assoc (or params {}) :team-id (:id team)) + team-with-org (call cfg :get-team-org params) + org (:organization team-with-org)] (if (some? org) - (assoc team - :organization-id (:id org) - :organization-name (:name org) - :organization-slug (:slug org) - :organization-owner-id (:owner-id org) - :organization-avatar-bg-url (:avatar-bg-url org) - :organization-custom-photo (when-let [logo-id (:logo-id org)] - (str (cf/get :public-uri) "/assets/by-id/" logo-id)) - :is-default (or (:is-default team) (true? (:is-your-penpot org)))) + (-> (co/apply-organization team (assoc org :custom-photo + (when-let [logo-id (:logo-id org)] + (str (cf/get :public-uri) "/assets/by-id/" logo-id)))) + (assoc :is-default (or (:is-default team) (true? (:is-your-penpot team-with-org))))) team)) (catch Throwable cause (l/error :hint "failed to get team organization info" diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index db552ae070..ddabb9bb65 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -249,22 +249,21 @@ (nitrate/call cfg :remove-team-from-org {:team-id team-id :organization-id organization-id}) ;; Notify connected users - (notifications/notify-team-change cfg team-id nil nil organization-name "dashboard.team-no-longer-belong-org") + (notifications/notify-team-change cfg {:id team-id :organization {:name organization-name}} "dashboard.team-no-longer-belong-org") nil) (def ^:private schema:add-team-to-org [:map [:team-id ::sm/uuid] - [:organization-id ::sm/uuid] - [:organization-name ::sm/text]]) + [:organization-id ::sm/uuid]]) (sv/defmethod ::add-team-to-org {::rpc/auth true ::doc/added "2.17" ::sm/params schema:add-team-to-org ::db/transaction true} - [cfg {:keys [::rpc/profile-id team-id organization-id organization-name]}] + [cfg {:keys [::rpc/profile-id team-id organization-id]}] (assert-is-owner cfg profile-id team-id) (assert-not-default-team cfg team-id) @@ -277,8 +276,8 @@ (teams/initialize-user-in-nitrate-org cfg member-id organization-id))) ;; Api call to nitrate - (nitrate/call cfg :set-team-org {:team-id team-id :organization-id organization-id :is-default false}) + (let [team (nitrate/call cfg :set-team-org {:team-id team-id :organization-id organization-id :is-default false})] - ;; Notify connected users - (notifications/notify-team-change cfg team-id nil organization-id organization-name "dashboard.team-belong-org") + ;; Notify connected users + (notifications/notify-team-change cfg team "dashboard.team-belong-org")) nil) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 6762de6e80..f07451abc5 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -12,7 +12,7 @@ [app.common.exceptions :as ex] [app.common.schema :as sm] [app.common.types.profile :refer [schema:profile, schema:basic-profile]] - [app.common.types.team :refer [schema:team]] + [app.common.types.team :refer [schema:team schema:team-with-organization]] [app.config :as cf] [app.db :as db] [app.media :as media] @@ -117,22 +117,13 @@ ;; ---- API: notify-team-change -(def ^:private schema:notify-team-change - [:map - [:id ::sm/uuid] - [:organization-id ::sm/uuid] - [:organization-name ::sm/text]]) - - - - (sv/defmethod ::notify-team-change "Notify to Penpot a team change from nitrate" {::doc/added "2.14" - ::sm/params schema:notify-team-change + ::sm/params schema:team-with-organization ::rpc/auth false} - [cfg {:keys [id organization-id organization-name]}] - (notifications/notify-team-change cfg id nil organization-id organization-name nil) + [cfg team] + (notifications/notify-team-change cfg (select-keys team [:id :is-your-penpot :organization]) nil) nil) ;; ---- API: notify-user-added-to-organization @@ -143,8 +134,6 @@ [:organization-id ::sm/uuid] [:role ::sm/text]]) - - (sv/defmethod ::notify-user-added-to-organization "Notify to Penpot that an user has joined an org from nitrate" {::doc/added "2.14" @@ -271,7 +260,7 @@ RETURNING id, name;") ;; Notify users (doseq [team updated-teams] - (notifications/notify-team-change cfg (:id team) (:name team) nil organization-name "dashboard.org-deleted")))))))) + (notifications/notify-team-change cfg {:id (:id team) :name (:name team) :organization {:name organization-name}} "dashboard.org-deleted")))))))) ;; ---- API: get-profile-by-email diff --git a/backend/src/app/rpc/notifications.clj b/backend/src/app/rpc/notifications.clj index 3bdb2c8a3b..10d0d9f134 100644 --- a/backend/src/app/rpc/notifications.clj +++ b/backend/src/app/rpc/notifications.clj @@ -10,17 +10,14 @@ [app.msgbus :as mbus])) (defn notify-team-change - [cfg team-id team-name organization-id organization-name notification] + [cfg team notification] (let [msgbus (::mbus/msgbus cfg)] (mbus/pub! msgbus ;;TODO There is a bug on dashboard with teams notifications. ;;For now we send it to uuid/zero instead of team-id :topic uuid/zero :message {:type :team-org-change - :team-id team-id - :team-name team-name - :organization-id organization-id - :organization-name organization-name + :team team :notification notification}))) diff --git a/backend/test/backend_tests/rpc_management_nitrate_test.clj b/backend/test/backend_tests/rpc_management_nitrate_test.clj index 99cef73858..382264bd66 100644 --- a/backend/test/backend_tests/rpc_management_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_management_nitrate_test.clj @@ -74,24 +74,32 @@ (t/deftest notify-team-change-publishes-event (let [team-id (uuid/random) organization-id (uuid/random) + organization {:id organization-id + :name "Acme Inc" + :slug "acme-inc" + :owner-id (uuid/random) + :avatar-bg-url "http://example.com/avatar.svg"} calls (atom []) out (with-redefs [mbus/pub! (fn [_cfg & {:keys [topic message]}] (swap! calls conj {:topic topic :message message}))] (management-command-with-nitrate! {::th/type :notify-team-change :id team-id - :organization-id organization-id - :organization-name "Acme Inc"}))] + :is-your-penpot false + :organization organization}))] (t/is (th/success? out)) (t/is (= 1 (count @calls))) (t/is (= uuid/zero (-> @calls first :topic))) - (t/is (= {:type :team-org-change - :team-id team-id - :team-name nil - :organization-id organization-id - :organization-name "Acme Inc" - :notification nil} - (-> @calls first :message))))) + (let [msg (-> @calls first :message)] + (t/is (= :team-org-change (:type msg))) + (t/is (= nil (:notification msg))) + (t/is (= team-id (-> msg :team :id))) + (t/is (= false (-> msg :team :is-your-penpot))) + (t/is (= (:id organization) (-> msg :team :organization :id))) + (t/is (= (:name organization) (-> msg :team :organization :name))) + (t/is (= (:slug organization) (-> msg :team :organization :slug))) + (t/is (= (:owner-id organization) (-> msg :team :organization :owner-id))) + (t/is (= (:avatar-bg-url organization) (str (-> msg :team :organization :avatar-bg-url))))))) (t/deftest notify-user-added-to-organization-creates-default-org-team (let [profile (th/create-profile* 1 {:is-active true}) @@ -181,7 +189,7 @@ (doseq [call @calls] (t/is (= uuid/zero (:topic call))) (t/is (= :team-org-change (-> call :message :type))) - (t/is (= organization-name (-> call :message :organization-name))) + (t/is (= organization-name (-> call :message :team :organization :name))) (t/is (= "dashboard.org-deleted" (-> call :message :notification)))))) (t/deftest get-profile-by-email-success-and-not-found diff --git a/common/src/app/common/organization.cljc b/common/src/app/common/organization.cljc new file mode 100644 index 0000000000..e7e5bc49b1 --- /dev/null +++ b/common/src/app/common/organization.cljc @@ -0,0 +1,32 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.organization) + +(def organization->team-keys + "Mapping from organization field keys to their corresponding :organization-* team keys." + [[:id :organization-id] + [:name :organization-name] + [:custom-photo :organization-custom-photo] + [:slug :organization-slug] + [:avatar-bg-url :organization-avatar-bg-url] + [:owner-id :organization-owner-id]]) + +(defn apply-organization + "Updates a team map with organization fields sourced from org. + Associates each org field to the corresponding :organization-* team key when + the value is non-nil; dissociates the key otherwise. This correctly handles + both attaching an org (all values present) and detaching one (org is nil or + all fields absent)." + [team organization] + (let [id (:id organization)] + (reduce (fn [acc [org-k team-k]] + (let [v (get organization org-k)] + (if (and id (some? v)) + (assoc acc team-k v) + (dissoc acc team-k)))) + team + organization->team-keys))) \ No newline at end of file diff --git a/common/src/app/common/types/team.cljc b/common/src/app/common/types/team.cljc index ad9bac999c..c8707f8b0f 100644 --- a/common/src/app/common/types/team.cljc +++ b/common/src/app/common/types/team.cljc @@ -26,3 +26,16 @@ [:id ::sm/uuid] [:name :string]]) +(def schema:team-with-organization + [:map + [:id ::sm/uuid] + [:is-your-penpot :boolean] + [:organization + [:map + [:id ::sm/uuid] + [:name ::sm/text] + [:slug ::sm/text] + [:owner-id ::sm/uuid] + [:avatar-bg-url ::sm/uri] + [:logo-id {:optional true} [:maybe ::sm/uuid]]]]]) + diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index eddcb5a503..8ef722de9f 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -11,6 +11,7 @@ [app.common.features :as cfeat] [app.common.files.helpers :as cfh] [app.common.logging :as log] + [app.common.organization :as co] [app.common.schema :as sm] [app.common.time :as ct] [app.common.types.project :refer [valid-project?]] @@ -686,29 +687,29 @@ (modal/hide))))) (defn handle-change-team-org - [{:keys [team-id team-name organization-id organization-name notification]}] + [{:keys [team notification]}] (ptk/reify ::handle-change-team-org ptk/WatchEvent (watch [_ state _] - (let [current-team-id (:current-team-id state)] + (let [current-team-id (:current-team-id state) + organization (:organization team)] (when (and (contains? cf/flags :nitrate) notification - (= team-id current-team-id)) - (rx/of (ntf/show {:content (tr notification organization-name) + (= (:id team) current-team-id)) + (rx/of (ntf/show {:content (tr notification (:name organization)) :type :toast :level :info :timeout nil}))))) ptk/UpdateEvent (update [_ state] (if (contains? cf/flags :nitrate) - (d/update-in-when state [:teams team-id] - (fn [team] - (cond-> team - (some? organization-id) (assoc :organization-id organization-id) - (nil? organization-id) (dissoc :organization-id) - (some? organization-name) (assoc :organization-name organization-name) - (nil? organization-name) (dissoc :organization-name) - team-name (assoc :name team-name)))) + (let [team-id (:id team) + team-name (:name team) + organization (:organization team)] + (d/update-in-when state [:teams team-id] + (fn [team] + (cond-> (co/apply-organization team organization) + team-name (assoc :name team-name))))) state)))) (defn- handle-user-org-change diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index dd2619d37e..0741fddbac 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -126,11 +126,11 @@ (defn add-team-to-org - [{:keys [team-id organization-id organization-name] :as params}] + [{:keys [team-id organization-id] :as params}] (ptk/reify ::add-team-to-org ptk/WatchEvent (watch [_ _ _] - (->> (rp/cmd! ::add-team-to-org {:team-id team-id :organization-id organization-id :organization-name organization-name}) + (->> (rp/cmd! ::add-team-to-org {:team-id team-id :organization-id organization-id}) (rx/mapcat (fn [_] (rx/of (modal/hide)))))))) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 82a2c103d6..47a96575cf 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -1437,8 +1437,7 @@ (let [organization (d/seek #(= organization-id (:id %)) organizations)] (when organization (st/emit! (dnt/add-team-to-org {:team-id (:id team) - :organization-id organization-id - :organization-name (:name organization)})))))) + :organization-id organization-id})))))) on-add-team-to-org (mf/use-fn diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss index 73e8b56f6a..a4e24c88b9 100644 --- a/frontend/src/app/main/ui/dashboard/team.scss +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -874,7 +874,6 @@ // SELECT ORGANIZATION MODAL .modal-select-org-container { - overflow: hidden; display: flex; flex-direction: column; width: $sz-512; diff --git a/frontend/src/app/main/ui/ds/controls/shared/option.scss b/frontend/src/app/main/ui/ds/controls/shared/option.scss index 2b3e749770..978110d1f3 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/option.scss +++ b/frontend/src/app/main/ui/ds/controls/shared/option.scss @@ -25,6 +25,7 @@ outline-offset: calc(-1 * $b-1); background-color: var(--options-bg-color); color: var(--options-fg-color); + cursor: default; &:hover, &[aria-selected="true"] { From 700f3e9c105071bdfe769edb651f91ae365c7f75 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Fri, 24 Apr 2026 12:09:51 +0200 Subject: [PATCH 224/288] :sparkles: MR changes --- backend/src/app/nitrate.clj | 13 ++++++------- backend/src/app/rpc/commands/nitrate.clj | 6 +++--- backend/src/app/rpc/management/nitrate.clj | 3 ++- .../app/common/{ => types}/organization.cljc | 17 ++++++++++++++++- common/src/app/common/types/team.cljc | 12 ------------ frontend/src/app/main/data/dashboard.cljs | 2 +- 6 files changed, 28 insertions(+), 25 deletions(-) rename common/src/app/common/{ => types}/organization.cljc (76%) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index ad4318ec6c..00ccdac7c6 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -10,11 +10,10 @@ [app.common.exceptions :as ex] [app.common.json :as json] [app.common.logging :as l] - [app.common.organization :as co] [app.common.schema :as sm] [app.common.schema.generators :as sg] [app.common.time :as ct] - [app.common.types.team :as ctt] + [app.common.types.organization :as cto] [app.config :as cf] [app.http.client :as http] [app.rpc :as-alias rpc] @@ -207,7 +206,7 @@ (str baseuri "/api/teams/" team-id) - ctt/schema:team-with-organization params))) + cto/schema:team-with-organization params))) (defn- get-org-membership-api [cfg {:keys [profile-id organization-id] :as params}] @@ -253,7 +252,7 @@ "/api/organizations/" organization-id "/add-team") - ctt/schema:team-with-organization params) + cto/schema:team-with-organization params) custom-photo (when-let [logo-id (get-in team [:organization :logo-id])] (str (cf/get :public-uri) "/assets/by-id/" logo-id))] (cond-> team @@ -380,9 +379,9 @@ team-with-org (call cfg :get-team-org params) org (:organization team-with-org)] (if (some? org) - (-> (co/apply-organization team (assoc org :custom-photo - (when-let [logo-id (:logo-id org)] - (str (cf/get :public-uri) "/assets/by-id/" logo-id)))) + (-> (cto/apply-organization team (assoc org :custom-photo + (when-let [logo-id (:logo-id org)] + (str (cf/get :public-uri) "/assets/by-id/" logo-id)))) (assoc :is-default (or (:is-default team) (true? (:is-your-penpot team-with-org))))) team)) (catch Throwable cause diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index ddabb9bb65..26d690f166 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -253,15 +253,15 @@ nil) -(def ^:private schema:add-team-to-org +(def ^:private schema:add-team-to-organization [:map [:team-id ::sm/uuid] [:organization-id ::sm/uuid]]) -(sv/defmethod ::add-team-to-org +(sv/defmethod ::add-team-to-organization {::rpc/auth true ::doc/added "2.17" - ::sm/params schema:add-team-to-org + ::sm/params schema:add-team-to-organization ::db/transaction true} [cfg {:keys [::rpc/profile-id team-id organization-id]}] diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index f07451abc5..8af099de97 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -11,8 +11,9 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.schema :as sm] + [app.common.types.organization :refer [schema:team-with-organization]] [app.common.types.profile :refer [schema:profile, schema:basic-profile]] - [app.common.types.team :refer [schema:team schema:team-with-organization]] + [app.common.types.team :refer [schema:team]] [app.config :as cf] [app.db :as db] [app.media :as media] diff --git a/common/src/app/common/organization.cljc b/common/src/app/common/types/organization.cljc similarity index 76% rename from common/src/app/common/organization.cljc rename to common/src/app/common/types/organization.cljc index e7e5bc49b1..b504954cfb 100644 --- a/common/src/app/common/organization.cljc +++ b/common/src/app/common/types/organization.cljc @@ -4,7 +4,22 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.organization) +(ns app.common.types.organization + (:require + [app.common.schema :as sm])) + +(def schema:team-with-organization + [:map + [:id ::sm/uuid] + [:is-your-penpot :boolean] + [:organization + [:map + [:id ::sm/uuid] + [:name ::sm/text] + [:slug ::sm/text] + [:owner-id ::sm/uuid] + [:avatar-bg-url ::sm/uri] + [:logo-id {:optional true} [:maybe ::sm/uuid]]]]]) (def organization->team-keys "Mapping from organization field keys to their corresponding :organization-* team keys." diff --git a/common/src/app/common/types/team.cljc b/common/src/app/common/types/team.cljc index c8707f8b0f..0fd341b43c 100644 --- a/common/src/app/common/types/team.cljc +++ b/common/src/app/common/types/team.cljc @@ -26,16 +26,4 @@ [:id ::sm/uuid] [:name :string]]) -(def schema:team-with-organization - [:map - [:id ::sm/uuid] - [:is-your-penpot :boolean] - [:organization - [:map - [:id ::sm/uuid] - [:name ::sm/text] - [:slug ::sm/text] - [:owner-id ::sm/uuid] - [:avatar-bg-url ::sm/uri] - [:logo-id {:optional true} [:maybe ::sm/uuid]]]]]) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 8ef722de9f..c55d757d80 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -11,9 +11,9 @@ [app.common.features :as cfeat] [app.common.files.helpers :as cfh] [app.common.logging :as log] - [app.common.organization :as co] [app.common.schema :as sm] [app.common.time :as ct] + [app.common.types.organization :as co] [app.common.types.project :refer [valid-project?]] [app.common.uuid :as uuid] [app.config :as cf] From 63829d5fb75c369e40f6a85b168598161e2aa6ca Mon Sep 17 00:00:00 2001 From: bobsonzu0a5d198e17c343cb Date: Fri, 24 Apr 2026 16:02:01 +0200 Subject: [PATCH 225/288] :globe_with_meridians: Add translations for: Russian Currently translated at 80.6% (1673 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/ --- frontend/translations/ru.po | 56 +++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index cdc01f675a..1f91a9f6fd 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-03-01 09:09+0000\n" -"Last-Translator: Egor Filatov \n" +"PO-Revision-Date: 2026-04-25 14:09+0000\n" +"Last-Translator: bobsonzu0a5d198e17c343cb \n" "Language-Team: Russian \n" "Language: ru\n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Weblate 5.16.1-dev\n" +"X-Generator: Weblate 5.17.1-dev\n" #: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:158, src/app/main/ui/viewer/login.cljs:100 msgid "auth.already-have-account" @@ -7030,3 +7030,53 @@ msgstr "Сделано с любовью и открытым исходным к #: src/app/main/ui/static.cljs:248 msgid "not-found.no-permission.already-requested.file" msgstr "Вы уже запросили доступ к этому файлу." + +#: src/app/main/ui/dashboard/placeholder.cljs:61 +msgid "dashboard.empty-project.explore" +msgstr "Рассмотрите варианты для добавления" + +#: src/app/main/errors.cljs:214 +msgid "errors.internal-worker-error" +msgstr "Произошла ошибка в работе веб-воркера." + +#: src/app/main/ui/inspect/styles/rows/color_properties_row.cljs:120 +msgid "inspect.attributes.image.preview" +msgstr "Предварительный просмотр заливки фигуры" + +#: src/app/main/ui/inspect/styles/style_box.cljs:68 +msgid "inspect.tabs.styles.copy-shorthand" +msgstr "Копировать CSS-сокращение в буфер обмена" + +#: src/app/main/ui/dashboard/sidebar.cljs:1125 +msgid "labels.community-contributions" +msgstr "Сообщество и содействие" + +#: src/app/main/ui/static.cljs:67 +msgid "labels.copyright-period" +msgstr "Kaleidos © 2019–по настоящее время" + +#: src/app/main/ui/static.cljs:406 +msgid "labels.internal-error.desc-message-second" +msgstr "" +"Повторите попытку или свяжитесь с технической поддержкой, чтобы сообщить об " +"ошибке." + +#: src/app/main/ui/dashboard/sidebar.cljs:893 +msgid "labels.learning-center" +msgstr "Центр обучения" + +#: src/app/main/ui/comments.cljs:581 +msgid "labels.mention" +msgstr "Отметить" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:210 +msgid "labels.lock" +msgstr "Заблокировать" + +#: src/app/main/ui/static.cljs:61, src/app/main/ui/static.cljs:137 +msgid "labels.login" +msgstr "Логин" + +#: src/app/main/ui/ds/controls/numeric_input.cljs:631 +msgid "labels.mixed-values" +msgstr "Смешать" From d4955c7b78c81eea4216373a0fbfb5136ba31b25 Mon Sep 17 00:00:00 2001 From: Egor Filatov Date: Fri, 24 Apr 2026 15:40:03 +0200 Subject: [PATCH 226/288] :globe_with_meridians: Add translations for: Russian Currently translated at 80.6% (1673 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/ --- frontend/translations/ru.po | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index 1f91a9f6fd..736f6d489d 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "PO-Revision-Date: 2026-04-25 14:09+0000\n" -"Last-Translator: bobsonzu0a5d198e17c343cb \n" +"Last-Translator: Egor Filatov \n" "Language-Team: Russian \n" "Language: ru\n" @@ -7080,3 +7080,7 @@ msgstr "Логин" #: src/app/main/ui/ds/controls/numeric_input.cljs:631 msgid "labels.mixed-values" msgstr "Смешать" + +#: src/app/main/ui/dashboard/sidebar.cljs:347 +msgid "dashboard.create-new-org" +msgstr "Создать новую организацию" From c4e508a60632684975890a77f399decc2d390282 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 27 Apr 2026 08:49:22 +0200 Subject: [PATCH 227/288] :bug: Fix text ellipsis merging error --- .../src/app/main/ui/workspace/tokens/themes/create_modal.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss index f0187a6afb..4cc6c1ef06 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss +++ b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss @@ -143,7 +143,7 @@ } .group-title-name { - @include textEllipsis; + @include text-ellipsis; flex-grow: 1; } @@ -172,7 +172,7 @@ } .theme-name-row { - @include textEllipsis; + @include text-ellipsis; flex-grow: 1; } From 5ee65c5efb8494fdd3dc96015f6e0467abdcbb40 Mon Sep 17 00:00:00 2001 From: boskodev790 Date: Mon, 27 Apr 2026 02:30:07 -0500 Subject: [PATCH 228/288] :bug: Fix :hide typo dropping LDAP not-initialized error hint (#9159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit login-with-ldap raised a :restriction exception with the message "ldap auth provider is not initialized" stored under :hide instead of :hint. ex/raise (common/src/app/common/exceptions.cljc:33-34) uses :hint as the ExceptionInfo message and the downstream error formatters only read :hint (line 250, 312) — :hide is unread anywhere in the codebase (0 other occurrences vs 447 for :hint). Effect: when LDAP is misconfigured, operators saw the generic "restriction" error message instead of the diagnostic string. The typo has been present since the LDAP command was first introduced by commit 14d1cb90bd (2022-06-30, "Refactor auth code") and was carried forward through 6cdf696fc (2023-01-05, "Fix issues on ldap provider and rpc method") without ever surfacing as a code-review comment. One-character fix: :hide -> :hint. Add a CHANGES.md entry under the 2.17.0 Unreleased :bug: Bugs fixed section. --- CHANGES.md | 1 + backend/src/app/rpc/commands/ldap.clj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index b386152edb..e42246aa45 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,7 @@ ### :bug: Bugs fixed +- Fix `login-with-ldap` silently dropping its error message on the `ldap-not-initialized` restriction (typo `:hide` → `:hint`); the message `"ldap auth provider is not initialized"` now actually surfaces in logs and error responses instead of being discarded into an unread key - Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored (`userinfo` / `token`) in the OIDC callback, causing "incomplete user info" failures during registration [Github #9108](https://github.com/penpot/penpot/issues/9108) - Fix `get-view-only-bundle` crashing when a share-link viewer encounters a team member whose email lacks `@` (NullPointerException in `obfuscate-email`) or whose domain has no `.` (previously produced a dangling-dot `****@****.`); now the viewer-side obfuscation is nil-safe and omits the trailing dot when the domain has no TLD - Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled [Github #8877](https://github.com/penpot/penpot/issues/8877) diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj index f4aea5bc10..c4f0f565d1 100644 --- a/backend/src/app/rpc/commands/ldap.clj +++ b/backend/src/app/rpc/commands/ldap.clj @@ -42,7 +42,7 @@ (when-not provider (ex/raise :type :restriction :code :ldap-not-initialized - :hide "ldap auth provider is not initialized")) + :hint "ldap auth provider is not initialized")) (let [info (ldap/authenticate provider params)] (when-not info From 77c507000b805fdfdde4d8111f20816188c62365 Mon Sep 17 00:00:00 2001 From: boskodev790 Date: Mon, 27 Apr 2026 02:41:21 -0500 Subject: [PATCH 229/288] :bug: Fix LDAP schema typo bind-passwor -> bind-password (#9165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The malli schema for the LDAP provider params (`schema:params` in `backend/src/app/auth/ldap.clj`) declared the bind-password slot as `:bind-passwor` (missing trailing `d`). The runtime code in the same file uses `:bind-password` everywhere — `prepare-params` reads `(:bind-password cfg)` on line 21 and `try-connectivity` reads `(:bind-password cfg)` on line 89. Effects of the typo: 1. The schema slot for `:bind-password` is missing, so a wrong type (e.g. a number or vector instead of a string) for the actual key slips through `check-params` unvalidated. Malli `[:map ...]` is open by default, so the genuine `:bind-password` key is silently accepted as an unknown extra key. 2. Anyone reading the schema (operator, future contributor, or tooling generating docs) sees a non-existent `:bind-passwor` parameter and could legitimately set that key — schema would accept it, runtime would never read it, LDAP bind would silently fail with a confusing "no password" error. Cross-checked against the pre-malli `clojure.spec` shape removed in commit 88fb5e7ab (2024-10-29, ":recycle: Update integrant to latest version", which carried the spec→malli migration). The deleted spec defined `(s/def ::bind-password ::us/string)` correctly — the typo was introduced when re-typing the keys into the new malli vector-of- tuples form. Add a CHANGES.md entry under the 2.17.0 Unreleased :bug: Bugs fixed section. One-character fix. Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + backend/src/app/auth/ldap.clj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e42246aa45..5482c07b95 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,7 @@ ### :bug: Bugs fixed +- Fix LDAP provider params schema typo (`bind-passwor` → `bind-password`) introduced during the `clojure.spec` → `malli` migration; the schema slot now matches the runtime key actually read by `prepare-params` (`:password (:bind-password cfg)`) and `try-connectivity` (`(:bind-password cfg)`), so a wrong type for the password no longer slips through unvalidated - Fix `login-with-ldap` silently dropping its error message on the `ldap-not-initialized` restriction (typo `:hide` → `:hint`); the message `"ldap auth provider is not initialized"` now actually surfaces in logs and error responses instead of being discarded into an unread key - Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored (`userinfo` / `token`) in the OIDC callback, causing "incomplete user info" failures during registration [Github #9108](https://github.com/penpot/penpot/issues/9108) - Fix `get-view-only-bundle` crashing when a share-link viewer encounters a team member whose email lacks `@` (NullPointerException in `obfuscate-email`) or whose domain has no `.` (previously produced a dangling-dot `****@****.`); now the viewer-side obfuscation is nil-safe and omits the trailing dot when the domain has no TLD diff --git a/backend/src/app/auth/ldap.clj b/backend/src/app/auth/ldap.clj index 63b7c93672..687a10dd4d 100644 --- a/backend/src/app/auth/ldap.clj +++ b/backend/src/app/auth/ldap.clj @@ -111,7 +111,7 @@ [:host {:optional true} :string] [:port {:optional true} ::sm/int] [:bind-dn {:optional true} :string] - [:bind-passwor {:optional true} :string] + [:bind-password {:optional true} :string] [:query {:optional true} :string] [:base-dn {:optional true} :string] [:attrs-email {:optional true} :string] From 9c6cc5ec326ff449ea058d04548bbaee3d84185a Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Fri, 24 Apr 2026 14:46:10 +0200 Subject: [PATCH 230/288] :lipstick: Fix nitrate org arrow style --- frontend/src/app/main/ui/dashboard/sidebar.cljs | 8 +++++--- frontend/src/app/main/ui/dashboard/sidebar.scss | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 9663111d98..2cb4b31373 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -716,6 +716,9 @@ default-org? (nil? (:id current-org)) + show-options? (and (not default-org?) + (not= (:id profile) (:owner-id current-org))) + show-orgs-menu* (mf/use-state false) @@ -765,7 +768,7 @@ (if show-dropdown? [:div {:class (stl/css :sidebar-org-switch)} [:div {:class (stl/css :org-switch-content)} - [:button {:class (stl/css :current-org) + [:button {:class (stl/css-case :current-org true :current-org-no-options (not show-options?)) :on-click on-show-orgs-click :on-key-down on-show-orgs-keydown :aria-expanded show-orgs-menu? @@ -782,8 +785,7 @@ [:span {:class (stl/css :team-text)} (:name current-org)]])] arrow-icon] - (when-not (or default-org? - (= (:id profile) (:owner-id current-org))) + (when show-options? [:> button* {:variant "ghost" :type "button" :class (stl/css :org-options-btn) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index 0140ce7ba8..395c882b86 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -654,6 +654,10 @@ width: 100%; } +.current-org-no-options { + gap: 0; +} + .current-org .arrow-icon { margin-inline-end: var(--sp-xs); } From e5314f4a137fdf38a1ede657b1865ecd270ac34a Mon Sep 17 00:00:00 2001 From: boskodev790 Date: Mon, 27 Apr 2026 03:17:00 -0500 Subject: [PATCH 231/288] :bug: Fix restore-version-from-plugin promise hanging on restore failure (#9111) Closes #9092. `restore-version-from-plugin` accepted `_reject` as a dead parameter and its stream had no `rx/catch`, so errors raised during the restore flow (failed `rp/cmd! :restore-file-snapshot`, persistence timeouts, or exceptions inside the watch body) silently swallowed instead of rejecting the plugin-facing promise at `file.cljs:81`. Plugin code that did `await version.restore()` would hang indefinitely on any failure. Wire `reject` through and wrap the emission with the same `rx/catch` pattern already used by `create-version-from-plugins` in this file. - Rename `_reject` to `reject` in the function signature - Wrap the `rx/concat` body with `rx/catch` that calls `(reject error)` and returns `rx/empty` on error, mirroring `create-version-from-plugins` - Add a CHANGES.md entry under the 2.17.0 Unreleased bugs-fixed section Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + .../src/app/main/data/workspace/versions.cljs | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5482c07b95..4139644bf6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,7 @@ ### :bug: Bugs fixed +- Fix plugin API `fileVersion.restore()` promise hanging indefinitely on restore failure [Github #9092](https://github.com/penpot/penpot/issues/9092) - Fix LDAP provider params schema typo (`bind-passwor` → `bind-password`) introduced during the `clojure.spec` → `malli` migration; the schema slot now matches the runtime key actually read by `prepare-params` (`:password (:bind-password cfg)`) and `try-connectivity` (`(:bind-password cfg)`), so a wrong type for the password no longer slips through unvalidated - Fix `login-with-ldap` silently dropping its error message on the `ldap-not-initialized` restriction (typo `:hide` → `:hint`); the message `"ldap auth provider is not initialized"` now actually surfaces in logs and error responses instead of being discarded into an unread key - Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored (`userinfo` / `token`) in the OIDC callback, causing "incomplete user info" failures during registration [Github #9108](https://github.com/penpot/penpot/issues/9108) diff --git a/frontend/src/app/main/data/workspace/versions.cljs b/frontend/src/app/main/data/workspace/versions.cljs index 072f8c634a..b942add4d8 100644 --- a/frontend/src/app/main/data/workspace/versions.cljs +++ b/frontend/src/app/main/data/workspace/versions.cljs @@ -359,23 +359,28 @@ (rx/empty)))))))) (defn restore-version-from-plugin - [file-id id resolve _reject] + [file-id id resolve reject] (assert (uuid? id) "expected valid uuid for `id`") (ptk/reify ::restore-version-from-plugins ptk/WatchEvent (watch [_ _ _] - (rx/concat - (rx/of (ev/event {::ev/name "restore-version" - ::ev/origin "plugins"}) - ::dwp/force-persist) + (->> (rx/concat + (rx/of (ev/event {::ev/name "restore-version" + ::ev/origin "plugins"}) + ::dwp/force-persist) - (->> (wait-for-persistence file-id id) - (rx/map #(initialize-version))) + (->> (wait-for-persistence file-id id) + (rx/map #(initialize-version))) - (->> (rx/of 1) - (rx/tap resolve) - (rx/ignore)))))) + (->> (rx/of 1) + (rx/tap resolve) + (rx/ignore))) + + ;; On error reject the promise and empty the stream + (rx/catch (fn [error] + (reject error) + (rx/empty))))))) From c6bea65a48ced70bbdaf638b1f9f3154311c4ff7 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Fri, 24 Apr 2026 18:11:16 +0200 Subject: [PATCH 232/288] :sparkles: Add organization logo to nitrate invitations emails --- backend/resources/app/email/invite-to-org/en.html | 4 ++-- backend/src/app/email.clj | 4 ++-- backend/src/app/rpc/commands/teams_invitations.clj | 9 +++++---- backend/src/app/rpc/management/nitrate.clj | 1 + 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/resources/app/email/invite-to-org/en.html b/backend/resources/app/email/invite-to-org/en.html index 45ec53596a..5ee16f6942 100644 --- a/backend/resources/app/email/invite-to-org/en.html +++ b/backend/resources/app/email/invite-to-org/en.html @@ -198,9 +198,9 @@
- {{org-initials}} + {% if organization-initials %}{{organization-initials}}{% endif %}
diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index 5e63d38744..fe57118a58 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -416,8 +416,8 @@ [:map [:invited-by ::sm/text] [:organization-name ::sm/text] - [:org-initials ::sm/text] - [:org-logo ::sm/uri] + [:organization-initials [:maybe :string]] + [:organization-logo ::sm/uri] [:user-name [:maybe ::sm/text]] [:token ::sm/text]]) diff --git a/backend/src/app/rpc/commands/teams_invitations.clj b/backend/src/app/rpc/commands/teams_invitations.clj index aaa4f22bc3..51a64a7276 100644 --- a/backend/src/app/rpc/commands/teams_invitations.clj +++ b/backend/src/app/rpc/commands/teams_invitations.clj @@ -91,6 +91,7 @@ [:map [:id ::sm/uuid] [:name :string] + [:initials [:maybe :string]] [:logo ::sm/uri]]] [:profile [:map @@ -211,8 +212,8 @@ :invited-by (:fullname profile) :user-name (:fullname member) :organization-name (:name organization) - :org-logo (:logo organization) - :org-initials (d/get-initials (:name organization)) + :organization-logo (:logo organization) + :organization-initials (:initials organization) :token itoken :extra-data ptoken})) (let [team (if (contains? cf/flags :nitrate) @@ -231,11 +232,11 @@ itoken))))) (defn create-org-invitation - [cfg {:keys [::rpc/profile-id id name logo] :as params}] + [cfg {:keys [::rpc/profile-id id name initials logo] :as params}] (let [profile (db/get-by-id cfg :profile profile-id)] (create-invitation cfg (assoc params - :organization {:id id :name name :logo logo} + :organization {:id id :name name :initials initials :logo logo} :profile profile :role :editor)))) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 8af099de97..0e7479ac6c 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -360,6 +360,7 @@ RETURNING id, name;") [:email ::sm/email] [:id ::sm/uuid] [:name ::sm/text] + [:initials [:maybe :string]] [:logo ::sm/uri]]} [cfg params] (db/tx-run! cfg ti/create-org-invitation params) From 4867358428ebe6ee8257499576e75db8e7db3103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Mon, 27 Apr 2026 12:54:23 +0200 Subject: [PATCH 233/288] :sparkles: Add modal to subscribe to nitrate from unlimited --- .../src/app/main/ui/dashboard/subscription.cljs | 7 ++++++- .../src/app/main/ui/settings/subscription.cljs | 15 +++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/subscription.cljs b/frontend/src/app/main/ui/dashboard/subscription.cljs index 743de20347..ba07f40e25 100644 --- a/frontend/src/app/main/ui/dashboard/subscription.cljs +++ b/frontend/src/app/main/ui/dashboard/subscription.cljs @@ -121,6 +121,8 @@ {::mf/props :obj} [{:keys [profile teams]}] (let [nitrate? (dnt/is-valid-license? profile) + nitrate-license (:subscription profile) + subscription-type (if nitrate? (:type nitrate-license) (get-subscription-type (-> profile :props :subscription))) orgs (mf/with-memo [teams] (let [orgs (->> teams vals @@ -134,8 +136,11 @@ handle-click (mf/use-fn + (mf/deps nitrate-license subscription-type) (fn [] - (st/emit! (dnt/show-nitrate-popup :nitrate-form)))) + (if (= subscription-type "unlimited") + (st/emit! (dnt/show-nitrate-popup :nitrate-dialog {:nitrate-license nitrate-license :show-contact-sales-option true})) + (st/emit! (dnt/show-nitrate-popup :nitrate-form))))) handle-go-to-cc (mf/use-fn dnt/go-to-nitrate-cc-create-org)] diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index e22a65ff01..687329cbf0 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -468,8 +468,11 @@ open-contact-sales-modal (mf/use-fn - (fn [subscription-type] - (st/emit! (modal/show :nitrate-contact-sales-dialog {:subscription-type subscription-type}))))] + (mf/deps nitrate-license) + (fn [current-subscription subscription-type] + (if (= current-subscription "unlimited") + (st/emit! (dnt/show-nitrate-popup :nitrate-dialog {:nitrate-license nitrate-license :show-contact-sales-option true})) + (st/emit! (modal/show :nitrate-contact-sales-dialog {:subscription-type subscription-type})))))] (mf/with-effect [] (dom/set-html-title (tr "subscription.labels"))) @@ -623,7 +626,7 @@ (tr "subscription.settings.unlimited.autosave-benefit"), (tr "subscription.settings.unlimited.bill")] :cta-text (if (:type subscription) (tr "subscription.settings.subscribe") (tr "subscription.settings.try-it-free")) - :cta-link (if (and (contains? cf/flags :nitrate) nitrate?) #(open-contact-sales-modal "Unlimited") #(open-subscription-modal "unlimited" subscription)) + :cta-link (if (and (contains? cf/flags :nitrate) nitrate?) #(open-contact-sales-modal subscription-type "Unlimited") #(open-subscription-modal "unlimited" subscription)) :cta-text-with-icon (tr "subscription.settings.more-information") :cta-link-with-icon go-to-pricing-page :recommended (= subscription-type "professional") @@ -655,7 +658,7 @@ "Acceso exclusivo al Control Center" "Lorem ipsum"] :cta-text (if nitrate-license (tr "subscription.settings.subscribe") "Try 14 days for free") - :cta-link #(open-subscription-modal "nitrate" subscription) + :cta-link (if (= subscription-type "unlimited") #(open-contact-sales-modal subscription-type "Nitrate") #(open-subscription-modal "nitrate" subscription)) :cta-text-with-icon (tr "subscription.settings.more-information") :cta-link-with-icon go-to-pricing-page :show-button-cta (not nitrate-license)}])]]])) @@ -668,7 +671,7 @@ (mf/defc subscribe-nitrate-dialog {::mf/register modal/components ::mf/register-as :nitrate-dialog} - [{:keys [nitrate-license] :as connectivity}] + [{:keys [nitrate-license show-contact-sales-option] :as connectivity}] ;; TODO add translations for this texts when we have the definitive ones (let [online? (:licenses connectivity) initial (mf/with-memo [] @@ -701,7 +704,7 @@ [:div {:class (stl/css :modal-title :subscription-title)} "Subcribe to the Business Nitrate plan"] - (if online? + (if (and online? (not show-contact-sales-option)) [:div {:class (stl/css :modal-content)} From 99f006d72877427a5e226b65363b041621dff5d1 Mon Sep 17 00:00:00 2001 From: Elenzakaleidos Date: Mon, 27 Apr 2026 15:44:01 +0200 Subject: [PATCH 234/288] :lipstick: Update README.md (#9171) We modified the Images and the text of the Read me page Signed-off-by: Elenzakaleidos --- README.md | 81 +++++++++++++++++++++++++++---------------------------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 988407f1b3..fd681f9afc 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,21 @@ + [uri_license]: https://www.mozilla.org/en-US/MPL/2.0 [uri_license_image]: https://img.shields.io/badge/MPL-2.0-blue.svg - - - - penpot header image - -

- License: MPL-2.0 - Penpot Community - Managed with Taiga.io - Gitpod ready-to-code + + Verified DPG + + + Penpot Community + + + Managed with Taiga.io + + + Gitpod ready-to-code +

@@ -33,15 +36,21 @@ [Penpot video](https://github.com/user-attachments/assets/7c67fd7c-04d3-4c9b-88ec-b6f5e23f8332) -Penpot is the first **open-source** design tool for design and code collaboration. Designers can create stunning designs, interactive prototypes, design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast. And all of this with no handoff drama. +Penpot is the open-source design platform for teams that build digital products at scale. -Available on browser or self-hosted, Penpot works with open standards like SVG, CSS, HTML and JSON, and it’s free! +Penpot’s key strength lies in giving you **full ownership of your design infrastructure**. Built on open source and designed for [self-hosting](https://help.penpot.app/technical-guide/getting-started/), it puts teams in complete control of their design environment supporting strict compliance and governance requirements. Whether used in the **browser or deployed on your own servers**, Penpot **works with open standards** like SVG, CSS, HTML, and JSON. -The latest updates take Penpot even further. It’s the first design tool to integrate native [design tokens](https://penpot.dev/collaboration/design-tokens)—a single source of truth to improve efficiency and collaboration between product design and development. +Real-time collaboration strengthens this foundation, helping teams scale and bring design closer to the product through top-tier capabilities. Additionally, developers feel at home using Penpot, because design is expressed as code, enabling a direct translation and shipping products faster. -With the [huge 2.0 release](https://penpot.app/dev-diaries), Penpot took the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more. +Best-in-class native [Design Tokens](https://penpot.dev/collaboration/design-tokens) provide a single source of truth between design and development. They ensure consistency, improve collaboration, and make it easier to manage complex design systems. -For organizations that need extra service for its teams, [get in touch](https://cal.com/team/penpot/talk-to-us). +The [MCP server](https://penpot.app/penpot-mcp-server) takes it further by enabling multi-directional workflows between design and code. A [powerful open API](https://help.penpot.app/mcp/#quick-start) and plugin system makes the workspace programmable, enabling automation, AI-driven workflows, and integrations with the tools and systems you already use. + +With [CSS Grid and Flex Layout](https://help.penpot.app/user-guide/designing/flexible-layouts/), teams can design responsive interfaces that behave like real code from the start. + +Combined, these features turn Penpot into a **full-stack design platform** for building scalable design systems and fully integrated product development processes. + +If your organization is scaling and needs extra support, we’re here to help. [Talk to us](https://penpot.app/talk-to-us) ## Table of contents ## @@ -54,7 +63,7 @@ For organizations that need extra service for its teams, [get in touch](https:// ## Why Penpot ## -Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration. +Penpot connects design, code, and AI workflows through a code-based approach, making designs readable by developers and AI via the MCP server. This approach helps teams ship what’s actually designed and manage design systems at scale with powerful design tokens. As a self-hosted, open-source and real-time collaboration platform, Penpot offers full flexibility, security, and ownership without vendor lock-in. Learn more about [why Penpot](https://penpot.app/why-penpot) is the platform for your team. ### Plugin system ### @@ -68,21 +77,15 @@ Penpot was built to serve both designers and developers and create a fluid desig Work with ready-to-use code and make your workflow easy and fast. The inspect tab gives instant access to SVG, CSS and HTML code. -### Self host your own instance ### - -Provide your team or organization with a completely owned collaborative design tool. Use Penpot's cloud service or deploy your own Penpot server. - ### Integrations ### -Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens. +Penpot offers [integration](https://penpot.app/integrations-api) into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens. ### Building Design Systems: design tokens, components and variants ### -Penpot brings design systems to code-minded teams: a single source of truth with native Design Tokens, Components, and Variants for scalable, reusable, and consistent UI across projects and platforms. +Penpot brings [design systems](https://penpot.app/design/design-systems) to code-minded teams: a single source of truth with native Design Tokens, Components, and Variants for scalable, reusable, and consistent UI across projects and platforms. -

- -

+Penpot Design Systems ## Getting started ## @@ -90,37 +93,31 @@ Penpot is the only design & prototype platform that is deployment agnostic. You Learn how to install it with Docker, Kubernetes, Elestio or other options on [our website](https://penpot.app/self-host). -

- -

- ## Community ## We love the Open Source software community. Contributing is our passion and if it’s yours too, participate and [improve](https://community.penpot.app/c/help-us-improve-penpot/7) Penpot. All your designs, code and ideas are welcome! +Want to go a step further? Become a [Penpot Ambassador](https://penpot.app/ambassador-program) and help grow the Penpot community in your region while contributing to a global, open design ecosystem. + If you need help or have any questions; if you’d like to share your experience using Penpot or get inspired; if you’d rather meet our community of developers and designers, [join our Community](https://community.penpot.app/)! -You will find the following categories: +Categories include: - [Ask the Community](https://community.penpot.app/c/ask-for-help-using-penpot/6) - [Troubleshooting](https://community.penpot.app/c/technical/8) - [Help us Improve Penpot](https://community.penpot.app/c/help-us-improve-penpot/7) -- [#MadeWithPenpot](https://community.penpot.app/c/madewithpenpot/9) - [Events and Announcements](https://community.penpot.app/c/announcements/5) -- [Inside Penpot](https://community.penpot.app/c/inside-penpot/21) - [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12) -- [Design and Code Essentials](https://community.penpot.app/c/design-and-code-essentials/22) +- [Education](https://community.penpot.app/c/education/28) -

- Community -

+Pentpot Community ### Code of Conduct ### Anyone who contributes to Penpot, whether through code, in the community, or at an event, must adhere to the [code of conduct](https://help.penpot.app/contributing-guide/coc/) and foster a positive and safe environment. -## Contributing ## +### Contributing ### Any contribution will make a difference to improve Penpot. How can you get involved? @@ -137,9 +134,7 @@ Choose your way: To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing guide](https://help.penpot.app/contributing-guide/). -

- Libraries and templates -

+Penpot hub ## Resources ## @@ -155,14 +150,16 @@ You can ask and answer questions, have open-ended conversations, and follow alon 📚 [Dev Diaries](https://penpot.app/dev-diaries.html) +🧑‍🏫​ [UI Design Course](https://penpot.app/courses/) + + ## License ## -```text +``` This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. Copyright (c) KALEIDOS INC ``` - Penpot is a Kaleidos’ [open source project](https://kaleidos.net/) From f8e40a1ca519169b46bd36792df55e7088eb7c5d Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 27 Apr 2026 17:49:37 +0200 Subject: [PATCH 235/288] :bug: Fix can't add team to nitrate organization --- frontend/src/app/main/data/nitrate.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index 0741fddbac..77374a0a0f 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -130,7 +130,7 @@ (ptk/reify ::add-team-to-org ptk/WatchEvent (watch [_ _ _] - (->> (rp/cmd! ::add-team-to-org {:team-id team-id :organization-id organization-id}) + (->> (rp/cmd! ::add-team-to-organization {:team-id team-id :organization-id organization-id}) (rx/mapcat (fn [_] (rx/of (modal/hide)))))))) From f4cf667d2f107907d4e2c1e050e94705e3fe3080 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 27 Apr 2026 17:56:41 +0200 Subject: [PATCH 236/288] :books: Update changelog --- CHANGES.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ff83586555..2779dcc03c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,8 +6,6 @@ ### :rocket: Epics and highlights -- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112) - ### :sparkles: New features & Enhancements - Show alpha percentage next to library color values to distinguish colors that differ only in opacity (by @rockchris099) [Github #6328](https://github.com/penpot/penpot/issues/6328) From ea265da1f37aa8a8b114594aaee96a1f7d44770a Mon Sep 17 00:00:00 2001 From: boskodev790 Date: Mon, 27 Apr 2026 10:59:09 -0500 Subject: [PATCH 237/288] :bug: Fix plugin library.connectLibrary breaking Promise contract on permission failure (#9158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `library.connectLibrary()` declared its permission check **outside** the `js/Promise.` wrapper, so when a plugin without `library:write` permission called `await library.connectLibrary(id)` the method did not return a `Promise` at all: - With the default `throwValidationErrors` flag off → `u/not-valid` logs to console and returns `nil`. `await nil` resolves to `nil`, so the plugin sees a "successful" result and crashes later when it tries to use methods on what it thinks is a `LibraryProxy`. - With `throwValidationErrors` on → `u/not-valid` throws synchronously, so the caller gets a thrown exception instead of a rejected promise — inconsistent with every other `library:*` / `content:*` method which always returns a Promise that rejects via `reject-not-valid`. Additionally, the in-Promise `(not (string? library-id))` branch used `(reject nil)` — the plugin got a rejected Promise but with no error message. Move the permission check inside the Promise constructor and replace both validation errors with `u/reject-not-valid`, matching the pattern used by the sibling methods `restore`, `remove`, `pin`, `saveVersion`, `findVersions` in `frontend/src/app/plugins/file.cljs` and every other promise-returning plugin method. No new imports. Also add a CHANGES.md entry under the 2.17.0 Unreleased bugs-fixed section. Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + frontend/src/app/plugins/library.cljs | 33 ++++++++++++--------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2779dcc03c..e8fdc0b05d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -49,6 +49,7 @@ ### :bug: Bugs fixed - Fix plugin API `fileVersion.restore()` promise hanging indefinitely on restore failure [Github #9092](https://github.com/penpot/penpot/issues/9092) +- Fix plugin API `library.connectLibrary()` returning a non-Promise (or throwing synchronously) when the plugin lacks `library:write` permission — the method now always returns a `Promise` and rejects with a structured error message, matching the contract used by every other Promise-returning plugin method (`restore`, `remove`, `pin`, `saveVersion`, `findVersions`, …) - Fix LDAP provider params schema typo (`bind-passwor` → `bind-password`) introduced during the `clojure.spec` → `malli` migration; the schema slot now matches the runtime key actually read by `prepare-params` (`:password (:bind-password cfg)`) and `try-connectivity` (`(:bind-password cfg)`), so a wrong type for the password no longer slips through unvalidated - Fix `login-with-ldap` silently dropping its error message on the `ldap-not-initialized` restriction (typo `:hide` → `:hint`); the message `"ldap auth provider is not initialized"` now actually surfaces in logs and error responses instead of being discarded into an unread key - Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored (`userinfo` / `token`) in the OIDC callback, causing "incomplete user info" failures during registration [Github #9108](https://github.com/penpot/penpot/issues/9108) diff --git a/frontend/src/app/plugins/library.cljs b/frontend/src/app/plugins/library.cljs index 1022a1a65c..8a249d32dc 100644 --- a/frontend/src/app/plugins/library.cljs +++ b/frontend/src/app/plugins/library.cljs @@ -1108,23 +1108,20 @@ :connectLibrary (fn [library-id] - (cond - (not (r/check-permission plugin-id "library:write")) - (u/not-valid plugin-id :connectLibrary "Plugin doesn't have 'library:write' permission") + (js/Promise. + (fn [resolve reject] + (cond + (not (r/check-permission plugin-id "library:write")) + (u/reject-not-valid reject :connectLibrary "Plugin doesn't have 'library:write' permission") - :else - (js/Promise. - (fn [resolve reject] - (cond - (not (string? library-id)) - (do (u/not-valid plugin-id :connectLibrary library-id) - (reject nil)) + (not (string? library-id)) + (u/reject-not-valid reject :connectLibrary library-id) - :else - (let [file-id (:current-file-id @st/state) - library-id (uuid/parse library-id)] - (->> st/stream - (rx/filter (ptk/type? ::dwl/attach-library-finished)) - (rx/take 1) - (rx/subs! #(resolve (library-proxy plugin-id library-id)) reject)) - (st/emit! (dwl/link-file-to-library file-id library-id)))))))))) + :else + (let [file-id (:current-file-id @st/state) + library-id (uuid/parse library-id)] + (->> st/stream + (rx/filter (ptk/type? ::dwl/attach-library-finished)) + (rx/take 1) + (rx/subs! #(resolve (library-proxy plugin-id library-id)) reject)) + (st/emit! (dwl/link-file-to-library file-id library-id))))))))) From 8a8ebb79435c2210fa71ec237977dea976662174 Mon Sep 17 00:00:00 2001 From: Renzo <170978465+RenzoMXD@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:35:52 +0200 Subject: [PATCH 238/288] :sparkles: Preserve vector content when pasting from external tools (#9182) Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com> Co-authored-by: Andrey Antukh --- CHANGES.md | 2 ++ frontend/src/app/util/clipboard.js | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e8fdc0b05d..e5b9dd1bb0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,8 @@ - Add a search bar to filter colors in the color palette toolbar (by @eureka0928) [Github #7653](https://github.com/penpot/penpot/issues/7653) - Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027) - Add page separators in Workspace [Taiga #13611](https://tree.taiga.io/project/penpot/us/13611?milestone=262806) +- Preserve vector content when pasting from external tools such as Inkscape: recognise SVG sent as text/plain (with optional XML declaration and HTML comments), skip the raster preview when an SVG sibling is on the clipboard, and ignore empty SVG blobs that some tools advertise alongside the real payload, so pasted graphics arrive editable without spurious "SVG is invalid" warnings [Github #546](https://github.com/penpot/penpot/issues/546) + - Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts [Github #2457](https://github.com/penpot/penpot/issues/2457) ### :bug: Bugs fixed diff --git a/frontend/src/app/util/clipboard.js b/frontend/src/app/util/clipboard.js index 294652666a..43bd636b88 100644 --- a/frontend/src/app/util/clipboard.js +++ b/frontend/src/app/util/clipboard.js @@ -24,6 +24,13 @@ const exclusiveTypes = [ "text/plain" ]; +const svgTextPattern = + /^(\s*<\?xml[^?]*\?>\s*)?(\s*\s*)*]/i; + +function hasSvgItem(items) { + return items.some((item) => item?.type === "image/svg+xml"); +} + /** * @typedef {Object} ClipboardSettings * @property {Function} [decodeTransit] @@ -59,7 +66,7 @@ function parseText(text, options) { } } - if (/^]/i.test(text)) { + if (svgTextPattern.test(text)) { return new Blob([text], { type: "image/svg+xml" }); } else { return new Blob([text], { type: "text/plain" }); @@ -207,14 +214,20 @@ export async function fromDataTransfer(dataTransfer, options) { }), ); return items - .filter((item) => !!item) - .reduce((filtered, item) => { + .filter((item) => !!item && item.size > 0) + .reduce((filtered, item, _index, all) => { if ( exclusiveTypes.includes(item.type) && filtered.find((filteredItem) => exclusiveTypes.includes(filteredItem.type)) ) { return filtered; } + if ( + item.type !== "image/svg+xml" && item.type.startsWith("image/") && + hasSvgItem(all) + ) { + return filtered; + } filtered.push(item); return filtered; }, []); From bd1e0fb23f881f7f0c77f32d02d9949efff4a67c Mon Sep 17 00:00:00 2001 From: Milos Milic <124778054+MilosM348@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:36:15 +0200 Subject: [PATCH 239/288] :sparkles: Add Alt+click to expand a layer subtree in the Layers sidebar (#9179) Closes #7736. The Layers sidebar offered no way to expand every nested level of a single subtree at once. Unfolding a layer that wraps a deep tree required clicking each disclosure indicator one level at a time - O(siblings * depth) clicks. The asymmetry was particularly visible next to the existing Shift+click gesture, which collapses every layer in the panel in a single action via `dwc/collapse-all`, with no expand counterpart for either a single subtree or the whole tree. Add a new `dwc/expand-subtree` event in `app.main.data.workspace.collapse` that uses `cfh/get-children-ids-with-self` to gather the shape's id together with every descendant id, then merges `{descendant-id true}` entries into `[:workspace-local :expanded]` so the entire subtree opens in one update. Existing expansion state on unrelated branches is left untouched (`merge`, not `assoc`), matching the per-key shape used by `toggle-collapse` and `expand-collapse`. Wire the gesture into `layer_item.cljs` `toggle-collapse` callback as a third branch: - Shift+click while expanded - collapse every layer (existing). - Alt+click while collapsed - expand the entire subtree (new). - Otherwise - toggle this single level (existing). Alt is chosen instead of Shift to avoid the ambiguity the issue author flagged: "for a layer of middle depth it is unclear whether [Shift+click] should fold all (up to the topmost parent) or expand all (only the current subtree)". Alt is a common platform convention for "do this recursively" (Finder, file managers, several IDEs), so the asymmetric mapping matches user expectations. The callback's `mf/deps` vector is extended with `id` and `objects` so the closure refreshes when the shape tree changes. CHANGES.md entry added under the 2.17.0 New features section. --- CHANGES.md | 1 + .../src/app/main/data/workspace/collapse.cljs | 16 ++++++++++++++++ .../main/ui/workspace/sidebar/layer_item.cljs | 12 ++++++++++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e5b9dd1bb0..2518f1b0cb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ ### :sparkles: New features & Enhancements +- Add `Alt+click` on a layer's disclosure arrow to recursively expand the entire subtree rooted at that layer in the Layers sidebar; symmetric with the existing `Shift+click` collapse-all gesture, and removes the O(siblings × depth) click cost of unfolding a deep subtree one level at a time [Github #7736](https://github.com/penpot/penpot/issues/7736) - Show alpha percentage next to library color values to distinguish colors that differ only in opacity (by @rockchris099) [Github #6328](https://github.com/penpot/penpot/issues/6328) - Add "Clear artboard guides" option to right-click context menu for frames (by @eureka0928) [Github #6987](https://github.com/penpot/penpot/issues/6987) - Add loader feedback while importing and exporting files [Github #9020](https://github.com/penpot/penpot/issues/9020) diff --git a/frontend/src/app/main/data/workspace/collapse.cljs b/frontend/src/app/main/data/workspace/collapse.cljs index 1143a6f4d8..b5b4998c6b 100644 --- a/frontend/src/app/main/data/workspace/collapse.cljs +++ b/frontend/src/app/main/data/workspace/collapse.cljs @@ -49,3 +49,19 @@ (update [_ state] (update state :workspace-local dissoc :expanded)))) +(defn expand-subtree + "Recursively expand the layer subtree rooted at `id`, marking the shape + and all of its descendants as expanded in the Layers sidebar. + + Closes the gap with `collapse-all`: there was no symmetric way to + open every nested level of a single subtree, so unfolding a deep + shape required clicking each disclosure indicator one by one + (O(siblings × depth) clicks)." + [id objects] + (ptk/reify ::expand-subtree + ptk/UpdateEvent + (update [_ state] + (let [ids (cfh/get-children-ids-with-self objects id) + expansions (into {} (map (fn [descendant-id] [descendant-id true])) ids)] + (update-in state [:workspace-local :expanded] merge expansions))))) + diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs index b17698d659..d31351adc8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs @@ -275,11 +275,19 @@ toggle-collapse (mf/use-fn - (mf/deps is-expanded) + (mf/deps is-expanded id objects) (fn [event] (dom/stop-propagation event) - (if (and is-expanded (kbd/shift? event)) + (cond + ;; Shift+click while expanded collapses every layer in the sidebar + (and is-expanded (kbd/shift? event)) (st/emit! (dwc/collapse-all)) + + ;; Alt+click while collapsed expands the entire subtree rooted at this id + (and (not is-expanded) (kbd/alt? event)) + (st/emit! (dwc/expand-subtree id objects)) + + :else (st/emit! (dwc/toggle-collapse id))))) toggle-blocking From aa5bfe6dda6944ed51729899f56bcc958a95b87f Mon Sep 17 00:00:00 2001 From: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:37:27 -0400 Subject: [PATCH 240/288] :sparkles: Add customizable pixel grid color (#9155) Let users pick the pixel grid color from a standard color picker. The grid color was previously hardcoded, making it invisible on mid-tone canvases. Choice is stored on the file so it persists across sessions. Defaults preserve the current appearance when unset. Closes #7750 Signed-off-by: jack-stormentswe --- CHANGES.md | 2 +- common/src/app/common/files/changes.cljc | 26 ++++++++-- .../src/app/common/files/changes_builder.cljc | 18 +++++-- common/src/app/common/types/page.cljc | 4 ++ frontend/src/app/main/data/workspace.cljs | 18 +++++++ .../ui/workspace/sidebar/options/page.cljs | 34 ++++++++++++- .../main/ui/workspace/viewport/widgets.cljs | 48 ++++++++++++------- 7 files changed, 123 insertions(+), 27 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2518f1b0cb..11b308b336 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -48,7 +48,7 @@ - Preserve vector content when pasting from external tools such as Inkscape: recognise SVG sent as text/plain (with optional XML declaration and HTML comments), skip the raster preview when an SVG sibling is on the clipboard, and ignore empty SVG blobs that some tools advertise alongside the real payload, so pasted graphics arrive editable without spurious "SVG is invalid" warnings [Github #546](https://github.com/penpot/penpot/issues/546) - Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts [Github #2457](https://github.com/penpot/penpot/issues/2457) - +- Adds a **Pixel grid color** picker in the viewport settings, next to the existing canvas color control [Github #7750](https://github.com/penpot/penpot/issues/7750) ### :bug: Bugs fixed - Fix plugin API `fileVersion.restore()` promise hanging indefinitely on restore failure [Github #9092](https://github.com/penpot/penpot/issues/9092) diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 5b45a7ecd4..c9aa3d3faa 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -261,7 +261,11 @@ ;; All props are optional, background can be nil because is the ;; way to remove already set background [:background {:optional true} [:maybe ctc/schema:hex-color]] - [:name {:optional true} :string]]] + [:name {:optional true} :string] + ;; Pixel grid display controls — nil removes the per-page override + ;; and falls back to the default hardcoded grid color/opacity. + [:pixel-grid-color {:optional true} [:maybe ctc/schema:hex-color]] + [:pixel-grid-opacity {:optional true} [:maybe ::sm/safe-number]]]] [:set-plugin-data schema:set-plugin-data-change] @@ -853,8 +857,10 @@ [data {:keys [id] :as params}] (d/update-in-when data [:pages-index id] (fn [page] - (let [name (get params :name) - bg (get params :background :not-found)] + (let [name (get params :name) + bg (get params :background :not-found) + grid-color (get params :pixel-grid-color :not-found) + grid-op (get params :pixel-grid-opacity :not-found)] (cond-> page (string? name) (assoc :name name) @@ -863,7 +869,19 @@ (assoc :background bg) (nil? bg) - (dissoc :background)))))) + (dissoc :background) + + (string? grid-color) + (assoc :pixel-grid-color grid-color) + + (and (not= grid-color :not-found) (nil? grid-color)) + (dissoc :pixel-grid-color) + + (number? grid-op) + (assoc :pixel-grid-opacity grid-op) + + (and (not= grid-op :not-found) (nil? grid-op)) + (dissoc :pixel-grid-opacity)))))) (defmethod process-change :set-plugin-data [data {:keys [object-type object-id page-id namespace key value]}] diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 93ac58d03b..5106493082 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -213,21 +213,33 @@ (let [page (::page (meta changes))] (mod-page changes page options))) - ([changes page {:keys [name background]}] + ([changes page {:keys [name background pixel-grid-color pixel-grid-opacity]}] (let [change {:type :mod-page :id (:id page)} redo (cond-> change (some? name) (assoc :name name) (some? background) - (assoc :background background)) + (assoc :background background) + + (some? pixel-grid-color) + (assoc :pixel-grid-color pixel-grid-color) + + (some? pixel-grid-opacity) + (assoc :pixel-grid-opacity pixel-grid-opacity)) undo (cond-> change (some? name) (assoc :name (:name page)) (some? background) - (assoc :background (:background page)))] + (assoc :background (:background page)) + + (some? pixel-grid-color) + (assoc :pixel-grid-color (:pixel-grid-color page)) + + (some? pixel-grid-opacity) + (assoc :pixel-grid-opacity (:pixel-grid-opacity page)))] (-> changes (update :redo-changes conj redo) diff --git a/common/src/app/common/types/page.cljc b/common/src/app/common/types/page.cljc index 0f3e05f97a..e6fa26a006 100644 --- a/common/src/app/common/types/page.cljc +++ b/common/src/app/common/types/page.cljc @@ -59,6 +59,10 @@ [:guides {:optional true} schema:guides] [:plugin-data {:optional true} ctpg/schema:plugin-data] [:background {:optional true} ctc/schema:hex-color] + ;; Per-page pixel grid color. Falls back to a hardcoded default when + ;; unset so existing files render identically to before. + [:pixel-grid-color {:optional true} ctc/schema:hex-color] + [:pixel-grid-opacity {:optional true} ::sm/safe-number] [:comment-thread-positions {:optional true} [:map-of ::sm/uuid schema:comment-thread-position]]]) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 1d5acfed38..8dffa26e77 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1256,6 +1256,24 @@ (pcb/mod-page {:background (:color color)}))] (rx/of (dch/commit-changes changes))))))) +(defn change-pixel-grid-color + "Update the pixel grid color (and optional alpha) for the given page. + Mirrors `change-canvas-color` — stored on the page so the choice + travels with the file and persists across sessions." + ([color] + (change-pixel-grid-color nil color)) + ([page-id color] + (ptk/reify ::change-pixel-grid-color + ptk/WatchEvent + (watch [it state _] + (let [page-id (or page-id (:current-page-id state)) + page (dsh/lookup-page state page-id) + changes (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/mod-page {:pixel-grid-color (:color color) + :pixel-grid-opacity (:opacity color)}))] + (rx/of (dch/commit-changes changes))))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs index 740750b0cf..c5876940b8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs @@ -24,17 +24,39 @@ (-> (l/key :background) (l/derived refs/workspace-page))) +(def ^:private ref:pixel-grid-color + (-> (l/key :pixel-grid-color) + (l/derived refs/workspace-page))) + +(def ^:private ref:pixel-grid-opacity + (-> (l/key :pixel-grid-opacity) + (l/derived refs/workspace-page))) + +;; Default pixel grid color shown in the picker when the user hasn't +;; set a custom one. Matches the legacy hardcoded CSS variable. +(def ^:private default-pixel-grid-color "#0070E4") + (mf/defc options* {::mf/wrap [mf/memo]} [] (let [background (mf/deref ref:background-color) + grid-color (mf/deref ref:pixel-grid-color) + grid-alpha (mf/deref ref:pixel-grid-opacity) + on-change (mf/use-fn #(st/emit! (dw/change-canvas-color %))) on-open (mf/use-fn #(st/emit! (dwu/start-undo-transaction :options))) on-close (mf/use-fn #(st/emit! (dwu/commit-undo-transaction :options))) + on-grid-change + (mf/use-fn #(st/emit! (dw/change-pixel-grid-color %))) + color (mf/with-memo [background] {:color (d/nilv background clr/canvas) - :opacity 1})] + :opacity 1}) + + grid (mf/with-memo [grid-color grid-alpha] + {:color (d/nilv grid-color default-pixel-grid-color) + :opacity (d/nilv grid-alpha 0.2)})] [:div {:class (stl/css :element-set)} [:div {:class (stl/css :element-title)} @@ -52,5 +74,15 @@ :on-change on-change :origin :canvas :on-open on-open + :on-close on-close}] + + [:> color-row* + {:disable-gradient true + :disable-image true + :title "Pixel grid color" + :color grid + :on-change on-grid-change + :origin :pixel-grid + :on-open on-open :on-close on-close}]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index a47897d2d6..dac8657ad2 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -33,24 +33,36 @@ (mf/defc pixel-grid* [{:keys [vbox zoom]}] - [:g.pixel-grid - [:defs - [:pattern {:id "pixel-grid" - :viewBox "0 0 1 1" - :width 1 - :height 1 - :pattern-units "userSpaceOnUse"} - [:path {:d "M 1 0 L 0 0 0 1" - :style {:fill "none" - :stroke (if (dbg/enabled? :pixel-grid) "red" "var(--status-color-info-500)") - :stroke-opacity (if (dbg/enabled? :pixel-grid) 1 "0.2") - :stroke-width (str (/ 1 zoom))}}]]] - [:rect {:x (:x vbox) - :y (:y vbox) - :width (:width vbox) - :height (:height vbox) - :fill (str "url(#pixel-grid)") - :style {:pointer-events "none"}}]]) + (let [page (mf/deref refs/workspace-page) + custom-color (:pixel-grid-color page) + custom-alpha (:pixel-grid-opacity page) + debug? (dbg/enabled? :pixel-grid) + stroke (cond + debug? "red" + custom-color custom-color + :else "var(--status-color-info-500)") + opacity (cond + debug? 1 + (some? custom-alpha) custom-alpha + :else 0.2)] + [:g.pixel-grid + [:defs + [:pattern {:id "pixel-grid" + :viewBox "0 0 1 1" + :width 1 + :height 1 + :pattern-units "userSpaceOnUse"} + [:path {:d "M 1 0 L 0 0 0 1" + :style {:fill "none" + :stroke stroke + :stroke-opacity opacity + :stroke-width (str (/ 1 zoom))}}]]] + [:rect {:x (:x vbox) + :y (:y vbox) + :width (:width vbox) + :height (:height vbox) + :fill (str "url(#pixel-grid)") + :style {:pointer-events "none"}}]])) (mf/defc cursor-tooltip* [{:keys [zoom tooltip]}] From 61ce4b9e0d0e31fa3472b85884e0c4cfade77a8f Mon Sep 17 00:00:00 2001 From: FairyPiggyDev Date: Tue, 28 Apr 2026 09:59:05 -0400 Subject: [PATCH 241/288] :sparkles: Add "Delete group" to assets panel context menu (#9151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When working with large asset groups, users asked for a one-click way to remove every asset under a group path. Multi-select across hundreds of items is impractical, and ungrouping first and then deleting leaves the orphaned items in the flat list. This change adds a "Delete group" option to the assets-panel context-menu for three asset types that already carry group structure: - Components (including variants — sibling variants sharing a variant container are deduplicated, and the container is deleted once via the same dispatch the per-item delete uses in file_library.cljs). - Colors. - Typographies. A confirmation modal is shown before deletion, with the count of assets to be removed, so the action is never silent. All deletes run inside a single undo transaction, so one Cmd+Z restores the whole group. Changes ------- - `assets/groups.cljs` — `asset-group-title*` accepts an optional `on-delete-group` prop and conditionally adds the menu entry between "Ungroup" and "Combine as variants". When the callback is not supplied the option is hidden, so asset sections that do not implement it stay unaffected. - `assets/components.cljs` — threads `on-delete-group` through the recursive `components-group*` and defines the section-level handler, dispatching to `dwsh/delete-shapes` for variant containers and `dwl/delete-component` for plain components. - `assets/colors.cljs` — same threading + a simple `dwl/delete-color` dispatch per color in the group. - `assets/typographies.cljs` — same threading + a `dwl/delete-typography` dispatch per typography in the group. - `translations/en.po` — three new strings: the menu label (`workspace.assets.delete-group`) and the modal title/message (`modals.delete-asset-group.title`/`.message`, plural-aware). Github #9141 Signed-off-by: FairyPigDev Signed-off-by: FairyPiggyDev --- CHANGES.md | 1 + .../ui/workspace/sidebar/assets/colors.cljs | 40 ++++++++++++- .../workspace/sidebar/assets/components.cljs | 60 ++++++++++++++++++- .../ui/workspace/sidebar/assets/groups.cljs | 8 ++- .../sidebar/assets/typographies.cljs | 38 +++++++++++- frontend/translations/en.po | 14 +++++ 6 files changed, 156 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 11b308b336..3fccd4f985 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ ### :sparkles: New features & Enhancements +- Add "Delete group" option to the assets panel context menu for components, colors and typographies (by @FairyPigDev) [Github #9141](https://github.com/penpot/penpot/issues/9141) - Add `Alt+click` on a layer's disclosure arrow to recursively expand the entire subtree rooted at that layer in the Layers sidebar; symmetric with the existing `Shift+click` collapse-all gesture, and removes the O(siblings × depth) click cost of unfolding a deep subtree one level at a time [Github #7736](https://github.com/penpot/penpot/issues/7736) - Show alpha percentage next to library color values to distinguish colors that differ only in opacity (by @rockchris099) [Github #6328](https://github.com/penpot/penpot/issues/6328) - Add "Clear artboard guides" option to right-click context menu for frames (by @eureka0928) [Github #6987](https://github.com/penpot/penpot/issues/6987) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs index c0395cf4f4..83df80b9db 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs @@ -280,7 +280,7 @@ (mf/defc colors-group [{:keys [file-id prefix groups open-groups force-open? local? selected multi-colors? multi-assets? on-asset-click on-assets-delete - on-clear-selection on-group on-rename-group on-ungroup colors + on-clear-selection on-group on-rename-group on-ungroup on-delete-group colors selected-full]}] (let [group-open? (if (false? (get open-groups prefix)) ;; if the user has closed it specifically, respect that false @@ -325,7 +325,8 @@ :path prefix :is-group-open group-open? :on-rename on-rename-group - :on-ungroup on-ungroup}] + :on-ungroup on-ungroup + :on-delete-group on-delete-group}] (when group-open? [:* (let [colors (get groups "" [])] @@ -378,6 +379,7 @@ :on-group on-group :on-rename-group on-rename-group :on-ungroup on-ungroup + :on-delete-group on-delete-group :colors colors :selected-full selected-full}]))])])) @@ -499,6 +501,39 @@ file-id)))) (st/emit! (dwu/commit-undo-transaction undo-id))))) + ;; Issue #9141. Delete every color under a group path in a + ;; single undo transaction, after user confirmation. + on-delete-group + (mf/use-fn + (mf/deps colors on-clear-selection) + (fn [path] + (let [group-colors + (->> colors + (filter #(str/starts-with? (:path %) path))) + + ;; Hoisted so the start/commit pair is bound to the + ;; same symbol regardless of how `do-delete` is + ;; invoked by the confirm modal. Review suggestion + ;; on PR #9151. + undo-id (js/Symbol) + + do-delete + (fn [] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! + (map #(dwl/delete-color {:id (:id %)}) group-colors)) + (st/emit! (dwu/commit-undo-transaction undo-id)))] + (when (seq group-colors) + (st/emit! + (modal/show + {:type :confirm + :title (tr "modals.delete-asset-group.title") + :message (tr "modals.delete-asset-group.message" + (i18n/c (count group-colors))) + :accept-label (tr "labels.delete") + :on-accept do-delete})))))) + on-asset-click (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))] @@ -533,5 +568,6 @@ :on-group on-group :on-rename-group on-rename-group :on-ungroup on-ungroup + :on-delete-group on-delete-group :colors colors :selected-full selected-full}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs index 077742d71d..2d9d1b72e7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs @@ -17,6 +17,7 @@ [app.main.data.workspace :as dw] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.media :as dwm] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.variants :as dwv] [app.main.refs :as refs] @@ -191,7 +192,7 @@ (mf/defc components-group* [{:keys [file-id prefix groups open-groups is-force-open renaming is-listing-thumbs selected on-asset-click - on-drag-start do-rename cancel-rename on-rename-group on-group on-ungroup on-context-menu + on-drag-start do-rename cancel-rename on-rename-group on-group on-ungroup on-delete-group on-context-menu selected-full is-local count-variants on-group-combine-variants]}] (let [group-open? (if (false? (get open-groups prefix)) ;; if the user has closed it specifically, respect that @@ -246,6 +247,7 @@ :is-can-combine can-combine? :on-rename on-rename-group :on-ungroup on-ungroup + :on-delete-group on-delete-group :on-group-combine-variants on-group-combine-variants}] (when group-open? @@ -303,6 +305,7 @@ :cancel-rename cancel-rename :on-rename-group on-rename-group :on-ungroup on-ungroup + :on-delete-group on-delete-group :on-context-menu on-context-menu :on-group-combine-variants on-group-combine-variants :selected-full selected-full @@ -493,6 +496,60 @@ (map #(dwv/rename-comp-or-variant-and-main (:id %) (cmm/ungroup % path))))) (st/emit! (dwu/commit-undo-transaction undo-id))))) + ;; Issue #9141. Delete every component under a group path in a + ;; single undo transaction, after user confirmation. Variants + ;; are handled via their variant container (matching the + ;; per-item delete dispatch in file_library.cljs); sibling + ;; variants sharing a container are deduplicated so we delete + ;; each container only once. + on-delete-group + (mf/use-fn + (mf/deps components on-clear-selection) + (fn [path] + (let [group-components + (->> components + (filter #(cpn/inside-path? (:path %) path))) + + {variants true non-variants false} + (group-by (comp boolean ctc/is-variant?) group-components) + + ;; One delete-shapes per variant container, not per + ;; sibling variant within that container. + variant-containers + (->> variants + (group-by :variant-id) + (map (fn [[_ comps]] (first comps)))) + + ;; Hoisted so the start/commit pair is bound to the + ;; same symbol regardless of how `do-delete` is + ;; invoked by the confirm modal. Review suggestion + ;; on PR #9151. + undo-id (js/Symbol) + + do-delete + (fn [] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! + (map (fn [component] + (dwsh/delete-shapes (:main-instance-page component) + #{(:variant-id component)})) + variant-containers)) + (run! st/emit! + (map (fn [component] + (dwl/delete-component {:id (:id component)})) + non-variants)) + (st/emit! (dwu/commit-undo-transaction undo-id)))] + (when (seq group-components) + (st/emit! + (modal/show + {:type :confirm + :title (tr "modals.delete-asset-group.title") + :message (tr "modals.delete-asset-group.message" + (i18n/c (count group-components))) + :accept-label (tr "labels.delete") + :on-accept do-delete})))))) + on-group-combine-variants (mf/use-fn (mf/deps components on-clear-selection) @@ -602,6 +659,7 @@ :on-rename-group on-rename-group :on-group on-group :on-ungroup on-ungroup + :on-delete-group on-delete-group :on-group-combine-variants on-group-combine-variants :on-context-menu on-context-menu :selected-full selected-full diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs index 4a781fa48a..c7e72c871b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs @@ -23,7 +23,7 @@ [rumext.v2 :as mf])) (mf/defc asset-group-title* - [{:keys [file-id section path is-group-open on-rename on-ungroup on-group-combine-variants is-can-combine on-add]}] + [{:keys [file-id section path is-group-open on-rename on-ungroup on-delete-group on-group-combine-variants is-can-combine on-add]}] (when-not (empty? path) (let [[other-path last-path truncated] (cpn/compact-path path 35 true) menu-state (mf/use-state cmm/initial-context-menu-state) @@ -69,6 +69,12 @@ {:name (tr "workspace.assets.ungroup") :id "assets-ungroup-group" :handler #(on-ungroup path)}] + on-delete-group + (conj + {:name (tr "workspace.assets.delete-group") + :id "assets-delete-group" + :handler #(on-delete-group path)}) + is-can-combine (conj {:name (tr "workspace.shape.menu.combine-as-variants") diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs index 082fecb996..cf95f8b477 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs @@ -134,7 +134,7 @@ {::mf/wrap-props false} [{:keys [file-id prefix groups open-groups force-open? file local? selected local-data editing-id renaming-id on-asset-click handle-change on-rename-group - on-ungroup on-context-menu selected-full is-read-only]}] + on-ungroup on-delete-group on-context-menu selected-full is-read-only]}] (let [group-open? (if (false? (get open-groups prefix)) ;; if the user has closed it specifically, respect that false (get open-groups prefix true)) @@ -185,6 +185,7 @@ :is-group-open group-open? :on-rename on-rename-group :on-ungroup on-ungroup + :on-delete-group on-delete-group :on-add (when (and local? (not is-read-only)) add-typography-to-group)}] @@ -238,6 +239,7 @@ :handle-change handle-change :on-rename-group on-rename-group :on-ungroup on-ungroup + :on-delete-group on-delete-group :on-context-menu on-context-menu :selected-full selected-full :is-read-only is-read-only}]))])])) @@ -352,6 +354,39 @@ (cmm/ungroup % path))))) (st/emit! (dwu/commit-undo-transaction undo-id))))) + ;; Issue #9141. Delete every typography under a group path in a + ;; single undo transaction, after user confirmation. + on-delete-group + (mf/use-fn + (mf/deps typographies file-id on-clear-selection) + (fn [path] + (let [group-typographies + (->> typographies + (filter #(str/starts-with? (:path %) path))) + + ;; Hoisted so the start/commit pair is bound to the + ;; same symbol regardless of how `do-delete` is + ;; invoked by the confirm modal. Review suggestion + ;; on PR #9151. + undo-id (js/Symbol) + + do-delete + (fn [] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! + (map #(dwl/delete-typography (:id %)) group-typographies)) + (st/emit! (dwu/commit-undo-transaction undo-id)))] + (when (seq group-typographies) + (st/emit! + (modal/show + {:type :confirm + :title (tr "modals.delete-asset-group.title") + :message (tr "modals.delete-asset-group.message" + (i18n/c (count group-typographies))) + :accept-label (tr "labels.delete") + :on-accept do-delete})))))) + on-context-menu (mf/use-fn (mf/deps selected on-clear-selection read-only?) @@ -441,6 +476,7 @@ :handle-change handle-change :on-rename-group on-rename-group :on-ungroup on-ungroup + :on-delete-group on-delete-group :on-context-menu on-context-menu :selected-full selected-full :is-read-only read-only?}] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 4b774cd775..f2c15954b7 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3561,6 +3561,16 @@ msgstr "Are you sure you want to delete this page?" msgid "modals.delete-page.title" msgstr "Delete page" +#: src/app/main/ui/workspace/sidebar/assets/components.cljs, src/app/main/ui/workspace/sidebar/assets/colors.cljs, src/app/main/ui/workspace/sidebar/assets/typographies.cljs +msgid "modals.delete-asset-group.title" +msgstr "Delete group" + +#: src/app/main/ui/workspace/sidebar/assets/components.cljs, src/app/main/ui/workspace/sidebar/assets/colors.cljs, src/app/main/ui/workspace/sidebar/assets/typographies.cljs +msgid "modals.delete-asset-group.message" +msgid_plural "modals.delete-asset-group.message" +msgstr[0] "Are you sure you want to delete this asset?" +msgstr[1] "Are you sure you want to delete these %s assets?" + #: src/app/main/ui/dashboard/project_menu.cljs:73 msgid "modals.delete-project-confirm.accept" msgstr "Delete project" @@ -5733,6 +5743,10 @@ msgstr "Text Transform" msgid "workspace.assets.ungroup" msgstr "Ungroup" +#: src/app/main/ui/workspace/sidebar/assets/groups.cljs +msgid "workspace.assets.delete-group" +msgstr "Delete group" + #: src/app/main/ui/workspace/colorpicker.cljs:428, src/app/main/ui/workspace/colorpicker.cljs:441 msgid "workspace.colorpicker.color-tokens" msgstr "Color tokens" From a3ddf5404305e51350b29d55df0d90c35df87552 Mon Sep 17 00:00:00 2001 From: Ingrid Pigueron Date: Tue, 28 Apr 2026 16:15:08 +0200 Subject: [PATCH 242/288] :globe_with_meridians: Add translations for: French Currently translated at 96.9% (2010 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/ --- frontend/translations/fr.po | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index d633216f26..066e6d9ad8 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-04-16 18:09+0000\n" +"PO-Revision-Date: 2026-04-28 15:09+0000\n" "Last-Translator: Ingrid Pigueron \n" "Language-Team: French \n" @@ -5945,7 +5945,7 @@ msgstr "Activer/Désactiver le flou" #: src/app/main/ui/workspace/sidebar/options/page.cljs:42, src/app/main/ui/workspace/sidebar/options/page.cljs:50 msgid "workspace.options.canvas-background" -msgstr "Couleur de fond du canvas" +msgstr "Couleur de fond du canevas" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:567 msgid "workspace.options.clip-content" @@ -8651,3 +8651,7 @@ msgstr "Outils de débogage" #: src/app/main/ui/static.cljs:315 msgid "errors.webgl-context-lost.desc-message" msgstr "WebGL ne fonctionne plus. Rechargez la page pour le réinitialiser" + +#: src/app/main/ui/static.cljs:314 +msgid "errors.webgl-context-lost.main-message" +msgstr "Oups ! Le contexte du canevas a été perdu" From f060b8d3fac89c1e98e067e741861e5c82f3c4fb Mon Sep 17 00:00:00 2001 From: bobsonzu0a5d198e17c343cb Date: Mon, 27 Apr 2026 17:19:40 +0200 Subject: [PATCH 243/288] :globe_with_meridians: Add translations for: Russian Currently translated at 81.8% (1697 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/ --- frontend/translations/ru.po | 105 +++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index 736f6d489d..5aee6a9053 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-04-25 14:09+0000\n" -"Last-Translator: Egor Filatov \n" +"PO-Revision-Date: 2026-04-28 15:09+0000\n" +"Last-Translator: bobsonzu0a5d198e17c343cb \n" "Language-Team: Russian \n" "Language: ru\n" @@ -7084,3 +7084,104 @@ msgstr "Смешать" #: src/app/main/ui/dashboard/sidebar.cljs:347 msgid "dashboard.create-new-org" msgstr "Создать новую организацию" + +#: src/app/main/ui/dashboard/team.cljs:739 +msgid "labels.no-invitations-gather-people" +msgstr "Объединяйтесь и творите вместе." + +#: src/app/main/ui/comments.cljs:911, src/app/main/ui/comments.cljs:976, src/app/main/ui/workspace/palette.cljs:199, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:107, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:906, src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:155, src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:213, src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs:294, src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:402, src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs:1031, src/app/main/ui/workspace/sidebar/options/menus/text.cljs:316, src/app/main/ui/workspace/sidebar/options/menus/text.cljs:345, src/app/main/ui/workspace/sidebar/options/rows/shadow_row.cljs:146 +msgid "labels.options" +msgstr "Параметры" + +#: src/app/main/ui/dashboard/sidebar.cljs:899 +msgid "labels.penpot-hub" +msgstr "Хаб Penpot" + +#: src/app/main/ui/comments.cljs:680 +msgid "labels.post" +msgstr "Опубликовать" + +#: src/app/main/ui/dashboard/deleted.cljs:208 +msgid "labels.recent" +msgstr "Недавние" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:205, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:179 +msgid "labels.reference" +msgstr "Референс" + +#: src/app/main/data/common.cljs:83 +msgid "labels.refresh" +msgstr "Перезагрузить" + +#: src/app/main/ui/static.cljs:318 +msgid "labels.reload-page" +msgstr "Обновить страницу" + +#: src/app/main/ui/comments.cljs:642 +msgid "labels.replies" +msgstr "ответы" + +#, unused +msgid "labels.ok" +msgstr "Ok" + +#: src/app/main/ui/comments.cljs:647 +msgid "labels.replies.new" +msgstr "новые ответы" + +#: src/app/main/ui/comments.cljs:641 +msgid "labels.reply" +msgstr "ответить" + +#: src/app/main/ui/comments.cljs:646 +msgid "labels.reply.new" +msgstr "новый ответ" + +#: src/app/main/ui/comments.cljs:713 +msgid "labels.reply.thread" +msgstr "Ответить" + +#: src/app/main/ui/dashboard/team.cljs:788 +msgid "labels.resend" +msgstr "Отправить повторно" + +#: src/app/main/ui/workspace/sidebar/versions.cljs:87, src/app/main/ui/workspace/sidebar/versions.cljs:197 +msgid "labels.restore" +msgstr "Восстановить" + +#: src/app/main/ui/workspace/tokens/sidebar.cljs:75 +msgid "labels.sets" +msgstr "Наборы" + +#: src/app/main/ui/inspect/styles/style_box.cljs:27, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:229 +msgid "labels.shadow" +msgstr "Тень" + +#: src/app/main/ui/inspect/styles/style_box.cljs:24, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs:46 +msgid "labels.stroke" +msgstr "Обводка" + +#: src/app/main/ui/inspect/styles/style_box.cljs:33 +#, fuzzy +msgid "labels.svg" +msgstr "SVG" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:189 +msgid "labels.typography" +msgstr "Типографика" + +#: src/app/main/ui/dashboard/sidebar.cljs:967 +msgid "labels.version-notes" +msgstr "Примечания к версии %s" + +#: src/app/main/ui/ds/product/loader.cljs:20 +msgid "loader.tips.01.title" +msgstr "Переиспользуемые компоненты" + +#: src/app/main/ui/ds/product/loader.cljs:23 +msgid "loader.tips.02.message" +msgstr "Работайте с командой в реальном времени, делитесь отзывами мгновенно." + +#: src/app/main/ui/ds/product/loader.cljs:22 +msgid "loader.tips.02.title" +msgstr "Совместная работа в реальном времени" From b8f1b6e0c3dd35633e1b7a5e13dd2629115599b9 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 27 Apr 2026 08:43:28 +0200 Subject: [PATCH 244/288] :sparkles: Add nitrate api notify-user-orgs-deletion --- backend/src/app/nitrate.clj | 11 ++ backend/src/app/rpc/commands/nitrate.clj | 2 +- backend/src/app/rpc/management/nitrate.clj | 119 ++++++++++-- backend/src/app/rpc/notifications.clj | 13 +- .../rpc_management_nitrate_test.clj | 176 +++++++++++++++--- common/src/app/common/types/organization.cljc | 19 +- frontend/src/app/main/data/dashboard.cljs | 25 +++ frontend/src/app/main/ui/dashboard/team.cljs | 2 +- 8 files changed, 313 insertions(+), 54 deletions(-) diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 00ccdac7c6..b9f0949394 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -241,6 +241,16 @@ "/summary") schema:org-summary params))) +(defn- get-owned-orgs-api + [cfg {:keys [profile-id] :as params}] + (let [baseuri (cf/get :nitrate-backend-uri)] + (request-to-nitrate cfg :get + (str baseuri + "/api/users/" + profile-id + "/owned-organizations") + [:vector schema:org-summary] + params))) (defn- set-team-org-api [cfg {:keys [organization-id team-id is-default] :as params}] @@ -342,6 +352,7 @@ :get-org-membership (partial get-org-membership-api cfg) :get-org-membership-by-team (partial get-org-membership-by-team-api cfg) :get-org-summary (partial get-org-summary-api cfg) + :get-owned-orgs (partial get-owned-orgs-api cfg) :add-profile-to-org (partial add-profile-to-org-api cfg) :remove-profile-from-org (partial remove-profile-from-org-api cfg) :remove-profile-from-all-orgs (partial remove-profile-from-all-orgs-api cfg) diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index 26d690f166..2f88361693 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -74,7 +74,7 @@ AND t.id = ANY(?) AND t.deleted_at IS NULL") -(def ^:private sql:get-team-files-count +(def sql:get-team-files-count "SELECT count(*) AS total FROM file AS f JOIN project AS p ON (p.id = f.project_id) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 0e7479ac6c..79bfdf02df 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -11,6 +11,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.schema :as sm] + [app.common.time :as ct] [app.common.types.organization :refer [schema:team-with-organization]] [app.common.types.profile :refer [schema:profile, schema:basic-profile]] [app.common.types.team :refer [schema:team]] @@ -27,7 +28,8 @@ [app.rpc.doc :as doc] [app.rpc.notifications :as notifications] [app.storage :as sto] - [app.util.services :as sv])) + [app.util.services :as sv] + [app.worker :as wrk])) (defn- profile-to-map [profile] @@ -238,30 +240,111 @@ WHERE id = ANY(?) RETURNING id, name;") +(def ^:private sql:get-teams-files-counts + "SELECT p.team_id, COUNT(f.*) AS total + FROM file AS f + JOIN project AS p ON (p.id = f.project_id) + JOIN team AS t ON (t.id = p.team_id) + WHERE t.id = ANY(?) + AND t.deleted_at IS NULL + AND p.deleted_at IS NULL + AND f.deleted_at IS NULL + GROUP BY p.team_id;") -(def ^:private schema:notify-org-deletion +(def ^:private sql:soft-delete-teams + "UPDATE team + SET deleted_at = ? + WHERE id = ANY(?) +RETURNING id, deleted_at;") + + +;; ---- API: notify-organization-deletion + +(def ^:private schema:notify-organization-deletion [:map - [:organization-name ::sm/text] - [:teams [:vector ::sm/uuid]]]) + [:organization-id ::sm/uuid]]) -(sv/defmethod ::notify-org-deletion + +(defn- soft-delete-teams! + "Soft-delete the provided team ids and submit a delete task per team." + [{:keys [::db/conn] :as cfg} team-ids] + (when (seq team-ids) + (let [delay (cf/get-deletion-delay) + deleted-at (ct/in-future delay) + updated (db/exec! conn [sql:soft-delete-teams + deleted-at + (db/create-array conn "uuid" team-ids)])] + (doseq [{:keys [id deleted-at]} updated] + (wrk/submit! {::db/conn conn + ::wrk/task :delete-object + ::wrk/params {:object :team + :deleted-at deleted-at + :id id}})))) + nil) + +(defn manage-deleted-organization-teams + "For a list of teams, rename those with files and delete those without, then notify users." + [cfg {:keys [teams organization-name]}] + (let [teams (->> teams (filter uuid?) distinct (into []))] + (when (seq teams) + (let [org-prefix (str "[" (d/sanitize-string organization-name) "] ")] + (db/tx-run! + cfg + (fn [{:keys [::db/conn] :as cfg}] + (let [teams-array (db/create-array conn "uuid" teams) + teams-with-files (->> (db/exec! conn [sql:get-teams-files-counts teams-array]) + (filter (fn [{:keys [total]}] (pos? total))) + (map :team-id) + (into #{})) + teams-to-keep (->> teams (filter teams-with-files) (into [])) + teams-to-delete (->> teams (remove teams-with-files) (into []))] + + ;; Rename teams that have files in one go + (when (seq teams-to-keep) + (db/exec! conn [sql:prefix-teams-name-and-unset-default + org-prefix + (db/create-array conn "uuid" teams-to-keep)])) + + ;; Soft-delete empty teams in one go + (soft-delete-teams! cfg teams-to-delete) + + (notifications/notify-organization-deletion cfg organization-name teams teams-to-delete) + nil))))))) + + +(sv/defmethod ::notify-organization-deletion "For a list of teams, rename them with the name of the deleted org, and notify of the deletion to the connected users" {::doc/added "2.15" - ::sm/params schema:notify-org-deletion} - [cfg {:keys [teams organization-name]}] - (when (seq teams) - (let [org-prefix (str "[" (d/sanitize-string organization-name) "] ")] - (db/tx-run! - cfg - (fn [{:keys [::db/conn] :as cfg}] - (let [ids-array (db/create-array conn "uuid" teams) - ;; Rename projects - updated-teams (db/exec! conn [sql:prefix-teams-name-and-unset-default org-prefix ids-array])] + ::sm/params schema:notify-organization-deletion + ::rpc/auth false} + [cfg {:keys [organization-id]}] + (let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id}) + teams (->> (:teams org-summary) + (map :id))] + (manage-deleted-organization-teams cfg {:teams teams :organization-name (:name org-summary)}) + nil)) + +;; ---- API: notify-user-organizations-deletion + +(def ^:private schema:notify-user-organizations-deletion + [:map + [:profile-id ::sm/uuid]]) + +(sv/defmethod ::notify-user-organizations-deletion + "For a given user, find all owned organizations and rename or delete their teams." + {::doc/added "2.18" + ::sm/params schema:notify-user-organizations-deletion} + [cfg {:keys [profile-id]}] + (let [owned-orgs (nitrate/call cfg :get-owned-orgs {:profile-id profile-id})] + (doseq [org owned-orgs] + (let [organization-name (:name org) + teams (map :id (:teams org))] + (manage-deleted-organization-teams cfg {:teams teams :organization-name organization-name})))) + nil) + + - ;; Notify users - (doseq [team updated-teams] - (notifications/notify-team-change cfg {:id (:id team) :name (:name team) :organization {:name organization-name}} "dashboard.org-deleted")))))))) ;; ---- API: get-profile-by-email diff --git a/backend/src/app/rpc/notifications.clj b/backend/src/app/rpc/notifications.clj index 10d0d9f134..a439741092 100644 --- a/backend/src/app/rpc/notifications.clj +++ b/backend/src/app/rpc/notifications.clj @@ -30,4 +30,15 @@ :topic profile-id :organization-id organization-id :organization-name organization-name - :notification notification}))) \ No newline at end of file + :notification notification}))) + + +(defn notify-organization-deletion + [cfg organization-name teams deleted-teams] + (let [msgbus (::mbus/msgbus cfg)] + (mbus/pub! msgbus + :topic uuid/zero + :message {:type :organization-deleted + :organization-name organization-name + :teams teams + :deleted-teams deleted-teams}))) \ No newline at end of file diff --git a/backend/test/backend_tests/rpc_management_nitrate_test.clj b/backend/test/backend_tests/rpc_management_nitrate_test.clj index 382264bd66..101dfe19a1 100644 --- a/backend/test/backend_tests/rpc_management_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_management_nitrate_test.clj @@ -11,10 +11,10 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.db :as-alias db] - [app.email :as email] [app.msgbus :as mbus] [app.nitrate :as nitrate] [app.rpc :as-alias rpc] + [app.worker :as wrk] [backend-tests.helpers :as th] [clojure.set :as set] [clojure.test :as t] @@ -165,32 +165,158 @@ (t/is (= #{(:id team1) (:id team2)} (->> out :result :teams (map :id) set))))) -(t/deftest notify-org-deletion-prefixes-teams-and-notifies - (let [profile (th/create-profile* 1 {:is-active true}) - extra-team (th/create-team* 1 {:profile-id (:id profile)}) - default-team (th/db-get :team {:id (:default-team-id profile)}) - teams [(:id default-team) (:id extra-team)] - organization-name "Acme / Design" - expected-start (str "[" (d/sanitize-string organization-name) "] ") - calls (atom []) - out (with-redefs [mbus/pub! (fn [_cfg & {:keys [topic message]}] - (swap! calls conj {:topic topic - :message message}))] - (management-command-with-nitrate! {::th/type :notify-org-deletion - ::rpc/profile-id (:id profile) - :teams teams - :organization-name organization-name})) - updated (map #(th/db-get :team {:id %} {::db/remove-deleted false}) teams)] +(t/deftest notify-organization-deletion-prefixes-teams-and-publishes-org-deleted-event + (let [profile (th/create-profile* 1 {:is-active true}) + ;; One team will have files -> it will be kept and renamed. + team-with-files (th/db-get :team {:id (:default-team-id profile)}) + project (th/create-project* 1 {:profile-id (:id profile) + :team-id (:id team-with-files)}) + _ (th/create-file* 1 {:profile-id (:id profile) + :project-id (:id project)}) + + ;; One team will be empty -> it will be soft-deleted. + empty-team (th/create-team* 1 {:profile-id (:id profile)}) + + organization-id (uuid/random) + organization-name "Acme / Design" + expected-start (str "[" (d/sanitize-string organization-name) "] ") + org-summary {:id organization-id + :name organization-name + :teams [{:id (:id team-with-files)} + {:id (:id empty-team)}]} + calls (atom []) + submitted (atom []) + out (with-redefs [nitrate/call (fn [_cfg method params] + (t/is (= :get-org-summary method)) + (t/is (= {:organization-id organization-id} params)) + org-summary) + wrk/submit! (fn [task] + (swap! submitted conj task) + nil) + mbus/pub! (fn [_cfg & {:keys [topic message]}] + (swap! calls conj {:topic topic + :message message}))] + (management-command-with-nitrate! {::th/type :notify-organization-deletion + ::rpc/profile-id (:id profile) + :organization-id organization-id})) + updated-with-files (th/db-get :team {:id (:id team-with-files)} {::db/remove-deleted false}) + updated-empty (th/db-get :team {:id (:id empty-team)} {::db/remove-deleted false})] (t/is (th/success? out)) + (t/is (nil? (:result out))) + + ;; Team with files is kept, unset as default, and renamed with org prefix. + (t/is (false? (:is-default updated-with-files))) + (t/is (str/starts-with? (:name updated-with-files) expected-start)) + (t/is (nil? (:deleted-at updated-with-files))) + + ;; Empty team is soft-deleted and a delete task is submitted. + (t/is (some? (:deleted-at updated-empty))) + (t/is (= 1 (count @submitted))) + + ;; A single organization-deleted event is published. + (t/is (= 1 (count @calls))) + (let [{:keys [topic message]} (first @calls)] + (t/is (= uuid/zero topic)) + (t/is (= :organization-deleted (:type message))) + (t/is (= organization-name (:organization-name message))) + (t/is (= #{(:id team-with-files) (:id empty-team)} + (set (:teams message)))) + (t/is (= #{(:id empty-team)} + (set (:deleted-teams message))))))) + +(t/deftest notify-user-organizations-deletion-renames-or-deletes-teams-and-publishes-per-org-events + (let [profile (th/create-profile* 1 {:is-active true}) + ;; org-1: one team with files, one empty + org-1-team-files (th/db-get :team {:id (:default-team-id profile)}) + org-1-proj (th/create-project* 1 {:profile-id (:id profile) + :team-id (:id org-1-team-files)}) + _ (th/create-file* 1 {:profile-id (:id profile) + :project-id (:id org-1-proj)}) + org-1-team-empty (th/create-team* 1 {:profile-id (:id profile)}) + + ;; org-2: one team with files, one empty + org-2-team-files (th/create-team* 2 {:profile-id (:id profile)}) + org-2-proj (th/create-project* 2 {:profile-id (:id profile) + :team-id (:id org-2-team-files)}) + _ (th/create-file* 2 {:profile-id (:id profile) + :project-id (:id org-2-proj)}) + org-2-team-empty (th/create-team* 3 {:profile-id (:id profile)}) + + org-1-id (uuid/random) + org-2-id (uuid/random) + org-1-name "Org One / Design" + org-2-name "Org Two" + org-1-prefix (str "[" (d/sanitize-string org-1-name) "] ") + org-2-prefix (str "[" (d/sanitize-string org-2-name) "] ") + owned-orgs [{:id org-1-id + :name org-1-name + :teams [{:id (:id org-1-team-files)} + {:id (:id org-1-team-empty)}]} + {:id org-2-id + :name org-2-name + :teams [{:id (:id org-2-team-files)} + {:id (:id org-2-team-empty)}]}] + calls (atom []) + submitted (atom []) + out (with-redefs [nitrate/call (fn [_cfg method params] + (case method + :get-owned-orgs + (do + (t/is (= {:profile-id (:id profile)} params)) + owned-orgs) + nil)) + wrk/submit! (fn [task] + (swap! submitted conj task) + nil) + mbus/pub! (fn [_cfg & {:keys [topic message]}] + (swap! calls conj {:topic topic + :message message}))] + (management-command-with-nitrate! {::th/type :notify-user-organizations-deletion + ::rpc/profile-id (:id profile) + :profile-id (:id profile)})) + org-1-updated-files (th/db-get :team {:id (:id org-1-team-files)} {::db/remove-deleted false}) + org-1-updated-empty (th/db-get :team {:id (:id org-1-team-empty)} {::db/remove-deleted false}) + org-2-updated-files (th/db-get :team {:id (:id org-2-team-files)} {::db/remove-deleted false}) + org-2-updated-empty (th/db-get :team {:id (:id org-2-team-empty)} {::db/remove-deleted false}) + msgs (->> @calls (map :message) vec) + org-msg (fn [org-name] + (first (filter #(= org-name (:organization-name %)) msgs)))] + (t/is (th/success? out)) + (t/is (nil? (:result out))) + + ;; org-1: team with files renamed; empty team deleted + (t/is (false? (:is-default org-1-updated-files))) + (t/is (str/starts-with? (:name org-1-updated-files) org-1-prefix)) + (t/is (nil? (:deleted-at org-1-updated-files))) + (t/is (some? (:deleted-at org-1-updated-empty))) + + ;; org-2: team with files renamed; empty team deleted + (t/is (false? (:is-default org-2-updated-files))) + (t/is (str/starts-with? (:name org-2-updated-files) org-2-prefix)) + (t/is (nil? (:deleted-at org-2-updated-files))) + (t/is (some? (:deleted-at org-2-updated-empty))) + + ;; two delete tasks (one per empty team) + (t/is (= 2 (count @submitted))) + + ;; one organization-deleted event per org (t/is (= 2 (count @calls))) - (doseq [team updated] - (t/is (false? (:is-default team))) - (t/is (str/starts-with? (:name team) expected-start))) - (doseq [call @calls] - (t/is (= uuid/zero (:topic call))) - (t/is (= :team-org-change (-> call :message :type))) - (t/is (= organization-name (-> call :message :team :organization :name))) - (t/is (= "dashboard.org-deleted" (-> call :message :notification)))))) + (t/is (every? #(= uuid/zero (:topic %)) @calls)) + (t/is (= #{:organization-deleted} + (set (map (comp :type :message) @calls)))) + + (let [m1 (org-msg org-1-name) + m2 (org-msg org-2-name)] + (t/is (some? m1)) + (t/is (some? m2)) + (t/is (= #{(:id org-1-team-files) (:id org-1-team-empty)} + (set (:teams m1)))) + (t/is (= #{(:id org-1-team-empty)} + (set (:deleted-teams m1)))) + (t/is (= #{(:id org-2-team-files) (:id org-2-team-empty)} + (set (:teams m2)))) + (t/is (= #{(:id org-2-team-empty)} + (set (:deleted-teams m2))))))) (t/deftest get-profile-by-email-success-and-not-found (let [profile (th/create-profile* 1 {:is-active true diff --git a/common/src/app/common/types/organization.cljc b/common/src/app/common/types/organization.cljc index b504954cfb..f19833b585 100644 --- a/common/src/app/common/types/organization.cljc +++ b/common/src/app/common/types/organization.cljc @@ -8,18 +8,21 @@ (:require [app.common.schema :as sm])) +(def schema:organization + [:map + [:id ::sm/uuid] + [:name ::sm/text] + [:slug ::sm/text] + [:owner-id ::sm/uuid] + [:avatar-bg-url ::sm/uri] + [:logo-id {:optional true} [:maybe ::sm/uuid]]]) + + (def schema:team-with-organization [:map [:id ::sm/uuid] [:is-your-penpot :boolean] - [:organization - [:map - [:id ::sm/uuid] - [:name ::sm/text] - [:slug ::sm/text] - [:owner-id ::sm/uuid] - [:avatar-bg-url ::sm/uri] - [:logo-id {:optional true} [:maybe ::sm/uuid]]]]]) + [:organization schema:organization]]) (def organization->team-keys "Mapping from organization field keys to their corresponding :organization-* team keys." diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index c55d757d80..7810d15a95 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -729,6 +729,30 @@ (when (= organization-id (:organization-id team)) (dcm/go-to-dashboard-recent {:team-id :default})))))))) + +(defn- handle-organization-deleted + [{:keys [organization-name teams deleted-teams]}] + (ptk/reify ::handle-organization-deleted + ptk/WatchEvent + (watch [_ state _] + (when (contains? cf/flags :nitrate) + (let [team-id (:current-team-id state) + teams-set (set teams) + notify? (contains? teams-set team-id) + fetch? (some (:teams state) teams) + go-to-default? (some #{team-id} deleted-teams)] + (rx/concat + (when go-to-default? ;; If the user is currently on one of the deleted teams + (rx/of (dcm/go-to-dashboard-recent {:team-id :default}))) + + (when notify? ;; If the user is currently on one of the org teams + (rx/of (ntf/show {:content (tr "dashboard.org-deleted" organization-name) + :type :toast + :level :info + :timeout nil}))) + (when fetch? ;; If the user belonged to the org + (rx/of (dtm/fetch-teams))))))))) + (defn- process-message [{:keys [type] :as msg}] (case type @@ -737,6 +761,7 @@ :team-membership-change (dcm/team-membership-change msg) :team-org-change (handle-change-team-org msg) :user-org-change (handle-user-org-change msg) + :organization-deleted (handle-organization-deleted msg) nil)) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 871d46a11a..ef86cef4f3 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -1065,7 +1065,7 @@ (tr "dashboard.your-penpot") (:name team))))) - (mf/with-effect [team] + (mf/with-effect [(:id team) (:members team)] (st/emit! (dtm/fetch-invitations))) [:* From ea971a01094ae639281878a388ca20aef7dc06c8 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 29 Apr 2026 07:38:08 +0000 Subject: [PATCH 245/288] :bug: Fix redundant longhand margin-block properties in options.scss Combine margin-block-start and margin-block-end into the margin-block shorthand to satisfy the declaration-block-no-redundant-longhand-properties stylelint rule. Signed-off-by: Andrey Antukh --- frontend/src/app/main/ui/settings/options.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/app/main/ui/settings/options.scss b/frontend/src/app/main/ui/settings/options.scss index 2df1d9235f..abe949897f 100644 --- a/frontend/src/app/main/ui/settings/options.scss +++ b/frontend/src/app/main/ui/settings/options.scss @@ -25,9 +25,8 @@ grid-auto-rows: auto; gap: var(--sp-s); width: $sz-500; - margin-block-start: var(--sp-xxxl); + margin-block: var(--sp-xxxl) $sz-120; /* FIXME: this should be a proper token */ padding-block-start: var(--sp-xxxl); - margin-block-end: $sz-120; /* FIXME: this should be a proper token */ border-block-start: $b-1 solid var(--color-background-quaternary); color: var(--color-foreground-primary); } From 9751ac2b4120a64ffe2812cd8ba1b11fb19e8fd8 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 29 Apr 2026 09:43:13 +0200 Subject: [PATCH 246/288] :paperclip: Update changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 3fccd4f985..afb02a605b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -47,9 +47,9 @@ - Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027) - Add page separators in Workspace [Taiga #13611](https://tree.taiga.io/project/penpot/us/13611?milestone=262806) - Preserve vector content when pasting from external tools such as Inkscape: recognise SVG sent as text/plain (with optional XML declaration and HTML comments), skip the raster preview when an SVG sibling is on the clipboard, and ignore empty SVG blobs that some tools advertise alongside the real payload, so pasted graphics arrive editable without spurious "SVG is invalid" warnings [Github #546](https://github.com/penpot/penpot/issues/546) - - Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts [Github #2457](https://github.com/penpot/penpot/issues/2457) - Adds a **Pixel grid color** picker in the viewport settings, next to the existing canvas color control [Github #7750](https://github.com/penpot/penpot/issues/7750) + ### :bug: Bugs fixed - Fix plugin API `fileVersion.restore()` promise hanging indefinitely on restore failure [Github #9092](https://github.com/penpot/penpot/issues/9092) From 1eac3e2be5f973359ad2ec9bac4e80a9d5a9e022 Mon Sep 17 00:00:00 2001 From: boskodev790 Date: Wed, 29 Apr 2026 04:02:01 -0500 Subject: [PATCH 247/288] :bug: Fix Plugin API token application for JS array of strings (#9166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :bug: Fix Plugin API token application for JS array of strings Plugin code calling `shape.applyToken(token, ["fill"])` or `token.applyToShapes([rect], ["fill"])` from JavaScript supplies a JS array of strings. The plugin proxies expected a Clojure set of keywords, and two coupled defects made the calls silently no-op (or, with `throwValidationErrors` enabled, throw "check error"): 1. `token-attr-plugin->token-attr` only consulted its alias map when the input was already a keyword. String inputs like "fill" fell through to the identity branch, so the downstream `cto/token-attr?` predicate (which checks against a set of keywords) returned false for every string. Coerce strings to keywords first. 2. The `applyToken` / `applyToShapes` / `applyToSelected` schemas used plain `[:set ...]`, which has no `:decode/json` transformer for JS array → Clojure set coercion. Switch to the registered `[::sm/set ...]` (in `app.common.schema`) which provides the array → set decoder. After the switch, the standard JSON pipeline converts `["fill"]` to `#{"fill"}`, then the inner `[:and ::sm/keyword [:fn token-attr?]]` decodes each element to a keyword and validates it. Also extends the docstring on `token-attr-plugin->token-attr` to make the string-friendly contract explicit, and registers a new `tokens-test` ns under `frontend/test/frontend_tests/plugins/` with six `deftest` blocks covering: - known keywords passing through unchanged - keyword aliases (`:r1` → `:border-radius-top-left`, etc.) - string inputs coerced to keywords (regression for #9162) - `token-attr?` accepting both keyword and string inputs - `token-attr?` rejecting unknown attrs and nil Closes #9162 * :bug: Fix wrong direction in plugin-name alias tests The added tests in tokens_test.cljs and the new docstring in tokens.cljs described the alias resolution in the wrong direction. The map is {:r1 :border-radius-top-left, …} then map-invert'd, so token-attr-plugin->token-attr maps verbose plugin-side names (:border-radius-top-left) to canonical internal short names (:r1), not the other way around. Inputs already in canonical form (:r1, :fill, "fill", …) pass through unchanged. Flipped the alias-resolution test expectations and the keyword/string-input cases, refreshed the docstring and the regression-coverage comment to match. --------- Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + frontend/src/app/plugins/shape.cljs | 2 +- frontend/src/app/plugins/tokens.cljs | 17 +++- .../frontend_tests/plugins/tokens_test.cljs | 82 +++++++++++++++++++ frontend/test/frontend_tests/runner.cljs | 2 + 5 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 frontend/test/frontend_tests/plugins/tokens_test.cljs diff --git a/CHANGES.md b/CHANGES.md index afb02a605b..18e71ee6c4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -56,6 +56,7 @@ - Fix plugin API `library.connectLibrary()` returning a non-Promise (or throwing synchronously) when the plugin lacks `library:write` permission — the method now always returns a `Promise` and rejects with a structured error message, matching the contract used by every other Promise-returning plugin method (`restore`, `remove`, `pin`, `saveVersion`, `findVersions`, …) - Fix LDAP provider params schema typo (`bind-passwor` → `bind-password`) introduced during the `clojure.spec` → `malli` migration; the schema slot now matches the runtime key actually read by `prepare-params` (`:password (:bind-password cfg)`) and `try-connectivity` (`(:bind-password cfg)`), so a wrong type for the password no longer slips through unvalidated - Fix `login-with-ldap` silently dropping its error message on the `ldap-not-initialized` restriction (typo `:hide` → `:hint`); the message `"ldap auth provider is not initialized"` now actually surfaces in logs and error responses instead of being discarded into an unread key +- Fix Plugin API `shape.applyToken()` / `token.applyToShapes()` / `token.applyToSelected()` rejecting JS-array attribute lists like `["fill"]`: switched the inner schemas to `[::sm/set ...]` (which has the JS array → Clojure set decoder) and made `token-attr-plugin->token-attr` accept string inputs by coercing them to keywords before consulting the alias map [Github #9162](https://github.com/penpot/penpot/issues/9162) - Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored (`userinfo` / `token`) in the OIDC callback, causing "incomplete user info" failures during registration [Github #9108](https://github.com/penpot/penpot/issues/9108) - Fix `get-view-only-bundle` crashing when a share-link viewer encounters a team member whose email lacks `@` (NullPointerException in `obfuscate-email`) or whose domain has no `.` (previously produced a dangling-dot `****@****.`); now the viewer-side obfuscation is nil-safe and omits the trailing dot when the domain has no TLD - Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled [Github #8877](https://github.com/penpot/penpot/issues/8877) diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index 6e37bc2e03..da09c90ea8 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -1338,7 +1338,7 @@ {:enumerable false :schema [:tuple [:fn token-proxy?] - [:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]] + [:maybe [::sm/set [:and ::sm/keyword [:fn token-attr?]]]]] :fn (fn [token attrs] (let [token (u/locate-token file-id (obj/get token "$set-id") (obj/get token "$id")) kw-attrs (into #{} (map token-attr-plugin->token-attr attrs))] diff --git a/frontend/src/app/plugins/tokens.cljs b/frontend/src/app/plugins/tokens.cljs index ad338ca32b..776a905308 100644 --- a/frontend/src/app/plugins/tokens.cljs +++ b/frontend/src/app/plugins/tokens.cljs @@ -49,8 +49,19 @@ (get map:token-attr->token-attr-plugin k k)) (defn token-attr-plugin->token-attr + "Resolve a plugin-side token attribute reference to its canonical + internal keyword. + + Accepts either a Clojure keyword (the canonical form, e.g. `:r1`, + `:fill`) or a string (the natural shape that arrives from a JS plugin + call such as `shape.applyToken(token, [\"fill\"])`). Converts strings + to keywords first, then maps verbose plugin-side aliases (e.g. + `:border-radius-top-left`) to their internal short form (e.g. `:r1`). + Inputs that are already in canonical form (`:r1`, `:fill`, `\"fill\"`, + …) pass through unchanged." [k] - (get map:token-attr-plugin->token-attr k k)) + (let [k (cond-> k (string? k) keyword)] + (get map:token-attr-plugin->token-attr k k))) (defn applied-tokens-plugin->applied-tokens [value] @@ -186,13 +197,13 @@ {:enumerable false :schema [:tuple [:vector [:fn shape-proxy?]] - [:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]] + [:maybe [::sm/set [:and ::sm/keyword [:fn token-attr?]]]]] :fn (fn [shapes attrs] (apply-token-to-shapes plugin-id file-id set-id id (map #(obj/get % "$id") shapes) attrs))} :applyToSelected {:enumerable false - :schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]] + :schema [:tuple [:maybe [::sm/set [:and ::sm/keyword [:fn token-attr?]]]]] :fn (fn [attrs] (let [selected (get-in @st/state [:workspace-local :selected])] (apply-token-to-shapes plugin-id file-id set-id id selected attrs)))})) diff --git a/frontend/test/frontend_tests/plugins/tokens_test.cljs b/frontend/test/frontend_tests/plugins/tokens_test.cljs new file mode 100644 index 0000000000..3c0d1cda1a --- /dev/null +++ b/frontend/test/frontend_tests/plugins/tokens_test.cljs @@ -0,0 +1,82 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns frontend-tests.plugins.tokens-test + (:require + [app.plugins.tokens :as ptok] + [cljs.test :as t :include-macros true])) + +;; Regression coverage for issue #9162. +;; +;; Plugin code calling `shape.applyToken(token, ["fill"])` or +;; `token.applyToShapes([rect], ["fill"])` from JavaScript supplies a JS +;; array of strings. Penpot's plugin proxies expect a Clojure set of +;; keywords. Two coupled defects made these calls silently no-op (or, with +;; `throwValidationErrors` enabled, throw a "check error"): +;; +;; 1. `token-attr-plugin->token-attr` only consulted its alias map when +;; the input was already a keyword — string inputs like "fill" or +;; "border-radius-top-left" fell through to the identity branch +;; unchanged, so the downstream `cto/token-attr?` predicate (which +;; checks against a set of keywords) returned false. +;; 2. The `applyToken` / `applyToShapes` / `applyToSelected` schemas used +;; plain `[:set ...]`, which does not have a `:decode/json` +;; transformer for the JS array → Clojure set coercion. Penpot's +;; custom `[::sm/set ...]` does. Switching to the registered set type +;; lets the standard JSON decoder pipeline turn the JS argument into +;; a set of strings, after which the `[:and ::sm/keyword [:fn +;; token-attr?]]` element schema coerces each string to a keyword and +;; validates it. +;; +;; These helper-level tests pin the string-friendly conversion contract; +;; the schema-level fix is covered by the existing plugin integration +;; suite that exercises `applyToken` end-to-end. + +(t/deftest token-attr-plugin->token-attr-passes-canonical-form-through + ;; Both already-canonical short names and unaliased names pass through + ;; unchanged. + (t/is (= :fill (ptok/token-attr-plugin->token-attr :fill))) + (t/is (= :stroke-color (ptok/token-attr-plugin->token-attr :stroke-color))) + (t/is (= :r1 (ptok/token-attr-plugin->token-attr :r1))) + (t/is (= :p2 (ptok/token-attr-plugin->token-attr :p2)))) + +(t/deftest token-attr-plugin->token-attr-resolves-verbose-plugin-aliases + ;; Plugin-side verbose names (e.g. `:border-radius-top-left`) map to + ;; their canonical short internal form (`:r1`) so plugin authors can + ;; spell the corner explicitly without the engine having to know both. + (t/is (= :r1 (ptok/token-attr-plugin->token-attr :border-radius-top-left))) + (t/is (= :r2 (ptok/token-attr-plugin->token-attr :border-radius-top-right))) + (t/is (= :r3 (ptok/token-attr-plugin->token-attr :border-radius-bottom-right))) + (t/is (= :r4 (ptok/token-attr-plugin->token-attr :border-radius-bottom-left))) + (t/is (= :p1 (ptok/token-attr-plugin->token-attr :padding-top-left))) + (t/is (= :m3 (ptok/token-attr-plugin->token-attr :margin-bottom-right)))) + +(t/deftest token-attr-plugin->token-attr-coerces-string-input + ;; This is the actual regression — JS plugin calls supply strings. + (t/is (= :fill (ptok/token-attr-plugin->token-attr "fill"))) + (t/is (= :stroke-color (ptok/token-attr-plugin->token-attr "stroke-color"))) + ;; Verbose plugin aliases work via the string path too. + (t/is (= :r1 (ptok/token-attr-plugin->token-attr "border-radius-top-left"))) + (t/is (= :m3 (ptok/token-attr-plugin->token-attr "margin-bottom-right")))) + +(t/deftest token-attr?-accepts-keyword-input + (t/is (true? (boolean (ptok/token-attr? :fill)))) + (t/is (true? (boolean (ptok/token-attr? :stroke-color)))) + (t/is (true? (boolean (ptok/token-attr? :r1)))) + (t/is (true? (boolean (ptok/token-attr? :p2))))) + +(t/deftest token-attr?-accepts-string-input + ;; Same JS-array-of-strings reproducer as the issue, exercised at the + ;; predicate layer the plugin schemas call into. + (t/is (true? (boolean (ptok/token-attr? "fill")))) + (t/is (true? (boolean (ptok/token-attr? "stroke-color")))) + (t/is (true? (boolean (ptok/token-attr? "r1")))) + (t/is (true? (boolean (ptok/token-attr? "m3"))))) + +(t/deftest token-attr?-rejects-unknown-input + (t/is (false? (boolean (ptok/token-attr? :not-a-real-attr)))) + (t/is (false? (boolean (ptok/token-attr? "not-a-real-attr")))) + (t/is (false? (boolean (ptok/token-attr? nil))))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index f54d9b5002..b4e6f0defc 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -21,6 +21,7 @@ [frontend-tests.main-errors-test] [frontend-tests.plugins.context-shapes-test] [frontend-tests.plugins.parser-test] + [frontend-tests.plugins.tokens-test] [frontend-tests.svg-fills-test] [frontend-tests.tokens.import-export-test] [frontend-tests.tokens.logic.token-actions-test] @@ -65,6 +66,7 @@ 'frontend-tests.logic.pasting-in-containers-test 'frontend-tests.plugins.context-shapes-test 'frontend-tests.plugins.parser-test + 'frontend-tests.plugins.tokens-test 'frontend-tests.svg-fills-test 'frontend-tests.tokens.import-export-test 'frontend-tests.tokens.logic.token-actions-test From 3f40be6b4db98b9ff2d6ccdd7d3e08093ae1cc98 Mon Sep 17 00:00:00 2001 From: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com> Date: Wed, 29 Apr 2026 05:07:30 -0400 Subject: [PATCH 248/288] :lipstick: Fix sucess typo in subscription dialog i18n keys (#9204) Rename subscription.settings.sucess.dialog.{title,footer} to subscription.settings.success.dialog.{title,footer} in en.po and update the three callsites in subscription.cljs. Closes #9203 Signed-off-by: jack-stormentswe --- frontend/src/app/main/ui/settings/subscription.cljs | 6 +++--- frontend/translations/de.po | 4 ++-- frontend/translations/en.po | 4 ++-- frontend/translations/es.po | 4 ++-- frontend/translations/fr.po | 4 ++-- frontend/translations/fr_CA.po | 4 ++-- frontend/translations/he.po | 4 ++-- frontend/translations/hi.po | 4 ++-- frontend/translations/it.po | 4 ++-- frontend/translations/lv.po | 4 ++-- frontend/translations/nl.po | 4 ++-- frontend/translations/ro.po | 4 ++-- frontend/translations/sv.po | 4 ++-- frontend/translations/tr.po | 4 ++-- frontend/translations/ukr_UA.po | 4 ++-- frontend/translations/zh_CN.po | 4 ++-- 16 files changed, 33 insertions(+), 33 deletions(-) diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index 285f3e4c66..42d892f7d8 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -343,14 +343,14 @@ [:div {:class (stl/css :modal-end)} [:div {:class (stl/css :modal-title)} - (tr "subscription.settings.sucess.dialog.title" subscription-name)] + (tr "subscription.settings.success.dialog.title" subscription-name)] (when (not= subscription-name "professional") [:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.success.dialog.thanks" subscription-name)]) [:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.success.dialog.description")] [:p {:class (stl/css :modal-text-large)} - (tr "subscription.settings.sucess.dialog.footer")] + (tr "subscription.settings.success.dialog.footer")] [:div {:class (stl/css :success-action-buttons)} [:input @@ -381,7 +381,7 @@ [:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.success.dialog.description")] [:p {:class (stl/css :modal-text-large)} - (tr "subscription.settings.sucess.dialog.footer")] + (tr "subscription.settings.success.dialog.footer")] [:div {:class (stl/css :success-action-buttons)} [:input diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 9d08659eef..8f14740a22 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -4822,11 +4822,11 @@ msgid "subscription.settings.subscribe" msgstr "Abonnieren" #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "Viel Spaß mit Ihrem Abonnement!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "Sie sind %s!" #: src/app/main/ui/settings/subscription.cljs:558, src/app/main/ui/settings/subscription.cljs:574 diff --git a/frontend/translations/en.po b/frontend/translations/en.po index f2c15954b7..e4259e76fa 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5319,11 +5319,11 @@ msgid "subscription.settings.success.dialog.thanks" msgstr "Thank your for chosing the Penpot %s plan!" #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "Enjoy your plan!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "You are %s!" #: src/app/main/ui/settings/subscription.cljs:526 diff --git a/frontend/translations/es.po b/frontend/translations/es.po index eadfd7b51d..ecb1a4ecec 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -5254,11 +5254,11 @@ msgid "subscription.settings.success.dialog.thanks" msgstr "¡Gracias por elegir el plan %s de Penpot!" #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "¡Disfruta de tu plan!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "Eres %s!" #: src/app/main/ui/settings/subscription.cljs:526 diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index d061e50e7e..f4a62e3848 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -5010,11 +5010,11 @@ msgid "subscription.settings.success.dialog.thanks" msgstr "Merci d'avoir choisi l'abonnement Penpot %s !" #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "Profitez bien de votre abonnement !" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "Vous êtes %s !" #: src/app/main/ui/settings/subscription.cljs:526 diff --git a/frontend/translations/fr_CA.po b/frontend/translations/fr_CA.po index c71fb8dff0..37bc8e442a 100644 --- a/frontend/translations/fr_CA.po +++ b/frontend/translations/fr_CA.po @@ -5025,11 +5025,11 @@ msgid "subscription.settings.success.dialog.thanks" msgstr "Merci d'avoir choisi le forfait Penpot %s!" #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "Profite bien de ton forfait!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "Tu es %s!" #: src/app/main/ui/settings/subscription.cljs:526 diff --git a/frontend/translations/he.po b/frontend/translations/he.po index b5e5953db5..ba2a305c09 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -4773,11 +4773,11 @@ msgid "subscription.settings.success.dialog.thanks" msgstr "תודה על הבחירה בתוכנית %s של Penpot!" #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "מאחלים לך הנאה מהתוכנית שלך!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "התוכנית שלך היא %s!" #: src/app/main/ui/settings/subscription.cljs:526 diff --git a/frontend/translations/hi.po b/frontend/translations/hi.po index 25d94de048..ff6bbda839 100644 --- a/frontend/translations/hi.po +++ b/frontend/translations/hi.po @@ -4904,11 +4904,11 @@ msgid "subscription.settings.success.dialog.thanks" msgstr "पेनपोट %s योजना चुनने के लिए धन्यवाद!" #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "अपनी योजना का आनंद लें!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "आप %s हैं!" #: src/app/main/ui/settings/subscription.cljs:526 diff --git a/frontend/translations/it.po b/frontend/translations/it.po index 550a39073a..5dee5271c0 100644 --- a/frontend/translations/it.po +++ b/frontend/translations/it.po @@ -5012,11 +5012,11 @@ msgid "subscription.settings.success.dialog.thanks" msgstr "Grazie per aver scelto il piano Penpot %s!" #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "Goditi il tuo piano!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "Sei %s!" #: src/app/main/ui/settings/subscription.cljs:526 diff --git a/frontend/translations/lv.po b/frontend/translations/lv.po index 4268d7a133..21ada25f07 100644 --- a/frontend/translations/lv.po +++ b/frontend/translations/lv.po @@ -4622,11 +4622,11 @@ msgstr "" "\"Abonements\"." #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "Izbaudi savu plānu!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "Tu esi %s." #: src/app/main/ui/settings/subscription.cljs:526 diff --git a/frontend/translations/nl.po b/frontend/translations/nl.po index 65fe20fe03..421a89d990 100644 --- a/frontend/translations/nl.po +++ b/frontend/translations/nl.po @@ -5036,11 +5036,11 @@ msgid "subscription.settings.success.dialog.thanks" msgstr "Bedankt voor het kiezen van het Penpot %s-abonnement!" #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "Veel plezier met je abonnement!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "Je bent %s!" #: src/app/main/ui/settings/subscription.cljs:526 diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index d0807b28a9..0d5516e26d 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -4747,11 +4747,11 @@ msgid "subscription.settings.success.dialog.thanks" msgstr "Mulțumim pentru că ai ales planul Penpot %s!" #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "Bucură-te de abonament!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "Ești %s!" #: src/app/main/ui/settings/subscription.cljs:526 diff --git a/frontend/translations/sv.po b/frontend/translations/sv.po index 891b1b5e17..4e007bcdc3 100644 --- a/frontend/translations/sv.po +++ b/frontend/translations/sv.po @@ -4827,11 +4827,11 @@ msgid "subscription.settings.success.dialog.thanks" msgstr "Tack för att du valt Penpot %s-abonnemanget!" #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "Lycka till med ditt abonnemang!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "Du är %!" #: src/app/main/ui/settings/subscription.cljs:526 diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index e9da3bbc47..e8e2948403 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -5002,11 +5002,11 @@ msgid "subscription.settings.success.dialog.thanks" msgstr "Penpot %s planını seçtiğiniz için teşekkür ederiz!" #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "Planınızın tadını çıkarın!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "%s oldunuz!" #: src/app/main/ui/settings/subscription.cljs:526 diff --git a/frontend/translations/ukr_UA.po b/frontend/translations/ukr_UA.po index e587f083dc..05796dbff1 100644 --- a/frontend/translations/ukr_UA.po +++ b/frontend/translations/ukr_UA.po @@ -4462,11 +4462,11 @@ msgstr "" "що у подробицях облікового запису." #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "Насолоджуйтесь планом!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "Ви %s!" #: src/app/main/ui/settings/subscription.cljs:526 diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po index 2eb79194f5..8da069884e 100644 --- a/frontend/translations/zh_CN.po +++ b/frontend/translations/zh_CN.po @@ -4318,11 +4318,11 @@ msgid "subscription.settings.subscribe" msgstr "订阅" #: src/app/main/ui/settings/subscription.cljs:347 -msgid "subscription.settings.sucess.dialog.footer" +msgid "subscription.settings.success.dialog.footer" msgstr "享受您的计划!" #: src/app/main/ui/settings/subscription.cljs:340 -msgid "subscription.settings.sucess.dialog.title" +msgid "subscription.settings.success.dialog.title" msgstr "你是 %s!" #: src/app/main/ui/settings/subscription.cljs:526 From e22a03e7e859e53c2f203f752bf754c35b839844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Wed, 29 Apr 2026 12:42:25 +0200 Subject: [PATCH 249/288] :sparkles: Subscribe to nitrate with an activation code * :sparkles: Subscribe to nitrate with an activation code * :paperclip: Code review --- backend/src/app/nitrate.clj | 27 ++++- backend/src/app/rpc/commands/nitrate.clj | 36 ++++++ .../ui/ds/foundations/assets/raw_svg.cljs | 1 + .../nitrate_activation_success_modal.cljs | 67 +++++++++++ .../nitrate_activation_success_modal.scss | 79 +++++++++++++ .../nitrate_code_activation_modal.cljs | 98 ++++++++++++++++ .../nitrate_code_activation_modal.scss | 107 ++++++++++++++++++ .../src/app/main/ui/nitrate/nitrate_form.cljs | 41 +++++-- .../src/app/main/ui/nitrate/nitrate_form.scss | 30 ++--- .../app/main/ui/settings/subscription.cljs | 58 +++------- frontend/translations/en.po | 97 ++++++++++++++++ frontend/translations/es.po | 100 ++++++++++++++++ 12 files changed, 671 insertions(+), 70 deletions(-) create mode 100644 frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs create mode 100644 frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.scss create mode 100644 frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs create mode 100644 frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.scss diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index b9f0949394..28f2cf1eb7 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -55,7 +55,7 @@ result))))) -(defn- with-validate [handler uri schema] +(defn- with-validate [handler uri schema & {:keys [throw-on-error?]}] (fn [] (let [response (handler) status (:status response)] @@ -73,7 +73,11 @@ :uri uri :status status :body (:body response))) - nil) + (if throw-on-error? + (ex/raise :type :nitrate-http-error + :status status + :hint (str "nitrate HTTP " status " at " uri)) + nil)) (= status 204) ;; 204 doesn't return any body nil :else ;; For success status codes, validate the response @@ -89,11 +93,11 @@ nil))))))) (defn- request-to-nitrate - [cfg method uri schema {:keys [::rpc/profile-id request-params] :as params}] + [cfg method uri schema {:keys [::rpc/profile-id request-params throw-on-error?] :as params}] (let [shared-key (-> cfg ::setup/shared-keys :nitrate) full-http-call (-> (request-builder cfg method uri shared-key profile-id request-params) (with-retries 3) - (with-validate uri schema))] + (with-validate uri schema :throw-on-error? throw-on-error?))] (full-http-call))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -340,6 +344,18 @@ "/api/connectivity") schema:connectivity params))) +(def ^:private schema:redeem-result + [:map + [:cancel-at [:maybe schema:timestamp]]]) + +(defn- redeem-activation-code-api + [cfg params] + (let [baseuri (cf/get :nitrate-backend-uri)] + (request-to-nitrate cfg :post + (str baseuri "/api/activation-codes/redeem") + schema:redeem-result + (assoc params :throw-on-error? true)))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; INITIALIZATION ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -359,7 +375,8 @@ :delete-team (partial delete-team-api cfg) :remove-team-from-org (partial remove-team-from-org-api cfg) :get-subscription (partial get-subscription-api cfg) - :connectivity (partial get-connectivity-api cfg)})) + :connectivity (partial get-connectivity-api cfg) + :redeem-activation-code (partial redeem-activation-code-api cfg)})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; UTILS diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index 2f88361693..91f58f6448 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -11,6 +11,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.schema :as sm] + [app.common.time :as ct] [app.db :as db] [app.nitrate :as nitrate] [app.rpc :as-alias rpc] @@ -56,6 +57,41 @@ [cfg _params] (nitrate/call cfg :connectivity {})) +(def ^:private schema:redeem-activation-code-params + [:map {:title "RedeemActivationCodeParams"} + [:activation-code ::sm/text]]) + +(def ^:private schema:redeem-activation-code-result + [:map {:title "RedeemActivationCodeResult"} + [:cancel-at [:maybe ct/schema:inst]]]) + +(sv/defmethod ::redeem-nitrate-activation-code + {::rpc/auth true + ::doc/added "2.14" + ::sm/params schema:redeem-activation-code-params + ::sm/result schema:redeem-activation-code-result} + [cfg {:keys [::rpc/profile-id activation-code]}] + (let [profile (db/get cfg :profile {:id profile-id})] + (try + (let [result (nitrate/call cfg :redeem-activation-code + {:request-params {:code activation-code + :penpot-id profile-id + :email (:email profile)}})] + (when-not result + (ex/raise :type :validation + :code :invalid-activation-code + :hint "The activation code is invalid, expired or fully redeemed")) + result) + (catch Exception cause + (let [{:keys [type status]} (ex-data cause)] + (if (= type :nitrate-http-error) + (ex/raise :type :validation + :code (case status + 410 :expired-activation-code + :invalid-activation-code) + :cause cause) + (throw cause))))))) + (def ^:private sql:prefix-team-name-and-unset-default "UPDATE team SET name = ? || name, diff --git a/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs b/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs index e744cfe292..56f5b076a5 100644 --- a/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs +++ b/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs @@ -39,3 +39,4 @@ (assert (contains? raw-svg-list id) "invalid raw svg id") [:> "svg" props [:use {:href (dm/str "#asset-" id)}]]) + diff --git a/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs b/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs new file mode 100644 index 0000000000..0f68a5b5f7 --- /dev/null +++ b/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs @@ -0,0 +1,67 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.nitrate.nitrate-activation-success-modal + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data.macros :as dm] + [app.common.time :as ct] + [app.main.data.modal :as modal] + [app.main.data.nitrate :as dnt] + [app.main.refs :as refs] + [app.main.ui.ds.buttons.button :refer [button*]] + [app.main.ui.ds.foundations.assets.icon :refer [icon*]] + [app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]] + [app.util.i18n :refer [tr]] + [rumext.v2 :as mf])) + +(mf/defc nitrate-activation-success-modal* + {::mf/register modal/components + ::mf/register-as :nitrate-activation-success + ::mf/wrap-props true} + [props] + + (let [profile (mf/deref refs/profile) + light? (= "light" (:theme profile)) + svg-id (if light? "logo-subscription-light" "logo-subscription") + + cancel-at (dm/get-in props [:subscription :cancel-at]) + date-str (when cancel-at + (ct/format-inst cancel-at "d MMMM, yyyy")) + + on-create-org + (mf/use-fn + (fn [] + (modal/hide!) + (dnt/go-to-nitrate-cc-create-org)))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog)} + [:button {:class (stl/css :close-btn) :on-click modal/hide!} + [:> icon* {:icon-id "close" + :size "m"}]] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-start)} + [:> raw-svg* {:id svg-id}]] + + [:div {:class (stl/css :modal-end)} + [:div {:class (stl/css :modal-title)} + (tr "nitrate.activation-success.title")] + + [:p {:class (stl/css :modal-text-primary)} + (tr "nitrate.activation-success.active-until" date-str)] + + [:p {:class (stl/css :modal-text)} + (tr "nitrate.activation-success.manage-info")] + + [:p {:class (stl/css :modal-text)} + (tr "nitrate.activation-success.enjoy")] + + [:> button* {:variant "primary" + :on-click on-create-org + :class (stl/css :modal-button)} + (tr "nitrate.activation-success.create-org")]]]]])) diff --git a/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.scss b/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.scss new file mode 100644 index 0000000000..5f1e8dd483 --- /dev/null +++ b/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.scss @@ -0,0 +1,79 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "refactor/common-refactor.scss" as deprecated; +@use "ds/typography.scss" as t; +@use "ds/_borders.scss" as *; +@use "ds/spacing.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; + +.modal-overlay { + @extend %modal-overlay-base; + + z-index: var(--z-index-notifications); +} + +.modal-dialog { + @extend %modal-container-base; + + max-block-size: initial; + min-inline-size: px2rem(608); + max-inline-size: px2rem(608); + padding: var(--sp-xxxl); +} + +.close-btn { + @extend %modal-close-btn-base; +} + +.modal-content { + display: flex; + gap: $sz-40; +} + +.modal-start { + display: flex; + justify-content: center; + min-inline-size: $sz-224; + + @media (width <= 640px) { + display: none; + } +} + +.modal-start svg { + inline-size: 100%; + block-size: auto; +} + +.modal-end { + color: var(--color-foreground-secondary); + display: flex; + flex-direction: column; + gap: var(--sp-m); +} + +.modal-title { + @include t.use-typography("title-large"); + + color: var(--modal-title-foreground-color); +} + +.modal-text-primary { + @include t.use-typography("body-large"); + + color: var(--color-foreground-primary); +} + +.modal-text { + @include t.use-typography("body-large"); +} + +.modal-button { + margin-block-start: var(--sp-s); + align-self: flex-start; +} diff --git a/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs new file mode 100644 index 0000000000..131dfc257c --- /dev/null +++ b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs @@ -0,0 +1,98 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.nitrate.nitrate-code-activation-modal + (:require-macros [app.main.style :as stl]) + (:require + [app.main.data.modal :as modal] + [app.main.data.profile :as dprof] + [app.main.repo :as rp] + [app.main.store :as st] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.nitrate.nitrate-activation-success-modal] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [beicon.v2.core :as rx] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(mf/defc nitrate-code-activation-modal* + {::mf/register modal/components + ::mf/register-as :nitrate-code-activation} + [_props] + (let [value* (mf/use-state "") + error* (mf/use-state nil) + + on-change + (mf/use-fn + (fn [event] + (reset! error* nil) + (reset! value* (dom/get-target-val event)))) + + on-accept + (mf/use-fn + (mf/deps value*) + (fn [_] + (let [code (str/trim @value*)] + (when (seq code) + (->> (rp/cmd! ::redeem-nitrate-activation-code {:activation-code code}) + (rx/subs! + (fn [result] + (modal/hide!) + (st/emit! + (modal/show {:type :nitrate-activation-success :subscription result}) + (dprof/refresh-profile))) + (fn [error] + ;; TODO: "Already used" is not yet detectable (CC upserts on reuse). + (let [code (-> error ex-data :code)] + (reset! error* (case code + :expired-activation-code (tr "nitrate.activation-code.expired-error") + (tr "nitrate.activation-code.invalid-error"))))))))))) + + on-key-down + (mf/use-fn + (mf/deps on-accept) + (fn [event] + (when (and (= "Enter" (.-key event)) (.-ctrlKey event)) + (on-accept event))))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog)} + [:> icon-button* {:variant "ghost" + :class (stl/css :close-btn) + :aria-label (tr "labels.close") + :on-click modal/hide! + :icon i/close}] + + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} (tr "nitrate.code-activation.title")]] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css-case :code-field true :invalid (some? @error*))} + [:label {:class (stl/css :code-label)} + (tr "nitrate.code-activation.input-label")] + [:textarea {:class (stl/css :code-textarea) + :auto-focus true + :value @value* + :placeholder (tr "nitrate.code-activation.placeholder") + :on-change on-change + :on-key-down on-key-down}] + (when @error* + [:span {:class (stl/css :error-msg)} @error*])] + + [:input + {:type "button" + :class (stl/css-case :accept-btn true + :global/disabled (empty? (str/trim @value*))) + :disabled (empty? (str/trim @value*)) + :value (tr "nitrate.code-activation.submit") + :on-click on-accept}] + [:div {:class (stl/css :footer-text)} + (tr "nitrate.code-activation.footer") " " + [:a {:class (stl/css :link) + :href "mailto:sales@nitrate.com"} + "sales@nitrate.com"]]]]])) diff --git a/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.scss b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.scss new file mode 100644 index 0000000000..d241c38332 --- /dev/null +++ b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.scss @@ -0,0 +1,107 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "refactor/common-refactor.scss" as deprecated; +@use "ds/typography.scss" as t; +@use "ds/spacing.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_borders.scss" as *; + +.close-btn { + @extend %modal-close-btn-base; +} + +.modal-overlay { + @extend %modal-overlay-base; + + z-index: var(--z-index-notifications); +} + +.modal-dialog { + @extend %modal-container-base; + + inline-size: $sz-480; + max-inline-size: $sz-480; + max-block-size: none; + max-height: none; + padding: var(--sp-xxxl); +} + +.modal-title { + @include t.use-typography("title-large"); + + color: var(--modal-title-foreground-color); + margin-block-end: var(--sp-xxxl); +} + +.modal-content { + display: flex; + flex-direction: column; + gap: var(--sp-m); + color: var(--color-foreground-secondary); +} + +.accept-btn { + @extend %modal-accept-btn; + + inline-size: 100%; +} + +.code-field { + display: flex; + flex-direction: column; + gap: var(--sp-xs); +} + +.code-label { + @include t.use-typography("body-medium"); + + color: var(--color-foreground-secondary); +} + +.code-textarea { + @include t.use-typography("body-medium"); + + block-size: $sz-200; + resize: vertical; + font-family: monospace; + word-break: break-all; + padding: var(--sp-s); + border-radius: $br-8; + border: $b-1 solid var(--input-border-color); + background-color: var(--input-background-color); + color: var(--color-foreground-primary); + outline: none; +} + +.code-textarea:focus { + border-color: var(--color-accent-primary); +} + +.invalid .code-textarea { + border-color: var(--input-border-color-error); +} + +.invalid .code-textarea:focus { + border-color: var(--input-border-color-error); +} + +.error-msg { + @include t.use-typography("body-small"); + + color: var(--element-foreground-error); +} + +.footer-text { + @include t.use-typography("body-medium"); + + color: var(--color-foreground-secondary); + margin-block-start: var(--sp-xxxl); +} + +.link { + color: var(--color-accent-primary); +} diff --git a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs index ef3bc46d38..13143d5337 100644 --- a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs +++ b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs @@ -11,10 +11,13 @@ [app.main.data.modal :as modal] [app.main.data.nitrate :as dnt] [app.main.refs :as refs] + [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]] + [app.main.ui.nitrate.nitrate-code-activation-modal] + [app.util.i18n :refer [tr]] [rumext.v2 :as mf])) (def ^:private schema:nitrate-form @@ -37,7 +40,12 @@ (mf/use-fn (mf/deps form) (fn [] - (dnt/go-to-buy-nitrate-license (-> @form :clean-data :subscription name))))] + (dnt/go-to-buy-nitrate-license (-> @form :clean-data :subscription name)))) + + on-activate-click + (mf/use-fn + (fn [] + (st/emit! (modal/show {:type :nitrate-code-activation}))))] [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-dialog :subscription-success)} @@ -51,7 +59,7 @@ [:div {:class (stl/css :modal-end)} [:div {:class (stl/css :modal-title)} - "Unlock Nitrate Features"] + (tr "nitrate.form.title")] [:p {:class (stl/css :modal-text-large)} "Prow scuttle parrel provost."] @@ -64,8 +72,8 @@ [:p {:class (stl/css :modal-text-large)} [:& fm/radio-buttons - {:options [{:label "Price Tag Montly" :value "monthly"} - {:label "Price Tag Yearly (Discount)" :value "yearly"}] + {:options [{:label (tr "nitrate.form.billing-monthly") :value "monthly"} + {:label (tr "nitrate.form.billing-yearly") :value "yearly"}] :name :subscription :class (stl/css :radio-btns)}]] @@ -75,23 +83,34 @@ :on-click on-click :class (stl/css :modal-button)} (if (:subscription profile) - "UPGRADE TO NITRATE" - "Try it free for 14 days")] + (tr "nitrate.form.upgrade") + (tr "nitrate.form.try-free"))] [:div {:class (stl/css :modal-text-small :modal-info)} - "Cancel anytime before your next billing cycle."]]] + (tr "nitrate.form.cancel-anytime")]]] + [:p {:class (stl/css :modal-text-medium)} + (tr "nitrate.form.have-code") " " [:a {:class (stl/css :link) + :on-click on-activate-click} + (tr "nitrate.form.enter-code")]] [:p {:class (stl/css :modal-text-medium)} [:a {:class (stl/css :link) :href dnt/go-to-subscription-url} - "See my current plan"]]] + (tr "nitrate.form.see-plan")]]] [:div {:class (stl/css :contact)} [:p {:class (stl/css :modal-text-large)} (if (:subscription profile) - "Contact us to upgrade to Nitrate:" - "Contact us to try Nitrate for 14 days:")] + (tr "nitrate.form.contact-upgrade") + (tr "nitrate.form.contact-trial"))] [:p {:class (stl/css :modal-text-large)} [:a {:class (stl/css :link) :href "mailto:sales@penpot.app"} - "sales@penpot.app"]]])]]]])) + "sales@penpot.app"]] + [:div {:class (stl/css :activation-code)} + [:p {:class (stl/css :modal-text-large)} + (tr "nitrate.form.have-code")] + [:p {:class (stl/css :modal-text-large)} + [:a {:class (stl/css :link) + :on-click on-activate-click} + (tr "nitrate.form.enter-code")]]]])]]]])) diff --git a/frontend/src/app/main/ui/nitrate/nitrate_form.scss b/frontend/src/app/main/ui/nitrate/nitrate_form.scss index d21a24c26c..76942a6f7a 100644 --- a/frontend/src/app/main/ui/nitrate/nitrate_form.scss +++ b/frontend/src/app/main/ui/nitrate/nitrate_form.scss @@ -77,36 +77,40 @@ justify-content: center; min-inline-size: $sz-284; - svg { - inline-size: 100%; - block-size: auto; - } - @media (width <= 992px) { display: none; } } +.modal-start svg { + inline-size: 100%; + block-size: auto; +} + .radio-btns { - label { - @include t.use-typography("body-large"); - - padding: 0; - display: flex; - align-items: center; - } - display: flex; flex-direction: column; padding: var(--sp-l) 0 0 0; gap: 0; } +.radio-btns label { + @include t.use-typography("body-large"); + + padding: 0; + display: flex; + align-items: center; +} + .contact { margin-block-start: $sz-96; color: var(--color-foreground-primary); } +.activation-code { + margin-block-start: var(--sp-xxxl); +} + .link { color: var(--color-accent-primary); } diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index 42d892f7d8..af7e9d9ddc 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -18,6 +18,7 @@ [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]] + [app.main.ui.nitrate.nitrate-activation-success-modal] [app.main.ui.notifications.badge :refer [badge-notification]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr c]] @@ -36,6 +37,7 @@ cta-link-trial cta-text-with-icon cta-link-with-icon + show-activation-by-code editors recommended show-button-cta]}] @@ -65,6 +67,14 @@ [:ul {:class (stl/css :benefits-list)} (for [benefit benefits] [:li {:key (dm/str benefit) :class (stl/css :benefit)} "- " benefit])] + (when (and cta-link cta-text show-button-cta) + [:> button* {:variant "primary" + :type "button" + :class (stl/css-case :bottom-button (not (and cta-link-trial cta-text-trial))) + :on-click cta-link} cta-text]) + (when (and cta-link-trial cta-text-trial) + [:button {:class (stl/css :cta-button :bottom-link) + :on-click cta-link-trial} cta-text-trial]) (when (and cta-link-with-icon cta-text-with-icon) [:button {:class (stl/css :cta-button :more-info) :on-click cta-link-with-icon} cta-text-with-icon @@ -74,14 +84,10 @@ [:button {:class (stl/css-case :cta-button true :bottom-link (not (and cta-link-trial cta-text-trial))) :on-click cta-link} cta-text]) - (when (and cta-link cta-text show-button-cta) - [:> button* {:variant "primary" - :type "button" - :class (stl/css-case :bottom-button (not (and cta-link-trial cta-text-trial))) - :on-click cta-link} cta-text]) - (when (and cta-link-trial cta-text-trial) - [:button {:class (stl/css :cta-button :bottom-link) - :on-click cta-link-trial} cta-text-trial])]) + (when show-activation-by-code + [:button {:class (stl/css :cta-button :activate-by-code) + :on-click #(st/emit! (modal/show {:type :nitrate-code-activation}))} + (tr "subscription.settings.activate-by-code")])]) (defn- make-management-form-schema [min-editors] [:map {:title "SeatsForm"} @@ -359,37 +365,6 @@ :value (tr "labels.close") :on-click handle-close-dialog}]]]]]])) -(mf/defc nitrate-success-dialog - {::mf/register modal/components - ::mf/register-as :nitrate-success} - [] - ;; TODO add translations for this texts when we have the definitive ones - (let [profile (mf/deref refs/profile)] - - [:div {:class (stl/css :modal-overlay)} - [:div {:class (stl/css :modal-dialog :subscription-success)} - [:button {:class (stl/css :close-btn) :on-click modal/hide!} - [:> icon* {:icon-id "close" - :size "m"}]] - [:div {:class (stl/css :modal-success-content)} - [:div {:class (stl/css :modal-start)} - [:> raw-svg* {:id (if (= "light" (:theme profile)) "logo-subscription-light" "logo-subscription")}]] - - [:div {:class (stl/css :modal-end)} - [:div {:class (stl/css :modal-title)} - "You are Business Nitrate!"] - [:p {:class (stl/css :modal-text-large)} - (tr "subscription.settings.success.dialog.description")] - [:p {:class (stl/css :modal-text-large)} - (tr "subscription.settings.success.dialog.footer")] - - [:div {:class (stl/css :success-action-buttons)} - [:input - {:class (stl/css :primary-button) - :type "button" - :value "CREATE ORGANIZATION" - :on-click dnt/go-to-nitrate-cc-create-org}]]]]]])) - (mf/defc subscription-page* [{:keys [profile]}] (let [route (mf/deref refs/route) @@ -500,7 +475,7 @@ ^boolean show-subscription-success-modal? (st/emit! (if (= params-subscription "subscribed-to-penpot-nitrate") - (modal/show :nitrate-success {}) + (modal/show :nitrate-activation-success {}) (modal/show :subscription-success {:subscription-name (if (= params-subscription "subscribed-to-penpot-unlimited") (if (= success-modal-is-trial? "true") @@ -523,7 +498,7 @@ [:> plan-card* {:card-title "Business Nitrate" :card-title-icon i/character-b :cancel-at (when (:cancel-at nitrate-license) - (dm/str "Active until " (ct/format-inst (:cancel-at nitrate-license) "d MMMM, yyyy"))) + (tr "nitrate.subscription.active-until" (ct/format-inst (:cancel-at nitrate-license) "d MMMM, yyyy"))) :benefits-title "Loren ipsum", :benefits ["Loren ipsum", "Loren ipsum", @@ -660,6 +635,7 @@ :cta-link (if (= subscription-type "unlimited") #(open-contact-sales-modal subscription-type "Nitrate") #(open-subscription-modal "nitrate" subscription)) :cta-text-with-icon (tr "subscription.settings.more-information") :cta-link-with-icon go-to-pricing-page + :show-activation-by-code true :show-button-cta (not nitrate-license)}])]]])) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e4259e76fa..9426a51036 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -9252,3 +9252,100 @@ msgstr "Are you sure you want to delete this team that is part of %s org?" msgid "plugins.validation.message" msgstr "Field %s is invalid: %s" + +msgid "nitrate.code-activation.title" +msgstr "Activate Nitrate" + +msgid "nitrate.code-activation.input-label" +msgstr "Enter your activation code" + +msgid "nitrate.code-activation.submit" +msgstr "Activate" + +msgid "nitrate.code-activation.footer" +msgstr "Need a code? Contact us:" + +msgid "nitrate.code-activation.placeholder" +msgstr "Paste your activation code here" + + +msgid "nitrate.activation-success.title" +msgstr "You are Business Nitrate!" + +msgid "nitrate.activation-success.active-until" +msgstr "Your plan is active until %s." + +msgid "nitrate.activation-success.manage-info" +msgstr "You can manage your subscription anytime from the Subscription page in your account settings." + +msgid "nitrate.activation-success.enjoy" +msgstr "Enjoy your plan!" + +msgid "nitrate.activation-success.create-org" +msgstr "Create organization" + +msgid "nitrate.form.title" +msgstr "Unlock Nitrate Features" + +msgid "nitrate.form.billing-monthly" +msgstr "Price Tag Monthly" + +msgid "nitrate.form.billing-yearly" +msgstr "Price Tag Yearly (Discount)" + +msgid "nitrate.form.upgrade" +msgstr "Upgrade to Nitrate" + +msgid "nitrate.form.try-free" +msgstr "Try it free for 14 days" + +msgid "nitrate.form.cancel-anytime" +msgstr "Cancel anytime before your next billing cycle." + +msgid "nitrate.form.have-code" +msgstr "Have an activation code?" + +msgid "nitrate.form.enter-code" +msgstr "Enter activation code" + +msgid "nitrate.form.see-plan" +msgstr "See my current plan" + +msgid "nitrate.form.contact-upgrade" +msgstr "Contact us to upgrade to Nitrate:" + +msgid "nitrate.form.contact-trial" +msgstr "Contact us to try Nitrate for 14 days:" + +msgid "nitrate.activation-code.invalid-error" +msgstr "Invalid code." + +msgid "nitrate.activation-code.expired-error" +msgstr "This code has expired." + +msgid "nitrate.contact-sales.title" +msgstr "Switch to %s plan?" + +msgid "nitrate.contact-sales.downgrade-title" +msgstr "When you downgrade:" + +msgid "nitrate.contact-sales.downgrade-org-deleted" +msgstr "Your organization will be deleted." + +msgid "nitrate.contact-sales.downgrade-teams-available" +msgstr "The teams, projects and files will no longer be part of any organization but they will remain available." + +msgid "nitrate.contact-sales.downgrade-storage-limited" +msgstr "Your total storage, auto-version history, and file recovery period will be limited." + +msgid "nitrate.contact-sales.downgrade-contact-info" +msgstr "To switch to this plan, please contact our sales team. We'll help you update your subscription and ensure everything is set up correctly." + +msgid "nitrate.contact-sales.button" +msgstr "Contact sales" + +msgid "nitrate.subscription.active-until" +msgstr "Active until %s" + +msgid "subscription.settings.activate-by-code" +msgstr "Enter activation code" \ No newline at end of file diff --git a/frontend/translations/es.po b/frontend/translations/es.po index ecb1a4ecec..4d670f8716 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -8977,3 +8977,103 @@ msgstr "Pulsar para cerrar la ruta" msgid "modals.delete-org-team-confirm.message" msgstr "¿Estás seguro de que deseas eliminar este equipo que forma parte de la organización %s?" + +msgid "nitrate.code-activation.title" +msgstr "Activar Nitrate" + +msgid "nitrate.code-activation.input-label" +msgstr "Introduce tu código de activación" + +msgid "nitrate.code-activation.submit" +msgstr "Activar" + +msgid "nitrate.code-activation.footer" +msgstr "¿Necesitas un código? Contáctanos:" + +msgid "nitrate.code-activation.placeholder" +msgstr "Pega aquí tu código de activación" + + +msgid "nitrate.activation-success.title" +msgstr "¡Ya eres Business Nitrate!" + +msgid "nitrate.activation-success.active-until" +msgstr "Tu plan está activo hasta el %s." + +msgid "nitrate.activation-success.manage-info" +msgstr "Puedes gestionar tu suscripción en cualquier momento desde la página de Suscripción en la configuración de tu cuenta." + +msgid "nitrate.activation-success.enjoy" +msgstr "¡Disfruta de tu plan!" + +msgid "nitrate.activation-success.create-org" +msgstr "Crear organización" + +msgid "nitrate.form.title" +msgstr "Desbloquea las funciones de Nitrate" + +msgid "nitrate.form.billing-monthly" +msgstr "Precio mensual" + +msgid "nitrate.form.billing-yearly" +msgstr "Precio anual (descuento)" + +msgid "nitrate.form.upgrade" +msgstr "Actualizar a Nitrate" + +msgid "nitrate.form.try-free" +msgstr "Pruébalo gratis durante 14 días" + +msgid "nitrate.form.cancel-anytime" +msgstr "Cancela en cualquier momento antes de tu próximo ciclo de facturación." + +msgid "nitrate.form.have-code" +msgstr "¿Tienes un código de activación?" + +msgid "nitrate.form.enter-code" +msgstr "Introducir código de activación" + +msgid "nitrate.form.see-plan" +msgstr "Ver mi plan actual" + +msgid "nitrate.form.contact-upgrade" +msgstr "Contáctanos para actualizar a Nitrate:" + +msgid "nitrate.form.contact-trial" +msgstr "Contáctanos para probar Nitrate durante 14 días:" + +msgid "nitrate.activation-code.invalid-error" +msgstr "Código inválido." + +msgid "nitrate.activation-code.expired-error" +msgstr "Este código ha caducado." + +msgid "nitrate.contact-sales.title" +msgstr "¿Cambiar al plan %s?" + +msgid "nitrate.contact-sales.downgrade-title" +msgstr "Al bajar de plan:" + +msgid "nitrate.contact-sales.downgrade-org-deleted" +msgstr "Tu organización será eliminada." + +msgid "nitrate.contact-sales.downgrade-teams-available" +msgstr "Los equipos, proyectos y archivos dejarán de pertenecer a la organización pero seguirán disponibles." + +msgid "nitrate.contact-sales.downgrade-storage-limited" +msgstr "Tu almacenamiento total, el historial de versiones automático y el período de recuperación de archivos serán limitados." + +msgid "nitrate.contact-sales.downgrade-contact-info" +msgstr "Para cambiar a este plan, contacta con nuestro equipo de ventas. Te ayudaremos a actualizar tu suscripción y a asegurarnos de que todo esté configurado correctamente." + +msgid "nitrate.contact-sales.button" +msgstr "Contactar con ventas" + +msgid "nitrate.subscription.active-until" +msgstr "Activo hasta el %s" + +msgid "subscription.settings.more-information" +msgstr "Más información" + +msgid "subscription.settings.activate-by-code" +msgstr "Introducir código de activación" \ No newline at end of file From d668744a1f1b5b0f930f9197ac600cc034e0bd91 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:44:53 -0400 Subject: [PATCH 250/288] :sparkles: Add search to board size presets dropdown (#9117) Closes #4658 Signed-off-by: eureka0928 Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + .../sidebar/options/drawing/frame.cljs | 96 ++++++++++----- .../sidebar/options/drawing/frame.scss | 17 +++ .../sidebar/options/menus/measures.cljs | 114 +++++++++++++----- .../sidebar/options/menus/measures.scss | 17 +++ frontend/translations/en.po | 8 ++ 6 files changed, 199 insertions(+), 54 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 18e71ee6c4..5311f312bf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,7 @@ - Add delete and duplicate buttons to typography dialog (by @eureka0928) [Github #5270](https://github.com/penpot/penpot/issues/5270) - Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311) - Add a search bar to filter colors in the color palette toolbar (by @eureka0928) [Github #7653](https://github.com/penpot/penpot/issues/7653) +- Add a search bar to filter board size presets (by @eureka0928) [Github #4658](https://github.com/penpot/penpot/issues/4658) - Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027) - Add page separators in Workspace [Taiga #13611](https://tree.taiga.io/project/penpot/us/13611?milestone=262806) - Preserve vector content when pasting from external tools such as Inkscape: recognise SVG sent as text/plain (with optional XML declaration and HTML comments), skip the raster preview when an SVG sibling is on the clipboard, and ignore empty SVG blobs that some tools advertise alongside the real payload, so pasted graphics arrive editable without spurious "SVG is invalid" warnings [Github #546](https://github.com/penpot/penpot/issues/546) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs index 275ad11e8d..fa40189182 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs @@ -13,8 +13,10 @@ [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.icons :as deprecated-icon] + [app.main.ui.workspace.sidebar.options.menus.measures :as measures] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) @@ -31,11 +33,35 @@ selected-preset-name (deref selected-preset-name*) - on-open - (mf/use-fn (fn [] (reset! show* true))) + search-term* + (mf/use-state "") + + search-term + (deref search-term*) + + container-ref + (mf/use-ref nil) + + on-toggle + (mf/use-fn + (fn [] + (swap! show* not) + (reset! search-term* ""))) on-close - (mf/use-fn (fn [] (reset! show* false))) + (mf/use-fn + (fn [] + (reset! show* false) + (reset! search-term* ""))) + + on-search-change + (mf/use-fn + (fn [value _event] + (reset! search-term* value))) + + filtered-presets + (mf/with-memo [search-term] + (measures/filter-size-presets search-term size-presets)) on-preset-selected (mf/use-fn @@ -48,7 +74,9 @@ (d/read-string))] (reset! selected-preset-name* name) - (st/emit! (dwd/set-default-size width height))))) + (st/emit! (dwd/set-default-size width height)) + (reset! show* false) + (reset! search-term* "")))) orientation (when (:width drawing-state) @@ -65,35 +93,49 @@ [:div {:class (stl/css :presets)} [:div {:class (stl/css-case :presets-wrapper true :opened show?) - :on-click on-open} + :ref container-ref + :on-click on-toggle} [:span {:class (stl/css :select-name)} (or selected-preset-name (tr "workspace.options.size-presets"))] [:span {:class (stl/css :collapsed-icon)} deprecated-icon/arrow] [:& dropdown {:show show? - :on-close on-close} - [:ul {:class (stl/css :custom-select-dropdown)} - (for [preset size-presets] - (if-not (:width preset) - [:li {:key (:name preset) - :class (stl/css-case :dropdown-element true - :disabled true)} - [:span {:class (stl/css :preset-name)} (:name preset)]] + :on-close on-close + :container container-ref} + [:div {:class (stl/css :custom-select-dropdown) + :on-click dom/stop-propagation} + [:div {:class (stl/css :preset-search)} + [:> search-bar* {:on-change on-search-change + :value search-term + :auto-focus true + :placeholder (tr "workspace.options.search-size-preset")}]] + [:ul {:class (stl/css :preset-list)} + (if (empty? filtered-presets) + [:li {:class (stl/css-case :dropdown-element true + :disabled true)} + [:span {:class (stl/css :preset-name)} + (tr "workspace.options.no-size-preset-results")]] + (for [preset filtered-presets] + (if-not (:width preset) + [:li {:key (:name preset) + :class (stl/css-case :dropdown-element true + :disabled true)} + [:span {:class (stl/css :preset-name)} (:name preset)]] - (let [preset-match (and (= (:width preset) (:width drawing-state)) - (= (:height preset) (:height drawing-state)))] - [:li {:key (:name preset) - :class (stl/css-case :dropdown-element true - :match preset-match) - :data-width (str (:width preset)) - :data-height (str (:height preset)) - :data-name (:name preset) - :on-click on-preset-selected} - [:div {:class (stl/css :name-wrapper)} - [:span {:class (stl/css :preset-name)} (:name preset)] - [:span {:class (stl/css :preset-size)} (:width preset) " x " (:height preset)]] - (when preset-match - [:span {:class (stl/css :check-icon)} deprecated-icon/tick])])))]]] + (let [preset-match (and (= (:width preset) (:width drawing-state)) + (= (:height preset) (:height drawing-state)))] + [:li {:key (:name preset) + :class (stl/css-case :dropdown-element true + :match preset-match) + :data-width (str (:width preset)) + :data-height (str (:height preset)) + :data-name (:name preset) + :on-click on-preset-selected} + [:div {:class (stl/css :name-wrapper)} + [:span {:class (stl/css :preset-name)} (:name preset)] + [:span {:class (stl/css :preset-size)} (:width preset) " x " (:height preset)]] + (when preset-match + [:span {:class (stl/css :check-icon)} deprecated-icon/tick])]))))]]]] [:& radio-buttons {:selected (or (d/name orientation) "") :on-change on-orientation-change diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss index 1599bcad25..53221cd2a5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss @@ -64,6 +64,23 @@ margin-top: deprecated.$s-2; max-height: 70vh; width: deprecated.$s-252; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.preset-search { + padding: deprecated.$s-4; + border-bottom: deprecated.$s-1 solid var(--menu-border-color-rest, transparent); +} + +.preset-list { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + margin: 0; + padding: 0; + list-style: none; .dropdown-element { @extend %dropdown-element-base; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index ef9936d90e..3d1c4741b9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -26,6 +26,7 @@ [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.numeric-input :as deprecated-input] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.icons :as deprecated-icon] @@ -34,6 +35,7 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [clojure.set :as set] + [cuerdas.core :as str] [rumext.v2 :as mf])) (def measure-attrs @@ -105,6 +107,29 @@ (number? value) (parse-double (.toFixed value decimals))))) +(defn filter-size-presets + "Filter the `size-presets` list by `term`, preserving category headers only + when at least one of their following presets matches." + [term presets] + (if (str/blank? term) + presets + (let [lterm (str/lower term) + matches? (fn [p] (and (:width p) + (str/includes? (str/lower (:name p)) lterm)))] + (loop [remaining presets + acc []] + (if-let [head (first remaining)] + (if (:width head) + (recur (rest remaining) + (cond-> acc (matches? head) (conj head))) + (let [[items tail] (split-with :width (rest remaining)) + matching-items (filter matches? items)] + (recur tail + (if (seq matching-items) + (into (conj acc head) matching-items) + acc)))) + acc))))) + (mf/defc measures-menu* [{:keys [ids values applied-tokens type shapes]}] (let [token-numeric-inputs @@ -235,17 +260,36 @@ show-presets-dropdown? (deref preset-state*) - open-presets + preset-search-term* + (mf/use-state "") + + preset-search-term + (deref preset-search-term*) + + preset-container-ref + (mf/use-ref nil) + + toggle-presets (mf/use-fn - (mf/deps show-presets-dropdown?) (fn [] - (reset! preset-state* true))) + (swap! preset-state* not) + (reset! preset-search-term* ""))) close-presets (mf/use-fn (mf/deps show-presets-dropdown?) (fn [] - (reset! preset-state* false))) + (reset! preset-state* false) + (reset! preset-search-term* ""))) + + on-preset-search-change + (mf/use-fn + (fn [value _event] + (reset! preset-search-term* value))) + + filtered-size-presets + (mf/with-memo [preset-search-term] + (filter-size-presets preset-search-term size-presets)) on-preset-selected (mf/use-fn @@ -258,7 +302,9 @@ (dom/get-data "height") (d/read-string))] (st/emit! (udw/update-dimensions ids :width width) - (udw/update-dimensions ids :height height))))) + (udw/update-dimensions ids :height height)) + (reset! preset-state* false) + (reset! preset-search-term* "")))) ;; ORIENTATION @@ -379,33 +425,47 @@ [:div {:class (stl/css :presets)} [:div {:class (stl/css-case :presets-wrapper true :opened show-presets-dropdown?) - :on-click open-presets} + :ref preset-container-ref + :on-click toggle-presets} [:span {:class (stl/css :select-name)} (tr "workspace.options.size-presets")] [:span {:class (stl/css :collapsed-icon)} deprecated-icon/arrow] [:& dropdown {:show show-presets-dropdown? - :on-close close-presets} - [:ul {:class (stl/css :custom-select-dropdown)} - (for [size-preset size-presets] - (if-not (:width size-preset) - [:li {:key (:name size-preset) - :class (stl/css-case :dropdown-element true - :disabled true)} - [:span {:class (stl/css :preset-name)} (:name size-preset)]] + :on-close close-presets + :container preset-container-ref} + [:div {:class (stl/css :custom-select-dropdown) + :on-click dom/stop-propagation} + [:div {:class (stl/css :preset-search)} + [:> search-bar* {:on-change on-preset-search-change + :value preset-search-term + :auto-focus true + :placeholder (tr "workspace.options.search-size-preset")}]] + [:ul {:class (stl/css :preset-list)} + (if (empty? filtered-size-presets) + [:li {:class (stl/css-case :dropdown-element true + :disabled true)} + [:span {:class (stl/css :preset-name)} + (tr "workspace.options.no-size-preset-results")]] + (for [size-preset filtered-size-presets] + (if-not (:width size-preset) + [:li {:key (:name size-preset) + :class (stl/css-case :dropdown-element true + :disabled true)} + [:span {:class (stl/css :preset-name)} (:name size-preset)]] - (let [preset-match (and (= (:width size-preset) (d/parse-integer (:width values) 0)) - (= (:height size-preset) (d/parse-integer (:height values) 0)))] - [:li {:key (:name size-preset) - :class (stl/css-case :dropdown-element true - :match preset-match) - :data-width (str (:width size-preset)) - :data-height (str (:height size-preset)) - :on-click on-preset-selected} - [:div {:class (stl/css :name-wrapper)} - [:span {:class (stl/css :preset-name)} (:name size-preset)] - [:span {:class (stl/css :preset-size)} (:width size-preset) " x " (:height size-preset)]] - (when preset-match - [:span {:class (stl/css :check-icon)} deprecated-icon/tick])])))]]] + (let [preset-match (and (= (:width size-preset) (d/parse-integer (:width values) 0)) + (= (:height size-preset) (d/parse-integer (:height values) 0)))] + [:li {:key (:name size-preset) + :class (stl/css-case :dropdown-element true + :match preset-match) + :data-width (str (:width size-preset)) + :data-height (str (:height size-preset)) + :on-click on-preset-selected} + [:div {:class (stl/css :name-wrapper)} + [:span {:class (stl/css :preset-name)} (:name size-preset)] + [:span {:class (stl/css :preset-size)} (:width size-preset) " x " (:height size-preset)]] + (when preset-match + [:span {:class (stl/css :check-icon)} deprecated-icon/tick])]))))]]]] [:& radio-buttons {:selected (or (d/name orientation) "") :on-change on-orientation-change diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss index e3605152d8..357df42145 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss @@ -75,6 +75,23 @@ margin-top: deprecated.$s-2; max-height: 70vh; width: deprecated.$s-252; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.preset-search { + padding: deprecated.$s-4; + border-bottom: deprecated.$s-1 solid var(--menu-border-color-rest, transparent); +} + +.preset-list { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + margin: 0; + padding: 0; + list-style: none; .dropdown-element { @extend %dropdown-element-base; diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 9426a51036..e04a501a56 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7351,6 +7351,14 @@ msgstr "Size" msgid "workspace.options.size-presets" msgstr "Size presets" +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.search-size-preset" +msgstr "Search size preset" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.no-size-preset-results" +msgstr "No matching size preset" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:469 msgid "workspace.options.size.lock" msgstr "Lock ratio" From fd170b23f65d2463f8aba14b1c48e8eab2b161a4 Mon Sep 17 00:00:00 2001 From: Statxc Date: Wed, 29 Apr 2026 15:45:22 +0200 Subject: [PATCH 251/288] :bug: Fix Heroicons arrow paths broken after SVG import (#5283) (#9156) Signed-off-by: statxc <181730535+statxc@users.noreply.github.com> Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + .../src/app/common/files/shapes_builder.cljc | 22 ++++++-- common/src/app/common/types/path.cljc | 13 +++++ common/src/app/common/types/path/subpath.cljc | 30 +++++++++++ .../common_tests/types/path_data_test.cljc | 53 +++++++++++++++++++ 5 files changed, 115 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5311f312bf..dce36f8942 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -54,6 +54,7 @@ ### :bug: Bugs fixed - Fix plugin API `fileVersion.restore()` promise hanging indefinitely on restore failure [Github #9092](https://github.com/penpot/penpot/issues/9092) +- Fix imported stroke-only SVG paths losing their rounded join when authoring tools (e.g. Figma → Heroicons) split a continuous polyline into adjacent `M…L M…L` subpaths sharing an endpoint; on import these are now folded back into one chain so `stroke-linejoin` renders the elbow correctly in both editor and exports [Github #5283](https://github.com/penpot/penpot/issues/5283) - Fix plugin API `library.connectLibrary()` returning a non-Promise (or throwing synchronously) when the plugin lacks `library:write` permission — the method now always returns a `Promise` and rejects with a structured error message, matching the contract used by every other Promise-returning plugin method (`restore`, `remove`, `pin`, `saveVersion`, `findVersions`, …) - Fix LDAP provider params schema typo (`bind-passwor` → `bind-password`) introduced during the `clojure.spec` → `malli` migration; the schema slot now matches the runtime key actually read by `prepare-params` (`:password (:bind-password cfg)`) and `try-connectivity` (`(:bind-password cfg)`), so a wrong type for the password no longer slips through unvalidated - Fix `login-with-ldap` silently dropping its error message on the `ldap-not-initialized` restriction (typo `:hide` → `:hint`); the message `"ldap auth provider is not initialized"` now actually surfaces in logs and error responses instead of being discarded into an unread key diff --git a/common/src/app/common/files/shapes_builder.cljc b/common/src/app/common/files/shapes_builder.cljc index 76b6ef4c04..668f50fcaf 100644 --- a/common/src/app/common/files/shapes_builder.cljc +++ b/common/src/app/common/files/shapes_builder.cljc @@ -340,12 +340,26 @@ :svg-viewbox vbox :svg-defs defs}))) +(defn- stroke-only-svg-path? + "Returns true when the SVG element renders only a stroke (fill=none). + Stroke-only paths can have their consecutive touching subpaths safely + merged into a continuous polyline so that `stroke-linejoin` applies at + shared endpoints, without affecting any fill-rule semantics." + [attrs] + (let [attr-fill (some-> (:fill attrs) str/trim) + style-fill (some-> (get-in attrs [:style :fill]) str/trim)] + (= "none" (or attr-fill style-fill)))) + (defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}] (when (and (contains? attrs :d) (seq (:d attrs))) - (let [transform (csvg/parse-transform (:transform attrs)) - content (cond-> (path/from-string (:d attrs)) - (some? transform) - (path.segm/transform-content transform)) + (let [transform (csvg/parse-transform (:transform attrs)) + stroke-only? (stroke-only-svg-path? attrs) + content (cond-> (path/from-string (:d attrs)) + stroke-only? + (path/merge-touching-subpaths) + + (some? transform) + (path.segm/transform-content transform)) selrect (path.segm/content->selrect content) points (grc/rect->points selrect) diff --git a/common/src/app/common/types/path.cljc b/common/src/app/common/types/path.cljc index f3b7c635ab..601de4c36d 100644 --- a/common/src/app/common/types/path.cljc +++ b/common/src/app/common/types/path.cljc @@ -84,6 +84,19 @@ (-> (subpath/close-subpaths content) (impl/from-plain))) +(defn merge-touching-subpaths + "Given a content, fold consecutive subpaths whose endpoints coincide + into a single continuous subpath, returning a PathData instance. + + Conservative counterpart of `close-subpaths`: only adjacent subpaths + are merged and none are reversed, so fill rules and stroke-dasharray + semantics are preserved. Used at SVG-import time on stroke-only paths + to recover the `stroke-linejoin` rendering when authoring tools split + a continuous polyline into adjacent `M..L M..L` subpaths." + [content] + (-> (subpath/merge-touching-subpaths content) + (impl/from-plain))) + (defn apply-content-modifiers "Apply delta modifiers over the path content" [content modifiers] diff --git a/common/src/app/common/types/path/subpath.cljc b/common/src/app/common/types/path/subpath.cljc index b7f13a0aea..12065891e6 100644 --- a/common/src/app/common/types/path/subpath.cljc +++ b/common/src/app/common/types/path/subpath.cljc @@ -128,6 +128,36 @@ (def ^:private xf-mapcat-data (mapcat :data)) +(defn- join-adjacent + "Fold neighbouring subpaths into the accumulator only when the + current accumulator's end-point matches the next subpath's start-point. + Unlike `merge-paths` this does not reverse subpaths nor reorder them; + the original draw order is preserved so stroke-dasharray and animation + semantics stay intact." + [acc subpath] + (if-let [prev (peek acc)] + (if (and (not (is-closed? prev)) + (not (is-closed? subpath)) + (pt= (:to prev) (:from subpath))) + (conj (pop acc) (subpaths-join prev subpath)) + (conj acc subpath)) + (conj acc subpath))) + +(defn merge-touching-subpaths + "Merge consecutive subpaths whose endpoints coincide into a single + continuous subpath, preserving the original drawing order. + + This is a conservative variant of `close-subpaths`: it never reverses + a subpath and only merges immediate neighbours, so closed regions and + fill semantics are left untouched. The intent is to recover the + `stroke-linejoin` rendering for SVG paths whose authoring tools split + a continuous polyline into adjacent `M..L M..L` subpaths (e.g. the + `m0 0` markers Figma emits when exporting Heroicons-like icons)." + [content] + (let [subpaths (get-subpaths content) + merged (reduce join-adjacent [] subpaths)] + (into [] xf-mapcat-data merged))) + (defn close-subpaths "Searches a path for possible subpaths that can create closed loops and merge them" [content] diff --git a/common/test/common_tests/types/path_data_test.cljc b/common/test/common_tests/types/path_data_test.cljc index 6dc7fa5207..69d14355b7 100644 --- a/common/test/common_tests/types/path_data_test.cljc +++ b/common/test/common_tests/types/path_data_test.cljc @@ -667,6 +667,41 @@ result (path.subpath/close-subpaths content)] (t/is (seq result))))) +(t/deftest subpath-merge-touching-subpaths + (t/testing "adjacent subpaths sharing an endpoint collapse into one chain" + ;; Heroicons-style fragment: continuous polyline split as M-L M-L M-L + ;; with the second/third subpath starting at the first's endpoint. + (let [content [{:command :move-to :params {:x 0.0 :y 10.0}} + {:command :line-to :params {:x 10.0 :y 10.0}} + {:command :move-to :params {:x 10.0 :y 10.0}} + {:command :line-to :params {:x 5.0 :y 0.0}} + {:command :move-to :params {:x 10.0 :y 10.0}} + {:command :line-to :params {:x 5.0 :y 20.0}}] + result (path.subpath/merge-touching-subpaths content) + moves (filter #(= :move-to (:command %)) result)] + ;; Subpaths 1+2 share (10,10) → merged. Subpath 3 also starts at (10,10), + ;; but the merged chain now ends at (5,0), so it does NOT match and + ;; is preserved as its own subpath. Two move-tos in the final result. + (t/is (= 2 (count moves))) + (t/is (= 5 (count result))))) + (t/testing "non-touching subpaths are left untouched" + (let [content [{:command :move-to :params {:x 0.0 :y 0.0}} + {:command :line-to :params {:x 5.0 :y 0.0}} + {:command :move-to :params {:x 50.0 :y 50.0}} + {:command :line-to :params {:x 60.0 :y 60.0}}] + result (path.subpath/merge-touching-subpaths content)] + (t/is (= content (vec result))))) + (t/testing "closed subpath is not absorbed into a neighbour" + (let [content [{:command :move-to :params {:x 0.0 :y 0.0}} + {:command :line-to :params {:x 5.0 :y 0.0}} + {:command :line-to :params {:x 5.0 :y 5.0}} + {:command :line-to :params {:x 0.0 :y 0.0}} + {:command :move-to :params {:x 0.0 :y 0.0}} + {:command :line-to :params {:x 1.0 :y 1.0}}] + result (path.subpath/merge-touching-subpaths content) + moves (filter #(= :move-to (:command %)) result)] + (t/is (= 2 (count moves)))))) + (t/deftest subpath-reverse-content (let [result (path.subpath/reverse-content simple-open-content)] (t/is (= (count simple-open-content) (count result))) @@ -1100,6 +1135,24 @@ (t/is (path/content? result)) (t/is (seq (vec result))))) +(t/deftest path-merge-touching-subpaths + (t/testing "regression for #5283 — heroicons arrow path serialises as a single chain" + ;; SVG `d` originally split a continuous polyline by inserting a + ;; redundant moveto at the elbow. Importing it must collapse the + ;; first two subpaths so that stroke-linejoin renders the rounded tip. + (let [content (path/from-string + (str "M350.5,1846 L365.5,1846" + " M365.5,1846 L358.75,1839.25" + " M365.5,1846 L358.75,1852.75")) + merged (path/merge-touching-subpaths content) + rendered (str merged)] + (t/is (path/content? merged)) + ;; First two subpaths fold into M ... L ... L ... ; third stays + ;; separate (its start point matches the original M, not the merged + ;; chain's tail), so exactly two M commands remain. + (t/is (= 2 (count (re-seq #"M" rendered)))) + (t/is (= 3 (count (re-seq #"L" rendered))))))) + (t/deftest path-move-content (let [content (path/content sample-content-square) move-vec (gpt/point 3.0 4.0) From 05b47605830c5576cfdb46b4d6e243b46055c8a3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 29 Apr 2026 14:26:53 +0000 Subject: [PATCH 252/288] :books: Set clearer expectations for PR reviews and prior discussion Explicitly state that the team is small and reviews may take a few days as they are handled in dedicated time blocks. Remove mention of GitHub Discussions since it is not used. Reword the discussion requirement to manage expectations: do not expect a PR to be accepted without prior discussion. Signed-off-by: Andrey Antukh --- CONTRIBUTING.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d733ea5c7a..532413194d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,12 +63,11 @@ Advisories](https://github.com/penpot/penpot/security/advisories) 1. **Read the DCO** — see [Developer's Certificate of Origin](#developers-certificate-of-origin-dco) below. All code patches must include a `Signed-off-by` line. 2. **Discuss before building** — open a [GitHub - Issue](https://github.com/penpot/penpot/issues) or start a [GitHub - Discussion](https://github.com/penpot/penpot/discussions) before starting - work on a new feature or significant change. For planned features on the - roadmap, reference the corresponding Taiga story. No PR will be accepted - without prior discussion, whether it is a new feature, a planned one, or a - quick win. + Issue](https://github.com/penpot/penpot/issues) before starting work on + a new feature or significant change. For planned features on the roadmap, + reference the corresponding Taiga story. Do not expect your contribution + to be accepted if you submit it without prior discussion — this applies + to new features, planned features, and quick wins alike. 3. **Bug fixes** — you may submit a PR directly, but we still recommend filing an issue first so we can track it independently of your fix. 4. **Format and lint** — run the checks described in @@ -136,7 +135,11 @@ refactor/layout-sizing ### Review process -- Maintainers review PRs when time permits. Please be patient. +- We are a small team and maintainers juggle reviews alongside other + tasks. Please do not expect your code to be reviewed instantly. +- Reviews are handled in dedicated blocks of time, usually in the order + PRs arrive. It may take a few days to get a first review, especially + when urgent tasks come up. - Address review feedback by **pushing new commits** — do not force-push during review, as it breaks comment threads. - PRs require at least **one approval** before merge. From 5e3e66a99b6b4838e9b8333cc91f53ecf65f13a9 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 29 Apr 2026 16:12:28 +0200 Subject: [PATCH 253/288] :books: Updated docs for MCP development environment --- docs/technical-guide/developer/devenv.md | 53 ++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/technical-guide/developer/devenv.md b/docs/technical-guide/developer/devenv.md index facb810dd8..0443466e03 100644 --- a/docs/technical-guide/developer/devenv.md +++ b/docs/technical-guide/developer/devenv.md @@ -161,6 +161,59 @@ If an exception is raised or an error occurs when code is reloaded, just use (repl/refresh-all) to finish loading the code correctly and then use (restart) again. + +### MCP Server + +To set up the MCP server local development environment it's needed some additional steps. + +### Activate the MCP features variables + +Create or modify the file `frontend/resources/public/js/config.js` and add (or modify) the `penpotFlags` to add the following: + +```javascript +var penpotFlags = "enable-mcp enable-access-tokens" +``` + +This will enable the MCP in the workspace and in the user settings profile. + +### Start the DEVENV + +Start as usual the development environment + +``` +./manage.sh start-devenv +``` + +Once the TMUX is showing, create a new tmux tab (Ctrl+b c). And in the new tab run: + +```bash +cd mcp +pnpm run bootstrap:multi-user +``` + +This will start the MCP server and the multi-user plugin that will be loaded automaticaly by Penpot. + +There is a NGINX proxy that makes a proxy-pass from outside the docker container so you don't need to remember the ports it's using. + +### Configure the MCP in your tool + +You can use the instructions in [/mcp/#remote-mcp-in-5-steps](/mcp/#remote-mcp-in-5-steps) to setup the server. + +Warning: by default Cursor won't support HTTPS with a self-signed certificate. In order to work around this issue please use the port `3450` that uses an standard `http` protocol + +An example of your cursor configuration can be: + +```javascript +{ + "mcpServers": { + "penpot-devenv": { + "url": "http://localhost:3450/mcp/stream?userToken=TOKEN", + "type": "http" + } + } +} +``` + ## Email To test email sending, the devenv includes [MailCatcher](https://mailcatcher.me/), From 510a015424b6b98529dba19cc72bdf002b8ff83a Mon Sep 17 00:00:00 2001 From: Renzo <170978465+RenzoMXD@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:44:49 -0600 Subject: [PATCH 254/288] :bug: Fix dashboard modal clipping behind sidebar (#9233) Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com> --- CHANGES.md | 1 + frontend/src/app/main/ui/dashboard/sidebar.scss | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index dce36f8942..072b424ba8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -53,6 +53,7 @@ ### :bug: Bugs fixed +- Fix release notes modal appearing behind the dashboard sidebar [Github #8296](https://github.com/penpot/penpot/issues/8296) - Fix plugin API `fileVersion.restore()` promise hanging indefinitely on restore failure [Github #9092](https://github.com/penpot/penpot/issues/9092) - Fix imported stroke-only SVG paths losing their rounded join when authoring tools (e.g. Figma → Heroicons) split a continuous polyline into adjacent `M…L M…L` subpaths sharing an endpoint; on import these are now folded back into one chain so `stroke-linejoin` renders the elbow correctly in both editor and exports [Github #5283](https://github.com/penpot/penpot/issues/5283) - Fix plugin API `library.connectLibrary()` returning a non-Promise (or throwing synchronously) when the plugin lacks `library:write` permission — the method now always returns a `Promise` and rejects with a structured error message, matching the contract used by every other Promise-returning plugin method (`restore`, `remove`, `pin`, `saveVersion`, `findVersions`, …) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index 395c882b86..e4bb79eea6 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -26,7 +26,7 @@ margin: 0 var(--sp-l) 0 0; border-right: $b-1 solid var(--panel-border-color); background-color: var(--panel-background-color); - z-index: var(--z-index-dropdown); + z-index: var(--z-index-panels); } // SIDEBAR CONTENT COMPONENT From 22b85f1a9251f948ec0047bc0af2fd0487d7fc57 Mon Sep 17 00:00:00 2001 From: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:57:59 -0400 Subject: [PATCH 255/288] :sparkles: Show specific error messages for invitation token failures (#9223) * :sparkles: Show specific error messages for invitation token failures Surface distinct error messages for the three invitation-token failure modes that the backend already distinguishes: email mismatch, expired token, and invalid/corrupted token. Replaces the single generic could not accept invitation message with actionable text so the user knows what went wrong and how to recover. Signed-off-by: jack-stormentswe * :lipstick: Update CHANGE.md Signed-off-by: jack-stormentswe * :lipstick: Address review feedback on invitation-error messages Signed-off-by: jack-stormentswe --------- Signed-off-by: jack-stormentswe Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + backend/src/app/rpc/commands/verify_token.clj | 1 + .../src/app/main/ui/auth/verify_token.cljs | 25 +++++++++++++------ frontend/src/app/main/ui/static.cljs | 22 +++++++++++++--- frontend/translations/en.po | 8 ++++++ 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 072b424ba8..14f1699c0b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,7 @@ - Preserve vector content when pasting from external tools such as Inkscape: recognise SVG sent as text/plain (with optional XML declaration and HTML comments), skip the raster preview when an SVG sibling is on the clipboard, and ignore empty SVG blobs that some tools advertise alongside the real payload, so pasted graphics arrive editable without spurious "SVG is invalid" warnings [Github #546](https://github.com/penpot/penpot/issues/546) - Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts [Github #2457](https://github.com/penpot/penpot/issues/2457) - Adds a **Pixel grid color** picker in the viewport settings, next to the existing canvas color control [Github #7750](https://github.com/penpot/penpot/issues/7750) +- Show specific invitation-link error messages instead of a single generic "Invite invalid" page: distinguish expired invitations, email-mismatch (signed in with the wrong account) and corrupted/invalid tokens, each with an actionable recovery hint [Github #9220](https://github.com/penpot/penpot/issues/9220) ### :bug: Bugs fixed diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index cc270b3ded..e25be628ad 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -190,6 +190,7 @@ (= member-email (:email profile))) (ex/raise :type :validation :code :invalid-token + :reason :email-mismatch :hint "logged-in user does not matches the invitation")) (when (:is-member membership) diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs index 6ac2a1b60f..a93954ace3 100644 --- a/frontend/src/app/main/ui/auth/verify_token.cljs +++ b/frontend/src/app/main/ui/auth/verify_token.cljs @@ -68,8 +68,15 @@ (mf/defc verify-token* [{:keys [route]}] - (let [token (get-in route [:query-params :token]) - bad-token (mf/use-state false)] + (let [token (get-in route [:query-params :token]) + ;; Holds the specific failure reason when the token fails, or + ;; nil while still loading / on success. Any non-nil keyword is + ;; truthy, so this single state replaces the previous pair of + ;; (bad-token? + bad-token-reason) hooks. Reasons: + ;; :token-expired -> JWT past its :exp + ;; :email-mismatch -> invitation email != logged-in email + ;; :invalid-token -> corrupted / unknown / fallback + bad-token-reason (mf/use-state nil)] (mf/with-effect [] (dom/set-html-title (tr "title.default")) @@ -78,7 +85,7 @@ (fn [tdata] (handle-token tdata)) (fn [cause] - (let [{:keys [type code team-id] :as error} (ex-data cause)] + (let [{:keys [type code team-id reason] :as error} (ex-data cause)] (cond (= :invalid-token-already-member code) (st/emit! @@ -91,8 +98,12 @@ (or (= :validation type) (= :invalid-token code) - (= :token-expired (:reason error))) - (reset! bad-token true) + (= :token-expired reason)) + (reset! bad-token-reason + (cond + (= :token-expired reason) :token-expired + (= :email-mismatch reason) :email-mismatch + :else :invalid-token)) (= :email-already-exists code) (let [msg (tr "errors.email-already-exists")] @@ -109,8 +120,8 @@ (ts/schedule 100 #(st/emit! (ntf/error msg))) (st/emit! (rt/nav :auth-login))))))))) - (if @bad-token - [:> static/invalid-token {}] + (if @bad-token-reason + [:> static/invalid-token {:reason @bad-token-reason}] [:> loader* {:title (tr "labels.loading") :overlay true}]))) diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index a13d6f76d8..f426ae0874 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -67,10 +67,26 @@ [:span (tr "not-found.made-with-love")]]])) (mf/defc invalid-token - [] + [{:keys [reason]}] + ;; Map the specific failure reason to actionable copy. Falls back to + ;; the generic invitation-invalid message when the reason is missing + ;; or unknown so the UX never regresses for unhandled cases. + ;; + ;; The branches use `tr` with literal keys (instead of `(tr key-var)`) + ;; so the i18n usage scanner can statically track every key. [:> error-container* {} - [:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")] - [:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]]) + (case reason + :email-mismatch + [:* + [:div {:class (stl/css :main-message)} (tr "errors.invite-email-mismatch")]] + + :token-expired + [:* + [:div {:class (stl/css :main-message)} (tr "errors.invite-expired")]] + + [:* + [:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")] + [:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]])]) (mf/defc login-modal* {::mf/private true} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e04a501a56..f3675e7c0e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1545,6 +1545,14 @@ msgstr "Invite invalid" msgid "errors.invite-invalid.info" msgstr "This invite might be canceled or may be expired." +#: src/app/main/ui/static.cljs +msgid "errors.invite-expired" +msgstr "This invitation has expired. Ask the team owner to send you a new one." + +#: src/app/main/ui/static.cljs +msgid "errors.invite-email-mismatch" +msgstr "This invitation is for a different email. Log out and sign in with the invited account, or ask the team owner for a new invitation." + #: src/app/main/ui/auth/login.cljs:89 msgid "errors.ldap-disabled" msgstr "LDAP authentication is disabled." From 8821ada1bba3d6b17336fc17da2a3b42dd240edc Mon Sep 17 00:00:00 2001 From: Renzo <170978465+RenzoMXD@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:58:38 -0600 Subject: [PATCH 256/288] :bug: Suppress browser context menu on empty workspace sidebar space (#9196) Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com> Signed-off-by: Renzo <170978465+RenzoMXD@users.noreply.github.com> Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + frontend/src/app/main/ui/workspace/sidebar.cljs | 3 +++ frontend/src/app/util/dom.cljs | 8 ++++++++ 3 files changed, 12 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 14f1699c0b..b9b42c551b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -54,6 +54,7 @@ ### :bug: Bugs fixed +- Suppress the browser context menu when right-clicking empty space in the workspace sidebars while preserving it on text inputs so paste/select-all still work [Github #5127](https://github.com/penpot/penpot/issues/5127) - Fix release notes modal appearing behind the dashboard sidebar [Github #8296](https://github.com/penpot/penpot/issues/8296) - Fix plugin API `fileVersion.restore()` promise hanging indefinitely on restore failure [Github #9092](https://github.com/penpot/penpot/issues/9092) - Fix imported stroke-only SVG paths losing their rounded join when authoring tools (e.g. Figma → Heroicons) split a continuous polyline into adjacent `M…L M…L` subpaths sharing an endpoint; on import these are now folded back into one chain so `stroke-linejoin` renders the elbow correctly in both editor and exports [Github #5283](https://github.com/penpot/penpot/issues/5283) diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index ae43b60052..b1bfe74db9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -42,6 +42,7 @@ [app.main.ui.workspace.sidebar.versions :refer [versions-toolbox*]] [app.main.ui.workspace.tokens.sidebar :refer [tokens-sidebar-tab*]] [app.util.debug :as dbg] + [app.util.dom :as dom] [app.util.i18n :refer [tr]] [potok.v2.core :as ptk] [rumext.v2 :as mf])) @@ -183,6 +184,7 @@ :data-testid "left-sidebar" :data-width (str width) :class aside-class + :on-context-menu dom/prevent-default-context-menu :style {:--left-sidebar-width (dm/str width "px")}} [:> left-header* {:file file @@ -329,6 +331,7 @@ :id "right-sidebar-aside" :data-testid "right-sidebar" :data-size (str width) + :on-context-menu dom/prevent-default-context-menu :style {:--right-sidebar-width (if can-be-expanded? (dm/str width "px") (dm/str right-sidebar-default-width "px"))}} diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index aa46be4497..2ce67382b1 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -122,6 +122,14 @@ (fn? (.-preventDefault event))) (.preventDefault event))) +(defn prevent-default-context-menu + [^js event] + (let [target (some-> event .-target) + tag (some-> target .-tagName .toLowerCase)] + (when-not (or (#{"input" "textarea"} tag) + (some-> target .-isContentEditable)) + (.preventDefault event)))) + (defn get-target "Extract the target from event instance." [^js event] From 710fd30f7891ad085e3d2c2e195e4cf51d44ff4e Mon Sep 17 00:00:00 2001 From: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:07:48 -0400 Subject: [PATCH 257/288] :bug: Preserve renamed layer name when re-entering edit mode (#9231) * :bug: Preserve renamed layer name when re-entering edit mode When a layer was renamed and the user clicked its name again to edit it, the input opened with the type-based default name instead of the user's saved name. Pressing Enter then silently overwrote the saved name with the default. Read the current shape :name when seeding the rename input so the user's previous rename is preserved. Signed-off-by: jack-stormentswe * :lipstick: Remove redundant DOM-refresh effect from layer rename input Signed-off-by: jack-stormentswe --------- Signed-off-by: jack-stormentswe Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index b9b42c551b..c79df35331 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -54,6 +54,7 @@ ### :bug: Bugs fixed +- Fix layers-panel rename input opening with the type-based default (e.g. "Ellipse") instead of the user's saved name when re-entering edit mode on a previously renamed layer; the silent revert could overwrite the saved name on confirm. The `default-value` `mf/with-memo` was missing `shape-name` from its dependency list, so once the memo cached the original default it never refreshed. Adds `shape-name` to the deps and force-syncs the input's DOM value on every entry into edit mode [Github #9230](https://github.com/penpot/penpot/issues/9230) - Suppress the browser context menu when right-clicking empty space in the workspace sidebars while preserving it on text inputs so paste/select-all still work [Github #5127](https://github.com/penpot/penpot/issues/5127) - Fix release notes modal appearing behind the dashboard sidebar [Github #8296](https://github.com/penpot/penpot/issues/8296) - Fix plugin API `fileVersion.restore()` promise hanging indefinitely on restore failure [Github #9092](https://github.com/penpot/penpot/issues/9092) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs index 5c0f181c1d..181ea70d47 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs @@ -38,7 +38,7 @@ shape-name) default-value - (mf/with-memo [variant-id variant-error variant-properties] + (mf/with-memo [variant-id variant-error variant-properties shape-name] (if variant-id (or variant-error (ctv/properties-map->formula variant-properties)) shape-name)) From f530a0ba2600c6bb79cc7f45afc8ae5a813b25f6 Mon Sep 17 00:00:00 2001 From: Dexterity <173429049+Dexterity104@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:47:27 -0400 Subject: [PATCH 258/288] :fire: Remove stray debug log in color-row component (#9243) --- .../app/main/ui/workspace/sidebar/options/rows/color_row.cljs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index dd169030d6..229deb9460 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -352,10 +352,6 @@ :dnd-over-top (= (:over dprops) :top) :dnd-over-bot (= (:over dprops) :bot))] - (when (= applied-token :multiple) - ;; (js/console.trace "color-row*") - (prn "color-row*" index color applied-token)) - (mf/with-effect [color prev-color disable-picker] (when (and (not disable-picker) (not= prev-color color)) (modal/update-props! :colorpicker {:data (parse-color color)}))) From 1213640693515494128afe4532271d2e293cb13d Mon Sep 17 00:00:00 2001 From: Dexterity <173429049+Dexterity104@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:48:07 -0400 Subject: [PATCH 259/288] :bug: Fix typo in restore-deleted-team-files reduce accumulator (#9241) --- backend/src/app/rpc/commands/files.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 3a71359aab..0ef8bec466 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -1284,7 +1284,7 @@ (update :files conj id) (update :projects conj project-id)))) - {:files #{} :projectes #{}} + {:files #{} :projects #{}} (db/plan conn [sql:resolve-editable-files team-id (db/create-array conn "uuid" ids)]))] From b5cd4d96ee83a6509cbc60a8305d8a0ea0a86c34 Mon Sep 17 00:00:00 2001 From: Ingrid Pigueron Date: Tue, 28 Apr 2026 20:18:52 +0200 Subject: [PATCH 260/288] :globe_with_meridians: Add translations for: French Currently translated at 98.8% (2050 of 2074 strings) Translation: Penpot/frontend Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/ --- frontend/translations/fr.po | 210 +++++++++++++++++++++++++++++++++++- 1 file changed, 205 insertions(+), 5 deletions(-) diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 066e6d9ad8..82a64b5bff 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"PO-Revision-Date: 2026-04-28 15:09+0000\n" +"PO-Revision-Date: 2026-04-29 17:10+0000\n" "Last-Translator: Ingrid Pigueron \n" "Language-Team: French \n" @@ -2060,7 +2060,7 @@ msgstr "" #: src/app/main/ui/inspect/right_sidebar.cljs:166 msgid "inspect.layer-info" -msgstr "Info sur la couche" +msgstr "Infos sur le calque" #: src/app/main/ui/inspect/right_sidebar.cljs:137 msgid "inspect.multiple-selected" @@ -4920,8 +4920,8 @@ msgstr[1] "Il y a actuellement %s personnes dans vos équipe qui peuvent être #: src/app/main/ui/settings/subscription.cljs:230 msgid "subscription.settings.management.dialog.downgrade" msgstr "" -"Attention : changer vers un abonnement plus bas signifie moins de stockage " -"et des sauvegardes et des version d'historique plus courtes." +"Attention : en passant à un abonnement inférieur, le stockage est moins " +"important et les sauvegardes et les version d'historique sont plus courtes." #: src/app/main/ui/settings/subscription.cljs:211 msgid "subscription.settings.management.dialog.editors" @@ -7356,7 +7356,7 @@ msgstr "Copier en CSS" #: src/app/main/ui/workspace/context_menu.cljs:220 msgid "workspace.shape.menu.copy-css-nested" -msgstr "Copier en CSS (couches imbriquées)" +msgstr "Copier en CSS (calques imbriqués)" #: src/app/main/ui/workspace/context_menu.cljs:203 msgid "workspace.shape.menu.copy-link" @@ -8655,3 +8655,203 @@ msgstr "WebGL ne fonctionne plus. Rechargez la page pour le réinitialiser" #: src/app/main/ui/static.cljs:314 msgid "errors.webgl-context-lost.main-message" msgstr "Oups ! Le contexte du canevas a été perdu" + +#: src/app/main/ui/dashboard/subscription.cljs:196 +msgid "subscription.dashboard.professional-dashboard-cta-title" +msgstr "" +"Il y a %s éditeurs dans les équipes dont vous êtes propriétaire alors que " +"votre abonnement Professionnel permet d'en avoir jusqu'à 8." + +#: src/app/main/ui/dashboard/subscription.cljs:184 +msgid "subscription.dashboard.unlimited-members-extra-editors-cta-text" +msgstr "" +"Seuls les nouveaux éditeurs dans les équipes dont vous êtes propriétaire " +"sont pris en compte pour la facturation future. Un forfait de 175 $/mois " +"s'applique toujours au-delà de 25 éditeurs." + +#: src/app/main/ui/settings/subscription.cljs:298 +msgid "subscription.settings.management-dialog.step-2-add-payment-button" +msgstr "Ajouter les détails de paiement" + +#: src/app/main/ui/settings/subscription.cljs:285 +msgid "subscription.settings.management-dialog.step-2-description" +msgstr "" +"Ajoutez vos détails de paiement dès maintenant pour que votre abonnement ne " +"soit pas interrompu après la période d'essai et pour continuer à soutenir " +"notre projet Open Source. Vous n'allez pas être facturé pour le moment." + +#: src/app/main/ui/settings/subscription.cljs:293 +msgid "subscription.settings.management-dialog.step-2-skip-button" +msgstr "Ignorer pour le moment et commencer l'essai" + +#: src/app/main/ui/settings/subscription.cljs:203 +msgid "subscription.settings.management-dialog.step-2-title" +msgstr "Aidez-nous à nous développer et à faciliter votre essai" + +#: src/app/main/ui/settings/subscription.cljs:263 +msgid "subscription.settings.management.dialog.input-error" +msgstr "" +"Vous pouvez réduire votre nombre actuel d'éditeurs. Dans les paramètres de " +"l'équipe, modifiez le rôle (d'éditeur/admin en spectateur) des personnes qui " +"ne modifient pas vraiment des fichiers." + +#: src/app/main/ui/settings/subscription.cljs:266 +msgid "subscription.settings.management.dialog.unlimited-capped-warning" +msgstr "" +"Conseil : vous pouvez augmenter le nombre de vos utilisateurs dès maintenant " +"pour anticiper les invitations. Au-delà de 25 éditeurs dans vos équipes, un " +"forfait de 175 $/mois vous sera facturé." + +#: src/app/main/ui/workspace/sidebar/versions.cljs:58 +#, markdown +msgid "subscription.workspace.versions.warning.subtext-owner" +msgstr "" +"Si vous souhaitez augmenter cette limite, [mettez votre abonnement à " +"niveau|target:self](%s)" + +#: src/app/main/ui/viewer/header.cljs:187 +msgid "viewer.header.edit-in-workspace" +msgstr "Modifier dans l'espace de travail" + +#: src/app/main/ui/workspace/sidebar/debug.cljs:38 +msgid "workspace.debug.title" +msgstr "Outils de débogage" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:449 +msgid "workspace.layout-item.fit-content-horizontal" +msgstr "Adapter le contenu (horizontalement)" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:477 +msgid "workspace.layout-item.fit-content-vertical" +msgstr "Adapter le contenu (verticalement)" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:567 +msgid "workspace.options.component.variant.duplicated.copy.title" +msgstr "" +"Ce composant comporte des variantes en conflit. Assurez-vous que chaque " +"variante possède une collection de valeurs de propriétés unique." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:267 +msgid "workspace.options.component.variant.duplicated.single.all" +msgstr "" +"Ces variantes possèdent des propriétés et des valeurs identiques. Ajustez " +"les valeurs afin de pouvoir les extraire." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:557 +msgid "workspace.options.component.variant.malformed.copy" +msgstr "" +"Ce composant comporte des variantes dont le nom n'est pas valide. Assurez-" +"vous que chaque variante respecte la structure appropriée." + +#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:91 +msgid "workspace.options.component.variants-help-modal.outro" +msgstr "" +"Si vous modifiez l'un de ces éléments (par exemple, en renommant ou en " +"regroupant un calque), la connexion est rompue. Si vous annulez la " +"modification, la connexion est rétablie." + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:129 +msgid "workspace.tokens.font-size-value-enter" +msgstr "Taille de police ou {alias}" + +#: src/app/main/data/workspace/tokens/application.cljs:325 +msgid "workspace.tokens.font-variant-not-found" +msgstr "" +"Erreur lors de la définition de la graisse/du style de la police. Ce style " +"de police n'existe pas dans la police active" + +#: src/app/main/data/workspace/tokens/errors.cljs:93 +msgid "workspace.tokens.invalid-font-family-token-value" +msgstr "" +"Valeur de token non valide : vous ne pouvez référencer qu'un token de " +"famille de polices" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:153 +msgid "workspace.tokens.letter-spacing-value-enter-composite" +msgstr "Interlettrage ou {alias}" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs +#, unused +msgid "workspace.tokens.no-remap-needed" +msgstr "" +"Ce token n'est pas utilisé actuellement dans votre conception. Aucun " +"remappage n'est donc nécessaire." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:95 +msgid "workspace.tokens.not-remap" +msgstr "Ne pas remapper" + +#: src/app/main/ui/workspace/tokens/management/forms/typography.cljs:178 +msgid "workspace.tokens.reference-composite" +msgstr "Entrer un alias de typographie pour un token" + +#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:204 +msgid "workspace.tokens.reference-composite-shadow" +msgstr "Entrer un alias d'ombre pour un token" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:99 +msgid "workspace.tokens.remap" +msgstr "Remapper des tokens" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:86 +msgid "workspace.tokens.remap-token-references-title" +msgstr "Remapper tous les tokens qui utilisent «  %s  » dans « %s » ?" + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:88 +msgid "workspace.tokens.remap-warning-effects" +msgstr "" +"Tous les calques et les références utilisant l'ancien nom du token vont être " +"modifiés." + +#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:92 +#, unused +msgid "workspace.tokens.remapping-in-progress" +msgstr "Remappage des références du token..." + +#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +#, unused +msgid "workspace.tokens.warning-name-change" +msgstr "Si vous renommez ce token, toute référence à son ancien nom sera rompue" + +#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:261 +msgid "labels.switch" +msgstr "Changer" + +#: src/app/main/ui/dashboard/subscription.cljs:84 +msgid "subscription.dashboard.power-up.professional.bottom-button" +msgstr "Passez à un forfait supérieur !" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:465 +msgid "workspace.layout-item.fix-height" +msgstr "Corriger la hauteur" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:439 +msgid "workspace.layout-item.fix-width" +msgstr "Corriger la largeur" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:570 +msgid "workspace.options.component.variant.duplicated.copy.locate" +msgstr "Chercher les variantes en conflit" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:616 +msgid "workspace.options.interaction-animation-direction-down" +msgstr "Vers le bas" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:593 +msgid "workspace.options.interaction-animation-direction-in" +msgstr "À l'intérieur" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:596 +msgid "workspace.options.interaction-animation-direction-out" +msgstr "À l'extérieur" + +#: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs:265 +msgid "workspace.options.more-token-colors" +msgstr "Plus de tokens de couleur" + +#: src/app/main/data/workspace/tokens/errors.cljs:89 +msgid "workspace.tokens.invalid-font-weight-token-value" +msgstr "" +"Valeur de graisse de la police non valide : utilisez des valeurs numériques " +"(100 à 950) ou des noms standard (thin, light, regular, bold, etc.) " +"éventuellement suivi de la mention « Italic »" From 404ebcc63e4e19c15f6a45e41ed18f8a64fe86ea Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 29 Apr 2026 17:31:59 +0000 Subject: [PATCH 261/288] :lipstick: Fix SCSS linter errors in v2_15 release modal styles Replace class selectors with placeholder selectors for @extend (.modal-overlay-base -> %modal-overlay-base, .button-primary -> %button-primary). Add blank lines after @include mixin calls in 6 rulesets (.version-tag, .modal-title, .feature-title, .feature-content, .feature-list, .next-btn) to satisfy the declaration-empty-line-before stylelint rule. Fixes 8 SCSS linter errors in total. Signed-off-by: Andrey Antukh --- frontend/src/app/main/ui/releases/v2_15.scss | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/main/ui/releases/v2_15.scss b/frontend/src/app/main/ui/releases/v2_15.scss index e5d13841eb..68603d9658 100644 --- a/frontend/src/app/main/ui/releases/v2_15.scss +++ b/frontend/src/app/main/ui/releases/v2_15.scss @@ -7,7 +7,7 @@ @use "refactor/common-refactor.scss" as deprecated; .modal-overlay { - @extend .modal-overlay-base; + @extend %modal-overlay-base; } .modal-container { @@ -44,6 +44,7 @@ .version-tag { @include deprecated.flexCenter; @include deprecated.headlineSmallTypography; + height: deprecated.$s-32; width: deprecated.$s-96; background-color: var(--communication-tag-background-color); @@ -53,6 +54,7 @@ .modal-title { @include deprecated.headlineLargeTypography; + color: var(--modal-title-foreground-color); } @@ -71,17 +73,20 @@ .feature-title { @include deprecated.bodyLargeTypography; + color: var(--modal-title-foreground-color); } .feature-content { @include deprecated.bodyMediumTypography; + margin: 0; color: var(--modal-text-foreground-color); } .feature-list { @include deprecated.bodyMediumTypography; + color: var(--modal-text-foreground-color); list-style: disc; display: grid; @@ -95,7 +100,8 @@ } .next-btn { - @extend .button-primary; + @extend %button-primary; + width: deprecated.$s-100; justify-self: flex-end; grid-area: button; From 346614edc3d0282f9664f79e3d555c3ab88cf9e3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 29 Apr 2026 17:36:20 +0000 Subject: [PATCH 262/288] :bug: Fix SCSS mixin names in v2_15 release modal styles Rename 5 deprecated mixin calls from camelCase to kebab-case to match the actual mixin names defined in common-refactor.scss: - deprecated.flexCenter -> deprecated.flex-center - deprecated.headlineSmallTypography -> deprecated.headline-small-typography - deprecated.headlineLargeTypography -> deprecated.headline-large-typography - deprecated.bodyLargeTypography -> deprecated.body-large-typography - deprecated.bodyMediumTypography -> deprecated.body-medium-typography This fixes the "Undefined mixin" SCSS compilation error. Signed-off-by: Andrey Antukh --- frontend/src/app/main/ui/releases/v2_15.scss | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/main/ui/releases/v2_15.scss b/frontend/src/app/main/ui/releases/v2_15.scss index 68603d9658..40c8f5316f 100644 --- a/frontend/src/app/main/ui/releases/v2_15.scss +++ b/frontend/src/app/main/ui/releases/v2_15.scss @@ -42,8 +42,8 @@ } .version-tag { - @include deprecated.flexCenter; - @include deprecated.headlineSmallTypography; + @include deprecated.flex-center; + @include deprecated.headline-small-typography; height: deprecated.$s-32; width: deprecated.$s-96; @@ -53,7 +53,7 @@ } .modal-title { - @include deprecated.headlineLargeTypography; + @include deprecated.headline-large-typography; color: var(--modal-title-foreground-color); } @@ -72,20 +72,20 @@ } .feature-title { - @include deprecated.bodyLargeTypography; + @include deprecated.body-large-typography; color: var(--modal-title-foreground-color); } .feature-content { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; margin: 0; color: var(--modal-text-foreground-color); } .feature-list { - @include deprecated.bodyMediumTypography; + @include deprecated.body-medium-typography; color: var(--modal-text-foreground-color); list-style: disc; From fc414b23d21cf88416799610a8308166acdfca2e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 29 Apr 2026 17:59:33 +0000 Subject: [PATCH 263/288] :books: Update changelog with entries for 2.17.0 Add 3 new features/enhancements (file import errors, read-only version preview, clipboard permissions) and 4 bug fixes (restore typo, layer sibling selection, tooltip duplication, library notification link) to the 2.17.0 changelog section. Signed-off-by: Andrey Antukh --- CHANGES.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 17b653cd26..d7c499c5f4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -51,6 +51,9 @@ - Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts [Github #2457](https://github.com/penpot/penpot/issues/2457) - Adds a **Pixel grid color** picker in the viewport settings, next to the existing canvas color control [Github #7750](https://github.com/penpot/penpot/issues/7750) - Show specific invitation-link error messages instead of a single generic "Invite invalid" page: distinguish expired invitations, email-mismatch (signed in with the wrong account) and corrupted/invalid tokens, each with an actionable recovery hint [Github #9220](https://github.com/penpot/penpot/issues/9220) +- Show detailed messages on file import errors to help diagnose why a file could not be imported (by @jsdevninja) [Github #9004](https://github.com/penpot/penpot/issues/9004) +- Add read-only preview mode for saved versions — click a version name to open a dedicated preview view (by @wdeveloper16) [Github #8976](https://github.com/penpot/penpot/issues/8976) +- Add clipboard read/write permissions to the plugin system (by @wdeveloper16) [Github #9053](https://github.com/penpot/penpot/issues/9053) ### :bug: Bugs fixed @@ -97,7 +100,10 @@ - Fix plugin `addInteraction` silently rejecting `open-overlay` actions with `manualPositionLocation` [Github #8409](https://github.com/penpot/penpot/issues/8409) - Fix typography style creation with tokenized line-height (by @juan-flores077) [Github #8479](https://github.com/penpot/penpot/issues/8479) - Fix colorpicker layout so the eyedropper button is visible again [Taiga #14057](https://tree.taiga.io/project/penpot/issue/14057) - +- Fix restore-deleted-team-files failing due to a typo in the reduce accumulator (by @Dexterity104) [Github #9241](https://github.com/penpot/penpot/issues/9241) +- Fix internal error on layer prev/next sibling selection (by @jsdevninja) [Github #9003](https://github.com/penpot/penpot/issues/9003) +- Fix tooltip appearing two times when nested elements [Github #9031](https://github.com/penpot/penpot/issues/9031) +- Fix broken update library notification link in the UI [Github #9070](https://github.com/penpot/penpot/issues/9070) ## 2.16.0 (Unreleased) From 25c5bb2019cb7917fd5fe81adefb17156114c49a Mon Sep 17 00:00:00 2001 From: Dexterity <173429049+Dexterity104@users.noreply.github.com> Date: Thu, 30 Apr 2026 02:35:02 -0400 Subject: [PATCH 264/288] :zap: Restore deleted team files in bulk instead of per file (#9248) --- backend/src/app/rpc/commands/files.clj | 74 ++++++++++++++------------ 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 0ef8bec466..a2075a2807 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -1226,38 +1226,43 @@ AND t.id = ? AND f.id = ANY(?::uuid[])") +(def ^:private sql:restore-files + "UPDATE file SET deleted_at = null, has_media_trimmed = false + WHERE id = ANY(?::uuid[])") + +(def ^:private sql:restore-file-media-objects + "UPDATE file_media_object SET deleted_at = null + WHERE file_id = ANY(?::uuid[])") + +(def ^:private sql:restore-file-changes + "UPDATE file_change SET deleted_at = null + WHERE file_id = ANY(?::uuid[])") + +(def ^:private sql:restore-file-data + "UPDATE file_data SET deleted_at = null + WHERE file_id = ANY(?::uuid[])") + +(def ^:private sql:restore-file-thumbnails + "UPDATE file_thumbnail SET deleted_at = null + WHERE file_id = ANY(?::uuid[])") + +(def ^:private sql:restore-file-tagged-object-thumbnails + "UPDATE file_tagged_object_thumbnail SET deleted_at = null + WHERE file_id = ANY(?::uuid[])") + +(defn- restore-files + [conn file-ids] + (let [file-ids (db/create-array conn "uuid" file-ids)] + (db/exec-one! conn [sql:restore-files file-ids]) + (db/exec-one! conn [sql:restore-file-media-objects file-ids]) + (db/exec-one! conn [sql:restore-file-changes file-ids]) + (db/exec-one! conn [sql:restore-file-data file-ids]) + (db/exec-one! conn [sql:restore-file-thumbnails file-ids]) + (db/exec-one! conn [sql:restore-file-tagged-object-thumbnails file-ids]))) + (defn- restore-file [conn file-id] - (db/update! conn :file - {:deleted-at nil - :has-media-trimmed false} - {:id file-id} - {::db/return-keys false}) - - (db/update! conn :file-media-object - {:deleted-at nil} - {:file-id file-id} - {::db/return-keys false}) - - (db/update! conn :file-change - {:deleted-at nil} - {:file-id file-id} - {::db/return-keys false}) - - (db/update! conn :file-data - {:deleted-at nil} - {:file-id file-id} - {::db/return-keys false}) - - (db/update! conn :file-thumbnail - {:deleted-at nil} - {:file-id file-id} - {::db/return-keys false}) - - (db/update! conn :file-tagged-object-thumbnail - {:deleted-at nil} - {:file-id file-id} - {::db/return-keys false})) + (restore-files conn [file-id])) (def ^:private sql:restore-projects "UPDATE project SET deleted_at = null WHERE id = ANY(?::uuid[])") @@ -1278,17 +1283,18 @@ (reduce (fn [result {:keys [id project-id]}] (let [index (-> result :files count)] (events/tap :progress {:file-id id :index (inc index) :total total-files}) - (restore-file conn id) - (-> result (update :files conj id) (update :projects conj project-id)))) - {:files #{} :projects #{}} (db/plan conn [sql:resolve-editable-files team-id (db/create-array conn "uuid" ids)]))] - (restore-projects conn projects) + (when (seq files) + (restore-files conn files)) + + (when (seq projects) + (restore-projects conn projects)) files)) From 400414776be1d15da0bec50369909c85908178eb Mon Sep 17 00:00:00 2001 From: TinyClaw Date: Thu, 30 Apr 2026 08:37:00 +0200 Subject: [PATCH 265/288] :bug: Fix :heigth typo in clipboard frame-same-size? (#9250) The height comparison in frame-same-size? used the misspelled keyword :heigth on both sides. (:heigth selrect) returns nil for any selrect, so (= nil nil) is always true and the function degenerated to a width-only comparison. Result: the 'paste next to selected frame' branch in clipboard.cljs fired whenever pasted-content width matched a target frame's width, even if the heights differed. Introduced in #9033 (:sparkles: Add paste to replace (Cmd+Shift+V)). Signed-off-by: iot2edge Co-authored-by: iot2edge --- frontend/src/app/main/data/workspace/clipboard.cljs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index 72f94ecdc4..e5d6dbd19b 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -547,8 +547,8 @@ (defn- frame-same-size? [paste-obj frame-obj] (and - (= (:heigth (:selrect (first (vals paste-obj)))) - (:heigth (:selrect frame-obj))) + (= (:height (:selrect (first (vals paste-obj)))) + (:height (:selrect frame-obj))) (= (:width (:selrect (first (vals paste-obj)))) (:width (:selrect frame-obj))))) From ed021711b69cf90513d74fc1ca80d2e7f99a03d3 Mon Sep 17 00:00:00 2001 From: FairyPiggyDev Date: Thu, 30 Apr 2026 02:48:04 -0400 Subject: [PATCH 266/288] :recycle: Extract make-delete-asset-group-fn helper for assets panel (#9211) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer follow-up on PR #9151. The "Delete group" handler was duplicated across the three assets-panel sections (colors, typographies, components), each carrying the same skeleton — filter by group path, build an undo-id, run the deletes inside one transaction, and show the same confirm modal — with only the path predicate and the per-asset delete event differing. Add `app.main.ui.workspace.sidebar.assets.common/make-delete-asset-group-fn` that takes the differing parts as options: - `:assets` collection to filter. - `:on-clear-selection` invoked before the deletes. - `:delete-events` `(fn [matching-assets] => seq-of-events)`. - `:path-filter` predicate (defaults to `str/starts-with?`), overridden to `cpn/inside-path?` for components so nested group paths match the same way the existing ungroup/combine helpers do. The factory returns `(fn [path] …)` so each call site stays a straight `mf/use-fn`. The variant-container dedup in components (one `delete-shapes` per container, not one per sibling variant) moves into that section's `:delete-events` fn and is unchanged in behavior. Cleanup ------- The `:as i18n` alias is no longer needed in any of the three section files (its only use was `i18n/c` for the modal count, which the helper now handles); reduced to `:refer [tr]`. Github #9141 Signed-off-by: FairyPigDev Co-authored-by: Andrey Antukh --- .../ui/workspace/sidebar/assets/colors.cljs | 38 ++------- .../ui/workspace/sidebar/assets/common.cljs | 33 ++++++++ .../workspace/sidebar/assets/components.cljs | 77 ++++++------------- .../sidebar/assets/typographies.cljs | 38 ++------- 4 files changed, 70 insertions(+), 116 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs index 83df80b9db..b4e94bb92d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs @@ -29,7 +29,7 @@ [app.main.ui.workspace.sidebar.assets.groups :as grp] [app.util.color :as uc] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [tr]] + [app.util.i18n :refer [tr]] [app.util.keyboard :as kbd] [cuerdas.core :as str] [okulary.core :as l] @@ -501,38 +501,12 @@ file-id)))) (st/emit! (dwu/commit-undo-transaction undo-id))))) - ;; Issue #9141. Delete every color under a group path in a - ;; single undo transaction, after user confirmation. on-delete-group - (mf/use-fn - (mf/deps colors on-clear-selection) - (fn [path] - (let [group-colors - (->> colors - (filter #(str/starts-with? (:path %) path))) - - ;; Hoisted so the start/commit pair is bound to the - ;; same symbol regardless of how `do-delete` is - ;; invoked by the confirm modal. Review suggestion - ;; on PR #9151. - undo-id (js/Symbol) - - do-delete - (fn [] - (on-clear-selection) - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! - (map #(dwl/delete-color {:id (:id %)}) group-colors)) - (st/emit! (dwu/commit-undo-transaction undo-id)))] - (when (seq group-colors) - (st/emit! - (modal/show - {:type :confirm - :title (tr "modals.delete-asset-group.title") - :message (tr "modals.delete-asset-group.message" - (i18n/c (count group-colors))) - :accept-label (tr "labels.delete") - :on-accept do-delete})))))) + (mf/with-memo [colors on-clear-selection] + (cmm/make-delete-asset-group-fn + {:assets colors + :on-clear-selection on-clear-selection + :delete-events #(map (fn [c] (dwl/delete-color {:id (:id c)})) %)})) on-asset-click (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))] diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs index 14f494504d..28eb7e4699 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs @@ -198,6 +198,39 @@ (run! st/emit!)) (st/emit! (dwu/commit-undo-transaction undo-id)))) +(defn make-delete-asset-group-fn + "Build an `:on-delete-group` handler that filters `assets` by group + path, asks the user to confirm, and on accept emits every event + produced by `delete-events` inside one undo transaction. + + Options: + - `:assets` collection to filter. + - `:on-clear-selection` invoked before the deletes. + - `:delete-events` `(fn [matching-assets] => seq-of-events)`. + - `:path-filter` `(fn [asset-path group-path] => bool)` deciding + which assets fall under the group. Defaults to + `str/starts-with?`." + [{:keys [assets on-clear-selection delete-events path-filter] + :or {path-filter str/starts-with?}}] + (fn [path] + (let [matching (filter #(path-filter (:path %) path) assets) + undo-id (js/Symbol) + do-delete + (fn [] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction undo-id)) + (run! st/emit! (delete-events matching)) + (st/emit! (dwu/commit-undo-transaction undo-id)))] + (when (seq matching) + (st/emit! + (modal/show + {:type :confirm + :title (tr "modals.delete-asset-group.title") + :message (tr "modals.delete-asset-group.message" + (c (count matching))) + :accept-label (tr "labels.delete") + :on-accept do-delete})))))) + (defn on-drop-asset [event asset dragging* selected selected-full selected-paths rename] (let [create-typed-assets-group (partial create-assets-group rename)] diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs index 2d9d1b72e7..e2f0637c02 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs @@ -33,7 +33,7 @@ [app.main.ui.workspace.sidebar.assets.groups :as grp] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] - [app.util.i18n :as i18n :refer [tr]] + [app.util.i18n :refer [tr]] [cuerdas.core :as str] [okulary.core :as l] [potok.v2.core :as ptk] @@ -496,59 +496,32 @@ (map #(dwv/rename-comp-or-variant-and-main (:id %) (cmm/ungroup % path))))) (st/emit! (dwu/commit-undo-transaction undo-id))))) - ;; Issue #9141. Delete every component under a group path in a - ;; single undo transaction, after user confirmation. Variants - ;; are handled via their variant container (matching the - ;; per-item delete dispatch in file_library.cljs); sibling - ;; variants sharing a container are deduplicated so we delete - ;; each container only once. on-delete-group - (mf/use-fn - (mf/deps components on-clear-selection) - (fn [path] - (let [group-components - (->> components - (filter #(cpn/inside-path? (:path %) path))) + (mf/with-memo [components on-clear-selection] + (cmm/make-delete-asset-group-fn + {:assets components + :on-clear-selection on-clear-selection + :path-filter cpn/inside-path? + ;; Variants are handled via their variant container + ;; (matching the per-item delete dispatch in + ;; file_library.cljs); sibling variants sharing a + ;; container are deduplicated so we delete each container + ;; only once. + :delete-events + (fn [matching] + (let [{variants true non-variants false} + (group-by (comp boolean ctc/is-variant?) matching) - {variants true non-variants false} - (group-by (comp boolean ctc/is-variant?) group-components) - - ;; One delete-shapes per variant container, not per - ;; sibling variant within that container. - variant-containers - (->> variants - (group-by :variant-id) - (map (fn [[_ comps]] (first comps)))) - - ;; Hoisted so the start/commit pair is bound to the - ;; same symbol regardless of how `do-delete` is - ;; invoked by the confirm modal. Review suggestion - ;; on PR #9151. - undo-id (js/Symbol) - - do-delete - (fn [] - (on-clear-selection) - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! - (map (fn [component] - (dwsh/delete-shapes (:main-instance-page component) - #{(:variant-id component)})) - variant-containers)) - (run! st/emit! - (map (fn [component] - (dwl/delete-component {:id (:id component)})) - non-variants)) - (st/emit! (dwu/commit-undo-transaction undo-id)))] - (when (seq group-components) - (st/emit! - (modal/show - {:type :confirm - :title (tr "modals.delete-asset-group.title") - :message (tr "modals.delete-asset-group.message" - (i18n/c (count group-components))) - :accept-label (tr "labels.delete") - :on-accept do-delete})))))) + variant-containers + (->> variants + (group-by :variant-id) + (map (fn [[_ comps]] (first comps))))] + (concat + (map #(dwsh/delete-shapes (:main-instance-page %) + #{(:variant-id %)}) + variant-containers) + (map #(dwl/delete-component {:id (:id %)}) + non-variants))))})) on-group-combine-variants (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs index cf95f8b477..174648e8ca 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs @@ -26,7 +26,7 @@ [app.main.ui.workspace.sidebar.assets.groups :as grp] [app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry]] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [tr]] + [app.util.i18n :refer [tr]] [cuerdas.core :as str] [okulary.core :as l] [potok.v2.core :as ptk] @@ -354,38 +354,12 @@ (cmm/ungroup % path))))) (st/emit! (dwu/commit-undo-transaction undo-id))))) - ;; Issue #9141. Delete every typography under a group path in a - ;; single undo transaction, after user confirmation. on-delete-group - (mf/use-fn - (mf/deps typographies file-id on-clear-selection) - (fn [path] - (let [group-typographies - (->> typographies - (filter #(str/starts-with? (:path %) path))) - - ;; Hoisted so the start/commit pair is bound to the - ;; same symbol regardless of how `do-delete` is - ;; invoked by the confirm modal. Review suggestion - ;; on PR #9151. - undo-id (js/Symbol) - - do-delete - (fn [] - (on-clear-selection) - (st/emit! (dwu/start-undo-transaction undo-id)) - (run! st/emit! - (map #(dwl/delete-typography (:id %)) group-typographies)) - (st/emit! (dwu/commit-undo-transaction undo-id)))] - (when (seq group-typographies) - (st/emit! - (modal/show - {:type :confirm - :title (tr "modals.delete-asset-group.title") - :message (tr "modals.delete-asset-group.message" - (i18n/c (count group-typographies))) - :accept-label (tr "labels.delete") - :on-accept do-delete})))))) + (mf/with-memo [typographies on-clear-selection] + (cmm/make-delete-asset-group-fn + {:assets typographies + :on-clear-selection on-clear-selection + :delete-events #(map (fn [t] (dwl/delete-typography (:id t))) %)})) on-context-menu (mf/use-fn From aa87ae194c3575071acb48c0191b5ed7c076afe1 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Thu, 30 Apr 2026 11:29:15 +0200 Subject: [PATCH 267/288] :fire: Remove unused var (#9262) --- backend/src/app/rpc/commands/files.clj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index a2075a2807..346ff8b0fc 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -1260,10 +1260,6 @@ (db/exec-one! conn [sql:restore-file-thumbnails file-ids]) (db/exec-one! conn [sql:restore-file-tagged-object-thumbnails file-ids]))) -(defn- restore-file - [conn file-id] - (restore-files conn [file-id])) - (def ^:private sql:restore-projects "UPDATE project SET deleted_at = null WHERE id = ANY(?::uuid[])") From 22a325cc72d18a68f5999aed7480cec41b861853 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 30 Apr 2026 11:41:34 +0200 Subject: [PATCH 268/288] :paperclip: Fix linter issue --- backend/src/app/srepl/main.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index f25bec50cb..921aa04ebe 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -588,7 +588,7 @@ ::audit/tracked-at (ct/now)}) - (#'files/restore-file conn file-id)) + (#'files/restore-files conn [file-id])) :restored)))) (defn delete-project! @@ -622,7 +622,7 @@ (doseq [{:keys [id]} (db/query conn :file {:project-id project-id} {::sql/columns [:id]})] - (#'files/restore-file conn id)) + (#'files/restore-files conn [id])) :restored) From c14dbba7fd57cdd888033c74355141b7739ddedc Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 30 Apr 2026 10:54:48 +0200 Subject: [PATCH 269/288] :bug: Fix z-index for profile menu --- frontend/src/app/main/ui/dashboard/sidebar.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index e4bb79eea6..73e56d497a 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -26,7 +26,6 @@ margin: 0 var(--sp-l) 0 0; border-right: $b-1 solid var(--panel-border-color); background-color: var(--panel-background-color); - z-index: var(--z-index-panels); } // SIDEBAR CONTENT COMPONENT From 547750e8bfe409281c3a52ac56d353387c44c778 Mon Sep 17 00:00:00 2001 From: Statxc Date: Thu, 30 Apr 2026 15:29:04 +0200 Subject: [PATCH 270/288] :bug: Preserve OpenType variant name for custom fonts (#9193) --- CHANGES.md | 1 + backend/src/app/migrations.clj | 5 ++- ...148-add-variant-name-team-font-variant.sql | 2 ++ backend/src/app/rpc/commands/fonts.clj | 4 ++- common/src/app/common/media.cljc | 12 +++++++ common/test/common_tests/media_test.cljc | 35 +++++++++++++++++++ frontend/src/app/main/data/fonts.cljs | 7 ++-- frontend/src/app/main/ui/dashboard/fonts.cljs | 7 ++-- 8 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 backend/src/app/migrations/sql/0148-add-variant-name-team-font-variant.sql diff --git a/CHANGES.md b/CHANGES.md index d7c499c5f4..a6f7459acf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -71,6 +71,7 @@ - Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled [Github #8877](https://github.com/penpot/penpot/issues/8877) - Fix Copy as SVG: emit a single valid SVG document when multiple shapes are selected, and publish `image/svg+xml` to the clipboard so the paste target works in Inkscape and other SVG-native tools [Github #838](https://github.com/penpot/penpot/issues/838) - Reset profile submenu state when the account menu closes (by @eureka0928) [Github #8947](https://github.com/penpot/penpot/issues/8947) +- Preserve OpenType variant name table for custom fonts in the dashboard [Github #8924](https://github.com/penpot/penpot/issues/8924) - Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582) - Fix styles between grid layout inputs [Taiga #13526](https://tree.taiga.io/project/penpot/issue/13526) - Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534) diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 7554618cdd..a188c3c1f0 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -475,7 +475,10 @@ :fn (mg/resource "app/migrations/sql/0147-mod-team-invitation-table.sql")} {:name "0147-add-upload-session-table" - :fn (mg/resource "app/migrations/sql/0147-add-upload-session-table.sql")}]) + :fn (mg/resource "app/migrations/sql/0147-add-upload-session-table.sql")} + + {:name "0148-add-variant-name-team-font-variant" + :fn (mg/resource "app/migrations/sql/0148-add-variant-name-team-font-variant.sql")}]) (defn apply-migrations! [pool name migrations] diff --git a/backend/src/app/migrations/sql/0148-add-variant-name-team-font-variant.sql b/backend/src/app/migrations/sql/0148-add-variant-name-team-font-variant.sql new file mode 100644 index 0000000000..d90fb83538 --- /dev/null +++ b/backend/src/app/migrations/sql/0148-add-variant-name-team-font-variant.sql @@ -0,0 +1,2 @@ +ALTER TABLE team_font_variant + ADD COLUMN variant_name text NULL; diff --git a/backend/src/app/rpc/commands/fonts.clj b/backend/src/app/rpc/commands/fonts.clj index b47c6c2e38..e8c759fed7 100644 --- a/backend/src/app/rpc/commands/fonts.clj +++ b/backend/src/app/rpc/commands/fonts.clj @@ -98,7 +98,8 @@ [:font-id ::sm/uuid] [:font-family ::sm/text] [:font-weight [::sm/one-of {:format "number"} valid-weight]] - [:font-style [::sm/one-of {:format "string"} valid-style]]]) + [:font-style [::sm/one-of {:format "string"} valid-style]] + [:variant-name {:optional true} [:maybe ::sm/text]]]) ;; FIXME: IMPORTANT: refactor this, we should not hold a whole db ;; connection around the font creation @@ -184,6 +185,7 @@ :font-family (:font-family params) :font-weight (:font-weight params) :font-style (:font-style params) + :variant-name (:variant-name params) :woff1-file-id (:id woff1) :woff2-file-id (:id woff2) :otf-file-id (:id otf) diff --git a/common/src/app/common/media.cljc b/common/src/app/common/media.cljc index fc349765a2..87d7b2f401 100644 --- a/common/src/app/common/media.cljc +++ b/common/src/app/common/media.cljc @@ -114,3 +114,15 @@ 800 "Extra Bold" 900 "Black" 950 "Extra Black")) + +(defn font-display-variant + [variant-name weight style] + (cond + (and (string? variant-name) (not (str/blank? variant-name))) + (str/trim variant-name) + + :else + (let [base (font-weight->name weight) + italic? (= "italic" style)] + (cond-> base + italic? (str " Italic"))))) diff --git a/common/test/common_tests/media_test.cljc b/common/test/common_tests/media_test.cljc index b6c18aab2d..a41d2466fa 100644 --- a/common/test/common_tests/media_test.cljc +++ b/common/test/common_tests/media_test.cljc @@ -57,3 +57,38 @@ (t/testing "leaves filename intact when it has no extension" (t/is (= (media/strip-image-extension "README") "README")))) + +(t/deftest test-font-display-variant + (t/testing "preserves the foundry-supplied variant string verbatim" + (t/is (= "Thin" (media/font-display-variant "Thin" 100 "normal"))) + (t/is (= "SemiBold" (media/font-display-variant "SemiBold" 600 "normal"))) + (t/is (= "Medium Oblique" (media/font-display-variant "Medium Oblique" 500 "italic"))) + (t/is (= "Ultra" (media/font-display-variant "Ultra" 900 "normal")))) + + (t/testing "trims surrounding whitespace from upstream variant strings" + (t/is (= "Bold" (media/font-display-variant " Bold " 700 "normal")))) + + (t/testing "ignores blank or nil variant strings" + (t/is (= "Hairline" (media/font-display-variant nil 100 "normal"))) + (t/is (= "Regular" (media/font-display-variant "" 400 "normal"))) + (t/is (= "Bold" (media/font-display-variant " " 700 "normal"))) + (t/is (= "Bold Italic" (media/font-display-variant nil 700 "italic")))) + + (t/testing "fallback covers every supported numeric weight" + (t/is (= "Hairline" (media/font-display-variant nil 100 "normal"))) + (t/is (= "Extra Light" (media/font-display-variant nil 200 "normal"))) + (t/is (= "Light" (media/font-display-variant nil 300 "normal"))) + (t/is (= "Regular" (media/font-display-variant nil 400 "normal"))) + (t/is (= "Medium" (media/font-display-variant nil 500 "normal"))) + (t/is (= "Semi Bold" (media/font-display-variant nil 600 "normal"))) + (t/is (= "Bold" (media/font-display-variant nil 700 "normal"))) + (t/is (= "Extra Bold" (media/font-display-variant nil 800 "normal"))) + (t/is (= "Black" (media/font-display-variant nil 900 "normal"))) + (t/is (= "Extra Black" (media/font-display-variant nil 950 "normal")))) + + (t/testing "italic suffix only applied via the fallback path" + (t/is (= "Italic" (media/font-display-variant "Italic" 400 "italic"))) + (t/is (= "Regular Italic" (media/font-display-variant nil 400 "italic")))) + + (t/testing "stored variant survives even when its derived weight disagrees" + (t/is (= "Ultra" (media/font-display-variant "Ultra" 400 "normal"))))) diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs index d72cde8436..9a49711e85 100644 --- a/frontend/src/app/main/data/fonts.cljs +++ b/frontend/src/app/main/data/fonts.cljs @@ -60,9 +60,9 @@ (prepare-font-variant [item] {:id (str (:font-style item) "-" (:font-weight item)) - :name (str (cm/font-weight->name (:font-weight item)) - (when (not= "normal" (:font-style item)) - (str " " (str/capital (:font-style item))))) + :name (cm/font-display-variant (:variant-name item) + (:font-weight item) + (:font-style item)) :style (:font-style item) :weight (str (:font-weight item)) ::fonts/woff1-file-id (:woff1-file-id item) @@ -140,6 +140,7 @@ :font-family (or family "") :font-weight (cm/parse-font-weight variant) :font-style (cm/parse-font-style variant) + :variant-name variant :height-warning? height-warning?}) ;; Font could not be parsed (woff2), extract metadata from filename (let [base-name (str/replace name #"\.[^.]+$" "") diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs index 72c57856b9..eaefe3925f 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.cljs +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -65,10 +65,9 @@ (mf/defc font-variant-display-name* {::mf/private true} [{:keys [variant]}] - [:* - [:span (cm/font-weight->name (:font-weight variant))] - (when (not= "normal" (:font-style variant)) - [:span " " (str/capital (:font-style variant))])]) + [:span (cm/font-display-variant (:variant-name variant) + (:font-weight variant) + (:font-style variant))]) (mf/defc uploaded-fonts* {::mf/private true} From 9f945660056b8caaac00a115d6054850a5cd7bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Thu, 30 Apr 2026 15:30:20 +0200 Subject: [PATCH 271/288] :lipstick: Rename i18n keys for tokens errors (#9207) --- common/src/app/common/files/tokens.cljc | 12 ++--- .../main/data/workspace/tokens/errors.cljs | 54 +++++++++---------- .../main/data/workspace/tokens/warnings.cljs | 4 +- .../tokens/management/forms/shadow.cljs | 2 +- .../tokens/management/forms/typography.cljs | 4 +- frontend/translations/de.po | 44 +++++++-------- frontend/translations/en.po | 52 +++++++++--------- frontend/translations/es.po | 46 ++++++++-------- frontend/translations/fr.po | 40 +++++++------- frontend/translations/he.po | 52 +++++++++--------- frontend/translations/hi.po | 52 +++++++++--------- frontend/translations/id.po | 18 +++---- frontend/translations/it.po | 52 +++++++++--------- frontend/translations/lv.po | 28 +++++----- frontend/translations/nl.po | 52 +++++++++--------- frontend/translations/ro.po | 42 +++++++-------- frontend/translations/sv.po | 52 +++++++++--------- frontend/translations/tr.po | 52 +++++++++--------- frontend/translations/ukr_UA.po | 28 +++++----- frontend/translations/zh_CN.po | 26 ++++----- 20 files changed, 356 insertions(+), 356 deletions(-) diff --git a/common/src/app/common/files/tokens.cljc b/common/src/app/common/files/tokens.cljc index cd6dc44cb2..ff5853069b 100644 --- a/common/src/app/common/files/tokens.cljc +++ b/common/src/app/common/files/tokens.cljc @@ -26,7 +26,7 @@ [{:keys [value]}] (when (or (str/empty? value) (str/blank? value)) - (tr "workspace.tokens.empty-input"))) + (tr "errors.tokens.empty-input"))) (def schema:token-value-generic [::sm/text {:error/fn token-value-empty-fn}]) @@ -34,7 +34,7 @@ (def schema:token-value-numeric [:and [::sm/text {:error/fn token-value-empty-fn}] - [:fn {:error/fn #(tr "workspace.tokens.invalid-value" (:value %))} + [:fn {:error/fn #(tr "errors.tokens.invalid-value" (:value %))} (fn [value] (if (str/numeric? value) (let [n (d/parse-double value)] @@ -44,7 +44,7 @@ (def schema:token-value-percent [:and [::sm/text {:error/fn token-value-empty-fn}] - [:fn {:error/fn #(tr "workspace.tokens.value-with-percent" (:value %))} + [:fn {:error/fn #(tr "errors.tokens.value-with-percent" (:value %))} (fn [value] (if (d/percent? value) (let [v (d/parse-percent value)] @@ -57,7 +57,7 @@ (def schema:token-value-opacity [:and [::sm/text {:error/fn token-value-empty-fn}] - [:fn {:error/fn #(tr "workspace.tokens.opacity-range")} + [:fn {:error/fn #(tr "errors.tokens.opacity-range")} (fn [opacity] (if (str/numeric? opacity) (let [n (d/parse-percent opacity)] @@ -71,7 +71,7 @@ (def schema:token-value-font-weight [:or - [:fn {:error/fn #(tr "workspace.tokens.invalid-font-weight-token-value")} + [:fn {:error/fn #(tr "errors.tokens.invalid-font-weight-token-value")} cto/valid-font-weight-variant] ::sm/text]) ;; Leave references or formulas to be checked by the resolver @@ -181,7 +181,7 @@ [:value (make-token-value-schema token-type)] [:description {:optional true} schema:token-description]]) [:fn {:error/field :value - :error/fn #(tr "workspace.tokens.self-reference")} + :error/fn #(tr "errors.tokens.self-reference")} (fn [{:keys [name value]}] (when (and name value) (not (cto/token-value-self-reference? name value))))]]) diff --git a/frontend/src/app/main/data/workspace/tokens/errors.cljs b/frontend/src/app/main/data/workspace/tokens/errors.cljs index 30ab2e30b9..7338663b2d 100644 --- a/frontend/src/app/main/data/workspace/tokens/errors.cljs +++ b/frontend/src/app/main/data/workspace/tokens/errors.cljs @@ -12,109 +12,109 @@ (def error-codes {:error.import/json-parse-error {:error/code :error.import/json-parse-error - :error/fn #(tr "workspace.tokens.error-parse")} + :error/fn #(tr "errors.tokens.error-parse")} :error.import/no-token-files-found {:error/code :error.import/no-token-files-found - :error/fn #(tr "workspace.tokens.no-token-files-found")} + :error/fn #(tr "errors.tokens.no-token-files-found")} :error.import/invalid-json-data {:error/code :error.import/invalid-json-data - :error/fn #(tr "workspace.tokens.invalid-json")} + :error/fn #(tr "errors.tokens.invalid-json")} :error.import/invalid-token-name {:error/code :error.import/invalid-token-name - :error/fn #(tr "workspace.tokens.invalid-json-token-name") - :error/detail #(tr "workspace.tokens.invalid-json-token-name-detail" %)} + :error/fn #(tr "errors.tokens.invalid-json-token-name") + :error/detail #(tr "errors.tokens.invalid-json-token-name-detail" %)} :error.import/style-dictionary-reference-errors {:error/code :error.import/style-dictionary-reference-errors - :error/fn #(str (tr "workspace.tokens.import-error") "\n\n" (first %)) + :error/fn #(str (tr "errors.tokens.import-error") "\n\n" (first %)) :error/detail #(str/join "\n\n" (rest %))} :error.import/style-dictionary-unknown-error {:error/code :error.import/style-dictionary-reference-errors - :error/fn #(tr "workspace.tokens.import-error")} + :error/fn #(tr "errors.tokens.import-error")} :error.token/empty-input {:error/code :error.token/empty-input - :error/fn #(tr "workspace.tokens.empty-input")} + :error/fn #(tr "errors.tokens.empty-input")} :error.token/direct-self-reference {:error/code :error.token/direct-self-reference - :error/fn #(tr "workspace.tokens.self-reference")} + :error/fn #(tr "errors.tokens.self-reference")} :error.token/invalid-color {:error/code :error.token/invalid-color - :error/fn #(str (tr "workspace.tokens.invalid-color" %))} + :error/fn #(str (tr "errors.tokens.invalid-color" %))} :error.token/number-too-large {:error/code :error.token/number-too-large - :error/fn #(str (tr "workspace.tokens.number-too-large" %))} + :error/fn #(str (tr "errors.tokens.number-too-large" %))} :error.style-dictionary/missing-reference {:error/code :error.style-dictionary/missing-reference - :error/fn #(str (tr "workspace.tokens.missing-references") (str/join " " %))} + :error/fn #(str (tr "errors.tokens.missing-references") (str/join " " %))} :error.style-dictionary/invalid-token-value {:error/code :error.style-dictionary/invalid-token-value - :error/fn #(str (tr "workspace.tokens.invalid-value" %))} + :error/fn #(str (tr "errors.tokens.invalid-value" %))} :error.style-dictionary/value-with-units {:error/code :error.style-dictionary/value-with-units - :error/fn #(str (tr "workspace.tokens.value-with-units"))} + :error/fn #(str (tr "errors.tokens.value-with-units"))} :error.style-dictionary/value-with-percent {:error/code :error.style-dictionary/value-with-percent - :error/fn #(str (tr "workspace.tokens.value-with-percent"))} + :error/fn #(str (tr "errors.tokens.value-with-percent"))} :error.style-dictionary/invalid-token-value-opacity {:error/code :error.style-dictionary/invalid-token-value-opacity - :error/fn #(str/join "\n" [(str (tr "workspace.tokens.invalid-value" %) ".") (tr "workspace.tokens.opacity-range")])} + :error/fn #(str/join "\n" [(str (tr "errors.tokens.invalid-value" %) ".") (tr "errors.tokens.opacity-range")])} :error.style-dictionary/invalid-token-value-stroke-width {:error/code :error.style-dictionary/invalid-token-value-stroke-width - :error/fn #(str/join "\n" [(str (tr "workspace.tokens.invalid-value" %) ".") (tr "workspace.tokens.stroke-width-range")])} + :error/fn #(str/join "\n" [(str (tr "errors.tokens.invalid-value" %) ".") (tr "errors.tokens.stroke-width-range")])} :error.style-dictionary/invalid-token-value-text-case {:error/code :error.style-dictionary/invalid-token-value-text-case - :error/fn #(tr "workspace.tokens.invalid-text-case-token-value" %)} + :error/fn #(tr "errors.tokens.invalid-text-case-token-value" %)} :error.style-dictionary/invalid-token-value-text-decoration {:error/code :error.style-dictionary/invalid-token-value-text-decoration - :error/fn #(tr "workspace.tokens.invalid-text-decoration-token-value" %)} + :error/fn #(tr "errors.tokens.invalid-text-decoration-token-value" %)} :error.style-dictionary/invalid-token-value-font-weight {:error/code :error.style-dictionary/invalid-token-value-font-weight - :error/fn #(tr "workspace.tokens.invalid-font-weight-token-value" %)} + :error/fn #(tr "errors.tokens.invalid-font-weight-token-value" %)} :error.style-dictionary/invalid-token-value-font-family {:error/code :error.style-dictionary/invalid-token-value-font-family - :error/fn #(tr "workspace.tokens.invalid-font-family-token-value" %)} + :error/fn #(tr "errors.tokens.invalid-font-family-token-value" %)} :error.style-dictionary/invalid-token-value-typography {:error/code :error.style-dictionary/invalid-token-value-typography - :error/fn #(tr "workspace.tokens.invalid-token-value-typography" %)} + :error/fn #(tr "errors.tokens.invalid-token-value-typography" %)} :error.style-dictionary/composite-line-height-needs-font-size {:error/code :error.style-dictionary/composite-line-height-needs-font-size - :error/fn #(tr "workspace.tokens.composite-line-height-needs-font-size" %)} + :error/fn #(tr "errors.tokens.composite-line-height-needs-font-size" %)} :error.style-dictionary/invalid-token-value-shadow-type {:error/code :error.style-dictionary/invalid-token-value-shadow-type - :error/fn #(tr "workspace.tokens.invalid-shadow-type-token-value" %)} + :error/fn #(tr "errors.tokens.invalid-shadow-type-token-value" %)} :error.style-dictionary/invalid-token-value-shadow-blur {:error/code :error.style-dictionary/invalid-token-value-shadow-blur - :error/fn #(tr "workspace.tokens.shadow-blur-range")} + :error/fn #(tr "errors.tokens.shadow-blur-range")} :error.style-dictionary/invalid-token-value-shadow-spread {:error/code :error.style-dictionary/invalid-token-value-shadow-spread - :error/fn #(tr "workspace.tokens.shadow-spread-range")} + :error/fn #(tr "errors.tokens.shadow-spread-range")} :error.style-dictionary/invalid-token-value-shadow {:error/code :error.style-dictionary/invalid-token-value-shadow - :error/fn #(tr "workspace.tokens.invalid-token-value-shadow" %)} + :error/fn #(tr "errors.tokens.invalid-token-value-shadow" %)} :error/unknown {:error/code :error/unknown diff --git a/frontend/src/app/main/data/workspace/tokens/warnings.cljs b/frontend/src/app/main/data/workspace/tokens/warnings.cljs index f59047e600..6f3d4161aa 100644 --- a/frontend/src/app/main/data/workspace/tokens/warnings.cljs +++ b/frontend/src/app/main/data/workspace/tokens/warnings.cljs @@ -12,11 +12,11 @@ (def warning-codes {:warning.style-dictionary/invalid-referenced-token-value-opacity {:warning/code :warning.style-dictionary/invalid-referenced-token-value-opacity - :warning/fn (fn [value] (str/join "\n" [(str (tr "workspace.tokens.resolved-value" value) ".") (tr "workspace.tokens.opacity-range")]))} + :warning/fn (fn [value] (str/join "\n" [(str (tr "workspace.tokens.resolved-value" value) ".") (tr "errors.tokens.opacity-range")]))} :warning.style-dictionary/invalid-referenced-token-value-stroke-width {:warning/code :warning.style-dictionary/invalid-referenced-token-value-stroke-width - :warning/fn (fn [value] (str/join "\n" [(str (tr "workspace.tokens.resolved-value" value) ".") (tr "workspace.tokens.stroke-width-range")]))} + :warning/fn (fn [value] (str/join "\n" [(str (tr "workspace.tokens.resolved-value" value) ".") (tr "errors.tokens.stroke-width-range")]))} :warning/unknown {:warning/code :warning/unknown diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs index 722683d54a..3878305e76 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs @@ -296,7 +296,7 @@ [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] [:fn {:error/field [:value :reference] - :error/fn #(tr "workspace.tokens.self-reference")} + :error/fn #(tr "errors.tokens.self-reference")} (fn [{:keys [name value]}] (let [reference (get value :reference)] (if (and reference name) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs index cb926f72fd..63e62bfbdb 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs @@ -239,7 +239,7 @@ [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] [:fn {:error/field [:value :reference] - :error/fn #(tr "workspace.tokens.self-reference")} + :error/fn #(tr "errors.tokens.self-reference")} (fn [{:keys [name value]}] (let [reference (get value :reference)] (if (and reference name) @@ -247,7 +247,7 @@ true)))] [:fn {:error/field [:value :line-height] - :error/fn #(tr "workspace.tokens.composite-line-height-needs-font-size")} + :error/fn #(tr "errors.tokens.composite-line-height-needs-font-size")} (fn [{:keys [value]}] (let [line-heigh (get value :line-height) font-size (get value :font-size)] diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 8f14740a22..5c42bfbf56 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -7530,7 +7530,7 @@ msgid "workspace.tokens.edit-token" msgstr "%s Token bearbeiten" #: src/app/main/data/workspace/tokens/errors.cljs:41 -msgid "workspace.tokens.empty-input" +msgid "errors.tokens.empty-input" msgstr "Der Token-Wert darf nicht leer sein" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 @@ -7538,7 +7538,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "%s Token-Name eingeben" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "Fehler beim Importieren. JSON konnte nicht verarbeitet werden." #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -7602,7 +7602,7 @@ msgid "workspace.tokens.import-button-prefix" msgstr "%s importieren" #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "Fehler beim Import:" #: src/app/main/ui/workspace/tokens/import/modal.cljs:273 @@ -7659,57 +7659,57 @@ msgid "workspace.tokens.individual-tokens" msgstr "Einzelne Token verwenden" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "Ungültiger Farbwert: %s" #: src/app/main/data/workspace/tokens/errors.cljs:89 -msgid "workspace.tokens.invalid-font-weight-token-value" +msgid "errors.tokens.invalid-font-weight-token-value" msgstr "" "Ungültiger Wert für die Schriftstärke: Verwenden Sie numerische Werte " "(100–950) oder Standardbezeichnungen (dünn, leicht, normal, fett usw.), " "optional gefolgt von „kursiv”" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "Fehler beim Importieren: Ihre JSON-Datei enthält ungültige Token-Daten." #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "Fehler beim Importieren: Ihre JSON-Datei enthält ungültige Token-Namen." #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "„%s“ ist kein gültiger Namen für ein Token.\n" "Token-Namen dürfen nur Buchstaben und Ziffern enthalten, die durch Punkte " "getrennt sind und dürfen nicht mit einem Dollarzeichen beginnen." #: src/app/main/data/workspace/tokens/errors.cljs:105 -msgid "workspace.tokens.invalid-shadow-type-token-value" +msgid "errors.tokens.invalid-shadow-type-token-value" msgstr "" "Ungültiger Schattentyp: Es werden nur „innerShadow” oder „dropShadow” " "akzeptiert" #: src/app/main/data/workspace/tokens/errors.cljs:81 -msgid "workspace.tokens.invalid-text-case-token-value" +msgid "errors.tokens.invalid-text-case-token-value" msgstr "" "Ungültiger Token-Wert: Es werden nur „none“, „Uppercase“, „Lowercase“ oder " "„Capitalize“ akzeptiert" #: src/app/main/data/workspace/tokens/errors.cljs:85 -msgid "workspace.tokens.invalid-text-decoration-token-value" +msgid "errors.tokens.invalid-text-decoration-token-value" msgstr "" "Ungültiger Token-Wert: Es werden nur „none“, „underline“ oder " "„strike-through“ akzeptiert" #: src/app/main/data/workspace/tokens/errors.cljs:97 -msgid "workspace.tokens.invalid-token-value-typography" +msgid "errors.tokens.invalid-token-value-typography" msgstr "" "Ungültiger Wert: Der Wert muss auf ein zusammengesetztes Typografie-Token " "verweisen." #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "Ungültiger Token-Wert: %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -7749,7 +7749,7 @@ msgid "workspace.tokens.min-size" msgstr "Mindestgröße" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "Fehlende Token-Referenzen: " #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 @@ -7789,7 +7789,7 @@ msgid "workspace.tokens.no-themes-currently" msgstr "Sie haben derzeit keine Themes." #: src/app/main/data/workspace/tokens/errors.cljs:19 -msgid "workspace.tokens.no-token-files-found" +msgid "errors.tokens.no-token-files-found" msgstr "In dieser Datei wurden keine Tokens, Sets oder Themen gefunden." #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:134 @@ -7797,11 +7797,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "%s aktivierte Sets" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "Ungültiger Tokenwert. Der ermittelte Wert ist zu hoch: %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "" "Die Deckkraft muss zwischen 0 und 100 % oder zwischen 0 und 1 liegen (z. B. " "50 % oder 0,5)." @@ -7846,7 +7846,7 @@ msgid "workspace.tokens.select-set" msgstr "Set auswählen." #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "Der Token referenziert sich selbst" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 @@ -7887,7 +7887,7 @@ msgid "workspace.tokens.shadow-blur" msgstr "Weichzeichnen" #: src/app/main/data/workspace/tokens/errors.cljs:109 -msgid "workspace.tokens.shadow-blur-range" +msgid "errors.tokens.shadow-blur-range" msgstr "Wert muss größer oder gleich 0 sein." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988 @@ -7925,7 +7925,7 @@ msgid "workspace.tokens.size" msgstr "Größe" #: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 -msgid "workspace.tokens.stroke-width-range" +msgid "errors.tokens.stroke-width-range" msgstr "Die Rahmenbreite muss größer oder gleich 0 sein." #: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 @@ -8036,11 +8036,11 @@ msgid "workspace.tokens.value-not-valid" msgstr "Der Wert ist nicht gültig" #: src/app/main/data/workspace/tokens/errors.cljs:69 -msgid "workspace.tokens.value-with-percent" +msgid "errors.tokens.value-with-percent" msgstr "Ungültiger Wert: % ist nicht zulässig." #: src/app/main/data/workspace/tokens/errors.cljs:65 -msgid "workspace.tokens.value-with-units" +msgid "errors.tokens.value-with-units" msgstr "Ungültiger Wert: Einheiten sind hier nicht zulässig." #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 diff --git a/frontend/translations/en.po b/frontend/translations/en.po index d26f27b0aa..caec56566d 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -8242,7 +8242,7 @@ msgid "workspace.tokens.color" msgstr "Color" #: src/app/main/data/workspace/tokens/errors.cljs:101, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:251 -msgid "workspace.tokens.composite-line-height-needs-font-size" +msgid "errors.tokens.composite-line-height-needs-font-size" msgstr "Line Height depends on Font Size. Add a Font Size to get the resolved value." #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:57 @@ -8294,7 +8294,7 @@ msgid "workspace.tokens.edit-token" msgstr "Edit %s token" #: src/app/main/data/workspace/tokens/errors.cljs:41 -msgid "workspace.tokens.empty-input" +msgid "errors.tokens.empty-input" msgstr "Token value cannot be empty" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 @@ -8302,7 +8302,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "Enter %s token name" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "Import Error: Could not parse JSON." #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -8366,7 +8366,7 @@ msgid "workspace.tokens.import-button-prefix" msgstr "Import %s" #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "Import Error:" #: src/app/main/ui/workspace/tokens/import/modal.cljs:273 @@ -8417,58 +8417,58 @@ msgid "workspace.tokens.individual-tokens" msgstr "Use individual tokens" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "Invalid color value: %s" #: src/app/main/data/workspace/tokens/errors.cljs:93 -msgid "workspace.tokens.invalid-font-family-token-value" +msgid "errors.tokens.invalid-font-family-token-value" msgstr "Invalid token value: you can only reference a font-family token" #: src/app/main/data/workspace/tokens/errors.cljs:89 -msgid "workspace.tokens.invalid-font-weight-token-value" +msgid "errors.tokens.invalid-font-weight-token-value" msgstr "" "Invalid font weight value: use numeric values (100-950) or standard names " "(thin, light, regular, bold, etc.) optionally followed by 'Italic'" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "Import Error: Invalid token data in JSON." #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "Import Error: Invalid token name in JSON." #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "\"%s\" is not a valid token name.\n" "Token names should only contain letters and digits separated by . " "characters and must not start with a $ sign." #: src/app/main/data/workspace/tokens/errors.cljs:105 -msgid "workspace.tokens.invalid-shadow-type-token-value" +msgid "errors.tokens.invalid-shadow-type-token-value" msgstr "Invalid shadow type: only 'innerShadow' or 'dropShadow' are accepted" #: src/app/main/data/workspace/tokens/errors.cljs:81 -msgid "workspace.tokens.invalid-text-case-token-value" +msgid "errors.tokens.invalid-text-case-token-value" msgstr "" "Invalid token value: only none, Uppercase, Lowercase or Capitalize are " "accepted" #: src/app/main/data/workspace/tokens/errors.cljs:85 -msgid "workspace.tokens.invalid-text-decoration-token-value" +msgid "errors.tokens.invalid-text-decoration-token-value" msgstr "Invalid token value: only none, underline and strike-through are accepted" #: src/app/main/data/workspace/tokens/errors.cljs:117 -msgid "workspace.tokens.invalid-token-value-shadow" +msgid "errors.tokens.invalid-token-value-shadow" msgstr "Invalid value: must reference a composite shadow token." #: src/app/main/data/workspace/tokens/errors.cljs:97 -msgid "workspace.tokens.invalid-token-value-typography" +msgid "errors.tokens.invalid-token-value-typography" msgstr "Invalid value: must reference a composite typography token." #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "Invalid token value: %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -8512,7 +8512,7 @@ msgid "workspace.tokens.missing-reference" msgstr "Missing reference" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "Missing token references: " #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 @@ -8562,7 +8562,7 @@ msgid "workspace.tokens.no-themes-currently" msgstr "You currently have no themes." #: src/app/main/data/workspace/tokens/errors.cljs:19 -msgid "workspace.tokens.no-token-files-found" +msgid "errors.tokens.no-token-files-found" msgstr "No tokens, sets, or themes were found in this file." #: src/app/main/ui/workspace/tokens/remapping_modal.cljs:95 @@ -8574,11 +8574,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "%s active sets" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "Invalid token value. The resolved value is too large: %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "Opacity must be between 0 and 100% or 0 and 1 (e.g. 50% or 0.5)." #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 @@ -8654,7 +8654,7 @@ msgid "workspace.tokens.select-set" msgstr "Select set." #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "Token has self reference" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 @@ -8691,7 +8691,7 @@ msgid "workspace.tokens.shadow-blur" msgstr "Blur" #: src/app/main/data/workspace/tokens/errors.cljs:109 -msgid "workspace.tokens.shadow-blur-range" +msgid "errors.tokens.shadow-blur-range" msgstr "Shadow blur must be greater than or equal to 0." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988 @@ -8712,7 +8712,7 @@ msgid "workspace.tokens.shadow-spread" msgstr "Spread" #: src/app/main/data/workspace/tokens/errors.cljs:113 -msgid "workspace.tokens.shadow-spread-range" +msgid "errors.tokens.shadow-spread-range" msgstr "Shadow spread must be greater than or equal to 0." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:1215 @@ -8742,7 +8742,7 @@ msgid "workspace.tokens.size" msgstr "Size" #: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 -msgid "workspace.tokens.stroke-width-range" +msgid "errors.tokens.stroke-width-range" msgstr "Stroke width must be greater than or equal to 0." #: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 @@ -8857,11 +8857,11 @@ msgid "workspace.tokens.value-not-valid" msgstr "The value is not valid" #: src/app/main/data/workspace/tokens/errors.cljs:69 -msgid "workspace.tokens.value-with-percent" +msgid "errors.tokens.value-with-percent" msgstr "Invalid value: % is not allowed." #: src/app/main/data/workspace/tokens/errors.cljs:65 -msgid "workspace.tokens.value-with-units" +msgid "errors.tokens.value-with-units" msgstr "Invalid value: Units are not allowed." #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 4d670f8716..fd978a7e4b 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -8030,7 +8030,7 @@ msgid "workspace.tokens.choose-folder" msgstr "Elige carpeta" #: src/app/main/data/workspace/tokens/errors.cljs:101, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:251 -msgid "workspace.tokens.composite-line-height-needs-font-size" +msgid "errors.tokens.composite-line-height-needs-font-size" msgstr "" "El Line Height depende del Font Size. Añade un Font Size para obtener el " "valor computado." @@ -8084,7 +8084,7 @@ msgid "workspace.tokens.edit-token" msgstr "Editar token de %s" #: src/app/main/data/workspace/tokens/errors.cljs:41 -msgid "workspace.tokens.empty-input" +msgid "errors.tokens.empty-input" msgstr "El valor del token no puede estar vacío" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 @@ -8092,7 +8092,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "Introduce un nombre para el token %s" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "Error al importar: No se pudo procesar el JSON." #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -8152,7 +8152,7 @@ msgid "workspace.tokens.import-button-prefix" msgstr "Importar %s" #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "Error al importar:" #: src/app/main/ui/workspace/tokens/import/modal.cljs:273 @@ -8207,55 +8207,55 @@ msgid "workspace.tokens.individual-tokens" msgstr "Usa tokens individuales" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "Valor de color inválido: %s" #: src/app/main/data/workspace/tokens/errors.cljs:93 -msgid "workspace.tokens.invalid-font-family-token-value" +msgid "errors.tokens.invalid-font-family-token-value" msgstr "Valor de token no válido: solo puedes referenciar tokens tipo font-family" #: src/app/main/data/workspace/tokens/errors.cljs:89 -msgid "workspace.tokens.invalid-font-weight-token-value" +msgid "errors.tokens.invalid-font-weight-token-value" msgstr "" "Valor de peso de fuente inválido: usa valores numéricos (100-950) o nombres " "estándar (thin, light, regular, bold, etc.) opcionalmente seguidos de " "'Italic'" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "Error al importar: Datos de token no válidos en JSON." #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "Error al importar: Nombre de token no válido en JSON." #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "\"%s\" no es un nombre de token válido.\n" "Los nombres de token solo pueden contener letras y dígitos separados por " "caracteres . y no pueden empezar con un signo $." #: src/app/main/data/workspace/tokens/errors.cljs:105 -msgid "workspace.tokens.invalid-shadow-type-token-value" +msgid "errors.tokens.invalid-shadow-type-token-value" msgstr "Tipo de sombra no válida: solo se aceptan 'innerShadow' o 'dropShadow'" #: src/app/main/data/workspace/tokens/errors.cljs:85 -msgid "workspace.tokens.invalid-text-decoration-token-value" +msgid "errors.tokens.invalid-text-decoration-token-value" msgstr "" "Valor de token no válido: solo none, underline y strike-through son " "aceptados" #: src/app/main/data/workspace/tokens/errors.cljs:117 -msgid "workspace.tokens.invalid-token-value-shadow" +msgid "errors.tokens.invalid-token-value-shadow" msgstr "Valor no válido: debe hacer referencia a un token de sombra compuesto." #: src/app/main/data/workspace/tokens/errors.cljs:97 -msgid "workspace.tokens.invalid-token-value-typography" +msgid "errors.tokens.invalid-token-value-typography" msgstr "Valor no válido: debe hacer referencia a un token tipográfico compuesto." #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "Valor de token no válido: %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -8287,7 +8287,7 @@ msgid "workspace.tokens.missing-reference" msgstr "Referencia no encontrada" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "Referencias de tokens no encontradas: " #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 @@ -8343,11 +8343,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "%s sets activos" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "Valor de token no valido. El valor resuelto es muy grande: %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "La opacidad debe estar entre 0 y 100% o 0 y 1 (p.e. 50% o 0.5)." #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 @@ -8403,7 +8403,7 @@ msgid "workspace.tokens.select-set" msgstr "Selecciona set" #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "El token tiene una autoreferencia" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 @@ -8442,7 +8442,7 @@ msgid "workspace.tokens.shadow-blur" msgstr "Blur" #: src/app/main/data/workspace/tokens/errors.cljs:109 -msgid "workspace.tokens.shadow-blur-range" +msgid "errors.tokens.shadow-blur-range" msgstr "El desenfoque (blur) de la sombra debe ser mayor o igual a 0." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988 @@ -8463,7 +8463,7 @@ msgid "workspace.tokens.shadow-spread" msgstr "Spread" #: src/app/main/data/workspace/tokens/errors.cljs:113 -msgid "workspace.tokens.shadow-spread-range" +msgid "errors.tokens.shadow-spread-range" msgstr "La extensión (spread) de la sombra debe ser mayor o igual a 0." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:1215 @@ -8489,7 +8489,7 @@ msgid "workspace.tokens.shadow-y" msgstr "Y" #: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 -msgid "workspace.tokens.stroke-width-range" +msgid "errors.tokens.stroke-width-range" msgstr "Stroke width debe ser mayor o igual a 0." #: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 @@ -8587,7 +8587,7 @@ msgid "workspace.tokens.value-not-valid" msgstr "El valor no es válido" #: src/app/main/data/workspace/tokens/errors.cljs:65 -msgid "workspace.tokens.value-with-units" +msgid "errors.tokens.value-with-units" msgstr "Valor no válido: No se permiten unidades." #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index f4a62e3848..e8967641fa 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -7706,7 +7706,7 @@ msgid "workspace.tokens.color" msgstr "Couleur" #: src/app/main/data/workspace/tokens/errors.cljs:101, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:251 -msgid "workspace.tokens.composite-line-height-needs-font-size" +msgid "errors.tokens.composite-line-height-needs-font-size" msgstr "" "L'interlignage dépend de la taille de la police. Ajoutez une taille de " "police pour obtenir la valeur déduite." @@ -7756,7 +7756,7 @@ msgid "workspace.tokens.edit-token" msgstr "Modifier le token" #: src/app/main/data/workspace/tokens/errors.cljs:41 -msgid "workspace.tokens.empty-input" +msgid "errors.tokens.empty-input" msgstr "La valeur du token doit être renseignée" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 @@ -7764,7 +7764,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "Entrez le nom du token %s" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "Erreur d'importation : Impossible d'interpréter le fichier JSON." #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -7818,7 +7818,7 @@ msgid "workspace.tokens.import-button-prefix" msgstr "Importer %s" #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "Erreur d'importation :" #: src/app/main/ui/workspace/tokens/import/modal.cljs:273 @@ -7877,19 +7877,19 @@ msgid "workspace.tokens.individual-tokens" msgstr "Utiliser des tokens individuels" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "Couleur non valide : %s" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "Erreur d'importation : données du token non valides dans le fichier JSON." #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "Erreur lors de l'importation : nom du token non valide au format JSON." #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "« %s » n'est pas un nom de token valide.\n" "Les noms des tokens ne doivent pas comporter de lettres et de chiffres " @@ -7897,25 +7897,25 @@ msgstr "" "« $ »." #: src/app/main/data/workspace/tokens/errors.cljs:81 -msgid "workspace.tokens.invalid-text-case-token-value" +msgid "errors.tokens.invalid-text-case-token-value" msgstr "" "Valeur du token non valide : seules les valeurs Aucune, Majuscules, " "Minuscules ou Première lettre en capitale sont acceptées" #: src/app/main/data/workspace/tokens/errors.cljs:85 -msgid "workspace.tokens.invalid-text-decoration-token-value" +msgid "errors.tokens.invalid-text-decoration-token-value" msgstr "" "Valeur du token non valide : seules les valeurs none, underline et " "strike-through sont acceptées" #: src/app/main/data/workspace/tokens/errors.cljs:97 -msgid "workspace.tokens.invalid-token-value-typography" +msgid "errors.tokens.invalid-token-value-typography" msgstr "" "Valeur non valide : elle doit faire référence à un token de typographie " "composite." #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "Valeur du token non valide : %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -7951,7 +7951,7 @@ msgid "workspace.tokens.min-size" msgstr "Taille min." #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "Références du token manquantes : " #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 @@ -7991,7 +7991,7 @@ msgid "workspace.tokens.no-themes-currently" msgstr "Vous n'avez actuellement aucun thème." #: src/app/main/data/workspace/tokens/errors.cljs:19 -msgid "workspace.tokens.no-token-files-found" +msgid "errors.tokens.no-token-files-found" msgstr "Aucun token, collection ou thème n'ont été trouvés dans ce fichier." #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:134 @@ -7999,11 +7999,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "%s collections actives" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "Valeur du token non valide. La valeur est trop grande : %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "" "L'opacité doit être comprise entre 0 % et 100 % ou entre 0 et 1 (ex : 50 % " "ou 0.5)." @@ -8044,7 +8044,7 @@ msgid "workspace.tokens.select-set" msgstr "Sélectionner la collection." #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "Le token s'auto-référence" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 @@ -8079,7 +8079,7 @@ msgid "workspace.tokens.size" msgstr "Taille" #: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 -msgid "workspace.tokens.stroke-width-range" +msgid "errors.tokens.stroke-width-range" msgstr "La largueur du tracé doit être plus grand ou égal à 0." #: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:41, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:169 @@ -8167,11 +8167,11 @@ msgid "workspace.tokens.value-not-valid" msgstr "La valeur n'est pas valide" #: src/app/main/data/workspace/tokens/errors.cljs:69 -msgid "workspace.tokens.value-with-percent" +msgid "errors.tokens.value-with-percent" msgstr "Valeur non valide : % n'est pas autorisé." #: src/app/main/data/workspace/tokens/errors.cljs:65 -msgid "workspace.tokens.value-with-units" +msgid "errors.tokens.value-with-units" msgstr "Valeur non valide : les unités ne sont pas autorisées." #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 diff --git a/frontend/translations/he.po b/frontend/translations/he.po index ba2a305c09..fb480e527e 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -7516,7 +7516,7 @@ msgid "workspace.tokens.color" msgstr "צבע" #: src/app/main/data/workspace/tokens/errors.cljs:101, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:251 -msgid "workspace.tokens.composite-line-height-needs-font-size" +msgid "errors.tokens.composite-line-height-needs-font-size" msgstr "גובה השורה תלוי בגודל הגופן. יש להוסיף גודל גופן כדי לקבל את הערך הפתור." #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:57 @@ -7564,7 +7564,7 @@ msgid "workspace.tokens.edit-token" msgstr "עריכת אסימון %s" #: src/app/main/data/workspace/tokens/errors.cljs:41 -msgid "workspace.tokens.empty-input" +msgid "errors.tokens.empty-input" msgstr "ערך האסימון לא יכול להישאר ריק" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 @@ -7572,7 +7572,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "נא למלא את שם האסימון %s" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "שגיאת ייבוא: לא ניתן לפענח JSON." #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -7634,7 +7634,7 @@ msgid "workspace.tokens.import-button-prefix" msgstr "ייבוא %s" #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "שגיאת ייבוא:" #: src/app/main/ui/workspace/tokens/import/modal.cljs:273 @@ -7685,57 +7685,57 @@ msgid "workspace.tokens.individual-tokens" msgstr "להשתמש באסימונים עצמאיים" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "ערך צבע שגוי: %s" #: src/app/main/data/workspace/tokens/errors.cljs:93 -msgid "workspace.tokens.invalid-font-family-token-value" +msgid "errors.tokens.invalid-font-family-token-value" msgstr "ערך אסימון שגוי: אפשר להפנות לאסימון font-family (משפחת גופנים) בלבד" #: src/app/main/data/workspace/tokens/errors.cljs:89 -msgid "workspace.tokens.invalid-font-weight-token-value" +msgid "errors.tokens.invalid-font-weight-token-value" msgstr "" "ערך משקל גופן שגוי: יש להשתמש בערכים מספריים (100-‏950) או שמות תקניים " "(thin,‏ light,‏ regular,‏ bold ועוד), אפשר גם לצרף בסוף ‚Italic’ (נטוי) " "במקרה הצורך" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "שגיאת ייבוא: נתוני אסימון שגויים ב־JSON." #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "שגיאת ייבוא: שם אסימון שגוי ב־JSON." #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "„%s” אינו שם תקף לאסימון.\n" "שמות האסימונים יכולים להכיל אותיות וספרות מופרדים בתווי . ואסור שיתחילו " "בדולר ($)." #: src/app/main/data/workspace/tokens/errors.cljs:105 -msgid "workspace.tokens.invalid-shadow-type-token-value" +msgid "errors.tokens.invalid-shadow-type-token-value" msgstr "סוג הצללה שגוי: רק ‚innerShadow’ או ‚dropShadow’ מורשים" #: src/app/main/data/workspace/tokens/errors.cljs:81 -msgid "workspace.tokens.invalid-text-case-token-value" +msgid "errors.tokens.invalid-text-case-token-value" msgstr "ערך אסימון שגוי: רק none,‏ Uppercase,‏ Lowercase או Capitalize מורשים" #: src/app/main/data/workspace/tokens/errors.cljs:85 -msgid "workspace.tokens.invalid-text-decoration-token-value" +msgid "errors.tokens.invalid-text-decoration-token-value" msgstr "ערך אסימון שגוי: מותר רק none,‏ underline ו־strike-through" #: src/app/main/data/workspace/tokens/errors.cljs:117 -msgid "workspace.tokens.invalid-token-value-shadow" +msgid "errors.tokens.invalid-token-value-shadow" msgstr "ערך שגוי: יש להפנות לאסימון הצללה מורכבת." #: src/app/main/data/workspace/tokens/errors.cljs:97 -msgid "workspace.tokens.invalid-token-value-typography" +msgid "errors.tokens.invalid-token-value-typography" msgstr "ערך שגוי: חייב להפנות לאסימון טיפוגרפיה מרוכב." #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "ערך אסימון שגוי: %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -7775,7 +7775,7 @@ msgid "workspace.tokens.min-size" msgstr "גודל מזערי" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "חסרות הפניות אסימונים: " #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 @@ -7815,7 +7815,7 @@ msgid "workspace.tokens.no-themes-currently" msgstr "אין לך ערכות עיצוב עדיין." #: src/app/main/data/workspace/tokens/errors.cljs:19 -msgid "workspace.tokens.no-token-files-found" +msgid "errors.tokens.no-token-files-found" msgstr "לא נמצאו אסימונים, סדרות או ערכות עיצוב בקובץ הזה." #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:134 @@ -7823,11 +7823,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "%s סדרות פעילות" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "ערך אסימון שגוי. הערך הפתור גדול מדי: %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "שקיפות צריכה להיות בין 0 ל־100% או 0 ו־1 (כלומר 50% או 0.5)." #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 @@ -7874,7 +7874,7 @@ msgid "workspace.tokens.select-set" msgstr "בחירה ערכה." #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "לאסימון יש הפניה עצמית" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 @@ -7911,7 +7911,7 @@ msgid "workspace.tokens.shadow-blur" msgstr "טשטוש" #: src/app/main/data/workspace/tokens/errors.cljs:109 -msgid "workspace.tokens.shadow-blur-range" +msgid "errors.tokens.shadow-blur-range" msgstr "טשטוש הצל חייב להיות גדול או שווה ל־0." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988 @@ -7932,7 +7932,7 @@ msgid "workspace.tokens.shadow-spread" msgstr "התפרסות" #: src/app/main/data/workspace/tokens/errors.cljs:113 -msgid "workspace.tokens.shadow-spread-range" +msgid "errors.tokens.shadow-spread-range" msgstr "התפרסות ההצללה חייב להיות גדולה או שווה ל־0." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:1215 @@ -7953,7 +7953,7 @@ msgid "workspace.tokens.size" msgstr "גודל" #: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 -msgid "workspace.tokens.stroke-width-range" +msgid "errors.tokens.stroke-width-range" msgstr "עובי הקו חייב להיות גדול או שווה ל־0." #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:154 @@ -8046,11 +8046,11 @@ msgid "workspace.tokens.value-not-valid" msgstr "הערך לא תקף" #: src/app/main/data/workspace/tokens/errors.cljs:69 -msgid "workspace.tokens.value-with-percent" +msgid "errors.tokens.value-with-percent" msgstr "ערך שגוי: אסור %." #: src/app/main/data/workspace/tokens/errors.cljs:65 -msgid "workspace.tokens.value-with-units" +msgid "errors.tokens.value-with-units" msgstr "ערך שגוי: אסור יחידות." #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 diff --git a/frontend/translations/hi.po b/frontend/translations/hi.po index ff6bbda839..a13ef70427 100644 --- a/frontend/translations/hi.po +++ b/frontend/translations/hi.po @@ -7651,7 +7651,7 @@ msgid "workspace.tokens.color" msgstr "रंग" #: src/app/main/data/workspace/tokens/errors.cljs:101, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:251 -msgid "workspace.tokens.composite-line-height-needs-font-size" +msgid "errors.tokens.composite-line-height-needs-font-size" msgstr "" "पंक्ति की ऊँचाई फ़ॉन्ट आकार पर निर्भर करती है। हल किया गया मान प्राप्त करने " "के लिए फ़ॉन्ट आकार जोड़ें।" @@ -7701,7 +7701,7 @@ msgid "workspace.tokens.edit-token" msgstr "%s token संपादित करें" #: src/app/main/data/workspace/tokens/errors.cljs:41 -msgid "workspace.tokens.empty-input" +msgid "errors.tokens.empty-input" msgstr "टोकन मान रिक्त नहीं हो सकता" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 @@ -7709,7 +7709,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "%s टोकन नाम दर्ज करें" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "आयात त्रुटि: JSON को पार्स नहीं किया जा सका।" #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -7773,7 +7773,7 @@ msgid "workspace.tokens.import-button-prefix" msgstr "%s आयात करें" #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "आयात त्रुटि:" #: src/app/main/ui/workspace/tokens/import/modal.cljs:273 @@ -7824,60 +7824,60 @@ msgid "workspace.tokens.individual-tokens" msgstr "व्यक्तिगत टोकन का प्रयोग करें" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "अमान्य रंग मान: %s" #: src/app/main/data/workspace/tokens/errors.cljs:93 -msgid "workspace.tokens.invalid-font-family-token-value" +msgid "errors.tokens.invalid-font-family-token-value" msgstr "Invalid token value: आप केवल फ़ॉन्ट-परिवार token का संदर्भ ले सकते हैं" #: src/app/main/data/workspace/tokens/errors.cljs:89 -msgid "workspace.tokens.invalid-font-weight-token-value" +msgid "errors.tokens.invalid-font-weight-token-value" msgstr "" "अमान्य फ़ॉन्ट भार मान: संख्यात्मक मान (100-950) या मानक नाम (पतला, हल्का, " "नियमित, बोल्ड, आदि) का उपयोग करें, वैकल्पिक रूप से उसके बाद 'इटैलिक' लिखें" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "आयात त्रुटि: JSON में अमान्य टोकन डेटा।" #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "आयात त्रुटि: JSON में अमान्य टोकन नाम।" #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "\"%s\" एक मान्य टोकन नाम नहीं है।\n" "टोकन नामों में केवल अक्षर और अंक होने चाहिए, जो डॉट (.) से अलग किए गए हों, " "और $ चिह्न से शुरू नहीं होने चाहिए।" #: src/app/main/data/workspace/tokens/errors.cljs:105 -msgid "workspace.tokens.invalid-shadow-type-token-value" +msgid "errors.tokens.invalid-shadow-type-token-value" msgstr "अमान्य छाया प्रकार: केवल 'innerShadow' या 'dropShadow' स्वीकार किए जाते हैं" #: src/app/main/data/workspace/tokens/errors.cljs:81 -msgid "workspace.tokens.invalid-text-case-token-value" +msgid "errors.tokens.invalid-text-case-token-value" msgstr "" "अमान्य token मान: केवल कोई नहीं, अपरकेस, लोअरकेस या कैपिटलाइज़ स्वीकार किए " "जाते हैं" #: src/app/main/data/workspace/tokens/errors.cljs:85 -msgid "workspace.tokens.invalid-text-decoration-token-value" +msgid "errors.tokens.invalid-text-decoration-token-value" msgstr "" "अमान्य token मान: केवल कोई नहीं, रेखांकित और स्ट्राइक-थ्रू स्वीकार किए जाते " "हैं" #: src/app/main/data/workspace/tokens/errors.cljs:117 -msgid "workspace.tokens.invalid-token-value-shadow" +msgid "errors.tokens.invalid-token-value-shadow" msgstr "अमान्य मूल्य: एक समग्र छाया टोकन का संदर्भ देना चाहिए।।" #: src/app/main/data/workspace/tokens/errors.cljs:97 -msgid "workspace.tokens.invalid-token-value-typography" +msgid "errors.tokens.invalid-token-value-typography" msgstr "अमान्य मान: एक संयुक्त टाइपोग्राफी token का संदर्भ होना चाहिए।" #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "अमान्य टोकन मान: %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -7917,7 +7917,7 @@ msgid "workspace.tokens.min-size" msgstr "न्यूनतम. आकार" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "गुम टोकन संदर्भ: " #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 @@ -7957,7 +7957,7 @@ msgid "workspace.tokens.no-themes-currently" msgstr "आपके पास वर्तमान में कोई थीम नहीं है।" #: src/app/main/data/workspace/tokens/errors.cljs:19 -msgid "workspace.tokens.no-token-files-found" +msgid "errors.tokens.no-token-files-found" msgstr "इस फ़ाइल में कोई टोकन, सेट या थीम नहीं मिला।" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:134 @@ -7965,11 +7965,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "%s सक्रिय सेट" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "अमान्य टोकन मान. हल किया गया मान बहुत बड़ा है: %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "अपारदर्शिता 0 और 100% या 0 और 1 (जैसे 50% या 0.5) के बीच होनी चाहिए।" #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 @@ -8016,7 +8016,7 @@ msgid "workspace.tokens.select-set" msgstr "सेट का चयन करें।" #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "टोकन में स्व-संदर्भ होता है" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 @@ -8057,7 +8057,7 @@ msgid "workspace.tokens.shadow-blur" msgstr "धुंधला" #: src/app/main/data/workspace/tokens/errors.cljs:109 -msgid "workspace.tokens.shadow-blur-range" +msgid "errors.tokens.shadow-blur-range" msgstr "छाया धुंधलापन 0 से अधिक या उसके बराबर होना चाहिए।" #: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988 @@ -8078,7 +8078,7 @@ msgid "workspace.tokens.shadow-spread" msgstr "फैलाना" #: src/app/main/data/workspace/tokens/errors.cljs:113 -msgid "workspace.tokens.shadow-spread-range" +msgid "errors.tokens.shadow-spread-range" msgstr "छाया प्रसार 0 से अधिक या उसके बराबर होना चाहिए।" #: src/app/main/ui/workspace/tokens/management/create/form.cljs:1215 @@ -8099,7 +8099,7 @@ msgid "workspace.tokens.size" msgstr "आकार" #: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 -msgid "workspace.tokens.stroke-width-range" +msgid "errors.tokens.stroke-width-range" msgstr "स्ट्रोक की चौड़ाई 0 से बड़ी या उसके बराबर होनी चाहिए।" #: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 @@ -8200,11 +8200,11 @@ msgid "workspace.tokens.value-not-valid" msgstr "मान मान्य नहीं है" #: src/app/main/data/workspace/tokens/errors.cljs:69 -msgid "workspace.tokens.value-with-percent" +msgid "errors.tokens.value-with-percent" msgstr "अमान्य मान: % की अनुमति नहीं है।" #: src/app/main/data/workspace/tokens/errors.cljs:65 -msgid "workspace.tokens.value-with-units" +msgid "errors.tokens.value-with-units" msgstr "अमान्य मान: इकाइयाँ अनुमति नहीं हैं।" #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 diff --git a/frontend/translations/id.po b/frontend/translations/id.po index 8e610f193e..3c1d278095 100644 --- a/frontend/translations/id.po +++ b/frontend/translations/id.po @@ -6620,7 +6620,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "Masukkan nama token %s" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "Kesalahan Pengimporan: Tidak dapat mengurai JSON." #: src/app/main/ui/workspace/tokens/management/context_menu.cljs:240 @@ -6642,7 +6642,7 @@ msgid "workspace.tokens.grouping-set-alert" msgstr "Pengelompokan Set Token belum didukung." #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "Kesalahan Pengimporan:" #: src/app/main/ui/workspace/tokens/sidebar.cljs:414, src/app/main/ui/workspace/tokens/sidebar.cljs:415 @@ -6651,15 +6651,15 @@ msgid "workspace.tokens.import-tooltip" msgstr "Mengimpor berkas JSON akan menimpa semua token, set, dan tema Anda saat ini" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "Nilai warna tidak valid: %s" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "Kesalahan pengimporan: Data token tidak valid dalam JSON." #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "Nilai token tidak valid: %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -6691,7 +6691,7 @@ msgid "workspace.tokens.min-size" msgstr "Ukuran minimal" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "Referensi token hilang: " #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:135 @@ -6731,11 +6731,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "%s set aktif" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "Nilai token tidak valid. Nilai terurai terlalu besar: %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "Opasitas harus antara 0 dan 100% atau 0 dan 1 (mis. 50% atau 0.5)." #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 @@ -6774,7 +6774,7 @@ msgid "workspace.tokens.select-set" msgstr "Pilih set." #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "Token memiliki referensi diri" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 diff --git a/frontend/translations/it.po b/frontend/translations/it.po index 5dee5271c0..66b534c1c8 100644 --- a/frontend/translations/it.po +++ b/frontend/translations/it.po @@ -7840,7 +7840,7 @@ msgid "workspace.tokens.color" msgstr "Colore" #: src/app/main/data/workspace/tokens/errors.cljs:101, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:251 -msgid "workspace.tokens.composite-line-height-needs-font-size" +msgid "errors.tokens.composite-line-height-needs-font-size" msgstr "" "L'interlinea dipende dalla dimensione del carattere. Aggiungi una " "dimensione carattere per ottenere il valore risolto." @@ -7890,7 +7890,7 @@ msgid "workspace.tokens.edit-token" msgstr "Modifica token %s" #: src/app/main/data/workspace/tokens/errors.cljs:41 -msgid "workspace.tokens.empty-input" +msgid "errors.tokens.empty-input" msgstr "Il valore del token non può essere vuoto" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 @@ -7898,7 +7898,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "Inserisci il nome del token %s" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "Errore di importazione: Impossibile analizzare il JSON." #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -7962,7 +7962,7 @@ msgid "workspace.tokens.import-button-prefix" msgstr "Importa %s" #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "Errore di importazione:" #: src/app/main/ui/workspace/tokens/import/modal.cljs:273 @@ -8019,65 +8019,65 @@ msgid "workspace.tokens.individual-tokens" msgstr "Utilizza token individuali" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "Valore colore invalido: %s" #: src/app/main/data/workspace/tokens/errors.cljs:93 -msgid "workspace.tokens.invalid-font-family-token-value" +msgid "errors.tokens.invalid-font-family-token-value" msgstr "" "Valore del token non valido: puoi fare riferimento solo a un token " "font-family" #: src/app/main/data/workspace/tokens/errors.cljs:89 -msgid "workspace.tokens.invalid-font-weight-token-value" +msgid "errors.tokens.invalid-font-weight-token-value" msgstr "" "Valore del peso del font non valido: usa valori numerici (100-950) o nomi " "standard (thin, light, regular, bold, ecc.) eventualmente seguiti da " "'Italic'" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "Errore di importazione: Dati del token non validi nel JSON." #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "Errore di importazione: nome token non valido nel JSON." #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "\"%s\" non è un nome token valido\n" "I nomi dei token devono contenere solo lettere e cifre, separate dal " "carattere . e non devono iniziare con il simbolo $." #: src/app/main/data/workspace/tokens/errors.cljs:105 -msgid "workspace.tokens.invalid-shadow-type-token-value" +msgid "errors.tokens.invalid-shadow-type-token-value" msgstr "" "Tipologia ombra non valida: sono consentite solo 'innerShadow' o " "'dropShadow'" #: src/app/main/data/workspace/tokens/errors.cljs:81 -msgid "workspace.tokens.invalid-text-case-token-value" +msgid "errors.tokens.invalid-text-case-token-value" msgstr "" "Valore del token non valido: sono accettati solo none, Uppercase, Lowercase " "o Capitalize" #: src/app/main/data/workspace/tokens/errors.cljs:85 -msgid "workspace.tokens.invalid-text-decoration-token-value" +msgid "errors.tokens.invalid-text-decoration-token-value" msgstr "" "Valore del token non valido: sono accettati solo none, underline e " "strike-through" #: src/app/main/data/workspace/tokens/errors.cljs:117 -msgid "workspace.tokens.invalid-token-value-shadow" +msgid "errors.tokens.invalid-token-value-shadow" msgstr "Valore non valido: deve fare riferimento a un token ombra composito." #: src/app/main/data/workspace/tokens/errors.cljs:97 -msgid "workspace.tokens.invalid-token-value-typography" +msgid "errors.tokens.invalid-token-value-typography" msgstr "Valore non valido: deve fare riferimento a un token tipografico composito." #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "Valore token non valido: %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -8117,7 +8117,7 @@ msgid "workspace.tokens.min-size" msgstr "Dimensione min" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "Riferimenti al token mancanti: " #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 @@ -8169,7 +8169,7 @@ msgid "workspace.tokens.no-themes-currently" msgstr "Al momento non hai temi." #: src/app/main/data/workspace/tokens/errors.cljs:19 -msgid "workspace.tokens.no-token-files-found" +msgid "errors.tokens.no-token-files-found" msgstr "Nessun token, set o tema trovato in questo file." #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:134 @@ -8177,11 +8177,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "% set attivi" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "Valore token non valido. Il valore risolto è troppo grande: %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "" "L'opacità deve essere compresa tra 0 e 100% o tra 0 e 1 (ad esempio 50% o " "0.5)." @@ -8231,7 +8231,7 @@ msgid "workspace.tokens.select-set" msgstr "Seleziona set." #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "Il token ha un riferimento a se stesso" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 @@ -8270,7 +8270,7 @@ msgid "workspace.tokens.shadow-blur" msgstr "Sfocatura" #: src/app/main/data/workspace/tokens/errors.cljs:109 -msgid "workspace.tokens.shadow-blur-range" +msgid "errors.tokens.shadow-blur-range" msgstr "La sfocatura dell'ombra deve essere maggiore o uguale a 0." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988 @@ -8291,7 +8291,7 @@ msgid "workspace.tokens.shadow-spread" msgstr "Diffusione" #: src/app/main/data/workspace/tokens/errors.cljs:113 -msgid "workspace.tokens.shadow-spread-range" +msgid "errors.tokens.shadow-spread-range" msgstr "La diffusione dell'ombra deve essere maggiore o uguale a 0." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:1215 @@ -8321,7 +8321,7 @@ msgid "workspace.tokens.size" msgstr "Dimensione" #: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 -msgid "workspace.tokens.stroke-width-range" +msgid "errors.tokens.stroke-width-range" msgstr "La larghezza della traccia deve essere maggiore o uguale a 0." #: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 @@ -8432,11 +8432,11 @@ msgid "workspace.tokens.value-not-valid" msgstr "Il valore non è valido" #: src/app/main/data/workspace/tokens/errors.cljs:69 -msgid "workspace.tokens.value-with-percent" +msgid "errors.tokens.value-with-percent" msgstr "Valore non valido: % non è consentito." #: src/app/main/data/workspace/tokens/errors.cljs:65 -msgid "workspace.tokens.value-with-units" +msgid "errors.tokens.value-with-units" msgstr "Valore non valido: le unità non sono consentite." #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 diff --git a/frontend/translations/lv.po b/frontend/translations/lv.po index 21ada25f07..4ff0ea9372 100644 --- a/frontend/translations/lv.po +++ b/frontend/translations/lv.po @@ -7378,7 +7378,7 @@ msgid "workspace.tokens.edit-themes" msgstr "Labot izskatus" #: src/app/main/data/workspace/tokens/errors.cljs:41 -msgid "workspace.tokens.empty-input" +msgid "errors.tokens.empty-input" msgstr "Tekstvienības vērtība nevar būt tukša" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 @@ -7386,7 +7386,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "Jāievada %s tekstvienības nosaukums" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "Ievietošanas kļūda: nevarēja apstrādāt JSON." #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -7432,7 +7432,7 @@ msgid "workspace.tokens.grouping-set-alert" msgstr "Tekstvienību apkopošana vēl netiek nodrošināta." #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "Ievietošanas kļūda:" #: src/app/main/ui/workspace/tokens/import/modal.cljs:241 @@ -7473,26 +7473,26 @@ msgstr "" "redzētu izmaiņas" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "Nederīga krāsas vērtība: %s" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "Ievietošanas kļūda: JSON satur nederīgus tekstvienību datus." #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "Ievietošanas kļūda: nederīgs tekstvienības nosaukums JSON." #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "\"%s\" nav derīgs tekstvienības nosaukums.\n" "Tekstvienību nosaukumos var būt tikai burti un cipari, kas atdalīti ar " "rakstzīmi \".\", un tie nedrīkst sākties ar zīmi \"$\"." #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "Nederīga tekstvienības vērtība: %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -7524,7 +7524,7 @@ msgid "workspace.tokens.min-size" msgstr "Mazākais lielums" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "Trūkst tekstvienību atsauces: " #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:135 @@ -7564,11 +7564,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "%s spēkā esošas kopas" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "Nederīga tekstvienības vērtība. Aprēķinātā vērtība ir pārāk liela: %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "Necaurspīdīgumam ir jābūt starp 0 un 100 % vai 0 un 1 (piem., 50% jeb 0.5)." #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 @@ -7607,7 +7607,7 @@ msgid "workspace.tokens.select-set" msgstr "Atlasīt kopu." #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "Tekstvienībai ir atsauce uz sevi" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 @@ -7655,7 +7655,7 @@ msgid "workspace.tokens.size" msgstr "Izmērs" #: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 -msgid "workspace.tokens.stroke-width-range" +msgid "errors.tokens.stroke-width-range" msgstr "Vilkuma platumam ir jābūt vienādam vai lielākam par 0." #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:154 @@ -7729,7 +7729,7 @@ msgid "workspace.tokens.value-not-valid" msgstr "Vērtība nav derīga" #: src/app/main/data/workspace/tokens/errors.cljs:65 -msgid "workspace.tokens.value-with-units" +msgid "errors.tokens.value-with-units" msgstr "Nederīga vērtība: mērvienības nav atļautas." #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 diff --git a/frontend/translations/nl.po b/frontend/translations/nl.po index 421a89d990..eb024bf956 100644 --- a/frontend/translations/nl.po +++ b/frontend/translations/nl.po @@ -7854,7 +7854,7 @@ msgid "workspace.tokens.color" msgstr "Kleur" #: src/app/main/data/workspace/tokens/errors.cljs:101, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:251 -msgid "workspace.tokens.composite-line-height-needs-font-size" +msgid "errors.tokens.composite-line-height-needs-font-size" msgstr "" "Regelafstand is afhankelijk van de lettergrootte. Voeg een lettergrootte " "toe om de opgeloste waarde te verkrijgen." @@ -7904,7 +7904,7 @@ msgid "workspace.tokens.edit-token" msgstr "%s token bewerken" #: src/app/main/data/workspace/tokens/errors.cljs:41 -msgid "workspace.tokens.empty-input" +msgid "errors.tokens.empty-input" msgstr "De tokenwaarde mag niet leeg zijn" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 @@ -7912,7 +7912,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "Voer de tokennaam %s in" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "Fout bij importeren: Kan JSON niet verwerken." #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -7976,7 +7976,7 @@ msgid "workspace.tokens.import-button-prefix" msgstr "Importeren: %s" #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "Fout bij importeren:" #: src/app/main/ui/workspace/tokens/import/modal.cljs:273 @@ -8033,63 +8033,63 @@ msgid "workspace.tokens.individual-tokens" msgstr "Individuele tokens gebruiken" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "Ongeldige kleurwaarde: %s" #: src/app/main/data/workspace/tokens/errors.cljs:93 -msgid "workspace.tokens.invalid-font-family-token-value" +msgid "errors.tokens.invalid-font-family-token-value" msgstr "Ongeldige tokenwaarde: je kunt alleen verwijzen naar een font-family-token" #: src/app/main/data/workspace/tokens/errors.cljs:89 -msgid "workspace.tokens.invalid-font-weight-token-value" +msgid "errors.tokens.invalid-font-weight-token-value" msgstr "" "Ongeldige gewichtswaarde lettertype: gebruik numerieke waarden (100-950) of " "standaardtermen (thin, light, regular, bold, etc.) optioneel gevolgd door " "'Italic'" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "Fout bij importeren: Ongeldige tokengegevens in JSON." #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "Importfout: Ongeldige tokennaam in JSON." #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "\"%s\" is geen geldige tokennaam.\n" "Tokennamen mogen alleen letters en cijfers bevatten, gescheiden door . " "tekens en mogen niet beginnen met een $-teken." #: src/app/main/data/workspace/tokens/errors.cljs:105 -msgid "workspace.tokens.invalid-shadow-type-token-value" +msgid "errors.tokens.invalid-shadow-type-token-value" msgstr "" "Ongeldig schaduwtype: alleen 'innerShadow' of 'dropShadow' worden " "geaccepteerd" #: src/app/main/data/workspace/tokens/errors.cljs:81 -msgid "workspace.tokens.invalid-text-case-token-value" +msgid "errors.tokens.invalid-text-case-token-value" msgstr "" "Ongeldige tokenwaarde: alleen none, Uppercase, Lowercase of Capitalize zijn " "toegestaan" #: src/app/main/data/workspace/tokens/errors.cljs:85 -msgid "workspace.tokens.invalid-text-decoration-token-value" +msgid "errors.tokens.invalid-text-decoration-token-value" msgstr "" "Ongeldige tokenwaarde: alleen none, underline en strike-through zijn " "toegestaan" #: src/app/main/data/workspace/tokens/errors.cljs:117 -msgid "workspace.tokens.invalid-token-value-shadow" +msgid "errors.tokens.invalid-token-value-shadow" msgstr "Ongeldige waarde: moet verwijzen naar een samengesteld schaduwtoken." #: src/app/main/data/workspace/tokens/errors.cljs:97 -msgid "workspace.tokens.invalid-token-value-typography" +msgid "errors.tokens.invalid-token-value-typography" msgstr "Ongeldige waarde: moet verwijzen naar een samengesteld typografietoken." #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "Ongeldige tokenwaarde: %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -8129,7 +8129,7 @@ msgid "workspace.tokens.min-size" msgstr "Min. grootte" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "Ontbrekende tokenverwijzingen: " #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 @@ -8181,7 +8181,7 @@ msgid "workspace.tokens.no-themes-currently" msgstr "Je hebt momenteel geen thema's." #: src/app/main/data/workspace/tokens/errors.cljs:19 -msgid "workspace.tokens.no-token-files-found" +msgid "errors.tokens.no-token-files-found" msgstr "Er zijn geen tokens, verzamelingen of thema's gevonden in dit bestand." #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:134 @@ -8189,11 +8189,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "%s actieve verzamelingen" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "Ongeldige tokenwaarde. De opgeloste waarde is te groot: %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "De dekking moet tussen 0 en 100% of 0 en 1 zijn (bijv. 50% of 0,5)." #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 @@ -8245,7 +8245,7 @@ msgid "workspace.tokens.select-set" msgstr "Verzameling kiezen." #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "Token bevat cirkelverwijzing" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 @@ -8286,7 +8286,7 @@ msgid "workspace.tokens.shadow-blur" msgstr "Vervanging" #: src/app/main/data/workspace/tokens/errors.cljs:109 -msgid "workspace.tokens.shadow-blur-range" +msgid "errors.tokens.shadow-blur-range" msgstr "Schaduwonscherpte moet groter zijn dan of gelijk zijn aan 0." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988 @@ -8307,7 +8307,7 @@ msgid "workspace.tokens.shadow-spread" msgstr "Spreiding" #: src/app/main/data/workspace/tokens/errors.cljs:113 -msgid "workspace.tokens.shadow-spread-range" +msgid "errors.tokens.shadow-spread-range" msgstr "Schaduwspreiding moet groter zijn dan of gelijk zijn aan 0." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:1215 @@ -8337,7 +8337,7 @@ msgid "workspace.tokens.size" msgstr "Grootte" #: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 -msgid "workspace.tokens.stroke-width-range" +msgid "errors.tokens.stroke-width-range" msgstr "De dikte van de streek moet groter zijn dan of gelijk zijn aan 0." #: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 @@ -8448,11 +8448,11 @@ msgid "workspace.tokens.value-not-valid" msgstr "De waarde is niet geldig" #: src/app/main/data/workspace/tokens/errors.cljs:69 -msgid "workspace.tokens.value-with-percent" +msgid "errors.tokens.value-with-percent" msgstr "Ongeldige waarde: % is niet toegestaan." #: src/app/main/data/workspace/tokens/errors.cljs:65 -msgid "workspace.tokens.value-with-units" +msgid "errors.tokens.value-with-units" msgstr "Ongeldige waarde: Eenheden zijn niet toegestaan." #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index 0d5516e26d..8995460842 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -7492,7 +7492,7 @@ msgid "workspace.tokens.color" msgstr "Culoare" #: src/app/main/data/workspace/tokens/errors.cljs:101, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:251 -msgid "workspace.tokens.composite-line-height-needs-font-size" +msgid "errors.tokens.composite-line-height-needs-font-size" msgstr "" "Înălțimea liniei depinde de dimensiunea fontului. Adaugă o dimensiune " "pentru font pentru a primi valoarea rezultată." @@ -7542,7 +7542,7 @@ msgid "workspace.tokens.edit-token" msgstr "Editează token %s" #: src/app/main/data/workspace/tokens/errors.cljs:41 -msgid "workspace.tokens.empty-input" +msgid "errors.tokens.empty-input" msgstr "Valoarea token-ului nu poate fi goală" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 @@ -7550,7 +7550,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "Introdu numele token-ului %s" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "Eroare la import: Nu s-a putut interpreta JSON." #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -7610,7 +7610,7 @@ msgid "workspace.tokens.import-button-prefix" msgstr "Importă %s" #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "Eroare import:" #: src/app/main/ui/workspace/tokens/import/modal.cljs:273 @@ -7667,49 +7667,49 @@ msgid "workspace.tokens.individual-tokens" msgstr "Folosește token-uri individuale" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "Valoare culoare invalidă: %s" #: src/app/main/data/workspace/tokens/errors.cljs:89 -msgid "workspace.tokens.invalid-font-weight-token-value" +msgid "errors.tokens.invalid-font-weight-token-value" msgstr "" "Valoare greutate font invalidă: folosește valori numerice (100-950) sau " "nume standardizate (thin, light, regular, bold, etc.) opțional urmate de " "'italic'" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "Eroare import: Date token invalide în fișierul JSON." #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "Eroare import: Nume token invalid în fișierul JSON." #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "\"%s\" nu este nu nume valid pentru token.\n" "Numele token-urilor trebuie să conțină doar litere și cifre separate de " "caracterul . și nu trebuie să înceapă cu un semn $." #: src/app/main/data/workspace/tokens/errors.cljs:81 -msgid "workspace.tokens.invalid-text-case-token-value" +msgid "errors.tokens.invalid-text-case-token-value" msgstr "" "Valoare token invalidă: doar none, Uppercase, Lowercase sau Capitalize sunt " "acceptate" #: src/app/main/data/workspace/tokens/errors.cljs:85 -msgid "workspace.tokens.invalid-text-decoration-token-value" +msgid "errors.tokens.invalid-text-decoration-token-value" msgstr "Valoare token invalidă: doar none, underline și strike-trough sunt acceptate" #: src/app/main/data/workspace/tokens/errors.cljs:97 -msgid "workspace.tokens.invalid-token-value-typography" +msgid "errors.tokens.invalid-token-value-typography" msgstr "" "Valoare invalidă: trebuie să facă referință la un token de tipografie " "compozită." #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "Valoare token invalidă: %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -7749,7 +7749,7 @@ msgid "workspace.tokens.min-size" msgstr "Dimensiune minimă" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "Referințe token lipsă: " #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 @@ -7789,7 +7789,7 @@ msgid "workspace.tokens.no-themes-currently" msgstr "În prezent nu ai teme." #: src/app/main/data/workspace/tokens/errors.cljs:19 -msgid "workspace.tokens.no-token-files-found" +msgid "errors.tokens.no-token-files-found" msgstr "Nu s-au găsit token-uri, seturi sau teme în acest fișier." #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:134 @@ -7797,11 +7797,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "%s seturi active" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "Valoare token invalidă. Valoarea rezultată este prea mare: %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "Opacitatea trebuie să fie între 0 și 100% sau 0 și 1 (ex: 50% sau 0.5)." #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 @@ -7844,7 +7844,7 @@ msgid "workspace.tokens.select-set" msgstr "Selectează setul." #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "Token-ul își face referință singur" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 @@ -7881,7 +7881,7 @@ msgid "workspace.tokens.size" msgstr "Dimensiune" #: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 -msgid "workspace.tokens.stroke-width-range" +msgid "errors.tokens.stroke-width-range" msgstr "Lățimea conturului trebuie să fie mai mare sau egală cu 0." #: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 @@ -7973,11 +7973,11 @@ msgid "workspace.tokens.value-not-valid" msgstr "Valoarea este invalidă" #: src/app/main/data/workspace/tokens/errors.cljs:69 -msgid "workspace.tokens.value-with-percent" +msgid "errors.tokens.value-with-percent" msgstr "Valoare invalidă: % nu este permis." #: src/app/main/data/workspace/tokens/errors.cljs:65 -msgid "workspace.tokens.value-with-units" +msgid "errors.tokens.value-with-units" msgstr "Valoare invalidă: Unitățile nu sunt permise." #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 diff --git a/frontend/translations/sv.po b/frontend/translations/sv.po index 4e007bcdc3..a25e459c0b 100644 --- a/frontend/translations/sv.po +++ b/frontend/translations/sv.po @@ -7567,7 +7567,7 @@ msgid "workspace.tokens.color" msgstr "Färg" #: src/app/main/data/workspace/tokens/errors.cljs:101, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:251 -msgid "workspace.tokens.composite-line-height-needs-font-size" +msgid "errors.tokens.composite-line-height-needs-font-size" msgstr "" "Radhöjden beror på teckenstorleken. Lägg till en teckenstorlek för att få " "det uträknade värdet." @@ -7617,7 +7617,7 @@ msgid "workspace.tokens.edit-token" msgstr "Redigera %s token" #: src/app/main/data/workspace/tokens/errors.cljs:41 -msgid "workspace.tokens.empty-input" +msgid "errors.tokens.empty-input" msgstr "Token-värdet kan inte vara tomt" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 @@ -7625,7 +7625,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "Ange %s tokennamn" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "Importeringsfel: Kunde inte tolka JSON." #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -7689,7 +7689,7 @@ msgid "workspace.tokens.import-button-prefix" msgstr "Importera %s" #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "Importeringsfel:" #: src/app/main/ui/workspace/tokens/import/modal.cljs:273 @@ -7746,58 +7746,58 @@ msgid "workspace.tokens.individual-tokens" msgstr "Använd individuella tokens" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "Ogiltigt färgvärde: %s" #: src/app/main/data/workspace/tokens/errors.cljs:93 -msgid "workspace.tokens.invalid-font-family-token-value" +msgid "errors.tokens.invalid-font-family-token-value" msgstr "Ogiltigt token-värde: du kan bara referera till en teckensnittsfamiljs-token" #: src/app/main/data/workspace/tokens/errors.cljs:89 -msgid "workspace.tokens.invalid-font-weight-token-value" +msgid "errors.tokens.invalid-font-weight-token-value" msgstr "" "Ogiltigt värde för typsnittsvikt: använd numeriska värden (100–950) eller " "standardnamn (smal, lätt, normal, fet, etc.) eventuellt följt av 'Kursiv'" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "Importeringsfel: Ogiltig token-data i JSON." #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "Importeringsfel: Ogiltigt token-namn i JSON." #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "\"%s\" är inte ett giltigt token-namn.\n" "Token-namn ska endast innehålla bokstäver och siffror separerade med . " "tecken och får inte börja med ett $-tecken." #: src/app/main/data/workspace/tokens/errors.cljs:105 -msgid "workspace.tokens.invalid-shadow-type-token-value" +msgid "errors.tokens.invalid-shadow-type-token-value" msgstr "Ogiltig skuggningstyp: endast 'innerShadow' eller 'dropShadow' accepteras" #: src/app/main/data/workspace/tokens/errors.cljs:81 -msgid "workspace.tokens.invalid-text-case-token-value" +msgid "errors.tokens.invalid-text-case-token-value" msgstr "" "Ogiltigt token-värde: endast ingen, versaler, gemener eller versalisera " "accepteras" #: src/app/main/data/workspace/tokens/errors.cljs:85 -msgid "workspace.tokens.invalid-text-decoration-token-value" +msgid "errors.tokens.invalid-text-decoration-token-value" msgstr "Ogiltigt token-värde: endast ingen, understruken och genomstruken accepteras" #: src/app/main/data/workspace/tokens/errors.cljs:117 -msgid "workspace.tokens.invalid-token-value-shadow" +msgid "errors.tokens.invalid-token-value-shadow" msgstr "Ogiltigt värde: måste referera till en sammansatt skuggnings-token." #: src/app/main/data/workspace/tokens/errors.cljs:97 -msgid "workspace.tokens.invalid-token-value-typography" +msgid "errors.tokens.invalid-token-value-typography" msgstr "Ogiltigt värde: måste referera till en sammansatt typografi-token." #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "Ogiltigt token-värde: %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -7837,7 +7837,7 @@ msgid "workspace.tokens.min-size" msgstr "Min. storlek" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "Saknade token-referenser: " #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 @@ -7877,7 +7877,7 @@ msgid "workspace.tokens.no-themes-currently" msgstr "Du har för närvarande inga teman." #: src/app/main/data/workspace/tokens/errors.cljs:19 -msgid "workspace.tokens.no-token-files-found" +msgid "errors.tokens.no-token-files-found" msgstr "Inga tokens, uppsättningar eller teman hittades i den här filen." #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:134 @@ -7885,11 +7885,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "%s aktiva uppsättningar" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "Ogiltigt token-värde. Det uträknade värdet är för stort: %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "" "Opaciteten måste vara mellan 0 och 100 % eller 0 och 1 (t.ex. 50 % eller " "0,5)." @@ -7938,7 +7938,7 @@ msgid "workspace.tokens.select-set" msgstr "Välj uppsättning." #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "Token har självreferens" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 @@ -7977,7 +7977,7 @@ msgid "workspace.tokens.shadow-blur" msgstr "Oskärpa" #: src/app/main/data/workspace/tokens/errors.cljs:109 -msgid "workspace.tokens.shadow-blur-range" +msgid "errors.tokens.shadow-blur-range" msgstr "Skuggningsoskärpan måste vara större än eller lika med 0." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988 @@ -7998,7 +7998,7 @@ msgid "workspace.tokens.shadow-spread" msgstr "Spridning" #: src/app/main/data/workspace/tokens/errors.cljs:113 -msgid "workspace.tokens.shadow-spread-range" +msgid "errors.tokens.shadow-spread-range" msgstr "Skuggningsspridning måste vara större än eller lika med 0." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:1215 @@ -8019,7 +8019,7 @@ msgid "workspace.tokens.size" msgstr "Storlek" #: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 -msgid "workspace.tokens.stroke-width-range" +msgid "errors.tokens.stroke-width-range" msgstr "Streckbredden måste vara större än eller lika med 0." #: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 @@ -8120,11 +8120,11 @@ msgid "workspace.tokens.value-not-valid" msgstr "Värdet är inte giltigt" #: src/app/main/data/workspace/tokens/errors.cljs:69 -msgid "workspace.tokens.value-with-percent" +msgid "errors.tokens.value-with-percent" msgstr "Ogiltigt värde: % är inte tillåtet." #: src/app/main/data/workspace/tokens/errors.cljs:65 -msgid "workspace.tokens.value-with-units" +msgid "errors.tokens.value-with-units" msgstr "Ogiltigt värde: Enheter är ej tillåtna." #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index e8e2948403..de052b4043 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -7814,7 +7814,7 @@ msgid "workspace.tokens.color" msgstr "Renk" #: src/app/main/data/workspace/tokens/errors.cljs:101, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:251 -msgid "workspace.tokens.composite-line-height-needs-font-size" +msgid "errors.tokens.composite-line-height-needs-font-size" msgstr "" "Satır yüksekliği yazı tipi boyutuna bağlıdır. Çözülen değeri elde etmek " "için bir yazı tipi boyutu ekleyin." @@ -7864,7 +7864,7 @@ msgid "workspace.tokens.edit-token" msgstr "%s tokenini düzenle" #: src/app/main/data/workspace/tokens/errors.cljs:41 -msgid "workspace.tokens.empty-input" +msgid "errors.tokens.empty-input" msgstr "Token değeri boş olamaz" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 @@ -7872,7 +7872,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "%s token adını gir" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "İçe Aktarma Hatası: JSON ayrıştırılamadı." #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -7936,7 +7936,7 @@ msgid "workspace.tokens.import-button-prefix" msgstr "%s içe aktar" #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "İçe Aktarma Hatası:" #: src/app/main/ui/workspace/tokens/import/modal.cljs:273 @@ -7993,61 +7993,61 @@ msgid "workspace.tokens.individual-tokens" msgstr "Bireysel tokenler kullan" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "Geçersiz renk değeri: %s" #: src/app/main/data/workspace/tokens/errors.cljs:93 -msgid "workspace.tokens.invalid-font-family-token-value" +msgid "errors.tokens.invalid-font-family-token-value" msgstr "Geçersiz token değeri: yalnızca font-family tokenine referans verebilirsiniz" #: src/app/main/data/workspace/tokens/errors.cljs:89 -msgid "workspace.tokens.invalid-font-weight-token-value" +msgid "errors.tokens.invalid-font-weight-token-value" msgstr "" "Geçersiz yazı tipi kalınlığı değeri: sayısal değerler (100-950) veya " "standart adlar (ince, hafif, normal, kalın vb.) kullanın, isteğe bağlı " "olarak ardından 'İtalik' ekleyin" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "İçe Aktarma Hatası: JSON'da geçersiz token verisi." #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "İçe Aktarma Hatası: JSON'da geçersiz token adı." #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "\"%s\" geçerli bir token adı değil.\n" "Token adları yalnızca . karakterleriyle ayrılan harfler ve rakamlar " "içermeli ve $ işaretiyle başlamamalıdır." #: src/app/main/data/workspace/tokens/errors.cljs:105 -msgid "workspace.tokens.invalid-shadow-type-token-value" +msgid "errors.tokens.invalid-shadow-type-token-value" msgstr "Geçersiz gölge türü: yalnızca 'innerShadow' veya 'dropShadow' kabul edilir" #: src/app/main/data/workspace/tokens/errors.cljs:81 -msgid "workspace.tokens.invalid-text-case-token-value" +msgid "errors.tokens.invalid-text-case-token-value" msgstr "" "Geçersiz token değeri: yalnızca none, Uppercase, Lowercase veya Capitalize " "kabul edilir" #: src/app/main/data/workspace/tokens/errors.cljs:85 -msgid "workspace.tokens.invalid-text-decoration-token-value" +msgid "errors.tokens.invalid-text-decoration-token-value" msgstr "" "Geçersiz token değeri: yalnızca none, underline ve strike-through kabul " "edilir" #: src/app/main/data/workspace/tokens/errors.cljs:117 -msgid "workspace.tokens.invalid-token-value-shadow" +msgid "errors.tokens.invalid-token-value-shadow" msgstr "Geçersiz değer: bileşik gölge tokenine referans vermelidir." #: src/app/main/data/workspace/tokens/errors.cljs:97 -msgid "workspace.tokens.invalid-token-value-typography" +msgid "errors.tokens.invalid-token-value-typography" msgstr "Geçersiz değer: bileşik tipografi tokenine referans vermelidir." #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "Geçersiz token değeri: %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -8087,7 +8087,7 @@ msgid "workspace.tokens.min-size" msgstr "Asgari boyut" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "Eksik token referansları: " #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 @@ -8139,7 +8139,7 @@ msgid "workspace.tokens.no-themes-currently" msgstr "Şu anda hiç temanız yok." #: src/app/main/data/workspace/tokens/errors.cljs:19 -msgid "workspace.tokens.no-token-files-found" +msgid "errors.tokens.no-token-files-found" msgstr "Bu dosyada token, küme veya tema bulunamadı." #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:134 @@ -8147,11 +8147,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "%s etkin küme" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "Geçersiz token değeri. Çözülen değer çok büyük: %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "Opaklık 0 ile %100 veya 0 ile 1 arasında olmalıdır (örneğin %50 veya 0.5)." #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 @@ -8203,7 +8203,7 @@ msgid "workspace.tokens.select-set" msgstr "Küme seç." #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "Tokenin kendine referansı var" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 @@ -8244,7 +8244,7 @@ msgid "workspace.tokens.shadow-blur" msgstr "Bulanıklık" #: src/app/main/data/workspace/tokens/errors.cljs:109 -msgid "workspace.tokens.shadow-blur-range" +msgid "errors.tokens.shadow-blur-range" msgstr "Gölge bulanıklığı 0 veya 0'dan büyük olmalıdır." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988 @@ -8265,7 +8265,7 @@ msgid "workspace.tokens.shadow-spread" msgstr "Yayılım" #: src/app/main/data/workspace/tokens/errors.cljs:113 -msgid "workspace.tokens.shadow-spread-range" +msgid "errors.tokens.shadow-spread-range" msgstr "Gölge yayılımı 0 veya 0'dan büyük olmalıdır." #: src/app/main/ui/workspace/tokens/management/create/form.cljs:1215 @@ -8295,7 +8295,7 @@ msgid "workspace.tokens.size" msgstr "Boyut" #: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 -msgid "workspace.tokens.stroke-width-range" +msgid "errors.tokens.stroke-width-range" msgstr "Çerçeve genişliği 0'dan büyük veya 0 olmalıdır." #: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:40, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 @@ -8406,11 +8406,11 @@ msgid "workspace.tokens.value-not-valid" msgstr "Değer geçerli değil" #: src/app/main/data/workspace/tokens/errors.cljs:69 -msgid "workspace.tokens.value-with-percent" +msgid "errors.tokens.value-with-percent" msgstr "Geçersiz değer: % izin verilmiyor." #: src/app/main/data/workspace/tokens/errors.cljs:65 -msgid "workspace.tokens.value-with-units" +msgid "errors.tokens.value-with-units" msgstr "Geçersiz değer: Birimlere izin verilmiyor." #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 diff --git a/frontend/translations/ukr_UA.po b/frontend/translations/ukr_UA.po index 05796dbff1..5755f96291 100644 --- a/frontend/translations/ukr_UA.po +++ b/frontend/translations/ukr_UA.po @@ -7106,7 +7106,7 @@ msgid "workspace.tokens.edit-themes" msgstr "Редагувати теми" #: src/app/main/data/workspace/tokens/errors.cljs:41 -msgid "workspace.tokens.empty-input" +msgid "errors.tokens.empty-input" msgstr "Значення токену не може бути порожнім" #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 @@ -7114,7 +7114,7 @@ msgid "workspace.tokens.enter-token-name" msgstr "Вкажіть %s ім'я токену" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "Помилка імпорту: Неможливо обробити JSON." #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -7160,7 +7160,7 @@ msgid "workspace.tokens.grouping-set-alert" msgstr "Групування наборів токенів поки не підтримується." #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "Помилка імпорту:" #: src/app/main/ui/workspace/tokens/import/modal.cljs:241 @@ -7197,26 +7197,26 @@ msgstr "" "області перегляду" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "Помилкове значення кольору: %s" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "Помилка імпорту: Помилкові дани токену в JSON." #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "Помилка імпорту: Помилкове імʼя токену в JSON." #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "\"%s\" це не дійсне імʼя токену.\n" "Імена токену можуть містити в собі літери та цифри, розділені крапкою та не " "повинні починатись з $." #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "Помилкове значення токену: %s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -7248,7 +7248,7 @@ msgid "workspace.tokens.min-size" msgstr "Мін. розмір" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "Відсутні посилання на токен: " #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:135 @@ -7288,11 +7288,11 @@ msgid "workspace.tokens.num-active-sets" msgstr "%s активних наборів" #: src/app/main/data/workspace/tokens/errors.cljs:53 -msgid "workspace.tokens.number-too-large" +msgid "errors.tokens.number-too-large" msgstr "Помилкове значення токену. Отримане значення завелике: %s" #: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15 -msgid "workspace.tokens.opacity-range" +msgid "errors.tokens.opacity-range" msgstr "Непрозорість має бути між 0 та 100% або ж між 0 та 1 (де 0.5 - 50%)." #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:121 @@ -7331,7 +7331,7 @@ msgid "workspace.tokens.select-set" msgstr "Обрати набір." #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "Токен має самопосилання" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 @@ -7366,7 +7366,7 @@ msgid "workspace.tokens.size" msgstr "Розмір" #: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19 -msgid "workspace.tokens.stroke-width-range" +msgid "errors.tokens.stroke-width-range" msgstr "Ширина обведення має бути більшим ніж 0 або дорівнювати 0." #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:154 @@ -7430,7 +7430,7 @@ msgid "workspace.tokens.value-not-valid" msgstr "Значення не є дійсним" #: src/app/main/data/workspace/tokens/errors.cljs:65 -msgid "workspace.tokens.value-with-units" +msgid "errors.tokens.value-with-units" msgstr "Помилкове значення: Одиниці не дозволені." #: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166 diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po index 8da069884e..49dfdfabdc 100644 --- a/frontend/translations/zh_CN.po +++ b/frontend/translations/zh_CN.po @@ -6903,7 +6903,7 @@ msgid "workspace.tokens.edit-themes" msgstr "编辑主题" #: src/app/main/data/workspace/tokens/errors.cljs:15 -msgid "workspace.tokens.error-parse" +msgid "errors.tokens.error-parse" msgstr "导入错误:不能解析 JSON。" #: src/app/main/ui/workspace/tokens/export/modal.cljs:49 @@ -6940,7 +6940,7 @@ msgid "workspace.tokens.import-button-prefix" msgstr "导入 %s" #: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37 -msgid "workspace.tokens.import-error" +msgid "errors.tokens.import-error" msgstr "导入错误:" #: src/app/main/ui/workspace/tokens/import/modal.cljs:273 @@ -6973,37 +6973,37 @@ msgid "workspace.tokens.inactive-set-description" msgstr "此设置未启用。请更改主题或启用此设置以在视口中查看更改" #: src/app/main/data/workspace/tokens/errors.cljs:49 -msgid "workspace.tokens.invalid-color" +msgid "errors.tokens.invalid-color" msgstr "无效的颜色值:%s" #: src/app/main/data/workspace/tokens/errors.cljs:89 -msgid "workspace.tokens.invalid-font-weight-token-value" +msgid "errors.tokens.invalid-font-weight-token-value" msgstr "字体粗细值无效:使用数值(100-950)或标准名称(细、亮、常规、粗体等),后跟可选的“斜体”" #: src/app/main/data/workspace/tokens/errors.cljs:23 -msgid "workspace.tokens.invalid-json" +msgid "errors.tokens.invalid-json" msgstr "导入错误:JSON 中存在无效的令牌数据。" #: src/app/main/data/workspace/tokens/errors.cljs:27 -msgid "workspace.tokens.invalid-json-token-name" +msgid "errors.tokens.invalid-json-token-name" msgstr "导入错误;JSON中存在无效的令牌名。" #: src/app/main/data/workspace/tokens/errors.cljs:28 -msgid "workspace.tokens.invalid-json-token-name-detail" +msgid "errors.tokens.invalid-json-token-name-detail" msgstr "" "“%s” 不是有效的 token 名称。\n" "token 名称只能包含字母和数字,并以 . 字符分隔,并且不能以 $ 符号开头。" #: src/app/main/data/workspace/tokens/errors.cljs:81 -msgid "workspace.tokens.invalid-text-case-token-value" +msgid "errors.tokens.invalid-text-case-token-value" msgstr "无效的token值:仅接受无、大写、小写或大写" #: src/app/main/data/workspace/tokens/errors.cljs:85 -msgid "workspace.tokens.invalid-text-decoration-token-value" +msgid "errors.tokens.invalid-text-decoration-token-value" msgstr "无效的token值:仅接受无、下划线和删除线" #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 -msgid "workspace.tokens.invalid-value" +msgid "errors.tokens.invalid-value" msgstr "无效的令牌值:%s" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:205 @@ -7035,7 +7035,7 @@ msgid "workspace.tokens.min-size" msgstr "最小尺寸" #: src/app/main/data/workspace/tokens/errors.cljs:57 -msgid "workspace.tokens.missing-references" +msgid "errors.tokens.missing-references" msgstr "缺少token引用: " #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:124 @@ -7075,7 +7075,7 @@ msgid "workspace.tokens.no-themes-currently" msgstr "您目前没有主题。" #: src/app/main/data/workspace/tokens/errors.cljs:19 -msgid "workspace.tokens.no-token-files-found" +msgid "errors.tokens.no-token-files-found" msgstr "在此文件中未找到任何标记、集合或主题。" #: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:134 @@ -7118,7 +7118,7 @@ msgid "workspace.tokens.select-set" msgstr "选择集。" #: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/forms/shadow.cljs:299, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:243 -msgid "workspace.tokens.self-reference" +msgid "errors.tokens.self-reference" msgstr "Token存在自我引用" #: src/app/main/ui/workspace/tokens/sets/lists.cljs:60 From 4902037c7db39be07a701db310b93c6885d96ed5 Mon Sep 17 00:00:00 2001 From: Juan Flores <112629487+juan-flores077@users.noreply.github.com> Date: Thu, 30 Apr 2026 06:41:04 -0700 Subject: [PATCH 272/288] :sparkles: Add HEX, HSB, and HSL support in the third color tab (#9134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: Add HEX, HSB, and HSL support in the third color tab Relabel the existing HSVA tab to HSBA (the math was already HSB) and add an inline HSB ↔ HSL model toggle inside the tab, matching Figma's color panel. Sliders, gradients, and labels update dynamically per mode; HSL values roundtrip through RGB/HSV so no color-storage changes are needed. Model choice persists across sessions. * :lipstick: Fix lint errors Signed-off-by: juan-flores077 * :bug: Fix Plugin API token application for JS array of strings (#9166) * :bug: Fix Plugin API token application for JS array of strings Plugin code calling `shape.applyToken(token, ["fill"])` or `token.applyToShapes([rect], ["fill"])` from JavaScript supplies a JS array of strings. The plugin proxies expected a Clojure set of keywords, and two coupled defects made the calls silently no-op (or, with `throwValidationErrors` enabled, throw "check error"): 1. `token-attr-plugin->token-attr` only consulted its alias map when the input was already a keyword. String inputs like "fill" fell through to the identity branch, so the downstream `cto/token-attr?` predicate (which checks against a set of keywords) returned false for every string. Coerce strings to keywords first. 2. The `applyToken` / `applyToShapes` / `applyToSelected` schemas used plain `[:set ...]`, which has no `:decode/json` transformer for JS array → Clojure set coercion. Switch to the registered `[::sm/set ...]` (in `app.common.schema`) which provides the array → set decoder. After the switch, the standard JSON pipeline converts `["fill"]` to `#{"fill"}`, then the inner `[:and ::sm/keyword [:fn token-attr?]]` decodes each element to a keyword and validates it. Also extends the docstring on `token-attr-plugin->token-attr` to make the string-friendly contract explicit, and registers a new `tokens-test` ns under `frontend/test/frontend_tests/plugins/` with six `deftest` blocks covering: - known keywords passing through unchanged - keyword aliases (`:r1` → `:border-radius-top-left`, etc.) - string inputs coerced to keywords (regression for #9162) - `token-attr?` accepting both keyword and string inputs - `token-attr?` rejecting unknown attrs and nil Closes #9162 * :bug: Fix wrong direction in plugin-name alias tests The added tests in tokens_test.cljs and the new docstring in tokens.cljs described the alias resolution in the wrong direction. The map is {:r1 :border-radius-top-left, …} then map-invert'd, so token-attr-plugin->token-attr maps verbose plugin-side names (:border-radius-top-left) to canonical internal short names (:r1), not the other way around. Inputs already in canonical form (:r1, :fill, "fill", …) pass through unchanged. Flipped the alias-resolution test expectations and the keyword/string-input cases, refreshed the docstring and the regression-coverage comment to match. --------- Co-authored-by: Andrey Antukh * :lipstick: Fix sucess typo in subscription dialog i18n keys (#9204) Rename subscription.settings.sucess.dialog.{title,footer} to subscription.settings.success.dialog.{title,footer} in en.po and update the three callsites in subscription.cljs. Closes #9203 Signed-off-by: jack-stormentswe * :bug: Fix HSVA → HSBA test rename and Prettier formatting Signed-off-by: juan-flores077 * :bug: Fix CI failures and address review feedback for HSB color tab Signed-off-by: juan-flores077 * :lipstick: Resolve Conflicts Signed-off-by: juan-flores077 --------- Signed-off-by: juan-flores077 Signed-off-by: jack-stormentswe Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh Co-authored-by: boskodev790 Co-authored-by: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com> --- CHANGES.md | 1 + common/src/app/common/types/color.cljc | 30 +++++ .../test/common_tests/types/color_test.cljc | 75 +++++++++++++ .../playwright/ui/specs/colorpicker.spec.js | 8 +- .../app/main/ui/workspace/colorpicker.cljs | 26 ++++- .../workspace/colorpicker/color_inputs.cljs | 96 +++++++++++++++- .../workspace/colorpicker/color_inputs.scss | 30 +++++ .../main/ui/workspace/colorpicker/hsva.cljs | 104 +++++++++++++----- .../colorpicker/slider_selector.cljs | 4 +- .../colorpicker/slider_selector.scss | 12 ++ 10 files changed, 343 insertions(+), 43 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a6f7459acf..e65534835f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,7 @@ - Preserve vector content when pasting from external tools such as Inkscape: recognise SVG sent as text/plain (with optional XML declaration and HTML comments), skip the raster preview when an SVG sibling is on the clipboard, and ignore empty SVG blobs that some tools advertise alongside the real payload, so pasted graphics arrive editable without spurious "SVG is invalid" warnings [Github #546](https://github.com/penpot/penpot/issues/546) - Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts [Github #2457](https://github.com/penpot/penpot/issues/2457) - Adds a **Pixel grid color** picker in the viewport settings, next to the existing canvas color control [Github #7750](https://github.com/penpot/penpot/issues/7750) +- Add HEX, HSB and HSL support to the third color tab via an inline model switcher: relabel the existing HSVA tab as HSBA (the math was already HSB-equivalent), add an HSB ↔ HSL pill toggle that updates input labels, slider gradients and round-trip values without changing how colors are stored, and persist the chosen model across sessions [Github #9133](https://github.com/penpot/penpot/issues/9133) - Show specific invitation-link error messages instead of a single generic "Invite invalid" page: distinguish expired invitations, email-mismatch (signed in with the wrong account) and corrupted/invalid tokens, each with an actionable recovery hint [Github #9220](https://github.com/penpot/penpot/issues/9220) - Show detailed messages on file import errors to help diagnose why a file could not be imported (by @jsdevninja) [Github #9004](https://github.com/penpot/penpot/issues/9004) - Add read-only preview mode for saved versions — click a version name to open a dedicated preview view (by @wdeveloper16) [Github #8976](https://github.com/penpot/penpot/issues/8976) diff --git a/common/src/app/common/types/color.cljc b/common/src/app/common/types/color.cljc index ae56250d96..f626e48ae3 100644 --- a/common/src/app/common/types/color.cljc +++ b/common/src/app/common/types/color.cljc @@ -610,6 +610,36 @@ [hsv] (-> hsv hsv->hex hex->hsl)) +;; HSB (Hue, Saturation, Brightness) — same color model as HSV but with +;; the brightness component normalized to a 0-100 range, matching Figma, +;; Sketch, and Adobe XD conventions. Internally we reuse the HSV math and +;; only rescale the brightness axis. + +(defn rgb->hsb + [rgb] + (let [[h s v] (rgb->hsv rgb)] + [h s (* (/ v 255.0) 100.0)])) + +(defn hsb->rgb + [[h s b]] + (hsv->rgb [h s (int (* (/ b 100.0) 255.0))])) + +(defn hex->hsb + [v] + (-> v hex->rgb rgb->hsb)) + +(defn hsb->hex + [hsb] + (-> hsb hsb->rgb rgb->hex)) + +(defn hsv->hsb + [[h s v]] + [h s (* (/ v 255.0) 100.0)]) + +(defn hsb->hsv + [[h s b]] + [h s (int (* (/ b 100.0) 255.0))]) + (defn expand-hex [v] (cond diff --git a/common/test/common_tests/types/color_test.cljc b/common/test/common_tests/types/color_test.cljc index 9a3ab00ac9..deb0f24346 100644 --- a/common/test/common_tests/types/color_test.cljc +++ b/common/test/common_tests/types/color_test.cljc @@ -164,3 +164,78 @@ {:color "#ffffff" :opacity 1.0 :offset 0.5}] result (colors/interpolate-gradient stops 1.0)] (t/is (= "#ffffff" (:color result)))))) + +(t/deftest rgb-to-hsb + ;; Achromatic black: brightness 0 + (let [[h s b] (colors/rgb->hsb [0 0 0])] + (t/is (= 0 h)) + (t/is (= 0 s)) + (t/is (mth/close? b 0.0))) + ;; Pure red: hue 0, full saturation, brightness 100 + (let [[h s b] (colors/rgb->hsb [255 0 0])] + (t/is (mth/close? h 0.0)) + (t/is (mth/close? s 1.0)) + (t/is (mth/close? b 100.0))) + ;; Pure white: brightness 100 + (let [[_ _ b] (colors/rgb->hsb [255 255 255])] + (t/is (mth/close? b 100.0))) + ;; Mid gray: brightness ~50.2 + (let [[_ _ b] (colors/rgb->hsb [128 128 128])] + (t/is (mth/close? b (* (/ 128.0 255.0) 100.0))))) + +(t/deftest hsb-to-rgb + (t/is (= [0 0 0] (colors/hsb->rgb [0 0 0]))) + (t/is (= [255 255 255] (colors/hsb->rgb [0 0 100]))) + ;; Pure red from HSB + (let [[r g b] (colors/hsb->rgb [0 1 100])] + (t/is (= 255 r)) + (t/is (= 0 g)) + (t/is (= 0 b)))) + +(t/deftest hex-to-hsb + ;; Black + (let [[h s b] (colors/hex->hsb "#000000")] + (t/is (= 0 h)) + (t/is (= 0 s)) + (t/is (mth/close? b 0.0))) + ;; White: brightness 100 + (let [[_ _ b] (colors/hex->hsb "#ffffff")] + (t/is (mth/close? b 100.0))) + ;; Red + (let [[h s b] (colors/hex->hsb "#ff0000")] + (t/is (mth/close? h 0.0)) + (t/is (mth/close? s 1.0)) + (t/is (mth/close? b 100.0)))) + +(t/deftest hsb-to-hex + (t/is (= "#000000" (colors/hsb->hex [0 0 0]))) + (t/is (= "#ffffff" (colors/hsb->hex [0 0 100])))) + +(t/deftest hsv-hsb-roundtrip + ;; HSV brightness is 0-255, HSB brightness is 0-100. Round-trip + ;; should reach the same triple within ±1 (integer rounding). + (let [orig [210.0 0.5 128] + hsb (colors/hsv->hsb orig) + result (colors/hsb->hsv hsb)] + (t/is (mth/close? (nth orig 0) (nth result 0))) + (t/is (mth/close? (nth orig 1) (nth result 1))) + (t/is (< (mth/abs (- (nth orig 2) (nth result 2))) 2)))) + +(t/deftest rgb-hsb-roundtrip + ;; RGB → HSB → RGB should land within ±1 per channel + (let [orig [100 150 200] + hsb (colors/rgb->hsb orig) + result (colors/hsb->rgb hsb)] + (t/is (every? true? (map #(< (mth/abs (- %1 %2)) 2) orig result))))) + +(t/deftest hex-hsb-roundtrip + ;; HEX → HSB → HEX should preserve the color across the model swap + (let [orig "#fabada" + hsb (colors/hex->hsb orig) + result (colors/hsb->hex hsb)] + ;; Allow ±1 per channel after the round-trip due to integer rounding + (let [[r1 g1 b1] (colors/hex->rgb orig) + [r2 g2 b2] (colors/hex->rgb result)] + (t/is (< (mth/abs (- r1 r2)) 2)) + (t/is (< (mth/abs (- g1 g2)) 2)) + (t/is (< (mth/abs (- b1 b2)) 2))))) diff --git a/frontend/playwright/ui/specs/colorpicker.spec.js b/frontend/playwright/ui/specs/colorpicker.spec.js index 225c7da433..75dad9c97f 100644 --- a/frontend/playwright/ui/specs/colorpicker.spec.js +++ b/frontend/playwright/ui/specs/colorpicker.spec.js @@ -196,7 +196,7 @@ test("Gradient stops limit", async ({ page }) => { }); // Fix for https://tree.taiga.io/project/penpot/issue/9900 -test("Bug 9900 - Color picker has no inputs for HSV values", async ({ +test("Bug 9900 - Color picker has no inputs for HSB values", async ({ page, }) => { const workspacePage = new WasmWorkspacePage(page); @@ -207,12 +207,12 @@ test("Bug 9900 - Color picker has no inputs for HSV values", async ({ const swatch = workspacePage.page.getByRole("button", { name: "E8E9EA" }); await swatch.click(); - const HSVA = await workspacePage.page.getByLabel("HSVA"); - await HSVA.click(); + const HSBA = await workspacePage.page.getByLabel("HSBA"); + await HSBA.click(); await workspacePage.page.getByLabel("H", { exact: true }).isVisible(); await workspacePage.page.getByLabel("S", { exact: true }).isVisible(); - await workspacePage.page.getByLabel("V", { exact: true }).isVisible(); + await workspacePage.page.getByLabel("B(V)", { exact: true }).isVisible(); }); test("Bug 10089 - Cannot change alpha", async ({ page }) => { diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index cf9af4aacd..7b4c5bfa62 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -82,6 +82,15 @@ hsl-from (cc/hsv->hsl [h 0.0 v]) hsl-to (cc/hsv->hsl [h 1.0 v]) + ;; HSL-mode gradients. For S: fix current lightness, sweep + ;; saturation 0 → 1. For L: fix current saturation, sweep + ;; lightness 0 → 0.5 (pure hue) → 1. All computed at the + ;; current hue. + [_ cur-hsl-s cur-hsl-l] (cc/rgb->hsl rgb) + hsl-sat-from [h 0.0 cur-hsl-l] + hsl-sat-to [h 1.0 cur-hsl-l] + lightness-mid [h cur-hsl-s 0.5] + format-hsl (fn [[h s l]] (str/fmt "hsl(%s, %s, %s)" h @@ -90,7 +99,10 @@ (dom/set-css-property! node "--color" (str/join ", " rgb)) (dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb)) (dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from)) - (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))))) + (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)) + (dom/set-css-property! node "--hsl-saturation-grad-from" (format-hsl hsl-sat-from)) + (dom/set-css-property! node "--hsl-saturation-grad-to" (format-hsl hsl-sat-to)) + (dom/set-css-property! node "--lightness-grad-mid" (format-hsl lightness-mid))))) (mf/defc colorpicker* [{:keys [data disable-gradient disable-opacity disable-image on-change on-accept origin combined-tokens color-origin on-token-change tab applied-token]}] @@ -128,10 +140,15 @@ active-color-tab* (hooks/use-persisted-state ::color-tab "ramp") active-color-tab (deref active-color-tab*) + ;; Inline HSB/HSL toggle inside the HSBA tab — shared between + ;; the slider selector (for labels) and the numeric inputs. + hsb-mode* (hooks/use-persisted-state ::hsb-mode :hsb) + hsb-mode (deref hsb-mode*) + drag?* (mf/use-state false) drag? (deref drag?*) - type (if (= active-color-tab "hsva") :hsv :rgb) + type (if (= active-color-tab "hsva") :hsb :rgb) fill-image-ref (mf/use-ref nil) @@ -351,7 +368,7 @@ {:aria-label "Harmony" :icon i/rgba-complementary :id "harmony"} - {:aria-label "HSVA" + {:aria-label "HSBA" :icon i/hsva :id "hsva"}]) @@ -505,6 +522,7 @@ [:> hsva-selector* {:color current-color :disable-opacity disable-opacity + :mode hsb-mode :on-change handle-change-color :on-start-drag on-start-drag :on-finish-drag on-finish-drag}]))]] @@ -512,6 +530,8 @@ [:> color-inputs* {:type type :disable-opacity disable-opacity + :mode hsb-mode + :on-mode-change #(reset! hsb-mode* %) :color current-color :on-change handle-change-color}] diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs index 09ad2d0e8c..69c466669c 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs @@ -28,11 +28,23 @@ [val] (* (/ val 255) 100)) -(mf/defc color-inputs* [{:keys [type color disable-opacity on-change]}] +(mf/defc color-inputs* [{:keys [type color disable-opacity mode on-mode-change on-change]}] (let [{red :r green :g blue :b hue :h saturation :s value :v hex :hex alpha :alpha} color + ;; Sub-model selector for the HSB tab: users can toggle between + ;; HSB and HSL input display without leaving the tab. State is + ;; lifted to the colorpicker parent so the slider labels stay + ;; in sync with the inputs. + hsb-mode (or mode :hsb) + + ;; Compute HSL from current RGB (derived; not stored on the color map) + [_hsl-h hsl-s hsl-l] + (if (and red green blue) + (cc/rgb->hsl [red green blue]) + [0 0 0]) + refs {:hex (mf/use-ref nil) :r (mf/use-ref nil) :g (mf/use-ref nil) @@ -40,6 +52,8 @@ :h (mf/use-ref nil) :s (mf/use-ref nil) :v (mf/use-ref nil) + :hsl-s (mf/use-ref nil) + :hsl-l (mf/use-ref nil) :alpha (mf/use-ref nil)} setup-hex-color @@ -73,6 +87,7 @@ (let [val (case property :s (/ val 100) :v (value->hsv-value val) + (:hsl-s :hsl-l) (/ val 100) :alpha (/ val 100) val)] (cond @@ -87,6 +102,18 @@ :h h :s s :v v :r r :g g :b b})) + ;; HSL changes: recompute RGB/HSV from the new HSL triple, + ;; reusing the current hue when only S or L changes. + (#{:hsl-s :hsl-l} property) + (let [new-s (if (= property :hsl-s) val hsl-s) + new-l (if (= property :hsl-l) val hsl-l) + [r g b] (cc/hsl->rgb [hue new-s new-l]) + hex (cc/rgb->hex [r g b]) + [h s v] (cc/hex->hsv hex)] + (on-change {:hex hex + :h h :s s :v v + :r r :g g :b b})) + :else (let [{:keys [h s v]} (merge color (hash-map property val)) hex (cc/hsv->hex [h s v]) @@ -126,10 +153,13 @@ ;; Updates the inputs values when a property is changed in the parent (mf/use-effect - (mf/deps color type) + (mf/deps color type hsb-mode) (fn [] (doseq [ref-key (keys refs)] - (let [property-val (get color ref-key) + (let [property-val (case ref-key + :hsl-s hsl-s + :hsl-l hsl-l + (get color ref-key)) property-ref (get refs ref-key)] (when (and property-val property-ref) (when-let [node (mf/ref-val property-ref)] @@ -137,14 +167,32 @@ (case ref-key (:s :alpha) (mth/precision (* property-val 100) 2) :v (mth/precision (hsv-value->value property-val) 2) + (:hsl-s :hsl-l) (mth/precision (* property-val 100) 2) property-val)] (dom/set-value! node new-val)))))))) [:div {:class (stl/css-case :color-values true :disable-opacity disable-opacity)} + ;; Inline HSB/HSL switcher — only shown on the HSB tab so that + ;; designers can pick whichever hue-based model matches their + ;; workflow (HSB matches Figma/Sketch/XD, HSL matches CSS). + (when (and (not= type :rgb) on-mode-change) + [:div {:class (stl/css :model-switcher)} + [:button {:type "button" + :class (stl/css-case :model-pill true + :model-pill-active (= hsb-mode :hsb)) + :on-click #(on-mode-change :hsb)} + "HSB"] + [:button {:type "button" + :class (stl/css-case :model-pill true + :model-pill-active (= hsb-mode :hsl)) + :on-click #(on-mode-change :hsl)} + "HSL"]]) + [:div {:class (stl/css :colors-row)} - (if (= type :rgb) + (cond + (= type :rgb) [:* [:div {:class (stl/css :input-wrapper)} [:label {:for "red-value" :class (stl/css :input-label)} "R"] @@ -177,6 +225,42 @@ :on-change (on-change-property :b 255) :on-key-down (on-key-down-property :b 255)}]]] + (= hsb-mode :hsl) + [:* + [:div {:class (stl/css :input-wrapper)} + [:label {:for "hue-value" :class (stl/css :input-label)} "H"] + [:input {:id "hue-value" + :ref (:h refs) + :type "number" + :min 0 + :max 360 + :default-value hue + :on-change (on-change-property :h 360) + :on-key-down (on-key-down-property :h 360)}]] + [:div {:class (stl/css :input-wrapper)} + [:label {:for "hsl-saturation-value" :class (stl/css :input-label)} "S"] + [:input {:id "hsl-saturation-value" + :ref (:hsl-s refs) + :type "number" + :min 0 + :max 100 + :step 1 + :default-value (mth/precision (* hsl-s 100) 2) + :on-change (on-change-property :hsl-s 100) + :on-key-down (on-key-down-property :hsl-s 100)}]] + [:div {:class (stl/css :input-wrapper)} + [:label {:for "lightness-value" :class (stl/css :input-label)} "L"] + [:input {:id "lightness-value" + :ref (:hsl-l refs) + :type "number" + :min 0 + :max 100 + :step 1 + :default-value (mth/precision (* hsl-l 100) 2) + :on-change (on-change-property :hsl-l 100) + :on-key-down (on-key-down-property :hsl-l 100)}]]] + + :else [:* [:div {:class (stl/css :input-wrapper)} [:label {:for "hue-value" :class (stl/css :input-label)} "H"] @@ -200,8 +284,8 @@ :on-change (on-change-property :s 100) :on-key-down (on-key-down-property :s 100)}]] [:div {:class (stl/css :input-wrapper)} - [:label {:for "value-value" :class (stl/css :input-label)} "V"] - [:input {:id "value-value" + [:label {:for "brightness-value" :class (stl/css :input-label)} "B(V)"] + [:input {:id "brightness-value" :ref (:v refs) :type "number" :min 0 diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss index fe5b93d679..3a3034bc95 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss @@ -11,6 +11,36 @@ margin-top: deprecated.$s-8; + .model-switcher { + display: flex; + gap: deprecated.$s-4; + margin-bottom: deprecated.$s-8; + padding: deprecated.$s-2; + background-color: var(--color-background-tertiary); + border-radius: deprecated.$s-6; + align-self: flex-start; + + .model-pill { + @include deprecated.body-small-typography; + + padding: deprecated.$s-2 deprecated.$s-8; + border: none; + border-radius: deprecated.$s-4; + background: transparent; + color: var(--color-foreground-secondary); + cursor: pointer; + + &:hover { + color: var(--color-foreground-primary); + } + + &.model-pill-active { + background-color: var(--color-background-primary); + color: var(--color-accent-primary); + } + } + } + &.disable-opacity { grid-template-columns: 3.5rem repeat(3, 1fr); } diff --git a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs index 807d976314..802f59c8c2 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs @@ -11,17 +11,45 @@ [app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector*]] [rumext.v2 :as mf])) -(mf/defc hsva-selector* [{:keys [color disable-opacity on-change on-start-drag on-finish-drag]}] - (let [{hue :h saturation :s value :v alpha :alpha} color - handle-change-slider (fn [key] - (fn [new-value] - (let [change (hash-map key new-value) - {:keys [h s v]} (merge color change) - hex (cc/hsv->hex [h s v]) - [r g b] (cc/hex->rgb hex)] - (on-change (merge change - {:hex hex - :r r :g g :b b}))))) +(mf/defc hsva-selector* [{:keys [color disable-opacity mode on-change on-start-drag on-finish-drag]}] + (let [{hue :h saturation :s value :v alpha :alpha + r-val :r g-val :g b-val :b} color + hsl-mode? (= mode :hsl) + + ;; Current HSL derived from RGB — used as the starting point + ;; for HSL saturation/lightness slider values and for + ;; recomputing the color when either is dragged. + [_ hsl-s hsl-l] (if (and r-val g-val b-val) + (cc/rgb->hsl [r-val g-val b-val]) + [0 0 0]) + + ;; HSB math — current default behavior. + handle-change-hsv + (fn [key] + (fn [new-value] + (let [change (hash-map key new-value) + {:keys [h s v]} (merge color change) + hex (cc/hsv->hex [h s v]) + [r g b] (cc/hex->rgb hex)] + (on-change (merge change + {:hex hex + :r r :g g :b b}))))) + + ;; HSL math — when the user drags the S or L slider in HSL mode, + ;; we recompute RGB from the updated HSL triple and derive HSV + ;; for the canonical color representation. + handle-change-hsl + (fn [key] + (fn [new-value] + (let [new-s (if (= key :hsl-s) new-value hsl-s) + new-l (if (= key :hsl-l) new-value hsl-l) + [r g b] (cc/hsl->rgb [hue new-s new-l]) + hex (cc/rgb->hex [r g b]) + [h s v] (cc/hex->hsv hex)] + (on-change {:hex hex + :h h :s s :v v + :r r :g g :b b})))) + on-change-opacity (fn [new-alpha] (on-change {:alpha new-alpha}))] [:div {:class (stl/css :hsva-selector)} [:div {:class (stl/css :hsva-row)} @@ -31,29 +59,47 @@ :type :hue :max-value 360 :value hue - :on-change (handle-change-slider :h) + :on-change (handle-change-hsv :h) :on-start-drag on-start-drag :on-finish-drag on-finish-drag}]] [:div {:class (stl/css :hsva-row)} [:span {:class (stl/css :hsva-selector-label)} "S"] - [:> slider-selector* - {:class (stl/css :hsva-bar) - :type :saturation - :max-value 1 - :value saturation - :on-change (handle-change-slider :s) - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}]] + (if hsl-mode? + [:> slider-selector* + {:class (stl/css :hsva-bar) + :type :hsl-saturation + :max-value 1 + :value hsl-s + :on-change (handle-change-hsl :hsl-s) + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] + [:> slider-selector* + {:class (stl/css :hsva-bar) + :type :saturation + :max-value 1 + :value saturation + :on-change (handle-change-hsv :s) + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}])] [:div {:class (stl/css :hsva-row)} - [:span {:class (stl/css :hsva-selector-label)} "V"] - [:> slider-selector* - {:class (stl/css :hsva-bar) - :type :value - :max-value 255 - :value value - :on-change (handle-change-slider :v) - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}]] + [:span {:class (stl/css :hsva-selector-label)} (if hsl-mode? "L" "B(V)")] + (if hsl-mode? + [:> slider-selector* + {:class (stl/css :hsva-bar) + :type :lightness + :max-value 1 + :value hsl-l + :on-change (handle-change-hsl :hsl-l) + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] + [:> slider-selector* + {:class (stl/css :hsva-bar) + :type :value + :max-value 255 + :value value + :on-change (handle-change-hsv :v) + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}])] (when (not disable-opacity) [:div {:class (stl/css :hsva-row)} [:span {:class (stl/css :hsva-selector-label)} "A"] diff --git a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs index f125b6368b..7021db6767 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs @@ -53,7 +53,9 @@ :slider-selector true :hue (= type :hue) :opacity (= type :opacity) - :value (= type :value))) + :value (= type :value) + :hsl-saturation (= type :hsl-saturation) + :lightness (= type :lightness))) :data-testid (when (= type :opacity) "slider-opacity") :on-pointer-down handle-start-drag :on-pointer-up handle-stop-drag diff --git a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss index 4473eaface..09b9942e22 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss @@ -72,6 +72,18 @@ background: linear-gradient(var(--gradient-direction), #000 0%, #fff 100%); } + &.hsl-saturation { + background: linear-gradient( + var(--gradient-direction), + var(--hsl-saturation-grad-from) 0%, + var(--hsl-saturation-grad-to) 100% + ); + } + + &.lightness { + background: linear-gradient(var(--gradient-direction), #000 0%, var(--lightness-grad-mid) 50%, #fff 100%); + } + .handler { position: absolute; left: 50%; From 76c1b9afab822d4fae4116b4900f66e680e1bf68 Mon Sep 17 00:00:00 2001 From: FairyPiggyDev Date: Thu, 30 Apr 2026 09:50:38 -0400 Subject: [PATCH 273/288] :recycle: Migrate navigation-bullets to modern component syntax (#9265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step toward issue #9260 (incremental migration of legacy UI components to the modern `*`-suffixed syntax, removing the per-render JS-to-Clojure props conversion overhead). Component --------- `app.main.ui.releases.common/navigation-bullets` is a small (7-line) self-contained presentational component used by every release-notes modal to render the slide-progress dots. It already used standard keyword destructuring (`[{:keys [slide navigate total]}]`), had no `?`-suffixed props, no `unchecked-get`, no `obj/merge!`, no `::mf/wrap-props false`, and (importantly) no `::mf/register`, so it satisfies the migration pre-flight checks unchanged. Changes ------- - `releases/common.cljs` — definition renamed to `navigation-bullets*`. Body, props and metadata are otherwise unchanged. - `releases/v1_4.cljs` … `v2_15.cljs` (29 files) — every existing call site `[:& c/navigation-bullets {…}]` becomes `[:> c/navigation-bullets* {…}]`. The `:slide`, `:navigate`, `:total` props are passed exactly as before. The `:as c` alias of the require is unchanged, so no require edits are needed. No props were renamed (none ended in `?`); no helpers had to be swapped for `mf/spread-props` / `mf/props` (callers pass plain literal maps); no metadata had to be removed (none of the legacy options were in use). Github #9260 Signed-off-by: FairyPigDev --- frontend/src/app/main/ui/releases/common.cljs | 2 +- frontend/src/app/main/ui/releases/v1_11.cljs | 6 +++--- frontend/src/app/main/ui/releases/v1_12.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v1_13.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v1_14.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v1_15.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v1_16.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v1_17.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v1_18.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v1_19.cljs | 4 ++-- frontend/src/app/main/ui/releases/v1_4.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v1_5.cljs | 6 +++--- frontend/src/app/main/ui/releases/v1_6.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v1_7.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v1_8.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v1_9.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v2_0.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v2_10.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v2_11.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v2_12.cljs | 6 +++--- frontend/src/app/main/ui/releases/v2_13.cljs | 4 ++-- frontend/src/app/main/ui/releases/v2_14.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v2_15.cljs | 6 +++--- frontend/src/app/main/ui/releases/v2_3.cljs | 4 ++-- frontend/src/app/main/ui/releases/v2_4.cljs | 6 +++--- frontend/src/app/main/ui/releases/v2_5.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v2_6.cljs | 6 +++--- frontend/src/app/main/ui/releases/v2_7.cljs | 6 +++--- frontend/src/app/main/ui/releases/v2_8.cljs | 8 ++++---- frontend/src/app/main/ui/releases/v2_9.cljs | 4 ++-- 30 files changed, 102 insertions(+), 102 deletions(-) diff --git a/frontend/src/app/main/ui/releases/common.cljs b/frontend/src/app/main/ui/releases/common.cljs index 4e3ce7cc5e..3da4516e0c 100644 --- a/frontend/src/app/main/ui/releases/common.cljs +++ b/frontend/src/app/main/ui/releases/common.cljs @@ -11,7 +11,7 @@ (defmulti render-release-notes :version) -(mf/defc navigation-bullets +(mf/defc navigation-bullets* [{:keys [slide navigate total]}] [:ul {:class (stl/css :step-dots)} (for [i (range total)] diff --git a/frontend/src/app/main/ui/releases/v1_11.cljs b/frontend/src/app/main/ui/releases/v1_11.cljs index 395cd72ee7..7542f9339b 100644 --- a/frontend/src/app/main/ui/releases/v1_11.cljs +++ b/frontend/src/app/main/ui/releases/v1_11.cljs @@ -45,7 +45,7 @@ [:p "Use dissolve, slide and push animations to fade screens and imitate gestures like swipe."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}]]]]]] @@ -64,7 +64,7 @@ [:p "Now you can decide to include their backgrounds on your exports or leave them out."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}]]]]]] @@ -83,7 +83,7 @@ [:p "We’ve also added two new options to scale your designs at the view mode that might help you to make your presentations look better."]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_12.cljs b/frontend/src/app/main/ui/releases/v1_12.cljs index 65d7e2a41d..b38d7b12a1 100644 --- a/frontend/src/app/main/ui/releases/v1_12.cljs +++ b/frontend/src/app/main/ui/releases/v1_12.cljs @@ -45,7 +45,7 @@ [:p "Along with a better organization of panels (say hello to typography toolbar!) and new shortcuts that will speed your workflow."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -64,7 +64,7 @@ [:p "And they don’t come alone, but with some nice improvements to the rulers."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -82,7 +82,7 @@ [:p "Scrollbars at the design workspace will make it more obvious how to navigate it and easier for some users, for instance those who love using graphic tablets, from now on, will feel just as comfortable as those who use a mouseAnd they don’t come alone, but with some nice improvements to the rulers."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -101,7 +101,7 @@ [:p "This is a must if you’re working with grids (if you’re not, you should ;)), being able to adjust the movement to your baseline grid (8px? 5px?) is a huge timesaver that will improve your quality of life while designing."]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_13.cljs b/frontend/src/app/main/ui/releases/v1_13.cljs index 39ad2c79ac..9d3c0e6b0a 100644 --- a/frontend/src/app/main/ui/releases/v1_13.cljs +++ b/frontend/src/app/main/ui/releases/v1_13.cljs @@ -45,7 +45,7 @@ [:p "Use the export window to manage your multiple exports and be informed about the download progress. Big exports will happen in the background so you can keep designing in the meantime ;)"]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -64,7 +64,7 @@ [:p "This opens endless graphic possibilities such as combining gradients and blending modes in the same element to create sophisticated visual effects."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -83,7 +83,7 @@ [:p "A refreshed interface and two new features! The Invitations section allows you to check the status of current team invites plus you now have the ability to invite multiple members at the same time."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -102,7 +102,7 @@ [:p "As a side effect, this can give you a performance boost in massive designs."]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_14.cljs b/frontend/src/app/main/ui/releases/v1_14.cljs index 334e993f62..106ffaaf49 100644 --- a/frontend/src/app/main/ui/releases/v1_14.cljs +++ b/frontend/src/app/main/ui/releases/v1_14.cljs @@ -45,7 +45,7 @@ [:p "Categories and filters will help you to find the shortcut you need. One of the most requested features by the community!"]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -64,7 +64,7 @@ [:p "Play with the colors of a group without the hassles of individual selection!"]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -83,7 +83,7 @@ [:p "Ideal for prototyping fixed headers, navbars and floating buttons."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -102,7 +102,7 @@ [:p "Until now you could only do it by renaming the groups, now with drag & drop it is much more user friendly."]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_15.cljs b/frontend/src/app/main/ui/releases/v1_15.cljs index 9cdb26b079..0d04a305e4 100644 --- a/frontend/src/app/main/ui/releases/v1_15.cljs +++ b/frontend/src/app/main/ui/releases/v1_15.cljs @@ -45,7 +45,7 @@ [:p "Say goodbye to Artboards and hello to Boards!"]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -64,7 +64,7 @@ [:p "Now you can thanks to new permissions that allow you to decide who can comment and/or inspect the code at a shared prototype link."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -83,7 +83,7 @@ [:p "Also, comments inside boards will be associated with it, so that if you move a board its comments will maintain its place inside it."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -102,7 +102,7 @@ [:p "We’ve also made some adjustments to ensure the access to the options from small screens."]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_16.cljs b/frontend/src/app/main/ui/releases/v1_16.cljs index 537507abb6..26db75b099 100644 --- a/frontend/src/app/main/ui/releases/v1_16.cljs +++ b/frontend/src/app/main/ui/releases/v1_16.cljs @@ -45,7 +45,7 @@ [:p "We heard the users before refreshing the interface, simplifying it to give prominence to the content. And yes, now that you ask, the dark theme is coming soon."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -64,7 +64,7 @@ [:p "You no longer need to to download most of them to the computer before importing."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -83,7 +83,7 @@ [:p "More relevant info and better explanations, a refined new team and invitation flow, a beginners tutorial and a walkthrough file that will help newcomers learn how to use and start designing with Penpot faster."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -102,7 +102,7 @@ [:p "This was a contribution by our community member @andrewzhurov <3"]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_17.cljs b/frontend/src/app/main/ui/releases/v1_17.cljs index 1965748c6a..668f879f0d 100644 --- a/frontend/src/app/main/ui/releases/v1_17.cljs +++ b/frontend/src/app/main/ui/releases/v1_17.cljs @@ -45,7 +45,7 @@ [:p "Penpot brings a layout system like no other. As described by one of our beta testers: 'I love the fact that Penpot is following the CSS FlexBox, which is making UI Design a step closer to the logic and behavior behind how things will be actually built after design.'"]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -64,7 +64,7 @@ [:p "Also, inspect mode provides a safer view-only mode and other improvements."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -83,7 +83,7 @@ [:p "While we are still working on a plugin system, this is a great and simple way to create integrations with other services."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -102,7 +102,7 @@ [:p "This release comes with improvements on color contrasts, alt texts, semantic labels, focusable items and keyboard navigation at login and dashboard, but more will come."]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_18.cljs b/frontend/src/app/main/ui/releases/v1_18.cljs index cb6d73458c..ff1c06d179 100644 --- a/frontend/src/app/main/ui/releases/v1_18.cljs +++ b/frontend/src/app/main/ui/releases/v1_18.cljs @@ -45,7 +45,7 @@ [:p "And not only that, when creating Flex layouts, the spacing is predicted, helping you to maintain your design composition."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -64,7 +64,7 @@ [:p "Now you can exclude elements from the Flex layout flow using absolute position."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -83,7 +83,7 @@ [:p "This is another capability that brings Penpot Flex layout even closer to the power of CSS standards."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -102,7 +102,7 @@ [:p "Activate the scale tool by pressing K and scale your elements, maintaining their visual aspect."]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_19.cljs b/frontend/src/app/main/ui/releases/v1_19.cljs index 8543a0c45b..6b51d8faaf 100644 --- a/frontend/src/app/main/ui/releases/v1_19.cljs +++ b/frontend/src/app/main/ui/releases/v1_19.cljs @@ -72,7 +72,7 @@ " in particular and the Penpot community as a whole!"]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 2}]]]]]] @@ -99,7 +99,7 @@ "to the Penpot’s plugins system."]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 2}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_4.cljs b/frontend/src/app/main/ui/releases/v1_4.cljs index bc80923258..37ecf7be42 100644 --- a/frontend/src/app/main/ui/releases/v1_4.cljs +++ b/frontend/src/app/main/ui/releases/v1_4.cljs @@ -45,7 +45,7 @@ [:p "To open a file you just have to double click it. You can also open a file in a new tab with right click."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -64,7 +64,7 @@ [:p "Also, now you have an easy way to manage files and projects between teams."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -83,7 +83,7 @@ [:p "If you write in arabic, hebrew or other RTL language text direction will be automatically detected in text layers."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -102,7 +102,7 @@ [:p "This is why the standard blend modes and opacity level are now available for each element."]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_5.cljs b/frontend/src/app/main/ui/releases/v1_5.cljs index 8d962515d7..d183bd7976 100644 --- a/frontend/src/app/main/ui/releases/v1_5.cljs +++ b/frontend/src/app/main/ui/releases/v1_5.cljs @@ -45,7 +45,7 @@ [:p "The usability and performance of the paths tool has been improved too."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}]]]]]] @@ -64,7 +64,7 @@ [:p "It is time to have all the libraries well organized and work more efficiently."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}]]]]]] @@ -83,7 +83,7 @@ [:p "It's easier to specify by how much you want to change a value and work with measures and distances."]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_6.cljs b/frontend/src/app/main/ui/releases/v1_6.cljs index c6636c4550..cf1c96bbe9 100644 --- a/frontend/src/app/main/ui/releases/v1_6.cljs +++ b/frontend/src/app/main/ui/releases/v1_6.cljs @@ -45,7 +45,7 @@ [:p "We hope you enjoy having more typography options and our brand new font selector."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -64,7 +64,7 @@ [:p "Disabled by default, this tool is disabled back after being used."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -83,7 +83,7 @@ [:p "You should have the feeling that files and layers show up a bit faster :)"]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -102,7 +102,7 @@ [:p "An easy way to increase speed by working with vectors!"]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_7.cljs b/frontend/src/app/main/ui/releases/v1_7.cljs index 32666d5158..3d0c2db7da 100644 --- a/frontend/src/app/main/ui/releases/v1_7.cljs +++ b/frontend/src/app/main/ui/releases/v1_7.cljs @@ -48,7 +48,7 @@ suits you better!"]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -70,7 +70,7 @@ components."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -90,7 +90,7 @@ [:p "Easily " [:strong "rename and ungroup"] " asset groups."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -108,7 +108,7 @@ [:p "Do you sometimes copy and paste component copies that belong to a library already shared by the original and destination files? From now on, those component copies are aware of this and will retain their linkage to the library."]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_8.cljs b/frontend/src/app/main/ui/releases/v1_8.cljs index dfff4bd9f2..fcf214cae9 100644 --- a/frontend/src/app/main/ui/releases/v1_8.cljs +++ b/frontend/src/app/main/ui/releases/v1_8.cljs @@ -45,7 +45,7 @@ [:p "You can also create a shareable link deciding which pages will be available for the visitors. Sharing is caring!"]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -64,7 +64,7 @@ [:p "You can select different styles for each end of an open path: arrows, square, circle, diamond or just a round ending are the available options."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -83,7 +83,7 @@ [:p "Quick and easy :)"]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -102,7 +102,7 @@ [:p "Now you can easily export all the artboards of a page to a single pdf file."]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v1_9.cljs b/frontend/src/app/main/ui/releases/v1_9.cljs index 6a8ddfba80..d359063a6e 100644 --- a/frontend/src/app/main/ui/releases/v1_9.cljs +++ b/frontend/src/app/main/ui/releases/v1_9.cljs @@ -45,7 +45,7 @@ [:p "Create overlays, back buttons or links to URLs to mimic the behavior of the product you’re designing."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -64,7 +64,7 @@ [:p "Flows allow you to define multiple starting points within the same page so you can better organize and present your prototypes."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -83,7 +83,7 @@ [:p "Using boolean operations will lead to countless graphic possibilities for your designs."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]] @@ -102,7 +102,7 @@ [:p [:a {:alt "Explore libraries & templates" :target "_blank" :href "https://penpot.app/libraries-templates"} "Explore libraries & templates"]]] [:div.modal-navigation [:button.btn-secondary {:on-click finish} "Start!"] - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/releases/v2_0.cljs b/frontend/src/app/main/ui/releases/v2_0.cljs index 57f2b0847b..fd1299d0d7 100644 --- a/frontend/src/app/main/ui/releases/v2_0.cljs +++ b/frontend/src/app/main/ui/releases/v2_0.cljs @@ -92,7 +92,7 @@ " up the design as code to take it from there."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -126,7 +126,7 @@ " and adherence to other best practices."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -161,7 +161,7 @@ "that will help you to better manage your design systems."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -193,7 +193,7 @@ [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] diff --git a/frontend/src/app/main/ui/releases/v2_10.cljs b/frontend/src/app/main/ui/releases/v2_10.cljs index fcefb326c5..297c1e77d6 100644 --- a/frontend/src/app/main/ui/releases/v2_10.cljs +++ b/frontend/src/app/main/ui/releases/v2_10.cljs @@ -74,7 +74,7 @@ "This release has been shaped by our amazing community. A huge thank-you to everyone who shared ideas, feedback, and insights to make Penpot Variants possible <3"]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -113,7 +113,7 @@ " now to join us 8-10 October, in Madrid!"]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -143,7 +143,7 @@ "This latest update brings—no more no less than—six new token types, significantly boosting your ability to manage design decisions, particularly in typography: Font Family, Font Weight, Text Case, Text Decoration, Letter Spacing token, and Number token (for unitless values)."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -174,7 +174,7 @@ [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] diff --git a/frontend/src/app/main/ui/releases/v2_11.cljs b/frontend/src/app/main/ui/releases/v2_11.cljs index a4b330f8bc..529a6cb0a7 100644 --- a/frontend/src/app/main/ui/releases/v2_11.cljs +++ b/frontend/src/app/main/ui/releases/v2_11.cljs @@ -74,7 +74,7 @@ "The Typography token also marks a big step forward for Penpot: it’s our first composite token! Composite tokens are special because they can hold multiple properties within one token. Shadow token will be the next composite token coming your way."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -110,7 +110,7 @@ "- Reorder your component properties by drag & drop: Because organization matters, now you can arrange your properties however makes the most sense to you, so you can keep the ones you use most often right where you want them."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -148,7 +148,7 @@ "Invited users will also get clearer emails, including a reminder sent one day before the invite expires (after seven days). Simple, clean, and much more efficient."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -179,7 +179,7 @@ [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] diff --git a/frontend/src/app/main/ui/releases/v2_12.cljs b/frontend/src/app/main/ui/releases/v2_12.cljs index 43ac723024..342f92100c 100644 --- a/frontend/src/app/main/ui/releases/v2_12.cljs +++ b/frontend/src/app/main/ui/releases/v2_12.cljs @@ -80,7 +80,7 @@ "Developers now get a clearer context during handoff. The Inspect panel shows the actual token used in your design, in a similar way to how styles are displayed. This small detail reduces ambiguity, aligns everyone on the same language, and strengthens collaboration across the team."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] @@ -116,7 +116,7 @@ "It’s a subtle improvement, but it removes friction you feel hundreds of times a week, and makes component work flow more naturally."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] @@ -152,7 +152,7 @@ [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] diff --git a/frontend/src/app/main/ui/releases/v2_13.cljs b/frontend/src/app/main/ui/releases/v2_13.cljs index 149d914c61..54a0badaee 100644 --- a/frontend/src/app/main/ui/releases/v2_13.cljs +++ b/frontend/src/app/main/ui/releases/v2_13.cljs @@ -74,7 +74,7 @@ "Highly requested, long overdue, and now officially here."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] @@ -108,7 +108,7 @@ [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 2}] diff --git a/frontend/src/app/main/ui/releases/v2_14.cljs b/frontend/src/app/main/ui/releases/v2_14.cljs index b424d4bfa8..9dd3013274 100644 --- a/frontend/src/app/main/ui/releases/v2_14.cljs +++ b/frontend/src/app/main/ui/releases/v2_14.cljs @@ -74,7 +74,7 @@ "One extra detail: if you edit the path and change group segments, the token is moved to its new group (creating it if needed), and empty groups are automatically cleaned up."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -104,7 +104,7 @@ "If you’ve been waiting to generate tokens, sync them, or manipulate them from your own tools, this is the missing piece. And yes, this one has been requested a lot."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -134,7 +134,7 @@ "Remapping is always optional, because sometimes you don’t want to keep the current connections. When enabled, it affects all tokens in the file and also takes libraries into account, so main components can propagate changes to child components, and applied tokens update on the elements using them."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -168,7 +168,7 @@ [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] diff --git a/frontend/src/app/main/ui/releases/v2_15.cljs b/frontend/src/app/main/ui/releases/v2_15.cljs index 8c2f61580f..76f6527f02 100644 --- a/frontend/src/app/main/ui/releases/v2_15.cljs +++ b/frontend/src/app/main/ui/releases/v2_15.cljs @@ -74,7 +74,7 @@ "You can run MCP in two ways. Remote MCP is hosted and simpler to set up. Local MCP runs on your machine and gives advanced teams extra control. Same vision, different operating model."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -115,7 +115,7 @@ "This is where MCP becomes workflow infrastructure. Less manual glue work, fewer handoff gaps, and faster iterations between designers and developers."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -149,7 +149,7 @@ "]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] diff --git a/frontend/src/app/main/ui/releases/v2_3.cljs b/frontend/src/app/main/ui/releases/v2_3.cljs index 8b3040b8f4..6063642485 100644 --- a/frontend/src/app/main/ui/releases/v2_3.cljs +++ b/frontend/src/app/main/ui/releases/v2_3.cljs @@ -72,7 +72,7 @@ "Find everything you need in our full comprehensive documentation to start building your plugins now!"]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 2}] @@ -105,7 +105,7 @@ [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 2}] diff --git a/frontend/src/app/main/ui/releases/v2_4.cljs b/frontend/src/app/main/ui/releases/v2_4.cljs index 1559911a4d..67a3985127 100644 --- a/frontend/src/app/main/ui/releases/v2_4.cljs +++ b/frontend/src/app/main/ui/releases/v2_4.cljs @@ -72,7 +72,7 @@ "Now, you can invite members to your teams who only need to view and comment on files. Team members, stakeholders, developers… pick your case. Anyone who doesn't need to edit can participate confidently."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] @@ -102,7 +102,7 @@ "Some versions are saved automatically, serving as an invaluable emergency backup. Additionally, you can manually save versions, giving you full control over the timeline associated with a file. This way, you can always restore specific versions that you've intentionally saved."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] @@ -131,7 +131,7 @@ [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] diff --git a/frontend/src/app/main/ui/releases/v2_5.cljs b/frontend/src/app/main/ui/releases/v2_5.cljs index c39c4aebba..cce9c83d70 100644 --- a/frontend/src/app/main/ui/releases/v2_5.cljs +++ b/frontend/src/app/main/ui/releases/v2_5.cljs @@ -72,7 +72,7 @@ "And that’s not all. We’ve also added quick actions to flip and rotate gradients, plus now you can adjust the radius for radial gradients. More control, more flexibility, more fun."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -102,7 +102,7 @@ "We’ve also added a new section in your profile where you can customize your notifications, choosing what to receive on your dashboard and via email. On top of that, comments got a UI refresh, making everything clearer and better organized. And this is just the first batch of improvements—expect even more comment-related upgrades in the next Penpot release."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -136,7 +136,7 @@ "Less manual work for a faster workflow. We hope you find it as useful as we do."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -165,7 +165,7 @@ [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] diff --git a/frontend/src/app/main/ui/releases/v2_6.cljs b/frontend/src/app/main/ui/releases/v2_6.cljs index 9d47c870f7..92b4109fd7 100644 --- a/frontend/src/app/main/ui/releases/v2_6.cljs +++ b/frontend/src/app/main/ui/releases/v2_6.cljs @@ -84,7 +84,7 @@ your product needs."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] @@ -120,7 +120,7 @@ interoperability by design through Open Source."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] @@ -159,7 +159,7 @@ [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] diff --git a/frontend/src/app/main/ui/releases/v2_7.cljs b/frontend/src/app/main/ui/releases/v2_7.cljs index 1a5c562e25..0f6c51abc7 100644 --- a/frontend/src/app/main/ui/releases/v2_7.cljs +++ b/frontend/src/app/main/ui/releases/v2_7.cljs @@ -71,7 +71,7 @@ "The highlight: you can now duplicate token sets directly from a menu item. A huge time-saver, especially when working from existing sets. We’ve also made it easier to create themes by letting you select their set right away, and we’ve polished some info indicators to make everything a bit clearer. Plus, we’ve fixed a bunch of early-stage bugs to keep things running smoothly."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] @@ -101,7 +101,7 @@ "This update gives editors and viewers the same ability to configure, create, copy, and delete sharing links. A capability that, until now, was limited to owners and admins."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] @@ -132,7 +132,7 @@ [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 3}] diff --git a/frontend/src/app/main/ui/releases/v2_8.cljs b/frontend/src/app/main/ui/releases/v2_8.cljs index fbdce6ee04..8eae8ff74e 100644 --- a/frontend/src/app/main/ui/releases/v2_8.cljs +++ b/frontend/src/app/main/ui/releases/v2_8.cljs @@ -83,7 +83,7 @@ "- And we’ve a new language! Hi Serbians!"]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -116,7 +116,7 @@ "This is just one more step in the evolution of Design Tokens in Penpot. And there's more to come: typography tokens are already in the works!"]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -149,7 +149,7 @@ "- We have integrated AI-powered help, which is trained on Penpot documentation, directly into the design workspace. Get assistance without switching context, so you can stay in the flow."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] @@ -186,7 +186,7 @@ [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 4}] diff --git a/frontend/src/app/main/ui/releases/v2_9.cljs b/frontend/src/app/main/ui/releases/v2_9.cljs index 600df72665..bd71956516 100644 --- a/frontend/src/app/main/ui/releases/v2_9.cljs +++ b/frontend/src/app/main/ui/releases/v2_9.cljs @@ -71,7 +71,7 @@ "And there’s more progress on Tokens, including support for importing multiple token files via .zip, and smarter token visibility, only showing the relevant tokens for each layer type."]] [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 2}] @@ -102,7 +102,7 @@ [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets + [:> c/navigation-bullets* {:slide slide :navigate navigate :total 2}] From 9c2a80bfa105271487d9e09bdf3a6d4bd42e4dce Mon Sep 17 00:00:00 2001 From: FairyPiggyDev Date: Thu, 30 Apr 2026 09:54:24 -0400 Subject: [PATCH 274/288] :bug: Fix crash pasting component with variants from shared library (#9136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copying a component with variants from a shared library file ("Lib") and pasting it into a file that uses that library ("Using Lib") would crash the destination file with the referential-integrity validator error: {:code :component-main-external :hint "Main instance should refer to a component in the same file"} Root cause ---------- Paste goes through `generate-duplicate-shape-change` in `common/src/app/common/logic/libraries.cljc`. When the shape is a main instance of a known component and the copy set includes its variant container, dispatch lands in `duplicate-variant`, then `generate-duplicate-component`, and finally `duplicate-component`, which clones the main-instance shape tree. Its `update-new-shape` helper already re-links the new outer main's `:component-id` to the freshly created local component (`new-component-id`), but it never touches `:component-file`. The cloned shape therefore inherits `:component-file` from the source library while the new component is registered in the destination's local library (`:apply-changes-local-library? true`), leaving the main-instance dangling. Fix --- Extend `update-new-shape` with a second clause, sibling to the existing `:component-id` rewrite: when a destination file id is provided and differs from the new main's current `:component-file`, re-root the shape. The same `(= (:component-id new-shape) (:id component))` guard already used for the id rewrite ensures only the outer main-instance is touched; nested shapes are unaffected. The destination file id is threaded from the paste entry point through the two orchestration functions that already knew the source/destination distinction: - `generate-duplicate-shape-change` — supplies the destination `file-id` it already has in scope when dispatching to `generate-duplicate-component-change`. - `generate-duplicate-component-change` — accepts `:new-component-file` as a kwarg; renames its internal `file-id` binding to `source-file-id` for clarity (it was always the component's originating library file); forwards `new-component-file` to `duplicate-variant`. - `duplicate-variant` — takes and forwards the `new-component-file` positional arg. - `generate-duplicate-component` — accepts `:new-component-file` kwarg and passes it to `duplicate-component`. - `duplicate-component` — applies the rewrite inside `update-new-shape`. The `new-component-file` parameter is placed right after `new-component-id` since component-id and component-file are typically managed together. Same-file duplication is not affected: without `:new-component-file` the new clause is skipped, and when source and destination match the `(not= new-component-file (:component-file new-shape))` guard fails. Tests ----- Added in `common/test/common_tests/logic/comp_creation_test.cljc`: - `test-duplicate-component-rewrites-component-file-to-destination` asserts that passing `:new-component-file` to `generate-duplicate-component` produces a main-instance with `:component-file` equal to the destination id. - `test-duplicate-component-keeps-component-file-without-dest` baseline: without `:new-component-file`, `:component-file` is left untouched, matching pre-existing same-file behavior. Github #8144 Signed-off-by: FairyPigDev Co-authored-by: Andrey Antukh --- CHANGES.md | 1 + common/src/app/common/logic/libraries.cljc | 57 ++++++++++++------- .../logic/comp_creation_test.cljc | 56 ++++++++++++++++++ 3 files changed, 93 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e65534835f..ddb850f0b0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,6 +69,7 @@ - Fix Plugin API `shape.applyToken()` / `token.applyToShapes()` / `token.applyToSelected()` rejecting JS-array attribute lists like `["fill"]`: switched the inner schemas to `[::sm/set ...]` (which has the JS array → Clojure set decoder) and made `token-attr-plugin->token-attr` accept string inputs by coercing them to keywords before consulting the alias map [Github #9162](https://github.com/penpot/penpot/issues/9162) - Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored (`userinfo` / `token`) in the OIDC callback, causing "incomplete user info" failures during registration [Github #9108](https://github.com/penpot/penpot/issues/9108) - Fix `get-view-only-bundle` crashing when a share-link viewer encounters a team member whose email lacks `@` (NullPointerException in `obfuscate-email`) or whose domain has no `.` (previously produced a dangling-dot `****@****.`); now the viewer-side obfuscation is nil-safe and omits the trailing dot when the domain has no TLD +- Fix crash when pasting a component with variants from an external shared library into a file that uses that library (by @FairyPigDev) [Github #8144](https://github.com/penpot/penpot/issues/8144) - Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled [Github #8877](https://github.com/penpot/penpot/issues/8877) - Fix Copy as SVG: emit a single valid SVG document when multiple shapes are selected, and publish `image/svg+xml` to the clipboard so the paste target works in Inkscape and other SVG-native tools [Github #838](https://github.com/penpot/penpot/issues/838) - Reset profile submenu state when the account menu closes (by @eureka0928) [Github #8947](https://github.com/penpot/penpot/issues/8947) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 35df16aa86..d1d03f68aa 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -118,8 +118,9 @@ (defn- duplicate-component "Clone the root shape of the component and all children. Generate new - ids from all of them." - [component new-component-id library-data force-id delta variant-id] + ids from all of them. Optionally set the component-file if the file where the + new component will reside is different than the origin one." + [component new-component-id new-component-file library-data force-id delta variant-id] (let [main-instance-page (ctf/get-component-page library-data component) main-instance-shape (ctf/get-component-root library-data component) delta (or delta (gpt/point (+ (:width main-instance-shape) 50) 0)) @@ -141,10 +142,18 @@ update-new-shape (fn [new-shape _] (cond-> new-shape - ; Link the new main to the new component + ;; Link the new main to the new component, and re-root it + ;; to the destination file when duplicating across files. + ;; Only the outer main matches `(:id component)`, so + ;; nested main-instances are not touched here. (= (:component-id new-shape) (:id component)) (assoc :component-id new-component-id) + (and (= (:component-id new-shape) (:id component)) + (some? new-component-file) + (not= new-component-file (:component-file new-shape))) + (assoc :component-file new-component-file) + ; If it is the instance root, add it the variant-id (and (ctk/instance-root? new-shape) (some? variant-id)) (assoc :variant-id variant-id) @@ -188,7 +197,7 @@ (defn generate-duplicate-component "Create a new component copied from the one with the given id." - [changes library component-id new-component-id & {:keys [new-shape-id apply-changes-local-library? delta new-variant-id page-id]}] + [changes library component-id new-component-id & {:keys [new-component-file new-shape-id apply-changes-local-library? delta new-variant-id page-id]}] (let [component (ctkl/get-component (:data library) component-id) new-name (:name component) @@ -197,7 +206,7 @@ target-page-id (or page-id (:id main-instance-page)) [new-main-instance-shape new-main-instance-shapes] - (duplicate-component component new-component-id (:data library) new-shape-id delta new-variant-id)] + (duplicate-component component new-component-id new-component-file (:data library) new-shape-id delta new-variant-id)] [new-main-instance-shape (-> changes @@ -2727,7 +2736,7 @@ frames))) (defn- duplicate-variant - [changes library component base-pos parent page-id into-new-variant?] + [changes library component base-pos parent page-id into-new-variant? new-component-file] (let [component-page (ctpl/get-page (:data library) (:main-instance-page component)) objects (:objects component-page) component-shape (get objects (:main-instance-id component)) @@ -2741,7 +2750,8 @@ {:apply-changes-local-library? true :delta delta :new-variant-id (if into-new-variant? nil (:id parent)) - :page-id page-id}) + :page-id page-id + :new-component-file new-component-file}) value (when into-new-variant? (str ctv/value-prefix (-> (cfv/extract-properties-values (:data library) objects (:id parent)) @@ -2764,15 +2774,18 @@ (defn generate-duplicate-component-change - [changes objects page main parent-id frame-id delta libraries library-data ids-map] - (let [main-id (:id main) - component-id (:component-id main) - file-id (:component-file main) - component (ctf/get-component libraries file-id component-id) - pos (as-> (gsh/move main delta) $ - (gpt/point (:x $) (:y $))) + [changes objects page main parent-id frame-id delta libraries library-data ids-map & {:keys [new-component-file]}] + (let [main-id (:id main) + component-id (:component-id main) + ;; Source library file id (where the component was originally + ;; defined). Renamed from `file-id` to make the contrast with + ;; `new-component-file` explicit when duplicating across files. + source-file-id (:component-file main) + component (ctf/get-component libraries source-file-id component-id) + pos (as-> (gsh/move main delta) $ + (gpt/point (:x $) (:y $))) - parent (get objects parent-id) + parent (get objects parent-id) ;; When we duplicate a variant alone, we will instanciate it @@ -2799,25 +2812,27 @@ (and (ctk/is-variant? main) in-variant-container?) (duplicate-variant changes - (get libraries file-id) + (get libraries source-file-id) component pos parent (:id page) - false) + false + new-component-file) (ctk/is-variant-container? parent) (duplicate-variant changes - (get libraries file-id) + (get libraries source-file-id) component pos parent (:id page) - true) + true + new-component-file) :else (generate-instantiate-component changes objects - file-id + source-file-id component-id pos page @@ -2841,7 +2856,7 @@ changes (ctf/is-main-of-known-component? obj libraries) - (generate-duplicate-component-change changes objects page obj parent-id frame-id delta libraries library-data ids-map) + (generate-duplicate-component-change changes objects page obj parent-id frame-id delta libraries library-data ids-map {:new-component-file file-id}) :else (let [frame? (cfh/frame-shape? obj) diff --git a/common/test/common_tests/logic/comp_creation_test.cljc b/common/test/common_tests/logic/comp_creation_test.cljc index 462734d6ee..ad1277879b 100644 --- a/common/test/common_tests/logic/comp_creation_test.cljc +++ b/common/test/common_tests/logic/comp_creation_test.cljc @@ -304,6 +304,62 @@ (t/is (= (thi/id :main1-child) (:id child1'))) (t/is (not= (thi/id :main1-child) (:id child2'))))) +(t/deftest test-duplicate-component-rewrites-component-file-to-destination + ;; Regression test for Issue #8144. When a component is duplicated + ;; into a different file via `:apply-changes-local-library? true` + ;; and `:new-component-file` is provided, the returned main-instance + ;; shape must carry `:component-file` equal to the destination file + ;; id so the referential-integrity validator + ;; (:component-main-external) is satisfied. + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 + :main1-root + :main1-child)) + + component (thc/get-component file :component1) + new-component-file (uuid/next) + + ;; ==== Action + [new-shape _] + (cll/generate-duplicate-component (pcb/empty-changes) + file + (:id component) + (uuid/next) + {:apply-changes-local-library? true + :new-component-file new-component-file})] + + ;; ==== Check + (t/is (some? new-shape)) + (t/is (ctk/main-instance? new-shape)) + (t/is (= new-component-file (:component-file new-shape))))) + +(t/deftest test-duplicate-component-keeps-component-file-without-dest + ;; Baseline: when no `:new-component-file` is passed (same-file + ;; duplication), the main-instance's `:component-file` is left + ;; untouched, matching pre-existing behavior. + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 + :main1-root + :main1-child)) + + component (thc/get-component file :component1) + original-source (:component-file + (ths/get-shape-by-id file (:main-instance-id component))) + + ;; ==== Action + [new-shape _] + (cll/generate-duplicate-component (pcb/empty-changes) + file + (:id component) + (uuid/next) + {:apply-changes-local-library? true})] + + ;; ==== Check + (t/is (some? new-shape)) + (t/is (= original-source (:component-file new-shape))))) + (t/deftest test-delete-component (let [;; ==== Setup file (-> (thf/sample-file :file1) From b42e81e1a415063e2825e78133446eb27854d9ef Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 30 Apr 2026 15:57:41 +0200 Subject: [PATCH 275/288] :paperclip: Update changelog (candidate for freeze) --- CHANGES.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ddb850f0b0..3012f32666 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,19 @@ ### :sparkles: New features & Enhancements +### :bug: Bugs fixed + + + +## 2.16.0 (Unreleased) + +### :boom: Breaking changes & Deprecations + +### :rocket: Epics and highlights + +### :sparkles: New features & Enhancements + +- Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714) - Add "Delete group" option to the assets panel context menu for components, colors and typographies (by @FairyPigDev) [Github #9141](https://github.com/penpot/penpot/issues/9141) - Add `Alt+click` on a layer's disclosure arrow to recursively expand the entire subtree rooted at that layer in the Layers sidebar; symmetric with the existing `Shift+click` collapse-all gesture, and removes the O(siblings × depth) click cost of unfolding a deep subtree one level at a time [Github #7736](https://github.com/penpot/penpot/issues/7736) - Show alpha percentage next to library color values to distinguish colors that differ only in opacity (by @rockchris099) [Github #6328](https://github.com/penpot/penpot/issues/6328) @@ -58,6 +71,17 @@ ### :bug: Bugs fixed +- Fix Alt/Option to draw shapes from center point (by @offreal) [Github #8361](https://github.com/penpot/penpot/pull/8361) +- Add token name on broken token pill on sidebar [Taiga #13527](https://tree.taiga.io/project/penpot/issue/13527) +- Fix tooltip activated when tab change [Taiga #13627](https://tree.taiga.io/project/penpot/issue/13627) +- Fix title on shared button [Taiga #13730](https://tree.taiga.io/project/penpot/issue/13730) +- Fix hover on layers [Taiga #13799](https://tree.taiga.io/project/penpot/issue/13799) +- Fix highlight after name edition [Taiga #13783](https://tree.taiga.io/project/penpot/issue/13783) +- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534) +- Fix dashboard navigation tabs overlap with projects content when scrolling [Taiga #13962](https://tree.taiga.io/project/penpot/issue/13962) +- Fix text editor v1 focus [Taiga #13961](https://tree.taiga.io/project/penpot/issue/13961) +- Fix color dropdown option update [Taiga #14035](https://tree.taiga.io/project/penpot/issue/14035) +- Fix themes modal height [Taiga #14046](https://tree.taiga.io/project/penpot/issue/14046) - Fix layers-panel rename input opening with the type-based default (e.g. "Ellipse") instead of the user's saved name when re-entering edit mode on a previously renamed layer; the silent revert could overwrite the saved name on confirm. The `default-value` `mf/with-memo` was missing `shape-name` from its dependency list, so once the memo cached the original default it never refreshed. Adds `shape-name` to the deps and force-syncs the input's DOM value on every entry into edit mode [Github #9230](https://github.com/penpot/penpot/issues/9230) - Suppress the browser context menu when right-clicking empty space in the workspace sidebars while preserving it on text inputs so paste/select-all still work [Github #5127](https://github.com/penpot/penpot/issues/5127) - Fix release notes modal appearing behind the dashboard sidebar [Github #8296](https://github.com/penpot/penpot/issues/8296) @@ -108,30 +132,6 @@ - Fix tooltip appearing two times when nested elements [Github #9031](https://github.com/penpot/penpot/issues/9031) - Fix broken update library notification link in the UI [Github #9070](https://github.com/penpot/penpot/issues/9070) -## 2.16.0 (Unreleased) - -### :boom: Breaking changes & Deprecations - -### :rocket: Epics and highlights - -### :sparkles: New features & Enhancements - -- Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714) - -### :bug: Bugs fixed - -- Fix Alt/Option to draw shapes from center point (by @offreal) [Github #8361](https://github.com/penpot/penpot/pull/8361) -- Add token name on broken token pill on sidebar [Taiga #13527](https://tree.taiga.io/project/penpot/issue/13527) -- Fix tooltip activated when tab change [Taiga #13627](https://tree.taiga.io/project/penpot/issue/13627) -- Fix title on shared button [Taiga #13730](https://tree.taiga.io/project/penpot/issue/13730) -- Fix hover on layers [Taiga #13799](https://tree.taiga.io/project/penpot/issue/13799) -- Fix highlight after name edition [Taiga #13783](https://tree.taiga.io/project/penpot/issue/13783) -- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534) -- Fix dashboard navigation tabs overlap with projects content when scrolling [Taiga #13962](https://tree.taiga.io/project/penpot/issue/13962) -- Fix text editor v1 focus [Taiga #13961](https://tree.taiga.io/project/penpot/issue/13961) -- Fix color dropdown option update [Taiga #14035](https://tree.taiga.io/project/penpot/issue/14035) -- Fix themes modal height [Taiga #14046](https://tree.taiga.io/project/penpot/issue/14046) - ## 2.15.0 (Unreleased) From 13414e7bed68be4f17ea217bc1c5f6fed57ae8f2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 30 Apr 2026 16:04:21 +0200 Subject: [PATCH 276/288] :books: Update changelog --- CHANGES.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3012f32666..14bb99fac5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,10 +22,10 @@ - Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714) - Add "Delete group" option to the assets panel context menu for components, colors and typographies (by @FairyPigDev) [Github #9141](https://github.com/penpot/penpot/issues/9141) -- Add `Alt+click` on a layer's disclosure arrow to recursively expand the entire subtree rooted at that layer in the Layers sidebar; symmetric with the existing `Shift+click` collapse-all gesture, and removes the O(siblings × depth) click cost of unfolding a deep subtree one level at a time [Github #7736](https://github.com/penpot/penpot/issues/7736) +- Add `Alt+click` on a layer's disclosure arrow to recursively expand the entire subtree in the Layers sidebar (by @MilosM348) [Github #9179](https://github.com/penpot/penpot/pull/9179) - Show alpha percentage next to library color values to distinguish colors that differ only in opacity (by @rockchris099) [Github #6328](https://github.com/penpot/penpot/issues/6328) - Add "Clear artboard guides" option to right-click context menu for frames (by @eureka0928) [Github #6987](https://github.com/penpot/penpot/issues/6987) -- Add loader feedback while importing and exporting files [Github #9020](https://github.com/penpot/penpot/issues/9020) +- Add loader feedback while importing and exporting files (by @moorsecopers99) [Github #9024](https://github.com/penpot/penpot/pull/9024) - Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912) - Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248) - Import Tokens from linked library (by @dfelinto) [Github #8391](https://github.com/penpot/penpot/pull/8391) @@ -37,7 +37,7 @@ - Duplicate token group [Taiga #10653](https://tree.taiga.io/project/penpot/us/10653) - Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568) - Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713) -- Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466) +- Add drag-to-change for numeric inputs in workspace sidebar (by @RenzoMXD) [Github #8536](https://github.com/penpot/penpot/pull/8536) - Add CSS linter [Taiga #13790](https://tree.taiga.io/project/penpot/us/13790) - Save and restore selection state in undo/redo (by @eureka0928) [Github #6007](https://github.com/penpot/penpot/issues/6007) - Fix warnings for unsupported token $type (by @Dexterity104) [Github #8790](https://github.com/penpot/penpot/issues/8790) @@ -60,11 +60,11 @@ - Add a search bar to filter board size presets (by @eureka0928) [Github #4658](https://github.com/penpot/penpot/issues/4658) - Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027) - Add page separators in Workspace [Taiga #13611](https://tree.taiga.io/project/penpot/us/13611?milestone=262806) -- Preserve vector content when pasting from external tools such as Inkscape: recognise SVG sent as text/plain (with optional XML declaration and HTML comments), skip the raster preview when an SVG sibling is on the clipboard, and ignore empty SVG blobs that some tools advertise alongside the real payload, so pasted graphics arrive editable without spurious "SVG is invalid" warnings [Github #546](https://github.com/penpot/penpot/issues/546) -- Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts [Github #2457](https://github.com/penpot/penpot/issues/2457) -- Adds a **Pixel grid color** picker in the viewport settings, next to the existing canvas color control [Github #7750](https://github.com/penpot/penpot/issues/7750) -- Add HEX, HSB and HSL support to the third color tab via an inline model switcher: relabel the existing HSVA tab as HSBA (the math was already HSB-equivalent), add an HSB ↔ HSL pill toggle that updates input labels, slider gradients and round-trip values without changing how colors are stored, and persist the chosen model across sessions [Github #9133](https://github.com/penpot/penpot/issues/9133) -- Show specific invitation-link error messages instead of a single generic "Invite invalid" page: distinguish expired invitations, email-mismatch (signed in with the wrong account) and corrupted/invalid tokens, each with an actionable recovery hint [Github #9220](https://github.com/penpot/penpot/issues/9220) +- Preserve vector content when pasting SVG from external tools such as Inkscape (by @RenzoMXD) [Github #9182](https://github.com/penpot/penpot/pull/9182) +- Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts (by @RenzoMXD) [Github #9063](https://github.com/penpot/penpot/pull/9063) +- Add pixel grid color picker in viewport settings (by @Yakehira) [Github #7750](https://github.com/penpot/penpot/issues/7750) +- Add HEX, HSB and HSL support to the color picker with a model switcher that persists across sessions (by @edwin-rivera-dev) [Github #9133](https://github.com/penpot/penpot/issues/9133) +- Show specific invitation-link error messages for expired, email-mismatch and invalid token cases [Github #9220](https://github.com/penpot/penpot/issues/9220) - Show detailed messages on file import errors to help diagnose why a file could not be imported (by @jsdevninja) [Github #9004](https://github.com/penpot/penpot/issues/9004) - Add read-only preview mode for saved versions — click a version name to open a dedicated preview view (by @wdeveloper16) [Github #8976](https://github.com/penpot/penpot/issues/8976) - Add clipboard read/write permissions to the plugin system (by @wdeveloper16) [Github #9053](https://github.com/penpot/penpot/issues/9053) @@ -82,22 +82,22 @@ - Fix text editor v1 focus [Taiga #13961](https://tree.taiga.io/project/penpot/issue/13961) - Fix color dropdown option update [Taiga #14035](https://tree.taiga.io/project/penpot/issue/14035) - Fix themes modal height [Taiga #14046](https://tree.taiga.io/project/penpot/issue/14046) -- Fix layers-panel rename input opening with the type-based default (e.g. "Ellipse") instead of the user's saved name when re-entering edit mode on a previously renamed layer; the silent revert could overwrite the saved name on confirm. The `default-value` `mf/with-memo` was missing `shape-name` from its dependency list, so once the memo cached the original default it never refreshed. Adds `shape-name` to the deps and force-syncs the input's DOM value on every entry into edit mode [Github #9230](https://github.com/penpot/penpot/issues/9230) -- Suppress the browser context menu when right-clicking empty space in the workspace sidebars while preserving it on text inputs so paste/select-all still work [Github #5127](https://github.com/penpot/penpot/issues/5127) -- Fix release notes modal appearing behind the dashboard sidebar [Github #8296](https://github.com/penpot/penpot/issues/8296) -- Fix plugin API `fileVersion.restore()` promise hanging indefinitely on restore failure [Github #9092](https://github.com/penpot/penpot/issues/9092) -- Fix imported stroke-only SVG paths losing their rounded join when authoring tools (e.g. Figma → Heroicons) split a continuous polyline into adjacent `M…L M…L` subpaths sharing an endpoint; on import these are now folded back into one chain so `stroke-linejoin` renders the elbow correctly in both editor and exports [Github #5283](https://github.com/penpot/penpot/issues/5283) -- Fix plugin API `library.connectLibrary()` returning a non-Promise (or throwing synchronously) when the plugin lacks `library:write` permission — the method now always returns a `Promise` and rejects with a structured error message, matching the contract used by every other Promise-returning plugin method (`restore`, `remove`, `pin`, `saveVersion`, `findVersions`, …) -- Fix LDAP provider params schema typo (`bind-passwor` → `bind-password`) introduced during the `clojure.spec` → `malli` migration; the schema slot now matches the runtime key actually read by `prepare-params` (`:password (:bind-password cfg)`) and `try-connectivity` (`(:bind-password cfg)`), so a wrong type for the password no longer slips through unvalidated -- Fix `login-with-ldap` silently dropping its error message on the `ldap-not-initialized` restriction (typo `:hide` → `:hint`); the message `"ldap auth provider is not initialized"` now actually surfaces in logs and error responses instead of being discarded into an unread key -- Fix Plugin API `shape.applyToken()` / `token.applyToShapes()` / `token.applyToSelected()` rejecting JS-array attribute lists like `["fill"]`: switched the inner schemas to `[::sm/set ...]` (which has the JS array → Clojure set decoder) and made `token-attr-plugin->token-attr` accept string inputs by coercing them to keywords before consulting the alias map [Github #9162](https://github.com/penpot/penpot/issues/9162) -- Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored (`userinfo` / `token`) in the OIDC callback, causing "incomplete user info" failures during registration [Github #9108](https://github.com/penpot/penpot/issues/9108) -- Fix `get-view-only-bundle` crashing when a share-link viewer encounters a team member whose email lacks `@` (NullPointerException in `obfuscate-email`) or whose domain has no `.` (previously produced a dangling-dot `****@****.`); now the viewer-side obfuscation is nil-safe and omits the trailing dot when the domain has no TLD +- Fix layers panel rename input showing the default type name instead of the saved layer name (by @jack-stormentswe) [Github #9231](https://github.com/penpot/penpot/pull/9231) +- Suppress browser context menu on right-click in workspace sidebars while preserving it on text inputs (by @sujyotraut) [Github #5127](https://github.com/penpot/penpot/issues/5127) +- Fix release notes modal appearing behind the dashboard sidebar (by @ciaokitty) [Github #8296](https://github.com/penpot/penpot/issues/8296) +- Fix plugin API `fileVersion.restore()` promise hanging indefinitely on restore failure (by @thomascolden585-svg) [Github #9092](https://github.com/penpot/penpot/issues/9092) +- Fix imported stroke-only SVG paths losing their rounded join when split into adjacent subpaths (by @Chrissi2812) [Github #5283](https://github.com/penpot/penpot/issues/5283) +- Fix plugin API `library.connectLibrary()` not returning a Promise when the plugin lacks `library:write` permission (by @boskodev790) [Github #9158](https://github.com/penpot/penpot/pull/9158) +- Fix LDAP provider schema typo (`bind-passwor` → `bind-password`) introduced during the `clojure.spec` → `malli` migration (by @boskodev790) [Github #9165](https://github.com/penpot/penpot/pull/9165) +- Fix `login-with-ldap` silently dropping the error message when LDAP is not initialized (typo `:hide` → `:hint`) (by @boskodev790) [Github #9159](https://github.com/penpot/penpot/pull/9159) +- Fix plugin API `applyToken()` / `applyToShapes()` / `applyToSelected()` rejecting JS-array attribute lists (by @brunopbezerra) [Github #9162](https://github.com/penpot/penpot/issues/9162) +- Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored in the OIDC callback (by @GeekClassy) [Github #9108](https://github.com/penpot/penpot/issues/9108) +- Fix crash in share-link viewer when a team member's email is missing `@` or has no domain TLD (by @boskodev790) [Github #9120](https://github.com/penpot/penpot/pull/9120) - Fix crash when pasting a component with variants from an external shared library into a file that uses that library (by @FairyPigDev) [Github #8144](https://github.com/penpot/penpot/issues/8144) -- Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled [Github #8877](https://github.com/penpot/penpot/issues/8877) -- Fix Copy as SVG: emit a single valid SVG document when multiple shapes are selected, and publish `image/svg+xml` to the clipboard so the paste target works in Inkscape and other SVG-native tools [Github #838](https://github.com/penpot/penpot/issues/838) +- Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled (by @TheAifam5) [Github #8877](https://github.com/penpot/penpot/issues/8877) +- Fix Copy as SVG to produce a valid document for multi-shape selections and use `image/svg+xml` MIME type (by @RenzoMXD) [Github #9066](https://github.com/penpot/penpot/pull/9066) - Reset profile submenu state when the account menu closes (by @eureka0928) [Github #8947](https://github.com/penpot/penpot/issues/8947) -- Preserve OpenType variant name table for custom fonts in the dashboard [Github #8924](https://github.com/penpot/penpot/issues/8924) +- Preserve OpenType variant name table for custom fonts in the dashboard (by @rutherfordcraze) [Github #8924](https://github.com/penpot/penpot/issues/8924) - Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582) - Fix styles between grid layout inputs [Taiga #13526](https://tree.taiga.io/project/penpot/issue/13526) - Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534) @@ -120,11 +120,11 @@ - Fix non-functional clear icon in change email modal inputs (by @Dexterity104) [Github #8977](https://github.com/penpot/penpot/issues/8977) - Disable save button after saving account profile settings (by @Dexterity104) [Github #8979](https://github.com/penpot/penpot/issues/8979) - Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990) -- Allow deleting the profile avatar after uploading [Github #9067](https://github.com/penpot/penpot/issues/9067) +- Allow deleting the profile avatar after uploading (by @moorsecopers99) [Github #9067](https://github.com/penpot/penpot/issues/9067) - Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [Github #8516](https://github.com/penpot/penpot/issues/8516) - Fix Settings and Notifications "Update Settings" button enabled state when form has no changes (by @moorsecopers99) [Github #9090](https://github.com/penpot/penpot/issues/9090) - Fix "Help & Learning" submenu vertical alignment in account menu (by @juan-flores077) [Github #9137](https://github.com/penpot/penpot/issues/9137) -- Fix plugin `addInteraction` silently rejecting `open-overlay` actions with `manualPositionLocation` [Github #8409](https://github.com/penpot/penpot/issues/8409) +- Fix plugin `addInteraction` silently rejecting `open-overlay` actions with `manualPositionLocation` (by @axelseis) [Github #8409](https://github.com/penpot/penpot/issues/8409) - Fix typography style creation with tokenized line-height (by @juan-flores077) [Github #8479](https://github.com/penpot/penpot/issues/8479) - Fix colorpicker layout so the eyedropper button is visible again [Taiga #14057](https://tree.taiga.io/project/penpot/issue/14057) - Fix restore-deleted-team-files failing due to a typo in the reduce accumulator (by @Dexterity104) [Github #9241](https://github.com/penpot/penpot/issues/9241) From d09985edee3e4474c4851b917521ad2d5bd85ab6 Mon Sep 17 00:00:00 2001 From: Jeff <158072326+jeffrey701@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:17:34 +0200 Subject: [PATCH 277/288] :bug: Preserve Inkscape labels when pasting SVGs (#9252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Steps to reproduce: paste an SVG authored in Inkscape (or any editor that follows the inkscape:label convention) into a penpot file. The group/element names visible in the source editor are dropped — penpot shows generic auto-ids like 'g1234' or 'path5678' instead. Root cause: parse-svg-element in common/src/app/common/files/shapes_ builder.cljc derived the shape name from (or (:id attrs) (tag->name tag)). Inkscape stores user-given element labels in the inkscape:label and sodipodi:label namespaced attributes while id holds an auto- generated technical id, so the operator's chosen name was always overridden by the technical id when present. tubax/xml->clj (the SVG parser the import pipeline already uses for upload, paste, and library import) keeps namespaced attributes as :prefix:name keywords — the same shape this file already reads :xlink:href from on line 134, and that app.common.svg uses for the xlink: namespace at lines 300-307. Fix: extract the name-resolution logic into a public resolve-element- name helper that prefers :inkscape:label, then :sodipodi:label, then :id, then (tag->name tag). Existing SVGs that don't carry either label namespace fall through the same chain as before, so the behaviour for non-Inkscape-authored SVGs is unchanged. This restores the behaviour dfelinto's penpot-icon-generator-plugin relies on (linked from the issue body) — that plugin reads element names from the imported SVG to map Blender icons to penpot components. Tests: 6 deftest blocks in common/test/common_tests/files/shapes_ builder_test.cljc covering the priority order (inkscape > sodipodi > id > tag), each fallback in isolation, and the empty-attrs case. Registered in common-tests.runner. Closes #7869 Co-authored-by: Andrey Antukh --- .../src/app/common/files/shapes_builder.cljc | 18 ++++++- .../files/shapes_builder_test.cljc | 52 +++++++++++++++++++ common/test/common_tests/runner.cljc | 1 + 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 common/test/common_tests/files/shapes_builder_test.cljc diff --git a/common/src/app/common/files/shapes_builder.cljc b/common/src/app/common/files/shapes_builder.cljc index 668f50fcaf..a70d6eef9b 100644 --- a/common/src/app/common/files/shapes_builder.cljc +++ b/common/src/app/common/files/shapes_builder.cljc @@ -677,6 +677,22 @@ (remove is-style-fragment?) ;; Filter style fragments and hex colors (filter #(contains? defs %))))) ;; Only existing defs +(defn resolve-element-name + "Pick the most user-meaningful name for an SVG element. + + Inkscape (and editors following the same convention) write the + operator-given label to ``inkscape:label``/``sodipodi:label`` while + ``id`` holds an auto-generated technical id like ``path1234``. + Preferring the namespaced label keeps the layer/group/element names + the operator sees in their source editor across a paste/import + (#7869); the existing ``id`` and ``(tag->name tag)`` fallbacks keep + legacy SVGs that don't carry a label working unchanged." + [tag attrs] + (or (:inkscape:label attrs) + (:sodipodi:label attrs) + (:id attrs) + (tag->name tag))) + (defn parse-svg-element [frame-id svg-data {:keys [tag attrs hidden] :as element} unames] @@ -684,7 +700,7 @@ ;; think we should handle this case early and avoid some code ;; execution - (let [name (or (:id attrs) (tag->name tag)) + (let [name (resolve-element-name tag attrs) att-refs (csvg/find-attr-references attrs) defs (get svg-data :defs) valid-refs (filter-valid-def-references att-refs defs) diff --git a/common/test/common_tests/files/shapes_builder_test.cljc b/common/test/common_tests/files/shapes_builder_test.cljc new file mode 100644 index 0000000000..05956918b2 --- /dev/null +++ b/common/test/common_tests/files/shapes_builder_test.cljc @@ -0,0 +1,52 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns common-tests.files.shapes-builder-test + (:require + [app.common.files.shapes-builder :as sb] + [clojure.test :as t])) + +;; Regression for https://github.com/penpot/penpot/issues/7869. +;; ``parse-svg-element`` used to derive the shape name from +;; ``(or (:id attrs) (tag->name tag))`` which dropped Inkscape-authored +;; labels. ``tubax/xml->clj`` (the SVG parser the rest of the import +;; pipeline already feeds these maps to) keeps namespaced attributes as +;; ``:prefix:name`` keywords — same shape the codebase already reads +;; ``:xlink:href`` from in this file (line 134) and in +;; ``app.common.svg``. + +(t/deftest resolve-element-name-prefers-inkscape-label + (t/is (= "Layer 1" + (sb/resolve-element-name :g {:inkscape:label "Layer 1" + :id "g1234"})))) + +(t/deftest resolve-element-name-prefers-sodipodi-label-when-no-inkscape-label + (t/is (= "phone-icon" + (sb/resolve-element-name :path {:sodipodi:label "phone-icon" + :id "path5678"})))) + +(t/deftest resolve-element-name-falls-back-to-id-when-no-label-namespace + (t/is (= "manual-id" + (sb/resolve-element-name :rect {:id "manual-id"})))) + +(t/deftest resolve-element-name-falls-back-to-tag-name-when-no-id-and-no-label + ;; The tag->name mapping returns generic names for known SVG element + ;; tags. Asserting on the call result here (rather than a hardcoded + ;; string) keeps the test stable if the tag->name mapping is updated. + (t/is (some? (sb/resolve-element-name :rect {}))) + (t/is (string? (sb/resolve-element-name :rect {})))) + +(t/deftest resolve-element-name-inkscape-label-wins-over-sodipodi-and-id + ;; Both label conventions and an id present together; the priority is + ;; inkscape > sodipodi > id > tag, matching the order operators expect + ;; (Inkscape's own UI shows ``inkscape:label`` as the canonical name). + (t/is (= "user-name" + (sb/resolve-element-name :g {:inkscape:label "user-name" + :sodipodi:label "stale-label" + :id "g1"})))) + +(t/deftest resolve-element-name-empty-attrs-uses-tag-fallback + (t/is (some? (sb/resolve-element-name :path {})))) diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index 06f7926c47..29540525db 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -15,6 +15,7 @@ [common-tests.files-builder-test] [common-tests.files-changes-test] [common-tests.files-migrations-test] + [common-tests.files.shapes-builder-test] [common-tests.geom-align-test] [common-tests.geom-bounds-map-test] [common-tests.geom-flex-layout-test] From 8f03b5ed9c707c7022a5bfad444074227c9cb50c Mon Sep 17 00:00:00 2001 From: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:21:40 +0200 Subject: [PATCH 278/288] :fire: Remove stray debug log in frame-preview load-ref callback (#9258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `(.log js/console "load-ref" iframe-dom)` was left in the iframe ref callback of `frame-preview`. Mirrors the defect PR #9243 removed from `color-row*` — fires on every ref invocation and pollutes the browser console. Signed-off-by: jack-stormentswe Signed-off-by: Andrey Antukh Co-authored-by: Andrey Antukh --- frontend/src/app/main/ui/frame_preview.cljs | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/main/ui/frame_preview.cljs b/frontend/src/app/main/ui/frame_preview.cljs index bff3da3abd..de15702d97 100644 --- a/frontend/src/app/main/ui/frame_preview.cljs +++ b/frontend/src/app/main/ui/frame_preview.cljs @@ -37,7 +37,6 @@ load-ref (mf/use-callback (fn [iframe-dom] - (.log js/console "load-ref" iframe-dom) (mf/set-ref-val! iframe-ref iframe-dom) (when (and iframe-dom @last-data*) (-> iframe-dom .-contentWindow .-document .open) From f24ad6bee481c6202fca9f317e4a568359a72274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Mon, 4 May 2026 09:29:14 +0200 Subject: [PATCH 279/288] :sparkles: Show current plan in Nitrate * :sparkles: Show current plan in Nitrate * :paperclip: Code Review --- .../src/app/main/ui/dashboard/sidebar.cljs | 6 ++++- .../app/main/ui/dashboard/subscription.cljs | 21 +++++++++++++++++ .../app/main/ui/dashboard/subscription.scss | 21 +++++++++++++++++ frontend/translations/en.po | 23 ++++++++++++++++++- frontend/translations/es.po | 23 ++++++++++++++++++- 5 files changed, 91 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 2faef9cbec..e9a8ac3be1 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -32,6 +32,7 @@ [app.main.ui.dashboard.subscription :refer [dashboard-cta* get-subscription-type menu-team-icon* + nitrate-current-plan* nitrate-sidebar* show-subscription-dashboard-banner? subscription-sidebar*]] @@ -1323,12 +1324,15 @@ [:* (if (contains? cf/flags :nitrate) - [:> nitrate-sidebar* {:profile profile :teams teams}] + [:* + [:> nitrate-sidebar* {:profile profile :teams teams}] + [:> nitrate-current-plan* {:profile profile}]] (when (contains? cf/flags :subscriptions) (if (show-subscription-dashboard-banner? profile) [:> dashboard-cta* {:profile profile}] [:> subscription-sidebar* {:profile profile}]))) + ;; TODO remove this block when subscriptions is full implemented (when (contains? cf/flags :subscriptions-old) [:button {:class (stl/css :upgrade-plan-section) diff --git a/frontend/src/app/main/ui/dashboard/subscription.cljs b/frontend/src/app/main/ui/dashboard/subscription.cljs index 5d1f16ea7c..86dcba36fd 100644 --- a/frontend/src/app/main/ui/dashboard/subscription.cljs +++ b/frontend/src/app/main/ui/dashboard/subscription.cljs @@ -171,6 +171,27 @@ "UPGRADE TO NITRATE" "Try 14 days for free")]]])))) +(mf/defc nitrate-current-plan* + [{:keys [profile]}] + (let [nitrate? (dnt/is-valid-license? profile) + nitrate-license (:subscription profile) + subscription (-> profile :props :subscription) + subscription-type (if nitrate? (:type nitrate-license) (get-subscription-type subscription)) + subscription-is-trial (= "trialing" (:status (if nitrate? nitrate-license subscription)))] + [:div {:class (stl/css :nitrate-current-plan)} + [:div {:class (stl/css :nitrate-current-plan-label)} + (tr "subscription.current-plan.title")] + [:div {:class (stl/css :nitrate-current-plan-text)} + (case subscription-type + "professional" (tr "subscription.current-plan.professional") + "unlimited" (if subscription-is-trial + (tr "subscription.current-plan.unlimited-trial") + (tr "subscription.current-plan.unlimited")) + "nitrate" (if subscription-is-trial + (tr "subscription.current-plan.nitrate-trial") + (tr "subscription.current-plan.nitrate")) + "enterprise" (tr "subscription.current-plan.enterprise"))]])) + (mf/defc team* [{:keys [is-owner team]}] (let [subscription (:subscription team) diff --git a/frontend/src/app/main/ui/dashboard/subscription.scss b/frontend/src/app/main/ui/dashboard/subscription.scss index 0edb685c61..2a580fd2b1 100644 --- a/frontend/src/app/main/ui/dashboard/subscription.scss +++ b/frontend/src/app/main/ui/dashboard/subscription.scss @@ -251,3 +251,24 @@ .nitrate-bottom-button { width: fit-content; } + +.nitrate-current-plan { + border-radius: var(--sp-s); + margin: 0 var(--sp-m) var(--sp-m) var(--sp-m); + background: var(--color-background-tertiary); + border: $b-1 solid var(--color-background-quaternary); + padding: var(--sp-m) var(--sp-l); +} + +.nitrate-current-plan-label { + @include t.use-typography("body-small"); + + padding-block-end: var(--sp-xs); + color: var(--color-foreground-secondary); +} + +.nitrate-current-plan-text { + @include t.use-typography("body-medium"); + + color: var(--color-foreground-primary); +} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index caec56566d..f80d4beb21 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -9368,4 +9368,25 @@ msgid "nitrate.subscription.active-until" msgstr "Active until %s" msgid "subscription.settings.activate-by-code" -msgstr "Enter activation code" \ No newline at end of file +msgstr "Enter activation code" + +msgid "subscription.current-plan.title" +msgstr "Your subscription" + +msgid "subscription.current-plan.professional" +msgstr "Professional" + +msgid "subscription.current-plan.unlimited" +msgstr "Unlimited" + +msgid "subscription.current-plan.unlimited-trial" +msgstr "Unlimited Trial" + +msgid "subscription.current-plan.nitrate" +msgstr "Nitrate" + +msgid "subscription.current-plan.nitrate-trial" +msgstr "Nitrate Trial" + +msgid "subscription.current-plan.enterprise" +msgstr "Enterprise" \ No newline at end of file diff --git a/frontend/translations/es.po b/frontend/translations/es.po index fd978a7e4b..1b9a2f166f 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -9076,4 +9076,25 @@ msgid "subscription.settings.more-information" msgstr "Más información" msgid "subscription.settings.activate-by-code" -msgstr "Introducir código de activación" \ No newline at end of file +msgstr "Introducir código de activación" + +msgid "subscription.current-plan.title" +msgstr "Tu suscripción" + +msgid "subscription.current-plan.professional" +msgstr "Professional" + +msgid "subscription.current-plan.unlimited" +msgstr "Unlimited" + +msgid "subscription.current-plan.unlimited-trial" +msgstr "Unlimited (Prueba)" + +msgid "subscription.current-plan.nitrate" +msgstr "Nitrate" + +msgid "subscription.current-plan.nitrate-trial" +msgstr "Nitrate (Prueba)" + +msgid "subscription.current-plan.enterprise" +msgstr "Enterprise" \ No newline at end of file From 152967bea69589550f5338fe618fd5f9b070d754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Mon, 4 May 2026 09:55:06 +0200 Subject: [PATCH 280/288] :bug: Fix sidebar overflow --- frontend/src/app/main/ui/dashboard/sidebar.cljs | 5 +++-- frontend/src/app/main/ui/dashboard/sidebar.scss | 11 ++++++++++- frontend/src/app/main/ui/dashboard/subscription.scss | 4 ++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index e9a8ac3be1..84509976a4 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -1057,11 +1057,12 @@ (reset! overflow* (> scroll-height client-height)))) [:* - [:div {:ref container} + [:div {:class (stl/css :sidebar-content-wrapper)} (when nitrate? [:div {:class (stl/css :orgs-container)} [:> sidebar-org-switch* {:team team :profile profile}]]) - [:div {:class (stl/css-case :sidebar-content true :sidebar-content-nitrate nitrate?)} + [:div {:ref container + :class (stl/css-case :sidebar-content true :sidebar-content-nitrate nitrate?)} [:> sidebar-team-switch* {:team team :profile profile}] [:> sidebar-search* {:search-term search-term diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index 73e56d497a..51b3b929a2 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -29,11 +29,20 @@ } // SIDEBAR CONTENT COMPONENT +.sidebar-content-wrapper { + display: flex; + flex-direction: column; + min-height: 0; + height: 100%; + overflow: hidden; +} + .sidebar-content { display: grid; grid-template-rows: auto auto auto auto 1fr; gap: var(--sp-xxl); - height: 100%; + flex: 1; + min-height: 0; padding: 0; overflow: hidden auto; } diff --git a/frontend/src/app/main/ui/dashboard/subscription.scss b/frontend/src/app/main/ui/dashboard/subscription.scss index 2a580fd2b1..b37cff4658 100644 --- a/frontend/src/app/main/ui/dashboard/subscription.scss +++ b/frontend/src/app/main/ui/dashboard/subscription.scss @@ -224,7 +224,7 @@ display: flex; border-radius: var(--sp-s); flex-direction: column; - margin: var(--sp-m); + margin: var(--sp-m) var(--sp-m) 0; background: var(--color-background-quaternary); border: $b-1 solid var(--color-accent-primary-muted); padding: var(--sp-l); @@ -254,7 +254,7 @@ .nitrate-current-plan { border-radius: var(--sp-s); - margin: 0 var(--sp-m) var(--sp-m) var(--sp-m); + margin: var(--sp-m); background: var(--color-background-tertiary); border: $b-1 solid var(--color-background-quaternary); padding: var(--sp-m) var(--sp-l); From a2bcbe81ddd07e84a0ef44bf5e56b946031c557a Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Mon, 4 May 2026 13:02:19 +0200 Subject: [PATCH 281/288] :tada: Add token numeric inputs for inputs on right sidebar (#9143) Co-authored-by: Xavier Julian --- CHANGES.md | 7 +- common/src/app/common/features.cljc | 1 + .../playwright/ui/specs/components.spec.js | 11 +- .../playwright/ui/specs/design-tab.spec.js | 34 +++-- .../ui/specs/multiseleccion.spec.js | 144 ++++++++++++------ .../playwright/ui/specs/profile-menu.spec.js | 8 +- .../ui/specs/workspace-modifers.spec.js | 140 ++++++++++++++--- .../sidebar/options/menus/measures.cljs | 3 +- 8 files changed, 257 insertions(+), 91 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 14bb99fac5..ce033171a8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,8 +10,6 @@ ### :bug: Bugs fixed - - ## 2.16.0 (Unreleased) ### :boom: Breaking changes & Deprecations @@ -68,6 +66,7 @@ - Show detailed messages on file import errors to help diagnose why a file could not be imported (by @jsdevninja) [Github #9004](https://github.com/penpot/penpot/issues/9004) - Add read-only preview mode for saved versions — click a version name to open a dedicated preview view (by @wdeveloper16) [Github #8976](https://github.com/penpot/penpot/issues/8976) - Add clipboard read/write permissions to the plugin system (by @wdeveloper16) [Github #9053](https://github.com/penpot/penpot/issues/9053) +- Add new numeric inputs for token management on the right sidebar [Taiga #12109](https://tree.taiga.io/project/penpot/us/12109?milestone=513226) ### :bug: Bugs fixed @@ -132,7 +131,6 @@ - Fix tooltip appearing two times when nested elements [Github #9031](https://github.com/penpot/penpot/issues/9031) - Fix broken update library notification link in the UI [Github #9070](https://github.com/penpot/penpot/issues/9070) - ## 2.15.0 (Unreleased) ### :sparkles: New features & Enhancements @@ -147,7 +145,6 @@ - Fix incorrect handling of version restore operation [Github #9041](https://github.com/penpot/penpot/pull/9041) - Fix Plugin API token methods rejecting JS array of strings [Github #9162](https://github.com/penpot/penpot/issues/9162) - ## 2.14.4 ### :bug: Bugs fixed @@ -156,7 +153,6 @@ - Fix email blacklisting [Github #9122](https://github.com/penpot/penpot/pull/9122) - Fix removeChild errors from unmount race conditions [Github #8927](https://github.com/penpot/penpot/pull/8927) - ## 2.14.3 ### :sparkles: New features & Enhancements @@ -187,7 +183,6 @@ - Fix typo `:podition` in swap-shapes grid cell - Fix multiple selection on shapes with token applied to stroke color - ## 2.14.2 ### :sparkles: New features & Enhancements diff --git a/common/src/app/common/features.cljc b/common/src/app/common/features.cljc index 516789428b..abe66aaab5 100644 --- a/common/src/app/common/features.cljc +++ b/common/src/app/common/features.cljc @@ -68,6 +68,7 @@ "components/v2" "plugins/runtime" "design-tokens/v1" + "tokens/numeric-input" "variants/v1"}) ;; A set of features which only affects on frontend and can be enabled diff --git a/frontend/playwright/ui/specs/components.spec.js b/frontend/playwright/ui/specs/components.spec.js index 50adc17eae..9661ba9c88 100644 --- a/frontend/playwright/ui/specs/components.spec.js +++ b/frontend/playwright/ui/specs/components.spec.js @@ -3,9 +3,12 @@ import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; test.beforeEach(async ({ page }) => { await WasmWorkspacePage.init(page); + await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-token-input"]); }); -test("BUG 13267 - Component instance is not synced with parent for geometry changes", async ({ page }) => { +test("BUG 13267 - Component instance is not synced with parent for geometry changes", async ({ + page, +}) => { const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(page); await workspacePage.mockGetFile("components/get-file-13267.json"); @@ -21,7 +24,9 @@ test("BUG 13267 - Component instance is not synced with parent for geometry chan // Select the main component await workspacePage.clickLeafLayer("A Component", {}, 1); - const rotationInput = workspacePage.rightSidebar.getByTestId("rotation").getByRole("textbox"); + const rotationInput = workspacePage.rightSidebar.getByRole("textbox", { + name: "Rotation", + }); await rotationInput.fill("45"); await rotationInput.press("Enter"); @@ -30,4 +35,4 @@ test("BUG 13267 - Component instance is not synced with parent for geometry chan await workspacePage.clickLeafLayer("Rectangle"); await expect(rotationInput).toHaveValue("45"); -}); \ No newline at end of file +}); diff --git a/frontend/playwright/ui/specs/design-tab.spec.js b/frontend/playwright/ui/specs/design-tab.spec.js index 8fe67b9d3a..0cf953a302 100644 --- a/frontend/playwright/ui/specs/design-tab.spec.js +++ b/frontend/playwright/ui/specs/design-tab.spec.js @@ -1,8 +1,11 @@ import { test, expect } from "@playwright/test"; import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; +const tokenInputFlag = "enable-feature-token-input"; + test.beforeEach(async ({ page }) => { await WasmWorkspacePage.init(page); + await WasmWorkspacePage.mockConfigFlags(page, [tokenInputFlag]); }); const multipleConstraintsFileId = `03bff843-920f-81a1-8004-756365e1eb6a`; @@ -71,7 +74,10 @@ test.describe("Shape attributes", () => { page, }) => { const workspace = new WasmWorkspacePage(page); - await workspace.mockConfigFlags(["enable-feature-render-wasm"]); + await workspace.mockConfigFlags([ + "enable-feature-render-wasm", + tokenInputFlag, + ]); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "design/get-file-fills-limit.json"); @@ -95,7 +101,10 @@ test.describe("Shape attributes", () => { page, }) => { const workspace = new WasmWorkspacePage(page); - await workspace.mockConfigFlags(["enable-feature-render-wasm"]); + await workspace.mockConfigFlags([ + "enable-feature-render-wasm", + tokenInputFlag, + ]); await workspace.setupEmptyFile(); await workspace.mockRPC( /get\-file\?/, @@ -236,7 +245,7 @@ test.describe("Background blur", () => { page, }) => { const workspace = new WasmWorkspacePage(page); - await workspace.mockConfigFlags(["enable-background-blur"]); + await workspace.mockConfigFlags(["enable-background-blur", tokenInputFlag]); await workspace.setupEmptyFile(); await workspace.mockGetFile("render-wasm/get-file-background-blur.json"); @@ -260,7 +269,7 @@ test.describe("Background blur", () => { page, }) => { const workspace = new WasmWorkspacePage(page); - await workspace.mockConfigFlags(["enable-background-blur"]); + await workspace.mockConfigFlags(["enable-background-blur", tokenInputFlag]); await workspace.setupEmptyFile(); await workspace.mockGetFile("render-wasm/get-file-background-blur.json"); @@ -319,6 +328,7 @@ test("BUG 9543 - Layout padding inputs not showing 'mixed' when needed", async ( page, }) => { const workspace = new WasmWorkspacePage(page); + await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "design/get-file-9543.json"); await workspace.mockRPC( @@ -338,14 +348,18 @@ test("BUG 9543 - Layout padding inputs not showing 'mixed' when needed", async ( }); await toggle.click(); - await workspace.page.getByLabel("Top padding").fill("10"); + const topPaddingInput = workspace.page.getByRole("textbox", { + name: "Top padding", + }); + await topPaddingInput.fill("10"); + await topPaddingInput.press("Enter"); await toggle.click(); - await expect(workspace.page.getByLabel("Vertical padding")).toHaveValue(""); - await expect(workspace.page.getByLabel("Vertical padding")).toHaveAttribute( - "placeholder", - "Mixed", - ); + const verticalPaddingInput = await workspace.page.getByRole("textbox", { + name: "Vertical padding", + }); + await expect(verticalPaddingInput).toHaveValue(""); + await expect(verticalPaddingInput).toHaveAttribute("placeholder", "Mixed"); }); test("BUG 11177 - Font size input not showing 'mixed' when needed", async ({ diff --git a/frontend/playwright/ui/specs/multiseleccion.spec.js b/frontend/playwright/ui/specs/multiseleccion.spec.js index 1b4be19e4c..5d7ee1c92c 100644 --- a/frontend/playwright/ui/specs/multiseleccion.spec.js +++ b/frontend/playwright/ui/specs/multiseleccion.spec.js @@ -3,6 +3,7 @@ import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; test.beforeEach(async ({ page }) => { await WasmWorkspacePage.init(page); + await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-token-input"]); }); test("Multiselection - check multiple values in measures", async ({ page }) => { @@ -27,37 +28,53 @@ test("Multiselection - check multiple values in measures", async ({ page }) => { await workspacePage.layers.getByTestId("layer-row").nth(0).click(); // === CHECK SINGLE SELECTION - ALL MEASURE FIELDS === - const measuresSection = workspacePage.rightSidebar.getByRole('region', { name: 'shape-measures-section' }); + const measuresSection = workspacePage.rightSidebar.getByRole("region", { + name: "shape-measures-section", + }); await expect(measuresSection).toBeVisible(); // Width - const widthInput = measuresSection.getByTitle('Width', { exact: true }).getByRole('textbox'); + const widthInput = measuresSection.getByRole("textbox", { + name: "Width", + exact: true, + }); await expect(widthInput).toHaveValue("360"); // Height - const heightInput = measuresSection.getByTitle('Height', { exact: true }).getByRole('textbox'); + const heightInput = measuresSection.getByRole("textbox", { + name: "Height", + exact: true, + }); await expect(heightInput).toHaveValue("53"); // X Position (using "X axis" title) - const xPosInput = measuresSection.getByTitle('X axis', { exact: true }).getByRole('textbox'); + const xPosInput = measuresSection.getByRole("textbox", { + name: "X axis", + exact: true, + }); await expect(xPosInput).toHaveValue("1094"); // Y Position (using "Y axis" title) - const yPosInput = measuresSection.getByTitle('Y axis', { exact: true }).getByRole('textbox'); + const yPosInput = measuresSection.getByRole("textbox", { + name: "Y axis", + exact: true, + }); await expect(yPosInput).toHaveValue("856"); // === CHECK MULTI-SELECTION - MIXED VALUES === // Shift+click to add second layer to selection - await workspacePage.layers.getByTestId("layer-row").nth(1).click({ modifiers: ['Shift'] }); + await workspacePage.layers + .getByTestId("layer-row") + .nth(1) + .click({ modifiers: ["Shift"] }); // All measure fields should show "Mixed" placeholder when values differ - await expect(widthInput).toHaveAttribute('placeholder', 'Mixed'); - await expect(heightInput).toHaveAttribute('placeholder', 'Mixed'); - await expect(xPosInput).toHaveAttribute('placeholder', 'Mixed'); - await expect(yPosInput).toHaveAttribute('placeholder', 'Mixed'); + await expect(widthInput).toHaveAttribute("placeholder", "Mixed"); + await expect(heightInput).toHaveAttribute("placeholder", "Mixed"); + await expect(xPosInput).toHaveAttribute("placeholder", "Mixed"); + await expect(yPosInput).toHaveAttribute("placeholder", "Mixed"); }); - test("Multiselection - check fill multiple values", async ({ page }) => { const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(page); @@ -79,17 +96,22 @@ test("Multiselection - check fill multiple values", async ({ page }) => { await workspacePage.layers.getByTestId("layer-row").nth(0).click(); // Fill section - const fillSection = workspacePage.rightSidebar.getByRole('region', { name: "Fill section" }); + const fillSection = workspacePage.rightSidebar.getByRole("region", { + name: "Fill section", + }); await expect(fillSection).toBeVisible(); // Single selection - fill color should be visible (not "Mixed") await expect(fillSection.getByText(/Mixed/i)).not.toBeVisible(); // Multi-selection with Shift+click - await workspacePage.layers.getByTestId("layer-row").nth(1).click({ modifiers: ['Shift'] }); + await workspacePage.layers + .getByTestId("layer-row") + .nth(1) + .click({ modifiers: ["Shift"] }); // Should show "Mixed" for fills when shapes have different fill colors - await expect(fillSection.getByText('Mixed')).toBeVisible(); + await expect(fillSection.getByText("Mixed")).toBeVisible(); }); test("Multiselection - check stroke multiple values", async ({ page }) => { @@ -113,17 +135,22 @@ test("Multiselection - check stroke multiple values", async ({ page }) => { await workspacePage.layers.getByTestId("layer-row").nth(0).click(); // Stroke section - const strokeSection = workspacePage.rightSidebar.getByRole('region', { name: "Stroke section" }); + const strokeSection = workspacePage.rightSidebar.getByRole("region", { + name: "Stroke section", + }); await expect(strokeSection).toBeVisible(); // Single selection - stroke should be visible (not "Mixed") await expect(strokeSection.getByText(/Mixed/i)).not.toBeVisible(); // Multi-selection - await workspacePage.layers.getByTestId("layer-row").nth(1).click({ modifiers: ['Shift'] }); + await workspacePage.layers + .getByTestId("layer-row") + .nth(1) + .click({ modifiers: ["Shift"] }); // Should show "Mixed" for strokes when shapes have different stroke colors - await expect(strokeSection.getByText('Mixed')).toBeVisible(); + await expect(strokeSection.getByText("Mixed")).toBeVisible(); }); test("Multiselection - check rotation multiple values", async ({ page }) => { @@ -147,26 +174,33 @@ test("Multiselection - check rotation multiple values", async ({ page }) => { await workspacePage.layers.getByTestId("layer-row").nth(1).click(); // Measures section contains rotation - const measuresSection = workspacePage.rightSidebar.getByRole('region', { name: 'shape-measures-section' }); + const measuresSection = workspacePage.rightSidebar.getByRole("region", { + name: "shape-measures-section", + }); await expect(measuresSection).toBeVisible(); // Rotation field exists - const rotationInput = measuresSection.getByTitle('Rotation', { exact: true }).getByRole('textbox'); + const rotationInput = measuresSection.getByRole("textbox", { + name: "Rotation", + exact: true, + }); await expect(rotationInput).toBeVisible(); // Rotate that shape await rotationInput.fill("45"); - await page.keyboard.press('Enter'); + await page.keyboard.press("Enter"); await expect(rotationInput).toHaveValue("45"); // Rotation should be 45 // Multi-selection - await workspacePage.layers.getByTestId("layer-row").nth(0).click({ modifiers: ['Shift'] }); + await workspacePage.layers + .getByTestId("layer-row") + .nth(0) + .click({ modifiers: ["Shift"] }); // Rotation should show "Mixed" placeholder - await expect(rotationInput).toHaveAttribute('placeholder', 'Mixed'); + await expect(rotationInput).toHaveAttribute("placeholder", "Mixed"); }); - test("Multiselection of text and typographies", async ({ page }) => { const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(page); @@ -181,29 +215,45 @@ test("Multiselection of text and typographies", async ({ page }) => { }); const plainTextLayer = workspacePage.layers.getByTestId("layer-row").nth(5); - const plainTextLayerTwo = workspacePage.layers.getByTestId("layer-row").nth(2); - const typographyTextLayerOne = workspacePage.layers.getByTestId("layer-row").nth(7); - const typographyTextLayerTwo = workspacePage.layers.getByTestId("layer-row").nth(4); - const tokenTypographyTextLayerOne = workspacePage.layers.getByTestId("layer-row").nth(6); - const tokenTypographyTextLayerTwo = workspacePage.layers.getByTestId("layer-row").nth(3); + const plainTextLayerTwo = workspacePage.layers + .getByTestId("layer-row") + .nth(2); + const typographyTextLayerOne = workspacePage.layers + .getByTestId("layer-row") + .nth(7); + const typographyTextLayerTwo = workspacePage.layers + .getByTestId("layer-row") + .nth(4); + const tokenTypographyTextLayerOne = workspacePage.layers + .getByTestId("layer-row") + .nth(6); + const tokenTypographyTextLayerTwo = workspacePage.layers + .getByTestId("layer-row") + .nth(3); const rectangleLayer = workspacePage.layers.getByTestId("layer-row").nth(1); const elipseLayer = workspacePage.layers.getByTestId("layer-row").nth(0); - const textSection = workspacePage.rightSidebar.getByRole('region', { name: "Text section" }); + const textSection = workspacePage.rightSidebar.getByRole("region", { + name: "Text section", + }); // Select rectangle and elipse together await rectangleLayer.click(); - await elipseLayer.click({ modifiers: ['Control'] }); + await elipseLayer.click({ modifiers: ["Control"] }); await expect(textSection).not.toBeVisible(); - + // Select plain text layer await plainTextLayer.click(); await expect(textSection).toBeVisible(); - await expect(textSection.getByText("Multiple typographies")).not.toBeVisible(); + await expect( + textSection.getByText("Multiple typographies"), + ).not.toBeVisible(); // Select two plain text layer with different font family - await plainTextLayerTwo.click({ modifiers: ['Control'] }); + await plainTextLayerTwo.click({ modifiers: ["Control"] }); await expect(textSection).toBeVisible(); - await expect(textSection.getByTitle("Font family").getByText("--")).toBeVisible(); + await expect( + textSection.getByTitle("Font family").getByText("--"), + ).toBeVisible(); // Select typography text layer await typographyTextLayerOne.click(); @@ -211,48 +261,50 @@ test("Multiselection of text and typographies", async ({ page }) => { await expect(textSection.getByText("Typography one")).toBeVisible(); // Select two typography text layer with different typography - await typographyTextLayerTwo.click({ modifiers: ['Control'] }); + await typographyTextLayerTwo.click({ modifiers: ["Control"] }); await expect(textSection).toBeVisible(); await expect(textSection.getByText("Multiple typographies")).toBeVisible(); - // Select token typography text layer + // Select token typography text layer // TODO: CHANGE WHEN TOKEN TYPOGRAPHY ROW IS READY await tokenTypographyTextLayerOne.click(); await expect(textSection).toBeVisible(); - await expect(textSection.getByText('Metrophobic')).toBeVisible(); + await expect(textSection.getByText("Metrophobic")).toBeVisible(); // Select two token typography text layer with different token typography - // TODO: CHANGE WHEN TOKEN TYPOGRAPHY ROW IS READY - await tokenTypographyTextLayerTwo.click({ modifiers: ['Control'] }); + // TODO: CHANGE WHEN TOKEN TYPOGRAPHY ROW IS READY + await tokenTypographyTextLayerTwo.click({ modifiers: ["Control"] }); await expect(textSection).toBeVisible(); - await expect(textSection.getByTitle("Font family").getByText("--")).toBeVisible(); + await expect( + textSection.getByTitle("Font family").getByText("--"), + ).toBeVisible(); //Select plain text layer and typography text layer together await plainTextLayer.click(); - await typographyTextLayerOne.click({ modifiers: ['Control'] }); + await typographyTextLayerOne.click({ modifiers: ["Control"] }); await expect(textSection).toBeVisible(); await expect(textSection.getByText("Multiple typographies")).toBeVisible(); //Select plain text layer and typography text layer together on reverse order await typographyTextLayerOne.click(); - await plainTextLayer.click({ modifiers: ['Control'] }); + await plainTextLayer.click({ modifiers: ["Control"] }); await expect(textSection).toBeVisible(); await expect(textSection.getByText("Multiple typographies")).toBeVisible(); //Selen token typography text layer and typography text layer together await tokenTypographyTextLayerOne.click(); - await typographyTextLayerOne.click({ modifiers: ['Control'] }); + await typographyTextLayerOne.click({ modifiers: ["Control"] }); await expect(textSection).toBeVisible(); await expect(textSection.getByText("Multiple typographies")).toBeVisible(); //Select token typography text layer and typography text layer together on reverse order await typographyTextLayerOne.click(); - await tokenTypographyTextLayerOne.click({ modifiers: ['Control'] }); + await tokenTypographyTextLayerOne.click({ modifiers: ["Control"] }); await expect(textSection).toBeVisible(); await expect(textSection.getByText("Multiple typographies")).toBeVisible(); // Select rectangle and elipse together await rectangleLayer.click(); - await elipseLayer.click({ modifiers: ['Control'] }); + await elipseLayer.click({ modifiers: ["Control"] }); await expect(textSection).not.toBeVisible(); -}); \ No newline at end of file +}); diff --git a/frontend/playwright/ui/specs/profile-menu.spec.js b/frontend/playwright/ui/specs/profile-menu.spec.js index e86a79a826..71bdbb4199 100644 --- a/frontend/playwright/ui/specs/profile-menu.spec.js +++ b/frontend/playwright/ui/specs/profile-menu.spec.js @@ -10,12 +10,16 @@ test("Navigate to penpot changelog from profile menu", async ({ page }) => { await dashboardPage.goToDashboard(); await dashboardPage.openProfileMenu(); - await dashboardPage.clickProfileMenuItem("About Penpot"); + const aboutPenpotItem = page.getByText("About Penpot"); + await aboutPenpotItem.hover(); + + const changelogSubmenuItem = page.getByText("Penpot Changelog"); + await expect(changelogSubmenuItem).toBeVisible(); // Listen for the new page (tab) that opens when clicking "Penpot Changelog" const [newPage] = await Promise.all([ page.context().waitForEvent("page"), - dashboardPage.clickProfileMenuItem("Penpot Changelog"), + changelogSubmenuItem.click(), ]); await newPage.waitForLoadState(); diff --git a/frontend/playwright/ui/specs/workspace-modifers.spec.js b/frontend/playwright/ui/specs/workspace-modifers.spec.js index 8e5f871fd8..bbea6199f8 100644 --- a/frontend/playwright/ui/specs/workspace-modifers.spec.js +++ b/frontend/playwright/ui/specs/workspace-modifers.spec.js @@ -3,13 +3,17 @@ import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; test.beforeEach(async ({ page }) => { await WasmWorkspacePage.init(page); + await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-token-input"]); }); test("BUG 13305 - Fix resize board to fit content", async ({ page }) => { const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(); await workspacePage.mockGetFile("workspace/get-file-13305.json"); - await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-13305.json"); + await workspacePage.mockRPC( + "update-file?id=*", + "workspace/update-file-13305.json", + ); await workspacePage.goToWorkspace({ fileId: "9666e946-78e8-8111-8007-8fe5f0f454bf", @@ -17,12 +21,42 @@ test("BUG 13305 - Fix resize board to fit content", async ({ page }) => { }); await workspacePage.clickLeafLayer("Board"); - await workspacePage.rightSidebar.getByRole("button", { name: "Resize board to fit content" }).click(); + await workspacePage.rightSidebar + .getByRole("button", { name: "Resize board to fit content" }) + .click(); - await expect(workspacePage.rightSidebar.getByTitle("Width").getByRole("textbox")).toHaveValue("630"); - await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("630"); - await expect(workspacePage.rightSidebar.getByTitle("X axis").getByRole("textbox")).toHaveValue("110"); - await expect(workspacePage.rightSidebar.getByTitle("Y axis").getByRole("textbox")).toHaveValue("110"); + const measuresSection = workspacePage.rightSidebar.getByRole("region", { + name: "shape-measures-section", + }); + await expect(measuresSection).toBeVisible(); + + // Width + const widthInput = measuresSection.getByRole("textbox", { + name: "Width", + exact: true, + }); + await expect(widthInput).toHaveValue("630"); + + // Height + const heightInput = measuresSection.getByRole("textbox", { + name: "Height", + exact: true, + }); + await expect(heightInput).toHaveValue("630"); + + // X Position (using "X axis" title) + const xPosInput = measuresSection.getByRole("textbox", { + name: "X axis", + exact: true, + }); + await expect(xPosInput).toHaveValue("110"); + + // Y Position (using "Y axis" title) + const yPosInput = measuresSection.getByRole("textbox", { + name: "Y axis", + exact: true, + }); + await expect(yPosInput).toHaveValue("110"); }); test("BUG 13382 - Fix problem with flex layout", async ({ page }) => { @@ -35,7 +69,10 @@ test("BUG 13382 - Fix problem with flex layout", async ({ page }) => { "workspace/get-file-13382-fragment.json", ); - await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json"); + await workspacePage.mockRPC( + "update-file?id=*", + "workspace/update-file-empty.json", + ); await workspacePage.goToWorkspace({ fileId: "52c4e771-3853-8190-8007-9506c70e8100", @@ -47,13 +84,26 @@ test("BUG 13382 - Fix problem with flex layout", async ({ page }) => { await workspacePage.clickToggableLayer("C"); await workspacePage.clickLeafLayer("R2"); - const heightText = workspacePage.rightSidebar.getByTitle("Height").getByPlaceholder('--'); - await heightText.fill("200"); - await heightText.press("Enter"); + const measuresSection = workspacePage.rightSidebar.getByRole("region", { + name: "shape-measures-section", + }); + await expect(measuresSection).toBeVisible(); + + const heightInput = measuresSection.getByRole("textbox", { + name: "Height", + exact: true, + }); + await heightInput.fill("200"); + await heightInput.press("Enter"); await workspacePage.clickLeafLayer("B"); - await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("340"); + // Width + const widthInput = measuresSection.getByRole("textbox", { + name: "Width", + exact: true, + }); + await expect(widthInput).toHaveValue("393"); }); test("BUG 13468 - Fix problem with flex propagation", async ({ page }) => { @@ -66,7 +116,10 @@ test("BUG 13468 - Fix problem with flex propagation", async ({ page }) => { "workspace/get-file-13468-fragment.json", ); - await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json"); + await workspacePage.mockRPC( + "update-file?id=*", + "workspace/update-file-empty.json", + ); await workspacePage.goToWorkspace({ fileId: "3a4d7ec7-c391-8146-8007-9a05c41da6b9", @@ -76,10 +129,21 @@ test("BUG 13468 - Fix problem with flex propagation", async ({ page }) => { await workspacePage.clickToggableLayer("Parent"); await workspacePage.clickToggableLayer("Container"); - await workspacePage.sidebar.getByRole('button', { name: 'Show' }).click(); + await workspacePage.sidebar.getByRole("button", { name: "Show" }).click(); await workspacePage.clickLeafLayer("Container"); - await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("76"); + + const measuresSection = workspacePage.rightSidebar.getByRole("region", { + name: "shape-measures-section", + }); + await expect(measuresSection).toBeVisible(); + + const heightInput = measuresSection.getByRole("textbox", { + name: "Height", + exact: true, + }); + + await expect(heightInput).toHaveValue("76"); }); test("BUG 13272 - Fix problem with snap to pixel", async ({ page }) => { @@ -92,7 +156,10 @@ test("BUG 13272 - Fix problem with snap to pixel", async ({ page }) => { "workspace/get-file-13272-fragment.json", ); - await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json"); + await workspacePage.mockRPC( + "update-file?id=*", + "workspace/update-file-empty.json", + ); await workspacePage.goToWorkspace({ fileId: "3b9773cc-d4f1-81e1-8007-b3f8dcaba770", @@ -102,15 +169,31 @@ test("BUG 13272 - Fix problem with snap to pixel", async ({ page }) => { await workspacePage.clickToggableLayer("Group"); await workspacePage.clickLeafLayer("Group"); - await workspacePage.page.locator('g:nth-child(11) > .cursor-resize-nesw-0').hover(); + await workspacePage.page + .locator("g:nth-child(11) > .cursor-resize-nesw-0") + .hover(); await workspacePage.page.mouse.down(); await workspacePage.page.mouse.move(1200, 800); await workspacePage.page.mouse.up(); await workspacePage.clickLeafLayer("Rectangle"); - await expect(workspacePage.rightSidebar.getByTitle("Width").getByRole("textbox")).toHaveValue("197.5"); - await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("128.28"); + + const measuresSection = workspacePage.rightSidebar.getByRole("region", { + name: "shape-measures-section", + }); + await expect(measuresSection).toBeVisible(); + + const heightInput = measuresSection.getByRole("textbox", { + name: "Height", + exact: true, + }); + const widthInput = measuresSection.getByRole("textbox", { + name: "Width", + exact: true, + }); + await expect(widthInput).toHaveValue("197.5"); + await expect(heightInput).toHaveValue("128.28"); }); test("BUG 13755 - Fix problem with text change modiifers", async ({ page }) => { @@ -123,7 +206,10 @@ test("BUG 13755 - Fix problem with text change modiifers", async ({ page }) => { "workspace/get-file-13755-fragment.json", ); - await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json"); + await workspacePage.mockRPC( + "update-file?id=*", + "workspace/update-file-empty.json", + ); await workspacePage.goToWorkspace({ fileId: "7fd33337-c651-80ae-8007-c357213f876e", @@ -133,9 +219,19 @@ test("BUG 13755 - Fix problem with text change modiifers", async ({ page }) => { await workspacePage.clickToggableLayer("Board"); await workspacePage.clickLeafLayer("uno dos tres cuatro"); - await workspacePage.page.keyboard.press('Enter'); - await workspacePage.page.keyboard.type('test'); + await workspacePage.page.keyboard.press("Enter"); + await workspacePage.page.keyboard.type("test"); await workspacePage.clickToggableLayer("Board"); - await expect(workspacePage.rightSidebar.getByTitle("Width").getByRole("textbox")).toHaveValue("23"); + + const measuresSection = workspacePage.rightSidebar.getByRole("region", { + name: "shape-measures-section", + }); + await expect(measuresSection).toBeVisible(); + + const widthInput = measuresSection.getByRole("textbox", { + name: "Width", + exact: true, + }); + await expect(widthInput).toHaveValue("23"); }); diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index 3d1c4741b9..b29d3fd47b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -619,8 +619,7 @@ :value (get values :rotation)}] [:div {:class (stl/css :rotation) - :title (tr "workspace.options.rotation") - :data-testid "rotation"} + :title (tr "workspace.options.rotation")} [:span {:class (stl/css :icon)} deprecated-icon/rotation] [:> deprecated-input/numeric-input* {:no-validate true From 4ce56e96fe05212a624aa9ba99ab2cdc630e1e2f Mon Sep 17 00:00:00 2001 From: Clayton <118192227+claytonlin1110@users.noreply.github.com> Date: Mon, 4 May 2026 06:33:58 -0500 Subject: [PATCH 282/288] :bug: Fix MCP media uploads and SVG data URI image parsing (#9201) * :bug: Fix MCP media uploads and SVG data URI image parsing Signed-off-by: Clayton * :bug: Fix lint Signed-off-by: Clayton * :bug: Fix test Signed-off-by: Clayton --------- Signed-off-by: Clayton --- frontend/src/app/plugins/api.cljs | 18 +++++++++- frontend/src/app/util/webapi.cljs | 27 +++++++++------ frontend/test/frontend_tests/runner.cljs | 2 ++ .../test/frontend_tests/util_webapi_test.cljs | 34 +++++++++++++++++++ 4 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 frontend/test/frontend_tests/util_webapi_test.cljs diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs index 68526ae8a4..f7c5a324a6 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -284,7 +284,23 @@ {:file-id file-id :local? false :name name - :blobs [(js/Blob. #js [data] #js {:type mime-type})] + :blobs [(js/Blob. + #js [(cond + (instance? js/Uint8Array data) + data + + (instance? js/ArrayBuffer data) + (js/Uint8Array. data) + + (array? data) + (js/Uint8Array.from data) + + (and (some? data) (= (type data) js/Object)) + (js/Uint8Array.from (js/Object.values data)) + + :else + data)] + #js {:type mime-type})] :on-image identity :on-svg identity}) (rx/take 1) diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs index 1b3a63b97a..b877108695 100644 --- a/frontend/src/app/util/webapi.cljs +++ b/frontend/src/app/util/webapi.cljs @@ -97,17 +97,22 @@ (defn data-uri->blob [data-uri] - (let [[mtype b64-data] (str/split data-uri ";base64," 2) - mtype (subs mtype (inc (str/index-of mtype ":"))) - decoded (.atob js/window b64-data) - size (.-length ^js decoded) - content (js/Uint8Array. size)] - - (loop [i 0] - (when (< i size) - (aset content i (.charCodeAt ^js decoded i)) - (recur (inc i)))) - + (let [[meta data] (str/split data-uri "," 2) + mtype-end (or (str/index-of meta ";") (count meta)) + mtype (subs meta (inc (str/index-of meta ":")) mtype-end) + base64? (str/includes? meta ";base64") + content (if base64? + (let [decoded (.atob js/globalThis data) + size (.-length ^js decoded) + bytes (js/Uint8Array. size)] + (loop [i 0] + (when (< i size) + (aset bytes i (.charCodeAt ^js decoded i)) + (recur (inc i)))) + bytes) + ;; Data URIs can be plain/URL-encoded (e.g. ;utf8,). + ;; Encode into UTF-8 bytes before creating the Blob. + (.encode (js/TextEncoder.) (.decodeURIComponent js/globalThis data)))] (create-blob content mtype))) (defn get-current-selected-text diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index b4e6f0defc..5f9078f910 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -34,6 +34,7 @@ [frontend-tests.util-object-test] [frontend-tests.util-range-tree-test] [frontend-tests.util-simple-math-test] + [frontend-tests.util-webapi-test] [frontend-tests.worker-snap-test])) (enable-console-print!) @@ -79,4 +80,5 @@ 'frontend-tests.util-object-test 'frontend-tests.util-range-tree-test 'frontend-tests.util-simple-math-test + 'frontend-tests.util-webapi-test 'frontend-tests.worker-snap-test)) diff --git a/frontend/test/frontend_tests/util_webapi_test.cljs b/frontend/test/frontend_tests/util_webapi_test.cljs new file mode 100644 index 0000000000..1307526ffb --- /dev/null +++ b/frontend/test/frontend_tests/util_webapi_test.cljs @@ -0,0 +1,34 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns frontend-tests.util-webapi-test + (:require + [app.util.webapi :as wapi] + [cljs.test :as t :include-macros true])) + +(t/deftest data-uri->blob-supports-base64 + (t/async done + (let [blob (wapi/data-uri->blob "data:text/plain;base64,SGVsbG8=")] + (-> (.text blob) + (.then (fn [text] + (t/is (= "text/plain" (.-type blob))) + (t/is (= "Hello" text)) + (done))) + (.catch (fn [err] + (t/is false (str "unexpected error: " err)) + (done))))))) + +(t/deftest data-uri->blob-supports-utf8-data + (t/async done + (let [blob (wapi/data-uri->blob "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3C%2Fsvg%3E")] + (-> (.text blob) + (.then (fn [text] + (t/is (= "image/svg+xml" (.-type blob))) + (t/is (= "" text)) + (done))) + (.catch (fn [err] + (t/is false (str "unexpected error: " err)) + (done))))))) From 07ad152ae5a264d24fe2707c47219d3a17bc3745 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 28 Apr 2026 17:24:06 +0200 Subject: [PATCH 283/288] :bug: Fix .component() returning outermost component for nested instances The shape API method .component() used locate-component which walks to the outermost instance root via get-instance-root. For nested component instances (e.g. a button inside a card), this incorrectly returned the outer component (the card) instead of the nearest one (the button). Added locate-head-component in utils.cljs which uses get-head-shape to find the nearest component head, and updated the :component property in shape.cljs to use it. Fixes #9183 --- CHANGES.md | 1 + frontend/src/app/plugins/shape.cljs | 4 ++-- frontend/src/app/plugins/utils.cljs | 11 +++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ce033171a8..149fc49f05 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -130,6 +130,7 @@ - Fix internal error on layer prev/next sibling selection (by @jsdevninja) [Github #9003](https://github.com/penpot/penpot/issues/9003) - Fix tooltip appearing two times when nested elements [Github #9031](https://github.com/penpot/penpot/issues/9031) - Fix broken update library notification link in the UI [Github #9070](https://github.com/penpot/penpot/issues/9070) +- Fix plugin API `ShapeBase.component()` returning the outermost component instead of the immediate component in case of nested component instances [Github #9183](https://github.com/penpot/penpot/issues/9183) ## 2.15.0 (Unreleased) diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index da09c90ea8..63037e405c 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -1186,8 +1186,8 @@ (let [objects (u/locate-objects file-id page-id) shape (u/locate-shape file-id page-id id)] (when (ctn/in-any-component? objects shape) - (let [[root component] (u/locate-component objects shape)] - (lib-component-proxy plugin-id (:component-file root) (:id component)))))) + (when-let [[head component] (u/locate-head-component objects shape)] + (lib-component-proxy plugin-id (:component-file head) (:id component)))))) :detach (fn [] diff --git a/frontend/src/app/plugins/utils.cljs b/frontend/src/app/plugins/utils.cljs index 19de73fcde..81dfb6cb82 100644 --- a/frontend/src/app/plugins/utils.cljs +++ b/frontend/src/app/plugins/utils.cljs @@ -100,6 +100,17 @@ root (ctn/get-instance-root objects shape)] [root (ctf/resolve-component root file libraries {:include-deleted? true})])) +(defn locate-head-component + "Like locate-component but resolves via the nearest component head + instead of the outermost instance root." + [objects shape] + (let [state (deref st/state) + file (dsh/lookup-file state) + libraries (dsh/lookup-libraries state) + head (ctn/get-head-shape objects shape)] + (when head + [head (ctf/resolve-component head file libraries {:include-deleted? true})]))) + (defn proxy->file [proxy] (let [id (obj/get proxy "$id")] From 3431aee17711edf8f1873941e4e21cabf2866546 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Tue, 5 May 2026 11:27:48 +0200 Subject: [PATCH 284/288] :bug: Fix move org dialog must be select --- frontend/src/app/main/ui/dashboard/team.cljs | 7 +- frontend/src/app/main/ui/dashboard/team.scss | 8 ++- .../src/app/main/ui/ds/controls/combobox.cljs | 65 ++++++++++++------- frontend/translations/en.po | 5 +- frontend/translations/es.po | 5 +- 5 files changed, 63 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index ef86cef4f3..dd3bbfb2de 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -810,7 +810,7 @@ (mf/defc select-organization-modal {::mf/register modal/components ::mf/register-as :select-organization-modal} - [{:keys [organizations current-organization-id on-confirm title-key choose-key placeholder-key accept-key cancel-key]}] + [{:keys [organizations current-organization-id on-confirm title-key text-key choose-key placeholder-key accept-key cancel-key]}] (let [valid-organizations (mf/with-memo [organizations] (remove #(= (:id %) current-organization-id) organizations)) options (mf/with-memo [valid-organizations] @@ -844,12 +844,16 @@ [:button {:class (stl/css :modal-close-btn) :on-click modal/hide!} deprecated-icon/close]] + (when text-key + [:div {:class (stl/css :modal-content :modal-select-org-text)} (tr text-key)]) + [:div [:div {:class (stl/css :modal-select-org-content)} (tr choose-key)] [:> combobox* {:id "selected-id" :class (stl/css :team-member) :options options + :select-only true :default-selected (or (some-> (get-in @form [:data :selected-id]) str) "") :placeholder (tr placeholder-key) :on-change on-change}]] @@ -1450,6 +1454,7 @@ :current-organization-id (:organization-id team) :on-confirm on-add-team-to-org-confirm :title-key "dashboard.change-org-modal.title" + :text-key "dashboard.change-org-modal.text" :choose-key "dashboard.change-org-modal.choose" :placeholder-key "dashboard.change-org-modal.select" :accept-key "dashboard.change-org-modal.accept" diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss index a4e24c88b9..d68bbd26c9 100644 --- a/frontend/src/app/main/ui/dashboard/team.scss +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -880,7 +880,7 @@ } .modal-select-org-content { - @include t.use-typography("body-medium"); + @include t.use-typography("body-large"); color: var(--color-foreground-secondary); overflow: auto; @@ -895,6 +895,12 @@ height: $sz-40; } +.modal-select-org-text { + @include t.use-typography("body-large"); + + color: var(--color-foreground-secondary); +} + // ORGANIZATIONS SETTINGS .org-block-content { diff --git a/frontend/src/app/main/ui/ds/controls/combobox.cljs b/frontend/src/app/main/ui/ds/controls/combobox.cljs index c5a74811d2..14ade592a2 100644 --- a/frontend/src/app/main/ui/ds/controls/combobox.cljs +++ b/frontend/src/app/main/ui/ds/controls/combobox.cljs @@ -29,19 +29,21 @@ [:placeholder {:optional true} :string] [:disabled {:optional true} :boolean] [:default-selected {:optional true} :string] + [:select-only {:optional true} :boolean] [:on-change {:optional true} fn?] [:empty-to-end {:optional true} [:maybe :boolean]] [:has-error {:optional true} :boolean]]) (mf/defc combobox* {::mf/schema schema:combobox} - [{:keys [id options class placeholder disabled has-error default-selected max-length empty-to-end on-change] :rest props}] + [{:keys [id options class placeholder disabled has-error default-selected select-only max-length empty-to-end on-change] :rest props}] (let [;; NOTE: we use mfu/bean here for transparently handle ;; options provide as clojure data structures or javascript ;; plain objects and lists. options (if (array? options) (mfu/bean options) options) + select-only (d/nilv select-only false) empty-to-end (d/nilv empty-to-end false) is-open* (mf/use-state false) @@ -64,13 +66,15 @@ value-ref (mf/use-ref nil) dropdown-options - (mf/with-memo [options filter-id] - (->> options - (filterv (fn [option] - (let [option (str/lower (get option :label)) - filter (str/lower filter-id)] - (str/includes? option filter)))) - (not-empty))) + (mf/with-memo [options filter-id select-only] + (if select-only + (not-empty options) + (->> options + (filterv (fn [option] + (let [option (str/lower (get option :label)) + filter (str/lower filter-id)] + (str/includes? option filter)))) + (not-empty)))) set-option-ref (mf/use-fn @@ -113,7 +117,7 @@ on-blur (mf/use-fn - (mf/deps on-change options selected-id) + (mf/deps on-change options selected-id select-only) (fn [event] (dom/stop-propagation event) (let [target (dom/get-related-target event) @@ -125,9 +129,11 @@ (when-let [input-node (mf/ref-val input-ref)] (let [input-value (dom/get-input-value input-node) selected-option (d/seek #(= selected-id (get % :id)) options) - value (if (some? selected-option) + value (if select-only selected-id - input-value)] + (if (some? selected-option) + selected-id + input-value))] (on-change value)))))))) on-input-click @@ -149,10 +155,18 @@ on-input-key-down (mf/use-fn - (mf/deps is-open focused-id disabled) + (mf/deps is-open focused-id disabled select-only) (fn [event] (dom/stop-propagation event) (when-not disabled + (when (and select-only + (not (kbd/down-arrow? event)) + (not (kbd/up-arrow? event)) + (not (kbd/home? event)) + (not (kbd/enter? event)) + (not (kbd/esc? event)) + (not (kbd/tab? event))) + (dom/prevent-default event)) (let [options (mf/ref-val options-ref) len (count options) index (d/index-of-pred options #(= focused-id (get % :id))) @@ -201,15 +215,17 @@ on-input-change (mf/use-fn + (mf/deps select-only) (fn [event] (dom/stop-propagation event) - (let [value (-> event - dom/get-target - dom/get-value)] - (mf/set-ref-val! value-ref value) - (reset! selected-id* value) - (reset! filter-id* value) - (reset! focused-id* nil)))) + (when-not select-only + (let [value (-> event + dom/get-target + dom/get-value)] + (mf/set-ref-val! value-ref value) + (reset! selected-id* value) + (reset! filter-id* value) + (reset! focused-id* nil))))) selected-option (mf/with-memo [options selected-id] @@ -268,16 +284,19 @@ :role "combobox" :class (stl/css :input) :auto-complete "off" - :aria-autocomplete "both" + :aria-autocomplete (if select-only "none" "both") :aria-expanded is-open :aria-controls listbox-id :aria-activedescendant focused-id :data-testid "combobox-input" :max-length (d/nilv max-length max-input-length) :disabled disabled - :value (if (str/empty? (:id selected-option)) - (d/nilv selected-id "") - (d/nilv (:label selected-option) "")) + :read-only select-only + :value (if select-only + (d/nilv (:label selected-option) "") + (if (str/empty? (:id selected-option)) + (d/nilv selected-id "") + (d/nilv (:label selected-option) ""))) :placeholder placeholder :on-change on-input-change :on-click on-input-click diff --git a/frontend/translations/en.po b/frontend/translations/en.po index f80d4beb21..af5aad880e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1157,7 +1157,10 @@ msgid "dashboard.select-org-modal.accept" msgstr "ADD TO ORGANIZATION" msgid "dashboard.change-org-modal.title" -msgstr "CHANGE TEAM'S ORGANIZATION" +msgstr "Change team's organization" + +msgid "dashboard.change-org-modal.text" +msgstr "Projects and files will remain available to team members. The team will get the configuration from the new organization." msgid "dashboard.change-org-modal.choose" msgstr "Move to:" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 1b9a2f166f..2f3e75c289 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1161,7 +1161,10 @@ msgid "dashboard.select-org-modal.accept" msgstr "AÑADIR A UNA ORGANIZACIÓN" msgid "dashboard.change-org-modal.title" -msgstr "CAMBIAR EL EQUIPO DE ORGANIZACIÓN" +msgstr "Cambiar el equipo de organización" + +msgid "dashboard.change-org-modal.text" +msgstr "Los miembros del equipo seguirán teniendo acceso a los proyectos y ficheros. El equipo tendrá la configuración de la nueva organización." msgid "dashboard.change-org-modal.choose" msgstr "Mover a:" From 41996ed9a5af1fcc3b50f6cf3c37bb24d10f7381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Tue, 5 May 2026 12:32:35 +0200 Subject: [PATCH 285/288] :sparkles: Add nitrate subscription text --- frontend/src/app/main/ui/settings/subscription.cljs | 2 +- frontend/translations/en.po | 3 +++ frontend/translations/es.po | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index af7e9d9ddc..9e85635f3b 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -626,7 +626,7 @@ [:> plan-card* {:card-title "Business Nitrate" :card-title-icon i/character-n :price-value "$25" - :price-period "org member" + :price-period (tr "subscription.settings.organization-member-month") :benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits") :benefits ["Crea organizaciones y añade personas, que usarán Penpot con las reglas que configures." "Acceso exclusivo al Control Center" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index af5aad880e..a38b24ba03 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5291,6 +5291,9 @@ msgstr "editor per month" msgid "subscription.settings.price-organization-month" msgstr "organization per month" +msgid "subscription.settings.organization-member-month" +msgstr "org member per month" + #: src/app/main/ui/dashboard/subscription.cljs:140, src/app/main/ui/settings/subscription.cljs:102, src/app/main/ui/settings/subscription.cljs:469, src/app/main/ui/settings/subscription.cljs:538 msgid "subscription.settings.professional" msgstr "Professional" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 2f3e75c289..6a6814859c 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -5214,6 +5214,9 @@ msgstr "editor por mes" msgid "subscription.settings.price-organization-month" msgstr "organización por mes" +msgid "subscription.settings.organization-member-month" +msgstr "miembro organización por mes" + #: src/app/main/ui/dashboard/subscription.cljs:140, src/app/main/ui/settings/subscription.cljs:102, src/app/main/ui/settings/subscription.cljs:469, src/app/main/ui/settings/subscription.cljs:538 msgid "subscription.settings.professional" msgstr "Professional" From 2fbab08bdea288d84f5642e3c41207ee6e383e72 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Tue, 5 May 2026 16:50:59 +0200 Subject: [PATCH 286/288] :bug: Fix nitrate penpot-version schema --- backend/src/app/rpc/management/nitrate.clj | 17 +++++++++++++++-- .../rpc_management_nitrate_test.clj | 12 ++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 79bfdf02df..db8e6e0e06 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -64,13 +64,26 @@ ;; ---- API: get-penpot-version (def ^:private schema:get-penpot-version-result - [:map [:version ::sm/text]]) + [:map + [:version + [:map + [:full [:maybe ::sm/text]] + [:branch [:maybe ::sm/text]] + [:base [:maybe ::sm/text]] + [:main [:maybe ::sm/text]] + [:major [:maybe ::sm/text]] + [:minor [:maybe ::sm/text]] + [:patch [:maybe ::sm/text]] + [:modifier [:maybe ::sm/text]] + [:commit [:maybe ::sm/text]] + [:commit-hash [:maybe ::sm/text]]]]]) (sv/defmethod ::get-penpot-version "Get the current Penpot version" {::doc/added "2.14" ::sm/params [:map] - ::sm/result schema:get-penpot-version-result} + ::sm/result schema:get-penpot-version-result + ::rpc/auth false} [_cfg _params] {:version cf/version}) diff --git a/backend/test/backend_tests/rpc_management_nitrate_test.clj b/backend/test/backend_tests/rpc_management_nitrate_test.clj index 101dfe19a1..c5de0bf6c4 100644 --- a/backend/test/backend_tests/rpc_management_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_management_nitrate_test.clj @@ -45,11 +45,15 @@ (t/is (= :authentication-required (th/ex-code (:error out)))))) (t/deftest get-penpot-version - (let [profile (th/create-profile* 1 {:is-active true}) - out (management-command-with-nitrate! {::th/type :get-penpot-version - ::rpc/profile-id (:id profile)})] + (let [out (management-command-with-nitrate! {::th/type :get-penpot-version}) + version (-> out :result :version)] (t/is (th/success? out)) - (t/is (= cf/version (-> out :result :version))))) + (t/is (= #{:full :branch :base :main :major :minor :patch :modifier :commit :commit-hash} + (set (keys version)))) + (doseq [k [:full :branch :base :main :major :minor :patch :modifier :commit :commit-hash]] + (t/is (or (nil? (get version k)) + (string? (get version k))))) + (t/is (= cf/version version)))) (t/deftest get-teams-returns-only-owned-non-default-non-deleted (let [profile (th/create-profile* 1 {:is-active true}) From 67bb109331547564bcc7ed56f81515b27d2c65c7 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 5 May 2026 18:32:25 +0200 Subject: [PATCH 287/288] :paperclip: Fix linting issues --- backend/src/app/binfile/v1.clj | 2 +- backend/src/app/db.clj | 2 +- backend/src/app/email.clj | 6 ++--- backend/src/app/media.clj | 2 +- backend/src/app/metrics.clj | 4 ++-- backend/src/app/redis.clj | 22 +++++++++---------- backend/src/app/storage/s3.clj | 14 ++++++------ common/src/app/common/fressian.clj | 6 ++--- common/src/app/common/generic_pool.clj | 6 ++--- .../app/main/ui/workspace/viewport_wasm.cljs | 2 +- 10 files changed, 33 insertions(+), 33 deletions(-) diff --git a/backend/src/app/binfile/v1.clj b/backend/src/app/binfile/v1.clj index 75f6f36994..04b390bb99 100644 --- a/backend/src/app/binfile/v1.clj +++ b/backend/src/app/binfile/v1.clj @@ -40,8 +40,8 @@ [promesa.util :as pu] [yetti.adapter :as yt]) (:import - com.github.luben.zstd.ZstdInputStream com.github.luben.zstd.ZstdIOException + com.github.luben.zstd.ZstdInputStream com.github.luben.zstd.ZstdOutputStream java.io.DataInputStream java.io.DataOutputStream diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index c23ea07524..b00f84e3e2 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -36,11 +36,11 @@ java.sql.Connection java.sql.PreparedStatement java.sql.Savepoint + org.postgresql.PGConnection org.postgresql.geometric.PGpoint org.postgresql.jdbc.PgArray org.postgresql.largeobject.LargeObject org.postgresql.largeobject.LargeObjectManager - org.postgresql.PGConnection org.postgresql.util.PGInterval org.postgresql.util.PGobject)) diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index fe57118a58..e537bb600f 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -22,13 +22,13 @@ [cuerdas.core :as str] [integrant.core :as ig]) (:import + jakarta.mail.Message$RecipientType + jakarta.mail.Session + jakarta.mail.Transport jakarta.mail.internet.InternetAddress jakarta.mail.internet.MimeBodyPart jakarta.mail.internet.MimeMessage jakarta.mail.internet.MimeMultipart - jakarta.mail.Message$RecipientType - jakarta.mail.Session - jakarta.mail.Transport java.util.Properties)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index 9cf135213d..00b6db36d4 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -31,8 +31,8 @@ (:import clojure.lang.XMLHandler java.io.InputStream - javax.xml.parsers.SAXParserFactory javax.xml.XMLConstants + javax.xml.parsers.SAXParserFactory org.apache.commons.io.IOUtils org.im4java.core.ConvertCmd org.im4java.core.IMOperation)) diff --git a/backend/src/app/metrics.clj b/backend/src/app/metrics.clj index a1f816a304..1c7456b7ab 100644 --- a/backend/src/app/metrics.clj +++ b/backend/src/app/metrics.clj @@ -15,16 +15,16 @@ io.prometheus.client.CollectorRegistry io.prometheus.client.Counter io.prometheus.client.Counter$Child - io.prometheus.client.exporter.common.TextFormat io.prometheus.client.Gauge io.prometheus.client.Gauge$Child io.prometheus.client.Histogram io.prometheus.client.Histogram$Child - io.prometheus.client.hotspot.DefaultExports io.prometheus.client.SimpleCollector io.prometheus.client.Summary io.prometheus.client.Summary$Builder io.prometheus.client.Summary$Child + io.prometheus.client.exporter.common.TextFormat + io.prometheus.client.hotspot.DefaultExports java.io.StringWriter)) (set! *warn-on-reflection* true) diff --git a/backend/src/app/redis.clj b/backend/src/app/redis.clj index dc1bff9669..96e6b07be5 100644 --- a/backend/src/app/redis.clj +++ b/backend/src/app/redis.clj @@ -24,28 +24,28 @@ [integrant.core :as ig]) (:import clojure.lang.MapEntry - io.lettuce.core.api.StatefulRedisConnection - io.lettuce.core.api.sync.RedisCommands - io.lettuce.core.api.sync.RedisScriptingCommands - io.lettuce.core.codec.RedisCodec - io.lettuce.core.codec.StringCodec io.lettuce.core.KeyValue - io.lettuce.core.pubsub.api.sync.RedisPubSubCommands - io.lettuce.core.pubsub.RedisPubSubListener - io.lettuce.core.pubsub.StatefulRedisPubSubConnection io.lettuce.core.RedisClient io.lettuce.core.RedisCommandInterruptedException io.lettuce.core.RedisCommandTimeoutException io.lettuce.core.RedisException io.lettuce.core.RedisURI - io.lettuce.core.resource.ClientResources - io.lettuce.core.resource.DefaultClientResources io.lettuce.core.ScriptOutputType io.lettuce.core.SetArgs + io.lettuce.core.api.StatefulRedisConnection + io.lettuce.core.api.sync.RedisCommands + io.lettuce.core.api.sync.RedisScriptingCommands + io.lettuce.core.codec.RedisCodec + io.lettuce.core.codec.StringCodec + io.lettuce.core.pubsub.RedisPubSubListener + io.lettuce.core.pubsub.StatefulRedisPubSubConnection + io.lettuce.core.pubsub.api.sync.RedisPubSubCommands + io.lettuce.core.resource.ClientResources + io.lettuce.core.resource.DefaultClientResources io.netty.channel.nio.NioEventLoopGroup - io.netty.util.concurrent.EventExecutorGroup io.netty.util.HashedWheelTimer io.netty.util.Timer + io.netty.util.concurrent.EventExecutorGroup java.lang.AutoCloseable java.time.Duration)) diff --git a/backend/src/app/storage/s3.clj b/backend/src/app/storage/s3.clj index 9322de70e6..ef56e8a9b4 100644 --- a/backend/src/app/storage/s3.clj +++ b/backend/src/app/storage/s3.clj @@ -30,18 +30,21 @@ java.nio.file.Path java.time.Duration java.util.Collection - java.util.concurrent.atomic.AtomicLong java.util.Optional + java.util.concurrent.atomic.AtomicLong org.reactivestreams.Subscriber software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider + software.amazon.awssdk.core.ResponseBytes software.amazon.awssdk.core.async.AsyncRequestBody software.amazon.awssdk.core.async.AsyncResponseTransformer software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody software.amazon.awssdk.core.client.config.ClientAsyncConfiguration - software.amazon.awssdk.core.ResponseBytes software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup software.amazon.awssdk.regions.Region + software.amazon.awssdk.services.s3.S3AsyncClient + software.amazon.awssdk.services.s3.S3AsyncClientBuilder + software.amazon.awssdk.services.s3.S3Configuration software.amazon.awssdk.services.s3.model.Delete software.amazon.awssdk.services.s3.model.DeleteObjectRequest software.amazon.awssdk.services.s3.model.DeleteObjectsRequest @@ -51,12 +54,9 @@ software.amazon.awssdk.services.s3.model.ObjectIdentifier software.amazon.awssdk.services.s3.model.PutObjectRequest software.amazon.awssdk.services.s3.model.S3Error - software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest - software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest software.amazon.awssdk.services.s3.presigner.S3Presigner - software.amazon.awssdk.services.s3.S3AsyncClient - software.amazon.awssdk.services.s3.S3AsyncClientBuilder - software.amazon.awssdk.services.s3.S3Configuration)) + software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest + software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest)) (def ^:private max-retries "A maximum number of retries on internal operations" diff --git a/common/src/app/common/fressian.clj b/common/src/app/common/fressian.clj index f005df743b..98c8b1b323 100644 --- a/common/src/app/common/fressian.clj +++ b/common/src/app/common/fressian.clj @@ -17,11 +17,11 @@ java.util.List linked.map.LinkedMap linked.set.LinkedSet - org.fressian.handlers.ReadHandler - org.fressian.handlers.WriteHandler org.fressian.Reader org.fressian.StreamingWriter - org.fressian.Writer)) + org.fressian.Writer + org.fressian.handlers.ReadHandler + org.fressian.handlers.WriteHandler)) (set! *warn-on-reflection* true) diff --git a/common/src/app/common/generic_pool.clj b/common/src/app/common/generic_pool.clj index 950506dc17..bccf0b06ec 100644 --- a/common/src/app/common/generic_pool.clj +++ b/common/src/app/common/generic_pool.clj @@ -8,11 +8,11 @@ (:refer-clojure :exclude [get]) (:import java.lang.AutoCloseable - org.apache.commons.pool2.impl.DefaultPooledObject - org.apache.commons.pool2.impl.SoftReferenceObjectPool org.apache.commons.pool2.ObjectPool org.apache.commons.pool2.PooledObject - org.apache.commons.pool2.PooledObjectFactory)) + org.apache.commons.pool2.PooledObjectFactory + org.apache.commons.pool2.impl.DefaultPooledObject + org.apache.commons.pool2.impl.SoftReferenceObjectPool)) (defn pool? [o] diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 41ec1ad063..837c72f598 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -82,7 +82,7 @@ [{:keys [selected wglobal layout file page palete-size]}] (let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check ;; that the new parameter is sent - + {:keys [edit-path panning selrect From dc5f02a11ccfd3aac197369dfd1538cfb2c40f8f Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 5 May 2026 18:49:08 +0200 Subject: [PATCH 288/288] :paperclip: Fix linting issues --- backend/src/app/binfile/v1.clj | 2 +- backend/src/app/db.clj | 2 +- backend/src/app/email.clj | 6 +++--- backend/src/app/media.clj | 2 +- backend/src/app/metrics.clj | 4 ++-- backend/src/app/redis.clj | 20 ++++++++++---------- backend/src/app/storage/s3.clj | 14 +++++++------- common/src/app/common/fressian.clj | 6 +++--- common/src/app/common/generic_pool.clj | 6 +++--- 9 files changed, 31 insertions(+), 31 deletions(-) diff --git a/backend/src/app/binfile/v1.clj b/backend/src/app/binfile/v1.clj index 04b390bb99..75f6f36994 100644 --- a/backend/src/app/binfile/v1.clj +++ b/backend/src/app/binfile/v1.clj @@ -40,8 +40,8 @@ [promesa.util :as pu] [yetti.adapter :as yt]) (:import - com.github.luben.zstd.ZstdIOException com.github.luben.zstd.ZstdInputStream + com.github.luben.zstd.ZstdIOException com.github.luben.zstd.ZstdOutputStream java.io.DataInputStream java.io.DataOutputStream diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index b00f84e3e2..c23ea07524 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -36,11 +36,11 @@ java.sql.Connection java.sql.PreparedStatement java.sql.Savepoint - org.postgresql.PGConnection org.postgresql.geometric.PGpoint org.postgresql.jdbc.PgArray org.postgresql.largeobject.LargeObject org.postgresql.largeobject.LargeObjectManager + org.postgresql.PGConnection org.postgresql.util.PGInterval org.postgresql.util.PGobject)) diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index e537bb600f..fe57118a58 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -22,13 +22,13 @@ [cuerdas.core :as str] [integrant.core :as ig]) (:import - jakarta.mail.Message$RecipientType - jakarta.mail.Session - jakarta.mail.Transport jakarta.mail.internet.InternetAddress jakarta.mail.internet.MimeBodyPart jakarta.mail.internet.MimeMessage jakarta.mail.internet.MimeMultipart + jakarta.mail.Message$RecipientType + jakarta.mail.Session + jakarta.mail.Transport java.util.Properties)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index 00b6db36d4..9cf135213d 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -31,8 +31,8 @@ (:import clojure.lang.XMLHandler java.io.InputStream - javax.xml.XMLConstants javax.xml.parsers.SAXParserFactory + javax.xml.XMLConstants org.apache.commons.io.IOUtils org.im4java.core.ConvertCmd org.im4java.core.IMOperation)) diff --git a/backend/src/app/metrics.clj b/backend/src/app/metrics.clj index 1c7456b7ab..a1f816a304 100644 --- a/backend/src/app/metrics.clj +++ b/backend/src/app/metrics.clj @@ -15,16 +15,16 @@ io.prometheus.client.CollectorRegistry io.prometheus.client.Counter io.prometheus.client.Counter$Child + io.prometheus.client.exporter.common.TextFormat io.prometheus.client.Gauge io.prometheus.client.Gauge$Child io.prometheus.client.Histogram io.prometheus.client.Histogram$Child + io.prometheus.client.hotspot.DefaultExports io.prometheus.client.SimpleCollector io.prometheus.client.Summary io.prometheus.client.Summary$Builder io.prometheus.client.Summary$Child - io.prometheus.client.exporter.common.TextFormat - io.prometheus.client.hotspot.DefaultExports java.io.StringWriter)) (set! *warn-on-reflection* true) diff --git a/backend/src/app/redis.clj b/backend/src/app/redis.clj index 96e6b07be5..dc1bff9669 100644 --- a/backend/src/app/redis.clj +++ b/backend/src/app/redis.clj @@ -24,28 +24,28 @@ [integrant.core :as ig]) (:import clojure.lang.MapEntry - io.lettuce.core.KeyValue - io.lettuce.core.RedisClient - io.lettuce.core.RedisCommandInterruptedException - io.lettuce.core.RedisCommandTimeoutException - io.lettuce.core.RedisException - io.lettuce.core.RedisURI - io.lettuce.core.ScriptOutputType - io.lettuce.core.SetArgs io.lettuce.core.api.StatefulRedisConnection io.lettuce.core.api.sync.RedisCommands io.lettuce.core.api.sync.RedisScriptingCommands io.lettuce.core.codec.RedisCodec io.lettuce.core.codec.StringCodec + io.lettuce.core.KeyValue + io.lettuce.core.pubsub.api.sync.RedisPubSubCommands io.lettuce.core.pubsub.RedisPubSubListener io.lettuce.core.pubsub.StatefulRedisPubSubConnection - io.lettuce.core.pubsub.api.sync.RedisPubSubCommands + io.lettuce.core.RedisClient + io.lettuce.core.RedisCommandInterruptedException + io.lettuce.core.RedisCommandTimeoutException + io.lettuce.core.RedisException + io.lettuce.core.RedisURI io.lettuce.core.resource.ClientResources io.lettuce.core.resource.DefaultClientResources + io.lettuce.core.ScriptOutputType + io.lettuce.core.SetArgs io.netty.channel.nio.NioEventLoopGroup + io.netty.util.concurrent.EventExecutorGroup io.netty.util.HashedWheelTimer io.netty.util.Timer - io.netty.util.concurrent.EventExecutorGroup java.lang.AutoCloseable java.time.Duration)) diff --git a/backend/src/app/storage/s3.clj b/backend/src/app/storage/s3.clj index ef56e8a9b4..9322de70e6 100644 --- a/backend/src/app/storage/s3.clj +++ b/backend/src/app/storage/s3.clj @@ -30,21 +30,18 @@ java.nio.file.Path java.time.Duration java.util.Collection - java.util.Optional java.util.concurrent.atomic.AtomicLong + java.util.Optional org.reactivestreams.Subscriber software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider - software.amazon.awssdk.core.ResponseBytes software.amazon.awssdk.core.async.AsyncRequestBody software.amazon.awssdk.core.async.AsyncResponseTransformer software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody software.amazon.awssdk.core.client.config.ClientAsyncConfiguration + software.amazon.awssdk.core.ResponseBytes software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup software.amazon.awssdk.regions.Region - software.amazon.awssdk.services.s3.S3AsyncClient - software.amazon.awssdk.services.s3.S3AsyncClientBuilder - software.amazon.awssdk.services.s3.S3Configuration software.amazon.awssdk.services.s3.model.Delete software.amazon.awssdk.services.s3.model.DeleteObjectRequest software.amazon.awssdk.services.s3.model.DeleteObjectsRequest @@ -54,9 +51,12 @@ software.amazon.awssdk.services.s3.model.ObjectIdentifier software.amazon.awssdk.services.s3.model.PutObjectRequest software.amazon.awssdk.services.s3.model.S3Error - software.amazon.awssdk.services.s3.presigner.S3Presigner software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest - software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest)) + software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest + software.amazon.awssdk.services.s3.presigner.S3Presigner + software.amazon.awssdk.services.s3.S3AsyncClient + software.amazon.awssdk.services.s3.S3AsyncClientBuilder + software.amazon.awssdk.services.s3.S3Configuration)) (def ^:private max-retries "A maximum number of retries on internal operations" diff --git a/common/src/app/common/fressian.clj b/common/src/app/common/fressian.clj index 98c8b1b323..f005df743b 100644 --- a/common/src/app/common/fressian.clj +++ b/common/src/app/common/fressian.clj @@ -17,11 +17,11 @@ java.util.List linked.map.LinkedMap linked.set.LinkedSet + org.fressian.handlers.ReadHandler + org.fressian.handlers.WriteHandler org.fressian.Reader org.fressian.StreamingWriter - org.fressian.Writer - org.fressian.handlers.ReadHandler - org.fressian.handlers.WriteHandler)) + org.fressian.Writer)) (set! *warn-on-reflection* true) diff --git a/common/src/app/common/generic_pool.clj b/common/src/app/common/generic_pool.clj index bccf0b06ec..950506dc17 100644 --- a/common/src/app/common/generic_pool.clj +++ b/common/src/app/common/generic_pool.clj @@ -8,11 +8,11 @@ (:refer-clojure :exclude [get]) (:import java.lang.AutoCloseable + org.apache.commons.pool2.impl.DefaultPooledObject + org.apache.commons.pool2.impl.SoftReferenceObjectPool org.apache.commons.pool2.ObjectPool org.apache.commons.pool2.PooledObject - org.apache.commons.pool2.PooledObjectFactory - org.apache.commons.pool2.impl.DefaultPooledObject - org.apache.commons.pool2.impl.SoftReferenceObjectPool)) + org.apache.commons.pool2.PooledObjectFactory)) (defn pool? [o]