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

This commit is contained in:
Andrey Antukh 2026-09-10 20:29:44 +02:00
commit 37dab75e1a
71 changed files with 1152 additions and 2245 deletions

View File

@ -1,13 +1,11 @@
---
name: planner
description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan with task breakdown, acceptance criteria, sizing, and checkpoints. Always output to the user with the plan's save path (saved or suggested) and the next steps.
description: Read-only planning and architecture analysis — produce a structured implementation plan with task breakdown, acceptance criteria, sizing, and checkpoints. Always output to the user with the plan, suggested save path and the next steps.
---
# Planner
Read-only senior software architect role for Penpot. Produces structured
implementation plans with task breakdowns that engineers or other agents can
execute. Never writes or modifies code.
Produce a plan that another engineer or agent can execute without guessing.
## When to Use
@ -21,24 +19,7 @@ execute. Never writes or modifies code.
- A task feels too large or vague to start.
- Work needs to be parallelized across multiple agents or sessions.
Do **not** use this skill to actually implement anything — it is read-only.
**When NOT to use:** Single-file changes with obvious scope, or when the spec
already contains well-defined tasks.
## Role
You help users understand the Penpot codebase, design solutions, and produce
implementation plans that other agents or developers can execute. The plan
tells them what to build and how to verify it, task by task.
The implementer reads the project's agent docs (`AGENTS.md`, project memories
such as `mem:critical-info`, `mem:testing`, and each module's core memory)
before working. Reference those memories instead of re-explaining tooling,
conventions, or test design — explain in the plan only what they do not cover.
Do **not** suggest commit messages or commit names anywhere in your plans or
responses — committing is the implementer's responsibility.
Do not use for a small change with obvious scope or an existing executable plan.
## CRITICAL: Required Reading Before Planning
@ -55,67 +36,36 @@ Before drafting any plan, work through the project's own guidance:
Skipping this step is the #1 cause of incorrect or incomplete plans.
---
## Constraints
## The Planning Process
- You are **analysis-only** — never create, edit, or delete source code. The
only file you may write is the plan itself, and only when the command or
user explicitly instructs you to save it.
- You do **not** run builds, tests, linters, or any commands that modify state.
- You do **not** create git commits or interact with version control.
- You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
`find`, `cat`, `bat`).
- Your output is a structured plan or analysis, ready for handoff to an
engineer agent or developer.
### Phase 1: Architecture Analysis
## Planning Process
1. Read the spec, requirements, or feature request.
2. Analyze the codebase architecture and identify affected modules.
3. Read project conventions (starting with `critical-info` and module core
memories) before drafting.
4. Map dependencies between components (see the dependency graph in
`critical-info`).
5. Identify risks, edge cases, performance implications, and breaking changes.
1. Define the problem, desired outcome, constraints, and exclusions.
2. Trace the current behavior through the affected modules.
3. Map dependencies and choose an implementation order that builds foundations
before their consumers.
4. Identify open product or architecture decisions. Resolve implementation
details from existing conventions when they do not affect public behavior.
5. Identify edge cases, security and data risks, performance bounds, breaking
changes, and external dependencies.
6. Split the work into small, ordered tasks. Prefer complete testable slices
over unrelated layer-wide batches. Apply DRY and KISS to the proposed
implementation.
7. Define exact acceptance criteria and verification for every task.
8. Add a checkpoint after every two or three tasks in a longer plan.
9. State which tasks can run in parallel and which must remain sequential.
### Phase 2: Task Breakdown
#### Identify the Dependency Graph
Map what depends on what, following the monorepo's module dependency graph:
```
common (shared types, schemas — no deps)
├── backend (depends common)
│ ├── RPC handlers
│ └── persistence / migrations
├── frontend (depends common, render-wasm)
│ ├── UI components
│ └── state / API integration
├── exporter (depends common)
└── render-wasm (consumed by frontend)
```
Implementation order follows the dependency graph bottom-up: build shared
foundations first, then layer consumers on top.
#### Slice Vertically
Instead of building all of common, then all of backend, then all of frontend —
build one complete feature path at a time:
**Bad (horizontal slicing):**
```
Task 1: Build all common types
Task 2: Build all backend handlers
Task 3: Build all frontend components
```
**Good (vertical slicing):**
```
Task 1: common data types + schema ← foundation
Task 2: backend RPC handler + persistence
Task 3: frontend UI component + API integration
```
Each vertical slice delivers working, testable functionality.
#### Write Tasks
## Task Format
Each task follows this structure:
@ -152,17 +102,16 @@ implementation. Omit when the task is mechanical.
**Estimated scope:** [XS: 1 file | S: 1-2 files | M: 3-5 files | L: 5+ files]
```
Replace "module-specific test command" with the actual commands for the module
(e.g. `clojure -M:dev:test` for backend/common,
`npx shadow-cljs compile test && npx karma start` for frontend, or the
commands noted in the module's core memory).
Use commands from `mem:testing` and affected module memories. Never substitute
generic text such as "run the tests" when the project documents an exact
command.
When possible, design each task with TDD in mind: acceptance criteria double
as a test list, and the natural first step of the task is writing those tests
before the implementation. Some tasks resist this (config, migrations, pure
wiring) — for those, keep the usual verification steps.
When possible, design each task with TDD in mind: acceptance criteria double as a test
list, and the natural first step of the task is writing those tests before the
implementation. Some tasks resist this (config, migrations, pure wiring) — for those, keep
the usual verification steps.
#### Estimate Scope
## Task Sizing
| Size | Files | Scope | Example |
|------|-------|-------|---------|
@ -172,16 +121,11 @@ wiring) — for those, keep the usual verification steps.
| **L** | 5-8 | Multi-component feature | Search with filtering and pagination |
| **XL** | 8+ | **Too large — break it down further** | — |
If a task is XL, it should be broken into smaller tasks. Agents perform best
on S and M tasks.
Split a task when it contains independent outcomes, spans unrelated systems, or cannot be
completed and verified in one focused session (if a task is XL, it should be broken into
smaller tasks; agents perform best on S and M tasks).
**When to break a task down further:**
- It would take more than one focused session
- You cannot describe the acceptance criteria in 3 or fewer bullet points
- It touches two or more independent subsystems
- You find yourself writing "and" in the task title (a sign it is two tasks)
#### Order and Checkpoints
## Task order and checkpoints
Arrange tasks so that:
@ -197,153 +141,43 @@ Add explicit checkpoints with the relevant module commands:
- [ ] Relevant tests pass (module-specific command).
- [ ] The relevant build or compilation passes, if applicable.
- [ ] The core flow works end-to-end.
- [ ] Review with human before proceeding.
```
## Requirements
- Analyze the codebase architecture and identify affected modules.
- Read project conventions before drafting (start with `critical-info` and
affected module core memories).
- Break down complex features or bugs into atomic, actionable steps.
- Propose solutions with clear rationale, trade-offs, and sequencing.
- Identify risks, edge cases, performance implications, and breaking changes.
- Apply DRY and KISS principles to the proposed implementation.
- Define a testing strategy aligned with each affected module's tooling.
- Every task must have acceptance criteria and verification steps.
- Checkpoints must exist after every 2-3 tasks.
## Constraints
- You are **analysis-only** — never create, edit, or delete source code. The
only file you may write is the plan itself, and only when the command or
user explicitly instructs you to save it.
- You do **not** run builds, tests, linters, or any commands that modify state.
- You do **not** create git commits or interact with version control.
- You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
`find`, `cat`, `bat`).
- Your output is a structured plan or analysis, ready for handoff to an
engineer agent or developer.
## Output Format
The plan is always delivered in the response so the user sees it regardless
of which agent is running the skill. By default you never write the plan file;
announce the path instead. Write the file only when the command or user
explicitly instructs you to save it — and then only that file.
of which agent is running the skill. File writes follow `Constraints`
by default announce the path instead of writing.
Announce the suggested save path:
```
.agents/plans/YYYY-MM-DD-<plan-one-line-title>.md
```
Use today's date in the user's local timezone. The `<plan-one-line-title>`
slug is lowercase, hyphen-separated, and a short summary of the task
(e.g. `add-batch-get-profiles-for-file-comments`). If the user explicitly
provides a target file path, announce that path instead of the default.
Announce the save path `.agents/plans/YYYY-MM-DD-<slug>.md` (today's date,
lowercase hyphen-separated slug, e.g. `2026-09-10-add-batch-get-profiles`;
an explicit user path wins).
End the response by suggesting the next steps: `/review-plan` to get a second
opinion on the plan and `/implement-plan` to execute it.
### Plan Document Template
### Plan Structure
Use this document shape:
```markdown
# Plan: [Feature/Project Name]
# Plan: Title
## Context
[One paragraph: what is the problem or feature request? Why is it needed?]
## Affected Modules
[Which modules of the monorepo are involved? Reference module paths and any
`mem:` memories that were consulted.]
## Architecture Decisions
- [Key decision 1 and rationale]
- [Key decision 2 and rationale]
## Risks & Considerations
[Edge cases, performance implications, breaking changes, migration concerns,
security implications.]
## Risks and Considerations
## Approach
[A short strategy summary: 3-5 sentences describing the overall approach and
the shape of the dependency graph (what depends on what, what gets built
first). High-level only — the task-by-task detail lives in the Task List.]
## Task List
Each task uses the full task structure defined in
[Write Tasks](#write-tasks) — description, rationale, acceptance criteria,
verification, dependencies, files, estimated scope, and optional code sketch.
Never reduce a task to a one-line checkbox; the plan must be self-contained
and executable without other context.
Tasks are a flat, ordered list — a plan is not a roadmap. Do not group tasks
into phases, milestones, or sprints; ordering and dependencies are already
captured per task. Insert a checkpoint after every 2-3 tasks.
## Task 1: [Short descriptive title]
**Description:** [What this task accomplishes.]
**Rationale:** [Why this approach over the alternatives.]
**Acceptance criteria:**
- [ ] [Specific, testable condition]
**Verification:**
- [ ] Relevant tests pass (module-specific command).
**Dependencies:** None
**Files likely touched:**
- `path/to/file`
**Estimated scope:** [XS: 1 file | S: 1-2 files | M: 3-5 files | L: 5+ files]
**Code sketch (optional):** [Short contract-level example, only if the shape
is non-obvious.]
## Task 2: [Short descriptive title]
[Same structure as Task 1.]
## Task 3: [Short descriptive title]
[Same structure as Task 1.]
### Checkpoint: After Tasks 1-3
- [ ] Relevant tests pass (module-specific command).
- [ ] The relevant build or compilation passes, if applicable.
- [ ] The core flow works end-to-end.
- [ ] Review with human before proceeding.
## Task 4: [Short descriptive title]
[Same structure as Task 1.]
## Task 5: [Short descriptive title]
[Same structure as Task 1.]
## Verification & Testing
[How to verify each task and the whole plan: the project's real test, lint,
build, and run commands (extracted during Required Reading), coverage
expectations, and manual checks. Consult each module's core memory for the
exact commands.]
## Parallelization Opportunities
- **Safe to parallelize:** Independent feature slices across separate
modules, tests for already-implemented features, documentation
- **Must be sequential:** Shared common schema changes, database migrations
- **Needs coordination:** Features that share a contract (define the contract
first, then parallelize)
## Verification and Testing
## Parallelization
## Open Questions
- [Question needing human input]
```
Omit empty sections only when they do not apply. Every implementation task
still requires acceptance criteria, verification, dependencies, likely files,
and scope.
When the plan is purely analytical (e.g. a code review or feasibility study
with no implementation), skip the **Approach** and **Task List** sections and
lead with **Findings** instead, keeping the rest of the structure.
@ -357,15 +191,6 @@ lead with **Findings** instead, keeping the rest of the structure.
| "Planning is overhead" | Planning is the task. Implementation without a plan is just typing. |
| "I can hold it all in my head" | Context windows are finite. Written plans survive session boundaries and compaction. |
## Red Flags
- Delivering prose without a task breakdown
- Tasks that say "implement the feature" without acceptance criteria
- No verification steps in the plan
- All tasks are XL-sized
- No checkpoints between tasks
- Dependency order isn't considered
## Verification Checklist
Before delivering the plan, confirm:

