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

This commit is contained in:
Andrey Antukh 2026-06-08 14:36:24 +02:00
commit c7fae1f353
5 changed files with 63 additions and 842 deletions

140
AGENTS.md
View File

@ -1,93 +1,79 @@
# AI Agent Guide
# AI AGENT GUIDE
This document provides the core context and operating guidelines for AI agents
working in this repository.
## CRITICAL: Read module memories BEFORE writing any code
## Before You Start
Do this **before planning, before coding, before touching any file**:
Before responding to any user request, you must:
1. Read `critical-info` (use `serena_read_memory critical-info` or read `.serena/memories/critical-info.md`).
It describes the project structure and tells you which modules exist.
2. From `critical-info`, identify which modules your task affects.
3. Read each affected module's **core memory** — the name is `<module>/core`
(e.g. `frontend/core`, `backend/core`, `common/core`).
4. If the core memory references deeper `mem:` memories relevant to your task, read those too.
1. Read this file completely.
2. Identify which modules are affected by the task.
3. Load the `AGENTS.md` file **only** for each affected module (see the
architecture table below). Not all modules have an `AGENTS.md` — verify the
file exists before attempting to read it.
4. Do **not** load `AGENTS.md` files for unrelated modules.
**STOP: Do not proceed until you have read the core memory of every affected module.**
Skipping this step is the #1 cause of incorrect or incomplete work.
## Role: Senior Software Engineer
---
# Memory system
Memories are the **primary project guidance** — not docs or readme files.
They are dense, agent-oriented notes: terse bullets, invariants, no prose.
## Entry point
Start at `critical-info` (the graph root). It describes the project structure,
module dependency graph, and references section-level core memories.
## Progressive discovery model
Memories form a **reference graph**, not a flat list:
```
critical-info ← read first (graph root)
└─ <section>/core ← top-level memory per section (e.g. frontend/core, backend/core)
└─ <topic> ← focused memories (e.g. frontend/handling-errors-and-debugging)
└─ ... ← deeper memories as needed
```
When working on a task:
1. Read `critical-info` to identify which sections are affected.
2. Read the affected section's `core` memory for an overview.
3. Follow `mem:` references in the core memory to focused memories relevant to your task.
4. Continue following references deeper as needed.
## Accessing memories
- **If `serena_read_memory` / `serena_list_memories` tools are available**: use them.
`serena_read_memory` takes a memory name (e.g. `critical-info`, `frontend/core`).
- **If tools are NOT available**: read the filesystem directly.
Memory name `mem:foo/bar` maps to file `.serena/memories/foo/bar.md`.
## Cross-reference convention
Memories reference other memories with `mem:<section>/<name>` inside backticks.
Example: `mem:common/changes-architecture`.
When you encounter a `mem:` reference relevant to your task, read that memory next.
## Topic/folder organization
Memories are grouped into folders that mirror project modules or topics:
`backend/`, `common/`, `frontend/`, `render-wasm/`, `exporter/`, `workflow/`, etc.
Each folder's top-level memory is `<folder>/core`.
---
# Role: Senior Software Engineer
You are a high-autonomy Senior Full-Stack Software Engineer. You have full
permission to navigate the codebase, modify files, and execute commands to
fulfill your tasks. Your goal is to solve complex technical tasks with high
precision while maintaining a strong focus on maintainability and performance.
### Operational Guidelines
## Operational Guidelines
1. Before writing code, describe your plan. If the task is complex, break it
down into atomic steps.
2. Be concise and autonomous.
3. Do **not** touch unrelated modules unless the task explicitly requires it.
4. Commit only when explicitly asked. Follow the commit format rules in
`CONTRIBUTING.md`.
5. When searching code, prefer `ripgrep` (`rg`) over `grep` — it respects
`.gitignore` by default.
## GitHub Operations
To obtain the list of repository members/collaborators:
```bash
gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login'
```
To obtain the list of open PRs authored by members:
```bash
MEMBERS=$(gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login' | tr '\n' '|' | sed 's/|$//')
gh pr list --state open --limit 200 --json author,title,number | jq -r --arg members "$MEMBERS" '
($members | split("|")) as $m |
.[] | select(.author.login as $a | $m | index($a)) |
"\(.number)\t\(.author.login)\t\(.title)"
'
```
To obtain the list of open PRs from external contributors (non-members):
```bash
MEMBERS=$(gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login' | tr '\n' '|' | sed 's/|$//')
gh pr list --state open --limit 200 --json author,title,number | jq -r --arg members "$MEMBERS" '
($members | split("|")) as $m |
.[] | select(.author.login as $a | $m | index($a) | not) |
"\(.number)\t\(.author.login)\t\(.title)"
'
```
## Architecture Overview
Penpot is an open-source design tool composed of several modules:
| Directory | Language | Purpose | Has `AGENTS.md` |
|-----------|----------|---------|:----------------:|
| `frontend/` | ClojureScript + SCSS | Single-page React app (design editor) | Yes |
| `backend/` | Clojure (JVM) | HTTP/RPC server, PostgreSQL, Redis | Yes |
| `common/` | Cljc (shared Clojure/ClojureScript) | Data types, geometry, schemas, utilities | Yes |
| `render-wasm/` | Rust -> WebAssembly | High-performance canvas renderer (Skia) | Yes |
| `exporter/` | ClojureScript (Node.js) | Headless Playwright-based export (SVG/PDF) | No |
| `mcp/` | TypeScript | Model Context Protocol integration | No |
| `plugins/` | TypeScript | Plugin runtime and example plugins | No |
Some submodules use `pnpm` workspaces. The root `package.json` and
`pnpm-lock.yaml` manage shared dependencies. Helper scripts live in `scripts/`.
### Module Dependency Graph
```
frontend ──> common
backend ──> common
exporter ──> common
frontend ──> render-wasm (loads compiled WASM)
```
`common` is referenced as a local dependency (`{:local/root "../common"}`) by
both `frontend` and `backend`. Changes to `common` can therefore affect multiple
modules — test across consumers when modifying shared code.

