Compare commits

..

4 Commits

Author SHA1 Message Date
Eva Marco
9e8e4909db 🎉 Add test 2026-01-27 11:56:43 +01:00
Eva Marco
64a6e51997 ♻️ Replace margin inputs 2026-01-27 08:56:37 +01:00
Eva Marco
f6dfac6e1f Add test 2026-01-27 08:56:37 +01:00
Eva Marco
4c823e7c74 Replace opacity numeric input 2026-01-27 08:56:37 +01:00
52 changed files with 1324 additions and 1323 deletions

3
.gitignore vendored
View File

@@ -21,7 +21,6 @@
.rebel_readline_history
.repl
.shadow-cljs
.pnpm-store/
/*.jpg
/*.md
/*.png
@@ -45,7 +44,6 @@
/backend/resources/public/media
/backend/target/
/backend/experiments
/backend/scripts/_env.local
/bundle*
/cd.md
/clj-profiler/
@@ -76,7 +74,6 @@
/library/target/
/library/*.zip
/external
/penpot-nitrate
clj-profiler/
node_modules

View File

@@ -13,7 +13,6 @@ export PENPOT_FLAGS="\
disable-login-with-google \
disable-login-with-github \
disable-login-with-gitlab \
disable-telemetry \
enable-backend-worker \
enable-backend-asserts \
disable-feature-fdata-pointer-map \
@@ -56,8 +55,6 @@ export PENPOT_OBJECTS_STORAGE_BACKEND=s3
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
export PENPOT_NITRATE_BACKEND_URI=http://localhost:3000/control-center
export JAVA_OPTS="\
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-Djdk.attach.allowAttachSelf \

View File

@@ -3,10 +3,6 @@
SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/_env;
if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local;
fi
# Initialize MINIO config
setup_minio;

View File

@@ -3,11 +3,6 @@
SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/_env;
if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local;
fi
export OPTIONS="-A:dev"
entrypoint=${1:-app.main};

View File

@@ -3,10 +3,6 @@
SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/_env;
if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local;
fi
# Initialize MINIO config
setup_minio;

View File

@@ -225,8 +225,6 @@
[:netty-io-threads {:optional true} ::sm/int]
[:executor-threads {:optional true} ::sm/int]
[:nitrate-backend-uri {:optional true} ::sm/uri]
;; DEPRECATED
[:assets-storage-backend {:optional true} :keyword]
[:storage-assets-fs-directory {:optional true} :string]

View File

@@ -323,7 +323,6 @@
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::rds/pool (ig/ref ::rds/pool)
:app.nitrate/client (ig/ref :app.nitrate/client)
::wrk/executor (ig/ref ::wrk/netty-executor)
::session/manager (ig/ref ::session/manager)
::ldap/provider (ig/ref ::ldap/provider)
@@ -340,9 +339,6 @@
::email/blacklist (ig/ref ::email/blacklist)
::email/whitelist (ig/ref ::email/whitelist)}
:app.nitrate/client
{::http.client/client (ig/ref ::http.client/client)}
:app.rpc/management-methods
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
@@ -352,7 +348,6 @@
::sto/storage (ig/ref ::sto/storage)
::mtx/metrics (ig/ref ::mtx/metrics)
::mbus/msgbus (ig/ref ::mbus/msgbus)
:app.nitrate/client (ig/ref :app.nitrate/client)
::rds/client (ig/ref ::rds/client)
::setup/props (ig/ref ::setup/props)}

View File

@@ -1,147 +0,0 @@
;; 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
[app.common.logging :as l]
[app.common.schema :as sm]
[app.config :as cf]
[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]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- request-builder
[cfg method uri management-key profile-id]
(fn []
(http/req! cfg {:method method
:headers {"content-type" "application/json"
"accept" "application/json"
"x-shared-key" management-key
"x-profile-id" (str profile-id)}
:uri uri
:version :http1.1})))
(defn- with-retries
[handler max-retries]
(fn []
(loop [attempt 1]
(let [result (try
(handler)
(catch Exception e
(if (< attempt max-retries)
::retry
(do
;; TODO Error handling
(l/error :hint "request fail after multiple retries" :cause e)
nil))))]
(if (= result ::retry)
(recur (inc attempt))
result)))))
(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)))))
(defn- request-to-nitrate
[{:keys [::management-key] :as cfg} method uri schema {:keys [::rpc/profile-id] :as params}]
(let [full-http-call (-> (request-builder cfg method uri management-key profile-id)
(with-retries 3)
(with-validate uri schema))]
(full-http-call)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn call
[cfg method params]
(when (contains? cf/flags :nitrate)
(let [client (get cfg ::client)
method (get client method)]
(method params))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:organization
[:map
[:id ::sm/text]
[:name ::sm/text]])
(def ^:private schema:user
[:map
[:valid ::sm/boolean]])
(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)))
(defn- is-valid-user
[cfg {:keys [profile-id] :as params}]
(let [baseuri (cf/get :nitrate-backend-uri)]
(request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user params)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INITIALIZATION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/init-key ::client
[_ {:keys [::setup/props] :as cfg}]
(if (contains? cf/flags :nitrate)
(let [management-key (or (cf/get :management-api-key)
(get props :management-key))
cfg (assoc cfg ::management-key management-key)]
{:get-team-org (partial get-team-org cfg)
:is-valid-user (partial is-valid-user cfg)})
{}))
(defmethod ig/halt-key! ::client
[_ {:keys []}]
(do :stuff))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; UTILS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn add-nitrate-licence-to-profile
[cfg profile]
(try
(let [nitrate-licence (call cfg :is-valid-user {:profile-id (:id profile)})]
(assoc profile :nitrate-licence (:valid nitrate-licence)))
(catch Throwable cause
(l/error :hint "failed to get nitrate licence"
:profile-id (:id profile)
:cause cause)
profile)))
(defn add-org-to-team
[cfg team params]
(let [params (assoc (or params {}) :team-id (:id team))
org (call cfg :get-team-org params)]
(assoc team :organization-id (:id org) :organization-name (:name org))))

View File

@@ -301,7 +301,6 @@
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)]
(->> (sv/scan-ns
'app.rpc.management.subscription
'app.rpc.management.nitrate
'app.rpc.management.exporter)
(map (partial process-method cfg "management" wrap-management))
(into {}))))

View File

@@ -21,7 +21,6 @@
[app.loggers.audit :as audit]
[app.main :as-alias main]
[app.media :as media]
[app.nitrate :as nitrate]
[app.rpc :as-alias rpc]
[app.rpc.climit :as climit]
[app.rpc.doc :as-alias doc]
@@ -89,8 +88,6 @@
;; --- QUERY: Get profile (own)
(sv/defmethod ::get-profile
{::rpc/auth false
::doc/added "1.18"
@@ -101,13 +98,9 @@
;; no profile-id is in session, and when db call raises not found. In all other
;; cases we need to reraise the exception.
(try
(let [profile (-> (get-profile pool profile-id)
(strip-private-attrs)
(update :props filter-props))]
(if (contains? cf/flags :nitrate)
(nitrate/add-nitrate-licence-to-profile cfg profile)
profile))
(-> (get-profile pool profile-id)
(strip-private-attrs)
(update :props filter-props))
(catch Throwable _
{:id uuid/zero :fullname "Anonymous User"})))

View File

@@ -23,7 +23,6 @@
[app.main :as-alias main]
[app.media :as media]
[app.msgbus :as mbus]
[app.nitrate :as nitrate]
[app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
@@ -191,9 +190,7 @@
::sm/params schema:get-teams}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(dm/with-open [conn (db/open pool)]
(cond->> (get-teams conn profile-id)
(contains? cf/flags :nitrate)
(map #(nitrate/add-org-to-team cfg % params)))))
(get-teams conn profile-id)))
(def ^:private sql:get-owned-teams
"SELECT t.id, t.name,

View File

@@ -1,112 +0,0 @@
;; 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.management.nitrate
"Internal Nitrate HTTP API.
Provides authenticated access to organization management and token validation endpoints.
All requests must include a valid shared key token in the `x-shared-key` header, and
a cookie `auth-token` with the user token.
They will return `401 Unauthorized` if the shared key or user token are invalid."
(:require
[app.common.schema :as sm]
[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.doc :as doc]
[app.util.services :as sv]))
;; ---- API: authenticate
(def ^:private schema:profile
[:map
[:id ::sm/uuid]
[:name :string]
[:email :string]
[:photo-url :string]])
(sv/defmethod ::authenticate
"Authenticate an user
@api GET /authenticate
@returns
200 OK: Returns the authenticated user."
{::doc/added "2.12"
::sm/result schema: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))}))
;; ---- API: get-teams
(def ^:private sql:get-teams
"SELECT t.*
FROM team AS t
JOIN team_profile_rel AS tpr ON t.id = tpr.team_id
WHERE tpr.profile_id = ?
AND tpr.is_owner = 't'
AND t.is_default = 'f'
AND t.deleted_at is null;")
(def ^:private schema:team
[:map
[:id ::sm/uuid]
[:name :string]])
(def ^:private schema:get-teams-result
[:vector schema:team])
(sv/defmethod ::get-teams
"List teams for which current user is owner.
@api GET /get-teams
@returns
200 OK: Returns the list of teams for the user."
{::doc/added "2.12"
::sm/result schema:get-teams-result}
[cfg {:keys [::rpc/profile-id]}]
(when (contains? cf/flags :nitrate)
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
(->> (db/exec! cfg [sql:get-teams current-user-id])
(map #(select-keys % [:id :name]))))))
;; ---- API: notify-team-change
(def ^:private schema:notify-team-change
[:map
[:id ::sm/uuid]
[:organization-id ::sm/text]])
(sv/defmethod ::notify-team-change
"Notify to Penpot a team change from nitrate
@api POST /notify-team-change
@returns
200 OK"
{::doc/added "2.12"
::sm/params schema:notify-team-change
::rpc/auth false}
[cfg {:keys [id organization-id organization-name]}]
(when (contains? cf/flags :nitrate)
(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
:organization-id organization-id
:organization-name organization-name}))))

View File

@@ -241,20 +241,7 @@
(with-out-str
(print-all cause)))))
(defn print-throwable
[cause & {:as opts}]
#?(:clj
(println (format-throwable cause opts))
:cljs
(let [prefix (get opts :prefix "exception")
title (str prefix ": " (ex-message cause))
exdata (ex-data cause)]
(js/console.group title)
(when-let [explain (get exdata ::sm/explain)]
(println (sm/humanize-explain explain)))
(js/console.log "\nData:")
(pp/pprint (dissoc exdata ::sm/explain))
(js/console.log "\nTrace:")
(js/console.error (.-stack cause)))))
#?(:clj
(defn print-throwable
[cause & {:as opts}]
(println (format-throwable cause opts))))

View File

@@ -148,10 +148,7 @@
;; A temporal flag, enables backend code use more extensivelly
;; redis for caching data
:redis-cache
;; Activates the nitrate module
:nitrate})
:redis-cache})
(def all-flags
(set/union email login varia))

View File

@@ -474,6 +474,8 @@
: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}

View File

@@ -141,14 +141,8 @@ http {
proxy_pass http://127.0.0.1:5000;
}
location /control-center {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
location /nitrate/ {
proxy_pass http://127.0.0.1:3000/;
}
location /wasm-playground {

View File

@@ -29,9 +29,8 @@ update_flags /var/www/app/js/config.js
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000}
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"

View File

@@ -139,14 +139,6 @@ http {
proxy_pass $PENPOT_BACKEND_URI/ws/notifications;
}
location /control-center {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $http_cf_connecting_ip;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass $PENPOT_NITRATE_URI$request_uri;
}
include /etc/nginx/overrides/server.d/*.conf;
location / {

View File

@@ -759,4 +759,87 @@ test.describe("Tokens: Apply token", () => {
});
await expect(StrokeWidthPillSmall).toBeVisible();
});
test("User applies margin token to a shape", async ({ page }) => {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
// Set up
await workspace.mockConfigFlags(["enable-feature-token-input"]);
await workspace.setupEmptyFile();
await workspace.mockGetFile("workspace/get-file-layout-stroke-token-json");
await workspace.goToWorkspace();
// Select shape apply stroke
await workspace.layers
.getByTestId("layer-row")
.nth(1)
.getByRole("button", { name: "Toggle layer" })
.click();
await workspace.layers.getByTestId("layer-row").nth(2).click();
const rightSidebar = page.getByTestId("right-sidebar");
await expect(rightSidebar).toBeVisible();
await rightSidebar.getByTestId("add-stroke").click();
// Apply margin token from token panel
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();
const tokensSidebar = workspace.tokensSidebar;
await expect(
tokensSidebar.getByRole("button", { name: "dim.md" }),
).toBeVisible();
await tokensSidebar
.getByRole("button", { name: "dim.md" })
.click({ button: "right" });
await page
.getByTestId("tokens-context-menu-for-token")
.getByText("Spacing")
.hover();
await page
.getByTestId("tokens-context-menu-for-token")
.getByText("Horizontal")
.click();
// Check if token pill is visible on right sidebar
const layoutItemSectionSidebar = rightSidebar.getByRole("region", {
name: "layout item menu",
});
await expect(layoutItemSectionSidebar).toBeVisible();
const marginPillMd = layoutItemSectionSidebar.getByRole("button", {
name: "dim.md",
});
await expect(marginPillMd).toBeVisible();
await marginPillMd.click();
const dimensionTokenOptionXl = page.getByRole("option", { name: "dim.xl" });
await expect(dimensionTokenOptionXl).toBeVisible();
await dimensionTokenOptionXl.click();
const marginPillXL = layoutItemSectionSidebar.getByRole("button", {
name: "dim.xl",
});
await expect(marginPillXL).toBeVisible();
// Detach token from right sidebar and apply another from dropdown
const detachButton = layoutItemSectionSidebar.getByRole("button", {
name: "Detach token",
});
await detachButton.click();
await expect(marginPillXL).not.toBeVisible();
const horizontalMarginInput = layoutItemSectionSidebar.getByText('Horizontal marginOpen token');
await expect(horizontalMarginInput).toBeVisible();
const tokenDropdown = horizontalMarginInput.getByRole('button', { name: 'Open token list' });
await tokenDropdown.click();
await expect(dimensionTokenOptionXl).toBeVisible();
await dimensionTokenOptionXl.click();
await expect(marginPillXL).toBeVisible();
});
});

View File

@@ -12,118 +12,88 @@ test.beforeEach(async ({ page }) => {
await BaseWebSocketPage.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 setupTokensFile(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 renameToken = async (page, oldName, newName) => {
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page, { flags: ["enable-token-shadow"] });
const baseToken = tokensSidebar.getByRole("button", {
name: oldName,
});
await baseToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill(newName);
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
};
const createCompositeDerivedToken = async (page, type, name, reference) => {
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
const { tokensUpdateCreateModal } = await setupTokensFile(page, {
flags: ["enable-token-shadow"],
});
await tokensTabPanel
.getByRole("button", { name: `Add Token: ${type}` })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Name",
});
await nameField.fill(name);
const referenceToggle = tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Reference",
});
await referenceField.fill(reference);
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
};
test.describe("Remapping Tokens", () => {
test.describe("Tokens: Remapping Feature", () => {
test.describe("Box Shadow Token Remapping", () => {
test("User renames box shadow token with alias references", async ({
page,
}) => {
const { tokensSidebar } = await setupTokensFile(page, {
flags: ["enable-token-shadow"],
});
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base shadow token
await createToken(page, "Shadow", "base-shadow", "Color", "#000000");
await tokensTabPanel
.getByRole("button", { name: "Add Token: Shadow" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("base-shadow");
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Color",
});
await colorField.fill("#000000");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived shadow token that references base-shadow
await createCompositeDerivedToken(
page,
"Shadow",
"derived-shadow",
"{base-shadow}",
);
await tokensTabPanel
.getByRole("button", { name: "Add Token: Shadow" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Name",
});
await nameField.fill("derived-shadow");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Reference",
});
await referenceField.fill("{base-shadow}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Rename base-shadow token
await renameToken(page, "base-shadow", "foundation-shadow");
const baseToken = tokensSidebar.getByRole("button", {
name: "base-shadow",
});
await baseToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("foundation-shadow");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
await expect(remappingModal).toContainText("base-shadow");
await expect(remappingModal).toContainText("foundation-shadow");
await expect(remappingModal).toContainText("1");
const confirmButton = remappingModal.getByRole("button", {
name: "remap tokens",
name: /remap/i,
});
await confirmButton.click();
@@ -146,16 +116,51 @@ test.describe("Remapping Tokens", () => {
workspacePage,
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base shadow token
await createToken(page, "Shadow", "primary-shadow", "Color", "#000000");
await tokensTabPanel
.getByRole("button", { name: "Add Token: Shadow" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("primary-shadow");
let colorField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Color",
});
await colorField.fill("#000000");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived shadow token that references base
await createCompositeDerivedToken(
page,
"Shadow",
"card-shadow",
"{primary-shadow}",
);
await tokensTabPanel
.getByRole("button", { name: "Add Token: Shadow" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("card-shadow");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Reference",
});
await referenceField.fill("{primary-shadow}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Apply the referenced token to a shape
await page.getByRole("tab", { name: "Layers" }).click();
@@ -178,16 +183,16 @@ test.describe("Remapping Tokens", () => {
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("main-shadow");
// Update the color value
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
colorField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Color",
});
await colorField.fill("#FF0000");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
@@ -197,7 +202,7 @@ test.describe("Remapping Tokens", () => {
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: "remap tokens",
name: /remap/i,
});
await confirmButton.click();
@@ -254,25 +259,73 @@ test.describe("Remapping Tokens", () => {
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base typography token
await createToken(page, "Typography", "base-text", "Font size", "16");
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("base-text");
const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Font size",
});
await fontSizeField.fill("16");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived typography token
await createCompositeDerivedToken(
page,
"Typography",
"body-text",
"{base-text}",
);
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Name",
});
await nameField.fill("body-text");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Reference",
});
await referenceField.fill("{base-text}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Rename base token
await renameToken(page, "base-text", "default-text");
const baseToken = tokensSidebar.getByRole("button", {
name: "base-text",
});
await baseToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("default-text");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: "remap tokens",
name: /remap/i,
});
await confirmButton.click();
@@ -298,7 +351,24 @@ test.describe("Remapping Tokens", () => {
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base typography token
await createToken(page, "Typography", "body-style", "Font size", "16");
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("body-style");
let fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Font size",
});
await fontSizeField.fill("16");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived typography token
await tokensTabPanel
@@ -306,7 +376,7 @@ test.describe("Remapping Tokens", () => {
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByRole("textbox", {
nameField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Name",
});
await nameField.fill("paragraph-style");
@@ -320,7 +390,7 @@ test.describe("Remapping Tokens", () => {
});
await referenceField.fill("{body-style}");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
@@ -351,7 +421,7 @@ test.describe("Remapping Tokens", () => {
await nameField.fill("text-base");
// Update the font size value
const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Font size",
});
await fontSizeField.fill("18");
@@ -366,7 +436,7 @@ test.describe("Remapping Tokens", () => {
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: "remap tokens",
name: /remap/i,
});
await confirmButton.click();
@@ -401,29 +471,72 @@ test.describe("Remapping Tokens", () => {
test("User renames border radius token with alias references", async ({
page,
}) => {
const { tokensSidebar } = await setupTokensFile(page);
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base border radius token
await createToken(page, "Border Radius", "base-radius", "Value", "4");
await tokensTabPanel
.getByRole("button", { name: "Add Token: Border Radius" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("base-radius");
const valueField = tokensUpdateCreateModal.getByLabel("Value");
await valueField.fill("4");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived border radius token
await createToken(
page,
"Border Radius",
"card-radius",
"Value",
"{base-radius}",
);
await tokensTabPanel
.getByRole("button", { name: "Add Token: Border Radius" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("card-radius");
const valueField2 = tokensUpdateCreateModal.getByLabel("Value");
await valueField2.fill("{base-radius}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Rename base token
await renameToken(page, "base-radius", "primary-radius");
const baseToken = tokensSidebar.getByRole("button", {
name: "base-radius",
});
await baseToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("primary-radius");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: "remap tokens",
name: /remap/i,
});
await confirmButton.click();
@@ -445,17 +558,43 @@ test.describe("Remapping Tokens", () => {
tokenContextMenuForToken,
} = await setupTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base border radius token
await createToken(page, "Border Radius", "radius-sm", "Value", "4");
await tokensTabPanel
.getByRole("button", { name: "Add Token: Border Radius" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("radius-sm");
let valueField = tokensUpdateCreateModal.getByLabel("Value");
await valueField.fill("4");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived border radius token
await createToken(
page,
"Border Radius",
"button-radius",
"Value",
"{radius-sm}",
);
await tokensTabPanel
.getByRole("button", { name: "Add Token: Border Radius" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("button-radius");
const valueField2 = tokensUpdateCreateModal.getByLabel("Value");
await valueField2.fill("{radius-sm}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Rename and update value of base token
const radiusToken = tokensSidebar.getByRole("button", {
@@ -465,14 +604,14 @@ test.describe("Remapping Tokens", () => {
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("radius-base");
// Update the value
const valueField = tokensUpdateCreateModal.getByLabel("Value");
valueField = tokensUpdateCreateModal.getByLabel("Value");
await valueField.fill("8");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
@@ -482,7 +621,7 @@ test.describe("Remapping Tokens", () => {
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: "remap tokens",
name: /remap/i,
});
await confirmButton.click();
@@ -509,82 +648,4 @@ test.describe("Remapping Tokens", () => {
await expect(currentValue).toHaveValue("{radius-base}");
});
});
test.describe("Cancel remap", () => {
test("Only rename - breaks reference", async ({ page }) => {
const { tokensSidebar } = await setupTokensFile(page, {
flags: ["enable-token-shadow"],
});
// Create base shadow token
await createToken(page, "Shadow", "base-shadow", "Color", "#000000");
// Create derived shadow token that references base-shadow
await createCompositeDerivedToken(
page,
"Shadow",
"derived-shadow",
"{base-shadow}",
);
// Rename base-shadow token
await renameToken(page, "base-shadow", "foundation-shadow");
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const cancelButton = remappingModal.getByRole("button", {
name: "don't remap",
});
await cancelButton.click();
// Verify token was renamed
await expect(
tokensSidebar.getByRole("button", {
name: "foundation-shadow",
}),
).toBeVisible();
await expect(
tokensSidebar.locator('[aria-label="Missing reference"]'),
).toBeVisible();
});
test("Cancel process - no changes applied", async ({ page }) => {
const { tokensSidebar } = await setupTokensFile(page, {
flags: ["enable-token-shadow"],
});
// Create base shadow token
await createToken(page, "Shadow", "base-shadow", "Color", "#000000");
// Create derived shadow token that references base-shadow
await createCompositeDerivedToken(
page,
"Shadow",
"derived-shadow",
"{base-shadow}",
);
// Rename base-shadow token
await renameToken(page, "base-shadow", "foundation-shadow");
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const closeButton = remappingModal.getByRole("button", {
name: "close",
});
await closeButton.click();
// Verify original token name still exists
await expect(
tokensSidebar.getByRole("button", { name: "base-shadow" }),
).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "derived-shadow" }),
).toBeVisible();
});
});
});

View File

@@ -216,32 +216,4 @@ test.describe("Tokens: Sets Tab", () => {
await expect(tokenSetItems.nth(1)).toHaveAttribute("aria-checked", "false");
await expect(tokenSetItems.nth(2)).toHaveAttribute("aria-checked", "true");
});
test("Display active set and verify if is enabled", async ({ page }) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenSetItems } =
await setupTokensFile(page);
// Create set
await tokenThemesSetsSidebar
.getByRole("button", { name: "Add set" })
.click();
await changeSetInput(tokenThemesSetsSidebar, "Inactive set");
await tokenThemesSetsSidebar
.getByRole("button", { name: "Inactive set" })
.click();
let activeSetTitle = await tokensSidebar.getByTestId(
"active-token-set-title",
);
await expect(activeSetTitle).toHaveText("TOKENS - Inactive set");
const inactiveSetInfo = await tokensSidebar.getByTitle(
"This set is not active.",
);
await expect(inactiveSetInfo).toBeVisible();
// Switch active set
await tokenThemesSetsSidebar.getByRole("button", { name: "theme" }).click();
await expect(activeSetTitle).toHaveText("TOKENS - theme");
await expect(inactiveSetInfo).not.toBeVisible();
});
});

View File

@@ -15,7 +15,6 @@
[app.common.time :as ct]
[app.common.types.project :refer [valid-project?]]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.constants :as mconst]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
@@ -684,25 +683,12 @@
(rx/of (dcm/change-team-role params)
(modal/hide)))))
(defn handle-change-team-org
[{:keys [team-id organization-id organization-name]}]
(ptk/reify ::handle-change-team-org
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)
state))))
(defn- process-message
[{:keys [type] :as msg}]
(case type
:notification (dcm/handle-notification msg)
:team-role-change (handle-change-team-role msg)
:team-membership-change (dcm/team-membership-change msg)
:team-org-change (handle-change-team-org msg)
nil))

View File

@@ -539,10 +539,11 @@
value shape-ids
#{:stroke-width}
page-id))
(some attributes #{:max-width :max-height})
(some attributes #{:max-width :max-height :layout-item-max-h :layout-item-max-w :layout-item-min-h :layout-item-min-w})
(conj #(update-layout-sizing-limits
value shape-ids
(set (filter attributes #{:max-width :max-height}))
(set (filter attributes #{:max-width :max-height :layout-item-max-h :layout-item-max-w :layout-item-min-h :layout-item-min-w}))
page-id))))
(defn apply-dimensions-token

View File

@@ -29,9 +29,6 @@
;; Will contain the latest error report assigned
(def last-report nil)
;; Will contain last uncaught exception
(def last-exception nil)
(defn- print-data!
[data]
(-> data
@@ -341,6 +338,7 @@
(print-data! werror)
(print-explain! werror))))))))
(defonce uncaught-error-handler
(letfn [(is-ignorable-exception? [cause]
(let [message (ex-message cause)]
@@ -351,31 +349,10 @@
(on-unhandled-error [event]
(.preventDefault ^js event)
(when-let [cause (unchecked-get event "error")]
(set! last-exception cause)
(when-not (is-ignorable-exception? cause)
(ex/print-throwable cause :prefix "uncaught exception")
(st/async-emit!
(ntf/show {:content (tr "errors.unexpected-exception" (ex-message cause))
:type :toast
:level :error
:timeout 3000})))))
(on-unhandled-rejection [event]
(.preventDefault ^js event)
(when-let [cause (unchecked-get event "reason")]
(set! last-exception cause)
(ex/print-throwable cause :prefix "uncaught rejection")
(st/async-emit!
(ntf/show {:content (tr "errors.unexpected-exception" (ex-message cause))
:type :toast
:level :error
:timeout 3000}))))]
(when-let [error (unchecked-get event "error")]
(when-not (is-ignorable-exception? error)
(on-error error))))]
(.addEventListener glob/window "error" on-unhandled-error)
(.addEventListener glob/window "unhandledrejection" on-unhandled-rejection)
(fn []
(.removeEventListener glob/window "error" on-unhandled-error)
(.removeEventListener glob/window "unhandledrejection" on-unhandled-rejection))))
(.removeEventListener glob/window "error" on-unhandled-error))))

View File

@@ -35,7 +35,6 @@
[app.main.ui.dashboard.team-form]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.nitrate.nitrate-form]
[app.util.dom :as dom]
[app.util.dom.dnd :as dnd]
[app.util.i18n :as i18n :refer [tr]]
@@ -281,8 +280,8 @@
(mf/defc teams-selector-dropdown*
{::mf/private true}
[{:keys [team profile teams show-default-team allow-create-teams allow-create-org] :rest props}]
(let [on-create-team-click
[{:keys [team profile teams] :rest props}]
(let [on-create-click
(mf/use-fn #(st/emit! (modal/show :team-form {})))
on-team-click
@@ -291,27 +290,18 @@
(let [team-id (-> (dom/get-current-target event)
(dom/get-data "value")
(uuid/parse))]
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))
on-create-org-click
(mf/use-fn
(fn []
(if (:nitrate-licence profile)
;; TODO update when org creation route is ready
(dom/open-new-window "/control-center/org/create")
(st/emit! (modal/show :nitrate-form {})))))]
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))]
[:> dropdown-menu* props
(when show-default-team
[:> dropdown-menu-item* {:on-click on-team-click
:data-value (:default-team-id profile)
:class (stl/css :team-dropdown-item)}
[:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon]
[:> dropdown-menu-item* {:on-click on-team-click
:data-value (:default-team-id profile)
: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))
tick-icon)])
[:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")]
(when (= (:default-team-id profile) (:id team))
tick-icon)]
(for [team-item (remove :is-default (vals teams))]
[:> dropdown-menu-item* {:on-click on-team-click
@@ -332,19 +322,11 @@
(when (= (:id team-item) (:id team))
tick-icon)])
(when allow-create-teams
[:hr {:role "separator" :class (stl/css :team-separator)}]
[:> dropdown-menu-item* {:on-click on-create-team-click
:class (stl/css :team-dropdown-item :action)}
[:span {:class (stl/css :icon-wrapper)} add-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]])
(when allow-create-org
[:hr {:role "separator" :class (stl/css :team-separator)}]
[:> dropdown-menu-item* {:on-click on-create-org-click
:class (stl/css :team-dropdown-item :action)}
[:span {:class (stl/css :icon-wrapper)} add-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-org")]])]))
[:hr {:role "separator" :class (stl/css :team-separator)}]
[:> dropdown-menu-item* {:on-click on-create-click
:class (stl/css :team-dropdown-item :action)}
[:span {:class (stl/css :icon-wrapper)} add-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]]]))
(mf/defc team-options-dropdown*
{::mf/private true}
@@ -494,80 +476,9 @@
:data-testid "delete-team"}
(tr "dashboard.delete-team")])]))
(mf/defc sidebar-org-switch*
[{:keys [team profile]}]
(let [teams (->> (mf/deref refs/teams)
vals
(group-by :organization-id)
(map (fn [[_group entries]] (first entries)))
vec
(d/index-by :id))
teams (update-vals teams
(fn [t]
(assoc t :name (str "ORG: " (:organization-name t)))))
team (assoc team :name (str "ORG: " (:organization-name team)))
show-teams-menu*
(mf/use-state false)
show-teams-menu?
(deref show-teams-menu*)
on-show-teams-click
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(swap! show-teams-menu* not)))
on-show-teams-keydown
(mf/use-fn
(fn [event]
(when (or (kbd/space? event)
(kbd/enter? event))
(dom/prevent-default event)
(dom/stop-propagation event)
(some-> (dom/get-current-target event)
(dom/click!)))))
close-teams-menu
(mf/use-fn #(reset! show-teams-menu* false))]
[:div {:class (stl/css :sidebar-team-switch)}
[:div {:class (stl/css :switch-content)}
[:button {:class (stl/css :current-team)
:on-click on-show-teams-click
:on-key-down on-show-teams-keydown}
[:div {:class (stl/css :team-name)}
[:img {:src (cf/resolve-team-photo-url team)
:class (stl/css :team-picture)
:alt (:name team)}]
[:span {:class (stl/css :team-text) :title (:name team)} (:name team)]]
arrow-icon]]
;; Teams Dropdown
[:> teams-selector-dropdown* {:show show-teams-menu?
:on-close close-teams-menu
:id "organizations-list"
:class (stl/css :dropdown :teams-dropdown)
:team team
:profile profile
:teams teams
:show-default-team false
:allow-create-teams false
:allow-create-org true}]]))
(mf/defc sidebar-team-switch*
[{:keys [team profile]}]
(let [nitrate? (contains? cf/flags :nitrate)
org-id (when nitrate? (:organization-id team))
teams (cond->> (mf/deref refs/teams)
nitrate?
(filter #(= (-> % val :organization-id) org-id)))
(let [teams (mf/deref refs/teams)
subscription
(get team :subscription)
@@ -675,10 +586,7 @@
:class (stl/css :dropdown :teams-dropdown)
:team team
:profile profile
:teams teams
:show-default-team true
:allow-create-teams true
:allow-create-org false}]
:teams teams}]
[:> team-options-dropdown* {:show show-team-options-menu?
:on-close close-team-options-menu
@@ -795,8 +703,6 @@
[:*
[:div {:class (stl/css-case :sidebar-content true)
:ref container}
(when (contains? cf/flags :nitrate)
[:> sidebar-org-switch* {:team team :profile profile}])
[:> sidebar-team-switch* {:team team :profile profile}]
[:> sidebar-search* {:search-term search-term

View File

@@ -189,6 +189,7 @@
:float
:string
[:= :multiple]]]]
[:text-icon {:optional true} :string]
[:default {:optional true} [:maybe :string]]
[:placeholder {:optional true} :string]
[:icon {:optional true} [:maybe schema:icon]]
@@ -216,7 +217,8 @@
is-selected-on-focus nillable
tokens applied-token empty-to-end
on-change on-blur on-focus on-detach
property align ref name]
property align ref name
text-icon]
:rest props}]
(let [;; NOTE: we use mfu/bean here for transparently handle
@@ -637,14 +639,23 @@
:on-change store-raw-value
:variant "comfortable"
:disabled disabled
:slot-start (when icon
(mf/html [:> tooltip*
{:content property
:id property}
[:> icon* {:icon-id icon
:size "s"
:aria-labelledby property
:class (stl/css :icon)}]]))
:slot-start (when (or icon text-icon)
(mf/html
[:> tooltip*
{:content property
:id property}
(cond
icon
[:> icon*
{:icon-id icon
:size "s"
:aria-labelledby property
:class (stl/css :icon)}]
text-icon
[:div {:class (stl/css :text-icon)
:aria-labelledby property}
text-icon])]))
:slot-end (when-not disabled
(when (some? tokens)
(mf/html [:> icon-button* {:variant "ghost"
@@ -676,14 +687,23 @@
:disabled disabled
:on-blur on-blur
:class inner-class
:slot-start (when icon
(mf/html [:> tooltip*
{:content property
:id property}
[:> icon* {:icon-id icon
:size "s"
:aria-labelledby property
:class (stl/css :icon)}]]))
:slot-start (when (or icon text-icon)
(mf/html
[:> tooltip*
{:content property
:id property}
(cond
icon
[:> icon*
{:icon-id icon
:size "s"
:aria-labelledby property
:class (stl/css :icon)}]
text-icon
[:div {:class (stl/css :text-icon)
:aria-labelledby property}
text-icon])]))
:token-wrapper-ref token-wrapper-ref
:token-detach-btn-ref token-detach-btn-ref
:detach-token detach-token})))]
@@ -718,21 +738,41 @@
(mf/with-effect [dropdown-options]
(mf/set-ref-val! options-ref dropdown-options))
[:div {:class (dm/str class " " (stl/css :input-wrapper))
:ref wrapper-ref}
(if (some? icon)
[:div {:class (dm/str class " " (stl/css :input-wrapper))
:ref wrapper-ref}
(if (and (some? token-applied)
(not= :multiple token-applied))
[:> token-field* token-props]
[:> input-field* input-props])
(if (and (some? token-applied)
(not= :multiple token-applied))
[:> token-field* token-props]
[:> input-field* input-props])
(when ^boolean is-open
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
[:> options-dropdown* {:on-click on-option-click
:id listbox-id
:options options
:selected selected-id
:focused focused-id
:align align
:empty-to-end empty-to-end
:ref set-option-ref}]))]))
(when ^boolean is-open
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
[:> options-dropdown* {:on-click on-option-click
:id listbox-id
:options options
:selected selected-id
:focused focused-id
:align align
:empty-to-end empty-to-end
:ref set-option-ref}]))]
[:div {:class (dm/str class " " (stl/css :input-wrapper))
:aria-labelledby property
:ref wrapper-ref}
(if (and (some? token-applied)
(not= :multiple token-applied))
[:> token-field* token-props]
[:> input-field* input-props])
(when ^boolean is-open
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
[:> options-dropdown* {:on-click on-option-click
:id listbox-id
:options options
:selected selected-id
:focused focused-id
:align align
:empty-to-end empty-to-end
:ref set-option-ref}]))])))

View File

@@ -29,7 +29,14 @@
.icon {
color: var(--color-foreground-secondary);
min-width: var(--sp-l);
min-inline-size: var(--sp-l);
}
.text-icon {
color: var(--color-foreground-secondary);
@include t.use-typography("code-font");
inline-size: fit-content;
min-inline-size: px2rem(40);
}
.invisible-button {

View File

@@ -17,7 +17,7 @@
--token-field-outline-color: none;
--token-field-height: var(--sp-xxxl);
--token-field-margin: unset;
display: grid;
display: inline-flex;
column-gap: var(--sp-xs);
align-items: center;
position: relative;

View File

@@ -8,7 +8,6 @@
"React error boundary components"
(:require
["react-error-boundary" :as reb]
[app.common.exceptions :as ex]
[app.main.errors :as errors]
[app.main.refs :as refs]
[goog.functions :as gfn]
@@ -35,8 +34,7 @@
;; very small amount of time, so we debounce for 100ms for
;; avoid duplicate and redundant reports
(gfn/debounce (fn [error info]
(set! errors/last-exception error)
(ex/print-throwable error)
(js/console.log "Cause stack: \n" (.-stack error))
(js/console.error
"Component trace: \n"
(unchecked-get info "componentStack")

View File

@@ -1,48 +0,0 @@
;; 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-form
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.modal :as modal]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
;; FIXME: rename to `form` (remove the nitrate prefix from namespace,
;; because it is already under nitrate)
(mf/defc nitrate-form-modal*
{::mf/register modal/components
::mf/register-as :nitrate-form}
[]
(let [on-click
(mf/use-fn
(fn []
(dom/open-new-window "/control-center/licenses/start")))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :nitrate-form)}
[:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-title)}
"BUY NITRATE"]
[:button {:class (stl/css :modal-close-btn)
:on-click modal/hide!} deprecated-icon/close]]
[:div {:class (stl/css :modal-content)}
"Nitrate is so cool! You should buy it!"]
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
[:> button* {:variant "primary"
:on-click on-click}
"BUY NOW!"]]]]]]))

View File

@@ -1,52 +0,0 @@
// 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;
.modal-overlay {
@extend .modal-overlay-base;
}
.modal-container {
@extend .modal-container-base;
}
.modal-header {
margin-bottom: deprecated.$s-24;
}
.modal-title {
@include deprecated.uppercaseTitleTipography;
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
@extend .modal-close-btn-base;
}
.modal-content {
margin-bottom: deprecated.$s-24;
}
.nitrate-form {
min-width: deprecated.$s-400;
}
.action-buttons {
@extend .modal-action-btns;
}
.cancel-button {
@extend .modal-cancel-btn;
}
.accept-btn {
@extend .modal-accept-btn;
&.danger {
@extend .modal-danger-btn;
}
}

View File

@@ -119,6 +119,7 @@
[:button {:class (stl/css-case
:toggle-content true
:inverse expanded?)
:aria-label "Toggle layer"
:on-click on-toggle-collapse}
deprecated-icon/arrow])

View File

@@ -9,13 +9,18 @@
(:require
[app.common.data :as d]
[app.common.types.shape.layout :as ctl]
[app.common.types.token :as tk]
[app.main.data.workspace :as udw]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.numeric-input :as deprecated-input]
[app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.context :as muc]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
@@ -24,6 +29,51 @@
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc numeric-input-wrapper*
{::mf/private true}
[{:keys [values name applied-tokens align on-detach placeholder] :rest props}]
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
input-type (cond
(some #{:m2 :m4} [name])
:horizontal-margin
(some #{:m1 :m3} [name])
:vertical-margin
(= name :layout-item-max-w)
:max-width
(= name :layout-item-max-h)
:max-height
(= name :layout-item-min-w)
:min-width
(= name :layout-item-min-h)
:min-height
:else
name)
tokens (mf/with-memo [tokens input-type]
(delay
(-> (deref tokens)
(select-keys (get tk/tokens-by-input input-type))
(not-empty))))
on-detach-attr
(mf/use-fn
(mf/deps on-detach name)
#(on-detach % name))
props (mf/spread-props props
{:placeholder (or placeholder "--")
:applied-token (get applied-tokens name)
:tokens tokens
:align align
:on-detach on-detach-attr
:value (get values name)})]
[:> numeric-input* props]))
(def layout-item-attrs
[:layout-item-margin ;; {:m1 0 :m2 0 :m3 0 :m4 0}
:layout-item-margin-type ;; :simple :multiple
@@ -46,147 +96,339 @@
(select-margins (= prop :m1) (= prop :m2) (= prop :m3) (= prop :m4)))
(mf/defc margin-simple*
[{:keys [value on-change on-blur]}]
(let [m1 (:m1 value)
[{:keys [value on-change on-blur applied-tokens ids]}]
(let [token-numeric-inputs
(features/use-feature "tokens/numeric-input")
m1 (:m1 value)
m2 (:m2 value)
m3 (:m3 value)
m4 (:m4 value)
m1-placeholder (if (and (not= value :multiple) (not= m1 m3)) (tr "settings.multiple") "--")
m2-placeholder (if (and (not= value :multiple) (not= m2 m4)) (tr "settings.multiple") "--")
m1 (when (and (not= value :multiple) (= m1 m3)) m1)
m2 (when (and (not= value :multiple) (= m2 m4)) m2)
token-applied-m1 (:m1 applied-tokens)
token-applied-m2 (:m2 applied-tokens)
token-applied-m3 (:m3 applied-tokens)
token-applied-m4 (:m4 applied-tokens)
token-applied-m1 (if (and (not= applied-tokens :multiple) (= token-applied-m1 token-applied-m3)) token-applied-m1
:multiple)
token-applied-m2 (if (and (not= applied-tokens :multiple) (= token-applied-m2 token-applied-m4)) token-applied-m2
:multiple)
m1-placeholder (if (and (not= value :multiple)
(= m1 m3)
(= token-applied-m1 token-applied-m3))
"--"
(tr "settings.multiple"))
m2-placeholder (if (and (not= value :multiple)
(= m2 m4)
(= token-applied-m2 token-applied-m4))
"--"
(tr "settings.multiple"))
on-focus
(mf/use-fn
(fn [event]
(let [attr (-> (dom/get-current-target event)
(dom/get-data "name")
(keyword))]
(case attr
:m1 (select-margins true false true false)
:m2 (select-margins false true false true))
(fn [attr event]
(case attr
:m1 (select-margins true false true false)
:m2 (select-margins false true false true))
(dom/select-target event)))
(dom/select-target event))))
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token attr]
(st/emit! (dwta/unapply-token {:token (first token)
:attributes #{attr}
:shape-ids ids}))))
on-detach-horizontal
(mf/use-fn
(mf/deps on-detach-token)
(fn [token]
(prn "token" token)
(run! #(on-detach-token token %) [:m2 :m4])))
on-detach-vertical
(mf/use-fn
(mf/deps on-detach-token)
(fn [token]
(run! #(on-detach-token token %) [:m1 :m3])))
on-change'
(mf/use-fn
(mf/deps on-change)
(fn [value event]
(let [attr (-> (dom/get-current-target event)
(dom/get-data "name")
(keyword))]
(on-change :simple attr value))))]
(mf/deps on-change ids)
(fn [value attr]
(if (or (string? value) (int? 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}))))))
on-focus-m1
(mf/use-fn (mf/deps on-focus) #(on-focus :m1))
on-focus-m2
(mf/use-fn (mf/deps on-focus) #(on-focus :m2))
on-m1-change
(mf/use-fn (mf/deps on-change') #(on-change' % :m1))
on-m2-change
(mf/use-fn (mf/deps on-change') #(on-change' % :m2))]
[:div {:class (stl/css :margin-simple)}
[:div {:class (stl/css :vertical-margin)
:title "Vertical margin"}
[:span {:class (stl/css :icon)}
deprecated-icon/margin-top-bottom]
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
:placeholder m1-placeholder
:data-name "m1"
:on-focus on-focus
:on-change on-change'
:on-blur on-blur
:nillable true
:value m1}]]
(if token-numeric-inputs
[:> numeric-input-wrapper*
{:on-change on-m1-change
:on-detach on-detach-vertical
:class (stl/css :vertical-margin-wrapper)
:on-blur on-blur
:on-focus on-focus-m1
:placeholder m1-placeholder
:icon i/margin-top-bottom
:min 0
:name :m1
:property "Vertical margin "
:nillable true
:applied-tokens {:m1 token-applied-m1}
:values {:m1 m1}}]
[:div {:class (stl/css :horizontal-margin)
:title "Horizontal margin"}
[:span {:class (stl/css :icon)}
deprecated-icon/margin-left-right]
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
:placeholder m2-placeholder
:data-name "m2"
:on-focus on-focus
:on-change on-change'
:on-blur on-blur
:nillable true
:value m2}]]]))
[:div {:class (stl/css :vertical-margin)
:title "Vertical margin"}
[:span {:class (stl/css :icon)}
deprecated-icon/margin-top-bottom]
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
:placeholder m1-placeholder
:data-name "m1"
:on-focus on-focus-m1
:on-change on-m1-change
:on-blur on-blur
:nillable true
:value m1}]])
(if token-numeric-inputs
[:> numeric-input-wrapper*
{:on-change on-m2-change
:on-detach on-detach-horizontal
:on-blur on-blur
:on-focus on-focus-m2
:placeholder m2-placeholder
:icon i/margin-left-right
:class (stl/css :horizontal-margin-wrapper)
:min 0
:name :m2
:align :right
:property "Horizontal margin"
:nillable true
:applied-tokens {:m2 token-applied-m2}
:values {:m2 m2}}]
[:div {:class (stl/css :horizontal-margin)
:title "Horizontal margin"}
[:span {:class (stl/css :icon)}
deprecated-icon/margin-left-right]
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
:placeholder m2-placeholder
:data-name "m2"
:on-focus on-focus-m2
:on-change on-m2-change
:on-blur on-blur
:nillable true
:value m2}]])]))
(mf/defc margin-multiple*
[{:keys [value on-change on-blur]}]
(let [m1 (:m1 value)
[{:keys [value on-change on-blur applied-tokens ids]}]
(let [token-numeric-inputs
(features/use-feature "tokens/numeric-input")
m1 (:m1 value)
m2 (:m2 value)
m3 (:m3 value)
m4 (:m4 value)
applied-token-to-m1 (:m1 applied-tokens)
applied-token-to-m2 (:m2 applied-tokens)
applied-token-to-m3 (:m3 applied-tokens)
applied-token-to-m4 (:m4 applied-tokens)
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token attr]
(st/emit! (dwta/unapply-token {:token (first token)
:attributes #{attr}
:shape-ids ids}))))
on-focus
(mf/use-fn
(fn [event]
(let [attr (-> (dom/get-current-target event)
(dom/get-data "name")
(keyword))]
(select-margin attr)
(dom/select-target event))))
(fn [attr event]
(select-margin attr)
(dom/select-target event)))
on-focus-m1
(mf/use-fn (mf/deps on-focus) #(on-focus :m1))
on-focus-m2
(mf/use-fn (mf/deps on-focus) #(on-focus :m2))
on-focus-m3
(mf/use-fn (mf/deps on-focus) #(on-focus :m1))
on-focus-m4
(mf/use-fn (mf/deps on-focus) #(on-focus :m2))
on-change'
(mf/use-fn
(mf/deps on-change)
(fn [value event]
(let [attr (-> (dom/get-current-target event)
(dom/get-data "name")
(keyword))]
(on-change :multiple attr value))))]
(mf/deps on-change ids)
(fn [value attr]
(if (or (string? value) (int? value))
(on-change :multiple attr value)
(do
(st/emit!
(dwta/toggle-token {:token (first value)
:attrs #{attr}
:shape-ids ids}))))))
on-m1-change
(mf/use-fn (mf/deps on-change') #(on-change' % :m1))
on-m2-change
(mf/use-fn (mf/deps on-change') #(on-change' % :m2))
on-m3-change
(mf/use-fn (mf/deps on-change') #(on-change' % :m3))
on-m4-change
(mf/use-fn (mf/deps on-change') #(on-change' % :m4))]
[:div {:class (stl/css :margin-multiple)}
[:div {:class (stl/css :top-margin)
:title "Top margin"}
[:span {:class (stl/css :icon)}
deprecated-icon/margin-top]
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
:placeholder "--"
:data-name "m1"
:on-focus on-focus
:on-change on-change'
:on-blur on-blur
:nillable true
:value m1}]]
[:div {:class (stl/css :right-margin)
:title "Right margin"}
[:span {:class (stl/css :icon)}
deprecated-icon/margin-right]
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
:placeholder "--"
:data-name "m2"
:on-focus on-focus
:on-change on-change'
:on-blur on-blur
:nillable true
:value m2}]]
(if token-numeric-inputs
[:> numeric-input-wrapper*
{:on-change on-m1-change
:on-detach on-detach-token
:on-blur on-blur
:on-focus on-focus-m1
:icon i/margin-top
:class (stl/css :top-margin-wrapper)
:min 0
:name :m1
:property "Top margin"
:nillable true
:applied-tokens {:m1 applied-token-to-m1}
:values {:m1 m1}}]
[:div {:class (stl/css :bottom-margin)
:title "Bottom margin"}
[:span {:class (stl/css :icon)}
deprecated-icon/margin-bottom]
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
:placeholder "--"
:data-name "m3"
:on-focus on-focus
:on-change on-change'
:on-blur on-blur
:nillable true
:value m3}]]
[:div {:class (stl/css :top-margin)
:title "Top margin"}
[:span {:class (stl/css :icon)}
deprecated-icon/margin-top]
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
:placeholder "--"
:data-name "m1"
:on-focus on-focus-m1
:on-change on-m1-change
:on-blur on-blur
:nillable true
:value m1}]])
(if token-numeric-inputs
[:> numeric-input-wrapper*
{:on-change on-m2-change
:on-detach on-detach-token
:on-blur on-blur
:on-focus on-focus-m2
:icon i/margin-right
:class (stl/css :right-margin-wrapper)
:min 0
:name :m2
:align :right
:property "Right margin"
:nillable true
:applied-tokens {:m2 applied-token-to-m2}
:values {:m2 m2}}]
[:div {:class (stl/css :left-margin)
:title "Left margin"}
[:span {:class (stl/css :icon)}
deprecated-icon/margin-left]
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
:placeholder "--"
:data-name "m4"
:on-focus on-focus
:on-change on-change'
:on-blur on-blur
:nillable true
:value m4}]]]))
[:div {:class (stl/css :right-margin)
:title "Right margin"}
[:span {:class (stl/css :icon)}
deprecated-icon/margin-right]
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
:placeholder "--"
:data-name "m2"
:on-focus on-focus-m2
:on-change on-m2-change
:on-blur on-blur
:nillable true
:value m2}]])
(if token-numeric-inputs
[:> numeric-input-wrapper*
{:on-change on-m3-change
:on-detach on-detach-token
:on-blur on-blur
:on-focus on-focus-m3
:icon i/margin-bottom
:class (stl/css :bottom-margin-wrapper)
:min 0
:name :m3
:align :right
:property "Bottom margin"
:nillable true
:applied-tokens {:m3 applied-token-to-m3}
:values {:m3 m3}}]
[:div {:class (stl/css :bottom-margin)
:title "Bottom margin"}
[:span {:class (stl/css :icon)}
deprecated-icon/margin-bottom]
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
:placeholder "--"
:data-name "m3"
:on-focus on-focus-m3
:on-change on-m3-change
:on-blur on-blur
:nillable true
:value m3}]])
(if token-numeric-inputs
[:> numeric-input-wrapper*
{:on-change on-m4-change
:on-detach on-detach-token
:on-blur on-blur
:on-focus on-focus-m4
:icon i/margin-left
:class (stl/css :left-margin-wrapper)
:min 0
:name :m4
:property "Left margin"
:nillable true
:applied-tokens {:m4 applied-token-to-m4}
:values {:m4 m4}}]
[:div {:class (stl/css :left-margin)
:title "Left margin"}
[:span {:class (stl/css :icon)}
deprecated-icon/margin-left]
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
:placeholder "--"
:data-name "m4"
:on-focus on-focus-m4
:on-change on-m4-change
:on-blur on-blur
:nillable true
:value m4}]])]))
(mf/defc margin-section*
{::mf/private true
::mf/expect-props #{:value :type :on-type-change :on-change}}
::mf/expect-props #{:value :type :on-type-change :on-change :applied-tokens :ids}}
[{:keys [type on-type-change] :as props}]
(let [type (d/nilv type :simple)
on-blur (mf/use-fn #(select-margins false false false false))
@@ -292,8 +534,187 @@
:label "Align self end"
:value "end"}]}])
(mf/defc layout-size-constraints*
{::mf/private true
::mf/expect-props #{:value :applied-tokens :ids :v-sizing}}
[{:keys [values v-sizing ids applied-tokens] :as props}]
(let [token-numeric-inputs
(features/use-feature "tokens/numeric-input")
min-w (get values :layout-item-min-w)
max-w (get values :layout-item-max-w)
min-h (get values :layout-item-min-h)
max-h (get values :layout-item-max-h)
applied-token-to-min-w (get applied-tokens :layout-item-min-w)
applied-token-to-max-w (get applied-tokens :layout-item-max-w)
applied-token-to-min-h (get applied-tokens :layout-item-min-h)
applied-token-to-max-h (get applied-tokens :layout-item-max-h)
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token attr]
(st/emit! (dwta/unapply-token {:token (first token)
:attributes #{attr}
:shape-ids ids}))))
on-size-change
(mf/use-fn
(mf/deps ids)
(fn [value attr]
(if (or (string? value) (int? value))
(st/emit! (dwsl/update-layout-child ids {attr value}))
(do
(st/emit!
(dwta/toggle-token {:token (first value)
:attrs #{attr}
:shape-ids ids}))))))
on-layout-item-min-w-change
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-min-w))
on-layout-item-max-w-change
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-max-w))
on-layout-item-min-h-change
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-min-h))
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)}
(when (= (:layout-item-h-sizing values) :fill)
[:div {:class (stl/css :horizontal-fill)}
(if token-numeric-inputs
[:> numeric-input-wrapper*
{:on-change on-layout-item-min-w-change
:on-detach on-detach-token
:class (stl/css :min-w-wrapper)
:min 0
:name :layout-item-min-w
:property (tr "workspace.options.layout-item.layout-item-min-w")
:text-icon "MIN W"
:nillable true
:applied-tokens {:layout-item-min-w applied-token-to-min-w}
:tooltip-class (stl/css :tooltip-wrapper)
:values {:layout-item-min-w min-w}}]
[:div {:class (stl/css :layout-item-min-w)
:title (tr "workspace.options.layout-item.layout-item-min-w")}
[:span {:class (stl/css :icon-text)} "MIN W"]
[:> deprecated-input/numeric-input*
{:class (stl/css :numeric-input)
:no-validate true
:min 0
:data-wrap true
:placeholder "--"
:data-attr "layout-item-min-w"
:on-focus dom/select-target
:on-change on-layout-item-min-w-change
:value (get values :layout-item-min-w)
:nillable true}]])
(if token-numeric-inputs
[:> numeric-input-wrapper*
{:on-change on-layout-item-max-w-change
:on-detach on-detach-token
:text-icon "MAX W"
:class (stl/css :max-w-wrapper)
:min 0
:name :layout-item-max-w
:property (tr "workspace.options.layout-item.layout-item-max-w")
:nillable true
:tooltip-class (stl/css :tooltip-wrapper)
:applied-tokens {:layout-item-max-w applied-token-to-max-w}
:values {:layout-item-max-w max-w}}]
[:div {:class (stl/css :layout-item-max-w)
:title (tr "workspace.options.layout-item.layout-item-max-w")}
[:span {:class (stl/css :icon-text)} "MAX W"]
[:> deprecated-input/numeric-input*
{:class (stl/css :numeric-input)
:no-validate true
:min 0
:data-wrap true
:placeholder "--"
:data-attr "layout-item-max-w"
:on-focus dom/select-target
:on-change on-layout-item-max-w-change
:value (get values :layout-item-max-w)
:nillable true}]])])
(when (= v-sizing :fill)
[:div {:class (stl/css :vertical-fill)}
(if token-numeric-inputs
[:> numeric-input-wrapper*
{:on-change on-layout-item-min-h-change
:on-detach on-detach-token
:text-icon "MIN H"
:class (stl/css :min-h-wrapper)
:min 0
:name :layout-item-min-h
:property (tr "workspace.options.layout-item.layout-item-min-h")
:nillable true
:tooltip-class (stl/css :tooltip-wrapper)
:applied-tokens {:layout-item-min-h applied-token-to-min-h}
:values {:layout-item-min-h min-h}}]
[:div {:class (stl/css :layout-item-min-h)
:title (tr "workspace.options.layout-item.layout-item-min-h")}
[:span {:class (stl/css :icon-text)} "MIN H"]
[:> deprecated-input/numeric-input*
{:class (stl/css :numeric-input)
:no-validate true
:min 0
:data-wrap true
:placeholder "--"
:data-attr "layout-item-min-h"
:on-focus dom/select-target
:on-change on-layout-item-min-h-change
:value (get values :layout-item-min-h)
:nillable true}]])
(if token-numeric-inputs
[:> numeric-input-wrapper*
{:on-change on-layout-item-max-h-change
:on-detach on-detach-token
:class (stl/css :max-h-wrapper)
:min 0
:text-icon "MAX H"
:name :layout-item-max-h
:property (tr "workspace.options.layout-item.layout-item-max-h")
:nillable true
:tooltip-class (stl/css :tooltip-wrapper)
:applied-tokens {:layout-item-max-h applied-token-to-max-h}
:values {:layout-item-max-h max-h}}]
[:div {:class (stl/css :layout-item-max-h)
:title (tr "workspace.options.layout-item.layout-item-max-h")}
[:span {:class (stl/css :icon-text)} "MAX H"]
[:> deprecated-input/numeric-input*
{:class (stl/css :numeric-input)
:no-validate true
:min 0
:data-wrap true
:placeholder "--"
:data-attr "layout-item-max-h"
:on-focus dom/select-target
:on-change on-layout-item-max-h-change
:value (get values :layout-item-max-h)
:nillable true}]])])]))
(mf/defc layout-item-menu
{::mf/memo #{:ids :values :type :is-layout-child? :is-grid-parent :is-flex-parent? :is-grid-layout? :is-flex-layout?}
{::mf/memo #{:ids :values :type :is-layout-child? :is-grid-parent :is-flex-parent? :is-grid-layout? :is-flex-layout? :applied-tokens}
::mf/props :obj}
[{:keys [ids values
^boolean is-layout-child?
@@ -301,7 +722,8 @@
^boolean is-grid-parent?
^boolean is-flex-parent?
^boolean is-flex-layout?
^boolean is-grid-layout?]}]
^boolean is-grid-layout?
applied-tokens]}]
(let [selection-parents* (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids))
selection-parents (mf/deref selection-parents*)
@@ -397,16 +819,7 @@
(fn [value]
(st/emit! (dwsl/update-layout-child ids {:layout-item-v-sizing (keyword value)}))))
;; Size and position
on-size-change
(mf/use-fn
(mf/deps ids)
(fn [value event]
(let [attr (-> (dom/get-current-target event)
(dom/get-data "attr")
(keyword))]
(st/emit! (dwsl/update-layout-child ids {attr value})))))
;; Position
on-change-position
(mf/use-fn
(mf/deps ids)
@@ -423,7 +836,8 @@
(fn [value]
(st/emit! (dwsl/update-layout-child ids {:layout-item-z-index value}))))]
[:div {:class (stl/css :element-set)}
[:section {:class (stl/css :element-set)
:aria-label "layout item menu"}
[:div {:class (stl/css :element-title)}
[:> title-bar* {:collapsable has-content?
:collapsed (not open?)
@@ -483,74 +897,13 @@
[:> margin-section* {:value (:layout-item-margin values)
:type (:layout-item-margin-type values)
:on-type-change on-margin-type-change
:applied-tokens applied-tokens
:ids ids
:on-change on-margin-change}])
(when (or (= h-sizing :fill)
(= v-sizing :fill))
[:div {:class (stl/css :advanced-options)}
(when (= (:layout-item-h-sizing values) :fill)
[:div {:class (stl/css :horizontal-fill)}
[:div {:class (stl/css :layout-item-min-w)
:title (tr "workspace.options.layout-item.layout-item-min-w")}
[:span {:class (stl/css :icon-text)} "MIN W"]
[:> deprecated-input/numeric-input*
{:class (stl/css :numeric-input)
:no-validate true
:min 0
:data-wrap true
:placeholder "--"
:data-attr "layout-item-min-w"
:on-focus dom/select-target
:on-change on-size-change
:value (get values :layout-item-min-w)
:nillable true}]]
[:div {:class (stl/css :layout-item-max-w)
:title (tr "workspace.options.layout-item.layout-item-max-w")}
[:span {:class (stl/css :icon-text)} "MAX W"]
[:> deprecated-input/numeric-input*
{:class (stl/css :numeric-input)
:no-validate true
:min 0
:data-wrap true
:placeholder "--"
:data-attr "layout-item-max-w"
:on-focus dom/select-target
:on-change on-size-change
:value (get values :layout-item-max-w)
:nillable true}]]])
(when (= v-sizing :fill)
[:div {:class (stl/css :vertical-fill)}
[:div {:class (stl/css :layout-item-min-h)
:title (tr "workspace.options.layout-item.layout-item-min-h")}
[:span {:class (stl/css :icon-text)} "MIN H"]
[:> deprecated-input/numeric-input*
{:class (stl/css :numeric-input)
:no-validate true
:min 0
:data-wrap true
:placeholder "--"
:data-attr "layout-item-min-h"
:on-focus dom/select-target
:on-change on-size-change
:value (get values :layout-item-min-h)
:nillable true}]]
[:div {:class (stl/css :layout-item-max-h)
:title (tr "workspace.options.layout-item.layout-item-max-h")}
[:span {:class (stl/css :icon-text)} "MAX H"]
[:> deprecated-input/numeric-input*
{:class (stl/css :numeric-input)
:no-validate true
:min 0
:data-wrap true
:placeholder "--"
:data-attr "layout-item-max-h"
:on-focus dom/select-target
:on-change on-size-change
:value (get values :layout-item-max-h)
:nillable true}]]])])])]))
[:> layout-size-constraints* {:ids ids
:values values
:applied-tokens applied-tokens
:v-sizing v-sizing}])])]))

View File

@@ -6,17 +6,21 @@
@use "refactor/common-refactor.scss" as deprecated;
@use "../../../sidebar/common/sidebar.scss" as sidebar;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/typography.scss" as *;
@use "ds/_utils.scss" as *;
.element-set {
margin: 0;
}
.title-spacing-layout-element {
margin: 0 0 deprecated.$s-4 0;
margin: 0 0 var(--sp-xs) 0;
}
.title-spacing-empty {
padding-left: deprecated.$s-2;
padding-inline-start: var(--sp-xxs);
}
.flex-element-menu {
@@ -35,8 +39,8 @@
}
.z-index-wrapper {
@include use-typography("body-small");
@extend .input-element;
@include deprecated.bodySmallTypography;
grid-column: 6 / span 3;
}
@@ -55,7 +59,7 @@
}
.position-options {
width: 100%;
inline-size: 100%;
grid-column: 1 / span 5;
}
@@ -75,7 +79,7 @@
.vertical-margin,
.horizontal-margin {
@extend .input-element;
@include deprecated.bodySmallTypography;
@include use-typography("body-small");
}
.vertical-margin {
grid-column: 1;
@@ -85,6 +89,16 @@
}
}
.vertical-margin-wrapper {
grid-column: 1;
--dropdown-width: var(--7-columns-dropdown-width);
}
.horizontal-margin-wrapper {
grid-column: 2;
--dropdown-width: var(--7-columns-dropdown-width);
}
.margin-multiple {
display: grid;
grid-template-columns: subgrid;
@@ -96,25 +110,33 @@
.left-margin,
.right-margin {
@extend .input-element;
@include deprecated.bodySmallTypography;
@include use-typography("body-small");
}
.top-margin {
.top-margin,
.top-margin-wrapper {
--dropdown-width: var(--7-columns-dropdown-width);
grid-column: 1;
grid-row: 1;
}
.bottom-margin {
.bottom-margin,
.bottom-margin-wrapper {
--dropdown-width: var(--7-columns-dropdown-width);
grid-column: 2;
grid-row: 1;
}
.left-margin {
.left-margin,
.left-margin-wrapper {
--dropdown-width: var(--7-columns-dropdown-width);
grid-column: 1;
grid-row: 2;
}
.right-margin {
.right-margin,
.right-margin-wrapper {
--dropdown-width: var(--7-columns-dropdown-width);
grid-column: 2;
grid-row: 2;
}
@@ -140,14 +162,18 @@
.layout-item-max-w,
.layout-item-max-h {
@extend .input-element;
@include deprecated.bodySmallTypography;
@include use-typography("body-small");
.icon-text {
justify-content: flex-start;
width: deprecated.$s-80;
padding-top: deprecated.$s-2;
inline-size: px2rem(80);
padding-block-start: var(--sp-xxs);
}
}
.inputs-wrapper {
grid-column: 1 / span 2;
}
.tooltip-wrapper {
inline-size: 100%;
}

View File

@@ -114,6 +114,7 @@
:is-layout-child? true
:is-flex-parent? is-flex-parent?
:is-grid-parent? is-grid-parent?
:applied-tokens applied-tokens
:shape shape}])
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)

View File

@@ -113,6 +113,7 @@
:is-layout-container? false
:is-flex-parent? is-flex-parent?
:is-grid-parent? is-grid-parent?
:applied-tokens applied-tokens
:shape shape}])
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)

View File

@@ -135,6 +135,7 @@
:is-flex-layout? is-flex-layout?
:is-grid-layout? is-grid-layout?
:is-layout-child? is-layout-child?
:applied-tokens applied-tokens
:is-layout-container? is-layout-container?
:shape shape}])

View File

@@ -139,6 +139,7 @@
:is-layout-container? false
:is-flex-parent? is-flex-parent?
:is-grid-parent? is-grid-parent?
:applied-tokens applied-tokens
:values layout-item-values}])
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)

View File

@@ -409,7 +409,7 @@
[layout-container-ids layout-container-values layout-container-tokens]
(get-attrs shapes objects :layout-container)
[layout-item-ids layout-item-values {}]
[layout-item-ids layout-item-values layout-item-tokens]
(get-attrs shapes objects :layout-item)
components
@@ -471,6 +471,7 @@
:is-layout-container? all-flex-layout-container?
:is-flex-parent? is-flex-parent?
:is-grid-parent? is-grid-parent?
:applied-tokens layout-item-tokens
:values layout-item-values}])
(when-not (or (empty? constraint-ids) ^boolean is-layout-child?)

View File

@@ -113,6 +113,7 @@
:is-layout-container? false
:is-flex-parent? is-flex-parent?
:is-grid-parent? is-grid-parent?
:applied-tokens applied-tokens
:shape shape}])
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)

View File

@@ -112,6 +112,7 @@
:values layout-item-values
:is-layout-child? true
:is-flex-parent? is-flex-parent?
:applied-tokens applied-tokens
:is-grid-parent? is-grid-parent?
:shape shape}])

View File

@@ -180,6 +180,7 @@
:is-layout-child? true
:is-flex-parent? is-flex-parent?
:is-grid-parent? is-grid-parent?
:applied-tokens applied-tokens
:shape shape}])
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)

View File

@@ -154,6 +154,7 @@
:is-layout-child? true
:is-flex-parent? is-flex-parent?
:is-grid-parent? is-grid-parent?
:applied-tokens applied-tokens
:shape shape}])
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)

View File

@@ -46,7 +46,7 @@
{::mf/private true}
[{:keys [tokens-lib selected-token-set-id]}]
(let [selected-token-set
(mf/with-memo [tokens-lib selected-token-set-id]
(mf/with-memo [tokens-lib]
(when selected-token-set-id
(some-> tokens-lib (ctob/get-set selected-token-set-id))))
@@ -62,20 +62,18 @@
[:div {:class (stl/css :sets-header-container)}
[:> text* {:as "span"
:typography "headline-small"
:class (stl/css :sets-header)
:data-testid "active-token-set-title"}
:class (stl/css :sets-header)}
(tr "workspace.tokens.tokens-section-title" (ctob/get-name selected-token-set))]
(when (and (some? selected-token-set-id)
(not (token-set-active? (ctob/get-name selected-token-set))))
[:div {:class (stl/css :sets-header-status) :title (tr "workspace.tokens.inactive-set-description")}
[:div {:class (stl/css :sets-header-status) :title (tr "workspace.tokens.inactive-set-description")}
;; NOTE: when no set in tokens-lib, the selected-token-set-id
;; will be `nil`, so for properly hide the inactive message we
;; check that at least `selected-token-set-id` has a value
(when (and (some? selected-token-set-id)
(not (token-set-active? (ctob/get-name selected-token-set))))
[:*
[:> icon* {:class (stl/css :sets-header-status-icon) :icon-id i/eye-off}]
[:> text* {:as "span" :typography "body-small" :class (stl/css :sets-header-status-text)}
(tr "workspace.tokens.inactive-set")]]])]))
(tr "workspace.tokens.inactive-set")]])]]))
(mf/defc tokens-section*
{::mf/private true}
@@ -160,7 +158,7 @@
[:& token-context-menu]
[:> token-node-context-menu* {:on-delete-node delete-node}]
[:> selected-set-info* {:tokens-lib tokens-lib
[:& selected-set-info* {:tokens-lib tokens-lib
:selected-token-set-id selected-token-set-id}]
(for [type filled-group]

View File

@@ -28,6 +28,7 @@
[app.main.ui.forms :as fc]
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
[app.main.ui.workspace.tokens.management.forms.validators :refer [default-validate-token]]
[app.main.ui.workspace.tokens.remapping-modal :as remapping-modal]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
@@ -181,33 +182,9 @@
(when (or (k/enter? e) (k/space? e))
(on-cancel e))))
on-remap-token
(mf/use-fn
(mf/deps token)
(fn [valid-token name old-name description]
(st/emit!
(dwtl/update-token (:id token)
{:name name
:value (:value valid-token)
:description description})
(remap/remap-tokens old-name name)
(dwtp/propagate-workspace-tokens)
(modal/hide!))))
on-rename-token
(mf/use-fn
(mf/deps token)
(fn [valid-token name description]
(st/emit!
(dwtl/update-token (:id token)
{:name name
:value (:value valid-token)
:description description})
(modal/hide!))))
on-submit
(mf/use-fn
(mf/deps validate-token token tokens token-type value-subfield type active-tab on-remap-token on-rename-token is-create)
(mf/deps validate-token token tokens token-type value-subfield type active-tab)
(fn [form _event]
(let [name (get-in @form [:clean-data :name])
path (str (d/name token-type) "." name)
@@ -225,15 +202,22 @@
file-data (dh/lookup-file-data state)
old-name (:name token)
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)]
references-count (remap/count-token-references file-data old-name)]
(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
:on-remap on-remap
:on-rename on-rename}))
(remapping-modal/show-remapping-modal
{:old-token-name old-name
:new-token-name name
:references-count references-count
:on-confirm (fn []
(st/emit!
(dwtl/update-token (:id token)
{:name name
:value (:value valid-token)
:description description})
(remap/remap-tokens old-name name)
(dwtp/propagate-workspace-tokens)
(modal/hide!)))
:on-cancel #(modal/hide!)})
(st/emit!
(if is-create
(dwtl/create-token (ctob/make-token {:name name

View File

@@ -295,8 +295,7 @@
errors?
[:> icon*
{:icon-id i/broken-link
:class (stl/css :token-pill-icon)
:aria-label (tr "workspace.tokens.missing-reference")}]
:class (stl/css :token-pill-icon)}]
color
[:> swatch* {:background color

View File

@@ -11,15 +11,22 @@
[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 :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.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
[rumext.v2 :as mf]))
(defn show-remapping-modal
"Show the token remapping confirmation modal"
[{:keys [old-token-name new-token-name references-count on-confirm on-cancel]}]
(let [props {:old-token-name old-token-name
:new-token-name new-token-name
:references-count references-count
:on-confirm on-confirm
:on-cancel on-cancel}]
(st/emit! (modal/show :tokens/remapping-confirmation props))))
(defn hide-remapping-modal
"Hide the token remapping confirmation modal"
[]
@@ -27,73 +34,73 @@
;; Remapping Modal Component
(mf/defc token-remapping-modal
{::mf/register modal/components
{::mf/wrap-props false
::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)
[{:keys [old-token-name new-token-name references-count on-confirm on-cancel]}]
(let [remapping-in-progress* (mf/use-state false)
remapping-in-progress? (deref remapping-in-progress*)
;; Remap logic on confirm
confirm-remap
on-confirm-remap
(mf/use-fn
(mf/deps on-remap remap-modal)
(fn []
(mf/deps on-confirm remapping-in-progress*)
(fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(reset! remapping-in-progress* true)
;; Call shared remapping logic
(let [old-token-name (:old-token-name remap-modal)
(let [state @st/state
remap-modal (:remap-modal state)
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))))
(when (fn? on-confirm)
(on-confirm))))
rename-token
on-cancel-remap
(mf/use-fn
(mf/deps on-rename)
(fn []
(when (fn? on-rename)
(on-rename))))
cancel-action
(mf/use-fn
(fn []
(hide-remapping-modal)))
;; Close modal on Escape key if not in progress
on-key-down
(mf/use-fn
(mf/deps cancel-action)
(fn [event]
(when (kbd/enter? event)
(cancel-action))))]
[:div {:class (stl/css :modal-overlay)
:on-key-down on-key-down
:role "alertdialog"
:aria-modal "true"
:aria-labelledby "modal-title"}
(mf/deps on-cancel)
(fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(modal/hide!)
(when (fn? on-cancel)
(on-cancel))))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog)
:data-testid "token-remapping-modal"}
[:> icon-button* {:on-click cancel-action
:class (stl/css :close-btn)
:icon i/close
:variant "action"
:aria-label (tr "labels.close")}]
[:div {:class (stl/css :modal-header)}
[:> heading* {:level 2
:id "modal-title"
:typography "headline-large"
:typography "headline-medium"
:class (stl/css :modal-title)}
(tr "workspace.tokens.remap-token-references-title" old-token-name new-token-name)]]
(tr "workspace.tokens.remap-token-references")]]
[:div {:class (stl/css :modal-content)}
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-effects")]
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-time")]]
[:> heading* {:level 3
:typography "title-medium"
:class (stl/css :modal-msg)}
(tr "workspace.tokens.renaming-token-from-to" old-token-name new-token-name)]
[:div {:class (stl/css :modal-scd-msg)}
(if (> references-count 0)
(tr "workspace.tokens.references-found" references-count)
(tr "workspace.tokens.no-references-found"))]
(when remapping-in-progress?
[:> context-notification*
{:level :info
:appearance :ghost}
(tr "workspace.tokens.remapping-in-progress")])]
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
[:> button* {:on-click rename-token
[:> button* {:on-click on-cancel-remap
:type "button"
:variant "secondary"}
(tr "workspace.tokens.not-remap")]
[:> button* {:on-click confirm-remap
:variant "secondary"
:disabled remapping-in-progress?}
(tr "labels.cancel")]
[:> button* {:on-click on-confirm-remap
:type "button"
:variant "primary"}
(tr "workspace.tokens.remap")]]]]]))
:variant "primary"
:disabled remapping-in-progress?}
(if (> references-count 0)
(tr "workspace.tokens.remap-and-rename")
(tr "workspace.tokens.rename-only"))]]]]]))

View File

@@ -10,9 +10,6 @@
@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;
@@ -20,22 +17,16 @@
position: fixed;
inset-inline-start: 0;
inset-block-start: 0;
block-size: 100%;
inline-size: 100%;
height: 100%;
width: 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;
block-size: 100%;
max-inline-size: 32rem;
max-block-size: unset;
width: 100%;
max-width: 32rem;
max-height: unset;
user-select: none;
position: relative;
}
@@ -54,7 +45,11 @@
.modal-content {
@include t.use-typography("body-large");
color: var(--modal-text-foreground-color);
margin-block-end: var(--sp-xxl);
padding: var(--sp-xxl) 0;
display: flex;
flex-direction: column;
gap: var(--sp-l);
}
.modal-footer {

View File

@@ -225,83 +225,118 @@
get-expr)])
(when set-expr
[schema-sym schema-n
(concat
(when (and schema-n (not (list? schema-n)))
[coercer-sym `(sm/coercer ~schema-n)])
coercer-sym `(if (and (some? ~schema-sym)
(not (fn? ~schema-sym)))
(sm/coercer ~schema-sym)
nil)
(when (and schema-n (list? schema-n))
[schema-sym schema-n])
decode-sym decode-expr
[decode-sym decode-expr]
(make-sym pname "set-fn")
`(fn [~val-sym]
(let [~this-sym (~'js* "this")
~fn-sym ~set-expr
;; We only emit schema and coercer bindings if
;; schema-n is provided
~@(if (some? schema-n)
[schema-sym `(if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
coercer-sym `(if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
val-sym (if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
val-sym `(~coercer-sym ~val-sym)]
[])]
~(if this?
`(.call ~fn-sym ~this-sym ~this-sym ~val-sym)
`(.call ~fn-sym ~this-sym ~val-sym))))])
[(make-sym pname "set-fn")
(if this?
`(fn [~val-sym]
(let [~@(if (and schema-n (list? schema-n))
[schema-sym `(~schema-sym ~val-sym)
coercer-sym `(sm/coercer ~schema-sym)]
[])
~this-sym (~'js* "this")
~fn-sym ~set-expr
~val-sym ~(if schema-n
(if decode-expr
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
val-sym)
~val-sym ~(if schema-n
`(~coercer-sym ~val-sym)
val-sym)]
(.call ~fn-sym ~this-sym ~this-sym ~val-sym)))
`(fn [~val-sym]
(let [~@(if (and schema-n (list? schema-n))
[schema-sym `(~schema-sym ~val-sym)
coercer-sym `(sm/coercer ~schema-sym)]
[])
~this-sym (~'js* "this")
~fn-sym ~set-expr
~val-sym ~(if schema-n
(if decode-expr
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
val-sym)
~val-sym ~(if schema-n
`(~coercer-sym ~val-sym)
val-sym)]
(.call ~fn-sym ~this-sym ~val-sym))))]))
(when fn-expr
[schema-sym (or schema-n schema-1)
coercer-sym `(if (and (some? ~schema-sym)
(not (fn? ~schema-sym)))
(sm/coercer ~schema-sym)
nil)
decode-sym decode-expr
(concat
(cond
(and schema-n (not (list? schema-n)))
[coercer-sym `(sm/coercer ~schema-n)]
(make-sym pname "get-fn")
`(fn []
(let [~this-sym (~'js* "this")
~fn-sym ~fn-expr
~fn-sym ~(if this?
`(.bind ~fn-sym ~this-sym ~this-sym)
`(.bind ~fn-sym ~this-sym))
(and schema-n (list? schema-n))
[schema-sym schema-n]
;; We only emit schema and coercer bindings if
;; schema-n or schema-1 is provided
~@(if (or schema-n schema-1)
[fn-sym `(fn* [~@(if schema-1 [val-sym] [])]
(let [~@(if schema-n
[val-sym `(into-array (cljs.core/js-arguments))]
[])
~val-sym ~(if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
(and schema-1 (not (list? schema-1)))
[coercer-sym `(sm/coercer ~schema-1)]
~schema-sym (if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
(and schema-1 (list? schema-1))
[schema-sym schema-1])
~coercer-sym (if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
[decode-sym decode-expr]
~val-sym (~coercer-sym ~val-sym)]
~(if schema-1
`(~fn-sym ~val-sym)
`(apply ~fn-sym ~val-sym))))]
[])]
~(if wrap
`(~wrap-sym ~fn-sym)
fn-sym)))]))))))]
[(make-sym pname "get-fn")
`(fn []
(let [~this-sym (~'js* "this")
~fn-sym ~fn-expr
~fn-sym ~(if this?
`(.bind ~fn-sym ~this-sym ~this-sym)
`(.bind ~fn-sym ~this-sym))
~@(if schema-1
[fn-sym `(fn* [~val-sym]
(let [~val-sym
~(if decode-expr
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
~@(if (or (and schema-n (list? schema-n))
(and schema-1 (list? schema-1)))
[schema-sym `(~schema-sym ~val-sym)
coercer-sym `(sm/coercer ~schema-sym)]
[])
~val-sym
(~coercer-sym ~val-sym)]
(~fn-sym ~val-sym)))]
[])
~@(if schema-n
[fn-sym `(fn* []
(let [~val-sym
(into-array (cljs.core/js-arguments))
~val-sym
~(if decode-expr
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
~@(if (or (and schema-n (list? schema-n))
(and schema-1 (list? schema-1)))
[schema-sym `(~schema-sym ~val-sym)
coercer-sym `(sm/coercer ~schema-sym)]
[])
~val-sym
(~coercer-sym ~val-sym)]
(apply ~fn-sym ~val-sym)))]
[])
~@(if wrap
[fn-sym `(~wrap-sym ~fn-sym)]
[])]
~fn-sym))])))))))]
`(let [~target-sym ~rsym
~@bindings]

View File

@@ -8,7 +8,6 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.files.repair :as cfr]
[app.common.files.validate :as cfv]
[app.common.json :as json]
@@ -457,8 +456,3 @@
(defn ^:export network-averages
[]
(.log js/console (clj->js @http/network-averages)))
(defn print-last-exception
[]
(some-> errors/last-exception ex/print-throwable))

View File

@@ -433,9 +433,6 @@ msgstr "(copy)"
msgid "dashboard.create-new-team"
msgstr "Create new team"
msgid "dashboard.create-new-org"
msgstr "Create new org"
#: src/app/main/ui/workspace/main_menu.cljs:661
msgid "dashboard.create-version-menu"
msgstr "Pin this version"
@@ -1466,9 +1463,6 @@ msgstr ""
msgid "errors.generic"
msgstr "Something wrong has happened."
msgid "errors.unexpected-exception"
msgstr "Unexpected exception: %s"
#: src/app/main/errors.cljs:200
msgid "errors.internal-assertion-error"
msgstr "Internal Assertion Error"
@@ -6719,19 +6713,19 @@ msgstr "Advanced options"
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:543
msgid "workspace.options.layout-item.layout-item-max-h"
msgstr "Max.Height"
msgstr "Max height"
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:510
msgid "workspace.options.layout-item.layout-item-max-w"
msgstr "Max.Width"
msgstr "Max width"
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:527
msgid "workspace.options.layout-item.layout-item-min-h"
msgstr "Min.Height"
msgstr "Min height"
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:494
msgid "workspace.options.layout-item.layout-item-min-w"
msgstr "Min.Width"
msgstr "Min width"
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs
#, unused
@@ -8075,10 +8069,6 @@ msgid "workspace.tokens.missing-references"
msgstr "Missing token references: "
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
msgid "workspace.tokens.missing-reference"
msgstr "Missing reference"
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:299
msgid "workspace.tokens.more-options"
msgstr "Right click to see options"
@@ -8169,29 +8159,36 @@ msgstr "Enter a token shadow alias"
msgid "workspace.tokens.reference-error"
msgstr "Reference Errors: "
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
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:86
msgid "workspace.tokens.references-found"
msgstr "%s references found in your design"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:105
msgid "workspace.tokens.remap-and-rename"
msgstr "Remap & Rename"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
#, unused
msgid "workspace.tokens.remap-explanation"
msgstr ""
"All references to this token will be automatically updated to use the new "
"name."
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
msgid "workspace.tokens.remap-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:78
msgid "workspace.tokens.remap-warning-time"
msgstr "This action could take a while."
msgid "workspace.tokens.remap-token-references"
msgstr "Remap Token References"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:92
msgid "workspace.tokens.remapping-in-progress"
msgstr "Remapping token references..."
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:106
msgid "workspace.tokens.not-remap"
msgstr "Don't remap"
msgid "workspace.tokens.rename-only"
msgstr "Rename"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:105
msgid "workspace.tokens.remap"
msgstr "Remap tokens"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:83
msgid "workspace.tokens.renaming-token-from-to"
msgstr "Renaming token from '%s' to '%s'"
#: 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:271, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:445, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:169, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:304, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:225, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:332, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:432, src/app/main/ui/workspace/tokens/management/token_pill.cljs:121
#, fuzzy

View File

@@ -442,9 +442,6 @@ msgstr "(copia)"
msgid "dashboard.create-new-team"
msgstr "Crear nuevo equipo"
msgid "dashboard.create-new-org"
msgstr "Crear nueva organización"
#: src/app/main/ui/workspace/main_menu.cljs:661
msgid "dashboard.create-version-menu"
msgstr "Guardar esta versión"
@@ -1462,9 +1459,6 @@ msgstr ""
msgid "errors.generic"
msgstr "Ha ocurrido algún error."
msgid "errors.unexpected-exception"
msgstr "Error inesperado: %s"
#: src/app/main/errors.cljs:200
msgid "errors.internal-assertion-error"
msgstr "Error interno de aserción"
@@ -7170,11 +7164,11 @@ msgstr "Ancho"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:535, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:552
msgid "workspace.options.x"
msgstr "eje X"
msgstr "Eje X"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:545, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:563
msgid "workspace.options.y"
msgstr "eje Y"
msgstr "Eje Y"
#: src/app/main/ui/workspace/viewport/path_actions.cljs:140
msgid "workspace.path.actions.add-node"
@@ -7997,10 +7991,6 @@ msgstr "Line height (multiplicador, px o %) o {alias}"
msgid "workspace.tokens.missing-references"
msgstr "Referencias de tokens no encontradas: "
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
msgid "workspace.tokens.missing-reference"
msgstr "Referencia no encontrada"
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
msgid "workspace.tokens.more-options"
msgstr "Click derecho para ver opciones"
@@ -8070,29 +8060,36 @@ msgstr "La referencia no es válida o no se encuentra en ningún set activo."
msgid "workspace.tokens.reference-error"
msgstr "Errores en referencias: "
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
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:86
msgid "workspace.tokens.references-found"
msgstr "%s referencias encontradas en tu diseño"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:105
msgid "workspace.tokens.remap-and-rename"
msgstr "Actualizar referencias y renombrar"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
#, unused
msgid "workspace.tokens.remap-explanation"
msgstr ""
"Todas las referencias a este token se actualizarán automáticamente para "
"usar el nuevo nombre."
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
msgid "workspace.tokens.remap-warning-effects"
msgstr "Esta acción actualizará todas las capas y referencias que usen el token antiguo"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
msgid "workspace.tokens.remap-warning-time"
msgstr "Este proceso puede durar un poco"
msgid "workspace.tokens.remap-token-references"
msgstr "Actualizar referencias de token"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:92
msgid "workspace.tokens.remapping-in-progress"
msgstr "Actualizando referencias de token..."
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:106
msgid "workspace.tokens.not-remap"
msgstr "No actualizar"
msgid "workspace.tokens.rename-only"
msgstr "Renombrar"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:105
msgid "workspace.tokens.remap"
msgstr "Actualizar tokens"
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:83
msgid "workspace.tokens.renaming-token-from-to"
msgstr "Renombrando el token de '%s' a '%s'"
#: 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:271, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:445, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:169, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:304, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:225, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:332, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:432, src/app/main/ui/workspace/tokens/management/token_pill.cljs:121
#, fuzzy