From 0e16db66b8ee1c5876d6630e35603ab3941ebd37 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 8 Jun 2026 14:34:31 +0200 Subject: [PATCH 1/2] :rewind: Backport from develop AGENTS.md changes --- AGENTS.md | 140 ++++++++++++++++++++++++------------------------------ 1 file changed, 63 insertions(+), 77 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 842cd15022..08103521e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 `/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) + └─
/core ← top-level memory per section (e.g. frontend/core, backend/core) + └─ ← 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:
/` 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 `/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. From 51a9eed02e69f5f069581832654ff471a8da9bff Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 8 Jun 2026 14:35:19 +0200 Subject: [PATCH 2/2] :rewind: Backport from develop AGENTS.md changes --- backend/AGENTS.md | 262 ----------------------------- common/AGENTS.md | 70 -------- frontend/AGENTS.md | 371 ------------------------------------------ render-wasm/AGENTS.md | 62 ------- 4 files changed, 765 deletions(-) delete mode 100644 backend/AGENTS.md delete mode 100644 common/AGENTS.md delete mode 100644 frontend/AGENTS.md delete mode 100644 render-wasm/AGENTS.md diff --git a/backend/AGENTS.md b/backend/AGENTS.md deleted file mode 100644 index a260a74c13..0000000000 --- a/backend/AGENTS.md +++ /dev/null @@ -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/`. - -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 -- 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" -``` diff --git a/common/AGENTS.md b/common/AGENTS.md deleted file mode 100644 index 2659b83939..0000000000 --- a/common/AGENTS.md +++ /dev/null @@ -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"}`). - diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md deleted file mode 100644 index 681528b4f3..0000000000 --- a/frontend/AGENTS.md +++ /dev/null @@ -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. - diff --git a/render-wasm/AGENTS.md b/render-wasm/AGENTS.md deleted file mode 100644 index 511b7da0c9..0000000000 --- a/render-wasm/AGENTS.md +++ /dev/null @@ -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.