View File

@ -1,262 +0,0 @@
# Penpot Backend Agent Instructions
Clojure backend (RPC) service running on the JVM.
Uses Integrant for dependency injection, PostgreSQL for storage, and
Redis for messaging/caching.
## General Guidelines
To ensure consistency across the Penpot JVM stack, all contributions must adhere
to these criteria.
IMPORTANT: all CLI commands should be executed under backend/
subdirectory for make them work correctly.
### 1. Testing & Validation
* **Coverage:** If code is added or modified in `src/`, corresponding
tests in `test/backend_tests/` must be added or updated.
* **Execution:**
* **Isolated:** Run `clojure -M:dev:test --focus backend-tests.my-ns-test` for the specific test namespace.
* **Regression:** Run `clojure -M:dev:test` to ensure the suite passes without regressions in related functional areas.
### 2. Code Quality & Formatting
* **Linting:** All code must pass linter checks (run `pnpm run lint:clj` or `pnpm run lint` on the repository root)
* **Formatting:** All the code must pass the formatting check (run `pnpm run
check-fmt`). Use `pnpm run fmt` to fix formatting issues. Avoid "dirty"
diffs caused by unrelated whitespace changes.
* **Type Hinting:** Use explicit JVM type hints (e.g., `^String`, `^long`) in
performance-critical paths to avoid reflection overhead.
## Code Conventions
### Namespace Overview
The source is located under `src` directory and this is a general overview of
namespaces structure:
- `app.rpc.commands.*` RPC command implementations (`auth`, `files`, `teams`, etc.)
- `app.http.*` HTTP routes and middleware
- `app.db.*` Database layer
- `app.tasks.*` Background job tasks
- `app.main` Integrant system setup and entrypoint
- `app.loggers` Internal loggers (auditlog, mattermost, etc.) (not to be confused with `app.common.logging`)
### RPC
The RPC methods are implemented using a multimethod-like structure via the
`app.util.services` namespace. The main RPC methods are collected under
`app.rpc.commands` namespace and exposed under `/api/rpc/command/<cmd-name>`.
The RPC method accepts POST and GET requests indistinctly and uses the `Accept`
header to negotiate the response encoding (which can be Transit — the default —
or plain JSON). It also accepts Transit (default) or JSON as input, which should
be indicated using the `Content-Type` header.
The main convention is: use `get-` prefix on RPC name when we want READ
operation.
Example of RPC method definition:
```clojure
(sv/defmethod ::my-command
{::rpc/auth true ;; requires auth
::doc/added "1.18"
::sm/params [:map ...] ;; malli input schema
::sm/result [:map ...]} ;; malli output schema
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
;; return a plain map or throw
{:id (uuid/next)})
```
Look under `src/app/rpc/commands/*.clj` to see more examples.
### Tests
Test namespaces match `.*-test$` under `test/`. Config is in `tests.edn`.
### Integrant System
The `src/app/main.clj` declares the system map. Each key is a component; values
are config maps with `::ig/ref` for dependencies. Components implement
`ig/init-key` / `ig/halt-key!`.
### Connecting to the Database
Two PostgreSQL databases are used in this environment:
| Database | Purpose | Connection string |
|---------------|--------------------|----------------------------------------------------|
| `penpot` | Development / app | `postgresql://penpot:penpot@postgres/penpot` |
| `penpot_test` | Test suite | `postgresql://penpot:penpot@postgres/penpot_test` |
**Interactive psql session:**
```bash
# development DB
psql "postgresql://penpot:penpot@postgres/penpot"
# test DB
psql "postgresql://penpot:penpot@postgres/penpot_test"
```
**One-shot query (non-interactive):**
```bash
psql "postgresql://penpot:penpot@postgres/penpot" -c "SELECT id, name FROM team LIMIT 5;"
```
**Useful psql meta-commands:**
```
\dt -- list all tables
\d <table> -- describe a table (columns, types, constraints)
\di -- list indexes
\q -- quit
```
> **Migrations table:** Applied migrations are tracked in the `migrations` table
> with columns `module`, `step`, and `created_at`. When renaming a migration
> logical name, update this table in both databases to match the new name;
> otherwise the runner will attempt to re-apply the migration on next startup.
```bash
# Example: fix a renamed migration entry in the test DB
psql "postgresql://penpot:penpot@postgres/penpot_test" \
-c "UPDATE migrations SET step = 'new-name' WHERE step = 'old-name';"
```
### Database Access (Clojure)
`app.db` wraps next.jdbc. Queries use a SQL builder that auto-converts kebab-case ↔ snake_case.
```clojure
;; Query helpers
(db/get cfg-or-pool :table {:id id}) ; fetch one row (throws if missing)
(db/get* cfg-or-pool :table {:id id}) ; fetch one row (returns nil)
(db/query cfg-or-pool :table {:team-id team-id}) ; fetch multiple rows
(db/insert! cfg-or-pool :table {:name "x" :team-id id}) ; insert
(db/update! cfg-or-pool :table {:name "y"} {:id id}) ; update
(db/delete! cfg-or-pool :table {:id id}) ; delete
;; Run multiple statements/queries on single connection
(db/run! cfg (fn [{:keys [::db/conn]}]
(db/insert! conn :table row1)
(db/insert! conn :table row2))
;; Transactions
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
(db/insert! conn :table row)))
```
Almost all methods in the `app.db` namespace accept `pool`, `conn`, or
`cfg` as params.
Migrations live in `src/app/migrations/` as numbered SQL files. They run automatically on startup.
### Error Handling
The exception helpers are defined on Common module, and are available under
`app.common.exceptions` namespace.
Example of raising an exception:
```clojure
(ex/raise :type :not-found
:code :object-not-found
:hint "File does not exist"
:file-id id)
```
Common types: `:not-found`, `:validation`, `:authorization`, `:conflict`, `:internal`.
### Performance Macros (`app.common.data.macros`)
Always prefer these macros over their `clojure.core` equivalents — they provide
optimized implementations:
```clojure
(dm/select-keys m [:a :b]) ;; faster than core/select-keys
(dm/get-in obj [:a :b :c]) ;; faster than core/get-in
(dm/str "a" "b" "c") ;; string concatenation
```
### Configuration
`src/app/config.clj` reads `PENPOT_*` environment variables, validated with
Malli. Access anywhere via `(cf/get :smtp-host)`. Feature flags: `(cf/flags
:enable-smtp)`.
### Background Tasks
Background tasks live in `src/app/tasks/`. Each task is an Integrant component
that exposes a `::handler` key and follows this three-method pattern:
```clojure
(defmethod ig/assert-key ::handler ;; validate config at startup
[_ params]
(assert (db/pool? (::db/pool params)) "expected a valid database pool"))
(defmethod ig/expand-key ::handler ;; inject defaults before init
[k v]
{k (assoc v ::my-option default-value)})
(defmethod ig/init-key ::handler ;; return the task fn
[_ cfg]
(fn [_task] ;; receives the task row from the worker
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
;; … do work …
))))
```
**Wiring a new task** requires two changes in `src/app/main.clj`:
1. **Handler config** add an entry in `system-config` with the dependencies:
```clojure
:app.tasks.my-task/handler
{::db/pool (ig/ref ::db/pool)}
```
2. **Registry + cron** register the handler name and schedule it:
```clojure
;; in ::wrk/registry ::wrk/tasks map:
:my-task (ig/ref :app.tasks.my-task/handler)
;; in worker-config ::wrk/cron ::wrk/entries vector:
{:cron #penpot/cron "0 0 0 * * ?" ;; daily at midnight
:task :my-task}
```
**Useful cron patterns** (Quartz format — six fields: s m h dom mon dow):
| Expression | Meaning |
|------------------------------|--------------------|
| `"0 0 0 * * ?"` | Daily at midnight |
| `"0 0 */6 * * ?"` | Every 6 hours |
| `"0 */5 * * * ?"` | Every 5 minutes |
**Time helpers** (`app.common.time`):
```clojure
(ct/now) ;; current instant
(ct/duration {:hours 1}) ;; java.time.Duration
(ct/minus (ct/now) some-duration) ;; subtract duration from instant
```
`db/interval` converts a `Duration` (or millis / string) to a PostgreSQL
interval object suitable for use in SQL queries:
```clojure
(db/interval (ct/duration {:hours 1})) ;; → PGInterval "3600.0 seconds"
```

