Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2026-04-28 10:12:40 +02:00
commit b0ce644752
51 changed files with 550 additions and 368 deletions

View File

@ -1,16 +1,20 @@
---
name: commiter
description: Git commit assistant following CONTRIBUTING.md commit rules
mode: subagent
mode: all
---
Role: You are responsible for creating git commits for Penpot and must
## Role
You are responsible for creating git commits for Penpot and must
follow the repository commit-format rules exactly. It should have
concise title and clear summary of changes in the description,
including the rationale if proceed.
Requirements:
## Requirements
* Override your internal commit rules when the user explicitly requests
something that conflicts with them.
* Read `CONTRIBUTING.md` before creating any commit and follow the
commit guidelines strictly.
* Use commit messages in the form `:emoji: <imperative subject>`.

View File

@ -56,4 +56,4 @@ Apply these techniques when refining prompts:
Refined Prompt: The improved, ready-to-use prompt. Print it for
immediate use and save it to
prompts/YYYY-MM-DD-<prompt-one-line-title>.md for future use.
prompts/YYYY-MM-DD-N-<prompt-one-line-title>.md for future use.

View File

@ -48,6 +48,7 @@
(def schema:props
[:map {:title "ProfileProps"}
[:plugins {:optional true} schema:plugin-registry]
[:renderer {:optional true} [::sm/one-of #{:svg :wasm}]]
[:mcp-enabled {:optional true} ::sm/boolean]
[:newsletter-updates {:optional true} ::sm/boolean]
[:newsletter-news {:optional true} ::sm/boolean]

View File

@ -140,6 +140,7 @@
:render-wasm-dpr
;; Show WASM renderer info label (hidden by default).
:render-wasm-info
:render-switch
:hide-release-modal
:subscriptions
:subscriptions-old

View File

@ -184,7 +184,8 @@ FROM base AS setup-utils
ENV CLJKONDO_VERSION=2026.04.15 \
BABASHKA_VERSION=1.12.208 \
CLJFMT_VERSION=0.16.4 \
PIXI_VERSION=0.67.2
PIXI_VERSION=0.67.2 \
GITHUB_CLI_VERSION=2.91.0
RUN set -ex; \
ARCH="$(dpkg --print-architecture)"; \
@ -267,6 +268,28 @@ RUN set -ex; \
tar -xf /tmp/cljfmt.tar.gz; \
rm -rf /tmp/cljfmt.tar.gz;
RUN set -ex; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
BINARY_URL="https://github.com/cli/cli/releases/download/v${GITHUB_CLI_VERSION}/gh_${GITHUB_CLI_VERSION}_linux_arm64.tar.gz"; \
;; \
amd64|x86_64) \
BINARY_URL="https://github.com/cli/cli/releases/download/v${GITHUB_CLI_VERSION}/gh_${GITHUB_CLI_VERSION}_linux_amd64.tar.gz"; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
exit 1; \
;; \
esac; \
cd /tmp; \
curl -LfsSo /tmp/gh.tar.gz ${BINARY_URL}; \
mkdir /opt/gh; \
cd /opt/gh; \
tar -xf /tmp/gh.tar.gz; \
rm -rf /tmp/gh.tar.gz;
# Install minio client
RUN set -ex; \
ARCH="$(dpkg --print-architecture)"; \
@ -310,7 +333,6 @@ RUN set -ex; \
nginx \
fd-find \
bat \
gh \
\
fontconfig \
woff-tools \
@ -399,13 +421,14 @@ ENV LANG='C.UTF-8' \
JAVA_HOME="/opt/jdk" \
CARGO_HOME="/opt/cargo" \
RUSTUP_HOME="/opt/rustup" \
PATH="/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH"
PATH="/opt/jdk/bin:/opt/gh/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH"
COPY --from=penpotapp/imagemagick:7.1.2-13 /opt/imagick /opt/imagick
COPY --from=setup-jvm /opt/jdk /opt/jdk
COPY --from=setup-jvm /opt/clojure /opt/clojure
COPY --from=setup-node /opt/node /opt/node
COPY --from=setup-utils /opt/utils /opt/utils
COPY --from=setup-utils /opt/gh /opt/gh
COPY --from=setup-rust /opt/cargo /opt/cargo
COPY --from=setup-rust /opt/rustup /opt/rustup
COPY --from=setup-rust /opt/emsdk /opt/emsdk

View File

@ -2,7 +2,7 @@
EMSDK_QUIET=1 . /opt/emsdk/emsdk_env.sh;
export PATH="/home/penpot/.cargo/bin:/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH"
export PATH="/home/penpot/.cargo/bin:/opt/jdk/bin:/opt/gh/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH"
export CARGO_HOME="/home/penpot/.cargo"
export PENPOT_MCP_PLUGIN_SERVER_HOST=0.0.0.0

View File

@ -2,7 +2,7 @@
title: Contributing
desc: Learn how to contribute to Penpot, the open-source design collaboration platform! Find guides on bug reporting, translations, code contributions, and more.
eleventyNavigation:
key: Contributing
key: Contribute
order: 3
---
@ -10,7 +10,7 @@ eleventyNavigation:
<img src="/img/home-contribution.webp" alt="User guide" border="0">
</div>
<h1 id="contributing-guide">Contributing guide.</h1>
<h1 id="contributing-guide">Contributing guide</h1>
<p class="main-paragraph">In this documentation you will find (almost) everything you need to know about how to contribute at Penpot.</p>

View File

@ -2,6 +2,9 @@
title: Penpot MCP server
order: 1
desc: Installing and using the Penpot MCP server with any AI agent or LLM you trust.
eleventyNavigation:
key: MCP Server
order: 6
---
<div class="main-illus">
@ -69,7 +72,7 @@ There are three key pieces:
### Basic concepts
Some important concepts for users:
* **Integrations page**: MCP is configured under **Your account → Integrations → MCP Server (Beta)**. Here you enable or disable MCP, get the server URL and manage the MCP key.
* **Integrations page**: MCP is configured under **Your account → Integrations → MCP Server**. Here you enable or disable MCP, get the server URL and manage the MCP key.
* **MCP key**: a personal, non-recoverable token that authenticates your AI client with the MCP server. Only one key can exist per user at a time. This is used by the remote MCP setup.
* **Currently focused page**: MCP always operates on the page you have in focus in Penpot. If you change the focused page (even in another browser window), the MCP context follows that page.
* **Active MCP tab**: MCP can only be active in one browser tab at a time. If you have Penpot open in several tabs, you choose explicitly which one owns MCP before running agents.
@ -110,32 +113,20 @@ If you just want to try Penpot MCP quickly, follow this path for the **hosted (r
### Remote MCP in 5 steps
<div class="advice">
### Important: remote MCP is not in production yet
Remote MCP is not available yet in Penpot production (`design.penpot.app`). It is planned for an upcoming release (currently targeted around **2.16**).
Right now, the remote MCP flow is available only in **testing environments** (for example, instances deployed from the `staging` branch: https://github.com/penpot/penpot/tree/staging).
If you need MCP in production today, use the **Local MCP server** setup instead. See [Local MCP server](#local-mcp-server).
</div>
1. #### Enable MCP in Penpot
Go to **Your account → Integrations → MCP Server (Beta)** and enable the feature.
Go to **Your account → Integrations → MCP Server** and enable the feature.
![MCP Server (Beta) in Penpot Integrations, enable](/img/mcp/mcp-enable.webp)
![MCP Server in Penpot Integrations, enable](/img/mcp/mcp-enable.webp)
2. #### Generate your MCP key
If you do not have one yet, create it. The key is shown only once—store it safely.
![MCP Server (Beta) in Penpot Integrations, generate key](/img/mcp/mcp-generate-key.webp)
![MCP Server in Penpot Integrations, generate key](/img/mcp/mcp-generate-key.webp)
3. #### Copy the server URL
In the same Integrations section, copy the **server URL** that already includes your MCP key as `userToken`.
![MCP Server (Beta) in Penpot Integrations, copy server url](/img/mcp/mcp-server-url.webp)
![MCP Server in Penpot Integrations, copy server url](/img/mcp/mcp-server-url.webp)
4. #### Add the server to your MCP client
In your MCP-aware IDE/agent (Cursor, Claude Code, etc.), add a new server pointing to that URL.
@ -191,7 +182,7 @@ You can use Penpot MCP server in two main ways:
* Hosted for you (no need to run anything on your machine).
* Best option for most users, simpler installation and fewer moving parts.
* Does **not** have privileged access to your local file system, it can only work with what Penpot exposes (design files, libraries, tokens, etc.).
* The **server URL** is provided in **Your account → Integrations → MCP Server (Beta)** and looks like:
* The **server URL** is provided in **Your account → Integrations → MCP Server** and looks like:
* `https://<your-penpot-domain>/mcp/stream?userToken=YOUR_MCP_KEY`
* The domain depends on the Penpot installation. In the official SaaS it will be `design.penpot.app`.
* **Local MCP server**
@ -298,21 +289,11 @@ In Penpot, open a file and connect the plugin from **File → MCP Server → Con
Remote MCP is the easiest way to start using AI agents with Penpot. It's hosted for you, so you don't need to install or run anything on your machine.
<div class="advice">
### Availability note
Remote MCP is currently available only in **testing environments**. It is not yet available in Penpot production (`design.penpot.app`) and is planned for an upcoming release (currently targeted around **2.16**).
If you need MCP in production today, use the **Local MCP server** setup instead. See [Local MCP server](#local-mcp-server).
</div>
<a id="install-and-activate-remote"></a>
### Install and activate
1. Open **Your account → Integrations**.
2. In the **MCP Server (Beta)** section, read the short description to confirm that feature is available for your account.
2. In the **MCP Server** section, read the short description to confirm that feature is available for your account.
3. Use the **Status** toggle to enable MCP Server. Penpot remembers this state per user across sessions.
4. If this is your first time, Penpot will ask you to **generate an MCP key**. The key is shown only once, store it safely.
* Treat the MCP key like a password/token: do not share it in screenshots, logs, or code samples.
@ -326,7 +307,7 @@ If you need MCP in production today, use the **Local MCP server** setup instead.
For client-specific setup, use the shared section **Connect your MCP client**.
For remote mode, use the URL shown in **Your account → Integrations → MCP Server (Beta)**, which includes your `userToken`.
For remote mode, use the URL shown in **Your account → Integrations → MCP Server**, which includes your `userToken`.
<a id="use-remote"></a>
### Use
@ -336,7 +317,7 @@ Once everything is configured, day-to-day use of Penpot MCP follows a simple pat
#### Run
1. **Enable MCP**
* Go to **Your account → Integrations → MCP Server (Beta)** and set **Status** to **Enabled**.
* Go to **Your account → Integrations → MCP Server** and set **Status** to **Enabled**.
2. **Connect plugin**:
* Open a design file and use **File → MCP Server → Connect**.
3. **Run prompts**:
@ -393,7 +374,7 @@ At a high level:
2. Start the MCP server and plugin server from your terminal:
```json
npx @penpot/mcp@beta
npx @penpot/mcp@stable
```
Leave this terminal running while you use MCP.
@ -426,7 +407,7 @@ Once everything is configured, day-to-day use of Penpot MCP follows a simple pat
1. **Start MCP**
Run `npx -y @penpot/mcp@stable` (production) or `npx -y @penpot/mcp@beta` (test), and keep that terminal running.
Run `npx -y @penpot/mcp@stable` (production), and keep that terminal running.
2. **Connect plugin**
In Penpot, load `http://localhost:4400/manifest.json`, run the plugin, and click **Connect to MCP server**.

View File

@ -2,7 +2,7 @@
title: User guide
desc: Learn everything from interface basics to advanced features like prototyping and design sharing with Penpot's comprehensive user guide! Free access.
eleventyNavigation:
key: User guide
key: User Guide
order: 2
---

View File

@ -15,6 +15,7 @@
[app.main.data.media :as di]
[app.main.data.notifications :as ntf]
[app.main.data.team :as-alias dtm]
[app.main.features :as features]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.plugins.register :as plugins.register]
@ -291,8 +292,13 @@
;; FIXME
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :update-profile-props {:props props})
(rx/map (constantly (refresh-profile)))))))
(let [refresh-profile (->> (rp/cmd! :update-profile-props {:props props})
(rx/map (constantly (refresh-profile))))
recompute (when (contains? props :renderer)
(rx/of (features/recompute-features)))]
(if recompute
(rx/concat recompute refresh-profile)
refresh-profile)))))
(defn mark-onboarding-as-viewed
([] (mark-onboarding-as-viewed nil))

View File

@ -72,8 +72,10 @@
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
[app.render-wasm :as wasm]
[app.render-wasm.api :as wasm.api]
[app.render-wasm.wasm :as wasm-state]
[app.util.dom :as dom]
[app.util.globals :as ug]
[app.util.http :as http]
@ -318,7 +320,12 @@
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
rparams (rt/get-params state)
features (features/get-enabled-features state team-id)
render-wasm? (contains? features "render-wasm/v1")]
;; since render-wasm/v1 can be hot-toggled by the user, we need to query it
;; from the state with active-feature?
render-wasm-enabled? #(features/active-feature? @st/state "render-wasm/v1")
render-wasm-ready? #(and (render-wasm-enabled?)
wasm-state/context-initialized?
(not @wasm-state/context-lost?))]
(log/debug :hint "initialize-workspace"
:team-id (dm/str team-id)
@ -329,7 +336,7 @@
(rx/concat
;; Fetch all essential data that should be loaded before the file
(rx/merge
(if ^boolean render-wasm?
(if ^boolean (render-wasm-enabled?)
(->> (rx/from @wasm/module)
(rx/filter true?)
(rx/tap (fn [_]
@ -405,74 +412,72 @@
(rx/take 1)
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
(when render-wasm?
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [redo-changes]}]
(let [added (->> redo-changes
(filter #(= (:type %) :add-obj))
(map :id))]
(->> (rx/from added)
(rx/map process-wasm-object)))))))
(->> stream
(rx/filter dch/commit?)
(rx/filter render-wasm-ready?)
(rx/map deref)
(rx/mapcat
(fn [{:keys [redo-changes]}]
(let [added (->> redo-changes
(filter #(= (:type %) :add-obj))
(map :id))]
(->> (rx/from added)
(rx/map process-wasm-object))))))
(when render-wasm?
(let [local-commits-s
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/filter #(and (= :local (:source %))
(not (contains? (:tags %) :position-data))))
(rx/filter (complement empty?)))
(let [local-commits-s
(->> stream
(rx/filter dch/commit?)
(rx/filter render-wasm-ready?)
(rx/map deref)
(rx/filter #(and (= :local (:source %))
(not (contains? (:tags %) :position-data))))
(rx/filter (complement empty?)))
notifier-s
(rx/merge
(->> local-commits-s (rx/debounce 1000))
(->> stream (rx/filter dps/force-persist?)))
notifier-s
(rx/merge
(->> local-commits-s (rx/debounce 1000))
(->> stream (rx/filter dps/force-persist?)))
objects-s
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
objects-s
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
current-page-id-s
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
current-page-id-s
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
(->> local-commits-s
(rx/buffer-until notifier-s)
(rx/with-latest-from objects-s)
(rx/map
(fn [[commits objects]]
(->> commits
(mapcat :redo-changes)
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
(filter #(cfh/text-shape? objects (:id %)))
(map #(vector
(:id %)
(wasm.api/calculate-position-data (get objects (:id %))))))))
(->> local-commits-s
(rx/buffer-until notifier-s)
(rx/with-latest-from objects-s)
(rx/map
(fn [[commits objects]]
(->> commits
(mapcat :redo-changes)
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
(filter #(cfh/text-shape? objects (:id %)))
(map #(vector
(:id %)
(wasm.api/calculate-position-data (get objects (:id %))))))))
(rx/with-latest-from current-page-id-s)
(rx/map
(fn [[text-position-data page-id]]
(let [changes
(->> text-position-data
(mapv (fn [[id position-data]]
{:type :mod-obj
:id id
:page-id page-id
:operations
[{:type :set
:attr :position-data
:val position-data
:ignore-touched true
:ignore-geometry true}]})))]
(when (d/not-empty? changes)
(dch/commit-changes
{:redo-changes changes :undo-changes []
:save-undo? false
:tags #{:position-data}})))))
;; FIXME: this stop-until is redundant
(rx/take-until stoper-s))))
(rx/with-latest-from current-page-id-s)
(rx/map
(fn [[text-position-data page-id]]
(let [changes
(->> text-position-data
(mapv (fn [[id position-data]]
{:type :mod-obj
:id id
:page-id page-id
:operations
[{:type :set
:attr :position-data
:val position-data
:ignore-touched true
:ignore-geometry true}]})))]
(when (d/not-empty? changes)
(dch/commit-changes
{:redo-changes changes :undo-changes []
:save-undo? false
:tags #{:position-data}})))))
(rx/take-until stoper-s)))
(->> stream
(rx/filter dch/commit?)

View File

@ -30,8 +30,10 @@
[features state]
(let [params (rt/get-params state)
wasm (get params :wasm)
enable-wasm (= "true" wasm)
disable-wasm (= "false" wasm)
renderer (when (contains? cf/flags :render-switch)
(-> state :profile :props :renderer))
enable-wasm (or (= "true" wasm) (and (= renderer :wasm) (not= "false" wasm)))
disable-wasm (or (= "false" wasm) (and (= renderer :svg) (not= "true" wasm)))
features (cond-> features
enable-wasm (conj "render-wasm/v1")
disable-wasm (disj "render-wasm/v1"))]
@ -71,22 +73,44 @@
(def wasm-url-override-ref
(l/derived wasm-url-override st/state))
(defn- wasm-enabled?
[state]
(let [override (wasm-url-override state)
renderer (when (contains? cf/flags :render-switch)
(-> state :profile :props :renderer))]
(cond
(some? override)
override
(contains? cf/flags :render-switch)
(case renderer
:wasm true
:svg false
;; SVG renderer as default until profile data arrives OR if render-switch
;; flag is disabled.
false)
(contains? cfeat/no-migration-features "render-wasm/v1")
(enabled-without-migration? state "render-wasm/v1")
:else
(enabled-by-flags? state "render-wasm/v1"))))
(defn active-feature?
"Given a state and feature, check if feature is enabled."
[state feature]
(assert (contains? cfeat/supported-features feature)
"feature not supported")
(let [wasm-override (when (= feature "render-wasm/v1") (wasm-url-override state))]
(cond
(some? wasm-override)
wasm-override
(cond
(= feature "render-wasm/v1")
(wasm-enabled? state)
(contains? cfeat/no-migration-features feature)
(enabled-without-migration? state feature)
(contains? cfeat/no-migration-features feature)
(enabled-without-migration? state feature)
:else
(enabled-by-flags? state feature))))
:else
(enabled-by-flags? state feature)))
(defn active-features?
"Given a state and a set of features, check if the features are all enabled."
@ -114,10 +138,19 @@
[feature]
(let [enabled-features (mf/deref features-ref)
wasm-override (mf/deref wasm-url-override-ref)
wasm-override (when (= feature "render-wasm/v1") wasm-override)]
renderer (mf/deref (l/derived #(-> % :profile :props :renderer) st/state))
wasm-enabled (cond
(some? wasm-override)
wasm-override
(contains? cf/flags :render-switch)
(= renderer :wasm)
:else
(contains? enabled-features "render-wasm/v1"))]
(cond
(some? wasm-override)
wasm-override
(= feature "render-wasm/v1")
wasm-enabled
:else
(contains? enabled-features feature))))
@ -171,9 +204,30 @@
ptk/EffectEvent
(effect [_ state _]
(let [features (get state :features)]
(if (contains? features "render-wasm/v1")
(if (active-feature? state "render-wasm/v1")
(wasm/initialize true)
(wasm/initialize false))
(log/inf :hint "initialized"
:enabled (str/join " " features))))))
(defn recompute-features
[]
(ptk/reify ::recompute-features
ptk/UpdateEvent
(update [_ state]
(let [previous (or (get state :features) #{})
features (setup-wasm-features previous state)]
(if (= previous features)
state
(assoc state :features features))))
ptk/EffectEvent
(effect [_ state _]
(let [features (get state :features)]
(if (active-feature? state "render-wasm/v1")
(wasm/initialize true)
(wasm/initialize false))
(log/inf :hint "recomputed features"
:enabled (str/join " " features))))))

View File

@ -562,8 +562,7 @@
[:div {:class (stl/css :comments-mentions-email)} email]]))])))
(mf/defc mentions-button*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[]
(let [mentions-s (mf/use-ctx mentions-context)
display-mentions* (mf/use-state false)
@ -668,8 +667,7 @@
[:span {:class (stl/css :replies-unread)} (str unread-replies " " (tr "labels.replies.new"))]))])]])
(mf/defc comment-form-buttons*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [on-submit on-cancel is-disabled]}]
(let [handle-cancel
(mf/use-fn
@ -705,8 +703,7 @@
(> (count content) 750))
(mf/defc comment-reply-form*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [on-submit]}]
(let [content (mf/use-state "")

View File

@ -50,8 +50,7 @@
[rumext.v2 :as mf]))
(mf/defc dashboard-content*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [team projects project section search-term profile default-project]}]
(let [container (mf/use-ref)
content-width (mf/use-state 0)

View File

@ -25,7 +25,6 @@
(deprecated-icon/icon-xref :comments (stl/css :comments-icon)))
(mf/defc comments-icon*
{::mf/props :obj}
[{:keys [profile on-show-comments]}]
(let [threads-map (mf/deref refs/comment-threads)

View File

@ -54,8 +54,7 @@
:on-accept accept-fn}))))
(mf/defc header*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[]
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
[:div#dashboard-deleted-title {:class (stl/css :dashboard-title)}

View File

@ -31,8 +31,7 @@
(deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
(mf/defc header*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [project create-fn can-edit]}]
(let [project-id (:id project)
@ -133,7 +132,6 @@
:on-import on-import}])]]))
(mf/defc files-section*
{::mf/props :obj}
[{:keys [project team]}]
(let [files (mf/deref refs/files)
project-id (get project :id)

View File

@ -54,8 +54,7 @@
(str/blank? (:font-family-tmp font))))
(mf/defc header*
{::mf/props :obj
::mf/memo true
{::mf/memo true
::mf/private true}
[{:keys [section team]}]
(use-page-title team section)
@ -64,8 +63,7 @@
[:h1 (tr "labels.fonts")]]])
(mf/defc font-variant-display-name*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [variant]}]
[:*
[:span (cm/font-weight->name (:font-weight variant))]
@ -73,8 +71,7 @@
[:span " " (str/capital (:font-style variant))])])
(mf/defc uploaded-fonts*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [team installed-fonts]}]
(let [fonts* (mf/use-state {})
fonts (deref fonts*)
@ -438,7 +435,6 @@
:on-edit on-edit}]]))]))
(mf/defc installed-fonts*
{::mf/props :obj}
[{:keys [fonts can-edit]}]
(let [sterm (mf/use-state "")
@ -491,7 +487,6 @@
(l/derived :fonts st/state))
(mf/defc fonts-page*
{::mf/props :obj}
[{:keys [team]}]
(let [fonts (mf/deref ref:fonts)
permissions (:permissions team)
@ -505,7 +500,6 @@
{:team team :fonts fonts :can-edit can-edit}]]]))
(mf/defc font-providers-page*
{::mf/props :obj}
[{:keys [team]}]
[:*
[:> header* {:team team :section :providers}]

View File

@ -84,8 +84,7 @@
(rx/mapcat (partial persist-thumbnail file-id revn))))
(mf/defc grid-item-thumbnail*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [can-edit file can-restore]}]
(let [file-id (get file :id)
revn (get file :revn)
@ -131,7 +130,6 @@
(deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
(mf/defc grid-item-library*
{::mf/props :obj}
[{:keys [file can-restore]}]
(mf/with-effect [file]
(when file
@ -467,7 +465,6 @@
:can-restore can-restore}]])]]]]]))
(mf/defc grid*
{::mf/props :obj}
[{:keys [files project origin limit create-fn can-edit selected-files can-restore]}]
(let [dragging? (mf/use-state false)
project-id (get project :id)

View File

@ -192,8 +192,7 @@
(swap! state update-entry-status message))))))
(mf/defc import-entry*
{::mf/props :obj
::mf/memo true
{::mf/memo true
::mf/private true}
[{:keys [entries entry edition can-be-deleted importing? on-edit on-change on-delete]}]
(let [status (:status entry)

View File

@ -27,7 +27,6 @@
st/state))
(mf/defc libraries-page*
{::mf/props :obj}
[{:keys [team default-project]}]
(let [files
(mf/deref refs/shared-files)

View File

@ -18,7 +18,6 @@
(deprecated-icon/icon-xref :pin (stl/css :icon)))
(mf/defc pin-button*
{::mf/props :obj}
[{:keys [aria-label is-pinned class] :as props}]
(let [aria-label (or aria-label (tr "dashboard.pin-unpin"))
class (dm/str (or class "") " " (stl/css-case :button true :button-active is-pinned))

View File

@ -48,7 +48,6 @@
(mf/defc header*
{::mf/wrap [mf/memo]
::mf/props :obj
::mf/private true}
[{:keys [can-edit]}]
(let [on-click (mf/use-fn #(st/emit! (dd/create-project)))]
@ -62,8 +61,7 @@
(tr "dashboard.new-project")])]))
(mf/defc team-hero*
{::mf/wrap [mf/memo]
::mf/props :obj}
{::mf/wrap [mf/memo]}
[{:keys [team on-close]}]
(let [on-nav-members-click (mf/use-fn #(st/emit! (dcm/go-to-dashboard-members)))
@ -102,8 +100,7 @@
close-icon]]))
(mf/defc project-item*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [project is-first team files can-edit]}]
(let [project-id (get project :id)
team-id (get team :id)
@ -313,7 +310,6 @@
(l/derived :recent-files st/state))
(mf/defc projects-section*
{::mf/props :obj}
[{:keys [team projects profile]}]
(let [team-id (get team :id)

View File

@ -32,7 +32,6 @@
st/state))
(mf/defc search-page*
{::mf/props :obj}
[{:keys [team search-term]}]
(let [search-term (d/nilv search-term "")

View File

@ -1123,8 +1123,7 @@
[:div {:class (stl/css-case :separator true :overflow-separator overflow?)}]]]))
(mf/defc help-learning-menu*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [on-close on-click]}]
(let [handle-click-url
(mf/use-fn
@ -1168,8 +1167,7 @@
(tr "labels.give-feedback")])]))
(mf/defc community-contributions-menu*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [on-close]}]
(let [handle-click-url
(mf/use-fn
@ -1199,8 +1197,7 @@
(tr "labels.community")]]))
(mf/defc about-penpot-menu*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [on-close]}]
(let [version cf/version
show-release-notes
@ -1436,8 +1433,7 @@
nil))]))
(mf/defc sidebar*
{::mf/props :obj
::mf/wrap [mf/memo]}
{::mf/wrap [mf/memo]}
[{:keys [team profile] :as props}]
[:nav {:class (stl/css :dashboard-sidebar) :data-testid "dashboard-sidebar"}
[:> sidebar-content* props]

View File

@ -118,7 +118,6 @@
:is-highlighted false}]))))
(mf/defc nitrate-sidebar*
{::mf/props :obj}
[{:keys [profile teams]}]
(let [nitrate? (dnt/is-valid-license? profile)
nitrate-license (:subscription profile)

View File

@ -372,8 +372,7 @@
(st/emit! (dtm/update-member-role params))))
(mf/defc team-member*
{::mf/wrap [mf/memo]
::mf/props :obj}
{::mf/wrap [mf/memo]}
[{:keys [team member total-members profile]}]
(let [member-id (:id member)
@ -501,8 +500,7 @@
:on-leave on-leave'}]]]))
(mf/defc team-members*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [team profile]}]
(let [members (get team :members)
@ -541,7 +539,6 @@
:total-members total-members}])]]))
(mf/defc team-members-page*
{::mf/props :obj}
[{:keys [team profile]}]
(mf/with-effect [team]
(dom/set-html-title
@ -570,7 +567,6 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc invitation-role-selector*
{::mf/props :obj}
[{:keys [can-invite role status on-change]}]
(let [show? (mf/use-state false)
label (cond
@ -616,8 +612,7 @@
(tr "labels.viewer")]]]]))
(mf/defc invitation-actions*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [invitation team-id]}]
(let [email (:email invitation)
copied* (mf/use-state false)
@ -671,8 +666,7 @@
(mf/defc invitation-row*
{::mf/wrap [mf/memo]
::mf/private true
::mf/props :obj}
::mf/private true}
[{:keys [invitation can-invite team-id selected on-select-change]}]
(let [expired? (:expired invitation)
@ -738,8 +732,7 @@
:team-id team-id}])]]))
(mf/defc empty-invitation-table*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [can-invite team]}]
(let
[route (mf/deref refs/route)
@ -879,8 +872,7 @@
(tr accept-key)]]]]]))
(mf/defc invitation-section*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [team]}]
(let [permissions (get team :permissions)
invitations (mf/use-state (get team :invitations))
@ -1064,7 +1056,6 @@
:on-select-change on-select-change}])])]))
(mf/defc team-invitations-page*
{::mf/props :obj}
[{:keys [team profile]}]
(mf/with-effect [team]
@ -1218,7 +1209,6 @@
(tr "modals.create-webhook.submit-label"))}]]]]]]))
(mf/defc webhooks-hero*
{::mf/props :obj}
[]
[:div {:class (stl/css :webhooks-hero-container)}
[:h2 {:class (stl/css :hero-title)}
@ -1230,8 +1220,7 @@
(tr "dashboard.webhooks.create")]])
(mf/defc webhook-actions*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [on-edit on-delete can-edit]}]
(let [show? (mf/use-state false)
on-show (mf/use-fn #(reset! show? true))
@ -1254,7 +1243,6 @@
(mf/defc webhook-item*
{::mf/wrap [mf/memo]
::mf/props :obj
::mf/private true}
[{:keys [webhook permissions]}]
(let [error-code (:error-code webhook)
@ -1320,8 +1308,7 @@
:can-edit can-edit}]]]))
(mf/defc webhooks-list*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [webhooks permissions]}]
[:div {:class (stl/css :table-rows :webhook-table)}
(for [webhook webhooks]
@ -1331,7 +1318,6 @@
:permissions permissions}])])
(mf/defc webhooks-page*
{::mf/props :obj}
[{:keys [team]}]
(let [webhooks (:webhooks team)]
@ -1363,7 +1349,6 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc team-settings-page*
{::mf/props :obj}
[{:keys [team]}]
(let [nitrate? (contains? cfg/flags :nitrate)
finput (mf/use-ref)

View File

@ -64,8 +64,7 @@
:on-finish-import on-finish}))))
(mf/defc title*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [on-click is-collapsed]}]
(let [on-key-down
(mf/use-fn
@ -171,7 +170,6 @@
[:div {:class (stl/css :template-link-text)} (tr "dashboard.libraries-and-templates.explore")]]]]]]))
(mf/defc templates-section*
{::mf/props :obj}
[{:keys [default-project-id profile project-id team-id]}]
(let [templates (mf/deref builtin-templates)
templates (mf/with-memo [templates]

View File

@ -26,7 +26,7 @@
transition: inset-block-end 300ms;
width: calc(100% - $sz-12);
pointer-events: none;
z-index: var(--z-index-set);
z-index: var(--z-index-panels);
&.collapsed {
inset-block-end: calc(-1 * px2rem(228));

View File

@ -45,8 +45,7 @@
:files files}))
(mf/defc export-entry*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [file]}]
[:div {:class (stl/css-case
:file-entry true

View File

@ -46,8 +46,7 @@
(st/emit! (modal/hide)))))
(mf/defc modal-wrapper*
{::mf/props :obj
::mf/wrap [mf/memo]}
{::mf/wrap [mf/memo]}
[{:keys [data]}]
(let [wrapper-ref (mf/use-ref nil)
components (mf/deref modal/components)
@ -82,7 +81,6 @@
(l/derived ::modal/modal st/state))
(mf/defc modal-container*
{::mf/props :obj}
[]
(let [container (hooks/use-portal-container :modal)]
(when-let [modal (mf/deref ref:modal)]

View File

@ -7,16 +7,24 @@
(ns app.main.ui.settings.options
(:require-macros [app.main.style :as stl])
(:require
[app.config :as cf]
[app.main.data.notifications :as ntf]
[app.main.data.profile :as du]
[app.main.refs :as refs]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.ds.controls.switch :refer [switch*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[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.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.theme :as theme]
[rumext.v2 :as mf]))
(def ^:private schema:options-form
[:map {:title "OptionsForm"}
[:lang {:optional true} [:string {:max 20}]]
@ -76,15 +84,45 @@
:data-testid "submit-lang-change"
:class (stl/css :btn-primary)}]]))
;; --- Password Page
(defn ^:private go-settings-feedback
[event]
(dom/prevent-default event)
(st/emit! (rt/nav :settings-feedback)))
(mf/defc webgl-settings*
[{:keys [renderer]}]
(let [wasm-renderer? (= renderer :wasm)
handle-render-change
(mf/use-fn
(fn [enabled?]
(st/emit! (du/update-profile-props {:renderer (if enabled? :wasm :svg)})
(ntf/success (tr (if enabled?
"webgl.toast.webgl-render-enabled"
"webgl.toast.webgl-render-disabled"))))))]
[:section {:class (stl/css :webgl-container)}
[:header {:class (stl/css :webgl-header)}
[:> heading* {:class (stl/css :title) :level 2 :typography t/title-large} (tr "dashboard.webgl-switch.title")]
[:> text* {:as "span" :class (stl/css :beta) :typography t/body-small} (tr "dashboard.webgl-switch.beta")]]
[:> text* {:class (stl/css :description) :typography t/body-medium} (tr "dashboard.webgl-switch.description")]
[:form {:class (stl/css :webgl-form)}
[:> heading* {:level 3 :typography t/headline-small} (tr "dashboard.webgl-switch.status")]
[:> switch* {:label (if wasm-renderer? (tr "dashboard.webgl-switch.enabled") (tr "dashboard.webgl-switch.disabled"))
:default-checked wasm-renderer?
:on-change handle-render-change}]]
[:> text* {:typography t/body-medium :class (stl/css :feedback)} [:a {:href "#" :on-click go-settings-feedback :class (stl/css :link)} (tr "dashboard.webgl-switch.feedback") [:> icon* {:icon-id "arrow-up-right" :size "s"}]]]]))
(mf/defc options-page
[]
(mf/use-effect
#(dom/set-html-title (tr "title.settings.options")))
(let [profile (mf/deref refs/profile)
renderer (or (-> profile :props :renderer) :svg)]
(mf/use-effect
#(dom/set-html-title (tr "title.settings.options")))
[:div {:class (stl/css :dashboard-settings)}
[:div {:class (stl/css :form-container) :data-testid "settings-form"}
[:h2 (tr "labels.settings")]
[:& options-form {}]]])
[:div {:class (stl/css :dashboard-settings)}
[:*
[:div {:class (stl/css :form-container) :data-testid "settings-form"}
[:h2 (tr "labels.settings")]
[:& options-form {}]]
(when (contains? cf/flags :render-switch)
[:> webgl-settings* {:renderer renderer}])]]))

View File

@ -5,3 +5,59 @@
// Copyright (c) KALEIDOS INC
@use "./profile" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
.dashboard-settings {
display: grid;
}
.form-container {
&:first-child {
margin-block-end: var(--sp-xxxl);
}
}
/* Copied from profile.scss .form-container, but without included nested
rules, since we want custom styles for it */
.webgl-container {
display: grid;
grid-auto-rows: auto;
gap: var(--sp-s);
width: $sz-500;
margin-block-start: var(--sp-xxxl);
padding-block-start: var(--sp-xxxl);
margin-block-end: $sz-120; /* FIXME: this should be a proper token */
border-block-start: $b-1 solid var(--color-background-quaternary);
color: var(--color-foreground-primary);
}
.webgl-header {
display: flex;
flex-direction: row;
align-items: baseline;
gap: var(--sp-m);
}
.description {
color: var(--color-foreground-secondary);
}
.beta {
color: var(--color-accent-primary);
padding: var(--sp-xxs) var(--sp-s);
border: $b-1 solid var(--color-accent-primary);
border-radius: $br-4;
}
.title {
color: var(--color-foreground-primary);
}
.link {
color: var(--color-accent-primary);
text-decoration: none;
display: flex;
align-items: center;
gap: var(--sp-xs);
}

View File

@ -25,7 +25,6 @@
[rumext.v2 :as mf]))
(mf/defc plan-card*
{::mf/props :obj}
[{:keys [card-title
card-title-icon
price-value price-period

View File

@ -41,7 +41,6 @@
(def TimeoutError rxjs/TimeoutError)
(mf/defc error-container*
{::mf/props :obj}
[{:keys [children]}]
(let [profile-id (:profile-id @st/state)
on-nav-root (mf/use-fn #(st/emit! (rt/nav-root)))]
@ -186,7 +185,6 @@
[:& recovery-sent-page {:email @user-email}]])]]]))
(mf/defc request-dialog*
{::mf/props :obj}
[{:keys [title content button-text on-button-click cancel-text on-close]}]
(let [on-click (or on-button-click on-close)]
[:div {:class (stl/css :overlay)}
@ -532,7 +530,6 @@
children])
(mf/defc exception-page*
{::mf/props :obj}
[{:keys [data route] :as props}]
(let [type (:type data)

View File

@ -278,7 +278,6 @@
:zoom zoom}])]])
(mf/defc viewer-content*
{::mf/props :obj}
[{:keys [data page-id share-id section index interactions-mode share]}]
(let [{:keys [file users project permissions]} data
allowed (or

View File

@ -235,7 +235,6 @@
:zoom zoom}])]]]))
(mf/defc comments-sidebar*
{::mf/props :obj}
[{:keys [profiles frame page]}]
(let [profile (mf/deref refs/profile)
local (mf/deref refs/comments-local)

View File

@ -219,8 +219,7 @@
:fixed? fixed?}]))
(mf/defc flows-menu*
{::mf/wrap [mf/memo]
::mf/props :obj}
{::mf/wrap [mf/memo]}
[{:keys [page index]}]
(let [flows (not-empty (:flows page))
frames (:frames page)
@ -268,7 +267,6 @@
[:span {:class (stl/css :icon)} deprecated-icon/tick])])]]])))
(mf/defc interactions-menu*
{::mf/props :obj}
[{:keys [interactions-mode]}]
(let [show-dropdown? (mf/use-state false)
toggle-dropdown (mf/use-fn #(swap! show-dropdown? not))

View File

@ -55,8 +55,7 @@
(dom/stop-propagation event))
(mf/defc menu-entry*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [title shortcut on-click on-pointer-enter on-pointer-leave
on-unmount children is-selected icon disabled value]}]
(let [submenu-ref (mf/use-ref nil)
@ -143,14 +142,12 @@
children])])))
(mf/defc menu-separator*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[]
[:li {:class (stl/css :separator)}])
(mf/defc context-menu-edit*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [shapes]}]
(let [multiple? (> (count shapes) 1)
@ -252,8 +249,7 @@
[:> menu-separator* {}]]))
(mf/defc context-menu-layer-position*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [shapes]}]
(let [do-bring-forward (mf/use-fn #(st/emit! (dw/vertical-order-selected :up)))
do-bring-to-front (mf/use-fn #(st/emit! (dw/vertical-order-selected :top)))
@ -299,8 +295,7 @@
[:> menu-separator* {}]]))
(mf/defc context-menu-flip*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[]
(let [do-flip-vertical #(st/emit! (dw/flip-vertical-selected))
do-flip-horizontal #(st/emit! (dw/flip-horizontal-selected))]
@ -315,8 +310,7 @@
[:> menu-separator* {}]]))
(mf/defc context-menu-thumbnail*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [shapes]}]
(let [single? (= (count shapes) 1)
has-frame? (some cfh/frame-shape? shapes)
@ -332,8 +326,7 @@
[:> menu-separator* {}]])))
(mf/defc context-menu-rename*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [shapes]}]
(let [do-rename #(st/emit! (dw/start-rename-selected))]
(when (= (count shapes) 1)
@ -344,8 +337,7 @@
:on-click do-rename}]])))
(mf/defc context-menu-group*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [shapes]}]
(let [multiple? (> (count shapes) 1)
single? (= (count shapes) 1)
@ -398,8 +390,7 @@
[:> menu-separator* {}]])]))
(mf/defc context-focus-mode-menu*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[]
(let [focus (mf/deref refs/workspace-focus-selected)
do-toggle-focus-mode #(st/emit! (dw/toggle-focus-mode))]
@ -411,8 +402,7 @@
:on-click do-toggle-focus-mode}]))
(mf/defc context-menu-path*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [shapes objects disable-flatten disable-booleans]}]
(let [multiple? (> (count shapes) 1)
single? (= (count shapes) 1)
@ -489,8 +479,7 @@
:on-click do-transform-to-path}]])])]))
(mf/defc context-menu-layer-options*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [shapes]}]
(let [ids (mapv :id shapes)
do-show-shape #(st/emit! (dw/update-shape-flags ids {:hidden false}))
@ -515,8 +504,7 @@
:on-click do-lock-shape}])]))
(mf/defc context-menu-prototype*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [shapes]}]
(let [flows (mf/deref refs/workspace-page-flows)
options-mode (mf/deref refs/options-mode-global)
@ -537,8 +525,7 @@
:on-click do-add-flow}]))))
(mf/defc context-menu-layout*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [shapes]}]
(let [single? (= (count shapes) 1)
objects (deref refs/workspace-page-objects)
@ -667,8 +654,7 @@
:on-click do-remove-guides}]])))
(mf/defc context-menu-delete*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[]
(let [do-delete #(st/emit! (dw/delete-selected))]
[:*
@ -711,8 +697,7 @@
[:> context-menu-delete* props]])))
(mf/defc page-item-context-menu*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [mdata]}]
(let [page (:page mdata)
deletable? (:deletable? mdata)
@ -739,7 +724,6 @@
:on-click do-duplicate}]]))
(mf/defc viewport-context-menu*
{::mf/props :obj}
[]
(let [focus (mf/deref refs/workspace-focus-selected)
read-only? (mf/use-ctx ctx/workspace-read-only?)
@ -762,8 +746,7 @@
:on-click do-toggle-focus-mode}])]))
(mf/defc grid-track-context-menu*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [mdata]}]
(let [{:keys [type index grid-id]} mdata
do-delete-track
@ -812,8 +795,7 @@
[:> menu-entry* {:title (tr "workspace.context-menu.grid-track.row.delete-shapes") :on-click do-delete-track-shapes}]])))
(mf/defc grid-cells-context-menu*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [mdata]}]
(let [{:keys [grid cells]} mdata

View File

@ -107,8 +107,7 @@
"\u00A0")))
(mf/defc library-description*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [summary]}]
(let [components-count (get summary :components)
graphics-count (get summary :graphics)
@ -133,8 +132,7 @@
(tr "workspace.libraries.typography" (c typography-count))])]))
(mf/defc sample-library-entry*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [library importing]}]
(let [id (:id library)
importing? (deref importing)
@ -185,8 +183,7 @@
(not (ctob/empty-lib? tokens-lib))))
(mf/defc libraries-tab*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [is-shared linked-libraries shared-libraries]}]
(let [file-id (mf/use-ctx ctx/current-file-id)
search-term* (mf/use-state "")
@ -485,8 +482,7 @@
:typographies typographies}]))
(mf/defc updates-tab*
{::mf/props :obj
::mf/private true}
{::mf/private true}
[{:keys [file-id libraries]}]
;; FIXME: naming
(let [summary?* (mf/use-state true)

View File

@ -18,6 +18,7 @@
[app.main.data.exports.assets :as de]
[app.main.data.exports.files :as fexp]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.plugins :as dp]
[app.main.data.profile :as du]
[app.main.data.shortcuts :as scd]
@ -226,8 +227,10 @@
(mf/defc preferences-menu*
{::mf/private true
::mf/wrap [mf/memo]}
[{:keys [layout profile toggle-flag on-close toggle-theme]}]
(let [show-nudge-options
[{:keys [layout profile toggle-flag on-close toggle-theme toggle-render]}]
(let [renderer (or (-> profile :props :renderer) :svg)
show-nudge-options
(mf/use-fn
#(modal/show! {:type :nudge-option}))]
@ -321,7 +324,17 @@
"light" (tr "workspace.header.menu.toggle-system-theme")
"system" (tr "workspace.header.menu.toggle-dark-theme")
(tr "workspace.header.menu.toggle-light-theme"))]
[:> shortcuts* {:id :toggle-theme}]]]))
[:> shortcuts* {:id :toggle-theme}]]
(when (contains? cf/flags :render-switch)
[:> dropdown-menu-item* {:on-click toggle-render
:class (stl/css :base-menu-item :submenu-item)
:on-key-down (fn [event]
(when (kbd/enter? event)
(toggle-render event)))}
[:span {:class (stl/css :item-name)}
(if (= renderer :wasm)
(tr "workspace.header.menu.disable-webgl")
(tr "workspace.header.menu.enable-webgl"))]])]))
(mf/defc view-menu*
{::mf/private true
@ -920,6 +933,18 @@
(dom/stop-propagation event)
(st/emit! (du/toggle-theme))))
toggle-render
(mf/use-fn
(mf/deps profile)
(fn [event]
(dom/stop-propagation event)
(let [renderer (or (-> profile :props :renderer) :svg)
next-renderer (if (= renderer :wasm) :svg :wasm)]
(st/emit! (du/update-profile-props {:renderer next-renderer})
(ntf/success (tr (if (= next-renderer :wasm)
"webgl.toast.webgl-render-enabled"
"webgl.toast.webgl-render-disabled")))))))
open-plugins-manager
(mf/use-fn
(fn [event]
@ -1099,6 +1124,7 @@
:profile profile
:toggle-flag toggle-flag
:toggle-theme toggle-theme
:toggle-render toggle-render
:on-close close-sub-menu}]
:plugins