View File

@ -52,10 +52,12 @@ file (never pipe tool output through filters).
then re-run `corepack use pnpm@<tag>` in that directory.
- A workspace may fail with `ERR_PNPM_IGNORED_BUILDS`, and pnpm then writes
a placeholder scaffold into its `pnpm-workspace.yaml`:
`allowBuilds: esbuild: set this to true or false` plus
`ignoredBuiltDependencies`. Repo convention is `allowBuilds: esbuild: true`.
Replace the placeholder and drop the `ignoredBuiltDependencies` entry,
then re-run.
`allowBuilds: esbuild: set this to true or false`. Current pnpm writes
only the `allowBuilds` placeholder; any legacy key still present
(`ignoredBuiltDependencies`, `onlyBuiltDependencies`,
`neverBuiltDependencies`) is ignored since pnpm 11. Repo convention is
`allowBuilds: esbuild: true`. Replace the placeholder and drop the
legacy entry, then re-run.
- `plugins/apps/composable-test-suite` once had its own
`pnpm-workspace.yaml` and acted as a nested workspace root. That state is
gone on purpose: pnpm picks the nearest `pnpm-workspace.yaml` walking up,

View File

@ -14,6 +14,9 @@
- **Never pipe test output directly to filters** (`| head`, `| tail`, `| grep`, etc.).
Always redirect to a file first: `command > /tmp/output.txt 2>&1`, then read/grep the file.
This prevents hiding test failures. See `mem:testing` for details.
- **`.claude/skills` is a symlink to `.agents/skills`.**
Edit skills only in their canonical location (`.agents/skills`); never edit
through `.claude/skills`.
- **Read the workflow memory BEFORE the corresponding action**:
- Before `git commit``mem:workflow/creating-commits` (commit format, AI-assisted-by trailer)
- Before `gh issue create``mem:workflow/creating-issues` (title derivation, body template, Issue Type)

