From 0ff5574b127ff32a31dce8dd998dd931f4e044a5 Mon Sep 17 00:00:00 2001 From: Dalai Felinto Date: Tue, 17 Feb 2026 14:48:07 +0100 Subject: [PATCH] :sparkles: Add the ability to import tokens from Linked Library Add the option to import tokens from a linked library. I know there are plans to link the tokens in together with the library. Once this happens this patch can be reverted. Until then it helps a lot to use a design system that relies on themes. Before that someones would need to: * Download the design system / add to their team. * Open the file, download the tokens. For every new file: * Link the Design System library. * Import the tokens file. With this patch all you need to get started is to download the design system and add to your team. From their importing the links is done on the same pop-up that is used to import the tokens. --- Technical considerations: I try adding this as a dialog that is called once the library is imported. I ran into a few issues though: * To find whether the library has tokens (and thus show the dialog) I would need to extend library summary to include tokens. * I couldn't find a reliable way to import the tokens after importing the library without resorting to a timer :/ I'm sure both of those hurdles are doable, I just wasted enough time trying it to the point I decided on a different approach. Signed-off-by: Dalai Felinto :paperclip: Fix minor issues and linter reports :paperclip: Reuse translations --- CHANGES.md | 1 + common/src/app/common/flags.cljc | 6 +- .../src/app/main/ui/workspace/libraries.cljs | 53 +++++++++-- .../src/app/main/ui/workspace/libraries.scss | 5 + .../workspace/tokens/import_from_library.cljs | 92 +++++++++++++++++++ .../workspace/tokens/import_from_library.scss | 70 ++++++++++++++ frontend/translations/en.po | 14 +++ 7 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/tokens/import_from_library.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/import_from_library.scss diff --git a/CHANGES.md b/CHANGES.md index 6a4833f240..626f472d7a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ - Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248) - 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) +- Import Tokens from linked library [Github #8391](https://github.com/penpot/penpot/pull/8391) ### :bug: Bugs fixed diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index 64cb7f9d68..6d7d49a98d 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -119,12 +119,13 @@ :strict-session-cookies :telemetry :terms-and-privacy-checkbox - ;; Only for developtment. :tiered-file-data-storage :token-base-font-size :token-color :token-shadow :token-tokenscript + :token-import-from-library + ;; Only for developtment. :transit-readable-response :user-feedback ;; TODO: remove this flag. @@ -180,7 +181,8 @@ :enable-token-color :enable-token-shadow :enable-inspect-styles - :enable-feature-fdata-objects-map]) + :enable-feature-fdata-objects-map + :enable-token-import-from-library]) (defn parse [& flags] diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index 18fd87e7c8..99d0e9c3de 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -13,8 +13,10 @@ [app.common.types.components-list :as ctkl] [app.common.types.file :as ctf] [app.common.types.library :as ctl] + [app.common.types.tokens-lib :as ctob] [app.common.types.typographies-list :as ctyl] [app.common.uuid :as uuid] + [app.config :as cf] [app.main.data.dashboard :as dd] [app.main.data.modal :as modal] [app.main.data.notifications :as ntf] @@ -36,6 +38,7 @@ [app.main.ui.ds.product.empty-state :refer [empty-state*]] [app.main.ui.hooks :as h] [app.main.ui.icons :as deprecated-icon] + [app.main.ui.workspace.tokens.import-from-library] [app.util.color :as uc] [app.util.dom :as dom] [app.util.i18n :refer [c tr]] @@ -180,6 +183,12 @@ [summary] (boolean (:is-empty summary))) +(defn- has-tokens? + "Check if library has tokens to be imported" + [{:keys [data]}] + (when-let [tokens-lib (get data :tokens-lib)] + (not (ctob/empty-lib? tokens-lib)))) + (mf/defc libraries-tab* {::mf/props :obj ::mf/private true} @@ -230,14 +239,18 @@ (keep library-names)))) (sort-by (comp str/lower :name)))) - linked-libraries-ids (mf/with-memo [linked-libraries] - (into #{} (map :id) linked-libraries)) + linked-libraries-ids + (mf/with-memo [linked-libraries] + (into #{} d/xf:map-id linked-libraries)) + importing* + (mf/use-state nil) - importing* (mf/use-state nil) - sample-libraries [{:id "penpot-design-system", :name "Design system example"} - {:id "wireframing-kit", :name "Wireframe library"} - {:id "whiteboarding-kit", :name "Whiteboarding Kit"}] + sample-libraries + (mf/with-memo [] + [{:id "penpot-design-system", :name "Design system example"} + {:id "wireframing-kit", :name "Wireframe library"} + {:id "whiteboarding-kit", :name "Whiteboarding Kit"}]) change-search-term @@ -267,6 +280,17 @@ (st/emit! (dwl/unlink-file-from-library file-id library-id) (dwl/sync-file file-id library-id))))) + import-tokens + (mf/use-fn + (mf/deps file-id) + (fn [event] + (let [library-id (some-> (dom/get-current-target event) + (dom/get-data "library-id") + (uuid/parse))] + (st/emit! (modal/show + :tokens/import-from-library {:file-id file-id + :library-id library-id}))))) + on-delete-accept (mf/use-fn (mf/deps file-id) @@ -332,8 +356,12 @@ :on-click publish}])] (for [{:keys [id name data connected-to connected-to-names] :as library} linked-libraries] - (let [disabled? (some #(contains? linked-libraries-ids %) connected-to)] - [:div {:class (stl/css :section-list-item) + (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)) :key (dm/str id) :data-testid "library-item"} [:div {:class (stl/css :item-content)} @@ -348,6 +376,15 @@ [:span {:class (stl/css :connected-to-values)} (str/join ", " connected-to-names)] [:span ")"]])])]] + (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 diff --git a/frontend/src/app/main/ui/workspace/libraries.scss b/frontend/src/app/main/ui/workspace/libraries.scss index 9776c08bad..978c2cbcba 100644 --- a/frontend/src/app/main/ui/workspace/libraries.scss +++ b/frontend/src/app/main/ui/workspace/libraries.scss @@ -116,6 +116,11 @@ border-radius: $br-8; } +.section-list-item-double-icon { + @extend .section-list-item; + grid-template-columns: 1fr auto auto; +} + .item-content { height: fit-content; } diff --git a/frontend/src/app/main/ui/workspace/tokens/import_from_library.cljs b/frontend/src/app/main/ui/workspace/tokens/import_from_library.cljs new file mode 100644 index 0000000000..4481e1306d --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/import_from_library.cljs @@ -0,0 +1,92 @@ +;; 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.tokens.import-from-library + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data.macros :as dm] + [app.main.data.modal :as modal] + [app.main.data.workspace.tokens.library-edit :as dwtl] + [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 :as t] + [app.main.ui.ds.foundations.typography.heading :refer [heading*]] + [app.main.ui.ds.foundations.typography.text :refer [text*]] + [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] + [app.util.i18n :refer [tr]] + [okulary.core :as l] + [rumext.v2 :as mf])) + + +(mf/defc import-modal-library* + {::mf/register modal/components + ::mf/register-as :tokens/import-from-library} + [all-props] + (let [{:keys [file-id library-id]} + (js->clj all-props :keywordize-keys true) + + library-file-ref (mf/with-memo [library-id] + (l/derived (fn [state] + (dm/get-in state [:files library-id :data])) + st/state)) + library-data (mf/deref library-file-ref) + + show-libraries-dialog + (mf/use-fn + (mf/deps file-id) + (fn [] + (modal/hide!) + (modal/show! :libraries-dialog {:file-id file-id}))) + + cancel + (mf/use-fn + (fn [] + (show-libraries-dialog))) + + import + (mf/use-fn + (mf/deps file-id library-id library-data) + (fn [] + (let [tokens-lib (:tokens-lib library-data)] + (st/emit! (dwtl/import-tokens-lib tokens-lib))) + (show-libraries-dialog)))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog)} + [:> icon-button* {:class (stl/css :close-btn) + :on-click cancel + :aria-label (tr "labels.close") + :variant "ghost" + :icon i/close}] + + [:div {:class (stl/css :modal-header)} + [:> heading* {:level 2 + :id "modal-title" + :typography "headline-large" + :class (stl/css :modal-title)} + (tr "modals.import-library-tokens.title")]] + + [:div {:class (stl/css :modal-content)} + [:> text* {:as "p" :typography t/body-medium} (tr "modals.import-library-tokens.description")]] + + [:> context-notification* {:type :context + :appearance "neutral" + :level "default" + :is-html true} + (tr "workspace.tokens.import-warning")] + + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:> button* {:on-click cancel + :type "button" + :variant "secondary"} + (tr "labels.cancel")] + [:> button* {:on-click import + :type "button" + :variant "primary"} + (tr "modals.import-library-tokens.import")]]]]])) 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 new file mode 100644 index 0000000000..d1394861db --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/import_from_library.scss @@ -0,0 +1,70 @@ +// 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/_sizes.scss" as *; + +.close-btn { + position: absolute; + inset-block-start: var(--sp-s); + inset-inline-end: var(--sp-s); +} + +.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); +} + +.modal-dialog { + @extend .modal-container-base; + inline-size: 100%; + max-inline-size: 32rem; + max-block-size: unset; + user-select: none; + position: relative; +} + +.modal-header { + margin-block-end: var(--sp-xxl); + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-title { + @include t.use-typography("headline-medium"); + color: var(--modal-title-foreground-color); + word-break: break-word; +} + +.modal-content { + @include t.use-typography("body-large"); + color: var(--modal-text-foreground-color); +} + +.modal-footer { + margin-block-start: var(--sp-xxl); + gap: var(--sp-s); +} + +.action-buttons { + @extend .modal-action-btns; + gap: var(--sp-s); +} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e2f0073eb5..ab676f7ce6 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1153,6 +1153,20 @@ msgstr "Type to search results" msgid "dashboard.unpublish-shared" msgstr "Unpublish Library" +#:src/app/main/ui/workspace/tokens/import_from_library.cljs +msgid "modals.import-library-tokens.title" +msgstr "Import tokens from library?" + +#:src/app/main/ui/workspace/tokens/import_from_library.cljs +msgid "modals.import-library-tokens.description" +msgstr "" +"The library has tokens and themes which " +"are likely used by its components." + +#:src/app/main/ui/workspace/tokens/import_from_library.cljs +msgid "modals.import-library-tokens.import" +msgstr "Import tokens" + #: src/app/main/ui/settings/options.cljs:74 msgid "dashboard.update-settings" msgstr "Update settings"