View File

@ -418,9 +418,10 @@
(cond-> #js {:pointerEvents "all"}
render-wasm?
(obj/merge!
#js {"--editor-container-width" (dm/str width "px")
"--editor-container-height" (dm/str height "px")
"--fallback-families" (if (seq fallback-families) (dm/str (str/join ", " fallback-families)) "sourcesanspro")})
#js {"--editor-container-width" "auto"
"--editor-container-height" "auto"
"--fallback-families" (if (seq fallback-families) (dm/str (str/join ", " fallback-families)) "sourcesanspro")
:display "flex"})
(not render-wasm?)
(obj/merge!

View File

@ -29,7 +29,6 @@
(mf/defc assets-libraries*
{::mf/wrap [mf/memo]
::mf/props :obj
::mf/private true}
[{:keys [filters]}]
(let [file-id (mf/use-ctx ctx/current-file-id)

View File

@ -26,7 +26,6 @@
[rumext.v2 :as mf]))
(mf/defc token-settings*
{::mf/wrap-props false}
[]
(let [file-data (deref refs/workspace-data)
base-font-size* (mf/use-state #(ctf/get-base-font-size file-data))

View File

@ -448,7 +448,6 @@
:fill "var(--app-white)"}]))]))
(mf/defc gradient-handlers-impl*
{::mf/props :obj}
[{:keys [zoom stops gradient editing shape]}]
(let [transform (gsh/transform-matrix shape)
transform-inverse (gsh/inverse-transform-matrix shape)
@ -517,8 +516,7 @@
:on-change-width on-change-width}]))
(mf/defc gradient-handlers*
{::mf/wrap [mf/memo]
::mf/props :obj}
{::mf/wrap [mf/memo]}
[{:keys [id zoom]}]
(let [shape-ref (mf/use-memo (mf/deps id) #(refs/object-by-id id))
shape (mf/deref shape-ref)

View File

@ -370,7 +370,7 @@
:else
(show-unavailable)))))]
(reset! canvas-init? false)
(->> wasm.api/module
(->> @wasm.api/module
(p/fmap (fn [ready?]
(when ready?
(try-init 3)))))
@ -378,6 +378,7 @@
(vreset! unmounted? true)
(when-let [timeout-id @timeout-id-ref]
(js/clearTimeout timeout-id))
(wasm.api/end-page-transition!)
(wasm.api/clear-canvas)))))
(mf/with-effect [show-text-editor? workspace-editor-state edition]

View File

@ -1077,16 +1077,18 @@
(defn intersect-position-in-shape
[id position]
(let [buffer (uuid/get-u32 id)
result
(h/call wasm/internal-module "_intersect_position_in_shape"
(aget buffer 0)
(aget buffer 1)
(aget buffer 2)
(aget buffer 3)
(:x position)
(:y position))]
(= result 1)))
(if (and wasm/context-initialized? (not @wasm/context-lost?))
(let [buffer (uuid/get-u32 id)
result
(h/call wasm/internal-module "_intersect_position_in_shape"
(aget buffer 0)
(aget buffer 1)
(aget buffer 2)
(aget buffer 3)
(:x position)
(:y position))]
(= result 1))
false))
(def render-finish
(letfn [(do-render []

View File

@ -1216,6 +1216,27 @@ msgstr "Type to search results"
msgid "dashboard.unpublish-shared"
msgstr "Unpublish Library"
msgid "dashboard.webgl-switch.title"
msgstr "WebGL rendering"
msgid "dashboard.webgl-switch.beta"
msgstr "Beta"
msgid "dashboard.webgl-switch.description"
msgstr "WebGL rendering can improve performance, but it is in beta and may be less stable. Some visual differences may appear between the canvas, exports (SVG/PDF), and view mode."
msgid "dashboard.webgl-switch.status"
msgstr "Status"
msgid "dashboard.webgl-switch.enabled"
msgstr "Enabled"
msgid "dashboard.webgl-switch.disabled"
msgstr "Disabled"
msgid "dashboard.webgl-switch.feedback"
msgstr "Give feedback"
#:src/app/main/ui/workspace/tokens/import_from_library.cljs
msgid "modals.import-library-tokens.title"
msgstr "Import tokens from library?"
@ -5995,6 +6016,12 @@ msgstr "Switch to light theme"
msgid "workspace.header.menu.toggle-system-theme"
msgstr "Switch to system theme"
msgid "workspace.header.menu.enable-webgl"
msgstr "Enable WebGL rendering (beta)"
msgid "workspace.header.menu.disable-webgl"
msgstr "Disable WebGL rendering (beta)"
#: src/app/main/ui/workspace/main_menu.cljs:492
msgid "workspace.header.menu.undo"
msgstr "Undo"
@ -9196,6 +9223,12 @@ msgstr "Projects and files will remain available to team members, but the organi
msgid "modals.remove-team-org.accept"
msgstr "Remove from organization"
msgid "webgl.toast.webgl-render-enabled"
msgstr "WebGL rendering enabled"
msgid "webgl.toast.webgl-render-disabled"
msgstr "WebGL rendering disabled"
#, unused
msgid "workspace.viewport.click-to-close-path"
msgstr "Click to close the path"

View File

@ -1291,6 +1291,27 @@ msgstr "Tu nombre"
msgid "dashboard.your-penpot"
msgstr "Tu Penpot"
msgid "dashboard.webgl-switch.title"
msgstr "Renderizado WebGL"
msgid "dashboard.webgl-switch.beta"
msgstr "Beta"
msgid "dashboard.webgl-switch.description"
msgstr "El renderizado WebGL puede mejorar el rendimiento, pero está en beta y puede ser menos estable. Pueden aparecer diferencias visuales entre el lienzo, las exportaciones (SVG/PDF) y el modo de vista."
msgid "dashboard.webgl-switch.status"
msgstr "Estado"
msgid "dashboard.webgl-switch.enabled"
msgstr "Activado"
msgid "dashboard.webgl-switch.disabled"
msgstr "Desactivado"
msgid "dashboard.webgl-switch.feedback"
msgstr "Enviar comentarios"
#: src/app/main/ui/alert.cljs:35
msgid "ds.alert-ok"
msgstr "Ok"
@ -5906,6 +5927,13 @@ msgstr "Cambiar a tema claro"
msgid "workspace.header.menu.toggle-system-theme"
msgstr "Cambiar a tema del sistema"
msgid "workspace.header.menu.enable-webgl"
msgstr "Activar renderizado WebGL (beta)"
msgid "workspace.header.menu.disable-webgl"
msgstr "Desactivar renderizado WebGL (beta)"
#: src/app/main/ui/workspace/main_menu.cljs:492
msgid "workspace.header.menu.undo"
msgstr "Deshacer"
@ -8937,6 +8965,12 @@ msgstr "Los proyectos y archivos seguirán estando disponibles para los miembros
msgid "modals.remove-team-org.accept"
msgstr "Eliminar de la organización"
msgid "webgl.toast.webgl-render-enabled"
msgstr "Renderizado WebGL activado"
msgid "webgl.toast.webgl-render-disabled"
msgstr "Renderizado WebGL desactivado"
#, unused
msgid "workspace.viewport.click-to-close-path"
msgstr "Pulsar para cerrar la ruta"

View File

@ -16,9 +16,9 @@
"fmt": "./scripts/fmt"
},
"devDependencies": {
"@github/copilot": "^1.0.35",
"@github/copilot": "^1.0.36",
"@types/node": "^25.6.0",
"esbuild": "^0.28.0",
"opencode-ai": "^1.14.22"
"opencode-ai": "^1.14.28"
}
}

164
pnpm-lock.yaml generated
View File

@ -9,8 +9,8 @@ importers:
.:
devDependencies:
'@github/copilot':
specifier: ^1.0.35
version: 1.0.35
specifier: ^1.0.36
version: 1.0.36
'@types/node':
specifier: ^25.6.0
version: 25.6.0
@ -18,8 +18,8 @@ importers:
specifier: ^0.28.0
version: 0.28.0
opencode-ai:
specifier: ^1.14.22
version: 1.14.22
specifier: ^1.14.28
version: 1.14.28
packages:
@ -179,44 +179,44 @@ packages:
cpu: [x64]
os: [win32]
'@github/copilot-darwin-arm64@1.0.35':
resolution: {integrity: sha512-NNZE0TOz0HOlv7eqlh6EcQbNkhtnIHReBLieW6pfDUUTKkgsqbUu1MOitF8m+LUQk3ml1T0MQ5MOfad1HSa/MQ==}
'@github/copilot-darwin-arm64@1.0.36':
resolution: {integrity: sha512-5qkb7frTS4K/LdTDLrzKo78VR4aw/EZ6JzLz4KfmaW4UYyPiNirExDFXa/By22X0o8YMfOp4MCA2KSCAxKdgTg==}
cpu: [arm64]
os: [darwin]
hasBin: true
'@github/copilot-darwin-x64@1.0.35':
resolution: {integrity: sha512-XCv/mfdv0rnrtrNVOluio/N/kyCge0uG2hghvtlgO/+z6EjvzFygkpXXS1gVxiXhWc3lX232cTXQU3zklC/8Ng==}
'@github/copilot-darwin-x64@1.0.36':
resolution: {integrity: sha512-AdsM8QtM5QSzMLpavLREh8HALO5G+VWzGNQqIHu4f0YQC/s1cGoiwo3wsgkpxRcLGBykFc+bDX3yK3MDQ8XvSw==}
cpu: [x64]
os: [darwin]
hasBin: true
'@github/copilot-linux-arm64@1.0.35':
resolution: {integrity: sha512-mbaadATfJPzmXq2SD1TWocIG/GobcYC6OvNFhCG8UXMsiXY5cevhszl5ujuayhPJBxS77Yj5uvIFjNQ1Kf5V8Q==}
'@github/copilot-linux-arm64@1.0.36':
resolution: {integrity: sha512-n7K1I6r0ggOJ4A9uAMS11USTvn6BKtAwvrOkzEaeRK89VNUJzpTe6p0mE13ItzRe5eot9WLBQOxvXLtL9f6E+g==}
cpu: [arm64]
os: [linux]
hasBin: true
'@github/copilot-linux-x64@1.0.35':
resolution: {integrity: sha512-NrZ0VjztdBbJ5qAmuUtuKsWkimOaqzjDV+ZGUv1FxSxoys40kiiakQ5WbnMFDzaIFaf47zDi++6ixgQzq7Jk5A==}
'@github/copilot-linux-x64@1.0.36':
resolution: {integrity: sha512-wBtCdR3ITZcq07BJbkwHfwI6ayiwbH5pF1ex+Ycl4UI+Lf1vP9eQD6wJppPgsrjwFcdeWRThaYTPCRTkSGHv5g==}
cpu: [x64]
os: [linux]
hasBin: true
'@github/copilot-win32-arm64@1.0.35':
resolution: {integrity: sha512-KQN7Q7+oPyglmvUEiMp6SYWjl30VSu91T0dUpNHbUs/xRM3qgnCymLPPUyBZGWHog/FueUAsRkhisMHWQVnO+g==}
'@github/copilot-win32-arm64@1.0.36':
resolution: {integrity: sha512-0GzZUZQn07alI8BgbzK0NlR5+ta/Rd0sWmd8kbRCns7oybAIkSALy6BKVwJmVHtXUi6h4iUE8oiFhkn0spymvw==}
cpu: [arm64]
os: [win32]
hasBin: true
'@github/copilot-win32-x64@1.0.35':
resolution: {integrity: sha512-J0XhXO2FmlFr8pGa970xEd4tr1rqFiZxoaPW5WvkJYZoZUHbBhFcGasp5/yEeJ71b3vI4PHm/mSZZebD3ALMKQ==}
'@github/copilot-win32-x64@1.0.36':
resolution: {integrity: sha512-UBX9qj0McCK/SLq93XIr1i80fj3b3XmE3befVFrzxQuTeOoxLURN35vi7W+4x+4ZfsDHQpRTlJNjZw9w0fPr+Q==}
cpu: [x64]
os: [win32]
hasBin: true
'@github/copilot@1.0.35':
resolution: {integrity: sha512-O1nUy8DXOTE+v86b/FTkyu09EMrDy+vj+2rhmUOcmsXGe0RE5ECyESsasUTUoHK/CSgAExFTziNxbubUoiMMfg==}
'@github/copilot@1.0.36':
resolution: {integrity: sha512-x0N5wLzw+tANzb+vCFYLHn3BV3qii2oyn14wC20RO7SsS8/YeBH8olvwlDLJ4PB0mL17QOiytNCdkvjvprm28w==}
hasBin: true
'@types/node@25.6.0':
@ -227,67 +227,67 @@ packages:
engines: {node: '>=18'}
hasBin: true
opencode-ai@1.14.22:
resolution: {integrity: sha512-J+q1Ehlfg7SSXw2aIY8Mb47FHhPTN8IciKNt0/D+H/brO8RWLe67WjFzxhh/z9SSad9wPcCiLRGAc/iAn8W8wA==}
opencode-ai@1.14.28:
resolution: {integrity: sha512-ZPukJNvujSVa+LVoXvj2ciUV57UcnuxmMtzpFQBYd6fbhjeT1vMC6jCurO/5mIp76fiPmGM7ilzRXVeY6bIwPw==}
hasBin: true
opencode-darwin-arm64@1.14.22:
resolution: {integrity: sha512-h9FjzNoDRsuJD0EEg535P9ul5TyrWovwx591VmuG8fp9d4PoSrAN1O3Zi07GJjkrYyrB8g3c+x5whDqJCz+qog==}
opencode-darwin-arm64@1.14.28:
resolution: {integrity: sha512-Gu2vZYACAeoewfPhgJDAaScwRo1K5YZq7tVpPKw2rpul34OpOPLk4oB4Pmr539iWiagK+DLuUxnbJIbRRYCS5g==}
cpu: [arm64]
os: [darwin]
opencode-darwin-x64-baseline@1.14.22:
resolution: {integrity: sha512-GgfP0wSm9/I+j3shOxfeA++7yZpXS6Y1Vis258nEFoRS9Xfv3YlHom7c/8BR9rYqeUE/+rrijP7PrGWGl+IHBw==}
opencode-darwin-x64-baseline@1.14.28:
resolution: {integrity: sha512-/KsZkZh5oh6urHWwIHJudS6sedBil59E/4o7/7TuxPy/pOdRlSlSWVkMJd20AmqM4G/qILF/GthXy3D2+f99Tg==}
cpu: [x64]
os: [darwin]
opencode-darwin-x64@1.14.22:
resolution: {integrity: sha512-cyKRo22sxDwu4ITOlENwXaqVM9kMGndwSaAd95gz1Rmz5NYMShUO/8eckrD2MhS2wm+QvKw9XkRVWVHWQlZw3Q==}
opencode-darwin-x64@1.14.28:
resolution: {integrity: sha512-D6BnAXlSdQDRtZgAg6OxWT6CEzbbONnlYof9hdPbaIIaNyBLjqK+Er2O1rrbiXFhSbs7YHBiDoGd+nNUymx4Ng==}
cpu: [x64]
os: [darwin]
opencode-linux-arm64-musl@1.14.22:
resolution: {integrity: sha512-DtSd5tbGk6R5+hGhqViSvbY8ICf+u4oVQhfvCAplQCb1UEwYVc0+oAF6PimFJ+o8i8L6x14O0rry0NaRzZ0CzA==}
opencode-linux-arm64-musl@1.14.28:
resolution: {integrity: sha512-7R1GHqSg/UuT9r77GF2skh8r66WkZcphmDWAWaV2dmptJlxEJeV9I2jbE2i8Ctp4BzPUexFqfSoBA82S9Dcf+A==}
cpu: [arm64]
os: [linux]
opencode-linux-arm64@1.14.22:
resolution: {integrity: sha512-ohK4LkkGvzB4ptr0nqDOVi2JEJMLROfy1s2U2A4Qrh+1Y0QimgH2b5VgTm+BjA3bC2Hm8Yf/IfkitqlUnCp7YA==}
opencode-linux-arm64@1.14.28:
resolution: {integrity: sha512-jdTrs4YpPGFGZOMLuiaSfOUzkjAA+lnIEaW6HYLvaey3WsBnu3S4utaBhXURincp20H1JPQcahDOe+jjGZH7xw==}
cpu: [arm64]
os: [linux]
opencode-linux-x64-baseline-musl@1.14.22:
resolution: {integrity: sha512-oZffotEbGXbA38Y0Dmj7IVq0ATl3nKbP8j91Z0zR5kBEBykOqExJIyc9pZpModgfPf86k98XBsRHiVLK4u9ARw==}
opencode-linux-x64-baseline-musl@1.14.28:
resolution: {integrity: sha512-GKxZXj8/Mbutfs1DW4v0/rEWcAQrD/RUI9kV9VhMoNA8vUt0nuA3H9UvbFXh9EJj2C+RBSPLlMGal++oCH4c4w==}
cpu: [x64]
os: [linux]
opencode-linux-x64-baseline@1.14.22:
resolution: {integrity: sha512-J67YAIWr3E03o9e6wNaPEqBo+9FcPKf5CzjIUSb8yNDyobWON1HHihcuu0hCJ6wF9J9awmlp2/4mO1HOoCo3QQ==}
opencode-linux-x64-baseline@1.14.28:
resolution: {integrity: sha512-Dtl+xjEAKaWNk2l3iC9ebwi79BkChHIdtx97ksZKTLjAeR424Zh3vnjuWjpMYk9YAnesVlwL8y4kHs2Y736Zpw==}
cpu: [x64]
os: [linux]
opencode-linux-x64-musl@1.14.22:
resolution: {integrity: sha512-r+QnqwR/OPmMm197Kb8VLD9mkZGFXz4m5QCZFxOAL34k8AhQZqn3d2mx2bfrMBVfoSiSVxa3jEjZEbNNFGlICQ==}
opencode-linux-x64-musl@1.14.28:
resolution: {integrity: sha512-XyzWl35L8N6El/hxAM28bDUHLCY0aujMtprDTCYXVckeNxBkN3idM4EfdLtJaUHkE6bqMr+m6wXQl4oYDoOtpg==}
cpu: [x64]
os: [linux]
opencode-linux-x64@1.14.22:
resolution: {integrity: sha512-MSUaO/Cvfb8DFRYETVrVeCnKtoIfgLflyB+O8xQOkVtjMKJ41M+1dFSMyZ3LQa2Vfp5tDskyMhj7eUxvT/owgQ==}
opencode-linux-x64@1.14.28:
resolution: {integrity: sha512-XnpQrud15bvUBvOI58tOGUBTrwqKHl6bYQ3eoy5HhGa2spUnRv3B/HU8QiS6QuNbmkPxRPR+vuTGtBYQvtRGPw==}
cpu: [x64]
os: [linux]
opencode-windows-arm64@1.14.22:
resolution: {integrity: sha512-8grcxLSf9BD9Bt38MIxXfkI6aOFophVgM0US5r8nAUdVU78/8TS9Flnn6D39GM5RmxzqGWMl1u10vMFrBtMwPA==}
opencode-windows-arm64@1.14.28:
resolution: {integrity: sha512-emR1oEoLe6soASahJNX6IwR9x8rJkbwBXDnXNTWQcGdSxKBMD4/cLkq84k/5zqLfB7dbUChTw7eFz7u8Sa5VQw==}
cpu: [arm64]
os: [win32]
opencode-windows-x64-baseline@1.14.22:
resolution: {integrity: sha512-R/o36LpmQmbv/tL2pkcmApn6030z/1oJIYmjDkW5a4K5MXmV7aq+jWrH5p6iYKp9fo9L8oCtOp/rELMBqDS3UA==}
opencode-windows-x64-baseline@1.14.28:
resolution: {integrity: sha512-ARKHTThHezib44QPLiivYI8c71iNE9iNDubwV5XxUhM2FtzMJkZGma+EgbcCsXwY5r0lAsarzzDMqYB0YfCZ1A==}
cpu: [x64]
os: [win32]
opencode-windows-x64@1.14.22:
resolution: {integrity: sha512-jVbZ4VA5b5MF2QhWQOE1VYBKdBE0v/ZebFjwzs6Vieazfgr6OFnGSHVP5WJbU/r6zDssbTBzzpnFxo0IY1SQWw==}
opencode-windows-x64@1.14.28:
resolution: {integrity: sha512-tEpblIEdmlJ7npo5Bq+1O7uup9jCOyqnnA63t+3JQiNQ1et3UTjNb5ruAjb7sudUer6i5MlQCwNXBjitjuU4Kg==}
cpu: [x64]
os: [win32]
@ -374,32 +374,32 @@ snapshots:
'@esbuild/win32-x64@0.28.0':
optional: true
'@github/copilot-darwin-arm64@1.0.35':
'@github/copilot-darwin-arm64@1.0.36':
optional: true
'@github/copilot-darwin-x64@1.0.35':
'@github/copilot-darwin-x64@1.0.36':
optional: true
'@github/copilot-linux-arm64@1.0.35':
'@github/copilot-linux-arm64@1.0.36':
optional: true
'@github/copilot-linux-x64@1.0.35':
'@github/copilot-linux-x64@1.0.36':
optional: true
'@github/copilot-win32-arm64@1.0.35':
'@github/copilot-win32-arm64@1.0.36':
optional: true
'@github/copilot-win32-x64@1.0.35':
'@github/copilot-win32-x64@1.0.36':
optional: true
'@github/copilot@1.0.35':
'@github/copilot@1.0.36':
optionalDependencies:
'@github/copilot-darwin-arm64': 1.0.35
'@github/copilot-darwin-x64': 1.0.35
'@github/copilot-linux-arm64': 1.0.35
'@github/copilot-linux-x64': 1.0.35
'@github/copilot-win32-arm64': 1.0.35
'@github/copilot-win32-x64': 1.0.35
'@github/copilot-darwin-arm64': 1.0.36
'@github/copilot-darwin-x64': 1.0.36
'@github/copilot-linux-arm64': 1.0.36
'@github/copilot-linux-x64': 1.0.36
'@github/copilot-win32-arm64': 1.0.36
'@github/copilot-win32-x64': 1.0.36
'@types/node@25.6.0':
dependencies:
@ -434,55 +434,55 @@ snapshots:
'@esbuild/win32-ia32': 0.28.0
'@esbuild/win32-x64': 0.28.0
opencode-ai@1.14.22:
opencode-ai@1.14.28:
optionalDependencies:
opencode-darwin-arm64: 1.14.22
opencode-darwin-x64: 1.14.22
opencode-darwin-x64-baseline: 1.14.22
opencode-linux-arm64: 1.14.22
opencode-linux-arm64-musl: 1.14.22
opencode-linux-x64: 1.14.22
opencode-linux-x64-baseline: 1.14.22
opencode-linux-x64-baseline-musl: 1.14.22
opencode-linux-x64-musl: 1.14.22
opencode-windows-arm64: 1.14.22
opencode-windows-x64: 1.14.22
opencode-windows-x64-baseline: 1.14.22
opencode-darwin-arm64: 1.14.28
opencode-darwin-x64: 1.14.28
opencode-darwin-x64-baseline: 1.14.28
opencode-linux-arm64: 1.14.28
opencode-linux-arm64-musl: 1.14.28
opencode-linux-x64: 1.14.28
opencode-linux-x64-baseline: 1.14.28
opencode-linux-x64-baseline-musl: 1.14.28
opencode-linux-x64-musl: 1.14.28
opencode-windows-arm64: 1.14.28
opencode-windows-x64: 1.14.28
opencode-windows-x64-baseline: 1.14.28
opencode-darwin-arm64@1.14.22:
opencode-darwin-arm64@1.14.28:
optional: true
opencode-darwin-x64-baseline@1.14.22:
opencode-darwin-x64-baseline@1.14.28:
optional: true
opencode-darwin-x64@1.14.22:
opencode-darwin-x64@1.14.28:
optional: true
opencode-linux-arm64-musl@1.14.22:
opencode-linux-arm64-musl@1.14.28:
optional: true
opencode-linux-arm64@1.14.22:
opencode-linux-arm64@1.14.28:
optional: true
opencode-linux-x64-baseline-musl@1.14.22:
opencode-linux-x64-baseline-musl@1.14.28:
optional: true
opencode-linux-x64-baseline@1.14.22:
opencode-linux-x64-baseline@1.14.28:
optional: true
opencode-linux-x64-musl@1.14.22:
opencode-linux-x64-musl@1.14.28:
optional: true
opencode-linux-x64@1.14.22:
opencode-linux-x64@1.14.28:
optional: true
opencode-windows-arm64@1.14.22:
opencode-windows-arm64@1.14.28:
optional: true
opencode-windows-x64-baseline@1.14.22:
opencode-windows-x64-baseline@1.14.28:
optional: true
opencode-windows-x64@1.14.22:
opencode-windows-x64@1.14.28:
optional: true
undici-types@7.19.2: {}