mirror of
https://github.com/penpot/penpot.git
synced 2026-02-23 10:17:35 -05:00
Compare commits
6 Commits
eva-refact
...
eva-create
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47c36b24ac | ||
|
|
b24bcccda6 | ||
|
|
0739a67493 | ||
|
|
f04356d3cf | ||
|
|
9db0d250c5 | ||
|
|
f78c76ffde |
@@ -87,10 +87,6 @@
|
||||
[:map
|
||||
[:valid ::sm/boolean]])
|
||||
|
||||
(def ^:private schema:connectivity
|
||||
[:map
|
||||
[:licenses ::sm/boolean]])
|
||||
|
||||
(defn- get-team-org
|
||||
[cfg {:keys [team-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
@@ -101,11 +97,6 @@
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user params)))
|
||||
|
||||
(defn- get-connectivity
|
||||
[cfg params]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get (str baseuri "/api/connectivity") schema:connectivity params)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; INITIALIZATION
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -114,8 +105,7 @@
|
||||
[_ cfg]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
{:get-team-org (partial get-team-org cfg)
|
||||
:is-valid-user (partial is-valid-user cfg)
|
||||
:connectivity (partial get-connectivity cfg)}))
|
||||
:is-valid-user (partial is-valid-user cfg)}))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; UTILS
|
||||
@@ -138,7 +128,3 @@
|
||||
(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))))
|
||||
|
||||
(defn connectivity
|
||||
[cfg]
|
||||
(call cfg :connectivity {}))
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
'app.rpc.commands.ldap
|
||||
'app.rpc.commands.management
|
||||
'app.rpc.commands.media
|
||||
'app.rpc.commands.nitrate
|
||||
'app.rpc.commands.profile
|
||||
'app.rpc.commands.projects
|
||||
'app.rpc.commands.search
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
(ns app.rpc.commands.nitrate
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]))
|
||||
|
||||
|
||||
(def schema:connectivity
|
||||
[:map {:title "nitrate-connectivity"}
|
||||
[:licenses ::sm/boolean]])
|
||||
|
||||
(sv/defmethod ::get-nitrate-connectivity
|
||||
{::rpc/auth false
|
||||
::doc/added "1.18"
|
||||
::sm/params [:map]
|
||||
::sm/result schema:connectivity}
|
||||
[cfg _params]
|
||||
(nitrate/connectivity cfg))
|
||||
@@ -122,6 +122,7 @@
|
||||
;; Only for developtment.
|
||||
:tiered-file-data-storage
|
||||
:token-base-font-size
|
||||
:token-combobox
|
||||
:token-color
|
||||
:token-shadow
|
||||
:token-tokenscript
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WorkspacePage } from "../../pages/WorkspacePage";
|
||||
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
import {
|
||||
setupTokensFileRender,
|
||||
setupTypographyTokensFileRender,
|
||||
setupEmptyTokensFile,
|
||||
setupTokensFile,
|
||||
setupTypographyTokensFile,
|
||||
unfoldTokenTree,
|
||||
} from "./helpers";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WasmWorkspacePage.init(page);
|
||||
await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-design-tokens-v1"]);
|
||||
await WorkspacePage.init(page);
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||
});
|
||||
|
||||
test.describe("Tokens: Apply token", () => {
|
||||
test("User applies color token to a shape", async ({ page }) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
@@ -44,7 +44,9 @@ test.describe("Tokens: Apply token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page, {
|
||||
flags: ["enable-token-combobox", "enable-feature-token-input"],
|
||||
});
|
||||
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
@@ -83,7 +85,9 @@ test.describe("Tokens: Apply token", () => {
|
||||
await brTokenPillSM.click();
|
||||
|
||||
// Change token from dropdown
|
||||
const brTokenOptionXl = borderRadiusSection.getByLabel("borderRadius.xl");
|
||||
const brTokenOptionXl = borderRadiusSection
|
||||
.getByRole("option", { name: "borderRadius.xl" })
|
||||
.getByLabel("borderRadius.xl");
|
||||
await expect(brTokenOptionXl).toBeVisible();
|
||||
await brTokenOptionXl.click();
|
||||
|
||||
@@ -105,7 +109,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
@@ -169,7 +173,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
|
||||
test("User applies typography token to a text shape", async ({ page }) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTypographyTokensFileRender(page);
|
||||
await setupTypographyTokensFile(page);
|
||||
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
@@ -203,7 +207,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
tokensSidebar,
|
||||
workspacePage,
|
||||
tokenContextMenuForToken,
|
||||
} = await setupTokensFileRender(page, { flags: ["enable-token-shadow"] });
|
||||
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
@@ -489,7 +493,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Unfolds dimensions on token panel
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
@@ -518,7 +522,9 @@ test.describe("Tokens: Apply token", () => {
|
||||
await dimensionSMTokenPill.nth(1).click();
|
||||
|
||||
// Change token from dropdown
|
||||
const dimensionTokenOptionXl = measuresSection.getByLabel("dimension.xl");
|
||||
const dimensionTokenOptionXl = measuresSection.getByRole("option", {
|
||||
name: "dimension.xl",
|
||||
});
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
@@ -540,7 +546,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Unfolds dimensions on token panel
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
@@ -572,7 +578,9 @@ test.describe("Tokens: Apply token", () => {
|
||||
await dimensionSMTokenPill.click();
|
||||
|
||||
// Change token from dropdown
|
||||
const dimensionTokenOptionXl = measuresSection.getByLabel("dimension.xl");
|
||||
const dimensionTokenOptionXl = measuresSection.getByRole("option", {
|
||||
name: "dimension.xl",
|
||||
});
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
@@ -594,7 +602,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Unfolds dimensions on token panel
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
@@ -626,7 +634,9 @@ test.describe("Tokens: Apply token", () => {
|
||||
await dimensionSMTokenPill.click();
|
||||
|
||||
// Change token from dropdown
|
||||
const dimensionTokenOptionXl = measuresSection.getByLabel("dimension.xl");
|
||||
const dimensionTokenOptionXl = measuresSection.getByRole("option", {
|
||||
name: "dimension.xl",
|
||||
});
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
@@ -648,7 +658,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Unfolds dimensions on token panel
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
@@ -681,8 +691,9 @@ test.describe("Tokens: Apply token", () => {
|
||||
await dimensionXSTokenPill.click();
|
||||
|
||||
// Change token from dropdown
|
||||
const dimensionTokenOptionXl =
|
||||
borderRadiusSection.getByLabel("dimension.xl");
|
||||
const dimensionTokenOptionXl = borderRadiusSection.getByRole("option", {
|
||||
name: "dimension.xl",
|
||||
});
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
@@ -701,7 +712,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
});
|
||||
|
||||
test("User applies stroke width token to a shape", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page, {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
// Set up
|
||||
@@ -751,7 +762,9 @@ test.describe("Tokens: Apply token", () => {
|
||||
});
|
||||
await tokenDropdown.click();
|
||||
|
||||
const widthOptionSmall = firstStrokeRow.getByLabel("width-small");
|
||||
const widthOptionSmall = firstStrokeRow.getByRole("option", {
|
||||
name: "width-small",
|
||||
});
|
||||
await expect(widthOptionSmall).toBeVisible();
|
||||
await widthOptionSmall.click();
|
||||
const StrokeWidthPillSmall = firstStrokeRow.getByRole("button", {
|
||||
@@ -761,7 +774,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
});
|
||||
|
||||
test("User applies margin token to a shape", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page, {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
// Set up
|
||||
@@ -853,7 +866,7 @@ test.describe("Tokens: Detach token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
import { WorkspacePage } from "../../pages/WorkspacePage";
|
||||
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
|
||||
import {
|
||||
setupEmptyTokensFileRender,
|
||||
setupTokensFileRender,
|
||||
setupTypographyTokensFileRender,
|
||||
setupEmptyTokensFile,
|
||||
setupTokensFile,
|
||||
setupTypographyTokensFile,
|
||||
testTokenCreationFlow,
|
||||
unfoldTokenTree,
|
||||
} from "./helpers";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WasmWorkspacePage.init(page);
|
||||
await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-design-tokens-v1"]);
|
||||
await WorkspacePage.init(page);
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||
});
|
||||
|
||||
@@ -31,6 +30,89 @@ test.describe("Tokens - creation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("User creates border radius token with combobox", async ({ page }) => {
|
||||
const invalidValueError = "Invalid token value";
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
const selfReferenceError = "Token has self reference";
|
||||
const missingReferenceError = "Missing token references";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFile(page , {
|
||||
flags: ["enable-token-combobox", "enable-feature-token-input"],
|
||||
});
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
const addTokenButton = tokensTabPanel.getByRole("button", {
|
||||
name: `Add Token: Border Radius`,
|
||||
});
|
||||
|
||||
await addTokenButton.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
// Placeholder checks
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByPlaceholder(
|
||||
"Enter border radius token name",
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByPlaceholder(
|
||||
"Enter a value or alias with {alias}",
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
// Elements
|
||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
const valueField = tokensUpdateCreateModal.getByRole("combobox", {
|
||||
name: "Value",
|
||||
});
|
||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
|
||||
// Create first token
|
||||
await nameField.fill("my-token");
|
||||
await valueField.fill("1 + 2");
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Resolved value: 3"),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(submitButton).toBeEnabled();
|
||||
|
||||
await submitButton.click();
|
||||
|
||||
await expect(
|
||||
tokensTabPanel.getByRole("button", { name: "my-token" }),
|
||||
).toBeEnabled();
|
||||
|
||||
// Create second token referencing the first one using the combobox options
|
||||
await addTokenButton.click();
|
||||
|
||||
await nameField.fill("my-token-2");
|
||||
const toggleDropdownButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Open token list",
|
||||
});
|
||||
await toggleDropdownButton.click();
|
||||
const option = page.getByRole("option", { name: "my-token" });
|
||||
await expect(option).toBeVisible();
|
||||
await option.click();
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Resolved value: 3"),
|
||||
).toBeVisible();
|
||||
|
||||
await valueField.pressSequentially(" + 2");
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Resolved value: 5"),
|
||||
).toBeVisible();
|
||||
await valueField.pressSequentially(" + {");
|
||||
await option.click();
|
||||
await expect(
|
||||
tokensUpdateCreateModal.getByText("Resolved value: 8"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("User creates dimensions token", async ({ page }) => {
|
||||
await testTokenCreationFlow(page, {
|
||||
tokenLabel: "Dimensions",
|
||||
@@ -159,7 +241,7 @@ test.describe("Tokens - creation", () => {
|
||||
const selfReferenceError = "Token has self reference";
|
||||
const missingReferenceError = "Missing token references";
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "Add Token: Color" })
|
||||
@@ -321,7 +403,7 @@ test.describe("Tokens - creation", () => {
|
||||
const missingReferenceError = "Missing token references";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -466,7 +548,7 @@ test.describe("Tokens - creation", () => {
|
||||
const missingReferenceError = "Missing token references";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -602,7 +684,7 @@ test.describe("Tokens - creation", () => {
|
||||
const missingReferenceError = "Missing token references";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -718,7 +800,7 @@ test.describe("Tokens - creation", () => {
|
||||
const missingReferenceError = "Missing token references";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -832,7 +914,7 @@ test.describe("Tokens - creation", () => {
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] });
|
||||
await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -1013,7 +1095,7 @@ test.describe("Tokens - creation", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||
await setupTypographyTokensFileRender(page);
|
||||
await setupTypographyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
await tokensTabPanel
|
||||
@@ -1048,7 +1130,7 @@ test.describe("Tokens - creation", () => {
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] });
|
||||
await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -1233,7 +1315,7 @@ test.describe("Tokens - creation", () => {
|
||||
test("User creates typography token", async ({ page }) => {
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -1480,7 +1562,7 @@ test.describe("Tokens - creation", () => {
|
||||
|
||||
test("User adds typography token with reference", async ({ page }) => {
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||
await setupTypographyTokensFileRender(page);
|
||||
await setupTypographyTokensFile(page);
|
||||
|
||||
const newTokenTitle = "NewReference";
|
||||
|
||||
@@ -1530,7 +1612,7 @@ test.describe("Tokens - creation", () => {
|
||||
|
||||
test("User creates grouped color token", async ({ page }) => {
|
||||
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "Add Token: Color" })
|
||||
@@ -1563,7 +1645,7 @@ test.describe("Tokens - creation", () => {
|
||||
test("User cant create regular token with value missing", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokensUpdateCreateModal } = await setupEmptyTokensFileRender(page);
|
||||
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
await tokensTabPanel
|
||||
@@ -1590,7 +1672,7 @@ test.describe("Tokens - creation", () => {
|
||||
|
||||
test("User duplicate color token", async ({ page }) => {
|
||||
const { tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
@@ -1614,7 +1696,7 @@ test.describe("Tokens - creation", () => {
|
||||
|
||||
test("User creates grouped color token", async ({ page }) => {
|
||||
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
await tokensSidebar.getByRole("button", { name: "Add Token: Color" }).click();
|
||||
|
||||
@@ -1643,7 +1725,7 @@ test("User creates grouped color token", async ({ page }) => {
|
||||
});
|
||||
|
||||
test("User cant create regular token with value missing", async ({ page }) => {
|
||||
const { tokensUpdateCreateModal } = await setupEmptyTokensFileRender(page);
|
||||
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
await tokensTabPanel
|
||||
@@ -1670,7 +1752,7 @@ test("User cant create regular token with value missing", async ({ page }) => {
|
||||
|
||||
test("User duplicate color token", async ({ page }) => {
|
||||
const { tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
@@ -1696,7 +1778,7 @@ test.describe("Tokens tab - edition", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||
await setupTypographyTokensFileRender(page);
|
||||
await setupTypographyTokensFile(page);
|
||||
|
||||
await tokensSidebar
|
||||
.getByRole("button")
|
||||
@@ -1792,7 +1874,7 @@ test.describe("Tokens tab - edition", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
@@ -1828,7 +1910,7 @@ test.describe("Tokens tab - edition", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
await tokensTabPanel
|
||||
@@ -1883,7 +1965,7 @@ test.describe("Tokens tab - edition", () => {
|
||||
test.describe("Tokens tab - delete", () => {
|
||||
test("User delete color token", async ({ page }) => {
|
||||
const { tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
@@ -1903,7 +1985,7 @@ test.describe("Tokens tab - delete", () => {
|
||||
});
|
||||
|
||||
test("User removes node and all child tokens", async ({ page }) => {
|
||||
const { tokensSidebar } = await setupTokensFileRender(page);
|
||||
const { tokensSidebar } = await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WorkspacePage } from "../../pages/WorkspacePage";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
|
||||
const setupEmptyTokensFile = async (page, options = {}) => {
|
||||
const { flags = [] } = options;
|
||||
@@ -41,45 +40,6 @@ const setupEmptyTokensFile = async (page, options = {}) => {
|
||||
};
|
||||
};
|
||||
|
||||
const setupEmptyTokensFileRender = async (page, options = {}) => {
|
||||
const { flags = [] } = options;
|
||||
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
if (flags.length > 0) {
|
||||
await workspacePage.mockConfigFlags(flags);
|
||||
}
|
||||
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.mockRPC(
|
||||
"get-team?id=*",
|
||||
"workspace/get-team-tokens.json",
|
||||
);
|
||||
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
"workspace/update-file-create-rect.json",
|
||||
);
|
||||
|
||||
await workspacePage.goToWorkspace({
|
||||
fileId: "c7ce0794-0992-8105-8004-38f280443849",
|
||||
pageId: "66697432-c33d-8055-8006-2c62cc084cad",
|
||||
});
|
||||
|
||||
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||
await tokensTabButton.click();
|
||||
|
||||
return {
|
||||
workspacePage,
|
||||
tokenThemeUpdateCreateModal: workspacePage.tokenThemeUpdateCreateModal,
|
||||
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
|
||||
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
|
||||
tokenSetItems: workspacePage.tokenSetItems,
|
||||
tokensSidebar: workspacePage.tokensSidebar,
|
||||
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
|
||||
tokenContextMenuForSet: workspacePage.tokenContextMenuForSet,
|
||||
};
|
||||
};
|
||||
|
||||
const setupTokensFile = async (page, options = {}) => {
|
||||
const {
|
||||
file = "workspace/get-file-tokens.json",
|
||||
@@ -125,51 +85,6 @@ const setupTokensFile = async (page, options = {}) => {
|
||||
};
|
||||
};
|
||||
|
||||
const setupTokensFileRender = async (page, options = {}) => {
|
||||
const {
|
||||
file = "workspace/get-file-tokens.json",
|
||||
fileFragment = "workspace/get-file-fragment-tokens.json",
|
||||
flags = ["enable-feature-token-input"],
|
||||
} = options;
|
||||
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
if (flags.length > 0) {
|
||||
await workspacePage.mockConfigFlags(flags);
|
||||
}
|
||||
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.mockRPC(
|
||||
"get-team?id=*",
|
||||
"workspace/get-team-tokens.json",
|
||||
);
|
||||
await workspacePage.mockRPC(/get\-file\?/, file);
|
||||
await workspacePage.mockRPC(/get\-file\-fragment\?/, fileFragment);
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
"workspace/update-file-create-rect.json",
|
||||
);
|
||||
|
||||
await workspacePage.goToWorkspace({
|
||||
fileId: "c7ce0794-0992-8105-8004-38f280443849",
|
||||
pageId: "66697432-c33d-8055-8006-2c62cc084cad",
|
||||
});
|
||||
|
||||
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||
await tokensTabButton.click();
|
||||
|
||||
return {
|
||||
workspacePage,
|
||||
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
|
||||
tokenThemeUpdateCreateModal: workspacePage.tokenThemeUpdateCreateModal,
|
||||
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
|
||||
tokenSetItems: workspacePage.tokenSetItems,
|
||||
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
|
||||
tokensSidebar: workspacePage.tokensSidebar,
|
||||
tokenContextMenuForToken: workspacePage.tokenContextMenuForToken,
|
||||
tokenContextMenuForSet: workspacePage.tokenContextMenuForSet,
|
||||
};
|
||||
};
|
||||
|
||||
const setupTypographyTokensFile = async (page, options = {}) => {
|
||||
return setupTokensFile(page, {
|
||||
file: "workspace/get-file-typography-tokens.json",
|
||||
@@ -178,14 +93,6 @@ const setupTypographyTokensFile = async (page, options = {}) => {
|
||||
});
|
||||
};
|
||||
|
||||
const setupTypographyTokensFileRender = async (page, options = {}) => {
|
||||
return setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-typography-tokens.json",
|
||||
fileFragment: "workspace/get-file-fragment-typography-tokens.json",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
const testTokenCreationFlow = async (
|
||||
page,
|
||||
{
|
||||
@@ -207,7 +114,7 @@ const testTokenCreationFlow = async (
|
||||
const missingReferenceError = "Missing token references";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -352,11 +259,8 @@ const unfoldTokenTree = async (tokensTabPanel, type, tokenName) => {
|
||||
|
||||
export {
|
||||
setupEmptyTokensFile,
|
||||
setupEmptyTokensFileRender,
|
||||
setupTokensFile,
|
||||
setupTokensFileRender,
|
||||
setupTypographyTokensFile,
|
||||
setupTypographyTokensFileRender,
|
||||
testTokenCreationFlow,
|
||||
unfoldTokenTree,
|
||||
};
|
||||
|
||||
@@ -40,6 +40,34 @@ const createToken = async (page, type, name, textFieldName, value) => {
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
};
|
||||
|
||||
const createTokenCombobox = 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 valueFill = tokensUpdateCreateModal.getByRole("combobox", {
|
||||
name: textFieldName,
|
||||
});
|
||||
await valueFill.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"] });
|
||||
@@ -401,13 +429,21 @@ test.describe("Remapping Tokens", () => {
|
||||
test("User renames border radius token with alias references", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokensSidebar } = await setupTokensFile(page);
|
||||
const { tokensSidebar } = await setupTokensFile(page, {
|
||||
flags: ["enable-token-combobox", "enable-feature-token-input"],
|
||||
});
|
||||
|
||||
// Create base border radius token
|
||||
await createToken(page, "Border Radius", "base-radius", "Value", "4");
|
||||
await createTokenCombobox(
|
||||
page,
|
||||
"Border Radius",
|
||||
"base-radius",
|
||||
"Value",
|
||||
"4",
|
||||
);
|
||||
|
||||
// Create derived border radius token
|
||||
await createToken(
|
||||
await createTokenCombobox(
|
||||
page,
|
||||
"Border Radius",
|
||||
"card-radius",
|
||||
@@ -443,13 +479,21 @@ test.describe("Remapping Tokens", () => {
|
||||
tokensUpdateCreateModal,
|
||||
tokensSidebar,
|
||||
tokenContextMenuForToken,
|
||||
} = await setupTokensFile(page);
|
||||
} = await setupTokensFile(page, {
|
||||
flags: ["enable-token-combobox", "enable-feature-token-input"],
|
||||
});
|
||||
|
||||
// Create base border radius token
|
||||
await createToken(page, "Border Radius", "radius-sm", "Value", "4");
|
||||
await createTokenCombobox(
|
||||
page,
|
||||
"Border Radius",
|
||||
"radius-sm",
|
||||
"Value",
|
||||
"4",
|
||||
);
|
||||
|
||||
// Create derived border radius token
|
||||
await createToken(
|
||||
await createTokenCombobox(
|
||||
page,
|
||||
"Border Radius",
|
||||
"button-radius",
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
(ns app.main.data.nitrate
|
||||
(:require
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.repo :as rp]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn show-nitrate-popup
|
||||
[]
|
||||
(ptk/reify ::show-nitrate-popup
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! ::get-nitrate-connectivity {})
|
||||
(rx/map (fn [connectivity]
|
||||
(modal/show :nitrate-form (or connectivity {}))))))))
|
||||
@@ -369,7 +369,7 @@
|
||||
(dom/trigger-download filename blob))
|
||||
(fn [error]
|
||||
(js/console.error "error downloading font" error)
|
||||
(st/emit! (ntf/error (tr "errors.generic")))))))))
|
||||
(st/emit! (ntf/error (tr "errors.download-font")))))))))
|
||||
|
||||
on-delete-variant
|
||||
(mf/use-fn
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.nitrate :as dnt]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.refs :as refs]
|
||||
@@ -305,7 +304,7 @@
|
||||
(if (:nitrate-licence profile)
|
||||
;; TODO update when org creation route is ready
|
||||
(dom/open-new-window "/control-center/org/create")
|
||||
(st/emit! (dnt/show-nitrate-popup)))))]
|
||||
(st/emit! (modal/show :nitrate-form {})))))]
|
||||
|
||||
[:> dropdown-menu* props
|
||||
|
||||
@@ -551,7 +550,7 @@
|
||||
(if (:nitrate-licence profile)
|
||||
;; TODO update when org creation route is ready
|
||||
(dom/open-new-window "/control-center/org/create")
|
||||
(st/emit! (dnt/show-nitrate-popup)))))]
|
||||
(st/emit! (modal/show :nitrate-form {})))))]
|
||||
(if empty?
|
||||
[:div {:class (stl/css :nitrate-orgs-empty)}
|
||||
[:span {:class (stl/css :nitrate-penpot-icon)}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.nitrate :as dnt]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown-menu :refer [dropdown-menu-item*]]
|
||||
@@ -121,7 +121,7 @@
|
||||
(let [handle-click
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (dnt/show-nitrate-popup))))]
|
||||
(st/emit! (modal/show :nitrate-form {}))))]
|
||||
|
||||
;; TODO add translations for this texts when we have the definitive ones
|
||||
[:div {:class (stl/css :nitrate-banner :highlighted)}
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
(def ^:private schema:options-dropdown
|
||||
[:map
|
||||
[:ref {:optional true} fn?]
|
||||
[:class {:optional true} :string]
|
||||
[:wrapper-ref {:optional true} :any]
|
||||
[:on-click fn?]
|
||||
[:options [:vector schema:option]]
|
||||
[:selected {:optional true} :any]
|
||||
@@ -83,6 +85,7 @@
|
||||
:name name
|
||||
:resolved (get option :resolved-value)
|
||||
:ref ref
|
||||
:role "option"
|
||||
:focused (= id focused)
|
||||
:on-click on-click}]
|
||||
|
||||
@@ -94,6 +97,7 @@
|
||||
:aria-label (get option :aria-label)
|
||||
:icon (get option :icon)
|
||||
:ref ref
|
||||
:role "option"
|
||||
:focused (= id focused)
|
||||
:dimmed (true? (:dimmed option))
|
||||
:on-click on-click}]))))
|
||||
@@ -101,15 +105,16 @@
|
||||
|
||||
(mf/defc options-dropdown*
|
||||
{::mf/schema schema:options-dropdown}
|
||||
[{:keys [ref on-click options selected focused empty-to-end align] :rest props}]
|
||||
[{:keys [ref on-click options selected focused empty-to-end align wrapper-ref class] :rest props}]
|
||||
(let [align
|
||||
(d/nilv align :left)
|
||||
|
||||
props
|
||||
(mf/spread-props props
|
||||
{:class (stl/css-case :option-list true
|
||||
:left-align (= align :left)
|
||||
:right-align (= align :right))
|
||||
{:class [class (stl/css-case :option-list true
|
||||
:left-align (= align :left)
|
||||
:right-align (= align :right))]
|
||||
:ref wrapper-ref
|
||||
:tab-index "-1"
|
||||
:role "listbox"})
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
:id id
|
||||
:on-click on-click
|
||||
:data-id id
|
||||
:aria-label name
|
||||
:data-testid "dropdown-option"}
|
||||
|
||||
(if selected
|
||||
|
||||
95
frontend/src/app/main/ui/ds/controls/utilities/utils.cljs
Normal file
95
frontend/src/app/main/ui/ds/controls/utilities/utils.cljs
Normal file
@@ -0,0 +1,95 @@
|
||||
(ns app.main.ui.ds.controls.utilities.utils
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.token :as cto]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn- token->dropdown-option
|
||||
[token]
|
||||
{:id (str (get token :id))
|
||||
:type :token
|
||||
:resolved-value (get token :value)
|
||||
:name (get token :name)})
|
||||
|
||||
(defn- generate-dropdown-options
|
||||
[tokens no-sets]
|
||||
(if (empty? tokens)
|
||||
[{:type :empty
|
||||
:label (if no-sets
|
||||
(tr "ds.inputs.numeric-input.no-applicable-tokens")
|
||||
(tr "ds.inputs.numeric-input.no-matches"))}]
|
||||
(->> tokens
|
||||
(map (fn [[type items]]
|
||||
(cons {:group true
|
||||
:type :group
|
||||
:id (dm/str "group-" (name type))
|
||||
:name (name type)}
|
||||
(map token->dropdown-option items))))
|
||||
(interpose [{:separator true
|
||||
:id "separator"
|
||||
:type :separator}])
|
||||
(apply concat)
|
||||
(vec)
|
||||
(not-empty))))
|
||||
|
||||
(defn- extract-partial-brace-text
|
||||
[s]
|
||||
(when-let [start (str/last-index-of s "{")]
|
||||
(subs s (inc start))))
|
||||
|
||||
(defn- filter-token-groups-by-name
|
||||
[tokens filter-text]
|
||||
(let [lc-filter (str/lower filter-text)]
|
||||
(into {}
|
||||
(keep (fn [[group tokens]]
|
||||
(let [filtered (filter #(str/includes? (str/lower (:name %)) lc-filter) tokens)]
|
||||
(when (seq filtered)
|
||||
[group filtered]))))
|
||||
tokens)))
|
||||
|
||||
(defn- sort-groups-and-tokens
|
||||
"Sorts both the groups and the tokens inside them alphabetically.
|
||||
|
||||
Input:
|
||||
A map where:
|
||||
- keys are groups (keywords or strings, e.g. :dimensions, :colors)
|
||||
- values are vectors of token maps, each containing at least a :name key
|
||||
|
||||
Example input:
|
||||
{:dimensions [{:name \"tres\"} {:name \"quini\"}]
|
||||
:colors [{:name \"azul\"} {:name \"rojo\"}]}
|
||||
|
||||
Output:
|
||||
A sorted map where:
|
||||
- groups are ordered alphabetically by key
|
||||
- tokens inside each group are sorted alphabetically by :name
|
||||
|
||||
Example output:
|
||||
{:colors [{:name \"azul\"} {:name \"rojo\"}]
|
||||
:dimensions [{:name \"quini\"} {:name \"tres\"}]}"
|
||||
|
||||
[groups->tokens]
|
||||
(into (sorted-map) ;; ensure groups are ordered alphabetically by their key
|
||||
(for [[group tokens] groups->tokens]
|
||||
[group (sort-by :name tokens)])))
|
||||
|
||||
(defn get-token-dropdown-options
|
||||
[tokens filter-term]
|
||||
(delay
|
||||
(let [tokens (if (delay? tokens) @tokens tokens)
|
||||
|
||||
sorted-tokens (sort-groups-and-tokens tokens)
|
||||
partial (extract-partial-brace-text filter-term)
|
||||
options (if (seq partial)
|
||||
(filter-token-groups-by-name sorted-tokens partial)
|
||||
sorted-tokens)
|
||||
no-sets? (nil? sorted-tokens)]
|
||||
(generate-dropdown-options options no-sets?))))
|
||||
|
||||
(defn filter-tokens-for-input
|
||||
[raw-tokens input-type]
|
||||
(delay
|
||||
(-> (deref raw-tokens)
|
||||
(select-keys (get cto/tokens-by-input input-type))
|
||||
(not-empty))))
|
||||
@@ -22,12 +22,10 @@
|
||||
|
||||
(mf/defc nitrate-form-modal*
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :nitrate-form
|
||||
::mf/wrap-props true}
|
||||
[connectivity]
|
||||
::mf/register-as :nitrate-form}
|
||||
[]
|
||||
|
||||
(let [show-buttons (:licenses connectivity)
|
||||
initial (mf/with-memo []
|
||||
(let [initial (mf/with-memo []
|
||||
{:subscription "yearly"})
|
||||
form (fm/use-form :schema schema:nitrate-form
|
||||
:initial initial)
|
||||
@@ -57,35 +55,26 @@
|
||||
"Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl."]
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
"Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors."]
|
||||
(if show-buttons
|
||||
[:& fm/form {:form form}
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
[:& fm/form {:form form}
|
||||
[:& fm/radio-buttons
|
||||
{:options [{:label "Price Tag Montly" :value "monthly"}
|
||||
{:label "Price Tag Yearly (Discount)" :value "yearly"}]
|
||||
:name :subscription
|
||||
:class (stl/css :radio-btns)}]]]
|
||||
|
||||
[:& fm/radio-buttons
|
||||
{:options [{:label "Price Tag Montly" :value "monthly"}
|
||||
{:label "Price Tag Yearly (Discount)" :value "yearly"}]
|
||||
:name :subscription
|
||||
:class (stl/css :radio-btns)}]]
|
||||
|
||||
[:p {:class (stl/css :modal-text-large :modal-buttons-section)}
|
||||
[:div {:class (stl/css :modal-buttons-section)}
|
||||
[:> button* {:variant "primary"
|
||||
:on-click on-click
|
||||
:class (stl/css :modal-button)}
|
||||
"UPGRADE TO NITRATE"]
|
||||
[:div {:class (stl/css :modal-text-small :modal-info)}
|
||||
"Cancel anytime before your next billing cycle."]]]
|
||||
[:p {:class (stl/css :modal-text-large :modal-buttons-section)}
|
||||
[:div {:class (stl/css :modal-buttons-section)}
|
||||
[:> button* {:variant "primary"
|
||||
:on-click on-click
|
||||
:class (stl/css :modal-button)}
|
||||
"UPGRADE TO NITRATE"]
|
||||
[:div {:class (stl/css :modal-text-small :modal-info)}
|
||||
"Cancel anytime before your next billing cycle."]]]
|
||||
|
||||
|
||||
[:p {:class (stl/css :modal-text-medium)}
|
||||
[:a {:class (stl/css :link)}
|
||||
"See my current plan"]]]
|
||||
|
||||
[:div {:class (stl/css :contact)}
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
"Contact us to upgrade to Nitrate:"]
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
[:a {:class (stl/css :link) :href "mailto:sales@penpot.app"}
|
||||
"sales@penpot.app"]]])]]]]))
|
||||
[:p {:class (stl/css :modal-text-medium)}
|
||||
[:a {:class (stl/css :link)}
|
||||
"See my current plan"]]]]]]))
|
||||
|
||||
|
||||
|
||||
@@ -89,8 +89,3 @@
|
||||
padding: var(--sp-l) 0 0 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.contact {
|
||||
margin-block-start: $sz-96;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.main.data.auth :as da]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
@@ -563,7 +562,7 @@
|
||||
:recommended (= subscription-type "professional")
|
||||
:show-button-cta (= subscription-type "professional")}])
|
||||
|
||||
(when (and (not= subscription-type "enterprise") (not (contains? cf/flags :nitrate)))
|
||||
(when (not= subscription-type "enterprise")
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
|
||||
:card-title-icon i/character-e
|
||||
:price-value "$950"
|
||||
@@ -576,21 +575,5 @@
|
||||
:cta-link #(open-subscription-modal "enterprise" subscription)
|
||||
:cta-text-with-icon (tr "subscription.settings.more-information")
|
||||
:cta-link-with-icon go-to-pricing-page
|
||||
:show-button-cta (= subscription-type "professional")}])
|
||||
|
||||
;; TODO add translations for this texts when we have the definitive ones
|
||||
(when (and (contains? cf/flags :nitrate) (not (:nitrate-licence profile)))
|
||||
[:> plan-card* {:card-title "Business Nitrate"
|
||||
:card-title-icon i/character-n
|
||||
:price-value "$25"
|
||||
:price-period "org member"
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
|
||||
:benefits ["Crea organizaciones y añade personas, que usarán Penpot con las reglas que configures."
|
||||
"Acceso exclusivo al Control Center"
|
||||
"Lorem ipsum"]
|
||||
:cta-text (tr "subscription.settings.subscribe")
|
||||
;; TODO add link to open nitrate modal
|
||||
:cta-link #(dom/open-new-window "https://penpot.app/nitrate")
|
||||
:cta-text-with-icon (tr "subscription.settings.more-information")
|
||||
:cta-link-with-icon go-to-pricing-page}])]]]))
|
||||
:show-button-cta (= subscription-type "professional")}])]]]))
|
||||
|
||||
|
||||
@@ -143,8 +143,7 @@
|
||||
(let [token-ids (set tokens-in-path-ids)
|
||||
remaining-tokens (filter (fn [token]
|
||||
(not (contains? token-ids (:id token))))
|
||||
selected-token-set-tokens)
|
||||
_ (prn "Remaining tokens:" remaining-tokens)]
|
||||
selected-token-set-tokens)]
|
||||
(seq remaining-tokens))))
|
||||
|
||||
delete-token
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.color-input :as color]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.combobox :as combobox]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.fonts-combobox :as fonts]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.input :as input]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.select :as select]))
|
||||
@@ -16,4 +17,6 @@
|
||||
(dm/export fonts/fonts-combobox*)
|
||||
(dm/export fonts/composite-fonts-combobox*)
|
||||
|
||||
(dm/export select/select-indexed*)
|
||||
(dm/export select/select-indexed*)
|
||||
|
||||
(dm/export combobox/combobox*)
|
||||
@@ -0,0 +1,290 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.forms.controls.combobox
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.input :as ds]
|
||||
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]]
|
||||
[app.main.ui.ds.controls.utilities.utils :as csu]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.forms :as fc]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.floating :refer [use-floating-dropdown]]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.navigation :refer [use-navigation]]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.token-parsing :as tp]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.object :as obj]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token token-name value]
|
||||
(let [valid-token-name?
|
||||
(and (string? token-name)
|
||||
(re-matches cto/token-name-validation-regex token-name))
|
||||
|
||||
token
|
||||
{:value value
|
||||
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||
token-name)}
|
||||
tokens
|
||||
(-> tokens
|
||||
;; Remove previous token when renaming a token
|
||||
(dissoc (:name prev-token))
|
||||
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
|
||||
|
||||
(->> tokens
|
||||
(sd/resolve-tokens-interactive)
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (first errors)}))))))))
|
||||
|
||||
(mf/defc combobox*
|
||||
[{:keys [name tokens token token-type empty-to-end ref] :rest props}]
|
||||
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
|
||||
token-name (get-in @form [:data :name] nil)
|
||||
touched?
|
||||
(and (contains? (:data @form) name)
|
||||
(get-in @form [:touched name]))
|
||||
|
||||
error
|
||||
(get-in @form [:errors name])
|
||||
|
||||
value
|
||||
(get-in @form [:data name] "")
|
||||
|
||||
is-open* (mf/use-state false)
|
||||
is-open (deref is-open*)
|
||||
|
||||
listbox-id (mf/use-id)
|
||||
filter-term* (mf/use-state "")
|
||||
filter-term (deref filter-term*)
|
||||
|
||||
options-ref (mf/use-ref nil)
|
||||
dropdown-ref (mf/use-ref nil)
|
||||
internal-ref (mf/use-ref nil)
|
||||
nodes-ref (mf/use-ref nil)
|
||||
wrapper-ref (mf/use-ref nil)
|
||||
icon-button-ref (mf/use-ref nil)
|
||||
ref (or ref internal-ref)
|
||||
|
||||
raw-tokens-by-type (mf/use-ctx muc/active-tokens-by-type)
|
||||
|
||||
filtered-tokens-by-type
|
||||
(mf/with-memo [raw-tokens-by-type token-type]
|
||||
(csu/filter-tokens-for-input raw-tokens-by-type token-type))
|
||||
|
||||
visible-options
|
||||
(mf/with-memo [filtered-tokens-by-type token]
|
||||
(if token
|
||||
(tp/remove-self-token filtered-tokens-by-type token)
|
||||
filtered-tokens-by-type))
|
||||
|
||||
dropdown-options
|
||||
(mf/with-memo [visible-options filter-term]
|
||||
(csu/get-token-dropdown-options visible-options (str "{" filter-term)))
|
||||
|
||||
set-option-ref
|
||||
(mf/use-fn
|
||||
(fn [node]
|
||||
(let [state (mf/ref-val nodes-ref)
|
||||
state (d/nilv state #js {})
|
||||
id (dom/get-data node "id")
|
||||
state (obj/set! state id node)]
|
||||
(mf/set-ref-val! nodes-ref state))))
|
||||
|
||||
toggle-dropdown
|
||||
(mf/use-fn
|
||||
(mf/deps is-open)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(swap! is-open* not)
|
||||
(let [input-node (mf/ref-val ref)]
|
||||
(dom/focus! input-node))))
|
||||
|
||||
resolve-stream
|
||||
(mf/with-memo [token]
|
||||
(if (contains? token :value)
|
||||
(rx/behavior-subject (:value token))
|
||||
(rx/subject)))
|
||||
|
||||
on-option-enter
|
||||
(mf/use-fn
|
||||
(mf/deps value resolve-stream name)
|
||||
(fn [id]
|
||||
(let [input-node (mf/ref-val ref)
|
||||
final-val (tp/select-option-by-id id options-ref input-node value)]
|
||||
(fm/on-input-change form name final-val true)
|
||||
(rx/push! resolve-stream final-val)
|
||||
(reset! filter-term* "")
|
||||
(reset! is-open* false))))
|
||||
|
||||
{:keys [focused-id on-key-down]}
|
||||
(use-navigation
|
||||
{:is-open is-open
|
||||
:options-ref options-ref
|
||||
:nodes-ref nodes-ref
|
||||
:toggle-dropdown toggle-dropdown
|
||||
:is-open* is-open*
|
||||
:on-enter on-option-enter})
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(mf/deps resolve-stream name form)
|
||||
(fn [event]
|
||||
(let [node (dom/get-target event)
|
||||
value (dom/get-input-value node)
|
||||
token (tp/active-token value node)]
|
||||
|
||||
(fm/on-input-change form name value)
|
||||
(rx/push! resolve-stream value)
|
||||
|
||||
(if token
|
||||
(do
|
||||
(reset! is-open* true)
|
||||
(reset! filter-term* (:partial token)))
|
||||
(do
|
||||
(reset! is-open* false)
|
||||
(reset! filter-term* ""))))))
|
||||
|
||||
on-option-click
|
||||
(mf/use-fn
|
||||
(mf/deps value resolve-stream ref name)
|
||||
(fn [event]
|
||||
(let [input-node (mf/ref-val ref)
|
||||
node (dom/get-current-target event)
|
||||
id (dom/get-data node "id")
|
||||
final-val (tp/select-option-by-id id options-ref input-node value)]
|
||||
|
||||
(fm/on-input-change form name final-val true)
|
||||
(rx/push! resolve-stream final-val)
|
||||
|
||||
(reset! filter-term* "")
|
||||
(reset! is-open* false)
|
||||
|
||||
(dom/focus! input-node)
|
||||
(let [new-cursor (+ (str/index-of final-val "}") 1)]
|
||||
(set! (.-selectionStart input-node) new-cursor)
|
||||
(set! (.-selectionEnd input-node) new-cursor)))))
|
||||
|
||||
hint*
|
||||
(mf/use-state {})
|
||||
|
||||
hint
|
||||
(deref hint*)
|
||||
|
||||
props
|
||||
(mf/spread-props props {:on-change on-change
|
||||
:value value
|
||||
:variant "comfortable"
|
||||
:hint-message (:message hint)
|
||||
:on-key-down on-key-down
|
||||
:hint-type (:type hint)
|
||||
:ref ref
|
||||
:role "combobox"
|
||||
:aria-activedescendant focused-id
|
||||
:aria-controls listbox-id
|
||||
:aria-expanded is-open
|
||||
:slot-end
|
||||
(when (some? @filtered-tokens-by-type)
|
||||
(mf/html
|
||||
[:> icon-button*
|
||||
{:variant "action"
|
||||
:icon i/arrow-down
|
||||
:ref icon-button-ref
|
||||
:tooltip-class (stl/css :button-tooltip)
|
||||
:class (stl/css :invisible-button)
|
||||
:tab-index "-1"
|
||||
:aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
|
||||
:on-mouse-down dom/prevent-default
|
||||
:on-click toggle-dropdown}]))})
|
||||
props
|
||||
(if (and error touched?)
|
||||
(mf/spread-props props {:hint-type "error"
|
||||
:hint-message (:message error)})
|
||||
props)
|
||||
|
||||
|
||||
{:keys [style ready?]} (use-floating-dropdown is-open wrapper-ref dropdown-ref)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token name token-name]
|
||||
(let [subs (->> resolve-stream
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
((:error/fn error) (:error/value error))))))
|
||||
(rx/subs! (fn [{:keys [error value]}]
|
||||
(let [touched? (get-in @form [:touched name])]
|
||||
(when touched?
|
||||
(if error
|
||||
(do
|
||||
(swap! form assoc-in [:extra-errors name] {:message error})
|
||||
(reset! hint* {:message error :type "error"}))
|
||||
(let [message (tr "workspace.tokens.resolved-value" value)]
|
||||
(swap! form update :extra-errors dissoc name)
|
||||
(reset! hint* {:message message :type "hint"}))))))))]
|
||||
(fn []
|
||||
(rx/dispose! subs))))
|
||||
|
||||
(mf/with-effect [dropdown-options]
|
||||
(mf/set-ref-val! options-ref dropdown-options))
|
||||
|
||||
(mf/with-effect [is-open* ref wrapper-ref]
|
||||
(when is-open
|
||||
(let [handler (fn [event]
|
||||
(let [wrapper-node (mf/ref-val wrapper-ref)
|
||||
dropdown-node (mf/ref-val dropdown-ref)
|
||||
target (dom/get-target event)]
|
||||
(when (and wrapper-node dropdown-node
|
||||
(not (dom/child? target wrapper-node))
|
||||
(not (dom/child? target dropdown-node)))
|
||||
(reset! is-open* false))))]
|
||||
|
||||
(.addEventListener js/document "mousedown" handler)
|
||||
|
||||
(fn []
|
||||
(.removeEventListener js/document "mousedown" handler)))))
|
||||
|
||||
|
||||
[:div {:ref wrapper-ref}
|
||||
[:> ds/input* props]
|
||||
(when ^boolean is-open
|
||||
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
|
||||
(mf/portal
|
||||
(mf/html
|
||||
[:> options-dropdown* {:on-click on-option-click
|
||||
:class (stl/css :dropdown)
|
||||
:style {:visibility (if ready? "visible" "hidden")
|
||||
:left (:left style)
|
||||
:top (or (:top style) "unset")
|
||||
:bottom (or (:bottom style) "unset")
|
||||
:width (:width style)}
|
||||
:id listbox-id
|
||||
:options options
|
||||
:focused focused-id
|
||||
:selected nil
|
||||
:align :right
|
||||
:empty-to-end empty-to-end
|
||||
:wrapper-ref dropdown-ref
|
||||
:ref set-option-ref}])
|
||||
(dom/get-body))))]))
|
||||
@@ -0,0 +1,19 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
// TODO: Remove this after creating a non deprecated scrollbar class
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
.dropdown {
|
||||
// TODO: create new scrollbar class not deprecated.
|
||||
@extend .new-scrollbar;
|
||||
position: fixed;
|
||||
max-block-size: $sz-400;
|
||||
max-block-size: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.forms.controls.floating
|
||||
(:require
|
||||
[app.util.dom :as dom]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn use-floating-dropdown [is-open wrapper-ref dropdown-ref]
|
||||
(let [position* (mf/use-state nil)
|
||||
position (deref position*)
|
||||
ready* (mf/use-state false)
|
||||
ready (deref ready*)
|
||||
calculate-position
|
||||
(fn [node]
|
||||
(let [combobox-rect (dom/get-bounding-rect node)
|
||||
dropdown-node (mf/ref-val dropdown-ref)
|
||||
dropdown-height (if dropdown-node
|
||||
(-> (dom/get-bounding-rect dropdown-node)
|
||||
(:height))
|
||||
0)
|
||||
|
||||
windows-height (-> (dom/get-window-size)
|
||||
(:height))
|
||||
|
||||
space-below (- windows-height (:bottom combobox-rect))
|
||||
|
||||
open-up? (and dropdown-height
|
||||
(> dropdown-height space-below))
|
||||
|
||||
position (if open-up?
|
||||
{:bottom (str (- windows-height (:top combobox-rect) 12) "px")
|
||||
:left (str (:left combobox-rect) "px")
|
||||
:width (str (:width combobox-rect) "px")
|
||||
:placement :top}
|
||||
|
||||
{:top (str (+ (:bottom combobox-rect) 4) "px")
|
||||
:left (str (:left combobox-rect) "px")
|
||||
:width (str (:width combobox-rect) "px")
|
||||
:placement :bottom})]
|
||||
(reset! ready* true)
|
||||
(reset! position* position)))]
|
||||
|
||||
(mf/with-effect [is-open dropdown-ref wrapper-ref]
|
||||
(when is-open
|
||||
(let [handler (fn [event]
|
||||
(let [dropdown-node (mf/ref-val dropdown-ref)
|
||||
target (dom/get-target event)]
|
||||
(when (or (nil? dropdown-node)
|
||||
(not (instance? js/Node target))
|
||||
(not (.contains dropdown-node target)))
|
||||
(js/requestAnimationFrame
|
||||
(fn []
|
||||
(let [wrapper-node (mf/ref-val wrapper-ref)]
|
||||
(reset! ready* true)
|
||||
(calculate-position wrapper-node)))))))]
|
||||
(handler nil)
|
||||
|
||||
(.addEventListener js/window "resize" handler)
|
||||
(.addEventListener js/window "scroll" handler true)
|
||||
|
||||
(fn []
|
||||
(.removeEventListener js/window "resize" handler)
|
||||
(.removeEventListener js/window "scroll" handler true)))))
|
||||
|
||||
{:style position
|
||||
:ready? ready
|
||||
:recalculate calculate-position}))
|
||||
@@ -0,0 +1,114 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.forms.controls.navigation
|
||||
(:require
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- focusable-option?
|
||||
[option]
|
||||
(and (:id option)
|
||||
(not= :group (:type option))
|
||||
(not= :separator (:type option))))
|
||||
|
||||
(defn- first-focusable-id
|
||||
[options]
|
||||
(some #(when (focusable-option? %) (:id %)) options))
|
||||
|
||||
(defn next-focus-id
|
||||
[options focused-id direction]
|
||||
(let [focusable (filter focusable-option? options)
|
||||
ids (map :id focusable)
|
||||
idx (.indexOf (clj->js ids) focused-id)
|
||||
next-idx (case direction
|
||||
:down (min (dec (count ids)) (inc (if (= idx -1) -1 idx)))
|
||||
:up (max 0 (dec (if (= idx -1) 0 idx))))]
|
||||
(nth ids next-idx nil)))
|
||||
|
||||
(defn use-navigation
|
||||
[{:keys [is-open options-ref nodes-ref is-open* toggle-dropdown on-enter]}]
|
||||
|
||||
(let [focused-id* (mf/use-state nil)
|
||||
focused-id (deref focused-id*)
|
||||
|
||||
on-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps is-open focused-id)
|
||||
(fn [event]
|
||||
(let [up? (kbd/up-arrow? event)
|
||||
down? (kbd/down-arrow? event)
|
||||
enter? (kbd/enter? event)
|
||||
esc? (kbd/esc? event)
|
||||
;; TODO: this should be optional?
|
||||
open-dropdown (kbd/is-key? event "{")
|
||||
close-dropdown (kbd/is-key? event "}")
|
||||
options (mf/ref-val options-ref)
|
||||
options (if (delay? options) @options options)]
|
||||
|
||||
(cond
|
||||
|
||||
down?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(if is-open
|
||||
(let [next-id (next-focus-id options focused-id :down)]
|
||||
(reset! focused-id* next-id))
|
||||
(do
|
||||
(toggle-dropdown event)
|
||||
(reset! focused-id* (first-focusable-id options)))))
|
||||
|
||||
up?
|
||||
(when is-open
|
||||
(dom/prevent-default event)
|
||||
(let [next-id (next-focus-id options focused-id :up)]
|
||||
(reset! focused-id* next-id)))
|
||||
|
||||
open-dropdown
|
||||
(reset! is-open* true)
|
||||
|
||||
close-dropdown
|
||||
(reset! is-open* false)
|
||||
|
||||
enter?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(if is-open
|
||||
(on-enter focused-id)
|
||||
(do
|
||||
(reset! focused-id* (first-focusable-id options))
|
||||
(toggle-dropdown event))))
|
||||
esc?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(reset! is-open* false))
|
||||
:else nil))))]
|
||||
|
||||
;; Initial focus on first option
|
||||
(mf/with-effect [is-open options-ref]
|
||||
(when is-open
|
||||
(let [options (mf/ref-val options-ref)
|
||||
options (if (delay? options) @options options)
|
||||
|
||||
first-id (first-focusable-id options)]
|
||||
|
||||
(when first-id
|
||||
(reset! focused-id* first-id)))))
|
||||
|
||||
;; auto scroll when key down
|
||||
(mf/with-effect [focused-id nodes-ref]
|
||||
(when focused-id
|
||||
(let [nodes (mf/ref-val nodes-ref)
|
||||
node (obj/get nodes focused-id)]
|
||||
(when node
|
||||
(dom/scroll-into-view-if-needed!
|
||||
node {:block "nearest"
|
||||
:inline "nearest"})))))
|
||||
|
||||
{:focused-id focused-id
|
||||
:on-key-down on-key-down}))
|
||||
@@ -0,0 +1,66 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.forms.controls.token-parsing
|
||||
(:require
|
||||
[app.main.ui.ds.controls.select :refer [get-option]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn extract-partial-token
|
||||
[value cursor]
|
||||
(let [text-before (subs value 0 cursor)
|
||||
last-open (str/last-index-of text-before "{")
|
||||
last-close (str/last-index-of text-before "}")]
|
||||
(when (and last-open (or (nil? last-close) (> last-open last-close)))
|
||||
{:start last-open
|
||||
:partial (subs text-before (inc last-open))})))
|
||||
|
||||
(defn replace-active-token
|
||||
[value cursor new-name]
|
||||
|
||||
(let [before (subs value 0 cursor)
|
||||
last-open (str/last-index-of before "{")
|
||||
last-close (str/last-index-of before "}")]
|
||||
|
||||
(if (and last-open
|
||||
(or (nil? last-close)
|
||||
(> last-open last-close)))
|
||||
|
||||
(let [after-start (subs value last-open)
|
||||
close-pos (str/index-of after-start "}")
|
||||
end (if close-pos
|
||||
(+ last-open close-pos 1)
|
||||
cursor)]
|
||||
(str (subs value 0 last-open)
|
||||
"{" new-name "}"
|
||||
(subs value end)))
|
||||
(str (subs value 0 cursor)
|
||||
"{" new-name "}"
|
||||
(subs value cursor)))))
|
||||
|
||||
(defn active-token [value input-node]
|
||||
(let [cursor (.-selectionStart input-node)]
|
||||
(extract-partial-token value cursor)))
|
||||
|
||||
(defn remove-self-token [filtered-options current-token]
|
||||
(let [group (:type current-token)
|
||||
current-id (:id current-token)
|
||||
filtered-options (deref filtered-options)]
|
||||
(update filtered-options group
|
||||
(fn [options]
|
||||
(remove #(= (:id %) current-id) options)))))
|
||||
|
||||
(defn select-option-by-id
|
||||
[id options-ref input-node value]
|
||||
(let [cursor (.-selectionStart input-node)
|
||||
options (mf/ref-val options-ref)
|
||||
options (if (delay? options) @options options)
|
||||
|
||||
option (get-option options id)
|
||||
name (:name option)
|
||||
final-val (replace-active-token value cursor name)]
|
||||
final-val))
|
||||
@@ -8,8 +8,10 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.config :as cf]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.workspace.tokens.management.forms.color :as color]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
|
||||
[app.main.ui.workspace.tokens.management.forms.font-family :as font-family]
|
||||
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
|
||||
[app.main.ui.workspace.tokens.management.forms.shadow :as shadow]
|
||||
@@ -39,7 +41,10 @@
|
||||
:token token})
|
||||
text-case-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")})
|
||||
text-decoration-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")})
|
||||
font-weight-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")})]
|
||||
font-weight-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")})
|
||||
border-radius-props (if (contains? cf/flags :token-combobox)
|
||||
(mf/spread-props props {:input-component token.controls/combobox*})
|
||||
props)]
|
||||
|
||||
(case token-type
|
||||
:color [:> color/form* props]
|
||||
@@ -49,4 +54,5 @@
|
||||
:text-case [:> generic/form* text-case-props]
|
||||
:text-decoration [:> generic/form* text-decoration-props]
|
||||
:font-weight [:> generic/form* font-weight-props]
|
||||
:border-radius [:> generic/form* border-radius-props]
|
||||
[:> generic/form* props])))
|
||||
@@ -21,6 +21,7 @@
|
||||
[app.main.data.workspace.tokens.remapping :as remap]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
@@ -97,6 +98,10 @@
|
||||
(and (:name token) (:value token))
|
||||
(assoc (:name token) token)))
|
||||
|
||||
active-tokens-by-type
|
||||
(mf/with-memo [tokens]
|
||||
(delay (ctob/group-by-type tokens)))
|
||||
|
||||
schema
|
||||
(mf/with-memo [tokens-tree-in-selected-set active-tab]
|
||||
(make-schema tokens-tree-in-selected-set active-tab))
|
||||
@@ -224,78 +229,80 @@
|
||||
error-message (first error-messages)]
|
||||
(swap! form assoc-in [:extra-errors :value] {:message error-message}))))))))]
|
||||
|
||||
[:> fc/form* {:class (stl/css :form-wrapper)
|
||||
:form form
|
||||
:on-submit on-submit}
|
||||
[:div {:class (stl/css :token-rows)}
|
||||
[(mf/provider muc/active-tokens-by-type) {:value active-tokens-by-type}
|
||||
[:> fc/form* {:class (stl/css :form-wrapper)
|
||||
:form form
|
||||
:on-submit on-submit}
|
||||
[:div {:class (stl/css :token-rows)}
|
||||
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
|
||||
(if (= action "edit")
|
||||
(tr "workspace.tokens.edit-token" token-type)
|
||||
(tr "workspace.tokens.create-token" token-type))]
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
|
||||
(if (= action "edit")
|
||||
(tr "workspace.tokens.edit-token" token-type)
|
||||
(tr "workspace.tokens.create-token" token-type))]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-name"
|
||||
:name :name
|
||||
:label (tr "workspace.tokens.token-name")
|
||||
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:trim true
|
||||
:auto-focus true}]]
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-name"
|
||||
:name :name
|
||||
:label (tr "workspace.tokens.token-name")
|
||||
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:trim true
|
||||
:auto-focus true}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
(case type
|
||||
:indexed
|
||||
[:> input-component
|
||||
{:token token
|
||||
:tokens tokens
|
||||
:tab active-tab
|
||||
:value-subfield value-subfield
|
||||
:handle-toggle on-toggle-tab}]
|
||||
[:div {:class (stl/css :input-row)}
|
||||
(case type
|
||||
:indexed
|
||||
[:> input-component
|
||||
{:token token
|
||||
:tokens tokens
|
||||
:tab active-tab
|
||||
:value-subfield value-subfield
|
||||
:handle-toggle on-toggle-tab}]
|
||||
|
||||
:composite
|
||||
[:> input-component
|
||||
{:token token
|
||||
:tokens tokens
|
||||
:tab active-tab
|
||||
:handle-toggle on-toggle-tab}]
|
||||
:composite
|
||||
[:> input-component
|
||||
{:token token
|
||||
:tokens tokens
|
||||
:tab active-tab
|
||||
:handle-toggle on-toggle-tab}]
|
||||
|
||||
[:> input-component
|
||||
{:placeholder (or input-value-placeholder
|
||||
(tr "workspace.tokens.token-value-enter"))
|
||||
:label (tr "workspace.tokens.token-value")
|
||||
:name :value
|
||||
:token token
|
||||
:tokens tokens}])]
|
||||
[:> input-component
|
||||
{:placeholder (or input-value-placeholder
|
||||
(tr "workspace.tokens.token-value-enter"))
|
||||
:label (tr "workspace.tokens.token-value")
|
||||
:name :value
|
||||
:token token
|
||||
:token-type token-type
|
||||
:tokens tokens}])]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-description"
|
||||
:name :description
|
||||
:label (tr "workspace.tokens.token-description")
|
||||
:placeholder (tr "workspace.tokens.token-description")
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:is-optional true}]]
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-description"
|
||||
:name :description
|
||||
:label (tr "workspace.tokens.token-description")
|
||||
:placeholder (tr "workspace.tokens.token-description")
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:is-optional true}]]
|
||||
|
||||
[:div {:class (stl/css-case :button-row true
|
||||
:with-delete (= action "edit"))}
|
||||
(when (= action "edit")
|
||||
[:> button* {:on-click on-delete-token
|
||||
:on-key-down handle-key-down-delete
|
||||
:class (stl/css :delete-btn)
|
||||
:type "button"
|
||||
:icon i/delete
|
||||
:variant "secondary"}
|
||||
(tr "labels.delete")])
|
||||
[:div {:class (stl/css-case :button-row true
|
||||
:with-delete (= action "edit"))}
|
||||
(when (= action "edit")
|
||||
[:> button* {:on-click on-delete-token
|
||||
:on-key-down handle-key-down-delete
|
||||
:class (stl/css :delete-btn)
|
||||
:type "button"
|
||||
:icon i/delete
|
||||
:variant "secondary"}
|
||||
(tr "labels.delete")])
|
||||
|
||||
[:> button* {:on-click on-cancel
|
||||
:on-key-down handle-key-down-cancel
|
||||
:type "button"
|
||||
:id "token-modal-cancel"
|
||||
:variant "secondary"}
|
||||
(tr "labels.cancel")]
|
||||
[:> button* {:on-click on-cancel
|
||||
:on-key-down handle-key-down-cancel
|
||||
:type "button"
|
||||
:id "token-modal-cancel"
|
||||
:variant "secondary"}
|
||||
(tr "labels.cancel")]
|
||||
|
||||
[:> fc/form-submit* {:variant "primary"
|
||||
:on-submit on-submit}
|
||||
(tr "labels.save")]]]]))
|
||||
[:> fc/form-submit* {:variant "primary"
|
||||
:on-submit on-submit}
|
||||
(tr "labels.save")]]]]]))
|
||||
|
||||
@@ -50,45 +50,31 @@ Follow the steps below to enable the integration.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
The project requires [Node.js](https://nodejs.org/) (tested with v22.x).
|
||||
Following the installation of Node.js, the tools `corepack` and `npx`
|
||||
should be available in your terminal.
|
||||
The project requires [Node.js](https://nodejs.org/) (tested with v22.x
|
||||
with corepack).
|
||||
|
||||
On Windows, use the Git Bash terminal to ensure compatibility with the provided scripts.
|
||||
Following the installation of Node.js, the tools `pnpm` and `npx`
|
||||
should be available in your terminal. For ensure corepack installed
|
||||
and enabled correctly, just execute the `./scripts/setup`.
|
||||
|
||||
### 0. Clone the Appropriate Branch of the Repository
|
||||
It is also required to have `caddy` executeable in the path, it is
|
||||
used for start a local server for generate types documentation from
|
||||
the current branch. If you want to run it outside devenv where all
|
||||
dependencies are already provided, please download caddy from
|
||||
[here](https://caddyserver.com/download).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The branches are subject to change in the future.
|
||||
> Be sure to check the instructions for the latest information on which branch to use.
|
||||
You should probably be using penpot devenv, where all this
|
||||
dependencies are already present and correctly setup. But nothing
|
||||
prevents you execute this outside of devenv if you satisfy the
|
||||
specified dependencies.
|
||||
|
||||
Clone the Penpot repository, using the proper branch depending on the
|
||||
version of Penpot you want to use the MCP server with.
|
||||
|
||||
* For released versions of Penpot, use the `mcp-prod` branch:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/penpot/penpot.git --branch mcp-prod --depth 1
|
||||
```
|
||||
|
||||
* For the latest development version of Penpot, use the `develop` branch:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/penpot/penpot.git --branch develop --depth 1
|
||||
```
|
||||
|
||||
Then change into the `mcp` directory:
|
||||
|
||||
```shell
|
||||
cd penpot/mcp
|
||||
```
|
||||
|
||||
### 1. Build & Launch the MCP Server and the Plugin Server
|
||||
|
||||
If it's your first execution, install the required dependencies.
|
||||
(If you are using the Penpot devenv, this step is not necessary, as dependencies are already installed.)
|
||||
If it's your first execution, install the required dependencies:
|
||||
|
||||
```shell
|
||||
cd mcp/
|
||||
./scripts/setup
|
||||
```
|
||||
|
||||
@@ -100,9 +86,9 @@ pnpm run bootstrap
|
||||
|
||||
This bootstrap command will:
|
||||
|
||||
* install dependencies for all components
|
||||
* build all components
|
||||
* start all components
|
||||
* install dependencies for all components (`pnpm -r run install`)
|
||||
* build all components (`pnpm -r run build`)
|
||||
* start all components (`pnpm -r --parallel run start`)
|
||||
|
||||
### 2. Load the Plugin in Penpot and Establish the Connection
|
||||
|
||||
@@ -209,30 +195,31 @@ To add the Penpot MCP server to a Claude Code project, issue the command
|
||||
|
||||
This repository is a monorepo containing four main components:
|
||||
|
||||
1. **Common Types** (`packages/common/`):
|
||||
1. **Common Types** (`common/`):
|
||||
- Shared TypeScript definitions for request/response protocol
|
||||
- Ensures type safety across server and plugin components
|
||||
|
||||
2. **Penpot MCP Server** (`packages/server/`):
|
||||
2. **Penpot MCP Server** (`mcp-server/`):
|
||||
- Provides MCP tools to LLMs for Penpot interaction
|
||||
- Runs a WebSocket server accepting connections from the Penpot MCP plugin
|
||||
- Implements request/response correlation with unique task IDs
|
||||
- Handles task timeouts and proper error reporting
|
||||
|
||||
3. **Penpot MCP Plugin** (`packages/plugin/`):
|
||||
3. **Penpot MCP Plugin** (`penpot-plugin/`):
|
||||
- Connects to the MCP server via WebSocket
|
||||
- Executes tasks in Penpot using the Plugin API
|
||||
- Sends structured responses back to the server#
|
||||
|
||||
4. **Types Generator** (`types-generator/`):
|
||||
- Generates data on API types for the MCP server (development use)
|
||||
4. **Helper Scripts** (`python-scripts/`):
|
||||
- Python scripts that prepare data for the MCP server (development use)
|
||||
|
||||
The core components are written in TypeScript, rendering interactions with the
|
||||
Penpot Plugin API both natural and type-safe.
|
||||
|
||||
## Configuration
|
||||
|
||||
The Penpot MCP server can be configured using environment variables.
|
||||
The Penpot MCP server can be configured using environment variables. All configuration
|
||||
options use the `PENPOT_MCP_` prefix for consistency.
|
||||
|
||||
### Server Configuration
|
||||
|
||||
@@ -276,9 +263,3 @@ you may set the following environment variables to configure the two servers
|
||||
* `PENPOT_MCP_SERVER_ADDRESS=<your-address>`: This sets the hostname or IP address
|
||||
where the MCP server can be reached. The Penpot MCP Plugin uses this to construct
|
||||
the WebSocket URL as `ws://<your-address>:<port>` (default port: `4402`).
|
||||
|
||||
## Development
|
||||
|
||||
* The [contribution guidelines for Penpot](../CONTRIBUTING.md) apply
|
||||
* Auto-formatting: Use `pnpm run fmt`
|
||||
* Generating API type data: See [types-generator/README.md](types-generator/README.md)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
You have access to Penpot tools in order to interact with a Penpot design project directly.
|
||||
As a precondition, the user must connect the Penpot design project to the MCP server using the Penpot MCP Plugin.
|
||||
|
||||
IMPORTANT: When transferring styles from a Penpot design to code, make sure that you strictly adhere to the design.
|
||||
NEVER make assumptions about missing values and don't get overly creative (e.g. don't pick your own colours and stick to
|
||||
non-creative defaults such as white/black if you are lacking information).
|
||||
|
||||
# Executing Code
|
||||
|
||||
One of your key tools is the `execute_code` tool, which allows you to run JavaScript code using the Penpot Plugin API
|
||||
@@ -44,8 +48,6 @@ Actual low-level shape types are `Rectangle`, `Path`, `Text`, `Ellipse`, `Image`
|
||||
**Other Writable Properties**:
|
||||
* `name` - Shape name
|
||||
* `fills`, `strokes` - Styling properties
|
||||
IMPORTANT: The contents of the arrays are read-only. You cannot modify individual fills/strokes; you need to replace the entire array to change them, e.g.
|
||||
`shape.fills = [{ fillColor: "#FF0000", fillOpacity: 1 }]` to set a single red fill.
|
||||
* `rotation`, `opacity`, `blocked`, `hidden`, `visible`
|
||||
|
||||
**Z-Order**:
|
||||
@@ -97,12 +99,11 @@ Boards can have layout systems that automatically control the positioning and sp
|
||||
- `dir`: "row" | "column" | "row-reverse" | "column-reverse"
|
||||
- Padding: `topPadding`, `rightPadding`, `bottomPadding`, `leftPadding`, or combined `verticalPadding`, `horizontalPadding`
|
||||
- To modify spacing: adjust `rowGap` and `columnGap` properties, not individual child positions.
|
||||
Optionally, adjust individual child margins via `child.layoutChild`.
|
||||
- Sizing: `verticalSizing` and `horizontalSizing` are NOT functional. You need to size manually for the time being.
|
||||
Optionally, adjust indivudual child margins via `child.layoutChild`.
|
||||
- When a board has flex layout,
|
||||
- child positions are controlled by the layout system, not by individual x/y coordinates (unless `child.layoutChild.absolute` is true);
|
||||
appending or inserting children automatically positions them according to the layout rules.
|
||||
- CRITICAL: For dir="column" or dir="row", the order of the `children` array is reversed relative to the visual order!
|
||||
- CRITICAL: For for dir="column" or dir="row", the order of the `children` array is reversed relative to the visual order!
|
||||
Therefore, the element that appears first in the array, appears visually at the end (bottom/right) and vice versa.
|
||||
ALWAYS BEAR IN MIND THAT THE CHILDREN ARRAY ORDER IS REVERSED FOR dir="column" OR dir="row"!
|
||||
- CRITICAL: The FlexLayout method `board.flex.appendChild` is BROKEN. To append children to a flex layout board such that
|
||||
@@ -209,6 +210,19 @@ Common tasks - Quick Reference (ALWAYS use penpotUtils for these):
|
||||
});
|
||||
Always validate against the root container that is supposed to contain the shapes.
|
||||
|
||||
# Visual Inspection of Designs
|
||||
|
||||
For many tasks, it can be critical to visually inspect the design. Remember to use the `export_shape` tool for this purpose!
|
||||
|
||||
# Revising Designs
|
||||
|
||||
* Before applying design changes, ask: "Would a designer consider this appropriate?"
|
||||
* When dealing with containment issues, ask: Is the parent too small OR is the child too large?
|
||||
Container sizes are usually intentional, check content first.
|
||||
* Check for reasonable font sizes and typefaces
|
||||
* The use of flex layouts is encouraged for cases where elements are arranged in rows or columns with consistent spacing/positioning.
|
||||
Consider converting boards to flex layout when appropriate.
|
||||
|
||||
# Asset Libraries
|
||||
|
||||
Libraries in Penpot are collections of reusable design assets (components, colors, and typographies) that can be shared across files.
|
||||
@@ -268,15 +282,14 @@ The token library: `penpot.library.local.tokens` (type: `TokenCatalog`)
|
||||
* `tokens: Token[]` - All tokens in set
|
||||
* `addToken(type: TokenType, name: string, value: TokenValueString): Token` - Creates a token, adding it to the set.
|
||||
- `TokenType`: "color" | "dimension" | "spacing" | "typography" | "shadow" | "opacity" | "borderRadius" | "borderWidth" | "fontWeights" | "fontSizes" | "fontFamilies" | "letterSpacing" | "textDecoration" | "textCase"
|
||||
- `value`: depends on the type of token (inspect `Token` and related types)
|
||||
- Examples:
|
||||
const token = set.addToken("color", "color.primary", "#0066FF"); // direct value
|
||||
const token2 = set.addToken("color", "color.accent", "{color.primary}"); // reference to another token
|
||||
|
||||
`Token`: union type encompassing various token types, with common properties:
|
||||
* `name: string` - Token name (typically structured, e.g. "color.base.white")
|
||||
* `value` - Raw value (direct value or reference to another token like "{color.primary}")
|
||||
* `resolvedValue` - Computed final value (follows references) - currently NOT working, do not use!
|
||||
`Token`:
|
||||
* `name: string` - Token name (may include group path like "color.base.white")
|
||||
* `value: string | TokenValueString` - Raw value (may be direct value or reference to another token like "{color.primary}")
|
||||
* `resolvedValue` - Computed final value (follows references)
|
||||
* `type: TokenType`
|
||||
|
||||
Discovering tokens:
|
||||
@@ -316,24 +329,5 @@ Removing tokens:
|
||||
Simply set the respective property directly - token binding is automatically removed, e.g.
|
||||
shape.fills = [{ fillColor: "#000000", fillOpacity: 1 }]; // Removes fill token
|
||||
|
||||
# Visual Inspection of Designs
|
||||
|
||||
For many tasks, it can be critical to visually inspect the design. Remember to use the `export_shape` tool for this purpose!
|
||||
|
||||
# Creating and Translating Designs
|
||||
|
||||
* When transferring styles from a Penpot design to code, make sure that you strictly adhere to the design.
|
||||
NEVER make assumptions about missing values and don't get overly creative (e.g. don't pick your own colours and stick to
|
||||
non-creative defaults such as white/black if you are lacking information).
|
||||
|
||||
# Revising Designs
|
||||
|
||||
* Before applying design changes, ask: "Would a designer consider this appropriate?"
|
||||
* When dealing with containment issues, ask: Is the parent too small OR is the child too large?
|
||||
Container sizes are usually intentional, check content first.
|
||||
* Check for reasonable font sizes and typefaces
|
||||
* The use of flex layouts is encouraged for cases where elements are arranged in rows or columns with consistent spacing/positioning.
|
||||
Consider converting boards to flex layout when appropriate.
|
||||
|
||||
--
|
||||
You have hereby read the 'Penpot High-Level Overview' and need not use a tool to read it again.
|
||||
|
||||
@@ -53,10 +53,9 @@ export class ExecuteCodeTool extends Tool<ExecuteCodeArgs> {
|
||||
"could come in handy later should be stored in `storage` instead of just a fleeting variable; " +
|
||||
"you can also store functions and thus build up a library).\n" +
|
||||
"Think of the code being executed as the body of a function: " +
|
||||
"The tool call returns whatever you return in the applicable `return` statement, if any. " +
|
||||
"You can return arbitrary JS objects; no need to apply JSON.stringify.\n" +
|
||||
"The tool call returns whatever you return in the applicable `return` statement, if any.\n" +
|
||||
"If an exception occurs, the exception's message will be returned to you.\n" +
|
||||
"Any output that you generate via the `console` object will be returned to you separately; so you may use it " +
|
||||
"Any output that you generate via the `console` object will be returned to you separately; so you may use it" +
|
||||
"to track what your code is doing, but you should *only* do so only if there is an ACTUAL NEED for this! " +
|
||||
"VERY IMPORTANT: Don't use logging prematurely! NEVER log the data you are returning, as you will otherwise receive it twice!\n" +
|
||||
"VERY IMPORTANT: In general, try a simple approach first, and only if it fails, try more complex code that involves " +
|
||||
|
||||
Reference in New Issue
Block a user