View File

@ -156,6 +156,21 @@
- Fix incorrect permission handling when managing share links on a file [#11289](https://github.com/penpot/penpot/issues/11289) (PR: [#11290](https://github.com/penpot/penpot/pull/11290))
- Fix backend session remaining valid after logout when the auth-token cookie is replayed [#11316](https://github.com/penpot/penpot/issues/11316) (PR: [#11317](https://github.com/penpot/penpot/pull/11317))
- Fix get-team-invitation-token requiring only read permissions [#11358](https://github.com/penpot/penpot/issues/11358) (PR: [#11359](https://github.com/penpot/penpot/pull/11359))
- Fix missing text in legacy SVG board thumbnails [#10182](https://github.com/penpot/penpot/issues/10182) (PR: [#11552](https://github.com/penpot/penpot/pull/11552))
- Fix workspace crash when applying transform modifiers in the WASM renderer [#10894](https://github.com/penpot/penpot/issues/10894) (PR: [#10896](https://github.com/penpot/penpot/pull/10896))
- Limit ZIP entry count and object size on V3 binfile import [#11021](https://github.com/penpot/penpot/issues/11021) (PR: [#11022](https://github.com/penpot/penpot/pull/11022))
- Block plugin UI iframe URLs targeting the Penpot domain [#11271](https://github.com/penpot/penpot/issues/11271) (PR: [#11273](https://github.com/penpot/penpot/pull/11273))
- Restrict the MCP REPL code execution endpoint to development environments [#11283](https://github.com/penpot/penpot/issues/11283) (PR: [#11282](https://github.com/penpot/penpot/pull/11282))
- Filter share-link tokens from the get-view-only-bundle response [#11285](https://github.com/penpot/penpot/issues/11285) (PR: [#11286](https://github.com/penpot/penpot/pull/11286))
- Disable MCP developer tools in multi-user mode [#11291](https://github.com/penpot/penpot/issues/11291) (PR: [#11310](https://github.com/penpot/penpot/pull/11310))
- Fix Hide comments setting being ignored after opening the Comments section [#11308](https://github.com/penpot/penpot/issues/11308) (PR: [#11492](https://github.com/penpot/penpot/pull/11492))
- Block NAT64/6to4/Teredo IPv6 transition addresses in the SSRF guard [#11319](https://github.com/penpot/penpot/issues/11319) (PR: [#11320](https://github.com/penpot/penpot/pull/11320))
- Prevent team admins from removing the team owner [#11367](https://github.com/penpot/penpot/issues/11367) (PR: [#11368](https://github.com/penpot/penpot/pull/11368))
- Enforce share-link comment permissions and page scope [#11370](https://github.com/penpot/penpot/issues/11370) (PR: [#11371](https://github.com/penpot/penpot/pull/11371))
- Clean up orphaned teams, projects and files on profile deletion [#11394](https://github.com/penpot/penpot/issues/11394) (PR: [#11395](https://github.com/penpot/penpot/pull/11395))
- Fix crash when pressing Ctrl+D with no shape selected [#11448](https://github.com/penpot/penpot/issues/11448) (PR: [#11491](https://github.com/penpot/penpot/pull/11491))
- Fix text layout not updating when auto-width is set by double-clicking the bounding box [#11480](https://github.com/penpot/penpot/issues/11480) (PR: [#11541](https://github.com/penpot/penpot/pull/11541))
- Fix boolean shapes rendering deformed in the WASM renderer and exports [#11482](https://github.com/penpot/penpot/issues/11482) (PR: [#11551](https://github.com/penpot/penpot/pull/11551))
### :sparkles: New features & Enhancements

View File

@ -56,6 +56,7 @@ If your organization is scaling and needs extra support, were here to help. [
- [Why Penpot](#why-penpot)
- [Getting Started](#getting-started)
- [Penpot Enterprise](#penpot-enterprise)
- [Community](#community)
- [Contributing](#contributing)
- [Resources](#resources)
@ -93,6 +94,12 @@ Penpot is the only design & prototype platform that is deployment agnostic. You
Learn how to install it with Docker, Kubernetes, Elestio or other options on [our website](https://penpot.app/self-host).
<img width="100%" height="1010" alt="2" src="https://github.com/user-attachments/assets/243e796e-a140-481a-b68f-b24be6a70e37" />
## Penpot Enterprise ##
Penpot Enterprise is our paid plan for organizations that need to scale their design work across multiple teams with advanced governance, security, and administration. Manage teams and access from a centralized **Admin Console**, configure advanced permissions, and connect your **identity provider through SSO**. Available for cloud and self-hosted environments, it combines enterprise controls with Penpots open-source foundation and open standards.
## Community ##
We love the Open Source software community. Contributing is our passion and if its yours too, participate and [improve](https://community.penpot.app/c/help-us-improve-penpot/7) Penpot. All your designs, code and ideas are welcome!

View File

@ -31,7 +31,7 @@ export PENPOT_MEDIA_PROCESSING_SERVICE_URI=http://localhost:6065
export PENPOT_FLAGS="\
$PENPOT_FLAGS \
enable-login-with-password \
disable-login-with-ldap \
enable-login-with-ldap \
disable-login-with-oidc \
disable-login-with-google \
disable-login-with-github \

View File

@ -10,7 +10,7 @@
[app.common.logging :as l]
[app.common.schema :as sm]
[clj-ldap.client :as ldap]
[clojure.string]
[cuerdas.core :as str]
[integrant.core :as ig]))
(defn- prepare-params
@ -36,11 +36,22 @@
:cause cause))))
(defn- replace-several [s & {:as replacements}]
(reduce-kv clojure.string/replace s replacements))
(reduce-kv str/replace s replacements))
(defn- escape-ldap-filter-value
"Escapes special characters in a string for use in LDAP filter values,
per RFC 4515 section 3."
[s]
(-> s
(str/replace "\\" "\\5c")
(str/replace "*" "\\2a")
(str/replace "(" "\\28")
(str/replace ")" "\\29")
(str/replace "\u0000" "\\00")))
(defn- search-user
[{:keys [::conn base-dn] :as cfg} email]
(let [query (replace-several (:query cfg) ":username" email)
(let [query (replace-several (:query cfg) ":username" (escape-ldap-filter-value email))
attrs [(:attrs-username cfg)
(:attrs-email cfg)
(:attrs-fullname cfg)]
@ -49,12 +60,19 @@
:attributes attrs}]
(first (ldap/search conn base-dn params))))
(defn- get-attr
"Retrieves an attribute from an LDAP entry. Handles multi-valued
attributes by returning the first value."
[entry attr-key]
(let [v (get entry attr-key)]
(if (coll? v) (first v) v)))
(defn- retrieve-user
[{:keys [::conn] :as cfg} {:keys [email password]}]
(when-let [{:keys [dn] :as user} (search-user cfg email)]
(when (ldap/bind? conn dn password)
{:fullname (get user (-> cfg :attrs-fullname keyword))
:email email
{:fullname (get-attr user (-> cfg :attrs-fullname keyword))
:email (get-attr user (-> cfg :attrs-email keyword))
:backend "ldap"})))
(def ^:private schema:info-data
@ -79,7 +97,7 @@
(l/warn :hint "invalid response from ldap, looks like ldap is not configured correctly" :data user)
(ex/raise :type :restriction
:code :wrong-ldap-response
:explain explain)))
::sm/explain explain)))
user)))
(defn- try-connectivity

View File

@ -15,6 +15,7 @@
(:require
[app.common.schema :as sm]
[app.util.ssrf :as ssrf]
[app.worker :as-alias wrk]
[cuerdas.core :as str]
[integrant.core :as ig]
[java-http-clj.core :as http])
@ -23,6 +24,8 @@
java.net.URI))
(def default-max-redirects 5)
(def default-connect-timeout 30000)
(def default-request-timeout 30000)
(defn client?
[o]
@ -33,15 +36,17 @@
:pred client?})
(defmethod ig/init-key ::client
[_ _]
(http/build-client {:connect-timeout 30000
[_ {:keys [::wrk/executor]}]
(http/build-client {:connect-timeout default-connect-timeout
:executor executor
:follow-redirects :never}))
(defn send!
([client req] (send! client req {}))
([client req {:keys [response-type] :or {response-type :string}}]
(assert (client? client) "expected valid http client")
(http/send req {:client client :as response-type})))
(http/send (merge {:timeout default-request-timeout} req)
{:client client :as response-type})))
(defn- resolve-client
[params]

View File

@ -60,7 +60,13 @@
(defmethod handle-error :restriction
[err request _]
(let [{:keys [code] :as data} (ex-data err)]
(let [data (ex-data err)
code (get data :code)
explain (ex/explain data)
data (-> data
(dissoc ::sm/explain)
(cond-> explain (assoc :explain explain)))]
(if (= code :method-not-allowed)
{::yres/status 405
::yres/body data}

View File

@ -205,7 +205,7 @@
::sto/storage (ig/ref ::sto/storage)}
::http.client/client
{}
{::wrk/executor (ig/ref ::wrk/executor)}
::session/manager
{::db/pool (ig/ref ::db/pool)}

View File

@ -75,10 +75,10 @@
{:method method
:uri uri
:body body
:headers headers}
:headers headers
:timeout timeout}
{:response-type :input-stream
:skip-ssrf-check? true
:timeout timeout})
:skip-ssrf-check? true})
status (:status resp)]
(when (not (<= 200 status 299))
(let [body (:body resp)]

View File

@ -390,18 +390,26 @@
(def ^:private sql:file-comment-users
"WITH available_profiles AS (
SELECT DISTINCT owner_id AS id
FROM comment
WHERE thread_id IN (SELECT id FROM comment_thread WHERE file_id=?)
SELECT DISTINCT c.owner_id AS id
FROM comment c
JOIN comment_thread ct
ON ct.id = c.thread_id
WHERE ct.file_id = ?::uuid
),
profile_ids AS (
SELECT id FROM available_profiles
UNION
SELECT ?::uuid
)
SELECT p.id,
p.email,
p.fullname AS name,
p.fullname AS fullname,
p.fullname,
p.photo_id,
p.is_active
FROM profile AS p
WHERE p.id IN (SELECT id FROM available_profiles) OR p.id=?")
FROM profile p
JOIN profile_ids AS x
ON x.id = p.id;")
(defn get-file-comments-users
[conn file-id profile-id]

View File

@ -41,6 +41,7 @@
[app.rpc.notifications :as notifications]
[app.storage :as sto]
[app.util.services :as sv]
[app.util.ssrf :as ssrf]
[app.worker :as wrk]
[cuerdas.core :as str]))
@ -960,13 +961,18 @@ RETURNING id, deleted_at;")
(sv/defmethod ::check-organization-sso
"Validate an organization SSO configuration by generating a login redirect URL.
Nitrate calls this while configuring SSO to verify client credentials and OIDC
discovery before saving the settings."
discovery before saving the settings. The issuer URL is nitrate-supplied
(customer-configured), so it is checked against the SSRF blocklist before
any outbound request is attempted."
{::doc/added "2.18"
::sm/params cto/schema:nitrate-sso
::sm/result schema:check-organization-sso-result
::rpc/auth false}
[cfg params]
{:valid (oidc/is-organization-sso-config-valid? cfg params)})
(let [issuer (oidc/organization-sso-discovery-uri params)]
{:valid (boolean (and issuer
(ssrf/safe-url? issuer)
(oidc/is-organization-sso-config-valid? cfg params)))}))
;; ---- API: notify-organization-sso-change
(sv/defmethod ::notify-organization-sso-change

View File

@ -0,0 +1,76 @@
;; 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 SUBSIDIARY SL
(ns backend-tests.auth-ldap-test
(:require
[app.auth.ldap :as ldap-auth]
[clj-ldap.client :as ldap]
[clojure.test :as t]))
;; --- search-user: filter must be escaped (RED: currently not escaped)
(t/deftest search-user-escapes-email-in-filter
(t/testing "wildcard * is escaped before building LDAP filter"
(let [captured-query (atom nil)
fake-search (fn [_conn _base-dn params]
(reset! captured-query (:filter params))
[])]
(with-redefs [ldap/search fake-search]
(#'ldap-auth/search-user {:query "(mail=:username)" :sizelimit 1
:attrs-username "uid" :attrs-email "mail"
:attrs-fullname "cn"}
"fry*@planetexpress.com"))
;; After fix: * should be escaped as \2a
(t/is (= "(mail=fry\\2a@planetexpress.com)" @captured-query)
"filter must have * escaped per RFC 4515"))))
;; --- retrieve-user: email must come from directory, not client (RED)
(t/deftest retrieve-user-uses-directory-email
(t/testing "returned email is from LDAP directory, not client input"
(let [fake-search (fn [_conn _base-dn _params]
[{:dn "cn=fry,ou=people,dc=planetexpress,dc=com"
:mail "fry@planetexpress.com"
:cn "Philip J. Fry"
:uid "fry"}])
fake-bind? (fn [_conn _dn _password] true)]
(with-redefs [ldap/search fake-search
ldap/bind? fake-bind?]
(let [cfg {:query "(mail=:username)" :sizelimit 1
:attrs-username "uid" :attrs-email "mail"
:attrs-fullname "cn"}
result (#'ldap-auth/retrieve-user cfg {:email "fry*@planetexpress.com" :password "fry"})]
;; After fix: email should be from directory (fry@planetexpress.com)
;; BUG: email is client input (fry*@planetexpress.com)
(t/is (= "fry@planetexpress.com" (:email result))
"email must come from LDAP directory attribute, not client input"))))))
;; --- authenticate: full flow with directory email (RED)
(t/deftest authenticate-returns-directory-email
(t/testing "authenticate returns directory email for profile"
(let [fake-search (fn [_conn _base-dn _params]
[{:dn "cn=amy,ou=people,dc=planetexpress,dc=com"
:mail "amy@planetexpress.com"
:cn "Amy Wong"
:uid "amy"}])
fake-bind? (fn [_conn _dn _password] true)]
(with-redefs [ldap/search fake-search
ldap/bind? fake-bind?
ldap/connect (fn [_cfg] (reify java.lang.AutoCloseable (close [_] nil)))]
(let [cfg {:query "(mail=:username)" :sizelimit 1
:attrs-username "uid" :attrs-email "mail"
:attrs-fullname "cn"
:bind-dn "cn=admin,dc=planetexpress,dc=com"
:bind-password "GoodNewsEveryone"
:host "localhost" :port 10389
:ssl false :tls false
:base-dn "ou=people,dc=planetexpress,dc=com"}
result (ldap-auth/authenticate cfg {:email "*@planetexpress.com" :password "amy"})]
;; After fix: email should be amy@planetexpress.com (directory)
;; BUG: email is *@planetexpress.com (client)
(t/is (= "amy@planetexpress.com" (:email result))
"authenticate must return directory email, not client-supplied wildcard"))))))

View File

@ -0,0 +1,30 @@
;; 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 SUBSIDIARY SL
(ns backend-tests.http-client-test
(:require
[app.http.client :as http]
[clojure.test :as t]
[java-http-clj.core :as jhttp]
[mockery.core :refer [with-mocks]]))
(t/deftest send-injects-default-timeout-when-absent
(with-mocks [mock {:target 'java-http-clj.core/send
:return {:status 200 :body ""}}]
(let [client (jhttp/build-client {})]
(http/send! client {:method :get :uri "https://example.com/"})
(let [[req _opts] (:call-args @mock)]
(t/is (= http/default-request-timeout (:timeout req)))))))
(t/deftest send-preserves-caller-supplied-timeout
(with-mocks [mock {:target 'java-http-clj.core/send
:return {:status 200 :body ""}}]
(let [client (jhttp/build-client {})]
(http/send! client {:method :get
:uri "https://example.com/"
:timeout 5000})
(let [[req _opts] (:call-args @mock)]
(t/is (= 5000 (:timeout req)))))))

View File

@ -8,6 +8,7 @@
(:require
[app.common.exceptions :as ex]
[app.config :as cf]
[app.http.client :as http]
[app.media.remote :as media.remote]
[app.setup :as-alias setup]
[app.util.json :as json]
@ -500,6 +501,22 @@
:headers {}})]
(t/is (= 200 (:status resp))))))))
(t/deftest service-request-puts-configured-timeout-in-request
(t/testing "service-request puts media-processing-service-timeout on the http request"
(let [captured (atom nil)]
(with-redefs [cf/get (th/config-get-mock config-mock)
http/req (fn [_client request _opts]
(reset! captured request)
{:status 200
:body (json-stream {:width 100 :height 100})})]
(media.remote/service-request
(mk-system)
{:method :post
:uri "http://localhost:6065/api/image/info"
:body nil
:headers {}})
(t/is (= 5000 (:timeout @captured)))))))
;; ---------------------------------------------------------------------------
;; Shared key
;; ---------------------------------------------------------------------------

View File

@ -17,6 +17,7 @@
[app.msgbus :as mbus]
[app.nitrate :as nitrate]
[app.rpc :as-alias rpc]
[app.util.ssrf :as ssrf]
[app.worker :as wrk]
[backend-tests.helpers :as th]
[clojure.set :as set]
@ -1806,13 +1807,14 @@
(t/deftest check-organization-sso-returns-valid-true
(let [organization-id (uuid/random)
out (with-redefs [oidc/is-organization-sso-config-valid? (constantly true)]
(th/management-command!
{::th/type :check-organization-sso
:organization-id organization-id
:client-id "test-client"
:client-secret "test-secret"
:issuer "https://idp.example.com"}))]
out (with-redefs [ssrf/safe-url? (constantly true)
oidc/is-organization-sso-config-valid? (constantly true)]
(th/management-command!
{::th/type :check-organization-sso
:organization-id organization-id
:client-id "test-client"
:client-secret "test-secret"
:issuer "https://idp.example.com"}))]
(t/is (th/success? out))
(t/is (true? (-> out :result :valid)))))
@ -1827,19 +1829,36 @@
(t/deftest check-organization-sso-passes-issuer-to-validation
(let [organization-id (uuid/random)
out (with-redefs [oidc/is-organization-sso-config-valid?
(fn [_cfg sso]
(and (= "test-client" (:client-id sso))
(= "https://idp.example.com/" (:issuer sso))))]
(th/management-command!
{::th/type :check-organization-sso
:organization-id organization-id
:client-id "test-client"
:client-secret "test-secret"
:issuer "https://idp.example.com/"}))]
out (with-redefs [ssrf/safe-url? (constantly true)
oidc/is-organization-sso-config-valid?
(fn [_cfg sso]
(and (= "test-client" (:client-id sso))
(= "https://idp.example.com/" (:issuer sso))))]
(th/management-command!
{::th/type :check-organization-sso
:organization-id organization-id
:client-id "test-client"
:client-secret "test-secret"
:issuer "https://idp.example.com/"}))]
(t/is (th/success? out))
(t/is (true? (-> out :result :valid)))))
(t/deftest check-organization-sso-returns-valid-false-on-ssrf-blocked-issuer
(t/testing "an SSRF-blocked issuer must not reach the OIDC validation flow"
(let [called? (atom false)
out (with-redefs [oidc/is-organization-sso-config-valid?
(fn [_cfg _sso] (reset! called? true) true)]
(th/management-command!
{::th/type :check-organization-sso
:organization-id (uuid/random)
:client-id "test-client"
:client-secret "test-secret"
:issuer "http://127.0.0.1/idp"}))]
(t/is (th/success? out))
(t/is (false? (-> out :result :valid)))
(t/is (false? @called?)
"OIDC validation should not run when the issuer is SSRF-blocked"))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUSH AUDIT EVENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -0,0 +1,106 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { rpcPost, extractCookie } from "./helpers/client.mjs";
async function loginWithLdap(email, password) {
const res = await rpcPost("login-with-ldap", { email, password });
if (res.status !== 200 || res.body.type) {
throw new Error(
`LDAP login failed: ${JSON.stringify(res.body)}`
);
}
const cookie = extractCookie(res.setCookie);
return { profile: res.body, cookie };
}
describe("LDAP injection — T5-N1-03", () => {
it("normal LDAP login works with valid credentials", async () => {
const { profile, cookie } = await loginWithLdap(
"fry@planetexpress.com",
"fry"
);
assert.equal(profile.email, "fry@planetexpress.com");
assert.ok(profile.id, "profile should have id");
assert.ok(cookie, "cookie should be set");
});
it("wildcard injection: *@planetexpress.com must not return client literal as email", async () => {
// ATTACK SCENARIO (from Criptored audit):
// 1. Attacker (amy) sends email="*@planetexpress.com" with her own password
// 2. LDAP filter becomes (mail=*@planetexpress.com) — * is a wildcard
// 3. With sizelimit=1, LDAP returns amy's entry (first match)
// 4. Bind succeeds: amy's DN + amy's password = valid
//
// EXPECTED BEHAVIOR AFTER FIX (two valid outcomes):
// A) If * is escaped: LDAP finds no match → wrong-credentials (injection blocked)
// B) If * matches: profile email must be "amy@planetexpress.com" (directory), not "*@planetexpress.com" (client)
//
// Either outcome is correct — the vulnerability is fixed.
try {
const { profile } = await loginWithLdap("*@planetexpress.com", "amy");
// Outcome B: login succeeded, verify email is from directory
assert.equal(
profile.email,
"amy@planetexpress.com",
"email must come from LDAP directory, not client input"
);
} catch (e) {
// Outcome A: injection blocked — * is escaped, no LDAP match
assert.ok(
e.message.includes("wrong-credentials"),
"wildcard should be rejected or return directory email"
);
}
});
it("identity swap: alternate email must return primary directory email", async () => {
// Professor has two emails in LDAP: professor@ and hubert@.
// Login with hubert@ — the profile email should be the one
// the LDAP directory returns as attrs-email, not what the client typed.
//
// EXPECTED BEHAVIOR AFTER FIX:
// Profile email should be "professor@planetexpress.com" (primary directory email),
// NOT "hubert@planetexpress.com" (client literal).
//
// CURRENT BUG: email is "hubert@planetexpress.com" (client literal) — test FAILS
const { profile, cookie } = await loginWithLdap(
"hubert@planetexpress.com",
"professor"
);
assert.ok(profile.id, "profile should have id");
assert.ok(cookie, "cookie should be set");
// This assertion FAILS with current code (RED) — proves the vulnerability
assert.equal(
profile.email,
"professor@planetexpress.com",
"email must come from LDAP directory, not client input"
);
});
it("wrong password fails", async () => {
try {
await loginWithLdap("fry@planetexpress.com", "wrong-password");
assert.fail("should have thrown");
} catch (e) {
assert.ok(
e.message.includes("LDAP login failed") ||
e.message.includes("wrong-credentials"),
"should fail with wrong credentials"
);
}
});
it("non-existent user fails", async () => {
try {
await loginWithLdap("nobody@planetexpress.com", "password");
assert.fail("should have thrown");
} catch (e) {
assert.ok(
e.message.includes("LDAP login failed") ||
e.message.includes("wrong-credentials"),
"should fail for non-existent user"
);
}
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -291,8 +291,6 @@
--modal-link-foreground-color: var(--color-accent-primary);
--modal-border-color: var(--color-background-quaternary);
--modal-separator-background-color: var(--color-background-quaternary);
--modal-navigator-foreground-color-rest: var(--color-background-quaternary);
--modal-navigator-foreground-color-active: var(--color-accent-primary);
// ALERTS, NOTIFICATION, TOAST & BADGES

View File

@ -36,6 +36,7 @@
[app.main.ui.releases.v2-15]
[app.main.ui.releases.v2-16]
[app.main.ui.releases.v2-17]
[app.main.ui.releases.v2-18]
[app.main.ui.releases.v2-2]
[app.main.ui.releases.v2-3]
[app.main.ui.releases.v2-4]
@ -107,4 +108,4 @@
(defmethod rc/render-release-notes "0.0"
[params]
(rc/render-release-notes (assoc params :version "2.17")))
(rc/render-release-notes (assoc params :version "2.18")))

View File

@ -0,0 +1,116 @@
// 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 SUBSIDIARY SL
@use "ds/_utils.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
@use "ds/typography.scss" as *;
.modal-overlay {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset: 0;
z-index: var(--z-index-set);
background-color: var(--color-overlay-default);
}
.modal-container {
display: grid;
grid-template-columns: px2rem(324) 1fr;
block-size: $sz-500;
inline-size: px2rem(888);
border-radius: $br-8;
background-color: var(--color-background-primary);
border: $b-2 solid var(--color-background-quaternary);
}
.start-image {
inline-size: px2rem(324);
border-radius: $br-8 0 0 $br-8;
}
.modal-content {
padding: $sz-40;
display: grid;
grid-template-rows: auto 1fr $sz-32;
gap: var(--sp-xxl);
}
.modal-header {
display: grid;
gap: var(--sp-s);
}
.link {
color: var(--color-accent-primary);
}
.version-tag {
@include use-typography("headline-small");
display: flex;
justify-content: center;
align-items: center;
block-size: $sz-32;
inline-size: $sz-96;
background-color: var(--color-foreground-primary);
color: var(--color-background-tertiary);
border-radius: $br-8;
}
.modal-title {
@include use-typography("headline-large");
color: var(--color-foreground-primary);
}
.features-block {
display: flex;
flex-direction: column;
gap: var(--sp-l);
inline-size: px2rem(440);
}
.feature {
display: flex;
flex-direction: column;
gap: var(--sp-s);
}
.feature-title {
@include use-typography("body-large");
color: var(--color-foreground-primary);
}
.feature-content {
@include use-typography("body-medium");
margin: 0;
color: var(--color-foreground-secondary);
}
.feature-list {
@include use-typography("body-medium");
color: var(--color-foreground-secondary);
list-style: disc;
display: grid;
gap: var(--sp-s);
}
.navigation {
inline-size: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
justify-self: flex-end;
grid-area: button;
}

View File

@ -4,28 +4,29 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
.step-dots {
display: grid;
grid-template-columns: none;
grid-auto-flow: column;
gap: deprecated.$s-8;
height: fit-content;
width: fit-content;
gap: var(--sp-s);
block-size: fit-content;
inline-size: fit-content;
margin: 0;
padding: 0;
place-self: center flex-start;
}
.dot {
height: deprecated.$s-12;
width: deprecated.$s-12;
border-radius: deprecated.$br-circle;
background-color: var(--modal-navigator-foreground-color-rest);
cursor: pointer;
block-size: $sz-12;
inline-size: $sz-12;
border-radius: $br-circle;
background-color: var(--color-background-quaternary);
}
.current {
background-color: var(--modal-navigator-foreground-color-active);
background-color: var(--color-accent-primary);
}

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -57,8 +58,10 @@
" Ready to dive in? Let 's get started!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -97,8 +100,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -131,8 +136,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
@ -166,8 +173,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
3
[:div {:class (stl/css-case :modal-overlay true)}
@ -198,6 +207,7 @@
:navigate navigate
:total 4}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,101 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -43,6 +44,7 @@
" Ready to dive in? Let 's get started!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click finish} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,80 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -46,8 +47,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -79,8 +82,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -107,7 +112,8 @@
"And one more thing…Tune into our product showcase to see future plans and help us shape Penpot. Come for the insights, stay for the community…"]
[:p {:class (stl/css :feature-content)}
[:a {:href "https://penpot.app/penpotfest"
[:a {:class (stl/css :link)
:href "https://penpot.app/penpotfest"
:target "_blank"}
"Get your tickets"]
" now to join us 8-10 October, in Madrid!"]]
@ -118,8 +124,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
@ -148,8 +156,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
3
[:div {:class (stl/css-case :modal-overlay true)}
@ -179,6 +189,7 @@
:navigate navigate
:total 4}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -49,8 +50,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -79,8 +82,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -115,8 +120,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
@ -153,8 +160,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
3
[:div {:class (stl/css-case :modal-overlay true)}
@ -184,6 +193,7 @@
:navigate navigate
:total 4}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -46,8 +47,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -85,8 +88,10 @@
:navigate navigate
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -121,8 +126,10 @@
:navigate navigate
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
@ -157,6 +164,7 @@
:navigate navigate
:total 3}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -46,8 +47,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -79,8 +82,10 @@
:navigate navigate
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -113,6 +118,7 @@
:navigate navigate
:total 2}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -46,8 +47,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -79,8 +82,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -109,8 +114,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
@ -139,8 +146,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
3
[:div {:class (stl/css-case :modal-overlay true)}
@ -173,6 +182,7 @@
:navigate navigate
:total 4}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -46,8 +47,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -79,8 +82,10 @@
:navigate navigate
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -120,8 +125,10 @@
:navigate navigate
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
@ -154,6 +161,7 @@
:navigate navigate
:total 3}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -44,8 +45,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -74,8 +77,10 @@
:navigate navigate
:total 5}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -104,8 +109,10 @@
:navigate navigate
:total 5}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
@ -134,8 +141,10 @@
:navigate navigate
:total 5}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
3
[:div {:class (stl/css-case :modal-overlay true)}
@ -177,8 +186,10 @@
:navigate navigate
:total 5}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
4
[:div {:class (stl/css-case :modal-overlay true)}
@ -207,6 +218,7 @@
:navigate navigate
:total 5}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -44,8 +45,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -74,8 +77,10 @@
:navigate navigate
:total 5}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -104,8 +109,10 @@
:navigate navigate
:total 5}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
@ -134,8 +141,10 @@
:navigate navigate
:total 5}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
3
[:div {:class (stl/css-case :modal-overlay true)}
@ -171,8 +180,10 @@
:navigate navigate
:total 5}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
4
[:div {:class (stl/css-case :modal-overlay true)}
@ -213,5 +224,7 @@
:navigate navigate
:total 5}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -0,0 +1,252 @@
;; 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 SUBSIDIARY SL
(ns app.main.ui.releases.v2-18
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
(defmethod c/render-release-notes "2.18"
[{:keys [slide klass next finish navigate version]}]
(mf/html
(case slide
:start
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.18-slide-0.jpg"
:class (stl/css :start-image)
:border "0"
:alt "Penpot 2.18 is here!"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Whats new in Penpot?"]
[:div {:class (stl/css :version-tag)}
(dm/str "Version " version)]]
[:div {:class (stl/css :features-block)}
[:p {:class (stl/css :feature-content)}
"Say hello to Penpot Enterprise, our new paid plan, shipping alongside the most requested stroke-to-path functionality and a fresh batch of features and fixes."]
[:p {:class (stl/css :feature-content)}
"Penpot stays free, unlimited, and open source. Enterprise is meant for organisations that need to govern over people, workflows or content."]
[:p {:class (stl/css :feature-content)}
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.18-enterprise.jpg"
:class (stl/css :start-image)
:border "0"
:alt "Introducing Penpot Enterprise"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Introducing Penpot Enterprise"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"As an open source product, Penpot brings the most advanced features to everyone for free."]
[:p {:class (stl/css :feature-content)}
"Penpot Enterprise is a paid layer that overrides, tweaks or restricts how these features can be used at scale."]]
[:div {:class (stl/css :navigation)}
[:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 6}]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.18-sso.jpg"
:class (stl/css :start-image)
:border "0"
:alt "One Sign in for everyone"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"One Sign in for everyone"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"Let your whole organization sign in through your own corporate identity provider, whether that's a generic OpenID Connect provider, Azure Active Directory, or Google."]
[:p {:class (stl/css :feature-content)}
"One consistent way into your organizations teams and files, governed by the directory you already run. From the Admin Console, youll be able to configure your identity provider setup to Penpot in just a few steps."]]
[:div {:class (stl/css :navigation)}
[:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 6}]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.18-permissions.jpg"
:class (stl/css :start-image)
:border "0"
:alt "Advanced permissions"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Advanced permissions"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"Decide who can do what across every team at once, who can create, edit, administer teams, projects, and files, and who's allowed to invite new people in."]
[:p {:class (stl/css :feature-content)}
"Your rules sit on top of everyone's normal role, so the whole organization stays aligned with how you want to work, and every change takes effect the moment you make it."]]
[:div {:class (stl/css :navigation)}
[:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 6}]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
3
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.18-console.jpg"
:class (stl/css :start-image)
:border "0"
:alt "An Admin Panel to rule them all"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"An Admin Panel to rule them all"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"A separate Admin Panel allows authorized users to create independent Organisations to which certain rules can be applied."]
[:p {:class (stl/css :feature-content)}
"We are starting with something straightforward; Per-Organisation SSO (choose between multiple identity providers) and advanced permissions (decide who can do what across every team)."]]
[:div {:class (stl/css :navigation)}
[:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 6}]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
4
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.18-billing.jpg"
:class (stl/css :start-image)
:border "0"
:alt "Penpot Enterprise billing"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Penpot Enterprise billing"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"This is equally important. Whether on a monthly or annual subscription, customers using Penpot Enterprise only pay once per identifiable user (email address), regardless of the number of organisations this user is a member of."]
[:p {:class (stl/css :feature-content)}
"Bulk adding or removing users is trivial and we are making sure that you have total transparency and ergonomics around your billing cycle and what it contains."]]
[:div {:class (stl/css :navigation)}
[:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 6}]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
5
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.18-stroke2path.gif"
:class (stl/css :start-image)
:border "0"
:alt "Stroke to Paths and new drawing tools"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Stroke to Paths and new drawing tools"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"Paths can now convert their stroke into a separate, editable path shape, letting you reshape or style the outline independently of the original path. Currently available under the new WebGL renderer."]
[:p {:class (stl/css :feature-content)}
"Lots of smaller wins add up this cycle. A few favorites:"]
[:p {:class (stl/css :feature-content)}
"• Draw faster with new shape and free-draw tool flyouts in the toolbar."]
[:p {:class (stl/css :feature-content)}
"• Dedicated Line and Arrow drawing tools (by @davidv399)."]
[:p {:class (stl/css :feature-content)}
"On top of that, 2.18 ships plugin API improvements, a broad round of bug fixes, and community-contributed fixes (thanks to @Krishcode264, @filipsajdak, @sawirricardo, and many more)."]]
[:div {:class (stl/css :navigation)}
[:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 6}]
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -0,0 +1,7 @@
// 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 SUBSIDIARY SL
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -46,6 +47,7 @@
"Thanks again to our awesome community for their amazing contributions to this release!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click finish} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,80 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-480;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -47,8 +48,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -77,8 +80,10 @@
:navigate navigate
:total 2}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
@ -98,7 +103,10 @@
[:p {:class (stl/css :feature-content)}
"Penpot plugins are quite easy to install."]
[:p {:class (stl/css :feature-content)}
"Be sure to keep an eye on our evolving " [:a {:href "https://penpot.app/penpothub" :target "_blank"} "Penpot Hub"] " to pick the ones that are best suited to enhance your workflow."]
"Be sure to keep an eye on our evolving "
[:a {:class (stl/css :link)
:href "https://penpot.app/penpothub"
:target "_blank"} "Penpot Hub"] " to pick the ones that are best suited to enhance your workflow."]
[:p {:class (stl/css :feature-content)}
"This is just the beginning of a myriad of possibilities. Lets build this community together ❤️."]]
@ -110,6 +118,7 @@
:navigate navigate
:total 2}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -47,8 +48,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -77,8 +80,10 @@
:navigate navigate
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -107,8 +112,10 @@
:navigate navigate
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
@ -136,6 +143,7 @@
:navigate navigate
:total 3}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -47,8 +48,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -77,8 +80,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -107,8 +112,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
@ -141,8 +148,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
3
[:div {:class (stl/css-case :modal-overlay true)}
@ -170,6 +179,7 @@
:navigate navigate
:total 4}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -48,8 +49,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -89,8 +92,10 @@
:navigate navigate
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -125,8 +130,10 @@
:navigate navigate
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
@ -164,6 +171,7 @@
:navigate navigate
:total 3}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -46,8 +47,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -76,8 +79,10 @@
:navigate navigate
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -106,8 +111,10 @@
:navigate navigate
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
@ -137,6 +144,7 @@
:navigate navigate
:total 3}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -46,8 +47,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -88,8 +91,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -121,8 +126,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
2
[:div {:class (stl/css-case :modal-overlay true)}
@ -154,8 +161,10 @@
:navigate navigate
:total 4}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
3
[:div {:class (stl/css-case :modal-overlay true)}
@ -191,6 +200,7 @@
:navigate navigate
:total 4}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
@ -46,8 +47,10 @@
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
@ -76,8 +79,10 @@
:navigate navigate
:total 2}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
[:> button* {:class (stl/css :next-btn)
:on-click next
:variant "primary"}
"Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
@ -107,6 +112,7 @@
:navigate navigate
:total 2}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))
[:> button* {:class (stl/css :next-btn)
:on-click finish
:variant "primary"}
"Let's go"]]]]]])))