View File

@ -1,70 +0,0 @@
# Penpot Common Agent Instructions
A shared module with code written in Clojure, ClojureScript, and
JavaScript. Contains multiplatform code that can be used and executed
from the frontend, backend, or exporter modules. It uses Clojure reader
conditionals to specify platform-specific implementations.
## General Guidelines
To ensure consistency across the Penpot stack, all contributions must adhere to
these criteria:
### 1. Testing & Validation
If code is added or modified in `src/`, corresponding tests in
`test/common_tests/` must be added or updated.
* **Environment:** Tests should run in both JS (Node.js) and JVM environments.
* **Location:** Place tests in the `test/common_tests/` directory, following the
namespace structure of the source code (e.g., `app.common.colors` ->
`common-tests.colors-test`).
* **Execution:** Tests should be executed on both JS (Node.js) and JVM environments:
* **Isolated:**
* JS: To run a focused ClojureScript unit test: edit the
`test/common_tests/runner.cljs` to narrow the test suite, then
`pnpm run test:js`.
* JVM: `pnpm run test:jvm --focus common-tests.my-ns-test`
* **Regression:**
* JS: Run `pnpm run test:js` without modifications on the runner (preferred)
* JVM: Run `pnpm run test:jvm`
### 2. Code Quality & Formatting
* **Linting:** All code changes must pass linter checks:
* Run `pnpm run lint:clj` for CLJ/CLJS/CLJC
* **Formatting:** All code changes must pass the formatting check
* Run `pnpm run check-fmt:clj` for CLJ/CLJS/CLJC
* Run `pnpm run check-fmt:js` for JS
* Use `pnpm run fmt` to fix all formatting issues (`pnpm run
fmt:clj` or `pnpm run fmt:js` for isolated formatting fix).
## Code Conventions
### Namespace Overview
The source is located under `src` directory and this is a general overview of
namespaces structure:
- `app.common.types.*` Shared data types for shapes, files, pages using Malli schemas
- `app.common.schema` Malli abstraction layer, exposes the most used functions from malli
- `app.common.geom.*` Geometry and shape transformation helpers
- `app.common.data` Generic helpers used across the entire application
- `app.common.math` Generic math helpers used across the entire application
- `app.common.json` Generic JSON encoding/decoding helpers
- `app.common.data.macros` Performance macros used everywhere
### Reader Conditionals
We use reader conditionals to differentiate implementations depending on the
target platform where the code runs:
```clojure
#?(:clj (import java.util.UUID)
:cljs (:require [cljs.core :as core]))
```
Both frontend and backend depend on `common` as a local library (`penpot/common
{:local/root "../common"}`).

