diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index a43ba2d887..ce45edd485 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -264,6 +264,7 @@ ;; 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]] + [:background-token {:optional true} [:maybe cto/schema:token-name]] [:name {:optional true} :string] ;; Pixel grid display controls — nil removes the per-page override ;; and falls back to the default hardcoded grid color/opacity. @@ -878,6 +879,7 @@ (fn [page] (let [name (get params :name) bg (get params :background :not-found) + bg-token (get params :background-token :not-found) grid-color (get params :pixel-grid-color :not-found) grid-op (get params :pixel-grid-opacity :not-found)] (cond-> page @@ -890,6 +892,12 @@ (nil? bg) (dissoc :background) + (string? bg-token) + (assoc :background-token bg-token) + + (and (not= bg-token :not-found) (nil? bg-token)) + (dissoc :background-token) + (string? grid-color) (assoc :pixel-grid-color grid-color) diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index b138c48b41..1e71d05b4d 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -242,7 +242,7 @@ (let [page (::page (meta changes))] (mod-page changes page options))) - ([changes page {:keys [name background pixel-grid-color pixel-grid-opacity]}] + ([changes page {:keys [name background pixel-grid-color pixel-grid-opacity] :as options}] (let [change {:type :mod-page :id (:id page)} redo (cond-> change (some? name) @@ -251,6 +251,9 @@ (some? background) (assoc :background background) + (contains? options :background-token) + (assoc :background-token (:background-token options)) + (some? pixel-grid-color) (assoc :pixel-grid-color pixel-grid-color) @@ -264,6 +267,9 @@ (some? background) (assoc :background (:background page)) + (contains? options :background-token) + (assoc :background-token (:background-token page)) + (some? pixel-grid-color) (assoc :pixel-grid-color (:pixel-grid-color page)) diff --git a/common/src/app/common/types/page.cljc b/common/src/app/common/types/page.cljc index e7c4e12efd..7b0e738663 100644 --- a/common/src/app/common/types/page.cljc +++ b/common/src/app/common/types/page.cljc @@ -14,6 +14,7 @@ [app.common.types.grid :as ctg] [app.common.types.plugins :as ctpg] [app.common.types.shape :as cts] + [app.common.types.token :as cto] [app.common.uuid :as uuid] [cuerdas.core :as str])) @@ -60,6 +61,8 @@ [:guides {:optional true} schema:guides] [:plugin-data {:optional true} ctpg/schema:plugin-data] [:background {:optional true} ctc/schema:hex-color] + ;; Name of the color token applied to the background, if any. + [:background-token {:optional true} [:maybe cto/schema:token-name]] ;; 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] diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 2ae1adc1aa..fd7a96e55f 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -605,6 +605,7 @@ :dimensions [:dimensions] :fill [:color] :stroke-color [:color] + :canvas [:color] :typography [:typography] :number [:number] :sizing [:sizing :dimensions] diff --git a/common/test/common_tests/files_changes_test.cljc b/common/test/common_tests/files_changes_test.cljc index 416b003224..935ff092fb 100644 --- a/common/test/common_tests/files_changes_test.cljc +++ b/common/test/common_tests/files_changes_test.cljc @@ -331,6 +331,32 @@ res (ch/process-changes data [chg])] (t/is (= res data)))))) +(t/deftest mod-page + (let [file-id (uuid/custom 2 2) + page-id (uuid/custom 1 1) + data (make-file-data file-id page-id)] + + (t/testing "sets background and background-token together" + (let [chg {:type :mod-page + :id page-id + :background "#ffffff" + :background-token "brand.bg"} + res (ch/process-changes data [chg]) + page (get-in res [:pages-index page-id])] + (t/is (= "#ffffff" (:background page))) + (t/is (= "brand.bg" (:background-token page))))) + + (t/testing "clears background-token while leaving background untouched" + (let [data' (-> data + (assoc-in [:pages-index page-id :background] "#ffffff") + (assoc-in [:pages-index page-id :background-token] "brand.bg")) + chg {:type :mod-page + :id page-id + :background-token nil} + res (ch/process-changes data' [chg]) + page (get-in res [:pages-index page-id])] + (t/is (= "#ffffff" (:background page))) + (t/is (not (contains? page :background-token))))))) (t/deftest del-obj (let [file-id (uuid/custom 2 2) diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js index 5b9ae462d0..d4b48a54d4 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -40,6 +40,32 @@ test.describe("Tokens: Apply token", () => { ).toBeVisible(); }); + test("User applies color token to the canvas background", async ({ + page, + }) => { + await setupTokensFileRender(page); + + // No shape is selected, so the right sidebar shows the page/canvas + // options by default. + const canvasSection = page.getByRole("region", { + name: "Canvas background section", + }); + await expect(canvasSection).toBeVisible(); + + // Open the color picker popover from the canvas background swatch. + await canvasSection.getByRole("button").first().click(); + + const colorPicker = page.getByTestId("colorpicker"); + await expect(colorPicker).toBeVisible(); + + await colorPicker.getByTestId('opt-token-color').click(); + await colorPicker.getByRole("button", { name: "black" }).click(); + + // The applied token badge replaces the plain color swatch/input in + // the canvas background row. + await expect(canvasSection.getByText("black")).toBeVisible(); + }); + test("User applies border-radius token to a shape from sidebar", async ({ page, }) => { diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 9efc65155a..866d5adafc 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1372,7 +1372,41 @@ page (dsh/lookup-page state page-id) changes (-> (pcb/empty-changes it) (pcb/with-page page) - (pcb/mod-page {:background (:color color)}))] + (pcb/mod-page {:background (:color color) + :background-token nil}))] + (rx/of (dch/commit-changes changes))))))) + +(defn apply-canvas-color-token + "Sets the canvas background to the resolved value of `token` and + remembers it as the applied token, so the background badge shows it." + ([color token-name] + (apply-canvas-color-token nil color token-name)) + ([page-id color token-name] + (ptk/reify ::apply-canvas-color-token + 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 {:background color + :background-token token-name}))] + (rx/of (dch/commit-changes changes))))))) + +(defn detach-canvas-color-token + "Removes the applied-token link from the canvas background, keeping + the current background color as a plain value." + ([] + (detach-canvas-color-token nil)) + ([page-id] + (ptk/reify ::detach-canvas-color-token + 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 {:background-token nil}))] (rx/of (dch/commit-changes changes))))))) (defn change-pixel-grid-color diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index 8cfc39c798..97c5ecd01d 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -376,7 +376,8 @@ :icon i/hsva :id "hsva"}]) - show-tokens? (contains? #{:fill :stroke-color :color-selection} color-origin)] + show-tokens? (or (contains? #{:fill :stroke-color :color-selection} color-origin) + (and (= color-origin :canvas) (seq combined-tokens)))] ;; Initialize colorpicker state (mf/with-effect [] diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs index 8fcf108bb7..e08eda1d10 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs @@ -139,10 +139,10 @@ on-token-pill-click (mf/use-fn - (mf/deps selected-shapes) + (mf/deps selected-shapes color-origin) (fn [event token] (dom/stop-propagation event) - (when (seq selected-shapes) + (when (or (= color-origin :canvas) (seq selected-shapes)) (on-token-change event token)))) create-token-on-set 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 91d0c4f0fb..edc2aa5c9d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs @@ -11,6 +11,7 @@ [app.common.data :as d] [app.common.types.color :as clr] [app.main.data.workspace :as dw] + [app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.store :as st] @@ -24,6 +25,10 @@ (-> (l/key :background) (l/derived refs/workspace-page))) +(def ^:private ref:background-token + (-> (l/key :background-token) + (l/derived refs/workspace-page))) + (def ^:private ref:pixel-grid-color (-> (l/key :pixel-grid-color) (l/derived refs/workspace-page))) @@ -36,6 +41,7 @@ {::mf/wrap [mf/memo]} [] (let [background (mf/deref ref:background-color) + background-token (mf/deref ref:background-token) grid-color (mf/deref ref:pixel-grid-color) grid-alpha (mf/deref ref:pixel-grid-opacity) @@ -43,6 +49,17 @@ 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-token-change + (mf/use-fn + (fn [_event token] + (when-let [color (dwta/value->color (:resolved-value token))] + (st/emit! (dw/apply-canvas-color-token (:color color) (:name token)))))) + + on-detach-token + (mf/use-fn + (fn [_token-name] + (st/emit! (dw/detach-canvas-color-token)))) + on-grid-change (mf/use-fn #(st/emit! (dw/change-pixel-grid-color %))) @@ -54,7 +71,8 @@ {:color (d/nilv grid-color clr/default-pixel-grid-color) :opacity (d/nilv grid-alpha clr/default-pixel-grid-opacity)})] - [:* [:div {:class (stl/css :element-set)} + [:* [:section {:class (stl/css :element-set) + :aria-label (tr "workspace.options.canvas-background-section")} [:div {:class (stl/css :element-title)} [:> title-bar* {:collapsable false :title (tr "workspace.options.canvas-background") @@ -68,6 +86,9 @@ :title (tr "workspace.options.canvas-background") :color color :on-change on-change + :on-token-change on-token-change + :on-detach-token on-detach-token + :applied-token background-token :origin :canvas :on-open on-open :on-close on-close}]]] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e44efe4093..37be3170c1 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -6955,6 +6955,10 @@ msgstr "Cancel" msgid "workspace.options.canvas-background" msgstr "Canvas background" +#: src/app/main/ui/workspace/sidebar/options/page.cljs:75 +msgid "workspace.options.canvas-background-section" +msgstr "Canvas background section" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:722 msgid "workspace.options.clip-content" msgstr "Clip content" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index ec04834dfb..9a66b929df 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -7021,6 +7021,10 @@ msgstr "Cancelar" msgid "workspace.options.canvas-background" msgstr "Color de fondo" +#: src/app/main/ui/workspace/sidebar/options/page.cljs:75 +msgid "workspace.options.canvas-background-section" +msgstr "Sección de fondo de lienzo" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:722 msgid "workspace.options.clip-content" msgstr "Truncar contenido"