View File

@ -4,105 +4,4 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: deprecated.$s-324 1fr;
height: deprecated.$s-500;
width: deprecated.$s-888;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
}
.start-image {
width: deprecated.$s-324;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
}
.modal-content {
padding: deprecated.$s-40;
display: grid;
grid-template-rows: auto 1fr deprecated.$s-32;
gap: deprecated.$s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: deprecated.$s-8;
}
.version-tag {
@include deprecated.flex-center;
@include deprecated.headline-small-typography;
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: deprecated.$br-8;
}
.modal-title {
@include deprecated.headline-large-typography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
width: deprecated.$s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
}
.feature-title {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include deprecated.body-medium-typography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include deprecated.body-medium-typography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: deprecated.$s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend %button-primary;
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
}
@use "base";

View File

@ -700,7 +700,9 @@
(defn use-shape
[id]
(when (initialized?)
;; Use `wasm/live?` (not `initialized?`) so context-restore reload can
;; select shapes while `reloading?` still blocks external app callers.
(when (wasm/live?)
(let [buffer (uuid/get-u32 id)]
(h/call wasm/internal-module "_use_shape"
(aget buffer 0)
@ -710,7 +712,7 @@
(defn has-shape
[id]
(when (initialized?)
(when (wasm/live?)
(let [buffer (uuid/get-u32 id)
result
@ -739,9 +741,11 @@
;; Cache content for text editor sync
(text-editor/cache-shape-text-content! shape-id content)
;; The WASM design state may not be ready (e.g. while switching renderer
;; with a text shape being edited). Skip the WASM layout calls in that case.
(when (initialized?)
;; Skip when the GL/WASM context is not usable. Use `wasm/live?` (not
;; `initialized?`) so context-restore reload can re-upload text while
;; `reloading?` still blocks external app callers. Geometry shapes already
;; use `live?` via `set-shape-base-props`; text must match that path.
(when (wasm/live?)
(h/call wasm/internal-module "_clear_shape_text")
(set-shape-vertical-align (get content :vertical-align))

View File

@ -1799,7 +1799,7 @@ msgstr "At least 1 uppercase letter"
#: src/app/main/ui/settings/password.cljs, src/app/main/ui/auth/register.cljs
msgid "errors.weak-password.insufficient-digits"
msgstr "At least 1 digit"
msgstr "At least 1 number"
#: src/app/main/ui/settings/password.cljs, src/app/main/ui/auth/register.cljs
msgid "errors.weak-password.insufficient-special"

View File

@ -1764,7 +1764,7 @@ msgstr "Al menos 1 letra mayúscula"
#: src/app/main/ui/settings/password.cljs, src/app/main/ui/auth/register.cljs
msgid "errors.weak-password.insufficient-digits"
msgstr "Al menos 1 dígito"
msgstr "Al menos 1 número"
#: src/app/main/ui/settings/password.cljs, src/app/main/ui/auth/register.cljs
msgid "errors.weak-password.insufficient-special"

View File

@ -2,5 +2,3 @@ storeDir: ../.pnpm-store
allowBuilds:
esbuild: true
onlyBuiltDependencies:
- esbuild