View File

@ -1,371 +0,0 @@
# Penpot Frontend Agent Instructions
ClojureScript-based frontend application that uses React and RxJS as its main
architectural pieces.
## General Guidelines
### 1. Testing & Validation
#### Unit Tests
If code is added or modified in `src/`, corresponding tests in
`test/frontend_tests/` must be added or updated.
* **Environment:** Tests should run in a Node.js or browser-isolated
environment without requiring the full application state or a
running backend. Test are developed using cljs.test.
* **Mocks & Stubs:** * Use proper mocks for any side-effecting
functions (e.g., API calls, storage access).
* Avoid testing through the UI (DOM); we have e2e tests for that.
* Use `with-redefs` or similar ClojureScript mocking utilities to isolate the logic under test.
* **No Flakiness:** Tests must be deterministic. Do not use `setTimeout` or real
network calls. Use synchronous mocks for asynchronous workflows where
possible.
* **Location:** Place tests in the `test/frontend_tests/` directory, following the
namespace structure of the source code (e.g., `app.utils.timers` ->
`frontend-tests.util-timers-test`).
* **Execution:**
* **Isolated:** To run a focused ClojureScript unit test: edit the
`test/frontend_tests/runner.cljs` to narrow the test suite, then `pnpm run
test`.
* **Regression:** To run `pnpm run test` without modifications on the runner (preferred)
#### Integration Tests (Playwright)
Integration tests are developed under `frontend/playwright` directory, we use
mocks for remote communication with the backend.
You should not add, modify or run the integration tests unless explicitly asked.
```
pnpm run test:e2e # Playwright e2e tests
pnpm run test:e2e --grep "pattern" # Single e2e test by pattern
```
Ensure everything is installed before executing tests with the `./scripts/setup` script.
### 2. Code Quality & Formatting
* **Linting:** All code changes must pass linter checks:
* Run `pnpm run lint:clj` for CLJ/CLJS/CLJC
* Run `pnpm run lint:js` for JS
* Run `pnpm run lint:scss` for SCSS
* **Formatting:** All code changes must pass the formatting check
* Run `pnpm run check-fmt:clj` for CLJ/CLJS/CLJC
* Run `pnpm run check-fmt:js` for JS
* Run `pnpm run check-fmt:scss` for SCSS
* Use the `pnpm run fmt` fix all the formatting issues (`pnpm run fmt:clj`,
`pnpm run fmt:js` or `pnpm run fmt:scss` for isolated formatting fix)
### 3. Implementation Rules
* **Logic vs. View:** If logic is embedded in a UI component, extract it into a
function in the same namespace if it is only used locally, or look for a helper
namespace to make it unit-testable.
### 4. Stack Trace Analysis
When analyzing production stack traces (minified code), you can generate a
production bundle locally to map the minified code back to the source.
**To build the production bundle:**
Run: `pnpm run build:app`
The compiled files and their corresponding source maps will be generated in
`resources/public/js`.
**Analysis Tips:**
- **Source Maps:** Use the `.map` files generated in `resources/public/js` with
tools like `source-map-lookup` or browser dev tools to resolve minified
locations.
- **Bundle Inspection:** If the issue is related to bundle size or unexpected
code inclusion, inspect the generated modules in `resources/public/js`.
- **Shadow-CLJS Reports:** For more detailed analysis of what is included in the
bundle, you can run shadow-cljs build reports (consult `shadow-cljs.edn` for
build IDs like `main` or `worker`).
## Code Conventions
### Namespace Overview
The source is located under `src` directory and this is a general overview of
namespaces structure:
- `app.main.ui.*` React UI components (`workspace`, `dashboard`, `viewer`)
- `app.main.data.*` Potok event handlers (state mutations + side effects)
- `app.main.refs` Reactive subscriptions (okulary lenses)
- `app.main.store` Potok event store
- `app.util.*` Utilities (DOM, HTTP, i18n, keyboard shortcuts)
### State Management (Potok)
State is a single atom managed by a Potok store. Events implement protocols
(funcool/potok library):
```clojure
(defn my-event
"doc string"
[data]
(ptk/reify ::my-event
ptk/UpdateEvent
(update [_ state] ;; synchronous state transition
(assoc state :key data))
ptk/WatchEvent
(watch [_ state stream] ;; async: returns an observable
(->> (rp/cmd! :some-rpc-command params)
(rx/map success-event)
(rx/catch error-handler)))
ptk/EffectEvent
(effect [_ state _] ;; pure side effects (DOM, logging)
(dom/focus (dom/get-element "id")))))
```
The state is located under `app.main.store` namespace where we have
the `emit!` function responsible for emitting events.
Example:
```cljs
(ns some.ns
(:require
[app.main.data.my-events :refer [my-event]]
[app.main.store :as st]))
(defn on-click
[event]
(st/emit! (my-event)))
```
On `app.main.refs` we have reactive references which look up the main state
for inner data or precalculated data. These references are very useful but
should be used with care because, for example, if we have a complex operation,
this operation will be executed on each state change. Sometimes it is better to
have simple references and use React `use-memo` for more granular memoization.
Prefer helpers from `app.util.dom` instead of using direct DOM calls. If no
helper is available, prefer adding a new helper and then using it.
### UI Components (React & Rumext: mf/defc)
The codebase contains various component patterns. When creating or refactoring
components, follow the Modern Syntax rules outlined below.
#### 1. The * Suffix Convention
The most recent syntax uses a * suffix in the component name (e.g.,
my-component*). This suffix signals the mf/defc macro to apply specific rules
for props handling and destructuring and optimization.
#### 2. Component Definition
Modern components should use the following structure:
```clj
(mf/defc my-component*
{::mf/wrap [mf/memo]} ;; Equivalent to React.memo
[{:keys [name on-click]}] ;; Destructured props
[:div {:class (stl/css :root)
:on-click on-click}
name])
```
#### 3. Hooks
Use the mf namespace for hooks to maintain consistency with the macro's
lifecycle management. These are analogous to standard React hooks:
```clj
(mf/use-state) ;; analogous to React.useState adapted to cljs semantics
(mf/use-effect) ;; analogous to React.useEffect
(mf/use-memo) ;; analogous to React.useMemo
(mf/use-fn) ;; analogous to React.useCallback
```
The `mf/use-state` in difference with React.useState, returns an atom-like
object, where you can use `swap!` or `reset!` to perform an update and
`deref` to get the current value.
You also have the `mf/deref` hook (which does not follow the `use-` naming
pattern) and its purpose is to watch (subscribe to changes on) an atom or
derived atom (from okulary) and get the current value. It is mainly used to
subscribe to lenses defined in `app.main.refs` or private lenses defined in
namespaces.
Rumext also comes with improved syntax macros as alternative to `mf/use-effect`
and `mf/use-memo` functions. Examples:
Example for `mf/with-effect` macro:
```clj
;; Using functions
(mf/use-effect
(mf/deps team-id)
(fn []
(st/emit! (dd/initialize team-id))
(fn []
(st/emit! (dd/finalize team-id)))))
;; The same effect but using mf/with-effect
(mf/with-effect [team-id]
(st/emit! (dd/initialize team-id))
(fn []
(st/emit! (dd/finalize team-id))))
```
Example for `mf/with-memo` macro:
```
;; Using functions
(mf/use-memo
(mf/deps projects team-id)
(fn []
(->> (vals projects)
(filterv #(= team-id (:team-id %))))))
;; Using the macro
(mf/with-memo [projects team-id]
(->> (vals projects)
(filterv #(= team-id (:team-id %)))))
```
Prefer using the macros for their syntax simplicity.
#### 4. Component Usage (Hiccup Syntax)
When invoking a component within Hiccup, always use the [:> component* props]
pattern.
Requirements for props:
- Must be a map literal or a symbol pointing to a JavaScript props object.
- To create a JS props object, use the `#js` literal or the `mf/spread-object` helper macro.
Examples:
```clj
;; Using object literal (no need of #js because macro already interprets it)
[:> my-component* {:data-foo "bar"}]
;; Using object literal (no need of #js because macro already interprets it)
(let [props #js {:data-foo "bar"
:className "myclass"}]
[:> my-component* props])
;; Using the spread helper
(let [props (mf/spread-object base-props {:extra "data"})]
[:> my-component* props])
```
#### 5. Styles
##### Styles on component code
Styles are co-located with components. Each `.cljs` file has a corresponding
`.scss` file.
Example of clojurescript code for reference classes defined on styles (we use
CSS modules pattern):
```clojure
;; In the component namespace:
(require '[app.main.style :as stl])
;; In the render function:
[:div {:class (stl/css :container :active)}]
;; Conditional:
[:div {:class (stl/css-case :some-class true :selected (= drawtool :rect))}]
;; When you need concat an existing class:
[:div {:class [existing-class (stl/css-case :some-class true :selected (= drawtool :rect))]}]
```
##### General rules for styling
- Prefer CSS custom properties ( `margin: var(--sp-xs);`) instead of scss
variables and get the already defined properties from `_sizes.scss`. The SCSS
variables are allowed and still used, just prefer properties if they are
already defined.
- If a value isn't in the DS, use the `px2rem(n)` mixin: `@use "ds/_utils.scss"
as *; padding: px2rem(23);`.
- Do **not** create new SCSS variables for one-off values.
- Use physical directions with logical ones to support RTL/LTR naturally:
- Avoid: `margin-left`, `padding-right`, `left`, `right`.
- Prefer: `margin-inline-start`, `padding-inline-end`, `inset-inline-start`.
- Always use the `use-typography` mixin from `ds/typography.scss`:
- Example: `@include t.use-typography("title-small");`
- Use `$br-*` for radius and `$b-*` for thickness from `ds/_borders.scss`.
- Use only tokens from `ds/colors.scss`. Do **NOT** use `design-tokens.scss` or
legacy color variables.
- Use mixins only from `ds/mixins.scss`. Avoid legacy mixins like
`@include flexCenter;`. Write standard CSS (flex/grid) instead.
- Use the `@use` instead of `@import`. If you go to refactor existing SCSS file,
try to replace all `@import` with `@use`. Example: `@use "ds/_sizes.scss" as
*;` (Use `as *` to expose variables directly).
- Avoid deep selector nesting or high-specificity (IDs). Flatten selectors:
- Avoid: `.card { .title { ... } }`
- Prefer: `.card-title { ... }`
- Leverage component-level CSS variables for state changes (hover/focus) instead
of rewriting properties.
##### Checklist
- [ ] No references to `common/refactor/`
- [ ] All `@import` converted to `@use` (only if refactoring)
- [ ] Physical properties (left/right) using logical properties (inline-start/end).
- [ ] Typography implemented via `use-typography()` mixin.
- [ ] Hardcoded pixel values wrapped in `px2rem()`.
- [ ] Selectors are flat (no deep nesting).
### Translations (`tr`) and Memoization
`(tr "some.key")` resolves the translation string from the **currently active
locale at call time**. This has two consequences:
- **Never call `(tr ...)` at namespace level** (inside a `def` or `defonce`).
Doing so would freeze the label to the locale active at module load time and
break runtime language switching.
- **Always call `(tr ...)` at render time** — either directly in the component
body or inside a `mf/with-memo` / `mf/use-memo` block.
When a component renders a **static list of options** whose labels come from
`(tr ...)` (e.g. radio button options, select options), wrap the vector in
`mf/with-memo []` with no dependencies. This ensures the vector and its
`(tr ...)` calls are evaluated once per component mount instead of on every
render, while still respecting the render-time requirement:
```clojure
(let [options (mf/with-memo []
[{:value "top" :label (tr "some.key.top")}
{:value "center" :label (tr "some.key.center")}
{:value "bottom" :label (tr "some.key.bottom")}])]
...)
```
### Performance Macros (`app.common.data.macros`)
Always prefer these macros over their `clojure.core` equivalents — they compile to faster JavaScript:
```clojure
(dm/select-keys m [:a :b]) ;; ~6x faster than core/select-keys
(dm/get-in obj [:a :b :c]) ;; faster than core/get-in
(dm/str "a" "b" "c") ;; string concatenation
```
### Configuration
`src/app/config.clj` reads globally defined variables and exposes precomputed
configuration values ready to be used from other parts of the application.

View File

@ -1,62 +0,0 @@
# render-wasm Agent Instructions
This component compiles Rust to WebAssembly using Emscripten +
Skia. It is consumed by the frontend as a canvas renderer.
## Commands
```bash
./build # Compile Rust → WASM (requires Emscripten environment)
./watch # Incremental rebuild on file change
./test # Run Rust unit tests (cargo test)
./lint # clippy -D warnings
cargo fmt --check
```
Run a single test:
```bash
cargo test my_test_name # by test function name
cargo test shapes:: # by module prefix
```
Build output lands in `../frontend/resources/public/js/` (consumed directly by the frontend dev server).
## Build Environment
The `_build_env` script sets required env vars (Emscripten paths,
`EMCC_CFLAGS`). `./build` sources it automatically. The WASM heap is
configured to 256 MB initial with geometric growth.
## Architecture
**Global state** — a single `unsafe static mut State` accessed
exclusively through `with_state!` / `with_state_mut!` macros. Never
access it directly.
**Tile-based rendering** — only 512×512 tiles within the viewport
(plus a pre-render buffer) are drawn each frame. Tiles outside the
range are skipped.
**Two-phase updates** — shape data is written via exported setter
functions (called from ClojureScript), then a single `render_frame()`
triggers the actual Skia draw calls.
**Shape hierarchy** — shapes live in a flat pool indexed by UUID;
parent/child relationships are tracked separately.
## Key Source Modules
| Path | Role |
|------|------|
| `src/lib.rs` | WASM exports — all functions callable from JS |
| `src/state.rs` | Global `State` struct definition |
| `src/render/` | Tile rendering pipeline, Skia surface management |
| `src/shapes/` | Shape types and Skia draw logic per shape |
| `src/wasm/` | JS interop helpers (memory, string encoding) |
## Frontend Integration
The WASM module is loaded by `app.render-wasm.*` namespaces in the
frontend. ClojureScript calls exported Rust functions to push shape
data, then calls `render_frame`. Do not change export function
signatures without updating the corresponding ClojureScript bridge.