mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
💄 Group tokens by name path (#7775)
Some checks failed
_STAGING / build-bundle (push) Has been cancelled
_STAGING / build-docker (push) Has been cancelled
_NITRATE MODULE / build-bundle (push) Has been cancelled
_NITRATE MODULE / build-docker (push) Has been cancelled
_DEVELOP / build-bundle (push) Has been cancelled
_DEVELOP / build-docker (push) Has been cancelled
Commit Message Check / Check Commit Message (push) Has been cancelled
CI / Linter (push) Has been cancelled
CI / Common Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / Render WASM Tests (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Library Tests (push) Has been cancelled
CI / Build Integration Bundle (push) Has been cancelled
CI / Integration Tests 1/4 (push) Has been cancelled
CI / Integration Tests 2/4 (push) Has been cancelled
CI / Integration Tests 3/4 (push) Has been cancelled
CI / Integration Tests 4/4 (push) Has been cancelled
Some checks failed
_STAGING / build-bundle (push) Has been cancelled
_STAGING / build-docker (push) Has been cancelled
_NITRATE MODULE / build-bundle (push) Has been cancelled
_NITRATE MODULE / build-docker (push) Has been cancelled
_DEVELOP / build-bundle (push) Has been cancelled
_DEVELOP / build-docker (push) Has been cancelled
Commit Message Check / Check Commit Message (push) Has been cancelled
CI / Linter (push) Has been cancelled
CI / Common Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / Render WASM Tests (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Library Tests (push) Has been cancelled
CI / Build Integration Bundle (push) Has been cancelled
CI / Integration Tests 1/4 (push) Has been cancelled
CI / Integration Tests 2/4 (push) Has been cancelled
CI / Integration Tests 3/4 (push) Has been cancelled
CI / Integration Tests 4/4 (push) Has been cancelled
* 💄 Group tokens by name path
This commit is contained in:
@@ -132,3 +132,94 @@ Some naming conventions:
|
|||||||
(if-let [last-period (str/last-index-of s ".")]
|
(if-let [last-period (str/last-index-of s ".")]
|
||||||
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
|
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
|
||||||
[s ""]))
|
[s ""]))
|
||||||
|
|
||||||
|
;; Tree building functions --------------------------------------------------
|
||||||
|
|
||||||
|
"Build tree structure from flat list of paths"
|
||||||
|
|
||||||
|
"`build-tree-root` is the main function to build the tree."
|
||||||
|
|
||||||
|
"Receives a list of segments with 'name' properties representing paths,
|
||||||
|
and a separator string."
|
||||||
|
"E.g segments = [{... :name 'one/two/three'} {... :name 'one/two/four'} {... :name 'one/five'}]"
|
||||||
|
|
||||||
|
"Transforms into a tree structure like:
|
||||||
|
[{:name 'one'
|
||||||
|
:path 'one'
|
||||||
|
:depth 0
|
||||||
|
:leaf nil
|
||||||
|
:children-fn (fn [] [{:name 'two'
|
||||||
|
:path 'one.two'
|
||||||
|
:depth 1
|
||||||
|
:leaf nil
|
||||||
|
:children-fn (fn [] [{... :name 'three'} {... :name 'four'}])}
|
||||||
|
{:name 'five'
|
||||||
|
:path 'one.five'
|
||||||
|
:depth 1
|
||||||
|
:leaf {... :name 'five'}
|
||||||
|
...}])}]"
|
||||||
|
|
||||||
|
(defn- sort-by-children
|
||||||
|
"Sorts segments so that those with children come first."
|
||||||
|
[segments separator]
|
||||||
|
(sort-by (fn [segment]
|
||||||
|
(let [path (split-path (:name segment) :separator separator)
|
||||||
|
path-length (count path)]
|
||||||
|
(if (= path-length 1)
|
||||||
|
1
|
||||||
|
0)))
|
||||||
|
segments))
|
||||||
|
|
||||||
|
(defn- group-by-first-segment
|
||||||
|
"Groups segments by their first path segment and update segment name."
|
||||||
|
[segments separator]
|
||||||
|
(reduce (fn [acc segment]
|
||||||
|
(let [[first-segment & remaining-segments] (split-path (:name segment) :separator separator)
|
||||||
|
rest-path (when (seq remaining-segments) (join-path remaining-segments :separator separator :with-spaces? false))]
|
||||||
|
(update acc first-segment (fnil conj [])
|
||||||
|
(if rest-path
|
||||||
|
(assoc segment :name rest-path)
|
||||||
|
segment))))
|
||||||
|
{}
|
||||||
|
segments))
|
||||||
|
|
||||||
|
(defn- sort-and-group-segments
|
||||||
|
"Sorts elements and groups them by their first path segment."
|
||||||
|
[segments separator]
|
||||||
|
(let [sorted (sort-by-children segments separator)
|
||||||
|
grouped (group-by-first-segment sorted separator)]
|
||||||
|
grouped))
|
||||||
|
|
||||||
|
(defn- build-tree-node
|
||||||
|
"Builds a single tree node with lazy children."
|
||||||
|
[segment-name remaining-segments separator parent-path depth]
|
||||||
|
(let [current-path (if parent-path
|
||||||
|
(str parent-path "." segment-name)
|
||||||
|
segment-name)
|
||||||
|
|
||||||
|
is-leaf? (and (seq remaining-segments)
|
||||||
|
(every? (fn [segment]
|
||||||
|
(let [remaining-segment-name (first (split-path (:name segment) :separator separator))]
|
||||||
|
(= segment-name remaining-segment-name)))
|
||||||
|
remaining-segments))
|
||||||
|
|
||||||
|
leaf-segment (when is-leaf? (first remaining-segments))
|
||||||
|
node {:name segment-name
|
||||||
|
:path current-path
|
||||||
|
:depth depth
|
||||||
|
:leaf leaf-segment
|
||||||
|
:children-fn (when-not is-leaf?
|
||||||
|
(fn []
|
||||||
|
(let [grouped-elements (sort-and-group-segments remaining-segments separator)]
|
||||||
|
(mapv (fn [[child-segment-name remaining-child-segments]]
|
||||||
|
(build-tree-node child-segment-name remaining-child-segments separator current-path (inc depth)))
|
||||||
|
grouped-elements))))}]
|
||||||
|
node))
|
||||||
|
|
||||||
|
(defn build-tree-root
|
||||||
|
"Builds the root level of the tree."
|
||||||
|
[segments separator]
|
||||||
|
(let [grouped-elements (sort-and-group-segments segments separator)]
|
||||||
|
(mapv (fn [[segment-name remaining-segments]]
|
||||||
|
(build-tree-node segment-name remaining-segments separator nil 0))
|
||||||
|
grouped-elements)))
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const setupEmptyTokensFile = async (page, options = {}) => {
|
|||||||
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
|
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
|
||||||
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
|
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
|
||||||
tokenSetItems: workspacePage.tokenSetItems,
|
tokenSetItems: workspacePage.tokenSetItems,
|
||||||
|
tokensSidebar: workspacePage.tokensSidebar,
|
||||||
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
|
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
|
||||||
tokenContextMenuForSet: workspacePage.tokenContextMenuForSet,
|
tokenContextMenuForSet: workspacePage.tokenContextMenuForSet,
|
||||||
};
|
};
|
||||||
@@ -110,15 +111,12 @@ const checkInputFieldWithError = async (
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkInputFieldWithoutError = async (
|
const checkInputFieldWithoutError = async (inputLocator) => {
|
||||||
tokenThemeUpdateCreateModal,
|
|
||||||
inputLocator,
|
|
||||||
) => {
|
|
||||||
expect(await inputLocator.getAttribute("aria-invalid")).toBeNull();
|
expect(await inputLocator.getAttribute("aria-invalid")).toBeNull();
|
||||||
expect(await inputLocator.getAttribute("aria-describedby")).toBeNull();
|
expect(await inputLocator.getAttribute("aria-describedby")).toBeNull();
|
||||||
};
|
};
|
||||||
|
|
||||||
async function testTokenCreationFlow(
|
const testTokenCreationFlow = async (
|
||||||
page,
|
page,
|
||||||
{
|
{
|
||||||
tokenLabel,
|
tokenLabel,
|
||||||
@@ -132,7 +130,7 @@ async function testTokenCreationFlow(
|
|||||||
resolvedValueText,
|
resolvedValueText,
|
||||||
secondResolvedValueText,
|
secondResolvedValueText,
|
||||||
},
|
},
|
||||||
) {
|
) => {
|
||||||
const invalidValueError = "Invalid token value";
|
const invalidValueError = "Invalid token value";
|
||||||
const emptyNameError = "Name should be at least 1 character";
|
const emptyNameError = "Name should be at least 1 character";
|
||||||
const selfReferenceError = "Token has self reference";
|
const selfReferenceError = "Token has self reference";
|
||||||
@@ -242,8 +240,46 @@ async function testTokenCreationFlow(
|
|||||||
await expect(
|
await expect(
|
||||||
tokensTabPanel.getByRole("button", { name: "my-token-2" }),
|
tokensTabPanel.getByRole("button", { name: "my-token-2" }),
|
||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
|
};
|
||||||
|
|
||||||
|
const unfoldTokenTree = async (tokensTabPanel, type, tokenName) => {
|
||||||
|
const tokenSegments = tokenName.split(".");
|
||||||
|
const tokenFolderTree = tokenSegments.slice(0, -1);
|
||||||
|
const tokenLeafName = tokenSegments.pop();
|
||||||
|
|
||||||
|
const typeParentWrapper = tokensTabPanel.getByTestId(`section-${type}`);
|
||||||
|
const typeSectionButton = typeParentWrapper
|
||||||
|
.getByRole("button", {
|
||||||
|
name: type,
|
||||||
|
})
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const isSectionExpanded =
|
||||||
|
await typeSectionButton.getAttribute("aria-expanded");
|
||||||
|
|
||||||
|
if (isSectionExpanded === "false") {
|
||||||
|
await typeSectionButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const segment of tokenFolderTree) {
|
||||||
|
const segmentButton = typeParentWrapper
|
||||||
|
.getByRole("listitem")
|
||||||
|
.getByRole("button", { name: segment })
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const isExpanded = await segmentButton.getAttribute("aria-expanded");
|
||||||
|
if (isExpanded === "false") {
|
||||||
|
await segmentButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
typeParentWrapper.getByRole("button", {
|
||||||
|
name: tokenLeafName,
|
||||||
|
}),
|
||||||
|
).toBeEnabled();
|
||||||
|
};
|
||||||
|
|
||||||
test.describe("Tokens: Tokens Tab", () => {
|
test.describe("Tokens: Tokens Tab", () => {
|
||||||
test("Clicking tokens tab button opens tokens sidebar tab", async ({
|
test("Clicking tokens tab button opens tokens sidebar tab", async ({
|
||||||
page,
|
page,
|
||||||
@@ -398,15 +434,12 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
const emptyNameError = "Name should be at least 1 character";
|
const emptyNameError = "Name should be at least 1 character";
|
||||||
const selfReferenceError = "Token has self reference";
|
const selfReferenceError = "Token has self reference";
|
||||||
const missingReferenceError = "Missing token references";
|
const missingReferenceError = "Missing token references";
|
||||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||||
await setupEmptyTokensFile(page);
|
await setupEmptyTokensFile(page);
|
||||||
|
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
await tokensSidebar
|
||||||
const addTokenButton = tokensTabPanel.getByRole("button", {
|
.getByRole("button", { name: "Add Token: Color" })
|
||||||
name: `Add Token: Color`,
|
.click();
|
||||||
});
|
|
||||||
|
|
||||||
await addTokenButton.click();
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
// Placeholder checks
|
// Placeholder checks
|
||||||
@@ -471,38 +504,34 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
await expect(submitButton).toBeEnabled();
|
await expect(submitButton).toBeEnabled();
|
||||||
await submitButton.click();
|
await submitButton.click();
|
||||||
|
|
||||||
await expect(
|
await unfoldTokenTree(tokensSidebar, "color", "color.primary");
|
||||||
tokensTabPanel.getByRole("button", {
|
|
||||||
name: "color.primary",
|
|
||||||
}),
|
|
||||||
).toBeEnabled();
|
|
||||||
|
|
||||||
// Create token referencing the previous one with keyboard
|
// Create token referencing the previous one with keyboard
|
||||||
|
|
||||||
await tokensTabPanel
|
await tokensSidebar
|
||||||
.getByRole("button", { name: "Add Token: Color" })
|
.getByRole("button", { name: "Add Token: Color" })
|
||||||
.click();
|
.click();
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
await nameField.click();
|
await nameField.click();
|
||||||
await nameField.fill("color.secondary");
|
await nameField.fill("secondary");
|
||||||
await nameField.press("Tab");
|
await nameField.press("Tab");
|
||||||
|
|
||||||
await valueField.click();
|
await valueField.click();
|
||||||
await valueField.fill("{color.primary}");
|
await valueField.fill("{color.primary}");
|
||||||
|
|
||||||
await expect(submitButton).toBeEnabled();
|
await expect(submitButton).toBeEnabled();
|
||||||
await nameField.press("Enter");
|
await submitButton.press("Enter");
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
tokensTabPanel.getByRole("button", {
|
tokensSidebar.getByRole("button", {
|
||||||
name: "color.secondary",
|
name: "secondary",
|
||||||
}),
|
}),
|
||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
|
|
||||||
// Tokens tab panel should have two tokens with the color red / #ff0000
|
// Tokens tab panel should have two tokens with the color red / #ff0000
|
||||||
await expect(
|
await expect(
|
||||||
tokensTabPanel.getByRole("button", { name: "#ff0000" }),
|
tokensSidebar.getByRole("button", { name: "#ff0000" }),
|
||||||
).toHaveCount(2);
|
).toHaveCount(2);
|
||||||
|
|
||||||
// Global set has been auto created and is active
|
// Global set has been auto created and is active
|
||||||
@@ -518,7 +547,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
).toHaveAttribute("aria-checked", "true");
|
).toHaveAttribute("aria-checked", "true");
|
||||||
|
|
||||||
// Check color picker
|
// Check color picker
|
||||||
await tokensTabPanel
|
await tokensSidebar
|
||||||
.getByRole("button", { name: "Add Token: Color" })
|
.getByRole("button", { name: "Add Token: Color" })
|
||||||
.click();
|
.click();
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
@@ -1507,24 +1536,15 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
test("User edits token and auto created set show up in the sidebar", async ({
|
test("User edits token and auto created set show up in the sidebar", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
|
||||||
workspacePage,
|
await setupTokensFile(page);
|
||||||
tokensUpdateCreateModal,
|
|
||||||
tokenThemesSetsSidebar,
|
|
||||||
tokensSidebar,
|
|
||||||
tokenContextMenuForToken,
|
|
||||||
} = await setupTokensFile(page);
|
|
||||||
|
|
||||||
await expect(tokensSidebar).toBeVisible();
|
await expect(tokensSidebar).toBeVisible();
|
||||||
|
|
||||||
const tokensColorGroup = tokensSidebar.getByRole("button", {
|
await unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
|
||||||
name: "Color 92",
|
|
||||||
});
|
|
||||||
await expect(tokensColorGroup).toBeVisible();
|
|
||||||
await tokensColorGroup.click();
|
|
||||||
|
|
||||||
const colorToken = tokensSidebar.getByRole("button", {
|
const colorToken = tokensSidebar.getByRole("button", {
|
||||||
name: "colors.blue.100",
|
name: "100",
|
||||||
});
|
});
|
||||||
await expect(colorToken).toBeVisible();
|
await expect(colorToken).toBeVisible();
|
||||||
await colorToken.click({ button: "right" });
|
await colorToken.click({ button: "right" });
|
||||||
@@ -1541,8 +1561,10 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||||
|
|
||||||
|
await unfoldTokenTree(tokensSidebar, "color", "colors.blue.100.changed");
|
||||||
|
|
||||||
const colorTokenChanged = tokensSidebar.getByRole("button", {
|
const colorTokenChanged = tokensSidebar.getByRole("button", {
|
||||||
name: "colors.blue.100.changed",
|
name: "changed",
|
||||||
});
|
});
|
||||||
await expect(colorTokenChanged).toBeVisible();
|
await expect(colorTokenChanged).toBeVisible();
|
||||||
});
|
});
|
||||||
@@ -1633,11 +1655,10 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("User creates grouped color token", async ({ page }) => {
|
test("User creates grouped color token", async ({ page }) => {
|
||||||
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
|
||||||
await setupEmptyTokensFile(page);
|
await setupEmptyTokensFile(page);
|
||||||
|
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
await tokensSidebar
|
||||||
await tokensTabPanel
|
|
||||||
.getByRole("button", { name: "Add Token: Color" })
|
.getByRole("button", { name: "Add Token: Color" })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
@@ -1649,7 +1670,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
||||||
|
|
||||||
await nameField.click();
|
await nameField.click();
|
||||||
await nameField.fill("color.dark.primary");
|
await nameField.fill("dark.primary");
|
||||||
|
|
||||||
await valueField.click();
|
await valueField.click();
|
||||||
await valueField.fill("red");
|
await valueField.fill("red");
|
||||||
@@ -1660,7 +1681,9 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
await expect(submitButton).toBeEnabled();
|
await expect(submitButton).toBeEnabled();
|
||||||
await submitButton.click();
|
await submitButton.click();
|
||||||
|
|
||||||
await expect(tokensTabPanel.getByLabel("color.dark.primary")).toBeEnabled();
|
await unfoldTokenTree(tokensSidebar, "color", "dark.primary");
|
||||||
|
|
||||||
|
await expect(tokensSidebar.getByLabel("primary")).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("User cant create regular token with value missing", async ({
|
test("User cant create regular token with value missing", async ({
|
||||||
@@ -1676,7 +1699,6 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
|
||||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
name: "Save",
|
name: "Save",
|
||||||
});
|
});
|
||||||
@@ -1686,7 +1708,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
|
|
||||||
// Fill in name but leave value empty
|
// Fill in name but leave value empty
|
||||||
await nameField.click();
|
await nameField.click();
|
||||||
await nameField.fill("color.primary");
|
await nameField.fill("primary");
|
||||||
|
|
||||||
// Submit button should remain disabled when value is empty
|
// Submit button should remain disabled when value is empty
|
||||||
await expect(submitButton).toBeDisabled();
|
await expect(submitButton).toBeDisabled();
|
||||||
@@ -1704,7 +1726,6 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
.click();
|
.click();
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
||||||
|
|
||||||
await valueField.click();
|
await valueField.click();
|
||||||
@@ -1754,15 +1775,10 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
|
|
||||||
await expect(tokensSidebar).toBeVisible();
|
await expect(tokensSidebar).toBeVisible();
|
||||||
|
|
||||||
const tokensColorGroup = tokensSidebar.getByRole("button", {
|
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
|
||||||
name: "Color 92",
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(tokensColorGroup).toBeVisible();
|
|
||||||
await tokensColorGroup.click();
|
|
||||||
|
|
||||||
const colorToken = tokensSidebar.getByRole("button", {
|
const colorToken = tokensSidebar.getByRole("button", {
|
||||||
name: "colors.blue.100",
|
name: "100",
|
||||||
});
|
});
|
||||||
|
|
||||||
await colorToken.click({ button: "right" });
|
await colorToken.click({ button: "right" });
|
||||||
@@ -1782,15 +1798,10 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
|
|
||||||
await expect(tokensSidebar).toBeVisible();
|
await expect(tokensSidebar).toBeVisible();
|
||||||
|
|
||||||
const tokensColorGroup = tokensSidebar.getByRole("button", {
|
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
|
||||||
name: "Color 92",
|
|
||||||
});
|
|
||||||
await expect(tokensColorGroup).toBeVisible();
|
|
||||||
|
|
||||||
await tokensColorGroup.click();
|
|
||||||
|
|
||||||
const colorToken = tokensSidebar.getByRole("button", {
|
const colorToken = tokensSidebar.getByRole("button", {
|
||||||
name: "colors.blue.100",
|
name: "100",
|
||||||
});
|
});
|
||||||
await expect(colorToken).toBeVisible();
|
await expect(colorToken).toBeVisible();
|
||||||
await colorToken.click({ button: "right" });
|
await colorToken.click({ button: "right" });
|
||||||
@@ -1803,8 +1814,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("User fold/unfold color tokens", async ({ page }) => {
|
test("User fold/unfold color tokens", async ({ page }) => {
|
||||||
const { tokensSidebar, tokenContextMenuForToken } =
|
const { tokensSidebar } = await setupTokensFile(page);
|
||||||
await setupTokensFile(page);
|
|
||||||
|
|
||||||
await expect(tokensSidebar).toBeVisible();
|
await expect(tokensSidebar).toBeVisible();
|
||||||
|
|
||||||
@@ -1814,8 +1824,10 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
await expect(tokensColorGroup).toBeVisible();
|
await expect(tokensColorGroup).toBeVisible();
|
||||||
await tokensColorGroup.click();
|
await tokensColorGroup.click();
|
||||||
|
|
||||||
|
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
|
||||||
|
|
||||||
const colorToken = tokensSidebar.getByRole("button", {
|
const colorToken = tokensSidebar.getByRole("button", {
|
||||||
name: "colors.blue.100",
|
name: "100",
|
||||||
});
|
});
|
||||||
await expect(colorToken).toBeVisible();
|
await expect(colorToken).toBeVisible();
|
||||||
await tokensColorGroup.click();
|
await tokensColorGroup.click();
|
||||||
@@ -2218,13 +2230,10 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||||
await tokensTabButton.click();
|
await tokensTabButton.click();
|
||||||
|
|
||||||
await tokensSidebar
|
unfoldTokenTree(tokensSidebar, "color", "colors.black");
|
||||||
.getByRole("button")
|
|
||||||
.filter({ hasText: "Color" })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await tokensSidebar
|
await tokensSidebar
|
||||||
.getByRole("button", { name: "colors.black" })
|
.getByRole("button", { name: "black" })
|
||||||
.click({ button: "right" });
|
.click({ button: "right" });
|
||||||
await tokenContextMenuForToken.getByText("Fill").click();
|
await tokenContextMenuForToken.getByText("Fill").click();
|
||||||
|
|
||||||
@@ -2462,7 +2471,7 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
await nameField.fill("shadow.primary");
|
await nameField.fill("primary");
|
||||||
|
|
||||||
// User adds first shadow with a color from the color ramp
|
// User adds first shadow with a color from the color ramp
|
||||||
const firstShadowFields = tokensUpdateCreateModal.getByTestId(
|
const firstShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||||
@@ -2709,9 +2718,11 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
await submitButton.click();
|
await submitButton.click();
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||||
|
|
||||||
|
unfoldTokenTree(tokensSidebar, "shadow", "primary");
|
||||||
|
|
||||||
// Verify token appears in sidebar
|
// Verify token appears in sidebar
|
||||||
const shadowToken = tokensSidebar.getByRole("button", {
|
const shadowToken = tokensSidebar.getByRole("button", {
|
||||||
name: "shadow.primary",
|
name: "primary",
|
||||||
});
|
});
|
||||||
await expect(shadowToken).toBeEnabled();
|
await expect(shadowToken).toBeEnabled();
|
||||||
|
|
||||||
|
|||||||
49
frontend/src/app/main/ui/ds/layers/layer_button.cljs
Normal file
49
frontend/src/app/main/ui/ds/layers/layer_button.cljs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.main.ui.ds.layers.layer-button
|
||||||
|
(:require-macros
|
||||||
|
[app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(def ^:private schema:layer-button
|
||||||
|
[:map
|
||||||
|
[:label :string]
|
||||||
|
[:description {:optional true} [:maybe :string]]
|
||||||
|
[:class {:optional true} :string]
|
||||||
|
[:expandable {:optional true} :boolean]
|
||||||
|
[:expanded {:optional true} :boolean]
|
||||||
|
[:icon {:optional true} :string]
|
||||||
|
[:on-toggle-expand fn?]])
|
||||||
|
|
||||||
|
(mf/defc layer-button*
|
||||||
|
{::mf/schema schema:layer-button}
|
||||||
|
[{:keys [label description class is-expandable expanded icon on-toggle-expand children] :rest props}]
|
||||||
|
(let [button-props (mf/spread-props props
|
||||||
|
{:class [class (stl/css-case :layer-button true
|
||||||
|
:layer-button--expandable is-expandable
|
||||||
|
:layer-button--expanded expanded)]
|
||||||
|
:type "button"
|
||||||
|
:on-click on-toggle-expand})]
|
||||||
|
[:div {:class (stl/css :layer-button-wrapper)}
|
||||||
|
[:> "button" button-props
|
||||||
|
[:div {:class (stl/css :layer-button-content)}
|
||||||
|
(when is-expandable
|
||||||
|
(if expanded
|
||||||
|
[:> icon* {:icon-id i/arrow-down :class (stl/css :folder-node-icon)}]
|
||||||
|
[:> icon* {:icon-id i/arrow-right :class (stl/css :folder-node-icon)}]))
|
||||||
|
(when icon
|
||||||
|
[:> icon* {:icon-id icon :class (stl/css :layer-button-icon)}])
|
||||||
|
[:span {:class (stl/css :layer-button-name)}
|
||||||
|
label]
|
||||||
|
(when description
|
||||||
|
[:span {:class (stl/css :layer-button-description)}
|
||||||
|
description])
|
||||||
|
[:span {:class (stl/css :layer-button-quantity)}]]]
|
||||||
|
[:div {:class (stl/css :layer-button-actions)}
|
||||||
|
children]]))
|
||||||
56
frontend/src/app/main/ui/ds/layers/layer_button.scss
Normal file
56
frontend/src/app/main/ui/ds/layers/layer_button.scss
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
@use "ds/_borders.scss" as *;
|
||||||
|
@use "ds/_sizes.scss" as *;
|
||||||
|
@use "ds/typography.scss" as *;
|
||||||
|
@use "ds/colors.scss" as *;
|
||||||
|
|
||||||
|
.layer-button-wrapper {
|
||||||
|
--layer-button-block-size: #{$sz-32};
|
||||||
|
--layer-button-background: var(--color-background-primary);
|
||||||
|
--layer-button-text: var(--color-foreground-secondary);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
block-size: var(--layer-button-block-size);
|
||||||
|
|
||||||
|
background: var(--layer-button-background);
|
||||||
|
color: var(--layer-button-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-button {
|
||||||
|
@include use-typography("body-small");
|
||||||
|
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-button--expanded {
|
||||||
|
& .layer-button-name {
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-button-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-button-description {
|
||||||
|
padding: var(--sp-xs);
|
||||||
|
background-color: var(--color-background-tertiary);
|
||||||
|
border-radius: $br-6;
|
||||||
|
}
|
||||||
@@ -44,6 +44,39 @@
|
|||||||
[(seq (array/sort! empty))
|
[(seq (array/sort! empty))
|
||||||
(seq (array/sort! filled))]))))
|
(seq (array/sort! filled))]))))
|
||||||
|
|
||||||
|
(mf/defc selected-set-info*
|
||||||
|
{::mf/private true}
|
||||||
|
[{:keys [tokens-lib selected-token-set-id]}]
|
||||||
|
(let [selected-token-set
|
||||||
|
(mf/with-memo [tokens-lib]
|
||||||
|
(when selected-token-set-id
|
||||||
|
(some-> tokens-lib (ctob/get-set selected-token-set-id))))
|
||||||
|
|
||||||
|
active-token-sets-names
|
||||||
|
(mf/with-memo [tokens-lib]
|
||||||
|
(some-> tokens-lib (ctob/get-active-themes-set-names)))
|
||||||
|
|
||||||
|
token-set-active?
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps active-token-sets-names)
|
||||||
|
(fn [name]
|
||||||
|
(contains? active-token-sets-names name)))]
|
||||||
|
[:div {:class (stl/css :sets-header-container)}
|
||||||
|
[:> text* {:as "span"
|
||||||
|
:typography "headline-small"
|
||||||
|
:class (stl/css :sets-header)}
|
||||||
|
(tr "workspace.tokens.tokens-section-title" (ctob/get-name selected-token-set))]
|
||||||
|
[: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")]])]]))
|
||||||
|
|
||||||
(mf/defc tokens-section*
|
(mf/defc tokens-section*
|
||||||
{::mf/private true}
|
{::mf/private true}
|
||||||
[{:keys [tokens-lib active-tokens resolved-active-tokens]}]
|
[{:keys [tokens-lib active-tokens resolved-active-tokens]}]
|
||||||
@@ -65,9 +98,7 @@
|
|||||||
selected-token-set-id
|
selected-token-set-id
|
||||||
(mf/deref refs/selected-token-set-id)
|
(mf/deref refs/selected-token-set-id)
|
||||||
|
|
||||||
selected-token-set
|
|
||||||
(when selected-token-set-id
|
|
||||||
(some-> tokens-lib (ctob/get-set selected-token-set-id)))
|
|
||||||
|
|
||||||
;; If we have not selected any set explicitly we just
|
;; If we have not selected any set explicitly we just
|
||||||
;; select the first one from the list of sets
|
;; select the first one from the list of sets
|
||||||
@@ -92,15 +123,9 @@
|
|||||||
tokens)]
|
tokens)]
|
||||||
(ctob/group-by-type tokens)))
|
(ctob/group-by-type tokens)))
|
||||||
|
|
||||||
active-token-sets-names
|
|
||||||
(mf/with-memo [tokens-lib]
|
|
||||||
(some-> tokens-lib (ctob/get-active-themes-set-names)))
|
|
||||||
|
|
||||||
token-set-active?
|
|
||||||
(mf/use-fn
|
|
||||||
(mf/deps active-token-sets-names)
|
|
||||||
(fn [name]
|
|
||||||
(contains? active-token-sets-names name)))
|
|
||||||
|
|
||||||
[empty-group filled-group]
|
[empty-group filled-group]
|
||||||
(mf/with-memo [tokens-by-type]
|
(mf/with-memo [tokens-by-type]
|
||||||
@@ -118,34 +143,27 @@
|
|||||||
|
|
||||||
[:*
|
[:*
|
||||||
[:& token-context-menu]
|
[:& token-context-menu]
|
||||||
[:div {:class (stl/css :sets-header-container)}
|
|
||||||
[:> text* {:as "span" :typography "headline-small" :class (stl/css :sets-header)} (tr "workspace.tokens.tokens-section-title" (ctob/get-name selected-token-set))]
|
[:& selected-set-info* {:tokens-lib tokens-lib
|
||||||
[:div {:class (stl/css :sets-header-status) :title (tr "workspace.tokens.inactive-set-description")}
|
:selected-token-set-id selected-token-set-id}]
|
||||||
;; 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")]])]]
|
|
||||||
|
|
||||||
(for [type filled-group]
|
(for [type filled-group]
|
||||||
(let [tokens (get tokens-by-type type)]
|
(let [tokens (get tokens-by-type type)]
|
||||||
[:> token-group* {:key (name type)
|
[:> token-group* {:key (name type)
|
||||||
:is-open (get open-status type false)
|
:tokens tokens
|
||||||
|
:is-expanded (get open-status type false)
|
||||||
:type type
|
:type type
|
||||||
:selected-ids selected
|
:selected-ids selected
|
||||||
:selected-shapes selected-shapes
|
:selected-shapes selected-shapes
|
||||||
:is-selected-inside-layout is-selected-inside-layout
|
:is-selected-inside-layout is-selected-inside-layout
|
||||||
:active-theme-tokens resolved-active-tokens
|
:active-theme-tokens resolved-active-tokens
|
||||||
:tokens tokens}]))
|
:tokens-lib tokens-lib
|
||||||
|
:selected-token-set-id selected-token-set-id}]))
|
||||||
|
|
||||||
(for [type empty-group]
|
(for [type empty-group]
|
||||||
[:> token-group* {:key (name type)
|
[:> token-group* {:key (name type)
|
||||||
|
:tokens []
|
||||||
:type type
|
:type type
|
||||||
:selected-shapes selected-shapes
|
:selected-shapes selected-shapes
|
||||||
:is-selected-inside-layout :is-selected-inside-layout
|
:is-selected-inside-layout is-selected-inside-layout
|
||||||
:active-theme-tokens resolved-active-tokens
|
:active-theme-tokens resolved-active-tokens}])]))
|
||||||
:tokens []}])]))
|
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
(ns app.main.ui.workspace.tokens.management.group
|
(ns app.main.ui.workspace.tokens.management.group
|
||||||
(:require-macros [app.main.style :as stl])
|
(:require-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.types.tokens-lib :as ctob]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.workspace.tokens.application :as dwta]
|
[app.main.data.workspace.tokens.application :as dwta]
|
||||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||||
@@ -16,51 +19,70 @@
|
|||||||
[app.main.ui.context :as ctx]
|
[app.main.ui.context :as ctx]
|
||||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||||
[app.main.ui.workspace.sidebar.assets.common :as cmm]
|
[app.main.ui.ds.layers.layer-button :refer [layer-button*]]
|
||||||
[app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]]
|
[app.main.ui.workspace.tokens.management.token-tree :refer [token-tree*]]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :refer [tr]]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
|
||||||
(defn token-section-icon
|
(defn token-section-icon
|
||||||
[type]
|
[type]
|
||||||
(case type
|
(case type
|
||||||
:border-radius "corner-radius"
|
:border-radius i/corner-radius
|
||||||
:color "drop"
|
:color i/drop
|
||||||
:boolean "boolean-difference"
|
:boolean i/boolean-difference
|
||||||
:font-family "text-font-family"
|
:font-family i/text-font-family
|
||||||
:font-size "text-font-size"
|
:font-size i/text-font-size
|
||||||
:letter-spacing "text-letterspacing"
|
:letter-spacing i/text-letterspacing
|
||||||
:text-case "text-mixed"
|
:text-case i/text-mixed
|
||||||
:text-decoration "text-underlined"
|
:text-decoration i/text-underlined
|
||||||
:font-weight "text-font-weight"
|
:font-weight i/text-font-weight
|
||||||
:typography "text-typography"
|
:typography i/text-typography
|
||||||
:opacity "percentage"
|
:opacity i/percentage
|
||||||
:number "number"
|
:number i/number
|
||||||
:rotation "rotation"
|
:rotation i/rotation
|
||||||
:spacing "padding-extended"
|
:spacing i/padding-extended
|
||||||
:string "text-mixed"
|
:string i/text-mixed
|
||||||
:stroke-width "stroke-size"
|
:stroke-width i/stroke-size
|
||||||
:dimensions "expand"
|
:dimensions i/expand
|
||||||
:sizing "expand"
|
:sizing i/expand
|
||||||
:shadow "drop-shadow"
|
:shadow i/drop-shadow
|
||||||
"add"))
|
"add"))
|
||||||
|
|
||||||
|
(def ^:private schema:token-group
|
||||||
|
[:map
|
||||||
|
[:type :keyword]
|
||||||
|
[:tokens :any]
|
||||||
|
[:selected-shapes :any]
|
||||||
|
[:is-selected-inside-layout {:optional true} [:maybe :boolean]]
|
||||||
|
[:active-theme-tokens {:optional true} :any]
|
||||||
|
[:selected-token-set-id {:optional true} :any]
|
||||||
|
[:tokens-lib {:optional true} :any]
|
||||||
|
[:on-token-pill-click {:optional true} fn?]
|
||||||
|
[:on-context-menu {:optional true} fn?]])
|
||||||
|
|
||||||
(mf/defc token-group*
|
(mf/defc token-group*
|
||||||
{::mf/private true}
|
{::mf/schema schema:token-group}
|
||||||
[{:keys [type tokens selected-shapes is-selected-inside-layout active-theme-tokens is-open selected-ids]}]
|
[{:keys [type tokens selected-shapes is-selected-inside-layout active-theme-tokens selected-token-set-id tokens-lib is-expanded selected-ids]}]
|
||||||
(let [{:keys [modal title]}
|
(let [{:keys [modal title]}
|
||||||
(get dwta/token-properties type)
|
(get dwta/token-properties type)
|
||||||
editing-ref (mf/deref refs/workspace-editor-state)
|
editing-ref (mf/deref refs/workspace-editor-state)
|
||||||
not-editing? (empty? editing-ref)
|
not-editing? (empty? editing-ref)
|
||||||
|
|
||||||
|
is-expanded (d/nilv is-expanded false)
|
||||||
|
|
||||||
can-edit?
|
can-edit?
|
||||||
(mf/use-ctx ctx/can-edit?)
|
(mf/use-ctx ctx/can-edit?)
|
||||||
|
|
||||||
|
is-selected-inside-layout (d/nilv is-selected-inside-layout false)
|
||||||
|
|
||||||
tokens
|
tokens
|
||||||
(mf/with-memo [tokens]
|
(mf/with-memo [tokens]
|
||||||
(vec (sort-by :name tokens)))
|
(vec (sort-by :name tokens)))
|
||||||
|
|
||||||
|
expandable? (d/nilv (seq tokens) false)
|
||||||
|
|
||||||
on-context-menu
|
on-context-menu
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [event token]
|
(fn [event token]
|
||||||
@@ -73,8 +95,8 @@
|
|||||||
|
|
||||||
on-toggle-open-click
|
on-toggle-open-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps is-open type)
|
(mf/deps is-expanded type)
|
||||||
#(st/emit! (dwtl/set-token-type-section-open type (not is-open))))
|
#(st/emit! (dwtl/set-token-type-section-open type (not is-expanded))))
|
||||||
|
|
||||||
on-popover-open-click
|
on-popover-open-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@@ -96,33 +118,36 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps not-editing? selected-ids)
|
(mf/deps not-editing? selected-ids)
|
||||||
(fn [event token]
|
(fn [event token]
|
||||||
|
(let [token (ctob/get-token tokens-lib selected-token-set-id (:id token))]
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(when (and not-editing? (seq selected-shapes) (not= (:type token) :number))
|
(when (and not-editing? (seq selected-shapes) (not= (:type token) :number))
|
||||||
(st/emit! (dwta/toggle-token {:token token
|
(st/emit! (dwta/toggle-token {:token token
|
||||||
:shape-ids selected-ids})))))]
|
:shape-ids selected-ids}))))))]
|
||||||
|
|
||||||
[:div {:on-click on-toggle-open-click :class (stl/css :token-section-wrapper)}
|
[:div {:class (stl/css :token-section-wrapper)
|
||||||
[:> cmm/asset-section* {:icon (token-section-icon type)
|
:data-testid (dm/str "section-" (name type))}
|
||||||
:title title
|
[:> layer-button* {:label title
|
||||||
:section :tokens
|
:expanded is-expanded
|
||||||
:assets-count (count tokens)
|
:description (when expandable? (dm/str (count tokens)))
|
||||||
:is-open is-open}
|
:is-expandable expandable?
|
||||||
[:> cmm/asset-section-block* {:role :title-button}
|
:aria-expanded is-expanded
|
||||||
|
:aria-controls (dm/str "token-tree-" (name type))
|
||||||
|
:on-toggle-expand on-toggle-open-click
|
||||||
|
:icon (token-section-icon type)}
|
||||||
(when can-edit?
|
(when can-edit?
|
||||||
[:> icon-button* {:on-click on-popover-open-click
|
[:> icon-button* {:id (str "add-token-button-" title)
|
||||||
|
:icon "add"
|
||||||
|
:aria-label (tr "workspace.tokens.add-token" title)
|
||||||
:variant "ghost"
|
:variant "ghost"
|
||||||
:icon i/add
|
:on-click on-popover-open-click
|
||||||
:id (str "add-token-button-" title)
|
:class (stl/css :token-section-icon)}])]
|
||||||
:aria-label (tr "workspace.tokens.add-token" title)}])]
|
(when is-expanded
|
||||||
(when is-open
|
[:> token-tree* {:tokens tokens
|
||||||
[:> cmm/asset-section-block* {:role :content}
|
:id (dm/str "token-tree-" (name type))
|
||||||
[:div {:class (stl/css :token-pills-wrapper)}
|
:tokens-lib tokens-lib
|
||||||
(for [token tokens]
|
|
||||||
[:> token-pill*
|
|
||||||
{:key (:name token)
|
|
||||||
:token token
|
|
||||||
:selected-shapes selected-shapes
|
:selected-shapes selected-shapes
|
||||||
:is-selected-inside-layout is-selected-inside-layout
|
|
||||||
:active-theme-tokens active-theme-tokens
|
:active-theme-tokens active-theme-tokens
|
||||||
:on-click on-token-pill-click
|
:selected-token-set-id selected-token-set-id
|
||||||
:on-context-menu on-context-menu}])]])]]))
|
:is-selected-inside-layout is-selected-inside-layout
|
||||||
|
:on-token-pill-click on-token-pill-click
|
||||||
|
:on-context-menu on-context-menu}])]))
|
||||||
|
|||||||
@@ -1,11 +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
|
|
||||||
|
|
||||||
.token-pills-wrapper {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--sp-xs);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
@@ -307,10 +307,9 @@
|
|||||||
:class (stl/css :token-pill-icon)}])
|
:class (stl/css :token-pill-icon)}])
|
||||||
|
|
||||||
(if contains-path?
|
(if contains-path?
|
||||||
(let [[first-part last-part] (cpn/split-by-last-period name)]
|
(let [[_ last-part] (cpn/split-by-last-period name)]
|
||||||
[:span {:class (stl/css :divided-name-wrapper)
|
[:span {:class (stl/css :divided-name-wrapper)
|
||||||
:aria-label name}
|
:aria-label name}
|
||||||
[:span {:class (stl/css :first-name-wrapper)} first-part]
|
|
||||||
[:span {:class (stl/css :last-name-wrapper)} last-part]])
|
[:span {:class (stl/css :last-name-wrapper)} last-part]])
|
||||||
[:span {:class (stl/css :name-wrapper)
|
[:span {:class (stl/css :name-wrapper)
|
||||||
:aria-label name}
|
:aria-label name}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
;; 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.token-tree
|
||||||
|
(:require-macros [app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
[app.common.path-names :as cpn]
|
||||||
|
[app.common.types.tokens-lib :as ctob]
|
||||||
|
[app.main.ui.ds.layers.layer-button :refer [layer-button*]]
|
||||||
|
[app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(def ^:private schema:folder-node
|
||||||
|
[:map
|
||||||
|
[:node :any]
|
||||||
|
[:selected-shapes :any]
|
||||||
|
[:is-selected-inside-layout {:optional true} :boolean]
|
||||||
|
[:active-theme-tokens {:optional true} :any]
|
||||||
|
[:selected-token-set-id {:optional true} :any]
|
||||||
|
[:tokens-lib {:optional true} :any]
|
||||||
|
[:on-token-pill-click {:optional true} fn?]
|
||||||
|
[:on-context-menu {:optional true} fn?]])
|
||||||
|
|
||||||
|
(mf/defc folder-node*
|
||||||
|
{::mf/schema schema:folder-node}
|
||||||
|
[{:keys [node selected-shapes is-selected-inside-layout active-theme-tokens selected-token-set-id tokens-lib on-token-pill-click on-context-menu]}]
|
||||||
|
(let [expanded* (mf/use-state false)
|
||||||
|
expanded (deref expanded*)
|
||||||
|
swap-folder-expanded #(swap! expanded* not)]
|
||||||
|
[:li {:class (stl/css :folder-node)}
|
||||||
|
[:> layer-button* {:label (:name node)
|
||||||
|
:expanded expanded
|
||||||
|
:aria-expanded expanded
|
||||||
|
:aria-controls (str "folder-children-" (:path node))
|
||||||
|
:is-expandable (not (:leaf node))
|
||||||
|
:on-toggle-expand swap-folder-expanded}]
|
||||||
|
(when expanded
|
||||||
|
(let [children-fn (:children-fn node)]
|
||||||
|
[:div {:class (stl/css :folder-children-wrapper)
|
||||||
|
:id (str "folder-children-" (:path node))}
|
||||||
|
(when children-fn
|
||||||
|
(let [children (children-fn)]
|
||||||
|
(for [child children]
|
||||||
|
(if (not (:leaf child))
|
||||||
|
[:ul {:class (stl/css :node-parent)}
|
||||||
|
[:> folder-node* {:key (:path child)
|
||||||
|
:node child
|
||||||
|
:selected-shapes selected-shapes
|
||||||
|
:is-selected-inside-layout is-selected-inside-layout
|
||||||
|
:active-theme-tokens active-theme-tokens
|
||||||
|
:on-token-pill-click on-token-pill-click
|
||||||
|
:on-context-menu on-context-menu
|
||||||
|
:tokens-lib tokens-lib
|
||||||
|
:selected-token-set-id selected-token-set-id}]]
|
||||||
|
(let [id (:id (:leaf child))
|
||||||
|
token (ctob/get-token tokens-lib selected-token-set-id id)]
|
||||||
|
[:> token-pill*
|
||||||
|
{:key id
|
||||||
|
:token token
|
||||||
|
:selected-shapes selected-shapes
|
||||||
|
:is-selected-inside-layout is-selected-inside-layout
|
||||||
|
:active-theme-tokens active-theme-tokens
|
||||||
|
:on-click on-token-pill-click
|
||||||
|
:on-context-menu on-context-menu}])))))]))]))
|
||||||
|
|
||||||
|
(def ^:private schema:token-tree
|
||||||
|
[:map
|
||||||
|
[:tokens :any]
|
||||||
|
[:selected-shapes :any]
|
||||||
|
[:is-selected-inside-layout {:optional true} :boolean]
|
||||||
|
[:active-theme-tokens {:optional true} :any]
|
||||||
|
[:selected-token-set-id {:optional true} :any]
|
||||||
|
[:tokens-lib {:optional true} :any]
|
||||||
|
[:on-token-pill-click {:optional true} fn?]
|
||||||
|
[:on-context-menu {:optional true} fn?]])
|
||||||
|
|
||||||
|
(mf/defc token-tree*
|
||||||
|
{::mf/schema schema:token-tree}
|
||||||
|
[{:keys [tokens selected-shapes is-selected-inside-layout active-theme-tokens tokens-lib selected-token-set-id on-token-pill-click on-context-menu]}]
|
||||||
|
(let [separator "."
|
||||||
|
tree (mf/use-memo
|
||||||
|
(mf/deps tokens)
|
||||||
|
(fn []
|
||||||
|
(cpn/build-tree-root tokens separator)))]
|
||||||
|
[:div {:class (stl/css :token-tree-wrapper)}
|
||||||
|
(for [node tree]
|
||||||
|
[:ul {:class (stl/css :node-parent)
|
||||||
|
:key (:path node)
|
||||||
|
:style {:--node-depth (inc (:depth node))}}
|
||||||
|
(if (:leaf node)
|
||||||
|
(let [token (ctob/get-token tokens-lib selected-token-set-id (get-in node [:leaf :id]))]
|
||||||
|
[:> token-pill*
|
||||||
|
{:token token
|
||||||
|
:selected-shapes selected-shapes
|
||||||
|
:is-selected-inside-layout is-selected-inside-layout
|
||||||
|
:active-theme-tokens active-theme-tokens
|
||||||
|
:on-click on-token-pill-click
|
||||||
|
:on-context-menu on-context-menu}])
|
||||||
|
;; Render segment folder
|
||||||
|
[:> folder-node* {:node node
|
||||||
|
:selected-shapes selected-shapes
|
||||||
|
:is-selected-inside-layout is-selected-inside-layout
|
||||||
|
:active-theme-tokens active-theme-tokens
|
||||||
|
:on-token-pill-click on-token-pill-click
|
||||||
|
:on-context-menu on-context-menu
|
||||||
|
:tokens-lib tokens-lib
|
||||||
|
:selected-token-set-id selected-token-set-id}])])]))
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
@use "ds/_borders.scss" as *;
|
||||||
|
|
||||||
|
.token-tree-wrapper {
|
||||||
|
padding-block-end: var(--sp-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-parent {
|
||||||
|
--node-spacing: var(--sp-l);
|
||||||
|
--node-depth: 0;
|
||||||
|
|
||||||
|
margin-block-end: 0;
|
||||||
|
padding-inline-start: calc(var(--node-spacing) * var(--node-depth));
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-children-wrapper:has(> button) {
|
||||||
|
margin-inline-start: var(--sp-s);
|
||||||
|
padding-inline-start: var(--sp-s);
|
||||||
|
border-inline-start: $b-2 solid var(--color-background-quaternary);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
column-gap: var(--sp-xs);
|
||||||
|
|
||||||
|
& .node-parent {
|
||||||
|
flex: 1 0 100%;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-block-end: var(--sp-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& .token-pill {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user