diff --git a/.opencode/commands/implement-plan.md b/.opencode/commands/implement-plan.md index 20eedb3fe5..8ecd1bd537 100644 --- a/.opencode/commands/implement-plan.md +++ b/.opencode/commands/implement-plan.md @@ -3,11 +3,8 @@ description: Execute a ready plan end-to-end — create a GitHub issue, branch i agent: build --- -# Implement Plan - This command is run once a plan is ready (for example, from plan mode). Execute -the plan already prepared in the current session context — it does not take -extra arguments. Follow these steps in order. +the plan already prepared in the current session context. Follow these steps in order. ## 1. Create the issue diff --git a/.opencode/commands/review.md b/.opencode/commands/review.md deleted file mode 100644 index 70bd89ac90..0000000000 --- a/.opencode/commands/review.md +++ /dev/null @@ -1,25 +0,0 @@ -Act as a senior software engineer and perform a thorough review. - -## Instructions - -1. **Determine what is being reviewed** from the provided context: - - **If it is a plan** (implementation plan, design document, task breakdown) → load the **`plan-review`** skill. - - **If it is code** (diff, PR, code change) → load the **`code-review`** skill. - -2. Read `AGENTS.md` and follow its instructions for finding and reading all related testing documentation from memories before reviewing. - -3. **Skip generated files, lockfile-only changes, and unrelated modifications** unless they introduce security risks. - -4. Follow the loaded skill's process and produce its output format. - -## Strong Rules - -1. Do not invent problems. Every finding must be real and actionable. -2. Do not modify any code and do not create a commit — this command only reviews. -3. Be specific and constructive. "This could be better" is not helpful — explain why and how. -4. Prioritize by impact. One structural issue outweighs ten nits. -5. Missing tests are an issue, not a suggestion. Report as a severity-tagged finding — never as a recommendation. - -## Context - -$ARGUMENTS diff --git a/.opencode/skills/code-review/SKILL.md b/.opencode/skills/code-review/SKILL.md index 7f06d90b59..7fa581efa0 100644 --- a/.opencode/skills/code-review/SKILL.md +++ b/.opencode/skills/code-review/SKILL.md @@ -106,6 +106,8 @@ For detailed security guidance, see `security-and-hardening`. | **Low:** | Minor, optional | Author may ignore — formatting, style preferences | | **Suggestion:** | Worth considering | Not required, but improves the code | +**Unique finding IDs.** Assign every finding a stable identifier: `F1`, `F2`, `F3`, … numbered in order of severity (Critical first, then High, Medium, Low, Suggestion). Use the ID everywhere the finding is mentioned — in section headers, in the verdict, in follow-up discussion. Never renumber within a review. Example: `**F3 (High)** — `app/validate.cljs:42` — duplicate branch logic…`. + For each finding, describe the circumstances under which it could fail: specific inputs, load conditions, timing, or user actions that trigger the problem. "This crashes when input is null" is actionable; "this might crash" is not. Lead with what matters: correctness and security first, then structural issues, then everything else. A few high-conviction comments beat a long list. @@ -122,11 +124,11 @@ Briefly explain what the code does and give an overall assessment. ### Critical and High-Priority Issues -List problems that could cause security incidents, data loss, crashes, incorrect behavior, or major performance degradation. For each: state the severity, identify the file/function/code section, explain why it's a problem, describe failure circumstances, and provide a concrete improvement with corrected code when useful. +List problems that could cause security incidents, data loss, crashes, incorrect behavior, or major performance degradation. Each finding gets its unique ID (`F1`, `F2`, …). For each: state the severity, identify the file/function/code section, explain why it's a problem, describe failure circumstances, and provide a concrete improvement with corrected code when useful. ### Other Findings -List medium- and low-priority issues, including maintainability and design concerns. +List medium- and low-priority issues, including maintainability and design concerns. Continue the ID sequence started above (`F3`, `F4`, …). ### Suggested Refactoring @@ -148,6 +150,8 @@ Choose one: - **Approve with minor changes** — Good to merge after addressing low/medium issues - **Request changes** — Critical or high issues must be resolved before merge +List the finding IDs the verdict depends on (e.g. "Request changes: F1, F4"). + ## Change Sizing Small, focused changes are easier to review, faster to merge, and safer to deploy. @@ -231,25 +235,13 @@ For supply-chain risk triage, follow the `security-and-hardening` skill. ## Verification -After review is complete: +Before emitting the verdict, verify the change as it stands. This is the reviewer's own due diligence — it covers the state of the code at review time, not the later resolution of findings (fixing findings is the author's job; confirming them is a new review): -- [ ] All Critical issues are resolved -- [ ] All Required (no-prefix) changes are resolved or explicitly deferred with justification -- [ ] Tests pass +- [ ] Tests pass — run them yourself, don't trust the claim - [ ] Build succeeds - [ ] The verification story is documented (what changed, how it was verified) - [ ] Dependency upgrades reviewed against changelog, isolated per package, verified by green suite -## Multi-Model Review Pattern - -Use different models for different review perspectives: - -``` -Model A writes the code → Model B reviews → Model A addresses feedback → Human makes the final call -``` - -Different models have different blind spots. - ## See Also - For detailed security review guidance, see `security-and-hardening` diff --git a/.opencode/skills/planner/SKILL.md b/.opencode/skills/planner/SKILL.md index d1802652e2..3598a0dc16 100644 --- a/.opencode/skills/planner/SKILL.md +++ b/.opencode/skills/planner/SKILL.md @@ -1,13 +1,13 @@ --- name: planner -description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan (Context, Affected modules, Approach, Risks, Testing). Always output to the user; additionally save to .opencode/plans/YYYY-MM-DD-.md. +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 and save to .opencode/plans/YYYY-MM-DD-<title>.md. --- # Planner Read-only senior software architect role for Penpot. Produces structured -implementation plans that engineers or other agents can execute. Never writes -or modifies code. +implementation plans with task breakdowns that engineers or other agents can +execute. Never writes or modifies code. ## When to Use @@ -18,24 +18,29 @@ or modifies code. - The user asks "how would I implement X?" or "what's involved in fixing Y?". - The user is about to start non-trivial work and wants a bite-sized task breakdown. +- 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 are a Senior Software Architect working on Penpot, an open-source design -tool. Your sole responsibility is planning and analysis — you do NOT write or -modify code. +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. -You help users understand the codebase, design solutions, and create detailed -implementation plans that other agents or developers can execute. Document -everything they need to know: which files to touch for each task, code patterns, -tests, and how to verify correctness. Apply DRY and KISS principles. +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 developer's responsibility. +responses — committing is the implementer's responsibility. -## Required Reading Before Planning +## CRITICAL: Required Reading Before Planning Before drafting any plan, work through the project's own guidance: @@ -50,6 +55,8 @@ Before drafting any plan, work through the project's own guidance: Skipping this step is the #1 cause of incorrect or incomplete plans. +--- + ## The Planning Process ### Phase 1: Architecture Analysis @@ -64,16 +71,42 @@ Skipping this step is the #1 cause of incorrect or incomplete plans. ### Phase 2: Task Breakdown -Implementation order follows the monorepo's dependency graph: -`frontend -> common`, `backend -> common`, `exporter -> common`, -`frontend -> render-wasm`. Build shared foundations first, then layer -consumers on top. +#### 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 @@ -89,39 +122,58 @@ Each task follows this structure: ```markdown ## Task [N]: [Short descriptive title] -**Description:** One paragraph explaining what this task accomplishes. +**Description:** One or two paragraphs explaining what this task accomplishes. +Should be clear and concise. + +**Rationale:** Why this task exists and why this approach over the obvious +alternatives — design decisions, trade-offs, constraints discovered during +analysis. One or two sentences; skip only if genuinely trivial. + +**Code sketch (optional):** Signature-, type-, or shape-level example when the +intended interface is non-obvious. Keep it short — a skeleton that fixes the +contract (function signature, model fields, error shape), never a full +implementation. Omit when the task is mechanical. **Acceptance criteria:** - [ ] [Specific, testable condition] - [ ] [Specific, testable condition] **Verification:** -- [ ] Tests pass (module-specific test command) -- [ ] Lint/formatter passes (module-specific check command) +- [ ] Relevant tests pass (module-specific test command). +- [ ] Lint/formatter passes (module-specific check command), if applicable. +- [ ] The core flow works end-to-end, if applicable. **Dependencies:** [Task numbers this depends on, or "None"] **Files likely touched:** - `path/to/file.clj` - `path/to/file_test.clj` + +**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). +(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). + +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 -| Size | Files | Scope | -|------|-------|-------| -| **XS** | 1 | Single function, config change, or schema tweak | -| **S** | 1-2 | One handler or component method | -| **M** | 3-5 | One vertical feature slice | -| **L** | 5-8 | Multi-component feature | -| **XL** | 8+ | **Too large — break it down further** | +| Size | Files | Scope | Example | +|------|-------|-------|---------| +| **XS** | 1 | Single function, config change, or schema tweak | Add a validation rule | +| **S** | 1-2 | One handler or component method | Add a new RPC endpoint | +| **M** | 3-5 | One vertical feature slice | Bookmark CRUD with tests | +| **L** | 5-8 | Multi-component feature | Search with filtering and pagination | +| **XL** | 8+ | **Too large — break it down further** | — | -If a task is L or larger, break it into smaller tasks. Agents perform best on -S and M tasks. +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 @@ -141,11 +193,11 @@ Arrange tasks so that: Add explicit checkpoints with the relevant module commands: ```markdown -## Checkpoint: After Tasks 1-3 -- [ ] All tests pass (module-specific command) -- [ ] Lint/format passes (module-specific command) -- [ ] Core flow works end-to-end -- [ ] Review with human before proceeding +### 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. ``` ## Requirements @@ -159,7 +211,7 @@ Add explicit checkpoints with the relevant module commands: - 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 between major phases. +- Checkpoints must exist after every 2-3 tasks. ## Constraints @@ -168,7 +220,8 @@ Add explicit checkpoints with the relevant module commands: `.opencode/plans/`. - 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. +- 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. @@ -188,8 +241,9 @@ slug is lowercase, hyphen-separated, and a short summary of the task (e.g. `add-batch-get-profiles-for-file-comments`). Create the `.opencode/plans/` directory if it does not exist. -Always attempt the write. If the user explicitly provides a target file path, -use that path instead of the default. +IMPORTANT: The plan agent has write permission specifically for +`.opencode/plans/` — always attempt the write. If the user explicitly provides +a target file path, use that path instead of the default. ### Plan Document Template @@ -212,41 +266,75 @@ use that path instead of the default. security implications.] ## Approach -[Step-by-step implementation plan with file paths, function names, and code -shape where applicable. Group steps into atomic, ordered tasks.] +[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 -### Phase 1: Foundation -- [ ] Task 1: ... -- [ ] Task 2: ... +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. -### Checkpoint: Phase 1 -- [ ] Tests pass, lint/formatter clean (module-specific commands) +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. -### Phase 2: Core Features -- [ ] Task 3: ... -- [ ] Task 4: ... +## Task 1: [Short descriptive title] -### Checkpoint: Phase 2 -- [ ] End-to-end flow works +**Description:** [What this task accomplishes.] -### Phase 3: Polish -- [ ] Task 5: ... -- [ ] Task 6: ... +**Rationale:** [Why this approach over the alternatives.] -### Checkpoint: Complete -- [ ] All acceptance criteria met -- [ ] Ready for review +**Acceptance criteria:** +- [ ] [Specific, testable condition] -## Testing Strategy -[How to verify: which test commands to run per module, what cases to cover, -manual verification steps, lint/format checks. Consult each module's core -memory for the exact commands.] +**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 + 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) @@ -259,13 +347,31 @@ 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. +## Common Rationalizations + +| Rationalization | Reality | +|---|---| +| "I'll figure it out as I go" | That's how you end up with a tangled mess and rework. 10 minutes of planning saves hours. | +| "The tasks are obvious" | Write them down anyway. Explicit tasks surface hidden dependencies and forgotten edge cases. | +| "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 starting implementation, confirm: +Before delivering the plan, confirm: - [ ] Every task has acceptance criteria - [ ] Every task has a verification step - [ ] Task dependencies are identified and ordered correctly -- [ ] No task touches more than ~5 files -- [ ] Checkpoints exist between major phases -- [ ] The human has reviewed and approved the plan +- [ ] No task is XL or larger — break it down instead +- [ ] Checkpoints exist after every 2-3 tasks +- [ ] The plan is ready for human review diff --git a/.serena/memories/devenv/core.md b/.serena/memories/devenv/core.md index 0651f872fe..db46542c99 100644 --- a/.serena/memories/devenv/core.md +++ b/.serena/memories/devenv/core.md @@ -6,6 +6,7 @@ Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Par - `penpotdev-infra`: shared `postgres`, `minio`, `minio-setup`, `mailer`, `ldap`. File: `docker-compose.infra.yml`. - `penpotdev-wsN` (N=0,1,…): per-instance `main` + `redis` (Valkey). File: `docker-compose.main.yml`. ws0 (a.k.a. `main`) binds `$PWD`; ws1+ bind clones at `${PENPOT_WORKSPACES_DIR}/wsN/` (default `~/.penpot/penpot_workspaces/`), maintained by the developer. +- Optional overlay `docker-compose.opencode.yml`: added by `instance-compose` as an extra `-f` only when `PENPOT_OPENCODE_CONFIG_DIR` is set (i.e. `run-devenv --opencode-config-dir DIR` ran in this process). Bind-mounts the host dir at `/home/penpot/.config/opencode` (`:z`). Flag-only, per-call; not read from ambient env. Parser `parse-opencode-config-dir` absolutizes (`~`, realpath) because compose resolves relative bind sources against the compose file's dir. Only instances brought up with the flag get the mount. - All projects join external network `penpot_shared`. Created idempotently by `ensure-devenv-network`, never removed by lifecycle commands. ## Source-of-truth files @@ -65,7 +66,7 @@ No `--delete` on the working-tree pass: gitignored caches in the workspace survi ## CLI surface -- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet). +- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX] [--opencode-config-dir DIR]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet). `--opencode-config-dir DIR` bind-mounts DIR at `~/.config/opencode` in-container via the optional overlay above; mount applies at container creation, so changing it requires stop + re-run. - `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` stops just that workspace. `--ws 0` or no flag stops ws0; shared infra shuts down only if no other instances remain. `--all` stops every ws highest-first then ws0, then infra. - `run-devenv`: legacy alias, ws0 non-agentic attached. - `attach-devenv [--ws main|0|wsN|N]`: pure attach. Fails fast if instance/session missing. diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 9dc140ba4a..a470f2451e 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -1045,3 +1045,18 @@ RETURNING id, deleted_at;") (update acc :created conj email))))) {:created [] :skipped []} emails))))) + +;; ---- API: get-air-gapped + +(def ^:private schema:get-air-gapped-result + [:map + [:air-gapped ::sm/boolean]]) + +(sv/defmethod ::get-air-gapped + "Returns whether this Penpot instance runs in air-gapped mode." + {::doc/added "2.18" + ::sm/params [:map] + ::sm/result schema:get-air-gapped-result + ::rpc/auth false} + [_cfg _params] + {:air-gapped (contains? cf/flags :air-gapped-conf)}) diff --git a/backend/test/backend_tests/rpc_management_nitrate_test.clj b/backend/test/backend_tests/rpc_management_nitrate_test.clj index aabf58f31d..da78b2e733 100644 --- a/backend/test/backend_tests/rpc_management_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_management_nitrate_test.clj @@ -194,6 +194,15 @@ (string? (get version k))))) (t/is (= cf/version version)))) +(t/deftest get-air-gapped + (let [out (th/management-command! {::th/type :get-air-gapped})] + (t/is (th/success? out)) + (t/is (false? (-> out :result :air-gapped)))) + (binding [cf/flags (conj cf/flags :air-gapped-conf)] + (let [out (th/management-command! {::th/type :get-air-gapped})] + (t/is (th/success? out)) + (t/is (true? (-> out :result :air-gapped)))))) + (t/deftest get-teams-returns-only-owned-non-default-non-deleted (with-mocks [nitrate-mock {:target 'app.nitrate/call :return nil}] (let [profile (th/create-profile* 1 {:is-active true}) diff --git a/docker/devenv/docker-compose.opencode.yml b/docker/devenv/docker-compose.opencode.yml new file mode 100644 index 0000000000..1af29e3749 --- /dev/null +++ b/docker/devenv/docker-compose.opencode.yml @@ -0,0 +1,10 @@ +# Optional compose overlay, included by manage.sh's instance-compose ONLY +# when PENPOT_OPENCODE_CONFIG_DIR is set (run-devenv --opencode-config-dir +# DIR). Bind-mounts a host directory over the container's opencode global +# config dir (~/.config/opencode) so personal agents/prompts/skills kept in +# a separate repo are available inside the devenv without committing them +# here. Without the flag this file is never referenced. +services: + main: + volumes: + - "${PENPOT_OPENCODE_CONFIG_DIR}:/home/penpot/.config/opencode:z" diff --git a/docs/technical-guide/developer/agentic-devenv.md b/docs/technical-guide/developer/agentic-devenv.md index 55b03c1492..932f555d6f 100644 --- a/docs/technical-guide/developer/agentic-devenv.md +++ b/docs/technical-guide/developer/agentic-devenv.md @@ -148,10 +148,15 @@ automatically, so regular users never run this. ```bash ./manage.sh run-devenv --agentic \ [--ws N] [--sync] [--serena-context CTX] \ + [--opencode-config-dir DIR] \ [--git-user-name NAME] [--git-user-email EMAIL] ``` Brings one agentic instance up. Errors out if the target is already running. +`--opencode-config-dir DIR` bind-mounts DIR over the container's +`~/.config/opencode` so personal agents/prompts/skills kept in a separate +repository are available to the coding agent; see the +[Dev environment guide](./devenv.md#personal-opencode-config-inside-the-container). `--ws N` (N ≥ 1) brings that workspace up independently — workspaces can be started and stopped in any order. Per-instance ports diff --git a/docs/technical-guide/developer/devenv.md b/docs/technical-guide/developer/devenv.md index 922ed24f1d..d6cc65e89a 100644 --- a/docs/technical-guide/developer/devenv.md +++ b/docs/technical-guide/developer/devenv.md @@ -139,6 +139,27 @@ until you set an identity. The values are applied every time `run-devenv` brings an instance up (idempotent), so re-running with different flags is the way to change the in-container identity. +### Personal opencode config inside the container + +`run-devenv --opencode-config-dir DIR` bind-mounts a host directory over the +container's `~/.config/opencode` (opencode's global config dir). This is how +you keep personal agents, prompts, and skills in a separate repository and +use them inside the devenv without committing them here or leaving untracked +files in the repo: + +```bash +./manage.sh run-devenv --agentic --opencode-config-dir ../penpot-opencode +``` + +The path must be an existing directory; `~` is expanded and the value is +resolved to an absolute path automatically. The mount is applied at container +creation, so changing it requires stopping and re-running `run-devenv` for +that instance, and it applies only to instances brought up with the flag — +other workspaces mount nothing. The directory is shared read-write with the +container (same UID mapping as the source tree). Opencode's own state +(sessions, `auth.json`) lives in `~/.local/share/opencode`, which stays in +the container's data volume regardless of this flag. + ### Shared state and workers All instances share one Penpot database and one MinIO bucket; users, teams, diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs index 790d8b0a3e..a77457c70c 100644 --- a/frontend/src/app/main/ui/auth.cljs +++ b/frontend/src/app/main/ui/auth.cljs @@ -13,7 +13,7 @@ [app.main.ui.auth.login :refer [login-page*]] [app.main.ui.auth.recovery :refer [recovery-page*]] [app.main.ui.auth.recovery-request :refer [recovery-request-page*]] - [app.main.ui.auth.register :refer [register-page* register-success-page* register-validate-page* terms-register*]] + [app.main.ui.auth.register :refer [register-page* register-success-page* register-validate-page* terms-service-privacy-policy*]] [app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*] :as raw-svg] [app.main.ui.ds.foundations.typography.heading :refer [heading*]] [app.util.dom :as dom] @@ -74,7 +74,7 @@ [:> recovery-page* {:params params}]) (when (= section :auth-register) - [:> terms-register*])]])) + [:> terms-service-privacy-policy*])]])) (mf/defc auth-page* diff --git a/frontend/src/app/main/ui/auth.scss b/frontend/src/app/main/ui/auth.scss index 7d593ce4fa..0462f23514 100644 --- a/frontend/src/app/main/ui/auth.scss +++ b/frontend/src/app/main/ui/auth.scss @@ -6,6 +6,7 @@ @use "ds/_sizes.scss" as *; @use "ds/_utils.scss" as *; +@use "./ds/mixins.scss" as *; .auth-section { display: grid; @@ -26,6 +27,8 @@ } .auth-section.register { + @include custom-scrollbar; + display: flex; justify-content: center; align-items: center; diff --git a/frontend/src/app/main/ui/auth/common.scss b/frontend/src/app/main/ui/auth/common.scss index 951ea442a6..066b6849d0 100644 --- a/frontend/src/app/main/ui/auth/common.scss +++ b/frontend/src/app/main/ui/auth/common.scss @@ -4,56 +4,15 @@ // // Copyright (c) KALEIDOS INC Sucursal en España SL -@use "ds/_utils.scss" as *; @use "ds/_sizes.scss" as *; @use "ds/_borders.scss" as *; @use "ds/typography.scss" as *; -.auth-form-wrapper { - inline-size: 100%; - padding-block-end: 0; - display: grid; +.form { + display: flex; + flex-direction: column; gap: var(--sp-m); - - // Native <form> inside auth-form-wrapper — no class available - form { - display: flex; - flex-direction: column; - gap: var(--sp-m); - margin-block-start: var(--sp-m); - } -} - -.auth-title-wrapper { - inline-size: 100%; - padding-block-end: 0; - display: grid; - gap: var(--sp-s); -} - -.separator { - border-color: var(--color-background-quaternary); - margin: 0; -} - -.auth-title { - @include use-typography("title-large"); - - line-height: 1.2; - color: var(--color-foreground-primary); -} - -.auth-subtitle { - @include use-typography("title-small"); - - color: var(--color-foreground-secondary); -} - -.auth-tagline { - @include use-typography("title-small"); - - margin: 0; - color: var(--color-foreground-secondary); + margin: var(--sp-m) 0; } .form-field { @@ -62,36 +21,40 @@ --input-min-width: 100%; } -.buttons-stack { - display: grid; - gap: var(--sp-s); -} +.form-submit-btn { + --button-bg-color: var(--color-accent-primary); + --button-border-color: var(--color-accent-primary); + --button-fg-color: var(--color-background-secondary); -.login-button, -.login-ldap-button { @include use-typography("headline-small"); display: flex; justify-content: center; align-items: center; - cursor: pointer; - background-color: var(--color-accent-primary); - border: $b-1 solid var(--color-accent-primary); - color: var(--color-background-secondary); + background-color: var(--button-bg-color); + border: $b-1 solid var(--button-border-color); + color: var(--button-fg-color); border-radius: $br-8; min-block-size: $sz-32; block-size: $sz-40; inline-size: 100%; + &:hover { + --button-bg-color: var(--color-accent-tertiary); + --button-border-color: var(--color-accent-tertiary); + --button-fg-color: var(--color-background-secondary); + + text-decoration: none; + } + &:disabled { - background-color: var(--color-background-quaternary); - border: 1px solid var(--color-background-quaternary); - color: var(--color-foreground-disabled); - cursor: unset; + --button-bg-color: var(--color-background-quaternary); + --button-border-color: var(--color-background-quaternary); + --button-fg-color: var(--color-foreground-disabled); } } -.go-back { +.go-back-row { display: flex; flex-direction: column; gap: var(--sp-m); @@ -100,19 +63,68 @@ } .go-back-link { + --button-bg-color: var(--color-background-tertiary); + --button-border-color: var(--color-background-tertiary); + --button-fg-color: var(--color-foreground-secondary); + + @include use-typography("headline-small"); + background: none; - cursor: pointer; display: flex; justify-content: center; align-items: center; border-radius: $br-8; - background-color: var(--color-background-tertiary); - border: $b-1 solid var(--color-background-tertiary); - color: var(--color-foreground-secondary); - - @include use-typography("headline-small"); - + background-color: var(--button-bg-color); + border: $b-1 solid var(--button-border-color); + color: var(--button-fg-color); block-size: $sz-40; + + &:hover { + --button-bg-color: var(--color-background-quaternary); + --button-border-color: var(--color-background-quaternary); + --button-fg-color: var(--color-accent-primary); + + text-decoration: none; + } +} + +.separator { + border-color: var(--color-background-quaternary); + margin: 0; +} + +.wrapper { + inline-size: 100%; + padding-block-end: 0; + display: grid; + gap: var(--sp-m); +} + +.title-wrapper { + inline-size: 100%; + padding-block-end: 0; + display: grid; + gap: var(--sp-s); +} + +.title { + @include use-typography("title-large"); + + line-height: 1.2; + color: var(--color-foreground-primary); +} + +.subtitle { + @include use-typography("title-small"); + + color: var(--color-foreground-secondary); +} + +.tagline { + @include use-typography("title-small"); + + margin: 0; + color: var(--color-foreground-secondary); } .links { @@ -120,104 +132,10 @@ gap: var(--sp-xxl); } -.register, -.account, -.recovery-request, -.demo-account { - display: flex; - justify-content: center; - gap: var(--sp-s); - padding: 0; -} +.notification-email { + @include use-typography("title-medium"); -.register-text, -.account-text, -.recovery-text, -.demo-account-text { - @include use-typography("title-small"); - - text-align: right; - color: var(--color-foreground-secondary); -} - -.register-link, -.account-link, -.recovery-link, -.forgot-pass-link, -.demo-account-link { - @include use-typography("title-small"); - - text-align: left; - background-color: transparent; - border: none; - display: inline; + line-height: 1.2; color: var(--color-accent-primary); - - &:hover { - text-decoration: underline; - } -} - -.forgot-password { - display: flex; - justify-content: flex-end; -} - -.submit-btn, -.register-btn, -.recover-btn { - @include use-typography("headline-small"); - - background: none; - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - background-color: var(--color-accent-primary); - border: $b-1 solid var(--color-accent-primary); - color: var(--color-background-secondary); - border-radius: $br-8; - min-block-size: $sz-32; - block-size: $sz-40; - inline-size: 100%; - - &:disabled { - background-color: var(--color-background-quaternary); - border: $b-1 solid var(--color-background-quaternary); - color: var(--color-foreground-disabled); - cursor: unset; - } -} - -.login-btn { - @include use-typography("title-small"); - - display: flex; - align-items: center; - gap: px2rem(6); - inline-size: 100%; - border-radius: $br-8; - background-color: var(--color-background-tertiary); - color: var(--color-foreground-primary); - - span { - padding-block-start: var(--sp-xxs); - } - - &:hover { - color: var(--color-foreground-primary); - background-color: var(--color-background-quaternary); - } - - &:disabled { - background-color: var(--color-background-quaternary); - border: 1px solid var(--color-background-quaternary); - color: var(--color-foreground-disabled); - cursor: unset; - } -} - -.auth-buttons { - display: flex; - gap: var(--sp-s); + margin-inline: $sz-36; } diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index c23cbf1c53..5cb6953f6d 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -71,10 +71,17 @@ (mf/defc login-form* [{:keys [params handle-redirect on-success-callback on-recovery-request origin] :as props}] (let [initial (mf/with-memo [params] params) - error (mf/use-state false) + form (fm/use-form :schema schema:login-form :initial initial) + + error (mf/use-state false) + + show-password-field* (mf/use-state #(not (contains? cf/flags :login-with-custom-sso))) + show-password-field? (deref show-password-field*) + callback-url (:callback-url params) + on-error (fn [cause] (let [cause (ex-data cause)] @@ -102,12 +109,6 @@ :else (reset! error (tr "errors.generic"))))) - show-password-field* - (mf/use-state #(not (contains? cf/flags :login-with-custom-sso))) - - show-password-field? - (deref show-password-field*) - on-success (fn [data] (when (fn? on-success-callback) @@ -130,7 +131,8 @@ (->> (rp/cmd! :get-sso-provider {:email (:email params)}) (rx/map :id) (rx/catch (fn [cause] - (log/error :hint "error on retrieving sso provider" :cause cause) + (log/error :hint "error on retrieving sso provider" + :cause cause) (rx/of nil))) (rx/subs! (fn [sso-provider-id] (if sso-provider-id @@ -139,7 +141,7 @@ (reset! show-password-field* true)))))))))) on-submit-ldap - (mf/use-callback + (mf/use-fn (mf/deps form) (fn [event] (dom/prevent-default event) @@ -168,91 +170,102 @@ {:level :error} message]) [:& fm/form {:on-submit on-submit - :class (stl/css :login-form) + :class (stl/css :form) :form form} - [:div {:class (stl/css :fields-row)} - [:& fm/input - {:name :email - :type "email" - :label (tr "auth.work-email") - :class (stl/css :form-field)}]] + [:div {:class (stl/css :form-row)} + [:& fm/input {:name :email + :type "email" + :label (tr "auth.work-email") + :class (stl/css :form-field)}]] (when show-password-field? - [:div {:class (stl/css :fields-row)} - [:& fm/input - {:type "password" - :name :password - :auto-focus? true - :label (tr "auth.password") - :class (stl/css :form-field)}]]) + [:div {:class (stl/css :form-row)} + [:& fm/input {:type "password" + :name :password + :auto-focus? true + :label (tr "auth.password") + :class (stl/css :form-field)}]]) (when (and (not= origin :viewer) (or (contains? cf/flags :login) (contains? cf/flags :login-with-password))) - [:div {:class (stl/css :fields-row :forgot-password)} + [:div {:class (stl/css :form-row :forgot-password-row)} [:> lk/link* {:action on-recovery-request - :class (stl/css :forgot-pass-link) + :class (stl/css :forgot-password-link) :data-testid "forgot-password"} (tr "auth.forgot-password")]]) - [:div {:class (stl/css :buttons-stack)} + [:div {:class (stl/css :form-submit-buttons)} (when (or (contains? cf/flags :login) (contains? cf/flags :login-with-password)) - [:> fm/submit-button* - {:label (tr "labels.continue") - :data-testid "login-submit" - :class (stl/css :login-button)}]) + [:> fm/submit-button* {:label (tr "labels.continue") + :data-testid "login-submit" + :class (stl/css :form-submit-btn)}]) (when (contains? cf/flags :login-with-ldap) - [:> fm/submit-button* - {:label (tr "auth.login-with-ldap-submit") - :class (stl/css :login-ldap-button) - :on-click on-submit-ldap}])]]])) + [:> fm/submit-button* {:label (tr "auth.login-with-ldap-submit") + :class (stl/css :form-submit-btn) + :on-click on-submit-ldap}])]]])) (defn raw-icon [id] (mf/html [:> raw-svg* {:id id :class (stl/css :sso-icon)}])) -(mf/defc login-sso-buttons* +(mf/defc sso-buttons* [{:keys [params] :as props}] - (let [login-with-google (mf/use-fn (mf/deps params) #(login-with-sso "google" params)) - login-with-github (mf/use-fn (mf/deps params) #(login-with-sso "github" params)) - login-with-gitlab (mf/use-fn (mf/deps params) #(login-with-sso "gitlab" params)) - login-with-oidc (mf/use-fn (mf/deps params) #(login-with-sso "oidc" params))] + (let [login-with-google + (mf/use-fn + (mf/deps params) + #(login-with-sso "google" params)) - [:div {:class (stl/css :auth-buttons)} + login-with-github + (mf/use-fn + (mf/deps params) + #(login-with-sso "github" params)) + + login-with-gitlab + (mf/use-fn + (mf/deps params) + #(login-with-sso "gitlab" params)) + + login-with-oidc + (mf/use-fn + (mf/deps params) + #(login-with-sso "oidc" params))] + + [:div {:class (stl/css :sso-row)} (when (contains? cf/flags :login-with-google) [:> bl/button-link* {:on-click login-with-google :icon (raw-icon raw-icons/brand-google) :label (tr "auth.login-with-google-submit") - :class (stl/css :login-btn :btn-google-auth)}]) + :class (stl/css :sso-btn)}]) (when (contains? cf/flags :login-with-github) [:> bl/button-link* {:on-click login-with-github :icon (raw-icon raw-icons/brand-github) :label (tr "auth.login-with-github-submit") - :class (stl/css :login-btn :btn-github-auth)}]) + :class (stl/css :sso-btn)}]) (when (contains? cf/flags :login-with-gitlab) [:> bl/button-link* {:on-click login-with-gitlab :icon (raw-icon raw-icons/brand-gitlab) :label (tr "auth.login-with-gitlab-submit") - :class (stl/css :login-btn :btn-gitlab-auth)}]) + :class (stl/css :sso-btn)}]) (when (contains? cf/flags :login-with-oidc) [:> bl/button-link* {:on-click login-with-oidc :icon (raw-icon raw-icons/brand-openid) :label (or (not-empty cf/oidc-name) (tr "auth.login-with-oidc-submit")) - :class (stl/css :login-btn :btn-oidc-auth)}])])) + :class (stl/css :sso-btn)}])])) (mf/defc login-dialog* [{:keys [params] :as props}] [:* (when show-sso-login-buttons? [:* - [:> login-sso-buttons* {:params params}] + [:> sso-buttons* {:params params}] (when (or (contains? cf/flags :login) (contains? cf/flags :login-with-password) @@ -270,11 +283,11 @@ (mf/use-fn #(st/emit! (rt/nav :auth-register params)))] - [:div {:class (stl/css :auth-form-wrapper)} - [:h1 {:class (stl/css :auth-title) + [:div {:class (stl/css :wrapper)} + [:h1 {:class (stl/css :title) :data-testid "login-title"} (tr "auth.login-account-title")] - [:p {:class (stl/css :auth-tagline)} + [:p {:class (stl/css :tagline)} (tr "auth.login-tagline")] (when (contains? cf/flags :demo-warning) @@ -286,7 +299,7 @@ [:div {:class (stl/css :links)} (when (contains? cf/flags :registration) - [:div {:class (stl/css :register)} + [:div {:class (stl/css :register-row)} [:span {:class (stl/css :register-text)} (tr "auth.register") " "] [:> lk/link* {:action go-register diff --git a/frontend/src/app/main/ui/auth/login.scss b/frontend/src/app/main/ui/auth/login.scss index 5aa37946bb..270eea41bf 100644 --- a/frontend/src/app/main/ui/auth/login.scss +++ b/frontend/src/app/main/ui/auth/login.scss @@ -6,8 +6,88 @@ @use "./common"; @use "ds/_utils.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/typography.scss" as *; + +.forgot-password-row { + display: flex; + justify-content: flex-end; +} + +.forgot-password-link { + @include use-typography("title-small"); + + text-align: left; + background-color: transparent; + border: none; + display: inline; + color: var(--color-accent-primary); + + &:hover { + text-decoration: underline; + } +} + +.form-submit-buttons { + display: grid; + gap: var(--sp-s); +} .sso-icon { max-inline-size: px2rem(26); max-block-size: px2rem(26); } + +.sso-row { + display: flex; + gap: var(--sp-s); +} + +.sso-btn { + --button-bg-color: var(--color-background-tertiary); + --button-fg-color: var(--color-foreground-primary); + + @include use-typography("title-small"); + + display: flex; + align-items: center; + gap: px2rem(6); + inline-size: 100%; + border-radius: $br-8; + background-color: var(--button-bg-color); + color: var(--button-fg-color); + + &:hover { + --button-bg-color: var(--color-background-quaternary); + --button-fg-color: var(--color-foreground-primary); + } +} + +.register-row { + display: flex; + justify-content: center; + gap: var(--sp-s); + padding: 0; +} + +.register-text { + @include use-typography("title-small"); + + text-align: right; + color: var(--color-foreground-secondary); +} + +.register-link { + @include use-typography("title-small"); + + text-align: left; + background-color: transparent; + border: none; + display: inline; + color: var(--color-accent-primary); + + &:hover { + text-decoration: underline; + } +} diff --git a/frontend/src/app/main/ui/auth/recovery.cljs b/frontend/src/app/main/ui/auth/recovery.cljs index d91f200db9..e51a2ecf88 100644 --- a/frontend/src/app/main/ui/auth/recovery.cljs +++ b/frontend/src/app/main/ui/auth/recovery.cljs @@ -60,39 +60,38 @@ :initial params)] [:& fm/form {:on-submit on-submit - :class (stl/css :recovery-form) + :class (stl/css :form) :form form} - [:div {:class (stl/css :fields-row)} + [:div {:class (stl/css :form-row)} [:& fm/input {:type "password" :name :password-1 :show-success? true :label (tr "auth.new-password") :class (stl/css :form-field)}]] - [:div {:class (stl/css :fields-row)} + [:div {:class (stl/css :form-row)} [:& fm/input {:type "password" :name :password-2 :show-success? true :label (tr "auth.confirm-password") :class (stl/css :form-field)}]] - [:> fm/submit-button* - {:label (tr "auth.recovery-submit") - :class (stl/css :submit-btn)}]])) + [:> fm/submit-button* {:label (tr "auth.recovery-submit") + :class (stl/css :form-submit-btn)}]])) ;; --- Recovery Request Page (mf/defc recovery-page* [{:keys [params]}] - [:div {:class (stl/css :auth-form-wrapper)} - [:h1 {:class (stl/css :auth-title)} "Forgot your password?"] - [:div {:class (stl/css :auth-subtitle)} "Please enter your new password"] + [:div {:class (stl/css :wrapper)} + [:h1 {:class (stl/css :title)} (tr "auth.recovery-request-title")] + [:div {:class (stl/css :subtitle)} (tr "auth.recovery-request-subtitle")] [:hr {:class (stl/css :separator)}] [:> recovery-form* {:params params}] [:div {:class (stl/css :links)} - [:div {:class (stl/css :go-back)} + [:div {:class (stl/css :go-back-row)} [:a {:on-click #(st/emit! (rt/nav :auth-login)) :class (stl/css :go-back-link)} (tr "profile.recovery.go-to-login")]]]]) diff --git a/frontend/src/app/main/ui/auth/recovery.scss b/frontend/src/app/main/ui/auth/recovery.scss index 4d0d4750bd..136a0cd63a 100644 --- a/frontend/src/app/main/ui/auth/recovery.scss +++ b/frontend/src/app/main/ui/auth/recovery.scss @@ -5,7 +5,3 @@ // Copyright (c) KALEIDOS INC Sucursal en España SL @use "./common"; - -.submit-btn { - margin-block-start: var(--sp-l); -} diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs index 78caa421ff..d6b8338bee 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.cljs +++ b/frontend/src/app/main/ui/auth/recovery_request.cljs @@ -70,18 +70,17 @@ (st/emit! (du/request-profile-recovery params)))))] [:& fm/form {:on-submit on-submit - :class (stl/css :recovery-request-form) + :class (stl/css :form) :form form} - [:div {:class (stl/css :fields-row)} + [:div {:class (stl/css :form-row)} [:& fm/input {:name :email :label (tr "auth.work-email") :type "text" :class (stl/css :form-field)}]] - [:> fm/submit-button* - {:label (tr "auth.recovery-request-submit") - :data-testid "recovery-resquest-submit" - :class (stl/css :recover-btn)}]])) + [:> fm/submit-button* {:label (tr "auth.recovery-request-submit") + :data-testid "recovery-resquest-submit" + :class (stl/css :form-submit-btn)}]])) ;; --- Recovery Request Page @@ -90,14 +89,14 @@ [{:keys [params on-success-callback go-back-callback]}] (let [default-go-back #(st/emit! (rt/nav :auth-login)) go-back (or go-back-callback default-go-back)] - [:div {:class (stl/css :auth-form-wrapper)} - [:h1 {:class (stl/css :auth-title)} (tr "auth.recovery-request-title")] - [:div {:class (stl/css :auth-subtitle)} (tr "auth.recovery-request-subtitle")] + [:div {:class (stl/css :wrapper)} + [:h1 {:class (stl/css :title)} (tr "auth.recovery-request-title")] + [:div {:class (stl/css :subtitle)} (tr "auth.recovery-request-subtitle")] [:hr {:class (stl/css :separator)}] [:> recovery-form* {:params params :on-success-callback on-success-callback}] [:hr {:class (stl/css :separator)}] - [:div {:class (stl/css :go-back)} + [:div {:class (stl/css :go-back-row)} [:> lk/link* {:action go-back :class (stl/css :go-back-link) :data-testid "go-back-link"} @@ -106,11 +105,10 @@ (mf/defc recovery-sent-page* [{:keys [email]}] - [:div {:class (stl/css :auth-form-wrapper :register-success)} - [:div {:class (stl/css :auth-title-wrapper)} - [:h2 {:class (stl/css :auth-title)} + [:div {:class (stl/css :wrapper :register-success)} + [:div {:class (stl/css :title-wrapper)} + [:h2 {:class (stl/css :title)} (tr "auth.check-email")] [:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery")]] - [:div {:class (stl/css :notification-text-email)} email] + [:div {:class (stl/css :notification-email)} email] [:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery-check")]]) - diff --git a/frontend/src/app/main/ui/auth/recovery_request.scss b/frontend/src/app/main/ui/auth/recovery_request.scss index 11d45df27b..136a0cd63a 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.scss +++ b/frontend/src/app/main/ui/auth/recovery_request.scss @@ -4,19 +4,4 @@ // // Copyright (c) KALEIDOS INC Sucursal en España SL -@use "ds/_utils.scss" as *; -@use "ds/_sizes.scss" as *; -@use "ds/typography.scss" as *; @use "./common"; - -.fields-row { - margin-block-end: var(--sp-s); -} - -.notification-text-email { - @include use-typography("title-medium"); - - line-height: 1.2; - color: var(--color-accent-primary); - margin-inline: $sz-36; -} diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index be5f3f280b..41acc9e841 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -26,39 +26,38 @@ ;; --- PAGE: Register -(mf/defc newsletter-options* +(mf/defc newsletter-checkbox* {::mf/private true} [] - (let [updates-label + (let [newsletter-label (mf/html - [:> i18n/tr-html* - {:tag-name "div" - :content (tr "onboarding-v2.newsletter.updates")}])] - [:div {:class (stl/css :fields-row :input-visible :newsletter-option-wrapper)} + [:> i18n/tr-html* {:tag-name "div" + :content (tr "onboarding-v2.newsletter.updates")}])] + + [:div {:class (stl/css :form-row :input-visible :newsletter-option-wrapper)} [:& fm/input {:name :accept-newsletter-updates :class (stl/css :checkbox-newsletter-updates) :type "checkbox" :default-checked false - :label updates-label}]])) + :label newsletter-label}]])) -(mf/defc terms-and-privacy* +(mf/defc terms-and-privacy-checkbox* {::mf/private true} [] - (let [terms-label + (let [terms-and-privacy-label (mf/html - [:> i18n/tr-html* - {:tag-name "div" - :content (tr "auth.terms-and-privacy-agreement" - cf/terms-of-service-uri - cf/privacy-policy-uri)}])] + [:> i18n/tr-html* {:tag-name "div" + :content (tr "auth.terms-and-privacy-agreement" + cf/terms-of-service-uri + cf/privacy-policy-uri)}])] - [:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)} + [:div {:class (stl/css :form-row :input-visible :accept-terms-and-privacy-wrapper)} [:& fm/input {:name :accept-terms-and-privacy :show-error false :class (stl/css :checkbox-terms-and-privacy) :type "checkbox" :default-checked false - :label terms-label}]])) + :label terms-and-privacy-label}]])) (def ^:private schema:register-form [:map {:title "RegisterForm"} @@ -76,8 +75,7 @@ form (fm/use-form :schema schema:register-form :initial initial) - submitted? - (mf/use-state false) + submitted? (mf/use-state false) on-error (mf/use-fn @@ -167,22 +165,24 @@ (->> (rp/cmd! :prepare-register-profile cdata) (rx/subs! on-register-profile on-error #(reset! submitted? false))))))] - [:& fm/form {:on-submit on-submit :form form} - [:div {:class (stl/css :fields-row)} + [:& fm/form {:on-submit on-submit + :form form + :class (stl/css :form)} + [:div {:class (stl/css :form-row)} [:& fm/input {:name :fullname :label (tr "auth.fullname") :type "text" :show-success? true :class (stl/css :form-field)}]] - [:div {:class (stl/css :fields-row)} + [:div {:class (stl/css :form-row)} [:& fm/input {:type "text" :name :email :label (tr "auth.work-email") :data-testid "email-input" :show-success? true :class (stl/css :form-field)}]] - [:div {:class (stl/css :fields-row)} + [:div {:class (stl/css :form-row)} [:& fm/input {:name :password :hint (tr "auth.password-length-hint") :label (tr "auth.password") @@ -191,21 +191,20 @@ :class (stl/css :form-field)}]] (when (contains? cf/flags :terms-and-privacy-checkbox) - [:> terms-and-privacy*]) + [:> terms-and-privacy-checkbox*]) - [:> newsletter-options*] + [:> newsletter-checkbox*] - [:> fm/submit-button* - {:label (tr "auth.register-submit") - :disabled @submitted? - :data-testid "register-form-submit" - :class (stl/css :register-btn)}]])) + [:> fm/submit-button* {:label (tr "auth.register-submit") + :disabled @submitted? + :data-testid "register-form-submit" + :class (stl/css :form-submit-btn)}]])) (mf/defc register-methods* [{:keys [params hide-separator on-success-callback]}] [:* (when login/show-sso-login-buttons? - [:> login/login-sso-buttons* {:params params}]) + [:> login/sso-buttons* {:params params}]) (when (or login/show-sso-login-buttons? (false? hide-separator)) [:hr {:class (stl/css :separator)}]) (when (contains? cf/flags :login-with-password) @@ -213,8 +212,8 @@ (mf/defc register-page* [{:keys [params]}] - [:div {:class (stl/css :auth-form-wrapper :register-form)} - [:h1 {:class (stl/css :auth-title) + [:div {:class (stl/css :wrapper :register-form)} + [:h1 {:class (stl/css :title) :data-testid "registration-title"} (tr "auth.register-title")] (when (contains? cf/flags :demo-warning) @@ -223,7 +222,7 @@ [:> register-methods* {:params params}] [:div {:class (stl/css :links)} - [:div {:class (stl/css :account)} + [:div {:class (stl/css :account-row)} [:span {:class (stl/css :account-text)} (tr "auth.already-have-account") " "] [:> lk/link* {:action #(st/emit! (rt/nav :auth-login params)) :class (stl/css :account-link) @@ -233,9 +232,9 @@ (when (contains? cf/flags :demo-users) [:* [:hr {:class (stl/css :separator)}] - [:div {:class (stl/css :demo-account)} + [:div {:class (stl/css :account-row)} [:> lk/link* {:action login/create-demo-profile - :class (stl/css :demo-account-link)} + :class (stl/css :account-link)} (tr "auth.create-demo-account")]]])]]) @@ -244,31 +243,31 @@ (mf/defc register-success-page* [{:keys [params]}] (let [email (or (:email params) (::email storage/user))] - [:div {:class (stl/css :auth-form-wrapper :register-success)} - [:div {:class (stl/css :auth-title-wrapper)} - [:h2 {:class (stl/css :auth-title)} + [:div {:class (stl/css :wrapper :register-success)} + [:div {:class (stl/css :title-wrapper)} + [:h2 {:class (stl/css :register-success-title)} (tr "auth.check-email")] [:div {:class (stl/css :notification-text)} (tr "auth.verification-sent-email")]] - [:div {:class (stl/css :notification-text-email)} email]])) + [:div {:class (stl/css :notification-email)} email]])) -(mf/defc terms-register* +(mf/defc terms-service-privacy-policy* [] (let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri) show-terms? (some? cf/terms-of-service-uri) show-privacy? (some? cf/privacy-policy-uri)] (when show-all? - [:div {:class (stl/css :terms-register)} + [:div {:class (stl/css :terms)} (when show-terms? - [:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :auth-link)} + [:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :terms-link)} (tr "auth.terms-of-service")]) (when show-all? - [:span {:class (stl/css :and-text)} + [:span {:class (stl/css :terms-and)} (dm/str " " (tr "labels.and") " ")]) (when show-privacy? - [:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :auth-link)} + [:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :terms-link)} (tr "auth.privacy-policy")])]))) ;; --- PAGE: register validation @@ -334,9 +333,9 @@ [:& fm/form {:on-submit on-submit :form form - :class (stl/css :register-validate-form)} + :class (stl/css :auth-form)} - [:div {:class (stl/css :fields-row)} + [:div {:class (stl/css :form-row)} [:& fm/input {:name :fullname :label (tr "auth.fullname") :type "text" @@ -344,30 +343,28 @@ :class (stl/css :form-field)}]] (when (contains? cf/flags :terms-and-privacy-checkbox) - [:> terms-and-privacy*]) + [:> terms-and-privacy-checkbox*]) - [:> newsletter-options*] - - [:> fm/submit-button* - {:label (tr "auth.register-submit") - :disabled @submitted? - :class (stl/css :register-btn)}]])) + [:> newsletter-checkbox*] + [:> fm/submit-button* {:label (tr "auth.register-submit") + :disabled @submitted? + :class (stl/css :form-submit-btn)}]])) (mf/defc register-validate-page* [{:keys [params]}] - [:div {:class (stl/css :auth-form-wrapper :register-form)} + [:div {:class (stl/css :wrapper :register-form)} - [:div {:class (stl/css :auth-title-wrapper)} - [:h2 {:class (stl/css :auth-title) + [:div {:class (stl/css :title-wrapper)} + [:h2 {:class (stl/css :title) :data-testid "register-title"} (tr "auth.register-account-title")] - [:div {:class (stl/css :auth-subtitle)} (tr "auth.register-account-tagline")]] + [:div {:class (stl/css :subtitle)} (tr "auth.register-account-tagline")]] [:> register-validate-form* {:params params}] [:div {:class (stl/css :links)} - [:div {:class (stl/css :go-back)} + [:div {:class (stl/css :go-back-row)} [:> lk/link* {:action #(st/emit! (rt/nav :auth-register {})) :class (stl/css :go-back-link)} (tr "labels.go-back")]]]]) diff --git a/frontend/src/app/main/ui/auth/register.scss b/frontend/src/app/main/ui/auth/register.scss index 1b238cb353..71b7c8b4a0 100644 --- a/frontend/src/app/main/ui/auth/register.scss +++ b/frontend/src/app/main/ui/auth/register.scss @@ -4,57 +4,64 @@ // // Copyright (c) KALEIDOS INC Sucursal en España SL +@use "./common"; @use "ds/_utils.scss" as *; @use "ds/_sizes.scss" as *; @use "ds/_borders.scss" as *; @use "ds/typography.scss" as *; -@use "./common"; -.checkbox-terms-and-privacy, .checkbox-newsletter-updates { align-items: flex-start; } +.checkbox-terms-and-privacy { + align-items: flex-start; +} + .register-form { gap: var(--sp-xxl); } +.account-row { + display: flex; + justify-content: center; + gap: var(--sp-s); + padding: 0; +} + +.account-text { + @include use-typography("title-small"); + + text-align: right; + color: var(--color-foreground-secondary); +} + +.account-link { + @include use-typography("title-small"); + + text-align: left; + background-color: transparent; + border: none; + display: inline; + color: var(--color-accent-primary); + + &:hover { + text-decoration: underline; + } +} + .register-success { gap: var(--sp-xxl); } -.register-success .auth-title { +.register-success-title { @include use-typography("title-medium"); line-height: 1.2; + color: var(--color-foreground-primary); } -.notification-text { - @include use-typography("body-medium"); - - color: var(--color-foreground-secondary); -} - -.notification-text-email { - @include use-typography("title-medium"); - - line-height: 1.2; - color: var(--color-accent-primary); - margin-inline: $sz-36; -} - -.logo-btn { - block-size: $sz-40; -} - -.logo-container { - display: flex; - justify-content: flex-start; - inline-size: $sz-120; - margin-block-end: var(--sp-xxl); -} - -.terms-register { +.terms { @include use-typography("body-small"); display: flex; @@ -63,15 +70,15 @@ inline-size: 100%; } -.and-text { - border-block-end: $b-1 solid transparent; - color: var(--color-foreground-secondary); -} - -.auth-link { +.terms-link { color: var(--color-accent-primary); &:hover { text-decoration: underline; } } + +.terms-and { + border-block-end: $b-1 solid transparent; + color: var(--color-foreground-secondary); +} diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs index f04c30b849..b356e88af4 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs @@ -839,7 +839,8 @@ (mf/with-effect [handle-unmount] handle-unmount) [:div {:class [class (stl/css-case :input-wrapper true - :resizable (not is-token-applied?))] + :resizable (and (not is-token-applied?) + (not disabled)))] :ref wrapper-ref :on-pointer-down on-scrub-pointer-down :on-pointer-move on-scrub-pointer-move diff --git a/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss b/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss index 765e6d5eae..e8d1586fbd 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss +++ b/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss @@ -22,9 +22,10 @@ column-gap: var(--sp-xs); align-items: center; inline-size: 100%; + block-size: var(--token-field-height); background: var(--token-field-bg-color); border-radius: $br-8; - padding-inline-end: var(--sp-xs); + padding: 0 var(--input-padding-size, var(--sp-s)); outline: $b-1 solid var(--token-field-outline-color); position: relative; diff --git a/frontend/src/app/main/ui/ds/layout/tab_switcher.scss b/frontend/src/app/main/ui/ds/layout/tab_switcher.scss index c03a03e047..de1c34ccfc 100644 --- a/frontend/src/app/main/ui/ds/layout/tab_switcher.scss +++ b/frontend/src/app/main/ui/ds/layout/tab_switcher.scss @@ -114,6 +114,7 @@ display: grid; width: 100%; height: 100%; + min-block-size: 0; outline: $b-1 solid var(--tab-panel-outline-color); } diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index 95b73d0191..543eede969 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -185,7 +185,7 @@ :on-click set-section} (tr "auth.login-here")]] [:div {:class (stl/css :links)} [:hr {:class (stl/css :separator)}] - [:> register/terms-register*]]] + [:> register/terms-service-privacy-policy*]]] :register-validate [:div {:class (stl/css :form-container)} diff --git a/frontend/src/app/main/ui/viewer/login.cljs b/frontend/src/app/main/ui/viewer/login.cljs index 0371714419..6ede841b73 100644 --- a/frontend/src/app/main/ui/viewer/login.cljs +++ b/frontend/src/app/main/ui/viewer/login.cljs @@ -12,8 +12,10 @@ [app.main.store :as st] [app.main.ui.auth.login :refer [login-dialog*]] [app.main.ui.auth.recovery-request :refer [recovery-request-page*]] - [app.main.ui.auth.register :refer [register-methods* register-success-page* terms-register* register-validate-form*]] - [app.main.ui.icons :as deprecated-icon] + [app.main.ui.auth.register :refer [register-methods* register-success-page* + register-validate-form* terms-service-privacy-policy*]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) @@ -24,14 +26,15 @@ {::mf/register modal/components ::mf/register-as :login-register} [_] - (let [user-email (mf/use-state "") + (let [user-email (mf/use-state "") register-token (mf/use-state "") current-section* (mf/use-state :login) - current-section (deref current-section*) + current-section (deref current-section*) set-current-section - (mf/use-fn #(reset! current-section* %)) + (mf/use-fn + #(reset! current-section* %)) set-section (mf/use-fn @@ -41,7 +44,9 @@ (keyword))] (set-current-section section)))) - go-back-to-login (mf/use-fn #(set-current-section :login)) + go-back-to-login + (mf/use-fn + #(set-current-section :login)) main-section (or (= current-section :login) @@ -51,13 +56,16 @@ (fn [event] (dom/prevent-default event) (st/emit! (modal/hide))) + success-email-sent (fn [email] (reset! user-email email) (set-current-section :email-sent)) + success-login (fn [] (.reload js/window.location true)) + success-register (fn [data] (reset! register-token (:token data)) @@ -66,48 +74,47 @@ [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-container)} [:div {:class (stl/css :modal-header)} - [:h2 {:class (stl/css :modal-title)} (tr "labels.continue-with-penpot")] - [:button {:class (stl/css :modal-close-btn) - :title (tr "labels.close") - :on-click close} deprecated-icon/close]] + [:h2 {:class (stl/css :modal-header-title)} (tr "labels.continue-with-penpot")] + [:> icon-button* {:variant "ghost" + :class (stl/css :modal-close) + :aria-label (tr "labels.close") + :on-click close + :icon i/close}]] [:div {:class (stl/css :modal-content)} (case current-section :login - [:div {:class (stl/css :form-container)} - [:> login-dialog* - {:on-success-callback success-login - :origin :viewer}] - [:div {:class (stl/css :links)} - [:div {:class (stl/css :recovery-request)} + [:div {:class (stl/css :login-form)} + [:> login-dialog* {:on-success-callback success-login + :origin :viewer}] + [:div {:class (stl/css :login-links)} + [:div [:a {:on-click set-section - :class (stl/css :recovery-link) :data-value "recovery-request"} (tr "auth.forgot-password")]] - [:div {:class (stl/css :register)} - [:span {:class (stl/css :register-text)} + [:div + [:span (tr "auth.register") " "] [:a {:on-click set-section - :class (stl/css :register-link) :data-value "register"} (tr "auth.register-submit")]]]] :register - [:div {:class (stl/css :form-container)} + [:div {:class (stl/css :login-form)} [:> register-methods* {:on-success-callback success-register}] - [:div {:class (stl/css :links)} - [:div {:class (stl/css :account)} + [:div {:class (stl/css :login-links)} + [:div [:span (tr "auth.already-have-account") " "] [:a {:on-click set-section :data-value "login"} (tr "auth.login-here")]]]] :register-validate - [:div {:class (stl/css :form-container)} + [:div {:class (stl/css :login-form)} [:> register-validate-form* {:params {:token @register-token} :on-success-callback success-email-sent}] - [:div {:class (stl/css :links)} - [:div {:class (stl/css :register)} + [:div {:class (stl/css :login-links)} + [:div [:a {:on-click set-section :data-value "register"} (tr "labels.go-back")]]]] @@ -115,10 +122,11 @@ :recovery-request [:> recovery-request-page* {:go-back-callback go-back-to-login :on-success-callback success-email-sent}] + :email-sent - [:div {:class (stl/css :form-container)} + [:div {:class (stl/css :login-form)} [:> register-success-page* {:params {:email @user-email}}]]) (when main-section - [:div {:class (stl/css :links)} - [:> terms-register*]])]]])) + [:div {:class (stl/css :login-links)} + [:> terms-service-privacy-policy*]])]]])) diff --git a/frontend/src/app/main/ui/viewer/login.scss b/frontend/src/app/main/ui/viewer/login.scss index 11cb81d678..227d160ea6 100644 --- a/frontend/src/app/main/ui/viewer/login.scss +++ b/frontend/src/app/main/ui/viewer/login.scss @@ -4,75 +4,71 @@ // // Copyright (c) KALEIDOS INC Sucursal en España SL -@use "refactor/common-refactor.scss" as deprecated; +@use "ds/_utils.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/typography.scss" as *; +@use "ds/z-index.scss" as *; .modal-overlay { - @extend %modal-overlay-base; + display: flex; + justify-content: center; + align-items: center; + position: fixed; + inset: 0; + block-size: 100%; + inline-size: 100%; + z-index: var(--z-index-set); + background-color: var(--color-overlay-default); } .modal-container { - @extend %modal-container-base; - - width: deprecated.$s-368; + position: relative; + padding: var(--sp-xxxl); + border-radius: $br-8; + background-color: var(--color-background-primary); + border: $b-2 solid var(--color-background-quaternary); + min-inline-size: $sz-364; + min-block-size: $sz-192; + max-inline-size: $sz-512; + max-block-size: $sz-712; + inline-size: $sz-364; } .modal-header { - margin-bottom: deprecated.$s-24; + margin-block-end: var(--sp-xxl); } -.modal-title { - @include deprecated.uppercase-title-typography; +.modal-header-title { + @include use-typography("headline-small"); - color: var(--modal-title-foreground-color); + color: var(--color-foreground-primary); } -.modal-close-btn { - @extend %modal-close-btn-base; +.modal-close { + position: absolute; + inset-block-start: var(--sp-s); + inset-inline-end: var(--sp-s); } .modal-content { - @include deprecated.flex-column; - @include deprecated.body-small-typography; + @include use-typography("body-small"); - gap: deprecated.$s-24; - max-height: deprecated.$s-400; + display: flex; + flex-direction: column; + gap: var(--sp-xxl); + max-block-size: px2rem(576); overflow: hidden auto; - - form { - display: flex; - flex-direction: column; - margin-bottom: 1.5rem; - gap: 0.75rem; - } } -.form-container { +.login-form { display: flex; justify-content: center; flex-direction: column; + gap: var(--sp-m); } -.links { +.login-links { position: relative; -} - -.link-entry { - display: flex; - flex-direction: column; - gap: deprecated.$s-12; - - span { - text-align: center; - font-size: deprecated.$fs-14; - color: var(--modal-text-foreground-color); - margin-top: deprecated.$s-12; - } - - a { - @extend %button-secondary; - - height: deprecated.$s-40; - text-transform: uppercase; - font-size: deprecated.$fs-11; - } + color: var(--color-foreground-primary); } diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index e08de97b03..4824dd1f99 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -344,7 +344,7 @@ (mf/defc view-menu* {::mf/private true ::mf/wrap [mf/memo]} - [{:keys [layout toggle-flag on-close]}] + [{:keys [layout toggle-flag on-close on-close-all]}] (let [read-only? (mf/use-ctx ctx/workspace-read-only?) toggle-color-palette @@ -365,11 +365,11 @@ toggle-comments-visibility (mf/use-fn - (mf/deps on-close) + (mf/deps on-close-all) (fn [event] (dom/stop-propagation event) (st/emit! (dwcm/toggle-comments-visibility {:origin "workspace:menu"})) - (on-close)))] + (on-close-all)))] [:> dropdown-menu* {:show true :class (stl/css :base-menu :sub-menu :pos-3) @@ -1139,7 +1139,8 @@ :view [:> view-menu* {:layout layout :toggle-flag toggle-flag - :on-close close-sub-menu}] + :on-close close-sub-menu + :on-close-all close-all-menus}] :preferences [:> preferences-menu* {:layout layout diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index d66dcdc4ff..bb17de88fb 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -97,9 +97,7 @@ sitemap-height (if sitemap-collapsed? 32 height)] - [:article {:class (stl/css :layers-tab) - :style {:--height (dm/str height "px")}} - + [:article {:class (stl/css :layers-tab)} [:> sitemap* {:layout layout :height sitemap-height :collapsed sitemap-collapsed? diff --git a/frontend/src/app/main/ui/workspace/sidebar.scss b/frontend/src/app/main/ui/workspace/sidebar.scss index 66239eb9e9..ebfaa57ea3 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/sidebar.scss @@ -79,13 +79,15 @@ .layers-tab { padding-block-start: var(--sp-xs); - overflow-x: hidden; + display: flex; + flex-direction: column; + overflow: hidden; + min-block-size: 0; } .layers-tab-resize-area { background-color: var(--color-background-primary); - position: absolute; - inset-inline-start: 0; + flex: 0 0 auto; inline-size: 100%; padding: px2rem(3) 0 px2rem(1); block-size: $sz-6; @@ -120,11 +122,16 @@ .left-sidebar-content { grid-area: content; inset-inline-end: calc(-1 * var(--sp-s)); + min-block-size: 0; + overflow: hidden; } .left-sidebar-tabs { --tabs-nav-padding-inline-start: var(--sp-m); --tabs-nav-padding-inline-end: var(--sp-m); + + block-size: 100%; + min-block-size: 0; } .left-sidebar-resize-area { diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.scss b/frontend/src/app/main/ui/workspace/sidebar/layers.scss index 328c92afdb..d329a3d2fb 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.scss @@ -31,11 +31,10 @@ } .tool-window-content { - --calculated-height: calc(#{px2rem(136)} + var(--height, #{$sz-200})); - display: flex; flex-direction: column; - block-size: calc(100vh - var(--calculated-height)); + flex: 1 1 auto; + min-block-size: 0; inline-size: calc(var(--left-sidebar-width) + var(--depth) * var(--layer-indentation-size)); overflow: auto; scrollbar-gutter: stable; @@ -117,6 +116,10 @@ .layers { position: relative; + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-block-size: 0; } .replace-wrapper { diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss index 4bfbe1c95d..e8f5e46574 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss @@ -15,7 +15,8 @@ position: relative; display: flex; flex-direction: column; - flex: 1; + flex: 0 0 auto; + flex-shrink: 0; inline-size: 100%; block-size: var(--height, $sz-200); } diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.cljs b/frontend/src/app/main/ui/workspace/top_toolbar.cljs index 2590122475..86d13c8a5c 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/top_toolbar.cljs @@ -149,16 +149,26 @@ on-main-key-down (mf/use-fn + (mf/deps open) (fn [event] (cond - (kbd/space? event) + (and open (kbd/esc? event)) + (reset! open* false) + + (or (kbd/enter? event) (kbd/space? event)) + (do + (dom/prevent-default event) + (if open + (reset! open* false) + (do + (cancel-timer! close-timer*) + (reset! open* true)))) + + (kbd/down-arrow? event) (do (dom/prevent-default event) (cancel-timer! close-timer*) - (reset! open* true)) - - (and open (kbd/esc? event)) - (reset! open* false)))) + (reset! open* true))))) on-flyout-key-down (mf/use-fn @@ -218,7 +228,10 @@ :aria-expanded open :has-tooltip false :icon default-icon - :on-click on-select-tool + :on-click (fn [event] + (cancel-timer! open-timer*) + (cancel-timer! close-timer*) + (on-select-tool event)) :on-key-down on-main-key-down :data-tool (name default-tool)}] diff --git a/manage.sh b/manage.sh index be6cc078b1..efa4da7601 100755 --- a/manage.sh +++ b/manage.sh @@ -223,8 +223,10 @@ function ensure-devenv-network { # those stale values would leak into substitution. And because Docker Compose # gives shell-env precedence over --env-file, the re-injected per-instance # overrides cleanly override the defaults.env baseline. Re-injected: HOME/PATH -# (tooling), CURRENT_USER_ID/PENPOT_SOURCE_PATH (always per-call), and the -# instance-env-overrides block. +# (tooling), CURRENT_USER_ID/PENPOT_SOURCE_PATH (always per-call), the +# optional PENPOT_OPENCODE_CONFIG_DIR (set only by run-devenv's +# --opencode-config-dir within this process), and the instance-env-overrides +# block. function infra-compose { env -i HOME="$HOME" PATH="$PATH" PWD="$PWD" \ docker compose -p penpotdev-infra \ @@ -245,14 +247,26 @@ function instance-compose { # Per-instance overrides apply to all workspaces uniformly. mapfile -t overrides < <(instance-env-overrides "$instance") + # Optional personal-opencode-config overlay: the extra -f and variable + # are only present when run-devenv --opencode-config-dir resolved a host + # directory into PENPOT_OPENCODE_CONFIG_DIR; when unset neither the file + # nor the variable is referenced, so default behaviour is unchanged. + local -a compose_files=(-f docker/devenv/docker-compose.main.yml) + local -a opencode_env=() + if [[ -n "${PENPOT_OPENCODE_CONFIG_DIR:-}" ]]; then + compose_files+=(-f docker/devenv/docker-compose.opencode.yml) + opencode_env=("PENPOT_OPENCODE_CONFIG_DIR=${PENPOT_OPENCODE_CONFIG_DIR}") + fi + env -i HOME="$HOME" PATH="$PATH" PWD="$PWD" \ CURRENT_USER_ID="${CURRENT_USER_ID:-$(id -u)}" \ PENPOT_SOURCE_PATH="$source_path" \ DEVENV_TAG="$DEVENV_TAG" \ + "${opencode_env[@]}" \ "${overrides[@]}" \ docker compose -p "penpotdev-${instance}" \ --env-file "$DEVENV_DEFAULTS_FILE" \ - -f docker/devenv/docker-compose.main.yml \ + "${compose_files[@]}" \ "$@" } @@ -658,6 +672,26 @@ function parse-ws-integer { echo "ws$raw" } +# Strict parser for --opencode-config-dir. Resolves the value to an absolute +# host directory (docker compose resolves relative bind-mount sources against +# the compose file's directory, not $PWD, so relative values would be +# misinterpreted) and verifies it exists. Echoes the absolute path; anything +# else fails fast. +function parse-opencode-config-dir { + local raw="$1" + if [[ -z "$raw" ]]; then + echo "Invalid --opencode-config-dir: value is empty." >&2 + return 1 + fi + raw="${raw/#\~/$HOME}" + local abs + if ! abs=$(realpath -e "$raw" 2>/dev/null) || [[ ! -d "$abs" ]]; then + echo "Invalid --opencode-config-dir: '$raw' is not an existing directory." >&2 + return 1 + fi + echo "$abs" +} + # Bring a single instance up: compose up + detached tmux start. When agentic # is true (the default) the tmux session gets MCP + Serena enabled; when false @@ -764,6 +798,7 @@ function run-devenv { local serena_context="desktop-app" local git_user_name="" local git_user_email="" + local opencode_config_dir="" local -a extra_env_args=() while [[ $# -gt 0 ]]; do @@ -778,6 +813,8 @@ function run-devenv { do_attach=true; shift;; --serena-context) serena_context="$2"; shift 2;; + --opencode-config-dir) + opencode_config_dir="$(parse-opencode-config-dir "$2")" || return 1; shift 2;; --git-user-name) git_user_name="$2"; shift 2;; --git-user-email) @@ -787,13 +824,16 @@ function run-devenv { -e*) extra_env_args+=(-e "${1#-e}"); shift;; -h|--help) - echo "Usage: run-devenv [--ws N] [--sync] [--attach] [--agentic] [--serena-context CTX] [--git-user-name NAME] [--git-user-email EMAIL] [-e KEY=VAL]" + echo "Usage: run-devenv [--ws N] [--sync] [--attach] [--agentic] [--serena-context CTX] [--opencode-config-dir DIR] [--git-user-name NAME] [--git-user-email EMAIL] [-e KEY=VAL]" echo " Bring a single workspace up." echo " --ws N target workspace (default: 0)." echo " --sync re-seed the wsN clone from the live repo (forbidden on ws0)." echo " --attach attach to the tmux session after startup." echo " --agentic enable MCP + Serena (AI-agent mode)." echo " --serena-context CTX context passed to Serena (default: desktop-app)." + echo " --opencode-config-dir DIR bind-mount DIR at ~/.config/opencode inside the" + echo " container (personal agents/prompts/skills kept in a" + echo " separate repo). Applied at container creation." echo " --git-user-name NAME git author name inside the container (default: host git config)." echo " --git-user-email EMAIL git author email inside the container." echo " -e KEY=VAL forward env var to docker exec on attach." @@ -863,6 +903,14 @@ function run-devenv { write-instance-mcp-configs "$target" fi + # Scope the personal opencode config to this process only: instance-compose + # includes the overlay compose file and the variable just when it is set, + # so instances brought up without the flag mount nothing. + if [[ -n "$opencode_config_dir" ]]; then + echo "[$target] mounting personal opencode config: $opencode_config_dir -> ~/.config/opencode" + export PENPOT_OPENCODE_CONFIG_DIR="$opencode_config_dir" + fi + echo "Starting $target..." start-instance "$target" "$serena_context" "$git_user_name" "$git_user_email" "$agentic" print-instance-info "$target" @@ -1338,6 +1386,11 @@ function usage { echo " --attach attach to the tmux session after startup." echo " --agentic enable MCP + Serena (AI-agent mode)." echo " --serena-context CTX passed to Serena (default: desktop-app)." + echo " --opencode-config-dir DIR" + echo " bind-mount DIR over the container's" + echo " ~/.config/opencode (personal opencode" + echo " agents/prompts/skills kept outside this" + echo " repo; applied at container creation)." echo " -e KEY=VAL forwarded to 'docker exec' on attach." echo " --git-user-name NAME / --git-user-email EMAIL" echo " identity wired into the container's git config"