mirror of
https://github.com/penpot/penpot.git
synced 2026-02-27 12:16:55 -05:00
Compare commits
15 Commits
niwinz-dev
...
eva-create
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76d85f7fa0 | ||
|
|
c49f9ab080 | ||
|
|
8956a78244 | ||
|
|
12fb7df068 | ||
|
|
9cdfcc1a9b | ||
|
|
be6b547252 | ||
|
|
8941b33ca5 | ||
|
|
908a639b69 | ||
|
|
ccfb3a063e | ||
|
|
071e2cd1a2 | ||
|
|
767a9a114c | ||
|
|
8305084739 | ||
|
|
192f1c8674 | ||
|
|
a7d2688711 | ||
|
|
d561c94c52 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -69,7 +69,7 @@
|
||||
/frontend/test-results/
|
||||
/other/
|
||||
/scripts/
|
||||
/nexus/
|
||||
/telemetry/
|
||||
/tmp/
|
||||
/vendor/**/target
|
||||
/vendor/svgclean/bundle*.js
|
||||
|
||||
@@ -10,12 +10,10 @@
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912)
|
||||
- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112), [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114)
|
||||
- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248)
|
||||
- Option to download custom fonts (by @dfelinto) [Github #8320](https://github.com/penpot/penpot/issues/8320)
|
||||
- Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313)
|
||||
- Import Tokens from linked library [Github #8391](https://github.com/penpot/penpot/pull/8391)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@@ -54,7 +52,6 @@
|
||||
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
|
||||
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
|
||||
- Fix unable to finish the create account form using keyboard [Taiga #11333](https://tree.taiga.io/project/penpot/issue/11333)
|
||||
- Fix 45 rotated board titles rendered incorrectly [Taiga #13306](https://tree.taiga.io/project/penpot/issue/13306)
|
||||
|
||||
## 2.13.3
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key
|
||||
export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key
|
||||
export PENPOT_NEXUS_SHARED_KEY=super-secret-nexus-api-key
|
||||
export PENPOT_SECRET_KEY=super-secret-devenv-key
|
||||
|
||||
# DEPRECATED: only used for subscriptions
|
||||
|
||||
@@ -103,7 +103,6 @@
|
||||
|
||||
[:exporter-shared-key {:optional true} :string]
|
||||
[:nitrate-shared-key {:optional true} :string]
|
||||
[:nexus-shared-key {:optional true} :string]
|
||||
[:management-api-key {:optional true} :string]
|
||||
|
||||
[:telemetry-uri {:optional true} :string]
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
;; an external storage and data cleared.
|
||||
|
||||
(def ^:private schema:event
|
||||
[:map {:title "AuditEvent"}
|
||||
[:map {:title "event"}
|
||||
[::type ::sm/text]
|
||||
[::name ::sm/text]
|
||||
[::profile-id ::sm/uuid]
|
||||
|
||||
@@ -10,11 +10,14 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.client :as http]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[integrant.core :as ig]
|
||||
[lambdaisland.uri :as u]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
;; This is a task responsible to send the accumulated events to
|
||||
@@ -49,18 +52,19 @@
|
||||
|
||||
(defn- send!
|
||||
[{:keys [::uri] :as cfg} events]
|
||||
(let [skey (-> cfg ::setup/shared-keys :nexus)
|
||||
(let [token (tokens/generate cfg
|
||||
{:iss "authentication"
|
||||
:uid uuid/zero})
|
||||
body (t/encode {:events events})
|
||||
headers {"content-type" "application/transit+json"
|
||||
"origin" (str (cf/get :public-uri))
|
||||
"x-shared-key" (str "nexus " skey)}
|
||||
"cookie" (u/map->query-string {:auth-token token})}
|
||||
params {:uri uri
|
||||
:timeout 12000
|
||||
:method :post
|
||||
:headers headers
|
||||
:body body}
|
||||
resp (http/req! cfg params)]
|
||||
|
||||
(if (= (:status resp) 204)
|
||||
true
|
||||
(do
|
||||
@@ -105,7 +109,7 @@
|
||||
(def ^:private schema:handler-params
|
||||
[:map
|
||||
::db/pool
|
||||
::setup/shared-keys
|
||||
::setup/props
|
||||
::http/client])
|
||||
|
||||
(defmethod ig/assert-key ::handler
|
||||
|
||||
@@ -466,17 +466,16 @@
|
||||
|
||||
::setup/shared-keys
|
||||
{::setup/props (ig/ref ::setup/props)
|
||||
:nexus (cf/get :nexus-shared-key)
|
||||
:nitrate (cf/get :nitrate-shared-key)
|
||||
:exporter (cf/get :exporter-shared-key)}
|
||||
:nitrate (cf/get :nitrate-shared-key)
|
||||
:exporter (cf/get :exporter-shared-key)}
|
||||
|
||||
::setup/clock
|
||||
{}
|
||||
|
||||
:app.loggers.audit.archive-task/handler
|
||||
{::setup/shared-keys (ig/ref ::setup/shared-keys)
|
||||
::http.client/client (ig/ref ::http.client/client)
|
||||
::db/pool (ig/ref ::db/pool)}
|
||||
{::setup/props (ig/ref ::setup/props)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::http.client/client (ig/ref ::http.client/client)}
|
||||
|
||||
:app.loggers.audit.gc-task/handler
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
[cfg profile]
|
||||
(try
|
||||
(let [nitrate-licence (call cfg :is-valid-user {:profile-id (:id profile)})]
|
||||
(assoc-in profile [:props :nitrate-license]
|
||||
(select-keys nitrate-licence [:valid :created-at])))
|
||||
(assoc profile :nitrate-licence (:valid nitrate-licence)))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "failed to get nitrate licence"
|
||||
:profile-id (:id profile)
|
||||
|
||||
@@ -82,37 +82,45 @@
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(db/xact-lock! conn 0)
|
||||
(when-not key
|
||||
(l/wrn :hint (str "using autogenerated secret-key, it will change "
|
||||
"on each restart and will invalidate "
|
||||
"all sessions on each restart, it is highly "
|
||||
"recommended setting up the "
|
||||
"PENPOT_SECRET_KEY environment variable")))
|
||||
(l/warn :hint (str "using autogenerated secret-key, it will change on each restart and will invalidate "
|
||||
"all sessions on each restart, it is highly recommended setting up the "
|
||||
"PENPOT_SECRET_KEY environment variable")))
|
||||
(let [secret (or key (generate-random-key))]
|
||||
(-> (get-all-props conn)
|
||||
(assoc :secret-key secret)
|
||||
(assoc :tokens-key (keys/derive secret :salt "tokens"))
|
||||
(update :instance-id handle-instance-id conn (db/read-only? pool)))))))
|
||||
|
||||
(sm/register! ::props [:map-of :keyword ::sm/any])
|
||||
|
||||
|
||||
(defmethod ig/init-key ::shared-keys
|
||||
[_ {:keys [::props] :as cfg}]
|
||||
(let [secret (get props :secret-key)]
|
||||
(reduce (fn [keys id]
|
||||
(let [key (or (get cfg id)
|
||||
(-> (keys/derive secret :salt (name id))
|
||||
(bc/bytes->b64-str true)))]
|
||||
(if (or (str/empty? key)
|
||||
(str/blank? key))
|
||||
(do
|
||||
(l/wrn :id (name id) :hint "key is disabled because empty string found")
|
||||
keys)
|
||||
(do
|
||||
(l/inf :id (name id) :hint "key initialized" :key (d/obfuscate-string key))
|
||||
(assoc keys id key)))))
|
||||
{}
|
||||
[:exporter
|
||||
:nitrate
|
||||
:nexus])))
|
||||
(d/without-nils
|
||||
{:exporter
|
||||
(let [key (or (get cfg :exporter)
|
||||
(-> (keys/derive secret :salt "exporter")
|
||||
(bc/bytes->b64-str true)))]
|
||||
(if (or (str/empty? key)
|
||||
(str/blank? key))
|
||||
(do
|
||||
(l/wrn :hint "exporter key is disabled because empty string found")
|
||||
nil)
|
||||
(do
|
||||
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
|
||||
key)))
|
||||
|
||||
(sm/register! ::props [:map-of :keyword ::sm/any])
|
||||
(sm/register! ::shared-keys [:map-of :keyword ::sm/text])
|
||||
:nitrate
|
||||
(let [key (or (get cfg :nitrate)
|
||||
(-> (keys/derive secret :salt "nitrate")
|
||||
(bc/bytes->b64-str true)))]
|
||||
(if (or (str/empty? key)
|
||||
(str/blank? key))
|
||||
(do
|
||||
(l/wrn :hint "nitrate key is disabled because empty string found")
|
||||
nil)
|
||||
(do
|
||||
(l/inf :hint "nitrate key initialized" :key (d/obfuscate-string key))
|
||||
key)))})))
|
||||
|
||||
|
||||
@@ -119,13 +119,13 @@
|
||||
:strict-session-cookies
|
||||
:telemetry
|
||||
:terms-and-privacy-checkbox
|
||||
;; Only for developtment.
|
||||
:tiered-file-data-storage
|
||||
:token-base-font-size
|
||||
:token-combobox
|
||||
:token-color
|
||||
:token-shadow
|
||||
:token-tokenscript
|
||||
:token-import-from-library
|
||||
;; Only for developtment.
|
||||
:transit-readable-response
|
||||
:user-feedback
|
||||
;; TODO: remove this flag.
|
||||
@@ -181,8 +181,7 @@
|
||||
:enable-token-color
|
||||
:enable-token-shadow
|
||||
:enable-inspect-styles
|
||||
:enable-feature-fdata-objects-map
|
||||
:enable-token-import-from-library])
|
||||
:enable-feature-fdata-objects-map])
|
||||
|
||||
(defn parse
|
||||
[& flags]
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.schema
|
||||
(:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys select-keys])
|
||||
(:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys])
|
||||
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
|
||||
(:require
|
||||
#?(:clj [malli.dev.pretty :as mdp])
|
||||
@@ -93,11 +93,6 @@
|
||||
[& items]
|
||||
(apply mu/merge (map schema items)))
|
||||
|
||||
(defn select-keys
|
||||
[s keys & {:as opts}]
|
||||
(let [s (schema s)]
|
||||
(mu/select-keys s keys opts)))
|
||||
|
||||
(defn assoc-key
|
||||
"Add a key & value to a schema of type [:map]. If the first level node of the schema
|
||||
is not a map, will do a depth search to find the first map node and add the key there."
|
||||
@@ -143,10 +138,10 @@
|
||||
(mu/optional-keys schema keys default-options)))
|
||||
|
||||
(defn required-keys
|
||||
([s]
|
||||
(mu/required-keys (schema s) nil default-options))
|
||||
([s keys]
|
||||
(mu/required-keys (schema s) keys default-options)))
|
||||
([schema]
|
||||
(mu/required-keys schema nil default-options))
|
||||
([schema keys]
|
||||
(mu/required-keys schema keys default-options)))
|
||||
|
||||
(defn transformer
|
||||
[& transformers]
|
||||
@@ -651,7 +646,7 @@
|
||||
{:title "set"
|
||||
:description "Set of Strings"
|
||||
:error/message "should be a set of strings"
|
||||
:gen/gen (sg/mcat (fn [_] (sg/generator kind)) sg/int)
|
||||
:gen/gen (-> kind sg/generator sg/set)
|
||||
:decode/string decode
|
||||
:decode/json decode
|
||||
:encode/string encode-string
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
["date-fns/locale/eu$default" :as dfn-eu]
|
||||
["date-fns/locale/fa-IR$default" :as dfn-fa-ir]
|
||||
["date-fns/locale/fr$default" :as dfn-fr]
|
||||
["date-fns/locale/fr-CA$default" :as dfn-fr-ca]
|
||||
["date-fns/locale/gl$default" :as dfn-gl]
|
||||
["date-fns/locale/he$default" :as dfn-he]
|
||||
["date-fns/locale/hr$default" :as dfn-hr]
|
||||
@@ -253,7 +252,6 @@
|
||||
:fa dfn-fa-ir
|
||||
:fa_ir dfn-fa-ir
|
||||
:fr dfn-fr
|
||||
:fr_ca dfn-fr-ca
|
||||
:he dfn-he
|
||||
:pt dfn-pt
|
||||
:pt_pt dfn-pt
|
||||
|
||||
@@ -43,7 +43,9 @@ test.describe("Tokens: Apply token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFileRender(page, {
|
||||
flags: ["enable-token-combobox", "enable-feature-token-input"],
|
||||
});
|
||||
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
@@ -82,7 +84,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();
|
||||
|
||||
@@ -517,7 +521,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();
|
||||
|
||||
@@ -571,7 +577,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();
|
||||
|
||||
@@ -625,7 +633,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();
|
||||
|
||||
@@ -680,8 +690,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();
|
||||
|
||||
@@ -750,7 +761,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", {
|
||||
|
||||
@@ -30,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 setupEmptyTokensFileRender(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",
|
||||
|
||||
@@ -42,6 +42,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 setupTokensFileRender(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 setupTokensFileRender(page, { flags: ["enable-token-shadow"] });
|
||||
@@ -403,13 +431,21 @@ test.describe("Remapping Tokens", () => {
|
||||
test("User renames border radius token with alias references", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokensSidebar } = await setupTokensFileRender(page);
|
||||
const { tokensSidebar } = await setupTokensFileRender(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",
|
||||
@@ -445,13 +481,21 @@ test.describe("Remapping Tokens", () => {
|
||||
tokensUpdateCreateModal,
|
||||
tokensSidebar,
|
||||
tokenContextMenuForToken,
|
||||
} = await setupTokensFileRender(page);
|
||||
} = await setupTokensFileRender(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",
|
||||
|
||||
@@ -288,7 +288,6 @@ export async function compileTranslations() {
|
||||
"es",
|
||||
"fa",
|
||||
"fr",
|
||||
"fr_CA",
|
||||
"he",
|
||||
"sr",
|
||||
"nb_NO",
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
(:require
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
@@ -17,12 +14,4 @@
|
||||
(rx/map (fn [connectivity]
|
||||
(modal/show popup-type (or connectivity {}))))))))
|
||||
|
||||
(defn go-to-nitrate-cc
|
||||
[]
|
||||
(st/emit! (dom/open-new-window "/control-center/")))
|
||||
|
||||
(defn go-to-nitrate-billing
|
||||
[]
|
||||
(st/emit! (rt/nav-raw :href "/control-center/licenses/billing")))
|
||||
|
||||
|
||||
|
||||
@@ -221,24 +221,6 @@
|
||||
(pcb/delete-color id))]
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
(defn duplicate-color
|
||||
[file-id color-id]
|
||||
(assert (uuid? file-id) "expected valid uuid for `file-id`")
|
||||
(assert (uuid? color-id) "expected valid uuid for `color-id`")
|
||||
|
||||
(ptk/reify ::duplicate-color
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
color (ctl/get-color data color-id)
|
||||
new-color (-> color
|
||||
(assoc :id (uuid/next))
|
||||
(d/without-nils)
|
||||
(ctc/check-library-color))
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/add-color new-color))]
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
;; FIXME: this should be deleted
|
||||
(defn add-media
|
||||
[media]
|
||||
@@ -368,23 +350,6 @@
|
||||
(pcb/delete-typography id))]
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
(defn duplicate-typography
|
||||
[file-id typography-id]
|
||||
(assert (uuid? file-id) "expected valid uuid for `file-id`")
|
||||
(assert (uuid? typography-id) "expected valid uuid for `typography-id`")
|
||||
|
||||
(ptk/reify ::duplicate-typography
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
typography (get-in data [:typographies typography-id])
|
||||
new-typography (-> typography
|
||||
(assoc :id (uuid/next))
|
||||
(ctt/check-typography))
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/add-typography new-typography))]
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
(defn- add-component2
|
||||
"This is the second step of the component creation."
|
||||
([selected]
|
||||
|
||||
@@ -302,8 +302,9 @@
|
||||
on-create-org-click
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(if (dm/get-in profile [:props :nitrate-license :valid])
|
||||
(dnt/go-to-nitrate-cc)
|
||||
(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 :nitrate-form)))))]
|
||||
|
||||
[:> dropdown-menu* props
|
||||
@@ -547,8 +548,9 @@
|
||||
on-create-org-click
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(if (dm/get-in profile [:props :nitrate-license :valid])
|
||||
(dnt/go-to-nitrate-cc)
|
||||
(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 :nitrate-form)))))]
|
||||
(if empty?
|
||||
[:div {:class (stl/css :nitrate-orgs-empty)}
|
||||
@@ -1086,7 +1088,7 @@
|
||||
|
||||
[:*
|
||||
(if (contains? cf/flags :nitrate)
|
||||
(when-not (dm/get-in profile [:props :nitrate-license :valid])
|
||||
(when-not (:nitrate-licence profile)
|
||||
[:> nitrate-sidebar* {:profile profile}])
|
||||
(when (contains? cf/flags :subscriptions)
|
||||
(if (show-subscription-dashboard-banner? profile)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[:map
|
||||
[:class {:optional true} :string]
|
||||
[:tooltip-class {:optional true} [:maybe :string]]
|
||||
[:type {:optional true} [:maybe [:enum "button" "submit" "reset"]]]
|
||||
[:icon-class {:optional true} :string]
|
||||
[:icon
|
||||
[:and :string [:fn #(contains? icon-list %)]]]
|
||||
@@ -29,7 +30,7 @@
|
||||
(mf/defc icon-button*
|
||||
{::mf/schema schema:icon-button
|
||||
::mf/memo true}
|
||||
[{:keys [class icon icon-class variant aria-label children tooltip-placement tooltip-class] :rest props}]
|
||||
[{:keys [class icon icon-class variant aria-label children tooltip-placement tooltip-class type] :rest props}]
|
||||
(let [variant
|
||||
(d/nilv variant "primary")
|
||||
|
||||
@@ -47,6 +48,7 @@
|
||||
props
|
||||
(mf/spread-props props
|
||||
{:class [class button-class]
|
||||
:type (d/nilv type "button")
|
||||
:aria-labelledby tooltip-id})]
|
||||
|
||||
[:> tooltip* {:content aria-label
|
||||
|
||||
@@ -11,6 +11,7 @@ $mint-250: #00d1b8;
|
||||
$mint-700: #426158;
|
||||
$mint-150-60: #7efff599;
|
||||
$mint-250-10: #00d1b81a;
|
||||
$mint-250-70: #00d1b8b3;
|
||||
|
||||
$green-200: #a7e8d9;
|
||||
$green-500: #2d9f8f;
|
||||
@@ -33,6 +34,7 @@ $purple-500: #a977d1;
|
||||
$purple-600: #8c33eb;
|
||||
$purple-700: #6911d4;
|
||||
$purple-600-10: #8c33eb1a;
|
||||
$purple-600-70: #8c33ebb3;
|
||||
$purple-700-60: #6911d499;
|
||||
|
||||
$aqua-200: #ddf7ff;
|
||||
@@ -77,6 +79,7 @@ $grayish-red: #bfbfbf;
|
||||
--color-accent-quaternary: #{$pink-400};
|
||||
--color-accent-overlay: #{$purple-700-60};
|
||||
--color-accent-select: #{$purple-600-10};
|
||||
--color-accent-background-select: #{$purple-600-70};
|
||||
--color-accent-action: #{$purple-400};
|
||||
--color-accent-action-hover: #{$purple-500};
|
||||
--color-accent-off: #{$gray-50};
|
||||
@@ -128,6 +131,7 @@ $grayish-red: #bfbfbf;
|
||||
--color-accent-quaternary: #{$pink-400};
|
||||
--color-accent-overlay: #{$mint-150-60};
|
||||
--color-accent-select: #{$mint-250-10};
|
||||
--color-accent-background-select: #{$mint-250-70};
|
||||
--color-accent-action: #{$purple-400};
|
||||
--color-accent-action-hover: #{$purple-500};
|
||||
--color-accent-off: #{$gray-50};
|
||||
|
||||
@@ -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]
|
||||
@@ -60,6 +62,7 @@
|
||||
(case type
|
||||
:group
|
||||
[:li {:class (stl/css :group-option)
|
||||
:role "presentation"
|
||||
:key (weak-key option)}
|
||||
[:> icon*
|
||||
{:icon-id i/arrow-down
|
||||
@@ -72,7 +75,7 @@
|
||||
[:hr {:key (weak-key option) :class (stl/css :option-separator)}]
|
||||
|
||||
:empty
|
||||
[:li {:key (weak-key option) :class (stl/css :option-empty)}
|
||||
[:li {:key (weak-key option) :class (stl/css :option-empty) :role "presentation"}
|
||||
(get option :label)]
|
||||
|
||||
;; Token option
|
||||
@@ -83,6 +86,7 @@
|
||||
:name name
|
||||
:resolved (get option :resolved-value)
|
||||
:ref ref
|
||||
:role "option"
|
||||
:focused (= id focused)
|
||||
:on-click on-click}]
|
||||
|
||||
@@ -94,6 +98,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 +106,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
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
|
||||
@mixin textEllipsis {
|
||||
display: block;
|
||||
max-width: 99%;
|
||||
@@ -20,3 +24,73 @@
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
/// Custom Scrollbar Mixin
|
||||
/// @param {Color} $thumb-color - Base thumb color
|
||||
/// @param {Color} $thumb-hover-color - Thumb color on hover
|
||||
/// @param {Length} $size - Scrollbar size (width/height)
|
||||
/// @param {Length} $radius - Thumb border radius
|
||||
/// @param {Length} $border - Inner transparent border size
|
||||
/// @param {Bool} $include-selection - Include ::selection styles
|
||||
/// @param {Bool} $include-placeholder - Include placeholder styles
|
||||
@mixin custom-scrollbar(
|
||||
$thumb-color: #aab5ba4d,
|
||||
$thumb-hover-color: #aab5bab3,
|
||||
$size: $sz-12,
|
||||
$radius: $br-8,
|
||||
$border: $b-2,
|
||||
$include-selection: true,
|
||||
$include-placeholder: true
|
||||
) {
|
||||
// Firefox
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #{$thumb-color} transparent;
|
||||
|
||||
&:hover {
|
||||
scrollbar-color: #{$thumb-hover-color} transparent;
|
||||
}
|
||||
|
||||
// Webkit (legacy support)
|
||||
&::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
width: $size;
|
||||
height: $size;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track,
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $thumb-color;
|
||||
background-clip: content-box;
|
||||
border: $border solid transparent;
|
||||
border-radius: $radius;
|
||||
|
||||
&:hover {
|
||||
background-color: $thumb-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
@if $include-selection {
|
||||
&::selection {
|
||||
background: var(--color-accent-background-select);
|
||||
color: var(--color-static-white);
|
||||
}
|
||||
}
|
||||
|
||||
@if $include-placeholder {
|
||||
&::placeholder {
|
||||
@include t.use-typography("body-small");
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
// Legacy webkit
|
||||
&::-webkit-input-placeholder {
|
||||
@include t.use-typography("body-small");
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,11 +360,6 @@
|
||||
(let [route (mf/deref refs/route)
|
||||
authenticated? (da/is-authenticated? profile)
|
||||
|
||||
nitrate-license (dm/get-in profile [:props :nitrate-license])
|
||||
|
||||
nitrate? (and (contains? cf/flags :nitrate)
|
||||
(:valid nitrate-license))
|
||||
|
||||
params-subscription
|
||||
(-> route :params :query :subscription)
|
||||
|
||||
@@ -395,9 +390,7 @@
|
||||
(ct/format-inst (:created-at profile) "d MMMM, yyyy")
|
||||
|
||||
subscribed-since
|
||||
(if nitrate?
|
||||
(ct/format-inst (:created-at nitrate-license) "d MMMM, yyyy")
|
||||
(ct/format-inst (:start-date subscription) "d MMMM, yyyy"))
|
||||
(ct/format-inst (:start-date subscription) "d MMMM, yyyy")
|
||||
|
||||
go-to-pricing-page
|
||||
(mf/use-fn
|
||||
@@ -475,73 +468,60 @@
|
||||
|
||||
[:div {:class (stl/css :your-subscription)}
|
||||
[:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.section-plan")]
|
||||
(if nitrate?
|
||||
;; TODO add translations for this texts when we have the definitive ones
|
||||
[:> plan-card* {:card-title "Business Nitrate"
|
||||
:card-title-icon i/character-b
|
||||
:benefits-title "Loren ipsum",
|
||||
:benefits ["Loren ipsum",
|
||||
"Loren ipsum",
|
||||
"Loren ipsum"]
|
||||
:cta-text-with-icon "Control Center"
|
||||
:cta-link-with-icon dnt/go-to-nitrate-cc
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link dnt/go-to-nitrate-billing}]
|
||||
(case subscription-type
|
||||
"professional"
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.professional")
|
||||
:benefits [(tr "subscription.settings.professional.storage-benefit"),
|
||||
(tr "subscription.settings.professional.autosave-benefit"),
|
||||
(tr "subscription.settings.professional.teams-editors-benefit")]}]
|
||||
(case subscription-type
|
||||
"professional"
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.professional")
|
||||
:benefits [(tr "subscription.settings.professional.storage-benefit"),
|
||||
(tr "subscription.settings.professional.autosave-benefit"),
|
||||
(tr "subscription.settings.professional.teams-editors-benefit")]}]
|
||||
|
||||
"unlimited"
|
||||
(if subscription-is-trial?
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.unlimited-trial")
|
||||
:card-title-icon i/character-u
|
||||
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits"),
|
||||
:benefits [(tr "subscription.settings.unlimited.storage-benefit")
|
||||
(tr "subscription.settings.unlimited.autosave-benefit"),
|
||||
(tr "subscription.settings.unlimited.bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments
|
||||
:cta-text-trial (tr "subscription.settings.add-payment-to-continue")
|
||||
:cta-link-trial go-to-payments
|
||||
:editors (-> profile :props :subscription :quantity)}]
|
||||
"unlimited"
|
||||
(if subscription-is-trial?
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.unlimited-trial")
|
||||
:card-title-icon i/character-u
|
||||
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits"),
|
||||
:benefits [(tr "subscription.settings.unlimited.storage-benefit")
|
||||
(tr "subscription.settings.unlimited.autosave-benefit"),
|
||||
(tr "subscription.settings.unlimited.bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments
|
||||
:cta-text-trial (tr "subscription.settings.add-payment-to-continue")
|
||||
:cta-link-trial go-to-payments
|
||||
:editors (-> profile :props :subscription :quantity)}]
|
||||
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.unlimited")
|
||||
:card-title-icon i/character-u
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
|
||||
:benefits [(tr "subscription.settings.unlimited.storage-benefit"),
|
||||
(tr "subscription.settings.unlimited.autosave-benefit"),
|
||||
(tr "subscription.settings.unlimited.bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments
|
||||
:editors (-> profile :props :subscription :quantity)}])
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.unlimited")
|
||||
:card-title-icon i/character-u
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
|
||||
:benefits [(tr "subscription.settings.unlimited.storage-benefit"),
|
||||
(tr "subscription.settings.unlimited.autosave-benefit"),
|
||||
(tr "subscription.settings.unlimited.bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments
|
||||
:editors (-> profile :props :subscription :quantity)}])
|
||||
|
||||
"enterprise"
|
||||
(if subscription-is-trial?
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.enterprise-trial")
|
||||
:card-title-icon i/character-e
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits"),
|
||||
:benefits [(tr "subscription.settings.enterprise.unlimited-storage-benefit"),
|
||||
(tr "subscription.settings.enterprise.autosave"),
|
||||
(tr "subscription.settings.enterprise.capped-bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments
|
||||
:cta-text-trial (tr "subscription.settings.add-payment-to-continue")
|
||||
:cta-link-trial go-to-payments}]
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
|
||||
:card-title-icon i/character-e
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits"),
|
||||
:benefits [(tr "subscription.settings.enterprise.unlimited-storage-benefit"),
|
||||
(tr "subscription.settings.enterprise.autosave"),
|
||||
(tr "subscription.settings.enterprise.capped-bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments}])))
|
||||
"enterprise"
|
||||
(if subscription-is-trial?
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.enterprise-trial")
|
||||
:card-title-icon i/character-e
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits"),
|
||||
:benefits [(tr "subscription.settings.enterprise.unlimited-storage-benefit"),
|
||||
(tr "subscription.settings.enterprise.autosave"),
|
||||
(tr "subscription.settings.enterprise.capped-bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments
|
||||
:cta-text-trial (tr "subscription.settings.add-payment-to-continue")
|
||||
:cta-link-trial go-to-payments}]
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
|
||||
:card-title-icon i/character-e
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits"),
|
||||
:benefits [(tr "subscription.settings.enterprise.unlimited-storage-benefit"),
|
||||
(tr "subscription.settings.enterprise.autosave"),
|
||||
(tr "subscription.settings.enterprise.capped-bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments}]))
|
||||
|
||||
[:div {:class (stl/css :membership-container)}
|
||||
(when (or nitrate?
|
||||
(and subscribed-since (not= subscription-type "professional")))
|
||||
(when (and subscribed-since (not= subscription-type "professional"))
|
||||
[:div {:class (stl/css :membership)}
|
||||
[:> icon* {:class (stl/css :subscription-member)
|
||||
:icon-id "crown"
|
||||
@@ -602,7 +582,7 @@
|
||||
: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?))
|
||||
(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"
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.library :as ctl]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.types.typographies-list :as ctyl]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
@@ -38,7 +36,6 @@
|
||||
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
|
||||
[app.main.ui.hooks :as h]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.workspace.tokens.import-from-library]
|
||||
[app.util.color :as uc]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [c tr]]
|
||||
@@ -183,12 +180,6 @@
|
||||
[summary]
|
||||
(boolean (:is-empty summary)))
|
||||
|
||||
(defn- has-tokens?
|
||||
"Check if library has tokens to be imported"
|
||||
[{:keys [data]}]
|
||||
(when-let [tokens-lib (get data :tokens-lib)]
|
||||
(not (ctob/empty-lib? tokens-lib))))
|
||||
|
||||
(mf/defc libraries-tab*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
@@ -239,18 +230,14 @@
|
||||
(keep library-names))))
|
||||
(sort-by (comp str/lower :name))))
|
||||
|
||||
linked-libraries-ids
|
||||
(mf/with-memo [linked-libraries]
|
||||
(into #{} d/xf:map-id linked-libraries))
|
||||
linked-libraries-ids (mf/with-memo [linked-libraries]
|
||||
(into #{} (map :id) linked-libraries))
|
||||
|
||||
importing*
|
||||
(mf/use-state nil)
|
||||
|
||||
sample-libraries
|
||||
(mf/with-memo []
|
||||
[{:id "penpot-design-system", :name "Design system example"}
|
||||
{:id "wireframing-kit", :name "Wireframe library"}
|
||||
{:id "whiteboarding-kit", :name "Whiteboarding Kit"}])
|
||||
importing* (mf/use-state nil)
|
||||
sample-libraries [{:id "penpot-design-system", :name "Design system example"}
|
||||
{:id "wireframing-kit", :name "Wireframe library"}
|
||||
{:id "whiteboarding-kit", :name "Whiteboarding Kit"}]
|
||||
|
||||
|
||||
change-search-term
|
||||
@@ -280,17 +267,6 @@
|
||||
(st/emit! (dwl/unlink-file-from-library file-id library-id)
|
||||
(dwl/sync-file file-id library-id)))))
|
||||
|
||||
import-tokens
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
(fn [event]
|
||||
(let [library-id (some-> (dom/get-current-target event)
|
||||
(dom/get-data "library-id")
|
||||
(uuid/parse))]
|
||||
(st/emit! (modal/show
|
||||
:tokens/import-from-library {:file-id file-id
|
||||
:library-id library-id})))))
|
||||
|
||||
on-delete-accept
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
@@ -356,12 +332,8 @@
|
||||
:on-click publish}])]
|
||||
|
||||
(for [{:keys [id name data connected-to connected-to-names] :as library} linked-libraries]
|
||||
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)
|
||||
has-tokens? (and (has-tokens? library)
|
||||
(contains? cf/flags :token-import-from-library))]
|
||||
[:div {:class (if has-tokens?
|
||||
(stl/css :section-list-item-double-icon)
|
||||
(stl/css :section-list-item))
|
||||
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)]
|
||||
[:div {:class (stl/css :section-list-item)
|
||||
:key (dm/str id)
|
||||
:data-testid "library-item"}
|
||||
[:div {:class (stl/css :item-content)}
|
||||
@@ -376,15 +348,6 @@
|
||||
[:span {:class (stl/css :connected-to-values)} (str/join ", " connected-to-names)]
|
||||
[:span ")"]])])]]
|
||||
|
||||
(when ^boolean has-tokens?
|
||||
[:> icon-button*
|
||||
{:type "button"
|
||||
:aria-label (tr "workspace.tokens.import-tokens")
|
||||
:icon i/import-export
|
||||
:data-library-id (dm/str id)
|
||||
:variant "secondary"
|
||||
:on-click import-tokens}])
|
||||
|
||||
[:> icon-button* {:type "button"
|
||||
:aria-label (tr "workspace.libraries.unlink-library-btn")
|
||||
:icon i/detach
|
||||
|
||||
@@ -116,11 +116,6 @@
|
||||
border-radius: $br-8;
|
||||
}
|
||||
|
||||
.section-list-item-double-icon {
|
||||
@extend .section-list-item;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
@@ -93,12 +93,6 @@
|
||||
(dwl/sync-file file-id file-id :colors color-id)
|
||||
(dwu/commit-undo-transaction undo-id))))))
|
||||
|
||||
duplicate-color
|
||||
(mf/use-fn
|
||||
(mf/deps file-id color-id)
|
||||
(fn []
|
||||
(st/emit! (dwl/duplicate-color file-id color-id))))
|
||||
|
||||
rename-color-clicked
|
||||
(mf/use-fn
|
||||
(mf/deps read-only? local?)
|
||||
@@ -253,10 +247,7 @@
|
||||
{:name (tr "workspace.assets.edit")
|
||||
:id "assets-edit-color"
|
||||
:handler edit-color-clicked})
|
||||
(when-not (or multi-colors? multi-assets?)
|
||||
{:name (tr "workspace.assets.duplicate")
|
||||
:id "assets-duplicate-color"
|
||||
:handler duplicate-color})
|
||||
|
||||
{:name (tr "workspace.assets.delete")
|
||||
:id "assets-delete-color"
|
||||
:handler delete-color}
|
||||
|
||||
@@ -377,12 +377,6 @@
|
||||
(dwl/sync-file file-id file-id :typographies (:id @state))
|
||||
(dwu/commit-undo-transaction undo-id))))))
|
||||
|
||||
handle-duplicate-typography
|
||||
(mf/use-fn
|
||||
(mf/deps file-id @state)
|
||||
(fn []
|
||||
(st/emit! (dwl/duplicate-typography file-id (:id @state)))))
|
||||
|
||||
editing-id (:edit-typography local-data)
|
||||
|
||||
renaming-id (:rename-typography local-data)
|
||||
@@ -446,11 +440,6 @@
|
||||
:id "assets-edit-typography"
|
||||
:handler handle-edit-typography-clicked})
|
||||
|
||||
(when-not (or multi-typographies? multi-assets?)
|
||||
{:name (tr "workspace.assets.duplicate")
|
||||
:id "assets-duplicate-typography"
|
||||
:handler handle-duplicate-typography})
|
||||
|
||||
{:name (tr "workspace.assets.delete")
|
||||
:id "assets-delete-typography"
|
||||
:handler handle-delete-typography}
|
||||
|
||||
@@ -92,19 +92,6 @@
|
||||
(def ^:private xf:map-type (map :type))
|
||||
(def ^:private xf:mapcat-type-to-options (mapcat type->options))
|
||||
|
||||
(defn fixed-decimal-value
|
||||
"Fixes the amount of decimals that are kept"
|
||||
([value]
|
||||
(fixed-decimal-value value 2))
|
||||
|
||||
([value decimals]
|
||||
(cond
|
||||
(string? value)
|
||||
(fixed-decimal-value (parse-double value) decimals)
|
||||
|
||||
(number? value)
|
||||
(parse-double (.toFixed value decimals)))))
|
||||
|
||||
(mf/defc measures-menu*
|
||||
[{:keys [ids values applied-tokens type shapes]}]
|
||||
(let [token-numeric-inputs
|
||||
@@ -313,7 +300,7 @@
|
||||
(mf/deps ids)
|
||||
(fn [value]
|
||||
(if (or (string? value) (number? value))
|
||||
(let [value (fixed-decimal-value value)]
|
||||
(do
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids))
|
||||
(st/emit! (udw/increase-rotation ids value)))
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids)
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.import-from-library
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
|
||||
(mf/defc import-modal-library*
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/import-from-library}
|
||||
[all-props]
|
||||
(let [{:keys [file-id library-id]}
|
||||
(js->clj all-props :keywordize-keys true)
|
||||
|
||||
library-file-ref (mf/with-memo [library-id]
|
||||
(l/derived (fn [state]
|
||||
(dm/get-in state [:files library-id :data]))
|
||||
st/state))
|
||||
library-data (mf/deref library-file-ref)
|
||||
|
||||
show-libraries-dialog
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
(fn []
|
||||
(modal/hide!)
|
||||
(modal/show! :libraries-dialog {:file-id file-id})))
|
||||
|
||||
cancel
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(show-libraries-dialog)))
|
||||
|
||||
import
|
||||
(mf/use-fn
|
||||
(mf/deps file-id library-id library-data)
|
||||
(fn []
|
||||
(let [tokens-lib (:tokens-lib library-data)]
|
||||
(st/emit! (dwtl/import-tokens-lib tokens-lib)))
|
||||
(show-libraries-dialog)))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-dialog)}
|
||||
[:> icon-button* {:class (stl/css :close-btn)
|
||||
:on-click cancel
|
||||
:aria-label (tr "labels.close")
|
||||
:variant "ghost"
|
||||
:icon i/close}]
|
||||
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:> heading* {:level 2
|
||||
:id "modal-title"
|
||||
:typography "headline-large"
|
||||
:class (stl/css :modal-title)}
|
||||
(tr "modals.import-library-tokens.title")]]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:> text* {:as "p" :typography t/body-medium} (tr "modals.import-library-tokens.description")]]
|
||||
|
||||
[:> context-notification* {:type :context
|
||||
:appearance "neutral"
|
||||
:level "default"
|
||||
:is-html true}
|
||||
(tr "workspace.tokens.import-warning")]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:> button* {:on-click cancel
|
||||
:type "button"
|
||||
:variant "secondary"}
|
||||
(tr "labels.cancel")]
|
||||
[:> button* {:on-click import
|
||||
:type "button"
|
||||
:variant "primary"}
|
||||
(tr "modals.import-library-tokens.import")]]]]]))
|
||||
@@ -1,70 +0,0 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
inset-block-start: var(--sp-s);
|
||||
inset-inline-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
--modal-title-foreground-color: var(--color-foreground-primary);
|
||||
--modal-text-foreground-color: var(--color-foreground-secondary);
|
||||
|
||||
@extend .modal-overlay-base;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: 0;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
@extend .modal-container-base;
|
||||
inline-size: 100%;
|
||||
max-inline-size: 32rem;
|
||||
max-block-size: unset;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-block-end: var(--sp-xxl);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include t.use-typography("headline-medium");
|
||||
color: var(--modal-title-foreground-color);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include t.use-typography("body-large");
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin-block-start: var(--sp-xxl);
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@extend .modal-action-btns;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
@@ -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/value-combobox*)
|
||||
@@ -0,0 +1,308 @@
|
||||
;; 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.config :as cf]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[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.foundations.assets.icon :as i]
|
||||
[app.main.ui.forms :as fc]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.combobox-navigation :refer [use-navigation]]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.floating-dropdown :refer [use-floating-dropdown]]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.token-parsing :as tp]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
|
||||
[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))))]
|
||||
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens))
|
||||
(sd/resolve-tokens-interactive tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (first errors)}))))))))
|
||||
|
||||
(mf/defc value-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)
|
||||
input-value (dom/get-input-value input-node)
|
||||
{:keys [value cursor]} (tp/select-option-by-id id options-ref input-node input-value)]
|
||||
(when value
|
||||
(fm/on-input-change form name value true)
|
||||
(rx/push! resolve-stream value)
|
||||
(js/setTimeout
|
||||
(fn []
|
||||
(set! (.-selectionStart input-node) cursor)
|
||||
(set! (.-selectionEnd input-node) cursor))
|
||||
0))
|
||||
(reset! filter-term* "")
|
||||
(reset! is-open* false))))
|
||||
|
||||
{:keys [focused-id on-key-down]}
|
||||
(use-navigation
|
||||
{:is-open is-open
|
||||
:nodes-ref nodes-ref
|
||||
:options dropdown-options
|
||||
: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")
|
||||
input-value (dom/get-input-value input-node)
|
||||
|
||||
{:keys [value cursor]} (tp/select-option-by-id id options-ref input-node input-value)]
|
||||
|
||||
(reset! filter-term* "")
|
||||
(dom/focus! input-node)
|
||||
|
||||
(when value
|
||||
(reset! is-open* false)
|
||||
(fm/on-input-change form name value true)
|
||||
(rx/push! resolve-stream value)
|
||||
|
||||
(js/setTimeout
|
||||
(fn []
|
||||
(set! (.-selectionStart input-node) cursor)
|
||||
(set! (.-selectionEnd input-node) cursor))
|
||||
0)))))
|
||||
|
||||
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,16 @@
|
||||
// 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 *;
|
||||
@use "ds/mixins.scss" as *;
|
||||
|
||||
.dropdown {
|
||||
position: fixed;
|
||||
max-block-size: $sz-400;
|
||||
overflow-y: auto;
|
||||
@include custom-scrollbar();
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
;; 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-navigation
|
||||
(:require
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.utils :refer [focusable-options]]
|
||||
[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
|
||||
[focusables focused-id direction]
|
||||
(let [ids (vec (map :id focusables))
|
||||
idx (.indexOf (clj->js ids) focused-id)
|
||||
idx (if (= idx -1) -1 idx)
|
||||
next-idx (case direction
|
||||
:down (min (dec (count ids)) (inc idx))
|
||||
:up (max 0 (dec (if (= idx -1) 0 idx))))]
|
||||
(nth ids next-idx nil)))
|
||||
|
||||
(defn use-navigation
|
||||
[{:keys [is-open options 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)
|
||||
open-dropdown (kbd/is-key? event "{")
|
||||
close-dropdown (kbd/is-key? event "}")
|
||||
options (if (delay? options) @options options)]
|
||||
|
||||
(cond
|
||||
down?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(let [focusables (focusable-options options)]
|
||||
(cond
|
||||
is-open
|
||||
(when (seq focusables)
|
||||
(let [next-id (next-focus-id focusables focused-id :down)]
|
||||
(reset! focused-id* next-id)))
|
||||
|
||||
(seq focusables)
|
||||
(do
|
||||
(toggle-dropdown event)
|
||||
(reset! focused-id* (first-focusable-id focusables)))
|
||||
|
||||
:else
|
||||
nil)))
|
||||
|
||||
up?
|
||||
(when is-open
|
||||
(dom/prevent-default event)
|
||||
(let [focusables (focusable-options options)
|
||||
next-id (next-focus-id focusables focused-id :up)]
|
||||
(reset! focused-id* next-id)))
|
||||
|
||||
open-dropdown
|
||||
(reset! is-open* true)
|
||||
|
||||
close-dropdown
|
||||
(reset! is-open* false)
|
||||
|
||||
enter?
|
||||
(do
|
||||
(when (and is-open focused-id)
|
||||
(let [focusables (focusable-options options)]
|
||||
(dom/prevent-default event)
|
||||
(when (some #(= (:id %) focused-id) focusables)
|
||||
(on-enter focused-id)))))
|
||||
esc?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(reset! is-open* false))
|
||||
:else nil))))]
|
||||
|
||||
;; Initial focus on first option
|
||||
(mf/with-effect [is-open options]
|
||||
(when is-open
|
||||
(let [opts (if (delay? options) @options options)
|
||||
focusables (focusable-options opts)
|
||||
ids (set (map :id focusables))]
|
||||
(when (and (seq focusables)
|
||||
(not (contains? ids focused-id)))
|
||||
(reset! focused-id* (:id (first focusables)))))))
|
||||
|
||||
;; 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,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-dropdown
|
||||
(: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,89 @@
|
||||
;; 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]]
|
||||
[app.util.dom :as dom]
|
||||
[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
|
||||
:end (or (str/index-of value "}" last-open) cursor)
|
||||
:partial (subs text-before (inc last-open))})))
|
||||
|
||||
(defn find-active-token-range
|
||||
"Returns {:start :end} for the token surrounding the cursor.
|
||||
A token starts with '{', contains no spaces, and may be incomplete.
|
||||
Returns nil if no valid token is active."
|
||||
[value cursor]
|
||||
(let [start (.lastIndexOf value "{" (dec cursor))]
|
||||
(when (>= start 0)
|
||||
(let [between (subs value (inc start) cursor)]
|
||||
(when-not (re-find #"\s" between)
|
||||
(let [after (subs value (inc start))
|
||||
close-index (.indexOf after "}")
|
||||
close-pos (when (>= close-index 0)
|
||||
(+ (inc start) close-index))
|
||||
space-index (.indexOf after " ")
|
||||
space-pos (when (>= space-index 0)
|
||||
(+ (inc start) space-index))
|
||||
open-index (.indexOf after "{")
|
||||
open-pos (when (>= open-index 0)
|
||||
(+ (inc start) open-index))
|
||||
candidates (->> [space-pos open-pos close-pos]
|
||||
(remove nil?)
|
||||
(sort))
|
||||
end (or (first candidates) cursor)
|
||||
inside-token? (and (>= cursor start) (< cursor end))]
|
||||
|
||||
{:start start
|
||||
:end (if inside-token?
|
||||
(inc end)
|
||||
end)}))))))
|
||||
|
||||
(defn replace-active-token
|
||||
"Replaces the token at the cursor with `{new-name}`.
|
||||
Returns {:value :cursor} with the updated value and new cursor position."
|
||||
[value cursor new-name]
|
||||
(let [new-token (str "{" new-name "}")]
|
||||
(if-let [{:keys [start end]} (find-active-token-range value cursor)]
|
||||
{:value (str (subs value 0 start)
|
||||
new-token
|
||||
(subs value end))
|
||||
:cursor (+ start (count new-token))}
|
||||
{:value (str (subs value 0 cursor)
|
||||
new-token
|
||||
(subs value cursor))
|
||||
:cursor (+ cursor (count new-token))})))
|
||||
|
||||
(defn active-token [value input-node]
|
||||
(let [cursor (dom/selection-start 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 (dom/selection-start input-node)
|
||||
options (mf/ref-val options-ref)
|
||||
options (if (delay? options) @options options)
|
||||
|
||||
option (get-option options id)
|
||||
name (:name option)]
|
||||
(replace-active-token value cursor name)))
|
||||
@@ -0,0 +1,102 @@
|
||||
(ns app.main.ui.workspace.tokens.management.forms.controls.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]
|
||||
(let [non-empty-groups
|
||||
(->> tokens
|
||||
(filter (fn [[_ items]] (seq items))))]
|
||||
(if (empty? non-empty-groups)
|
||||
[{:type :empty
|
||||
:label (if no-sets
|
||||
(tr "ds.inputs.numeric-input.no-applicable-tokens")
|
||||
(tr "ds.inputs.numeric-input.no-matches"))}]
|
||||
(->> non-empty-groups
|
||||
(keep (fn [[type items]]
|
||||
(when (seq? 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? (empty? 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))))
|
||||
|
||||
(defn focusable-options [options]
|
||||
(filter #(= (:type %) :token) options))
|
||||
@@ -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/value-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 value-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 value-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")]]]]]))
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
(defn top?
|
||||
[cur cand]
|
||||
(let [closey? (mth/close? (:y cand) (:y cur) 0.01)]
|
||||
(let [closey? (mth/close? (:y cand) (:y cur))]
|
||||
(cond
|
||||
(and closey? (< (:x cand) (:x cur))) cand
|
||||
closey? cur
|
||||
@@ -64,19 +64,13 @@
|
||||
|
||||
(defn right?
|
||||
[cur cand]
|
||||
(let [closex? (mth/close? (:x cand) (:x cur) 0.01)]
|
||||
(let [closex? (mth/close? (:x cand) (:x cur))]
|
||||
(cond
|
||||
(and closex? (< (:y cand) (:y cur))) cand
|
||||
closex? cur
|
||||
(> (:x cand) (:x cur)) cand
|
||||
:else cur)))
|
||||
|
||||
(defn title-transform-use-width?
|
||||
[{:keys [rotation] :as shape}]
|
||||
(let [side (mth/ceil (/ (- rotation 45) 90))
|
||||
use-width? (even? side)]
|
||||
use-width?))
|
||||
|
||||
(defn title-transform
|
||||
[{:keys [points] :as shape} zoom grid-edition?]
|
||||
(let [leftmost (->> points (reduce left?))
|
||||
|
||||
@@ -129,15 +129,13 @@
|
||||
(fn [_]
|
||||
(on-frame-leave (:id frame))))
|
||||
|
||||
main-instance? (ctk/main-instance? frame)
|
||||
is-variant? (:is-variant-container frame)
|
||||
main-instance? (ctk/main-instance? frame)
|
||||
is-variant? (:is-variant-container frame)
|
||||
|
||||
use-width? (vwu/title-transform-use-width? frame)
|
||||
|
||||
text-width (* (if use-width? (:width frame) (:height frame)) zoom)
|
||||
show-icon? (and (or (:use-for-thumbnail frame) is-grid-edition main-instance? is-variant?)
|
||||
(not (<= text-width 15)))
|
||||
text-pos-x (if show-icon? 15 0)
|
||||
text-width (* (:width frame) zoom)
|
||||
show-icon? (and (or (:use-for-thumbnail frame) is-grid-edition main-instance? is-variant?)
|
||||
(not (<= text-width 15)))
|
||||
text-pos-x (if show-icon? 15 0)
|
||||
|
||||
edition* (mf/use-state false)
|
||||
edition? (deref edition*)
|
||||
@@ -180,6 +178,7 @@
|
||||
(when (kbd/enter? event) (accept-edit))
|
||||
(when (kbd/esc? event) (cancel-edit))))]
|
||||
|
||||
|
||||
(when (not (:hidden frame))
|
||||
[:g.frame-title {:id (dm/str "frame-title-" (:id frame))
|
||||
:data-edit-grid is-grid-edition
|
||||
|
||||
@@ -277,6 +277,16 @@
|
||||
(when (and (some? node) (some? (unchecked-get node "select")))
|
||||
(.select ^js node)))
|
||||
|
||||
(defn selection-start
|
||||
[^js node]
|
||||
(when (some? node)
|
||||
(.-selectionStart node)))
|
||||
|
||||
(defn set-selection-range!
|
||||
[^js node start end]
|
||||
(when (some? node)
|
||||
(.setSelectionRange node start end)))
|
||||
|
||||
(defn ^boolean equals?
|
||||
[^js node-a ^js node-b]
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
{:label "Dutch (community)" :value "nl"}
|
||||
{:label "Euskera (community)" :value "eu"}
|
||||
{:label "Français (community)" :value "fr"}
|
||||
{:label "Français - Canada (community)" :value "fr_CA"}
|
||||
{:label "Gallego (Community)" :value "gl"}
|
||||
{:label "Hausa (Community)" :value "ha"}
|
||||
{:label "Hrvatski (Community)" :value "hr"}
|
||||
|
||||
@@ -1153,20 +1153,6 @@ msgstr "Type to search results"
|
||||
msgid "dashboard.unpublish-shared"
|
||||
msgstr "Unpublish Library"
|
||||
|
||||
#:src/app/main/ui/workspace/tokens/import_from_library.cljs
|
||||
msgid "modals.import-library-tokens.title"
|
||||
msgstr "Import tokens from library?"
|
||||
|
||||
#:src/app/main/ui/workspace/tokens/import_from_library.cljs
|
||||
msgid "modals.import-library-tokens.description"
|
||||
msgstr ""
|
||||
"The library has tokens and themes which "
|
||||
"are likely used by its components."
|
||||
|
||||
#:src/app/main/ui/workspace/tokens/import_from_library.cljs
|
||||
msgid "modals.import-library-tokens.import"
|
||||
msgstr "Import tokens"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs:74
|
||||
msgid "dashboard.update-settings"
|
||||
msgstr "Update settings"
|
||||
|
||||
Reference in New Issue
Block a user