mirror of
https://github.com/penpot/penpot.git
synced 2026-07-04 05:15:08 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
a4ba5fc2e0
3
.gitignore
vendored
3
.gitignore
vendored
@ -13,6 +13,8 @@
|
||||
.nyc_output
|
||||
.rebel_readline_history
|
||||
.repl
|
||||
opencode.json
|
||||
.opencode/package-lock.json
|
||||
/*.jpg
|
||||
/*.md
|
||||
!CHANGES.md
|
||||
@ -30,7 +32,6 @@
|
||||
/.clj-kondo/.cache
|
||||
/_dump
|
||||
/notes
|
||||
/.opencode/package-lock.json
|
||||
/plans
|
||||
/prompts
|
||||
/playground/
|
||||
|
||||
@ -1,28 +1,145 @@
|
||||
---
|
||||
name: commiter
|
||||
description: Git commit assistant
|
||||
mode: all
|
||||
mode: subagent
|
||||
permission:
|
||||
read: allow
|
||||
glob: allow
|
||||
grep: allow
|
||||
list: allow
|
||||
edit: deny
|
||||
webfetch: deny
|
||||
websearch: deny
|
||||
task: deny
|
||||
skill: deny
|
||||
lsp: deny
|
||||
todowrite: deny
|
||||
question: allow
|
||||
external_directory: deny
|
||||
bash:
|
||||
# Broad read-side: any non-write git query
|
||||
"git status*": allow
|
||||
"git log*": allow
|
||||
"git diff*": allow
|
||||
"git show*": allow
|
||||
"git rev-parse*": allow
|
||||
"git branch*": allow
|
||||
"git remote -v*": allow
|
||||
"git config --get*": allow
|
||||
|
||||
# Commit flow: staged, explicit paths only. `git commit*` (no space)
|
||||
# also covers `git commit --amend`. `git add -*` overrides the allow
|
||||
# below to block flag-driven bulk adds (`-A`, `--all`, `-u`, ...).
|
||||
"git add *": allow
|
||||
"git commit*": allow
|
||||
"git add -*": deny
|
||||
|
||||
# Read-only filesystem helpers used in the commit flow
|
||||
"cat *": allow
|
||||
"head *": allow
|
||||
"tail *": allow
|
||||
"wc *": allow
|
||||
"date *": allow
|
||||
|
||||
# Dangerous: deny outright
|
||||
"rm *": deny
|
||||
"rmdir *": deny
|
||||
"mv *": deny
|
||||
"cp *": deny
|
||||
"dd *": deny
|
||||
"chmod *": deny
|
||||
"chown *": deny
|
||||
"sudo *": deny
|
||||
"git push*": deny
|
||||
"git clean*": deny
|
||||
"git reset*": deny
|
||||
"git checkout*": deny
|
||||
"git restore*": deny
|
||||
"git config --global*": deny
|
||||
"curl *": deny
|
||||
"wget *": deny
|
||||
"ssh *": deny
|
||||
"scp *": deny
|
||||
"eval *": deny
|
||||
|
||||
# Risky-but-sometimes-needed: ask the user
|
||||
"git stash*": ask
|
||||
"git rebase*": ask
|
||||
"git merge*": ask
|
||||
"git tag*": ask
|
||||
"git fetch*": ask
|
||||
"git pull*": ask
|
||||
# Note: `git config <anything-other-than-(--get|--global)>` falls
|
||||
# through to the `*` catch-all below and is asked.
|
||||
|
||||
# Safety net
|
||||
"*": ask
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
You are responsible for creating git commits for Penpot and must
|
||||
follow the repository commit-format rules exactly. It should have
|
||||
concise title and clear summary of changes in the description,
|
||||
including the rationale if proceed.
|
||||
You are the Penpot commit assistant. You produce git commits that follow the
|
||||
repository's commit conventions exactly: an emoji-prefixed imperative
|
||||
subject, a body that explains the why, and the required trailers. You do
|
||||
not implement features, review code, or push branches — you commit.
|
||||
|
||||
## Requirements
|
||||
## Required Reading
|
||||
|
||||
* Override your internal commit rules when the user explicitly requests
|
||||
something that conflicts with them.
|
||||
* Read `.serena/memories/workflows/creating-commits.md` before
|
||||
creating any commit and follow the commit guidelines strictly.
|
||||
* Keep the description (commit body) with maximum line length of 80
|
||||
characters. Use manual line breaks to wrap text before it exceeds
|
||||
this limit.
|
||||
* Use `git commit -s` so the commit includes the required
|
||||
`Signed-off-by` line.
|
||||
* Do not guess or hallucinate git author information (Name or
|
||||
Email). Never include the `--author` flag in git commands unless
|
||||
specifically instructed by the user for a unique case; assume the
|
||||
local environment is already configured.
|
||||
Before drafting any commit, read `.serena/memories/workflow/creating-commits.md`
|
||||
end-to-end. It is the canonical source for the emoji menu, subject/body
|
||||
limits, and trailer format. The summary in this file does not replace it.
|
||||
|
||||
## Pre-commit Workflow
|
||||
|
||||
1. Run `git status` to inspect the working tree. If there are unstaged or
|
||||
untracked changes that are unrelated to the user's request, STOP and ask
|
||||
the user how to handle them. Do not silently include unrelated work in
|
||||
the commit.
|
||||
2. Run `git diff --staged` (or `git diff` for unstaged changes) and review
|
||||
the content. If you see secrets (API keys, tokens, passwords, private
|
||||
keys, `.env` values), debug prints, or anything that does not match the
|
||||
user's stated intent, STOP and tell the user before committing.
|
||||
3. Pick the commit emoji from the menu in
|
||||
`mem:workflow/creating-commits`. If none of the listed emojis fit, use
|
||||
`:paperclip:` (other) and explain in the body why.
|
||||
4. Draft the commit message (see format below), then run
|
||||
`git commit -s -m "<subject>" -m "<body>"` (or pass the message via
|
||||
`git commit -s -F -` if the body has unusual characters).
|
||||
|
||||
## Commit Message Format
|
||||
|
||||
```
|
||||
:emoji: Subject line (imperative, capitalized, no period, <=70 chars)
|
||||
|
||||
Body explaining what changed and why. Wrap at 80 chars. Use manual
|
||||
line breaks; do not rely on the terminal to wrap.
|
||||
|
||||
Co-authored-by: <model-name> <model-name@penpot.app>
|
||||
```
|
||||
|
||||
- Subject: imperative mood, capitalized, no trailing period, max 70 chars.
|
||||
- Body: wraps at 80 chars. Explain the *why*, not just the *what* — what
|
||||
was wrong before, what this change does about it, and any non-obvious
|
||||
trade-offs.
|
||||
- `Co-authored-by` trailer is mandatory. Replace `<model-name>` with your
|
||||
own model identifier (e.g. `claude-sonnet-4-6`).
|
||||
- `Signed-off-by` is added automatically by `git commit -s`, using the
|
||||
local `git config user.name` / `user.email`.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do not push. Pushing is a separate workflow handled by the user (the
|
||||
agent's permission set also denies `git push*`).
|
||||
- Do not run `git reset*`, `git checkout*`, `git restore*`, `git clean*`,
|
||||
or `rm*` — the permission set denies these outright. If staged work
|
||||
needs to be discarded, ask the user to do it.
|
||||
- Do not pass `--author`. Author identity comes from the local git
|
||||
config. Never guess or hallucinate a name or email.
|
||||
- Do not amend a commit you did not create in this session, unless the
|
||||
user explicitly asks. `git commit --amend` rewrites history and is
|
||||
irreversible once pushed.
|
||||
- Do not bypass pre-commit hooks (`--no-verify`) unless the user
|
||||
explicitly asks, and call out the deviation in your response.
|
||||
- If the user asks for something that conflicts with these rules, follow
|
||||
the user's request and explain the deviation in your response. Do not
|
||||
silently override the format.
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
---
|
||||
name: Engineer
|
||||
description: Senior Full-Stack Software Engineer
|
||||
mode: primary
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
You are a high-autonomy Senior Full-Stack Software Engineer working on Penpot, an
|
||||
open-source design tool. 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.
|
||||
|
||||
## Before Start
|
||||
|
||||
**Read `AGENTS.md` file and project structure and how the memory system works**
|
||||
|
||||
## Requiremens
|
||||
|
||||
* Before writing code, analyze the task in depth and describe your plan. If the task is
|
||||
complex, break it down into atomic steps.
|
||||
* Do **not** touch unrelated modules unless the task explicitly requires it.
|
||||
* Only reference functions, namespaces, or APIs that actually exist in the
|
||||
codebase. Verify their existence before citing them. If unsure, search first.
|
||||
* Be concise and autonomous — avoid unnecessary explanations.
|
||||
@ -1,60 +0,0 @@
|
||||
---
|
||||
name: Planner
|
||||
description: Software architect for planning and analysis only
|
||||
mode: primary
|
||||
permission:
|
||||
edit: ask
|
||||
---
|
||||
|
||||
## 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,
|
||||
modify any code.
|
||||
|
||||
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, testing,
|
||||
docs they might need to check, how to test it. Give them the whole plan as
|
||||
bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
|
||||
|
||||
Do **not** suggest commit messages or commit names anywhere in your plans or
|
||||
responses — committing is the developer's responsibility.
|
||||
|
||||
Assume they are a skilled developer, but know almost nothing about our toolset
|
||||
or problem domain. Assume they don't know good test design very well.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Analyze the codebase architecture and identify affected modules.
|
||||
* Read `AGENTS.md` file and project structure and how the memory system works and how to
|
||||
navigate and read relevant information conventions.
|
||||
* Break down complex features or bugs into atomic, actionable steps.
|
||||
* Propose solutions with clear rationale, trade-offs, and sequencing.
|
||||
* Identify risks, edge cases, and testing considerations.
|
||||
|
||||
Save plans to: plans/YYYY-MM-DD-<plan-one-line-title>.md
|
||||
|
||||
## Constraints
|
||||
|
||||
* You are **read-only** — never create, edit, or delete files.
|
||||
* You do **not** run builds, tests, linters, or any commands that modify state.
|
||||
* You do **not** create git commits or interact with version control.
|
||||
* You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
|
||||
`find`, `cat`).
|
||||
* Your output is a structured plan or analysis, ready for handoff to an
|
||||
engineer agent or developer.
|
||||
|
||||
## Output format
|
||||
|
||||
When producing a plan, structure it as:
|
||||
|
||||
1. **Context** — What is the problem or feature request?
|
||||
2. **Affected modules** — Which parts of the codebase are involved?
|
||||
3. **Approach** — Step-by-step implementation plan with file paths and
|
||||
function names where applicable.
|
||||
4. **Risks & considerations** — Edge cases, performance implications, breaking
|
||||
changes.
|
||||
5. **Testing strategy** — How to verify the implementation works correctly.
|
||||
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
---
|
||||
name: Prompt Assistant
|
||||
description: Refines and improves prompts for maximum clarity and effectiveness
|
||||
mode: all
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
You are an expert Prompt Engineer with strong knowledge of
|
||||
penpot. Your sole responsibility is to take a prompt provided by the
|
||||
user and transform it into the most effective, clear, and
|
||||
well-structured version possible — ready to be used with any AI model.
|
||||
|
||||
## Requirements
|
||||
|
||||
* You do NOT execute tasks. You do NOT write code. You only design and refine prompts
|
||||
* Read `AGENTS.md` file and project structure and how the memory system works and how to
|
||||
navigate and read relevant information conventions.
|
||||
* Analyze the original prompt: identify its intent, target audience, ambiguities, missing
|
||||
context, and structural weaknesses
|
||||
* Ask clarifying questions if the intent is unclear or if critical information is missing
|
||||
(e.g. target model, expected output format, tone, constraints). Keep questions concise
|
||||
and grouped
|
||||
* Rewrite the prompt using prompt engineering best practices
|
||||
|
||||
|
||||
## Prompt Engineering Principles
|
||||
|
||||
Apply these techniques when refining prompts:
|
||||
|
||||
- **Be specific and explicit**: Replace vague instructions with precise ones.
|
||||
- **Set the context**: Include background information the model needs to
|
||||
perform well.
|
||||
- **Specify the output format**: State the desired structure, length, tone,
|
||||
or format (e.g. bullet list, JSON, step-by-step).
|
||||
- **Add constraints**: Include what the model should avoid or not do.
|
||||
- **Use examples** (few-shot): When applicable, suggest adding examples to
|
||||
anchor the model's behaviour.
|
||||
- **Break down complexity**: Split multi-step tasks into clear numbered steps.
|
||||
- **Avoid ambiguity**: Remove pronouns and references that could be
|
||||
misinterpreted.
|
||||
- **Chain of thought**: For reasoning tasks, include "Think step by step."
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do NOT execute the prompt yourself.
|
||||
- Do NOT answer the question inside the prompt.
|
||||
- Do NOT add unnecessary verbosity — prompts should be as short as they can
|
||||
be while remaining complete.
|
||||
- Always preserve the user's original intent.
|
||||
|
||||
## Output
|
||||
|
||||
Refined Prompt: The improved, ready-to-use prompt. Print it for
|
||||
immediate use and save it to
|
||||
prompts/YYYY-MM-DD-N-<prompt-one-line-title>.md for future use.
|
||||
135
.opencode/skills/planner/SKILL.md
Normal file
135
.opencode/skills/planner/SKILL.md
Normal file
@ -0,0 +1,135 @@
|
||||
---
|
||||
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 plans/YYYY-MM-DD-<title>.md only when the calling agent has write permission.
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
## When to Use
|
||||
|
||||
- The user asks for a plan, design, or analysis of a feature or bug.
|
||||
- The user wants to understand which parts of the codebase a task will touch.
|
||||
- The user needs a step-by-step implementation plan with file paths, function
|
||||
names, and test strategy.
|
||||
- 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 (DRY, YAGNI, TDD, frequent commits).
|
||||
|
||||
Do **not** use this skill to actually implement anything — it is read-only.
|
||||
|
||||
## 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 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, tests,
|
||||
docs they might need to check, and how to verify it. Give them the whole plan
|
||||
as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
|
||||
|
||||
Assume the implementer is a skilled developer, but knows almost nothing about
|
||||
our toolset or problem domain. Assume they don't know good test design very
|
||||
well.
|
||||
|
||||
Do **not** suggest commit messages or commit names anywhere in your plans or
|
||||
responses — committing is the developer's responsibility.
|
||||
|
||||
## Required Reading Before Planning
|
||||
|
||||
Before drafting any plan, work through the project's own guidance:
|
||||
|
||||
1. Read `AGENTS.md` (root) for the project-level rules.
|
||||
2. Read `.serena/memories/critical-info.md` (or the equivalent entry point) to
|
||||
identify which modules are affected.
|
||||
3. Read each affected module's core memory, e.g. `mem:frontend/core`,
|
||||
`mem:backend/core`, `mem:common/core`, `mem:exporter/core`,
|
||||
`mem:render-wasm/core`. Follow `mem:` references deeper as needed.
|
||||
4. For frontend/backend work, check the relevant section's notes on lint,
|
||||
format, and test commands so the plan can include them.
|
||||
|
||||
Skipping this step is the #1 cause of incorrect or incomplete plans.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Analyze the codebase architecture and identify affected modules.
|
||||
- Read `AGENTS.md` and the memory system conventions before drafting.
|
||||
- Break down complex features or bugs into atomic, actionable steps.
|
||||
- Propose solutions with clear rationale, trade-offs, and sequencing.
|
||||
- Identify risks, edge cases, performance implications, and breaking changes.
|
||||
- Apply DRY and KISS principles to the proposed implementation.
|
||||
- Define a testing strategy aligned with each affected module's tooling.
|
||||
|
||||
## Constraints
|
||||
|
||||
- You are **analysis-only** — never create, edit, or delete source code.
|
||||
- The only file write you may attempt is the plan itself, and only when the
|
||||
calling agent has write permission (see "Plan Output"). If the write is
|
||||
denied, deliver the plan in the response and move on.
|
||||
- You do **not** run builds, tests, linters, or any commands that modify state.
|
||||
- You do **not** create git commits or interact with version control.
|
||||
- You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
|
||||
`find`, `cat`, `bat`).
|
||||
- Your output is a structured plan or analysis, ready for handoff to an
|
||||
engineer agent or developer.
|
||||
|
||||
## Plan Output
|
||||
|
||||
The plan is always delivered in the response so the user sees it regardless
|
||||
of which agent is running the skill.
|
||||
|
||||
Persistence is a **separate, best-effort step** that only runs when the
|
||||
calling agent has `edit` write permission:
|
||||
|
||||
- **Has write permission** (e.g. `build`, `general`, `engineer`): in addition
|
||||
to the in-response plan, save the plan to:
|
||||
|
||||
```
|
||||
plans/YYYY-MM-DD-<plan-one-line-title>.md
|
||||
```
|
||||
|
||||
Use today's date in the user's local timezone. The `<plan-one-line-title>`
|
||||
slug is lowercase, hyphen-separated, and a short summary of the task
|
||||
(e.g. `add-batch-get-profiles-for-file-comments`). Create the `plans/`
|
||||
directory if it does not exist.
|
||||
|
||||
- **No write permission** (e.g. the built-in `plan` agent, which denies
|
||||
`edit`): do not attempt to write the file — the write tool will be
|
||||
rejected. Just deliver the plan in the response. The user can copy it into
|
||||
`plans/...` manually if they want it persisted.
|
||||
|
||||
If the user explicitly provides a target file path, use that path instead of
|
||||
the default `plans/YYYY-MM-DD-<slug>.md` (still subject to write permission).
|
||||
|
||||
How to detect write permission: try the write. If it is denied, treat the
|
||||
plan as response-only and proceed — do not retry, do not ask the user, and do
|
||||
not mention the failed write in the response.
|
||||
|
||||
## Output Format
|
||||
|
||||
Structure the plan as:
|
||||
|
||||
1. **Context** — What is the problem or feature request? Why is it needed?
|
||||
2. **Affected modules** — Which parts of the codebase are involved? Reference
|
||||
module paths and any `mem:` memories that were consulted.
|
||||
3. **Approach** — Step-by-step implementation plan with file paths, function
|
||||
names, and code shape where applicable. Group steps into atomic, ordered
|
||||
tasks.
|
||||
4. **Risks & considerations** — Edge cases, performance implications,
|
||||
breaking changes, migration concerns, security implications.
|
||||
5. **Testing strategy** — How to verify the implementation works correctly:
|
||||
which test commands to run per module, what cases to cover, manual
|
||||
verification steps, lint/format checks.
|
||||
|
||||
Each step in **Approach** should be small enough to be reviewed and committed
|
||||
independently. Cite exact file paths (`path/to/file.ext:line` when useful) so
|
||||
the implementer can navigate directly.
|
||||
|
||||
When the plan is purely analytical (e.g. a code review or feasibility study
|
||||
with no implementation), skip the **Approach** section and lead with
|
||||
**Findings** instead, keeping the rest of the structure.
|
||||
114
.opencode/skills/refine-prompt/SKILL.md
Normal file
114
.opencode/skills/refine-prompt/SKILL.md
Normal file
@ -0,0 +1,114 @@
|
||||
---
|
||||
name: refine-prompt
|
||||
description: Refine and improve a user-supplied prompt for maximum clarity and effectiveness using prompt-engineering best practices and Penpot project context. Outputs a rewritten prompt (and brief rationale); never executes the prompt.
|
||||
---
|
||||
|
||||
# Refine Prompt
|
||||
|
||||
Expert prompt-engineering pass on a user-supplied prompt. Takes a draft prompt
|
||||
and returns a clearer, more effective, well-structured version — ready to be
|
||||
used with any AI model. Never executes the prompt itself.
|
||||
|
||||
## When to Use
|
||||
|
||||
- The user shares a prompt and asks to improve, refine, polish, or rewrite it.
|
||||
- The user asks "make this prompt better" or "can you clean this up?".
|
||||
- The user wants to add structure, constraints, examples, or output format to
|
||||
a vague prompt.
|
||||
- The user wants a prompt adapted for a specific target model, audience, or
|
||||
task type.
|
||||
|
||||
Do **not** use this skill to actually answer the prompt or do the task — it
|
||||
only rewrites the prompt.
|
||||
|
||||
## Role
|
||||
|
||||
You are an expert Prompt Engineer with strong knowledge of Penpot. Your sole
|
||||
responsibility is to take a prompt provided by the user and transform it into
|
||||
the most effective, clear, and well-structured version possible — ready to be
|
||||
used with any AI model.
|
||||
|
||||
You do **not** execute tasks. You do **not** write code. You only design and
|
||||
refine prompts.
|
||||
|
||||
## Required Reading Before Refining
|
||||
|
||||
Before rewriting, internalize the project context the prompt will likely run
|
||||
against:
|
||||
|
||||
1. Read `AGENTS.md` (root) for the project-level rules and conventions.
|
||||
2. Read `.serena/memories/critical-info.md` (or the equivalent entry point) to
|
||||
understand the module layout (`frontend`, `backend`, `common`,
|
||||
`render-wasm`, `exporter`, `mcp`, `plugins`, `library`).
|
||||
3. Skim the relevant module's core memory (`mem:frontend/core`,
|
||||
`mem:backend/core`, etc.) when the prompt targets a specific module — this
|
||||
lets you inject precise vocabulary, file conventions, and test commands
|
||||
into the refined prompt.
|
||||
|
||||
This step matters most when the user is preparing a prompt *about* the
|
||||
Penpot codebase. For generic prompts, focus on prompt-engineering principles
|
||||
and only weave in Penpot context when it is clearly relevant.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Analyze the original prompt: identify its intent, target audience,
|
||||
ambiguities, missing context, and structural weaknesses.
|
||||
- Ask clarifying questions if the intent is unclear or if critical information
|
||||
is missing (e.g. target model, expected output format, tone, constraints).
|
||||
Keep questions concise and grouped. Prefer to ask 1–4 questions at once
|
||||
rather than one at a time.
|
||||
- Rewrite the prompt using prompt-engineering best practices (see below).
|
||||
- Preserve the user's original intent — do not change the underlying task.
|
||||
- When the user provides Penpot project context, weave in the relevant
|
||||
conventions, module paths, and tooling.
|
||||
|
||||
## Prompt Engineering Principles
|
||||
|
||||
Apply these techniques when refining prompts:
|
||||
|
||||
- **Be specific and explicit**: Replace vague instructions with precise ones.
|
||||
- **Set the context**: Include background information the model needs to
|
||||
perform well.
|
||||
- **Specify the output format**: State the desired structure, length, tone,
|
||||
or format (e.g. bullet list, JSON, step-by-step).
|
||||
- **Add constraints**: Include what the model should avoid or not do.
|
||||
- **Use examples** (few-shot): When applicable, suggest adding examples to
|
||||
anchor the model's behaviour.
|
||||
- **Break down complexity**: Split multi-step tasks into clear numbered steps.
|
||||
- **Avoid ambiguity**: Remove pronouns and references that could be
|
||||
misinterpreted.
|
||||
- **Chain of thought**: For reasoning tasks, include "Think step by step."
|
||||
- **Role framing**: When helpful, give the model a clear role
|
||||
("You are a senior backend engineer...").
|
||||
- **Tool awareness**: When the prompt targets an agentic model, mention
|
||||
relevant tools (`grep`, `glob`, `read`, `bash`, etc.) so the model uses the
|
||||
right surface.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do **not** execute the prompt yourself.
|
||||
- Do **not** answer the question inside the prompt.
|
||||
- Do **not** add unnecessary verbosity — prompts should be as short as they
|
||||
can be while remaining complete.
|
||||
- Always preserve the user's original intent.
|
||||
- If the user provides Penpot project context, prefer Penpot-specific
|
||||
vocabulary over generic terms (e.g. name actual modules and `mem:`
|
||||
references instead of "the codebase").
|
||||
|
||||
## Output Format
|
||||
|
||||
Deliver the result in the response as two clearly separated blocks:
|
||||
|
||||
1. **Refined prompt** — a single fenced code block (markdown ```) containing
|
||||
the rewritten prompt, ready to copy and use.
|
||||
2. **What changed (brief)** — a short bulleted list of the most important
|
||||
changes you made and why (3–7 bullets max). Skip the rationale if the
|
||||
changes are trivial.
|
||||
|
||||
If you asked clarifying questions, list them in a separate **Clarifying
|
||||
questions** section above the refined prompt and stop — do not produce a
|
||||
refined prompt until the user answers. If the user explicitly told you to
|
||||
proceed without questions (e.g. "just rewrite it"), make reasonable
|
||||
assumptions and note them under **Assumptions made** in the rationale block.
|
||||
|
||||
No file persistence — the refined prompt lives entirely in the response.
|
||||
@ -60,6 +60,33 @@ RUN set -eux; \
|
||||
corepack enable; \
|
||||
rm -rf /tmp/nodejs.tar.gz;
|
||||
|
||||
################################################################################
|
||||
## OPENCODE SETUP
|
||||
################################################################################
|
||||
|
||||
FROM base AS setup-opencode
|
||||
|
||||
ENV OPENCODE_VERSION=1.17.13
|
||||
|
||||
RUN set -ex; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
case "${ARCH}" in \
|
||||
aarch64|arm64) \
|
||||
BINARY_URL="https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-arm64.tar.gz"; \
|
||||
;; \
|
||||
amd64|x86_64) \
|
||||
BINARY_URL="https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz"; \
|
||||
;; \
|
||||
*) \
|
||||
echo "Unsupported arch: ${ARCH}"; exit 1; \
|
||||
;; \
|
||||
esac; \
|
||||
curl -fsSL ${BINARY_URL} -o /tmp/opencode.tar.gz; \
|
||||
mkdir -p /tmp/opencode; \
|
||||
tar -xzf /tmp/opencode.tar.gz -C /tmp/opencode; \
|
||||
chmod +x /tmp/opencode/opencode; \
|
||||
rm -f /tmp/opencode.tar.gz;
|
||||
|
||||
|
||||
################################################################################
|
||||
## CADDYSERVER SETUP
|
||||
@ -67,7 +94,7 @@ RUN set -eux; \
|
||||
|
||||
FROM base AS setup-caddy
|
||||
|
||||
ENV CADDY_VERSION=2.11.2
|
||||
ENV CADDY_VERSION=2.11.4
|
||||
|
||||
RUN set -eux; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
@ -99,7 +126,7 @@ RUN set -eux; \
|
||||
FROM base AS setup-jvm
|
||||
|
||||
# https://clojure.org/releases/tools
|
||||
ENV CLOJURE_VERSION=1.12.4.1618
|
||||
ENV CLOJURE_VERSION=1.12.5.1654
|
||||
|
||||
RUN set -eux; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
@ -181,11 +208,11 @@ RUN set -eux; \
|
||||
|
||||
FROM base AS setup-utils
|
||||
|
||||
ENV CLJKONDO_VERSION=2026.04.15 \
|
||||
BABASHKA_VERSION=1.12.208 \
|
||||
ENV CLJKONDO_VERSION=2026.05.25 \
|
||||
BABASHKA_VERSION=1.12.218 \
|
||||
CLJFMT_VERSION=0.16.4 \
|
||||
PIXI_VERSION=0.67.2 \
|
||||
GITHUB_CLI_VERSION=2.91.0 \
|
||||
GITHUB_CLI_VERSION=2.96.0 \
|
||||
UV_VERSION=0.11.9 \
|
||||
UV_TOOL_DIR=/opt/uv/tools \
|
||||
UV_TOOL_BIN_DIR=/opt/utils/bin \
|
||||
@ -471,6 +498,7 @@ COPY --from=setup-rust /opt/cargo /opt/cargo
|
||||
COPY --from=setup-rust /opt/rustup /opt/rustup
|
||||
COPY --from=setup-rust /opt/emsdk /opt/emsdk
|
||||
COPY --from=setup-caddy /usr/bin/caddy /usr/bin/caddy
|
||||
COPY --from=setup-opencode /tmp/opencode/opencode /opt/utils/bin/opencode
|
||||
|
||||
COPY files/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY files/nginx-mime.types /etc/nginx/mime.types
|
||||
|
||||
@ -23,3 +23,11 @@ alias lsf='ls -h *(.)'
|
||||
if [ -f "$HOME/.bashrc.local" ]; then
|
||||
. "$HOME/.bashrc.local"
|
||||
fi
|
||||
|
||||
# pnpm
|
||||
export PNPM_HOME="/home/penpot/.local/share/pnpm"
|
||||
case ":$PATH:" in
|
||||
*":$PNPM_HOME:"*) ;;
|
||||
*) export PATH="$PNPM_HOME:$PATH" ;;
|
||||
esac
|
||||
# pnpm end
|
||||
|
||||
@ -104,6 +104,60 @@ test("Selection size badge appears on selection and hides on deselect", async ({
|
||||
await expect(badge).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Selection size badge uses component color for component selection", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.mockGetFile("components/get-file-13267.json");
|
||||
|
||||
await workspacePage.goToWorkspace({
|
||||
fileId: "e9c84e12-dd29-80fc-8007-86d559dced7f",
|
||||
pageId: "e9c84e12-dd29-80fc-8007-86d559dced80",
|
||||
});
|
||||
|
||||
await workspacePage.clickLeafLayer("A Component");
|
||||
|
||||
const badge = page.locator(".selection-size-badge");
|
||||
await expect(badge).toBeVisible();
|
||||
await expect(badge.locator("rect")).toHaveCSS("fill", "rgb(187, 151, 216)");
|
||||
await expect(badge.locator("text")).toHaveCSS("fill", "rgb(255, 255, 255)");
|
||||
});
|
||||
|
||||
test("Selection size badge shows unrotated dimensions for rotated single selection", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.mockRPC(
|
||||
/get\-file\?/,
|
||||
"workspace/get-file-not-empty.json",
|
||||
);
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
"workspace/update-file-create-rect.json",
|
||||
);
|
||||
|
||||
await workspacePage.goToWorkspace({
|
||||
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
|
||||
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
|
||||
});
|
||||
|
||||
await workspacePage.clickLeafLayer("Rectangle");
|
||||
|
||||
const badgeText = page.locator(".selection-size-badge text");
|
||||
await expect(badgeText).toHaveText("126 x 134");
|
||||
|
||||
const rotationInput = workspacePage.rightSidebar.getByRole("textbox", {
|
||||
name: "Rotation",
|
||||
});
|
||||
await rotationInput.fill("45");
|
||||
await rotationInput.press("Enter");
|
||||
|
||||
await expect(rotationInput).toHaveValue("45");
|
||||
await expect(badgeText).toHaveText("126 x 134");
|
||||
});
|
||||
|
||||
test("User makes a group", async ({ page }) => {
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
|
||||
@ -308,3 +308,24 @@
|
||||
normal progress becomes tagged as slow if no event received in the
|
||||
specified amount of time"
|
||||
1000)
|
||||
|
||||
;; ------------------------------------------------
|
||||
;; Typography
|
||||
;; ------------------------------------------------
|
||||
|
||||
(def ^:const font-size 11)
|
||||
|
||||
;; ------------------------------------------------
|
||||
;; Colors (CSS custom properties)
|
||||
;; ------------------------------------------------
|
||||
|
||||
(def ^:const select-color "var(--color-accent-tertiary)")
|
||||
|
||||
(def ^:const distance-color "var(--color-accent-quaternary)")
|
||||
(def ^:const distance-text-color "var(--app-white)")
|
||||
|
||||
;; ------------------------------------------------
|
||||
;; Selection rectangle & guides
|
||||
;; ------------------------------------------------
|
||||
|
||||
(def ^:const selection-rect-width 1)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
(ns app.main.ui.flex-controls.common
|
||||
(:require
|
||||
[app.main.constants :as mconst]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@ -7,10 +8,8 @@
|
||||
;; CONSTANTS
|
||||
;; ------------------------------------------------
|
||||
|
||||
(def font-size 11)
|
||||
(def distance-color "var(--color-accent-quaternary)")
|
||||
(def distance-text-color "var(--app-white)")
|
||||
(def warning-color "var(--status-color-warning-500)")
|
||||
|
||||
(def flex-display-pill-width 40)
|
||||
(def flex-display-pill-height 20)
|
||||
(def flex-display-pill-border-radius 4)
|
||||
@ -30,6 +29,6 @@
|
||||
:y (+ y (/ height 2))
|
||||
:text-anchor "middle"
|
||||
:dominant-baseline "central"
|
||||
:style {:fill distance-text-color
|
||||
:style {:fill mconst/distance-text-color
|
||||
:font-size font-size}}
|
||||
(fmt/format-number (or value 0))]])
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
[app.common.geom.shapes.points :as gpo]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.main.constants :as mconst]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
@ -109,7 +110,7 @@
|
||||
:on-pointer-down on-move-selected
|
||||
:on-context-menu on-context-menu
|
||||
|
||||
:style {:fill (if (or is-hover is-selected) fcc/distance-color "none")
|
||||
:style {:fill (if (or is-hover is-selected) mconst/distance-color "none")
|
||||
:opacity (if is-selected 0.5 0.25)}}]
|
||||
|
||||
(let [handle-width
|
||||
@ -134,7 +135,7 @@
|
||||
:on-context-menu on-context-menu
|
||||
:class (when (or is-hover is-selected)
|
||||
(if (= (:resize-axis rect-data) :x) (cur/get-dynamic "resize-ew" 0) (cur/get-dynamic "resize-ew" 90)))
|
||||
:style {:fill (if (or is-hover is-selected) fcc/distance-color "none")
|
||||
:style {:fill (if (or is-hover is-selected) mconst/distance-color "none")
|
||||
:opacity (if is-selected 0 1)}}])]))
|
||||
|
||||
(mf/defc gap-rects*
|
||||
@ -341,9 +342,9 @@
|
||||
[:& fcc/flex-display-pill
|
||||
{:height pill-height
|
||||
:width pill-width
|
||||
:font-size (/ fcc/font-size zoom)
|
||||
:font-size (/ mconst/font-size zoom)
|
||||
:border-radius (/ fcc/flex-display-pill-border-radius zoom)
|
||||
:color fcc/distance-color
|
||||
:color mconst/distance-color
|
||||
:x (:x @mouse-pos)
|
||||
:y (- (:y @mouse-pos) pill-width)
|
||||
:value @hover-value}])]))
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.main.constants :as mconst]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.features :as features]
|
||||
@ -224,7 +225,7 @@
|
||||
[:& fcc/flex-display-pill
|
||||
{:height pill-height
|
||||
:width pill-width
|
||||
:font-size (/ fcc/font-size zoom)
|
||||
:font-size (/ mconst/font-size zoom)
|
||||
:border-radius (/ fcc/flex-display-pill-border-radius zoom)
|
||||
:color fcc/warning-color
|
||||
:x (:x @mouse-pos)
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.main.constants :as mconst]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.features :as features]
|
||||
@ -115,7 +116,7 @@
|
||||
:on-pointer-move on-pointer-move
|
||||
:on-pointer-down on-move-selected
|
||||
:on-context-menu on-context-menu
|
||||
:style {:fill (if (or is-hover is-selected) fcc/distance-color "none")
|
||||
:style {:fill (if (or is-hover is-selected) mconst/distance-color "none")
|
||||
:opacity (if is-selected 0.5 0.25)}}]
|
||||
|
||||
(let [handle-width
|
||||
@ -145,7 +146,7 @@
|
||||
(cur/get-dynamic "resize-ew" 90)))
|
||||
|
||||
:style
|
||||
{:fill (if (or is-hover is-selected) fcc/distance-color "none")
|
||||
{:fill (if (or is-hover is-selected) mconst/distance-color "none")
|
||||
:opacity (if is-selected 0 1)}}])]))
|
||||
|
||||
(mf/defc padding-rects*
|
||||
@ -267,9 +268,9 @@
|
||||
[:& fcc/flex-display-pill
|
||||
{:height pill-height
|
||||
:width pill-width
|
||||
:font-size (/ fcc/font-size zoom)
|
||||
:font-size (/ mconst/font-size zoom)
|
||||
:border-radius (/ fcc/flex-display-pill-border-radius zoom)
|
||||
:color fcc/distance-color
|
||||
:color mconst/distance-color
|
||||
:x (:x @mouse-pos)
|
||||
:y (- (:y @mouse-pos) pill-width)
|
||||
:value @hover-value}])]))
|
||||
|
||||
@ -8,18 +8,10 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.constants :as mconst]
|
||||
[app.main.ui.measurements :refer [size-display* measurement*]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; ------------------------------------------------
|
||||
;; CONSTANTS
|
||||
;; ------------------------------------------------
|
||||
|
||||
(def select-color "var(--color-accent-tertiary)")
|
||||
(def selection-rect-width 1)
|
||||
(def select-guide-width 1)
|
||||
(def select-guide-dasharray 5)
|
||||
|
||||
(defn resolve-shapes
|
||||
[objects ids]
|
||||
(let [resolve-shape (d/getf objects)]
|
||||
@ -41,14 +33,14 @@
|
||||
|
||||
(mf/defc selection-rect [{:keys [selrect zoom]}]
|
||||
(let [{:keys [x y width height]} selrect
|
||||
selection-rect-width (/ selection-rect-width zoom)]
|
||||
selection-rect-width (/ mconst/selection-rect-width zoom)]
|
||||
[:g.selection-rect
|
||||
[:rect {:x x
|
||||
:y y
|
||||
:width width
|
||||
:height height
|
||||
:style {:fill "none"
|
||||
:stroke select-color
|
||||
:stroke mconst/select-color
|
||||
:stroke-width selection-rect-width}}]]))
|
||||
|
||||
(mf/defc selection-feedback
|
||||
|
||||
@ -13,7 +13,9 @@
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.constants :as mconst]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@ -21,36 +23,30 @@
|
||||
;; CONSTANTS
|
||||
;; ------------------------------------------------
|
||||
|
||||
(def font-size 11)
|
||||
(def selection-rect-width 1)
|
||||
(def ^:private ^:const size-display-color "var(--app-white)")
|
||||
(def ^:private ^:const size-display-opacity 0.7)
|
||||
(def ^:private ^:const size-display-text-color "var(--app-black)")
|
||||
(def ^:private ^:const size-display-width-min 50)
|
||||
(def ^:private ^:const size-display-width-max 75)
|
||||
(def ^:private ^:const size-display-height 16)
|
||||
|
||||
(def select-color "var(--color-accent-tertiary)")
|
||||
(def select-guide-width 1)
|
||||
(def select-guide-dasharray 5)
|
||||
|
||||
(def hover-color "var(--color-accent-quaternary)")
|
||||
|
||||
(def size-display-color "var(--app-white)")
|
||||
(def size-display-opacity 0.7)
|
||||
(def size-display-text-color "var(--app-black)")
|
||||
(def size-display-width-min 50)
|
||||
(def size-display-width-max 75)
|
||||
(def size-display-height 16)
|
||||
|
||||
(def distance-color "var(--color-accent-quaternary)")
|
||||
(def distance-text-color "var(--app-white)")
|
||||
(def distance-border-radius 2)
|
||||
(def distance-pill-width 50)
|
||||
(def distance-pill-height 16)
|
||||
(def distance-line-stroke 1)
|
||||
(def ^:private ^:const distance-border-radius 2)
|
||||
(def ^:private ^:const distance-pill-width 50)
|
||||
(def ^:private ^:const distance-pill-height 16)
|
||||
(def ^:private ^:const distance-line-stroke 1)
|
||||
|
||||
(def ^:private ^:const selection-badge-bg-color "var(--color-accent-tertiary)")
|
||||
(def ^:private ^:const selection-badge-bg-color-component "var(--color-accent-secondary)")
|
||||
(def ^:private ^:const selection-badge-height 16)
|
||||
(def ^:private ^:const selection-badge-padding-x 6)
|
||||
(def ^:private ^:const selection-badge-vertical-gap 8)
|
||||
(def ^:private ^:const selection-badge-border-radius 2)
|
||||
(def ^:private ^:const selection-badge-char-width 6.5)
|
||||
|
||||
(def ^:private ^:const select-guide-width 1)
|
||||
(def ^:private ^:const select-guide-dasharray 5)
|
||||
|
||||
(def ^:private ^:const hover-color "var(--color-accent-quaternary)")
|
||||
|
||||
;; ------------------------------------------------
|
||||
;; HELPERS
|
||||
@ -128,13 +124,13 @@
|
||||
:height rect-height
|
||||
:text-anchor "middle"
|
||||
:style {:fill size-display-text-color
|
||||
:font-size (/ font-size zoom)}}
|
||||
:font-size (/ mconst/font-size zoom)}}
|
||||
size-label]]))
|
||||
|
||||
(mf/defc distance-display-pill* [{:keys [x y zoom distance bounds]}]
|
||||
(let [distance-pill-width (/ distance-pill-width zoom)
|
||||
distance-pill-height (/ distance-pill-height zoom)
|
||||
font-size (/ font-size zoom)
|
||||
font-size (/ mconst/font-size zoom)
|
||||
text-padding (/ 3 zoom)
|
||||
distance-border-radius (/ distance-border-radius zoom)
|
||||
|
||||
@ -162,7 +158,7 @@
|
||||
:ry distance-border-radius
|
||||
:width distance-pill-width
|
||||
:height distance-pill-height
|
||||
:style {:fill distance-color}}]
|
||||
:style {:fill mconst/distance-color}}]
|
||||
|
||||
[:text {:x (+ text-x offset-x)
|
||||
:y (+ text-y offset-y)
|
||||
@ -171,13 +167,13 @@
|
||||
:text-anchor "middle"
|
||||
:width distance-pill-width
|
||||
:height distance-pill-height
|
||||
:style {:fill distance-text-color
|
||||
:style {:fill mconst/distance-text-color
|
||||
:font-size font-size}}
|
||||
(fmt/format-pixels distance)]]))
|
||||
|
||||
(mf/defc selection-rect* [{:keys [selrect zoom]}]
|
||||
(let [{:keys [x y width height]} selrect
|
||||
selection-rect-width (/ selection-rect-width zoom)]
|
||||
selection-rect-width (/ mconst/selection-rect-width zoom)]
|
||||
[:g.selection-rect
|
||||
[:rect {:x x
|
||||
:y y
|
||||
@ -187,34 +183,124 @@
|
||||
:stroke hover-color
|
||||
:stroke-width selection-rect-width}}]]))
|
||||
|
||||
(defn- get-edge-for-badge
|
||||
"For each rotation range, select the 'most horizontal' edge at the bottom,
|
||||
as seen from the user's perspective."
|
||||
[rotation]
|
||||
(let [rot (mod rotation 360)]
|
||||
(cond
|
||||
(or (< rot 45) (>= rot 315)) :bottom
|
||||
(< rot 135) :right
|
||||
(< rot 225) :top
|
||||
:else :left)))
|
||||
|
||||
(defn- get-edge-points
|
||||
"Get the points that define the selected side of a rectangle"
|
||||
[points edge]
|
||||
(let [[p0 p1 p2 p3] points]
|
||||
(case edge
|
||||
:bottom [p2 p3]
|
||||
:right [p1 p2]
|
||||
:top [p0 p1]
|
||||
:left [p3 p0])))
|
||||
|
||||
(mf/defc selection-size-badge*
|
||||
[{:keys [selrect zoom]}]
|
||||
(let [{:keys [x y width height]} selrect
|
||||
size-label (dm/str (fmt/format-number width) " x " (fmt/format-number height))
|
||||
badge-height (/ selection-badge-height zoom)
|
||||
padding-x (/ selection-badge-padding-x zoom)
|
||||
gap (/ selection-badge-vertical-gap zoom)
|
||||
radius (/ selection-badge-border-radius zoom)
|
||||
text-width (* (count size-label) (/ selection-badge-char-width zoom))
|
||||
badge-width (+ text-width (* 2 padding-x))
|
||||
center-x (+ x (/ width 2))
|
||||
badge-x (- center-x (/ badge-width 2))
|
||||
badge-y (+ y height gap)
|
||||
text-y (+ badge-y (/ badge-height 2))]
|
||||
[:g.selection-size-badge {:pointer-events "none"}
|
||||
[:rect {:x badge-x
|
||||
:y badge-y
|
||||
:width badge-width
|
||||
:height badge-height
|
||||
:rx radius
|
||||
:ry radius
|
||||
:style {:fill selection-badge-bg-color}}]
|
||||
[:text {:class (stl/css :badge-text)
|
||||
:x center-x
|
||||
:y text-y
|
||||
:text-anchor "middle"
|
||||
:dominant-baseline "middle"}
|
||||
size-label]]))
|
||||
[{:keys [zoom shapes]}]
|
||||
(let [badge-height (/ selection-badge-height zoom)
|
||||
badge-padding-x (/ selection-badge-padding-x zoom)
|
||||
badge-gap (/ selection-badge-vertical-gap zoom)
|
||||
badge-radius (/ selection-badge-border-radius zoom)
|
||||
badge-char-width (/ selection-badge-char-width zoom)
|
||||
|
||||
single-shape (and (= (count shapes) 1) (first shapes))
|
||||
|
||||
component-color? (if single-shape
|
||||
(ctk/instance-head? single-shape)
|
||||
(every? ctk/instance-head? shapes))
|
||||
badge-bg-color (if component-color?
|
||||
selection-badge-bg-color-component
|
||||
selection-badge-bg-color)
|
||||
|
||||
rotation (when single-shape (dm/get-prop single-shape :rotation))
|
||||
has-rotation? (and rotation (not (mth/almost-zero? rotation)))
|
||||
|
||||
;; Always compute the selrect from :points via shapes->rect.
|
||||
;; This gives the correct bounding box for all shape types,
|
||||
;; including component instances and shapes with transforms.
|
||||
selrect (gsh/shapes->rect shapes)
|
||||
|
||||
;; For single shapes we show the original dimensions,
|
||||
;; for multiple shapes show the bounding box
|
||||
shape-width (if single-shape
|
||||
(dm/get-prop single-shape :width)
|
||||
(:width selrect))
|
||||
shape-height (if single-shape
|
||||
(dm/get-prop single-shape :height)
|
||||
(:height selrect))
|
||||
|
||||
text (dm/str (fmt/format-number shape-width) " x " (fmt/format-number shape-height))
|
||||
|
||||
text-width (* (count text) badge-char-width)
|
||||
badge-width (+ text-width (* 2 badge-padding-x))]
|
||||
|
||||
(if has-rotation?
|
||||
(let [edge (get-edge-for-badge rotation)
|
||||
points (dm/get-prop single-shape :points)
|
||||
|
||||
[ep1 ep2] (get-edge-points points edge)
|
||||
|
||||
mid-point (gpt/lerp ep1 ep2 0.5)
|
||||
normal (gpt/normal-right (gpt/subtract ep2 ep1))
|
||||
|
||||
rot-offset (case edge
|
||||
:bottom 0
|
||||
:right 270
|
||||
:top 180
|
||||
:left 90)
|
||||
badge-rot (+ rotation rot-offset)
|
||||
offset (+ badge-gap (/ badge-height 2))
|
||||
|
||||
badge-x (- (/ badge-width 2))
|
||||
badge-y (- (/ badge-height 2))
|
||||
badge-cx (+ (:x mid-point) (* (:x normal) offset))
|
||||
badge-cy (+ (:y mid-point) (* (:y normal) offset))]
|
||||
|
||||
[:g.selection-size-badge {:pointer-events "none"
|
||||
:transform (dm/str "translate(" badge-cx "," badge-cy ") rotate(" badge-rot ")")}
|
||||
[:rect {:x badge-x
|
||||
:y badge-y
|
||||
:width badge-width
|
||||
:height badge-height
|
||||
:rx badge-radius
|
||||
:ry badge-radius
|
||||
:style {:fill badge-bg-color}}]
|
||||
[:text {:class (stl/css :badge-text)
|
||||
:x 0
|
||||
:y 0
|
||||
:text-anchor "middle"
|
||||
:dominant-baseline "middle"}
|
||||
text]])
|
||||
|
||||
(let [badge-x (- (/ badge-width 2))
|
||||
badge-y (- (/ badge-height 2))
|
||||
badge-cx (+ (:x selrect) (/ (:width selrect) 2))
|
||||
badge-cy (+ (:y selrect) (:height selrect) badge-gap (/ badge-height 2))]
|
||||
|
||||
[:g.selection-size-badge {:pointer-events "none"
|
||||
:transform (dm/str "translate(" badge-cx "," badge-cy ")")}
|
||||
[:rect {:x badge-x
|
||||
:y badge-y
|
||||
:width badge-width
|
||||
:height badge-height
|
||||
:rx badge-radius
|
||||
:ry badge-radius
|
||||
:style {:fill badge-bg-color}}]
|
||||
[:text {:class (stl/css :badge-text)
|
||||
:x 0
|
||||
:y 0
|
||||
:text-anchor "middle"
|
||||
:dominant-baseline "middle"}
|
||||
text]]))))
|
||||
|
||||
(mf/defc distance-display* [{:keys [from to zoom bounds]}]
|
||||
(let [fixed-x (if (gsh/fully-contained? from to)
|
||||
@ -245,7 +331,7 @@
|
||||
:y1 y1
|
||||
:x2 x2
|
||||
:y2 y2
|
||||
:style {:stroke distance-color
|
||||
:style {:stroke mconst/distance-color
|
||||
:stroke-width distance-line-stroke}}]
|
||||
|
||||
[:> distance-display-pill*
|
||||
@ -263,7 +349,7 @@
|
||||
:y1 y1
|
||||
:x2 x2
|
||||
:y2 y2
|
||||
:style {:stroke select-color
|
||||
:style {:stroke mconst/select-color
|
||||
:stroke-width (/ select-guide-width zoom)
|
||||
:stroke-dasharray (/ select-guide-dasharray zoom)}}])])
|
||||
|
||||
|
||||
@ -4,10 +4,9 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.badge-text {
|
||||
fill: var(--app-black);
|
||||
font-size: calc(deprecated.$fs-12 / var(--zoom));
|
||||
font-family: "worksans", "vazirmatn", sans-serif;
|
||||
fill: var(--color-static-white);
|
||||
font-size: calc(px2rem(12) / var(--zoom));
|
||||
}
|
||||
|
||||
@ -510,7 +510,7 @@
|
||||
(not edition)
|
||||
(not mode-inspect?))
|
||||
[:> msr/selection-size-badge*
|
||||
{:selrect (gsh/shapes->rect selected-shapes)
|
||||
{:shapes selected-shapes
|
||||
:zoom zoom}])
|
||||
|
||||
(when show-measures?
|
||||
|
||||
@ -781,7 +781,7 @@
|
||||
(not mode-inspect?)
|
||||
(not page-transition?))
|
||||
[:> msr/selection-size-badge*
|
||||
{:selrect (gsh/shapes->rect selected-shapes)
|
||||
{:shapes selected-shapes
|
||||
:zoom zoom}])
|
||||
|
||||
(when show-measures?
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user